Description
Problem
Embedded targets typically do not target the same architectures that are readily available on CI services like Travis and developers machines, yet we would still like for CI to be able to build our software for alternative architectures and, ideally, run tests on that architecture.
As an example, let's consider another project I am a maintainer on, nix-rust/nix, which although not strictly focused on embedded is likely to be needed on many embedded systems (it is a dependency of mio and many other crates as it provides a set of safer APIs on top of libc which may not be present in std
). For this project, we want to do the following:
- Ensure the software builds correctly
- Ensure the software built for passes all unit tests
- (For Rust Applications) Generate working debug/release binaries
Matrixed with these additional things for each of the above:
- With each major feature combination
- On each supported target
- On each support version of Rust
Project Case Studies
Currently, there are several projects that implement their own solutions to this problem (to varying degrees of success) and a few projects which exist to help aid developers who are seeking to build/test for several different platforms.
rust-lang/libc
The libc crate is built for and runs tests against a number of different targets including several which are not yet officially supported. The libc crate contains a CI Directory which provides an overview of the strategy it uses for doing cross-build/testing.
This boils down to the following (ignoring platforms like Windows/OSX that are not really relevant for embedded):
- Triples are specified using the
TARGET
variable and the desired rust version is specified with therust
variable in the travis matrix. Linux is used for the host OS. E.g.
- os: linux
env: TARGET=arm-unknown-linux-gnueabihf
rust: stable
- os: linux
env: TARGET=x86_64-unknown-openbsd QEMU=openbsd.qcow2
rust: stable
script: sh ci/run-docker.sh $TARGET
- The ci/run-docker.sh script will do the following for
$TARGET
:- Runs
docker build
to create a docker image using the Dockerfile inci/docker/$TARGET
. E.g. https://github.com/rust-lang/libc/blob/master/ci/docker/arm-unknown-linux-gnueabihf/Dockerfile. The docker images contain all non-rust dependencies that are required to build and test for$TARGET
. Typically, this is gcc/libc (for the libc crate) andqemu-user
. - Runs
docker run
to launch a new container from the previously built image and executes the run.sh script - With the run command, the rust sysroot (
-v
rustc --print sysroot:/rust:ro
) is shared with the container at /rust - If the
QEMU
environment variable is specified, the QEMU image is fetched and and a bunch of rigamarole and setup is performed in order to set things up. Finally, QEMU is run and the output is parsed to determine success. The only emulated machine currently is x86_64. - QEMU- is automatically selected for specific targets (see https://github.com/rust-lang/libc/blob/master/ci/run.sh#L107) and QEMU is just used for running the
libc-test
binary (cross compilation of this binary for the target is done on the host.
- Runs
Things work well, but there is a moderate amount of complexity. Effort that is done to improve testing for the libc crate do not directly help out other projects which might want the same enhancements.
Major contributors here have been @alexchrichton, @japaric, @semarie
nix-rust/nix
This nix-rust/nix crate is based off of the work done in libc in an earlier version and has diverged some since then. This work was originally done by yours truly.
- Travis is used to perform testing for the various *nix platforms that are supported.
- Cross target builds are specified as follows:
- os: linux
env: TARGET=aarch64-unknown-linux-gnu DOCKER_IMAGE=posborne/rust-cross:arm
rust: 1.7.0
sudo: true
- os: linux
env: TARGET=arm-unknown-linux-gnueabihf DOCKER_IMAGE=posborne/rust-cross:arm
rust: 1.7.0
sudo: true
- os: linux
env: TARGET=mips-unknown-linux-gnu DOCKER_IMAGE=posborne/rust-cross:mips
rust: 1.7.0
sudo: true
- os: linux
- At present, it is assumed that containers that will be used are published. No images are built as part of the travis build process itself
- Rust and the sysroot for targets supported by the docker image are built into the target image itself rather than being mounted into the container as is the case with libc.
- CI executes the run-docker.sh script which for cross-targets will in turn call run-docker.sh
- This script will run (and pull) the specified docker container from dockerhub, executing the run.sh script in the container.
- Cross compilation will be performed and, similar to libc, based on the Target we will either execute the tests directly or run them using QEMU.
- Since nix has multiple test files (as will most projects other than libc), there are some hacks in order to attempt to figure out what those are so they can be run: https://github.com/nix-rust/nix/blob/master/ci/run.sh#L66
This work was done by @posborne based on libc.
japaric/smoke
I haven't looked at this one much but @japaric has some relevant experience. Appears to use some combination of Docker/QEMU for build/testing. Dockerfile appears to be monolithic.
Others?
Looking for feedback on other projects that are doing a non-trivial amount with doing cross-build/test within the Rust ecosystem. MCU targets would be appreciated in addition to the above projects which focus on targets with an OS.
Ecosystem Projects
rust-embedded/docker-rust-cross
The goal of this repository was to provide the Docker images (a common theme for this work) in order to perform cross-compilation and cross-test for non-host architectures. To date, I have not been diligent about keeping these images up-to-date with each Rust release.
The goal is to have a common repository of Docker images that can be reused across projects like libc/nix/others so each project doesn't need to go in the weeds on that front.
@posborne is the maintainer of this project.
japaric/rust-everywhere
Rust-everywhere seeks to make it easier to cross-compile crates for other targets and publish binaries. It does not appear to help out with running tests on foreign architectures, although this is something that is likely to be desirable for projects targeting other architectures.
Final Note
For many libraries that do not do FFI (especially libc/kernel FFI), the importance of actually running tests on the target architecture is probably not as strong. Within embedded, however, there seems to be a much greater chance that using APIs that may not already have nice interfaces is increased. As such, I feel this forum is still an appropriate place to discuss how we want to handle this problem moving forward.