Cross-Building and Distributing Static Binaries in Rust for Arm/Intel in CI for Linux
I recently wrote a little application that I needed to format text for the console. I initially built it in Qt and I created the necessary scripts to build and cross-build it to x64 and aarch64, by using my Qt docker images. I wrote something about it here.
It proved to be an excellent way to format logs from apps and make debugging and investigation easier.
Now I’d like to also have a armv7 version, which is a problem as I do not maintain an armv7 Qt build. Rewriting it in Rust seems to be a good idea: porting it to multiple architectures and distributing it in various forms was very simple with the provided tool.
Rust
Rust brings excellent tools, and I thought it might be interesting to use them.
I wrote the entire code in Rust, and I can now use GitLab CI to build and cross-build Rust to armv7, aarch64 and x64 in a complete static binary, which is very handy.
musl
glibc is not very good when it comes to static builds, but musl was designed for that. Rust can build against musl and create a complete static binary, which is extremely handy. By using this simple command, everything is built and embedded into a single binary, with no dependencies:
cargo build --release --target=x86_64-unknown-linux-musl
armv7 and aarch64
I wanted to use this binary also on armv7 and aarch64 embedded systems. cross makes this extremely simple:
cross build --release --target aarch64-unknown-linux-musl cross build --release --target armv7-unknown-linux-musleabihf
By using musl again here I ensure the binary is static. I tested this on Ubuntu aarch64 on a Raspberry Pi and on Raspberry OS.
CI in docker containers
Apparently, with a little bit of setup, all these builds can be done in docker containers, which is my preferred setup when setting up CI.
To do this, I had to add CROSS_CONTAINER_IN_CONTAINER
, to inform cross it is running in docker. Also, I experienced an error in the runner:
$ cross build --release --target aarch64-unknown-linux-musl --manifest-path cgrc-rust/Cargo.toml info: downloading component 'rust-src' info: installing component 'rust-src' Error: 0: docker inspect runner-42wczs85-project-139-concurrent-0 failed with exit status: 1 Location: /rustc/90c541806f23a127002de5b4038be731ba1458ca/library/core/src/convert/mod.rs:727 Stderr: Error: No such object: runner-42wczs85-project-139-concurrent-0 Stdout: [] Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it. Run with RUST_BACKTRACE=full to include source snippets. ERROR: Job failed: exit code 1
This can be fixed by also setting the variable HOSTNAME
to $(docker ps -ql)
. For the cgrc app, for example, this is the CI GitLab script I use:
Build: stage: build_rust image: name: "carlonluca/cgrc-ci:latest" services: - docker:dind artifacts: paths: - cgrc-dist/x86_64-unknown-linux-musl/cgrc - cgrc-dist/aarch64-unknown-linux-musl/cgrc - cgrc-dist/armv7-unknown-linux-musleabihf/cgrc untracked: true script: - export CROSS_CONTAINER_IN_CONTAINER=true - export HOSTNAME=$(docker ps -ql) - cd cgrc-rust - cargo build --release --target=x86_64-unknown-linux-musl - mkdir -p ../cgrc-dist/x86_64-unknown-linux-musl - mv target/x86_64-unknown-linux-musl/release/cgrc ../cgrc-dist/x86_64-unknown-linux-musl/ - cargo clean - cd .. - cargo install cross --git https://github.com/cross-rs/cross - cross build --release --target aarch64-unknown-linux-musl --manifest-path cgrc-rust/Cargo.toml - mkdir -p cgrc-dist/aarch64-unknown-linux-musl - mv cgrc-rust/target/aarch64-unknown-linux-musl/release/cgrc cgrc-dist/aarch64-unknown-linux-musl/ - cargo clean --manifest-path cgrc-rust/Cargo.toml - cross build --release --target armv7-unknown-linux-musleabihf --manifest-path cgrc-rust/Cargo.toml - mkdir -p cgrc-dist/armv7-unknown-linux-musleabihf - mv cgrc-rust/target/armv7-unknown-linux-musleabihf/release/cgrc cgrc-dist/armv7-unknown-linux-musleabihf/
Snap
Creating snaps for Rust apps is very simple:
architectures: - build-on: arm64 - build-on: armhf - build-on: amd64 apps: cgrc: command: bin/cgrc parts: cgrc: plugin: rust source-type: git source-subdir: cgrc-rust source-branch: master source: https://github.com/carlonluca/cgrc.git
The Rust plugin will do the rest. Here is the published app.
AUR
I sometimes use Manjaro, so it is pretty comfortable for me to also have an AUR package. It is not difficult to create it:
pkgname=cgrc pkgver=2.0.3 pkgrel=2 pkgdesc='Generic log formatter' arch=(any) url='https://github.com/carlonluca/cgrc' license=(GPL) makedepends=(git cargo) source=(git+https://github.com/carlonluca/cgrc.git#tag=v$pkgver) md5sums=('SKIP') prepare() { export RUSTUP_TOOLCHAIN=stable git submodule update --init cd "$srcdir/$pkgname/cgrc-rust" cargo fetch --locked --target "$CARCH-unknown-linux-gnu" } build() { export RUSTUP_TOOLCHAIN=stable export CARGO_TARGET_DIR=target cd "$srcdir/$pkgname/cgrc-rust" cargo build --frozen --release --all-features } check() { export RUSTUP_TOOLCHAIN=stable cd "$srcdir/$pkgname/cgrc-rust" cargo test --frozen --all-features } package() { cd "$srcdir/$pkgname/cgrc-rust" install -Dm0755 -t "$pkgdir/usr/bin/" "target/release/$pkgname" }
In the prepare I had to add the fetch step here.
Macports
I also use Mac OS, so I’m used to create packages for macports:
PortSystem 1.0 PortGroup github 1.0 PortGroup cargo 1.0 fetch.type git github.setup carlonluca cgrc 2.0.1 v revision 0 license GPL-3 categories textproc maintainers {@carlonluca gmail.com:carlon.luca} openmaintainer description Configurable terminal text formatter long_description cgrc formats text from stdin according to custom configuration files \ and outputs the result with ANSI escape codes to stdout. Configuration \ files includes a set of regular expressions with the related format \ to be used to the match and the captures. build.dir ${worksrcpath}/cgrc-rust post-fetch { system -W ${worksrcpath} "git submodule update --init" system -W ${worksrcpath}/cgrc-rust "cargo fetch --locked" } destroot { xinstall -m 0755 ${worksrcpath}/cgrc-rust/target/[cargo.rust_platform]/release/cgrc \ ${destroot}${prefix}/bin/ }
Here is the page of the application.
Apparently, building and distributing simple Rust applications is pretty simple. Other technologies, like Qt, require more work.
Have fun 😉