With vendored OpenSSL + ring (rustls) -- a complete working guide.
Cross-compiling Rust projects targeting aarch64-unknown-linux-musl on x86_64 GitHub Actions runners fails when:
-
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_chkthat don't exist in musl. -
Missing
ARenvironment variable -- theringcrate andopenssl-srcneed the archiver (ar) to create static libraries. WithoutAR_aarch64_unknown_linux_musl, thecccrate falls back to the host x86_64 archiver. -
YAML heredoc indentation -- using
cat <<'EOF'inside a YAMLrun: |block is fragile; indentation can produce invalid TOML config.
Use the musl-native cross-compiler toolchain from musl.cc with explicit CC, AR, and linker environment variables.
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/| 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 |
C Compiler: CC_aarch64_unknown_linux_musl → CC_aarch64-unknown-linux-musl → TARGET_CC → CC
Archiver: AR_aarch64_unknown_linux_musl → AR_aarch64-unknown-linux-musl → TARGET_AR → AR
openssl = { features = ["vendored"] }pulls inopenssl-srcopenssl-srcbuild script creates acc::Buildwith.target("aarch64-unknown-linux-musl")cccrate readsCC_aarch64_unknown_linux_musl→ findsaarch64-linux-musl-gccopenssl-srcpasses that compiler to OpenSSL's./ConfigureasCC=aarch64-linux-musl-gcc- OpenSSL C code is compiled with musl headers (not glibc) → no fortified symbol issues
| 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 |
# 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# 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# 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.tomlIf 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.
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 }}