Cross-compiling Rust Code using GitLab CI

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.

Further Reading