├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.md ├── Makefile ├── Makefile.example ├── README.md └── scripts ├── expand.sh ├── mount.sh ├── qemu-cleanup.sh ├── qemu-launch.sh ├── qemu-setup.sh ├── run.sh └── unmount.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | images/ 2 | test/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/ 2 | *.zip 3 | *.img 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | before_install: 7 | - sudo apt-get update 8 | 9 | install: 10 | - sudo apt-get install -y qemu qemu-user-static 11 | 12 | script: 13 | - make test 14 | 15 | cache: 16 | directories: 17 | - images/*.img 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | MAINTAINER Ryan Kurte 4 | LABEL Description="Qemu based emulation for raspberry pi using loopback images" 5 | 6 | # Update package repository 7 | RUN apt-get update 8 | 9 | # Install required packages 10 | RUN apt-get install -y --allow-unauthenticated \ 11 | qemu \ 12 | qemu-user-static \ 13 | binfmt-support \ 14 | parted \ 15 | vim \ 16 | sudo 17 | 18 | # Clean up after apt 19 | RUN apt-get clean 20 | RUN rm -rf /var/lib/apt 21 | 22 | # Setup working directory 23 | RUN mkdir -p /usr/rpi 24 | WORKDIR /usr/rpi 25 | 26 | # Setup home directory 27 | RUN mkdir -p /home/pi 28 | 29 | COPY scripts/* /usr/rpi/ 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Ryan Kurte 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Helper makefile to demonstrate the use of the rpi-emu docker environment 2 | # This is mostly useful for development and extension as part of an image builder 3 | # 4 | # For an example using this in a project, see Makefile.example 5 | 6 | DATE=2019-06-20 7 | 8 | DIST=$(DATE)-raspbian-buster-lite 9 | ZIP=$(DIST).zip 10 | IMAGE=$(DIST).img 11 | 12 | DL_PATH=http://director.downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-06-24/$(ZIP) 13 | CWD=$(shell pwd) 14 | 15 | # Docker arguments 16 | # Interactive mode, remove container after running, privileged mode for loopback access 17 | # Mount images to /usr/rpi/images to access image files from container 18 | # Change working directory to /usr/rpi (which is loaded with the helper scripts) 19 | RUN_ARGS=-it --rm --privileged=true -v $(CWD)/images:/usr/rpi/images -w /usr/rpi ryankurte/docker-rpi-emu 20 | MOUNT_DIR=/media/rpi 21 | 22 | # Build the docker image 23 | build: 24 | @echo "Building base docker image" 25 | @docker build -t ryankurte/docker-rpi-emu . 26 | 27 | # Bootstrap a RPI image into the images directory 28 | bootstrap: images/$(IMAGE) 29 | 30 | # Fetch the RPI image from the path above 31 | images/$(IMAGE): 32 | @echo "Pulling Raspbian image" 33 | @mkdir -p images 34 | wget -O images/$(ZIP) -c $(DL_PATH) 35 | @unzip -d images/ images/$(ZIP) 36 | @touch $@ 37 | 38 | # Expand the image by a specified size 39 | # TODO: implement expand script to detect partition sizes 40 | expand: build bootstrap 41 | dd if=/dev/zero bs=1M count=1024 >> images/$(IMAGE) 42 | @docker run $(RUN_ARGS) ./expand.sh images/$(IMAGE) 1024 43 | 44 | # Launch the docker image without running any of the utility scripts 45 | run: build bootstrap 46 | @echo "Launching interactive docker session" 47 | @docker run $(RUN_ARGS) /bin/bash 48 | 49 | # Launch the docker image into an emulated session 50 | run-emu: build bootstrap 51 | @echo "Launching interactive emulated session" 52 | @docker run $(RUN_ARGS) /bin/bash -c './run.sh images/$(IMAGE)' 53 | 54 | test: build bootstrap 55 | @echo "Running test command" 56 | @docker run $(RUN_ARGS) /bin/bash -c './run.sh images/$(IMAGE) "uname -a"' 57 | -------------------------------------------------------------------------------- /Makefile.example: -------------------------------------------------------------------------------- 1 | # Example makefile demonstrating the use of the emulated container to prepare a build environment 2 | # This makefile is standalone, and can be used as the basis of projects that build applications or images for the RPi 3 | 4 | DATE=2016-05-27 5 | 6 | DIST=$(DATE)-raspbian-jessie-lite 7 | ZIP=$(DIST).zip 8 | IMAGE=$(DIST).img 9 | 10 | PATH=http://vx2-downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2016-05-31/$(ZIP) 11 | CWD=$(shell pwd) 12 | 13 | # Docker run arguments 14 | # Container must run in privileged mode to allow binding of loopback interfaces 15 | RUN_ARGS=-it --rm --privileged=true -v $(CWD)/images:/usr/rpi/images -v $(CWD)/resources:/usr/rpi/resources -w /usr/rpi ryankurte/docker-rpi-emu 16 | 17 | # Internal container mount directory 18 | MOUNT_DIR=/media/rpi 19 | 20 | # Build the docker image 21 | pull: 22 | @docker pull ryankurte/docker-rpi-emu 23 | 24 | # Bootstrap a RPI image into the images directory 25 | bootstrap: images/$(IMAGE) 26 | 27 | # Fetch the RPI image from the path above 28 | images/$(IMAGE): 29 | @mkdir -p images 30 | @wget -O images/$(ZIP) -c $(PATH) 31 | @unzip -d images/ images/$(ZIP) 32 | @touch $@ 33 | 34 | # Launch the docker image without running any of the utility scripts 35 | run: pull bootstrap 36 | @docker run $(RUN_ARGS) /bin/bash 37 | 38 | # Launch the docker image into an emulated session 39 | run-emu: pull bootstrap 40 | @docker run $(RUN_ARGS) /bin/bash -c './run.sh images/$(IMAGE)' 41 | 42 | # Build some fake resources to copy / execute 43 | resources: resources/setup.sh 44 | resources/setup.sh: 45 | @mkdir -p resources 46 | @echo "#!/bin/bash" > resources/setup.sh 47 | @echo "echo Executing example setup script" >> resources/setup.sh 48 | @echo "uname -a" >> resources/setup.sh 49 | @chmod +x resources/setup.sh 50 | 51 | # Copy files from local resources directory into image /usr/resources 52 | # Note that the resources directory is mapped to the container as a volume in the RUN_ARGS variable above 53 | copy: pull bootstrap resources 54 | @echo Copying files 55 | @docker run $(RUN_ARGS) /bin/bash -c 'mkdir $(MOUNT_DIR) && \ 56 | ./mount.sh images/$(IMAGE) $(MOUNT_DIR) && \ 57 | cp -Rv /usr/rpi/resources $(MOUNT_DIR)/usr/; \ 58 | ./unmount.sh $(MOUNT_DIR)' 59 | 60 | # Run a command inside the qemu environment 61 | setup: copy 62 | @docker run $(RUN_ARGS) /bin/bash -c './run.sh images/$(IMAGE) /usr/resources/setup.sh' 63 | 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker emulation environment for Raspberry Pi 2 | 3 | Are you sick of long compile times on your Raspberry Pi? 4 | How much time have you spent loading Raspbian images from raspberrypi.org and hand customising them? 5 | Do you too wish you could run Raspberry Pi apps in a docker container against a persistant .img file inside xhyve on macOS? 6 | Then this is the project for you! 7 | 8 | This project provides a dockerised (err, containerised) Qemu based emulated environment for the Raspberry Pi, useful for building Raspberry Pi based projects on x64 computers, and for customising Raspbian images for distribution. 9 | 10 | Please note that this is very new. It works pretty well uner linux and OSX for emulation and for creating images to deploy, but YMMV. 11 | It's also not a very good docker container, requiring priveledged mode to mount loopback adaptors and qemu on the docker host. All the scripts here can be run on native linux if you're that way inclined. 12 | 13 | Check it out on [Github](https://github.com/ryankurte/docker-rpi-emu/) or [Dockerhub](https://hub.docker.com/r/ryankurte/docker-rpi-emu/) 14 | 15 | ![Example](https://raw.github.com/ryankurte/docker-rpi-emu/gh_pages/screenshots/02.png) 16 | 17 | ## Usage 18 | 19 | Note that your docker host machine must have qemu installed. Using Docker for Mac the host environment includes this as standard (OSX docker-machines however do not come with qemu), in Debian you will need to install the qemu and qemu-user-static packages. 20 | 21 | ### From the Repo 22 | 23 | To get started with an Emulated CLI: 24 | 25 | 1. Run `git clone git@github.com:ryankurte/docker-rpi-emu.git` check out this repository 26 | 2. Run `cd docker-rpi-emu` to change into the directory 27 | 3. Run `make run-emu` to launch the emulated environment 28 | 29 | This will bootstrap a Raspbian image from raspberrypi.org, build the docker image, and launch the emulated environment. 30 | 31 | For examples of how to customise this, checkout the [Makefile](Makefile). 32 | 33 | ### From Dockerhub 34 | 35 | To get started with Docker, first pull the image with `docker pull ryankurte/docker-rpi-emu`. 36 | 37 | Ensure you have a Raspbian image handy (and you may want to back this up, it will be modified by anything you do in the emulated environment), then run the following command. 38 | 39 | `docker run -it --rm --privileged=true -v IMAGE_LOCATION:/usr/rpi/images -w /usr/rpi ryankurte/docker-rpi-emu /bin/bash -c './run.sh images/IMAGE_NAME [COMMAND]'` 40 | 41 | Where IMAGE_LOCATION is the directory containing your Raspbian image to be mounted, IMAGE_NAME is the name of the image to be used, and [COMMAND] is the optional command to be executed (inside the image). 42 | 43 | For example: 44 | 45 | `docker run -it --rm --privileged=true -v /Users/ryan/projects/docker-rpi-emu/images:/usr/rpi/images -w /usr/rpi ryankurte/docker-rpi-emu /bin/bash -c './run.sh images/2016-05-27-raspbian-jessie-lite.img /bin/bash'` 46 | 47 | Will mount the image directory `/Users/ryan/projects/docker-rpi-emu/images` and the image `2016-05-27-raspbian-jessie-lite.img` then run the command `/bin/bash` in the emulated environment. 48 | 49 | 50 | ## Components 51 | 52 | The docker container includes the required Qemu components to support emulation. This must be launched in privileged mode to allow mounting of loopback devices. 53 | 54 | The container also includes a set of scripts to streamline the loading/customization/launch/unloading of Qemu environments, which are installed into the `/usr/rpi` directory on the device. 55 | 56 | 57 | ## Commands 58 | 59 | Commands are installed into the `/usr/rpi` directory of the docker image. 60 | 61 | `./mount.sh IMAGE DIR` identifies the partition sizes in the image and mounts the raspbian image to the location specified by DIR (both root and boot partitions). 62 | `./unmount.sh DIR` unmounts both partitions mounted to DIR using the above script. 63 | `./qemu-setup.sh DIR` adds Qemu components to the image at the mount point specified by DIR. 64 | `./qemu-cleanup.sh DIR` removes Qemu components from the image at the mount point specified by DIR. 65 | `./qemu-launch.sh DIR` runs Qemu instance from the directory specified by DIR 66 | `./run.sh IMAGE [COMMAND]` wires all the above commands together to simplify launching an emulated environment with the provided IMAGE and optional COMMAND to execute. 67 | 68 | If you have any questions, comments, or suggestions, feel free to open an issue or a pull request. 69 | 70 | -------------------------------------------------------------------------------- /scripts/expand.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Expand partition 2 of an ISO image the specified amount 3 | 4 | if [ "$#" -ne 2 ]; then 5 | echo "Usage: $0 IMAGE SIZE" 6 | echo "IMAGE - raspberry pi .img file" 7 | echo "SIZE - size in mb to expand image" 8 | exit 9 | fi 10 | 11 | echo "Starting Expansion" && echo 12 | 13 | fdisk -lu $1 14 | 15 | # Attach loopback device 16 | LOOP_BASE=`losetup -f --show $1` 17 | 18 | echo && echo "Attached base loopback at: $LOOP_BASE" 19 | 20 | BLOCK_SIZE=512 21 | 22 | # Fetch and parse partition info 23 | P1_INFO=($`fdisk -l $LOOP_BASE | grep ${LOOP_BASE}p1`) 24 | P2_INFO=($`fdisk -l $LOOP_BASE | grep ${LOOP_BASE}p2`) 25 | 26 | # Locate partition 2 start address 27 | 28 | P2_START=${P2_INFO[1]} 29 | 30 | echo "Located partition 2 at $P2_START" 31 | 32 | # Attach second loopback device 33 | LOOP_P2=`losetup -f --show -o $(($P2_START*$BLOCK_SIZE)) $1` 34 | 35 | echo "Attached p2 at $LOOP_P2" && echo 36 | 37 | parted $LOOP_BASE print 38 | 39 | PARTITION_INFO=($`parted $LOOP_BASE print -m`) 40 | 41 | RESIZE_END=`echo ${PARTITION_INFO[1]} | grep -oP "(?<=${LOOP_BASE}:)[0-9.]+"` 42 | RESIZE_START=`echo ${PARTITION_INFO[4]} | grep -oP "(?<=2:)[0-9.]+"` 43 | echo "Making new partition from ${RESIZE_START}MB to ${RESIZE_END}MB" 44 | 45 | # Repartition 46 | parted $LOOP_BASE --script rm 2 47 | parted $LOOP_BASE --script mkpart primary ${RESIZE_START} ${RESIZE_END} 48 | 49 | # Check and resize file system 50 | e2fsck -p -f $LOOP_P2 51 | resize2fs $LOOP_P2 52 | 53 | # Cleanup loopbacks 54 | losetup -d $LOOP_BASE $LOOP_P2 55 | 56 | 57 | -------------------------------------------------------------------------------- /scripts/mount.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to determine offsets and mount a provided raspbian image to a provided location 4 | # This requires fdisk and permission to run the `mount` command, which is provided when 5 | # run from the docker container. 6 | 7 | # Check inputs 8 | if [ "$#" -ne 2 ]; then 9 | echo "Usage: $0 IMAGE MOUNT" 10 | echo "IMAGE - raspberry pi .img file" 11 | echo "MOUNT - mount location in the file system" 12 | exit 13 | fi 14 | 15 | if [ ! -f $1 ]; then 16 | echo "Image file $1 does not exist" 17 | exit 1 18 | fi 19 | 20 | if [ ! -d $2 ]; then 21 | echo "Mount point $2 does not exist" 22 | exit 2 23 | fi 24 | 25 | echo "Attempting to mount $1 to $2" 26 | 27 | set -e 28 | 29 | # Loopback mount is used to determine block offset for partition mounting 30 | 31 | # Attach loopback device 32 | LOOP_BASE=`losetup -f --show $1` 33 | 34 | echo "Attached base loopback at: $LOOP_BASE" 35 | 36 | # TODO: could grab this from fdisk instead of hard coding 37 | BLOCK_SIZE=512 38 | 39 | # Fetch and parse partition info 40 | P1_INFO=($`fdisk -l $LOOP_BASE | grep ${LOOP_BASE}p1`) 41 | P2_INFO=($`fdisk -l $LOOP_BASE | grep ${LOOP_BASE}p2`) 42 | 43 | # Locate partition start sectors 44 | P1_START=${P1_INFO[1]} 45 | P2_START=${P2_INFO[1]} 46 | 47 | echo "Located partitions: p1 (/boot) at $P1_START and p2 (/) at $P2_START" 48 | 49 | # Cleanup loopbacks 50 | losetup -d $LOOP_BASE 51 | echo "Closed loopback $LOOP_BASE" 52 | 53 | # Mount image with the offsets determined above 54 | mkdir -p $2 55 | mount $1 -o loop,offset=$(($P2_START*$BLOCK_SIZE)),rw $2 56 | mount $1 -o loop,offset=$(($P1_START*$BLOCK_SIZE)),sizelimit=$((($P2_START-$P1_START)*$BLOCK_SIZE)),rw $2/boot 57 | 58 | echo "Mounted to $2 and $2/boot" 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /scripts/qemu-cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Bootsrap qemu into the provided directory 4 | 5 | # Check inputs 6 | if [ "$#" -ne 1 ]; then 7 | echo "Usage: $0 MOUNT" 8 | echo "MOUNT - mount location in the file system" 9 | exit 10 | fi 11 | 12 | echo "Cleaning up Qemu" 13 | 14 | PRELOAD_FILE=$1/etc/ld.so.preload 15 | 16 | # Revert to original preload file (if backup exists) 17 | if [ ! -f $PRELOAD_FILE ]; then 18 | rm $PRELOAD_FILE 19 | mv $PRELOAD_FILE.old $PRELOAD_FILE 20 | fi 21 | 22 | QEMU_BIN=$1/usr/bin/qemu-arm-static 23 | 24 | # Remove binary interpreter 25 | if [ ! -f $QEMU_BIN ]; then 26 | rm $QEMU_BIN 27 | fi 28 | 29 | # Unmount dirs 30 | umount $1/dev/pts 31 | umount $1/sys/ 32 | umount $1/dev/ 33 | umount $1/proc/ 34 | 35 | -------------------------------------------------------------------------------- /scripts/qemu-launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # 4 | 5 | if [ "$#" -ne 1 ]; then 6 | echo "Usage: $0 [PATH]" 7 | echo "PATH - location of rpi mount" 8 | exit 9 | fi 10 | 11 | chroot $1 bin/bash 12 | -------------------------------------------------------------------------------- /scripts/qemu-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Bootsrap qemu into the provided directory 4 | 5 | # Check inputs 6 | if [ "$#" -ne 1 ]; then 7 | echo "Usage: $0 MOUNT" 8 | echo "MOUNT - mount location in the file system" 9 | exit 10 | fi 11 | 12 | echo "Bootstrapping Qemu" 13 | 14 | # Remove original preload file 15 | mv $1/etc/ld.so.preload $1/etc/ld.so.preload.old 16 | touch $1/etc/ld.so.preload 17 | 18 | # Copy binary interpreter 19 | cp /usr/bin/qemu-arm-static $1/usr/bin 20 | 21 | # Mount running dirs 22 | mount --bind /proc $1/proc/ 23 | mount --bind /dev $1/dev/ 24 | mount --bind /sys $1/sys/ 25 | mount --bind /dev/pts $1/dev/pts 26 | 27 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Convenience script to manage qemu inside docker container 4 | # Chains the provided helper scripts to mount the image, bootstrap qemu, run a command 5 | # and tear down the environment correctly. 6 | 7 | if [ "$#" -ne 1 ] && [ "$#" -ne 2 ]; then 8 | echo "Usage: $0 IMAGE MOUNT [COMMAND]" 9 | echo "IMAGE - raspberry pi .img file" 10 | echo "[COMMAND] - optional command to execute, defaults to /bin/bash" 11 | exit 12 | fi 13 | 14 | CWD=`pwd` 15 | MOUNT_DIR=/media/rpi 16 | 17 | if [ -z "$2" ]; then 18 | COMMAND=bin/bash 19 | else 20 | COMMAND=$2 21 | fi 22 | 23 | set -e 24 | 25 | # Create mount dir 26 | mkdir -p $MOUNT_DIR 27 | 28 | # Mount ISO 29 | ./mount.sh $1 $MOUNT_DIR 30 | 31 | # Bootstrap QEMU 32 | ./qemu-setup.sh $MOUNT_DIR 33 | 34 | # Launch QEMU 35 | chroot $MOUNT_DIR $COMMAND 36 | 37 | # Remove QEMU 38 | ./qemu-cleanup.sh $MOUNT_DIR 39 | 40 | # Exit 41 | ./unmount.sh $MOUNT_DIR 42 | 43 | -------------------------------------------------------------------------------- /scripts/unmount.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Convenience script to unmount rpi file system mounted using the included mount.sh 4 | # helper script 5 | 6 | if [ "$#" -ne 1 ]; then 7 | echo "Usage: $0 MOUNT" 8 | echo "MOUNT - mount location in the file system" 9 | exit 10 | fi 11 | 12 | umount $1/boot 13 | umount $1 14 | 15 | echo "Unmounted $1 and $1/boot" 16 | --------------------------------------------------------------------------------