Cross-compiling Rust Code using GitLab CI
I've been excited to learn the Rust language for two years now since it compiles the performance properties of statically-compiled languages with modern language ergonomics (and allows you to parallelize things you normally wouldn't dare to thanks to its safety model). Another nice, less highlighted aspect of Rust is that it supports compiling to many different platforms including Windows, Linux and MacOS but also to the Web via WebAssembly - and cross-compilation is very straightforward, too.
Since I've recently been playing a lot with GitLab and especially it's wonderful built-in CI/CD platform based on Docker, I wanted to use Gitlab CI/CD to automatically compile a Rust command line application from the same code base for Windows an Linux every time that the pipeline is triggered.
TL;DR: Check out the CI workflow in spai and the statically compiled binaries for Windows and Linux that it produces.
A setup for cross-compiling to Windows
First, we need a way to cross-compile our rust code to Windows. rustup target list
shows the target x86_64-pc-windows-gnu
which uses MinGW to compile. When installing the target, however, the MinGW compiler toolchain is not installed. We could get it from the distribution's package manager, or we could take advantage of docker to make the compiler setup more portable!
Enter rust-musl-builder
The rust-musl-builder project is a Docker image that can be used to quickly set up all dependencies for statically compiling Linux Rust binaries using the musl libc. It simply starts with a Ubuntu base image and adds the Rust toolchain plus the tools and libraries needed to compile to the x86_64-unknown-linux-musl
target.
Additions for compiling with MinGW
From such an ubuntu base system, it is easy to additionally install MinGW and add the additional x86_64-pc-windows-gnu
target:
$ sudo apt-get update && sudo apt-get install -y gcc-mingw-w64
$ rustup target add x86_64-pc-windows-gnu
Additional configurations are necessary to correctly configure cargo
to find the right linker and ar
binaries:
~/.cargo/config
:
[target.x86_64-pc-windows-gnu]
linker = "/usr/bin/x86_64-w64-mingw32-gcc"
ar = "/usr/x86_64-w64-mingw32/bin/ar"
[target.i686-pc-windows-gnu]
linker = "/usr/bin/i686-w64-mingw32-gcc"
ar = "/usr/i686-w64-mingw32/bin/ar"
rust-musl-mingw-builder
I've packaged the changes in the above section into the rust-musl-builder-mingw project and associated docker image. Thanks to this, you can simply use an alias to cross-compile to windows from any (Linux) machine running Docker:
alias rust-musl-mingw-builder='docker run --rm -it -v "$(pwd)":/home/rust/src sseemayer/rust-musl-builder-mingw'
GitLab CI
We can now use the above docker image to build a Gitlab CI/CD pipeline that will rebuild Windows and Linux binaries everytime the pipeline is triggered:
image: "sseemayer/rust-musl-builder-mingw:latest"
stages:
- test
- build
test:
stage: test
script:
- rustc --version && cargo --version
- cargo test --all --verbose
linux-musl:
stage: build
artifacts:
paths:
- target/x86_64-unknown-linux-musl/release/spai
script:
- cargo build --release --target x86_64-unknown-linux-musl
windows-mingw:
stage: build
artifacts:
paths:
- target/x86_64-pc-windows-gnu/release/spai.exe
script:
- cargo build --release --target x86_64-pc-windows-gnu
The artifacts
property defines which files to keep as the output of the build operation. These files can then be reached directly from a link:
https://gitlab.com/<namespace>/<project>/-/jobs/artifacts/<ref>/raw/<path>?job=<job_name>
In our example, <ref>
could be master
, <path>
could be target/x86_64-unknown-linux-musl/release/spai
and <job_name>
could be linux-musl
.
The Final Result
This is the main idea behind how spai, my cross-platform tool for monitoring a folder for changes and posting them to an HTTP URL, is compiled. You can follow the link to see all of the sourcecode and give it a try.