├── .gitignore ├── README.md ├── boot-bash.sh ├── boot-fromx.sh ├── boot.sh ├── build-docker-from-rootfs.sh ├── build-prepare.sh ├── build-rootfs.sh ├── config ├── docker ├── .dockerignore ├── Dockerfile └── startup.sh ├── install.sh ├── recalbox.desktop ├── recalbox.service ├── service-exec.sh └── service-exit.sh /.gitignore: -------------------------------------------------------------------------------- 1 | rootfs-excluded/ 2 | docker/rootfs/ 3 | img-mount/ 4 | *.img 5 | *.img.xz 6 | *.7z 7 | *.gz 8 | *.bz 9 | *.bz2 10 | *.log 11 | *.swp 12 | *.iso 13 | *.zip 14 | *.rar 15 | *.lz 16 | /.release 17 | etc-default-*-launcher 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # recalbox-in-docker 2 | 3 | Launch and play Recalbox/kodi (rockpro64/rock64 and Debian/Ubuntu host currently) via docker from your desktop. Xorg will close but your services will continue. Shutdown computer from Kodi to return to Recalbox. Shutdown computer from Recalbox to return to Xorg. Your settings are stored in $HOME/recalbox-share/ 4 | 5 | - This pulls the latest release from https://github.com/mrfixit2001/recalbox_*/releases and builds a docker container from the rootfs, allowing you to launch recalbox (and kodi) from most any debian/ubuntu host -- as long as the kernel rockchip version matches. (4.4.171 tested, with arm64 kernel and armhf userspace) 6 | 7 | # Steps 8 | 9 | 1. cd to this directory. 10 | 2. ./install.sh (not with sudo; if a RECALBOX folder pops up during install, close/ignore it; takes 15-20 minutes to complete) 11 | 3. Launch Recalbox from desktop application launcher (Media/Games/Video) 12 | 13 | Run ./install.sh again at any time to update recalbox to the latest version. Your settings/files/add-ons will be retained in $HOME/recalbox-share/ 14 | 15 | # Service/start on boot 16 | 17 | If you want to boot directly into Recalbox: 18 | 19 | - systemctl enable recalbox.service 20 | - Notes: 21 | 22 | 1. When using the autostart service, selecting "Shutdown" in Recalbox will exit the container and start X. 23 | 2. Reboot from X or use the desktop icon to return to Recalbox. 24 | 3. You can also start that service manually at any time to stop your display-manager and switch back to Recalbox. 25 | 4. Rebooting from Kodi will do a soft-reboot of the system? 26 | 27 | # Debugging steps 28 | 29 | 1. cd to this directory. 30 | 2. ./build-prepare.sh - Install Docker and dependencies. 31 | 3. ./build-rootfs.sh - Grab and build rootfs. There will be some rsync errors/warnings - it's OK. 32 | 4. Optional: Review docker/rootfs and build-docker-from-rootfs.sh for files that will be removed. Review docker/startup.sh for how the image is booted. 33 | 5. ./build-docker-from-rootfs.sh - Build docker from docker/rootfs. 34 | 6. Launch Recalbox or /usr/local/bin/recalbox-boot-stopx.sh from desktop, or close your desktop and launch /usr/local/bin/recalbox-boot.sh from tty1. 35 | 36 | # Working/tested 37 | 38 | - ROCKPro64 (RK3399) 39 | - https://github.com/ayufan-rock64/linux-build/releases 0.8.0rc5 -- rc6 does not work -- openmediavault armhf, mate armhf, bionic LXDE arm64, minimal armhf releases tested. The container release does not work. Must be on -1161 kernel or newer. 40 | - https://github.com/mrfixit2001/debian_desktop/releases - Second Release or newer. 41 | 42 | # Should work 43 | 44 | - ROCK64 (RK3328) 45 | - https://github.com/ayufan-rock64/linux-build/releases 0.8.0rc5 -- rc6 does not work -- openmediavault armhf, mate armhf, bionic LXDE arm64, minimal armhf releases tested. The container release does not work. Must be on -1161 kernel or newer. 46 | 47 | # Other notes 48 | 49 | - If you lose audio in Kodi, try different shutdown/reboot/quit options. I think there may be an issue with multiple kodilauncher.sh running simultaneously. 50 | 51 | Regards, 52 | - Jason Fisher 53 | - jason dot fisher at gmail dot com 54 | -------------------------------------------------------------------------------- /boot-bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p $HOME/recalbox-share 3 | docker run --privileged -it -v $HOME/recalbox-share:/recalbox/share -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v /media:/media -v /mnt:/mnt -v /etc/machine-id:/etc/machine-id -v /dev:/dev recalbox-launcher/recalbox-launcher:local bash 4 | 5 | RETVAL=$? 6 | # echo "returned: $RETVAL" 7 | 8 | # 130 = container shutdown 9 | # 129 = container reboot 10 | 11 | # if [ $RETVAL -eq 130 ]; then 12 | # echo "Starting display manager.." 13 | # systemctl start display-manager.service 14 | 15 | echo "ended with $RETVAL" 16 | exit $RETVAL 17 | # any exit other than 0 or 255 should restart the service? 18 | # exit 0 19 | -------------------------------------------------------------------------------- /boot-fromx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | read -p "WARNING: The graphical server is about to be shut down! 3 | Make sure you close all other apps before proceeding. 4 | Press Enter to continue, or Ctrl+C to cancel." 5 | CURUSER=$(id -un) 6 | CURHOME=$HOME 7 | CURVT=$(sudo fgconsole) 8 | 9 | if [ $CURVT -eq 7 ] 10 | then 11 | sudo chvt 1 12 | fi 13 | 14 | sudo screen bash -c "systemctl stop display-manager ; su $CURUSER -c /usr/local/bin/recalbox-boot.sh ; systemctl start display-manager" 15 | 16 | -------------------------------------------------------------------------------- /boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p $HOME/recalbox-share 3 | docker run --privileged -it -v $HOME/recalbox-share:/recalbox/share -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v /media:/media -v /mnt:/mnt -v /etc/machine-id:/etc/machine-id -v /dev:/dev recalbox-launcher/recalbox-launcher:local 4 | 5 | RETVAL=$? 6 | # echo "returned: $RETVAL" 7 | 8 | # 130 = container shutdown 9 | # 129 = container reboot 10 | 11 | # if [ $RETVAL -eq 130 ]; then 12 | # echo "Starting display manager.." 13 | # systemctl start display-manager.service 14 | 15 | echo "ended with $RETVAL" 16 | exit $RETVAL 17 | # any exit other than 0 or 255 should restart the service? 18 | # exit 0 19 | -------------------------------------------------------------------------------- /build-docker-from-rootfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . config 4 | 5 | echo "Removing files that break things .." 6 | 7 | mkdir -p rootfs-excluded 8 | 9 | sudo mv docker/rootfs/etc/init.d/S*mountboot rootfs-excluded/ 10 | sudo mv docker/rootfs/etc/init.d/S*network rootfs-excluded/ 11 | sudo mv docker/rootfs/etc/init.d/S*connman rootfs-excluded/ 12 | sudo mv docker/rootfs/etc/init.d/S*wifi rootfs-excluded/ 13 | sudo mv docker/rootfs/etc/init.d/S11share rootfs-excluded/ 14 | sudo mv docker/rootfs/etc/init.d/S*upgrade rootfs-excluded/ 15 | sudo mv docker/rootfs/etc/init.d/S*bluetooth rootfs-excluded/ 16 | sudo mv docker/rootfs/lost+found rootfs-excluded/ 17 | sudo rm -rf docker/rootfs/dev/* 18 | 19 | sudo cp config docker/rootfs/.docker-config 20 | 21 | echo "Building docker/Dockerfile for $name from rootfs.." 22 | 23 | 24 | # sudo not needed here? 25 | sudo docker build -t $name-launcher/$name-launcher:local docker 26 | 27 | -------------------------------------------------------------------------------- /build-prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | user=$USER 3 | 4 | . config 5 | 6 | # Determine OS platform 7 | UNAME=$(uname | tr "[:upper:]" "[:lower:]") 8 | # If Linux, try to determine specific distribution 9 | if [ "$UNAME" == "linux" ]; then 10 | # If available, use LSB to identify distribution 11 | if [ -f /etc/lsb-release -o -d /etc/lsb-release.d ]; then 12 | export DISTRO=$(lsb_release -i | cut -d: -f2 | sed s/'^\t'//) 13 | # Otherwise, use release info file 14 | else 15 | export DISTRO=$(ls -d /etc/[A-Za-z]*[_-][rv]e[lr]* | grep -v "lsb" | cut -d'/' -f3 | cut -d'-' -f1 | cut -d'_' -f1) 16 | fi 17 | array=( $DISTRO ) 18 | export DISTRO=${array[0]} 19 | fi 20 | 21 | # For everything else (or if above failed), just use generic identifier 22 | [ "$DISTRO" == "" ] && export DISTRO=$UNAME 23 | [ "$DISTROID" == "" ] && export DISTROID=$(echo $DISTRO | tr "[:upper:]" "[:lower:]") 24 | 25 | # Determine arch 26 | ARCH=`dpkg --print-architecture` 27 | 28 | 29 | unset UNAME 30 | 31 | echo 32 | echo "* Build container for latest $name from $release_owner/$release" 33 | 34 | while true; do 35 | echo 36 | echo "Please review that you have all required kernel modules for docker:" 37 | echo 38 | sleep 5 39 | curl -fsSL https://raw.githubusercontent.com/moby/moby/master/contrib/check-config.sh | bash | more 40 | read -p "Do you wish to continue installing docker? [yn] " yn 41 | case $yn in 42 | [Yy]* ) break;; 43 | [Nn]* ) exit;; 44 | * ) echo "Please answer yes or no.";; 45 | esac 46 | done 47 | 48 | sudo apt install -y \ 49 | apt-transport-https \ 50 | ca-certificates \ 51 | curl \ 52 | gnupg \ 53 | gpgv \ 54 | kbd \ 55 | wget \ 56 | screen 57 | 58 | curl -fsSL https://download.docker.com/linux/${DISTROID}/gpg | sudo apt-key add - 59 | echo "deb [arch=${ARCH}] https://download.docker.com/linux/${DISTROID} \ 60 | $(lsb_release -cs) stable" | \ 61 | sudo tee /etc/apt/sources.list.d/docker.list 62 | sudo apt update 63 | sudo apt install -y docker-ce 64 | # sudo apt install -y docker.io 65 | sudo docker version 66 | sudo addgroup $user docker 67 | sudo usermod -aG docker $user 68 | 69 | sudo cp boot.sh /usr/local/bin/${name}-boot.sh 70 | sudo cp boot-fromx.sh /usr/local/bin/${name}-boot-fromx.sh 71 | sudo cp service-exit.sh /usr/local/bin/${name}-service-exit.sh 72 | sudo cp service-exec.sh /usr/local/bin/${name}-service-exec.sh 73 | sudo cp *.desktop /usr/share/applications/ 74 | sudo cp *.service /etc/systemd/system/ 75 | echo "serviceuser=$user" >etc-default-${name}-launcher 76 | echo "servicehome=$HOME" >>etc-default-${name}-launcher 77 | sudo cp etc-default-${name}-launcher /etc/default/${name}-launcher 78 | 79 | -------------------------------------------------------------------------------- /build-rootfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . config 4 | 5 | xzfilename="${url##*/}" 6 | filename=`basename ${xzfilename} .xz` 7 | 8 | if [ -e "${xzfilename}" ] 9 | then 10 | echo "Already have the image file: $xzfilename" 11 | else 12 | if [ -e "${filename}" ] 13 | then 14 | echo "$filename exists. Not downloading $url." 15 | else 16 | echo "Retrieving $url .." 17 | wget -q --show-progress --progress=bar:force:noscroll $url 18 | fi 19 | fi 20 | 21 | if [ -e "${filename}" ] 22 | then 23 | echo "Already extracted to: $filename" 24 | else 25 | echo "Extracting $filename .." 26 | xz -d "${xzfilename}" 27 | fi 28 | 29 | declare -i start_sector 30 | start_sector=$(/sbin/fdisk -l ./${filename} | awk -F" " '{ print $3 }' | tail -n1) 31 | (( start_offset = $start_sector * 512 )) 32 | 33 | mkdir -p img-mount 34 | (sudo umount img-mount || /bin/true) 35 | sudo mount -o loop,offset=$start_offset ./${filename} ./img-mount 36 | 37 | mkdir -p docker/rootfs 38 | sudo rsync -axHAX --info=progress2 img-mount/ docker/rootfs/ 39 | 40 | sudo umount ./img-mount 41 | rmdir img-mount 42 | 43 | echo $name >.release 44 | 45 | echo "$url extracted to rootfs/" 46 | 47 | # cleanup 48 | rm "${filename}" 49 | rm "${xzfilename}" 50 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | # github user/repo and app name 2 | name="recalbox" 3 | 4 | release_owner="mrfixit2001" 5 | releasename="recalbox" 6 | release="detect" 7 | #release="rockpro64" 8 | 9 | # wait for this process to start and then exit before letting the docker container die 10 | waitproc="bootexec" 11 | inport=80 12 | extport=18080 13 | 14 | # autodetection needs work! 15 | if [ $release == "detect" ]; then 16 | if (lsmod | grep "midgard_kbase" >/dev/null); then 17 | gpu=midgard 18 | chipset=rk3399 19 | release=rockpro64 20 | elif (lsmod | grep "mali" >/dev/null); then 21 | gpu=utgard 22 | chipset=rk3328 23 | release=rock64 24 | else 25 | gpu=midgard 26 | release=rockpro64 27 | fi 28 | echo "Detect: $release ($chipset/$gpu)" 29 | fi 30 | 31 | 32 | apiurl="https://api.github.com/repos/$release_owner/${releasename}_${release}/releases/latest" 33 | url=`curl -s "$apiurl" | grep -oP '"browser_download_url": "\K(.*)(?=")'` 34 | 35 | # no latest page. trying to scrape the first valid release. 36 | if [ -z "$url" ]; then 37 | apiurl="https://api.github.com/repos/$release_owner/${releasename}_${release}/releases" 38 | url=`curl -s "$apiurl" | grep -oP '"browser_download_url": "\K(.*)(?=")' | grep "${release}_" | head -n5 | sort -r -n | head -n1` 39 | fi 40 | 41 | # get specific file instead 42 | # url="https://github.com/mrfixit2001/recalbox_rockpro64/releases/download/190222/recalbox_rockpro64_190222.img.xz" 43 | 44 | echo "Release: $url" 45 | -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | # Remove folders mentioned here: 2 | # https://wiki.archlinux.org/index.php/Rsync#As_a_backup_utility 3 | /dev 4 | #/proc 5 | #/sys 6 | #/tmp 7 | #/run 8 | #/mnt 9 | #/media 10 | /lost+found 11 | 12 | # Remove database's data 13 | # /var/lib/postgresql 14 | 15 | # Remove useless heavy files like /var/lib/scrapyd/reports.old 16 | #**/*.old 17 | #**/*.log 18 | #**/*.bak 19 | 20 | # Remove docker 21 | /var/lib/lxcfs 22 | /var/lib/docker 23 | /etc/docker 24 | /root/.docker 25 | /etc/init/docker.conf 26 | 27 | # Remove the current program 28 | /.dockerignore 29 | /Dockerfile 30 | 31 | # Remove unneeded init 32 | /etc/init.d/S01mountboot 33 | /etc/init.d/S02splash 34 | #S03populate 35 | #S05udev 36 | #S06dbus 37 | /etc/init.d/S07network 38 | /etc/init.d/S08connman 39 | /etc/init.d/S09wifi 40 | #S100kintaro 41 | #S11share 42 | /etc/init.d/S11upgrade 43 | #S12populateshare 44 | #S16modprobe 45 | #S20urandom 46 | #S25hyperion 47 | #S25lircd 48 | #S26recalboxsystem 49 | #S31emulationstation 50 | #S31sixad 51 | /etc/init.d/S32bluetooth 52 | #S35securepasswd 53 | /etc/init.d/S49ntp 54 | #S50avahi-daemon 55 | /etc/init.d/S50dropbear 56 | #S91smb 57 | #S92switch 58 | #S92virtualgamepads 59 | #S94manager 60 | #S98cleanup 61 | #S98xboxone 62 | #S99custom 63 | #/etc/init.d/S99rockprofan 64 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY rootfs/ / 4 | 5 | # COPY extra-libs/ /usr/lib/ 6 | # COPY libmali-midgard-t86x-r14p0-r0p0-x11-gbm.so /usr/lib/libmali.so 7 | 8 | COPY startup.sh / 9 | 10 | VOLUME [ "/sys/fs/cgroup" ] 11 | 12 | RUN chmod 777 /startup.sh 13 | CMD ["bash","/startup.sh"] 14 | -------------------------------------------------------------------------------- /docker/startup.sh: -------------------------------------------------------------------------------- 1 | # Redirect all traffic from 127.0.0.1:5432 to 172.20.0.1:5432 2 | # so any connection to Postgresql keeps working without any other modification. 3 | # Requires the --privileged flag when creating container: 4 | 5 | #. /.docker-config 6 | 7 | sysctl -w net.ipv4.conf.all.route_localnet=1 8 | iptables -t nat -A OUTPUT -p tcp -s 127.0.0.1 --dport 18080 -j DNAT --to-destination 172.20.0.1:80 9 | iptables -t nat -A POSTROUTING -j MASQUERADE 10 | 11 | /etc/init.d/rcS 12 | 13 | # busybox wait for process to start, gnu ps needs: ps a instead 14 | until (ps | grep bootexec | grep -v grep); do sleep 2; done 15 | 16 | # Method 1 17 | # busybox wait for process to exit, gnu ps needs: ps a instead 18 | while (ps | grep bootexec | grep -v grep); do sleep 2; done 19 | 20 | # Method 2 21 | #pid=`pidof $waitproc` 22 | # Wait for pid to finish before leaving 23 | #while ps -p $pid > /dev/null; do sleep 5; done; 24 | 25 | # Method 3 26 | # Little hack to keep the container running in foreground 27 | # tail -f /dev/null 28 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . config 4 | 5 | ./build-prepare.sh 6 | ./build-rootfs.sh 7 | ./build-docker-from-rootfs.sh 8 | 9 | # cleanup 10 | sudo rm -rf ./docker/rootfs 11 | sudo rm -rf ./rootfs-excluded 12 | 13 | echo 14 | echo "Docker and $name installed. Launch $name from your Applications menu." 15 | echo 16 | 17 | echo "To launch from a tty, logout and back in and then run $name-boot.sh" 18 | echo 19 | echo "To boot directly into $name: systemctl enable $name" 20 | echo 21 | 22 | -------------------------------------------------------------------------------- /recalbox.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=Recalbox (shutdown X) 4 | Comment=Close your graphical Xorg session and launch recalbox. 5 | Exec=/usr/local/bin/recalbox-boot-fromx.sh 6 | Terminal=true 7 | X-MultipleArgs=false 8 | Type=Application 9 | Icon=recalbox 10 | Categories=Games;Media;AudioVideo;Player;Video; 11 | -------------------------------------------------------------------------------- /recalbox.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Recalbox-in-docker 3 | After=network.target docker.service 4 | Before=display-manager.service 5 | ConditionPathExists=/usr/local/bin/recalbox-service-exec.sh 6 | Conflicts=display-manager.service 7 | Conflicts=libreelec.service 8 | 9 | [Service] 10 | StandardInput=tty-force 11 | EnvironmentFile=/etc/environment 12 | EnvironmentFile=-/etc/default/recalbox-launcher 13 | ExecStart=/usr/local/bin/recalbox-service-exec.sh 14 | ExecStopPost=/usr/local/bin/recalbox-service-exit.sh 15 | KillMode=process 16 | 17 | # Uncomment this to always restart this container on shutdown 18 | Restart=on-failure 19 | 20 | # docker returns 130 on shutdown, 129 on reboot 21 | RestartPreventExitStatus=0 130 129 SIGKILL 22 | SuccessExitStatus=0 130 129 SIGKILL 23 | 24 | #type=simple 25 | #type=oneshot 26 | #TimeoutStartSec=5m 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | -------------------------------------------------------------------------------- /service-exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /etc/defaults/recalbox-launcher 4 | 5 | CURUSER=$serviceuser 6 | #$(id -un) 7 | CURHOME=$servicehome 8 | CURVT=$(sudo fgconsole) 9 | 10 | if [ $CURVT -eq 7 ] 11 | then 12 | sudo chvt 1 13 | fi 14 | 15 | #sudo screen bash -c " 16 | systemctl stop display-manager 17 | su $CURUSER -c /usr/local/bin/recalbox-boot.sh 18 | 19 | RETVAL=$? 20 | echo "docker returned $RETVAL" 21 | 22 | if [ $RETVAL -eq 0 ]; then 23 | systemctl start display-manager 24 | fi 25 | 26 | if [ $RETVAL -eq 129 ]; then 27 | echo "Reboot requested." 28 | fi 29 | 30 | exit $RETVAL 31 | 32 | -------------------------------------------------------------------------------- /service-exit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "cleaning up" 3 | exit 0 4 | --------------------------------------------------------------------------------