Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save zeroows/d169db310b39aae941eee71203105447 to your computer and use it in GitHub Desktop.

Select an option

Save zeroows/d169db310b39aae941eee71203105447 to your computer and use it in GitHub Desktop.
Cross-Compiling Rust for aarch64-unknown-linux-musl on GitHub Actions (with vendored OpenSSL + ring)

Cross-Compiling Rust for aarch64-unknown-linux-musl on GitHub Actions

With vendored OpenSSL + ring (rustls) -- a complete working guide.

The Problem

Cross-compiling Rust projects targeting aarch64-unknown-linux-musl on x86_64 GitHub Actions runners fails when:

  1. Using gcc-aarch64-linux-gnu (glibc cross-compiler) with musl target -- vendored OpenSSL gets compiled against glibc headers, introducing symbols like __memcpy_chk, __memset_chk, __vfprintf_chk that don't exist in musl.

  2. Missing AR environment variable -- the ring crate and openssl-src need the archiver (ar) to create static libraries. Without AR_aarch64_unknown_linux_musl, the cc crate falls back to the host x86_64 archiver.

  3. YAML heredoc indentation -- using cat <<'EOF' inside a YAML run: | block is fragile; indentation can produce invalid TOML config.

The Solution

Use the musl-native cross-compiler toolchain from musl.cc with explicit CC, AR, and linker environment variables.

Working GitHub Actions Workflow

env:
  CARGO_TERM_COLOR: always
  SQLX_OFFLINE: "true"
  BINARY_NAME: myapp

jobs:
  build-binaries:
    name: Build ${{ matrix.arch }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - arch: amd64
            rust_target: x86_64-unknown-linux-musl
          - arch: arm64
            rust_target: aarch64-unknown-linux-musl
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install musl toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools musl-dev

      - name: Install aarch64 musl cross-compiler
        if: matrix.arch == 'arm64'
        run: |
          curl -fsSL https://musl.cc/aarch64-linux-musl-cross.tgz | tar xz -C /opt
          echo "/opt/aarch64-linux-musl-cross/bin" >> "$GITHUB_PATH"

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.91.0"
          targets: ${{ matrix.rust_target }}

      - name: Configure cross-compilation linker
        if: matrix.arch == 'arm64'
        run: |
          mkdir -p ~/.cargo
          printf '[target.aarch64-unknown-linux-musl]\nlinker = "aarch64-linux-musl-gcc"\n' >> ~/.cargo/config.toml

      - name: Cache cargo registry and build artifacts
        uses: Swatinem/rust-cache@v2
        with:
          key: ${{ runner.os }}-${{ matrix.rust_target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          cache-targets: true

      - name: Build release binary
        env:
          CC_aarch64_unknown_linux_musl: ${{ matrix.arch == 'arm64' && 'aarch64-linux-musl-gcc' || '' }}
          AR_aarch64_unknown_linux_musl: ${{ matrix.arch == 'arm64' && 'aarch64-linux-musl-ar' || '' }}
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: ${{ matrix.arch == 'arm64' && 'aarch64-linux-musl-gcc' || '' }}
        run: cargo build --verbose --release --target ${{ matrix.rust_target }}

      - name: Stage artifacts
        run: |
          mkdir -p staging
          cp target/${{ matrix.rust_target }}/release/${{ env.BINARY_NAME }} staging/${{ env.BINARY_NAME }}-${{ matrix.arch }}
          chmod +x staging/${{ env.BINARY_NAME }}-${{ matrix.arch }}

      - name: Upload binary artifact
        uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.arch }}
          path: staging/

Key Environment Variables Explained

Variable Value Used By Purpose
CC_aarch64_unknown_linux_musl aarch64-linux-musl-gcc cc crate → ring, openssl-src C compiler for aarch64 target
AR_aarch64_unknown_linux_musl aarch64-linux-musl-ar cc crate → ring, openssl-src Archiver for static libraries
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER aarch64-linux-musl-gcc Cargo Final binary linking

How the cc crate resolves tools (lookup order)

C Compiler: CC_aarch64_unknown_linux_muslCC_aarch64-unknown-linux-muslTARGET_CCCC

Archiver: AR_aarch64_unknown_linux_muslAR_aarch64-unknown-linux-muslTARGET_ARAR

How vendored OpenSSL propagation works

  1. openssl = { features = ["vendored"] } pulls in openssl-src
  2. openssl-src build script creates a cc::Build with .target("aarch64-unknown-linux-musl")
  3. cc crate reads CC_aarch64_unknown_linux_musl → finds aarch64-linux-musl-gcc
  4. openssl-src passes that compiler to OpenSSL's ./Configure as CC=aarch64-linux-musl-gcc
  5. OpenSSL C code is compiled with musl headers (not glibc) → no fortified symbol issues

Variables NOT Needed

Variable Why Not
OPENSSL_DIR Vendored mode compiles from source
PKG_CONFIG_ALLOW_CROSS Vendored mode bypasses pkg-config
CROSS_COMPILE Harmful -- openssl-src explicitly unsets it to avoid double-prefixing
RANLIB cc crate derives it from the archiver automatically

Common Mistakes

1. Using glibc cross-compiler with musl target

# WRONG -- installs glibc-based cross-compiler
sudo apt-get install -y gcc-aarch64-linux-gnu

# RIGHT -- uses musl-native cross-compiler
curl -fsSL https://musl.cc/aarch64-linux-musl-cross.tgz | tar xz -C /opt

2. Forgetting the AR variable

# WRONG -- ring will fail when creating static libraries
env:
  CC_aarch64_unknown_linux_musl: aarch64-linux-musl-gcc

# RIGHT -- both CC and AR needed
env:
  CC_aarch64_unknown_linux_musl: aarch64-linux-musl-gcc
  AR_aarch64_unknown_linux_musl: aarch64-linux-musl-ar

3. Using heredoc in YAML for TOML config

# FRAGILE -- indentation issues with heredoc in YAML literal blocks
run: |
  cat >> ~/.cargo/config.toml <<'EOF'
  [target.aarch64-unknown-linux-musl]
  linker = "aarch64-linux-musl-gcc"
  EOF

# SAFE -- printf avoids indentation issues entirely
run: |
  printf '[target.aarch64-unknown-linux-musl]\nlinker = "aarch64-linux-musl-gcc"\n' >> ~/.cargo/config.toml

Fallback: clang/llvm Approach

If ring v0.17.x has issues with the GCC musl cross-compiler:

      - name: Install LLVM toolchain
        if: matrix.arch == 'arm64'
        run: sudo apt-get install -y clang llvm lld

      - name: Build release binary
        env:
          CC_aarch64_unknown_linux_musl: clang
          AR_aarch64_unknown_linux_musl: llvm-ar
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-gcc
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS: "-Clink-self-contained=yes -Clinker=rust-lld"
        run: cargo build --verbose --release --target ${{ matrix.rust_target }}

Note: You still need the musl.cc toolchain for the sysroot/linker even with clang.

Multi-Arch Docker Image

Pair with a minimal runtime Dockerfile using Docker's TARGETARCH:

FROM cgr.dev/chainguard/static:latest
ARG TARGETARCH
COPY --chmod=755 ./myapp-${TARGETARCH} /usr/local/bin/myapp
WORKDIR /app
ENTRYPOINT ["/usr/local/bin/myapp"]

Build and push with:

      - name: Build and push multi-arch image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile.release
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment