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 😉

Leave a Reply

Your email address will not be published. Required fields are marked *