Cross-Compiling a rust app for target aarch64 on host x86_64 using Fedora 42
Cross-Compiling Rust for aarch64 on Fedora x86_64
Introduction
Over the last several years, the cloud has shifted from being dominated by x86_64 and sprinkle of PowerPC based systems to including a significant share of aarch64 across all major hyperscalers. As a result, cross-compiling Rust projects for aarch64 on a Fedora x86_64 host — especially with real-world native library dependencies — is now more common.
Problem Statement
With a heterogeneous system it becomes harder to quickly patch and upload a binary. Best case, your CI infra is also integrated with your CD infra and allows blue green testing, i.e. of an odd SIGSEGV such that you can test hypothesis quickly without much pain. However, many small to medium size teams do not have that luxury of a sophisticated deployment pipeline.
There are still options left, like compiling on a remote machine, compiling in qemu, all with their particular set of tradeoffs.
The most popular being musl, but musl suffers from a mediocre integration into buildtools, always failing in unexpected ways, and being difficult to get right consistently with dependencies that assume glibc presence.
For the immediate and fast cycle times, I do cross compile locally, and that's what we'll delve into.
Definition of Cross Compiling
Cross compiling means building a binary on one machine (the host) that is intended to run on another machine or environment (the target). The difference can be CPU architecture (x86_64 vs aarch64), operating system (Linux vs Windows), or even libc (glibc vs musl).
- Host: the system doing the compilation (your local workstation)
- Target: the system where the binary will run (the server/device)
- Architecture: the CPU instruction set (x86_64, aarch64, etc.)
In short: you compile on host for a target with a different architecture, and that implies you need the right toolchain, headers, and libraries for the given target.
First steps
Install the cross-compilation toolchain:
# Add Rust aarch64 target
# Install aarch64 GCC cross-compiler
See the Rust Cross-Compilation Guide for more details on adding targets.
Now, if you thought
would be good to go, you are likely to be disappointed. It will work for a pure rust project. But reality doesn't quite comply.
Example Project
In the following we shall assume our example has dependencies - either dynamically linked or statically linked -
for which packages exist in the target specific distribution repositories, i.e. libsqlite3-devel.aarch64 exists
in fedora:42.
We'll assume cargo as our build tool of choice.
Solution: Sysroot Configuration
What is Sysroot?
It's really just a minimal filesystem for the target architecture. It contains a basic directory structure.
- Location:
/usr/aarch64-redhat-linux/sys-root/fc42/ - Structure: Mirrors a typical Linux root with
/usr/lib64,/usr/include - Purpose: Provides headers and libraries for the target architecture
It does not provide you the ability to execute aarch64 binaries on x86_64, for that you need an emulator like qemu.
How it's done
When you specify --sysroot, the compiler/linker searches there instead of /usr on your host system.
Fedora provides a basic sysroot package for aarch64, and for a few others:
sysroot-aarch64-fc42-glibc.noarch Sysroot package for glibc, aarch64 architecture
sysroot-i386-fc42-glibc.noarch Sysroot package for glibc, i386 architecture
sysroot-ppc64le-fc42-glibc.noarch Sysroot package for glibc, ppc64le architecture
sysroot-s390x-fc42-glibc.noarch Sysroot package for glibc, s390x architecture
sysroot-x86_64-fc42-glibc.noarch Sysroot package for glibc, x86_64 architecture
It contains a directory, just like a root-filesystem, but nothing can be executed since host and target differ in architecture.
Use dnf --installroot to populate it with aarch64 packages1:
# BUG: Create development symlink for libgcc_s.so since it's missing in fedora:42
Key flags:
--installroot: Specifies the target root directory--releasever: Matches your Fedora version (42 in this example)--forcearch: Forces installation of aarch64 packages--use-host-config: Uses your host's repository configuration - which works as long as your/etc/yum.repos.d/*.repofiles are parameterized across architecture
With the sysroot now in place, we need to ensure it's being picked up by rustc as well as any gcc/clang/clang++ invocations
as well as the linker.
For that we must provide the paths to the respective invocations via cargo.
Cargo Configuration
Create or update .cargo/config.toml2 in your project or globally in ~/.cargo/config.toml:
[]
# you can skip `linker = ` if you're using `mold`[^3]
= "/usr/bin/aarch64-linux-gnu-gcc"
= ["-C", "link-arg=--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42"]
# add the target specific sysroot flags for usage in `cc-rs`
[]
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42 -I/usr/aarch64-redhat-linux/sys-root/fc42/usr/include"
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42 -I/usr/aarch64-redhat-linux/sys-root/fc42/usr/include -I/usr/aarch64-redhat-linux/sys-root/fc42/usr/include/c++/15 -I/usr/aarch64-redhat-linux/sys-root/fc42/usr/include/c++ -I/usr/aarch64-redhat-linux/sys-root/fc42/usr/include/c++/15/aarch64-redhat-linux"
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42"
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42"
= "/usr/aarch64-redhat-linux/sys-root/fc42"
= "/usr/aarch64-redhat-linux/sys-root/fc42/usr/lib64/pkgconfig:/usr/aarch64-redhat-linux/sys-root/fc42/usr/share/pkgconfig"
= "1"
Note: include paths are still absolute to the host filesystem, not relative to the sysroot.
cc-rs honors target-suffixed env vars like CFLAGS_<target> or CFLAGS_<target_with_underscores>, so the example uses the underscore form to scope flags to aarch64 only.
Cargo .cargo/config.toml does not support target-specific env vars in [env], so the vars must be named with the target suffix themselves.3
Why this is needed:
- The linker needs
--sysrootto find C runtime objects for the target architecture (Scrt1.o,crti.o,crtn.o) - C/C++ compilation (from build scripts via i.e.
cc-rs) needs sysroot for various standard headers and linking to target architecture libraries- Without these flags, the compiler/linker searches the host system and finds x86_64 libraries
Building
As per usual
Container-Based Cross-Compilation
If you don't use Fedora as your host OS, or want a somewhat reproducible build environment, use a container:
Containerfile.cross:
Sets up a compilation environment (rust, cargo, gcc, ..) as well as the aarch64 sysroot within the container itself,
so you'll need to modify it to your specific project needs!
FROM fedora:42
# Install build dependencies (including C++ cross-compiler and CMake)
RUN dnf install -y \
gcc \
gcc-c++ \
gcc-aarch64-linux-gnu \
gcc-c++-aarch64-linux-gnu \
sqlite-devel \
make \
curl \
pkg-config \
clang \
cmake \
git \
&& dnf clean all
# Copy rust-toolchain.toml to read the version
COPY rust-toolchain.toml /tmp/rust-toolchain.toml
# Install Rust using rustup with version from rust-toolchain.toml
RUN RUST_VERSION=$(grep '^channel' /tmp/rust-toolchain.toml | awk -F'"' '{print $2}') && \
echo "Installing Rust version: $RUST_VERSION" && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "$RUST_VERSION" --profile minimal
ENV PATH="/root/.cargo/bin:${PATH}"
# Set the default toolchain explicitly
RUN RUST_VERSION=$(grep '^channel' /tmp/rust-toolchain.toml | awk -F'"' '{print $2}') && \
rustup default "$RUST_VERSION"
# Verify rustup is working and add aarch64 target
RUN rustup --version && rustup show && rustup target add aarch64-unknown-linux-gnu
# Setup sysroot with aarch64 dependencies
ENV SYSROOT=/usr/aarch64-redhat-linux/sys-root/fc42
RUN dnf --installroot=${SYSROOT} \
--releasever=42 \
--forcearch=aarch64 \
--use-host-config \
install -y sqlite-devel libgcc glibc-devel gcc gcc-c++ libstdc++-devel openssl-devel pkgconf-pkg-config \
&& dnf clean all
# Verify pkg-config files exist
RUN ls -la ${SYSROOT}/usr/lib64/pkgconfig/openssl.pc
# Create libgcc_s.so symlink (linker expects unversioned name)
RUN ln -sf libgcc_s.so.1 ${SYSROOT}/usr/lib64/libgcc_s.so
# Create libstdc++.so symlink (CMake needs this for linking)
RUN if [ -f ${SYSROOT}/usr/lib64/libstdc++.so.6 ]; then \
ln -sf libstdc++.so.6 ${SYSROOT}/usr/lib64/libstdc++.so; \
fi
# Set working directory
WORKDIR /x
# Install oq for TOML manipulation
RUN /root/.cargo/bin/cargo install oq
# Ensure cargo is in PATH for CMD
ENV PATH="/root/.cargo/bin:${PATH}"
# Copy build entrypoint script
COPY build-entrypoint.sh /usr/local/bin/build-entrypoint.sh
RUN chmod +x /usr/local/bin/build-entrypoint.sh
# Default command - run build script
CMD ["/usr/local/bin/build-entrypoint.sh"]
build-entrypoint.sh:
The actual execution happening inside the container - patching .cargo/config.toml, building your binary.
#!/bin/bash
SYSROOT="/usr/aarch64-redhat-linux/sys-root/fc42"
# Ensure .cargo/config.toml exists
if [; then
fi
# Patch .cargo/config.toml
# Build for aarch64
Shell Wrapper:
Convenience helper, build the container using podman and executing the build
while sharing cargo cache and some binaries. If you're on OS X, it will need
adjustments for mapping volumes.
# The `crisscross` function expects:
# - `rust-toolchain.toml` - To determine which Rust version to install
# - `Containerfile.cross` - Container build instructions
# - `build-entrypoint.sh` - Script that patches cargo config and runs build
Usage: crisscross or crisscross Containerfile.aarch64 my-cross-build-env-setup-containing-sysroot-oci-image:tag
A full working demo can be found at https://github.com/konifay/blogpost-demos/tree/main/2026-0001-cross-compile-rust-fedora.
Enjoy!
Resources
- DNF5 Documentation - See command reference for installroot usage
- Rust Cross-Compilation Guide
- Cargo Configuration Reference
- Specifying glibc Version in Cargo Build
- mold - A Modern Linker
- sysroot setup for gnome builder
Troubleshooting Guide
Below is a comprehensive list of common issues, quirks, and gotchas encountered during cross-compilation.
Issue 1: Missing libgcc_s.so
Error:
/usr/bin/aarch64-linux-gnu-ld: cannot find -lgcc_s: No such file or directory
Typical offenders: toolchain/runtime pieces expected by C/C++ build systems and linkers.
Solution:
The linker looks for libgcc_s.so (without version suffix), but only libgcc_s.so.1 exists in the sysroot. Create a symlink:
Issue 2: Missing standard headers (e.g., assert.h)
Error:
c/blake3_impl.h:4:10: fatal error: assert.h: No such file or directory
Typical offenders: build scripts using cc-rs or C/C++ code in crates like blake3 that expect glibc headers.
Solution:
Install glibc-devel in the sysroot (see the dnf --installroot command above). Ensure CFLAGS/CXXFLAGS include --sysroot so headers resolve correctly.
Issue 3: Missing target libraries (e.g., SQLite, libpq, libgit2)
Error:
/usr/bin/aarch64-linux-gnu-ld: cannot find -lsqlite3: No such file or directory
Typical offenders: native -sys crates (sqlite, libpq, libgit2) that expect target-arch -devel packages.
Solution:
Install the relevant -devel packages in the sysroot via dnf --installroot:
Issue 4: OpenSSL Not Found (pkg-config fails)
Error:
Could not find directory of OpenSSL installation, and this `-sys` crate cannot
proceed without this knowledge. If OpenSSL is installed and this crate had
trouble finding it, you can set the `OPENSSL_DIR` environment variable for the
compilation process.
Could not find openssl via pkg-config:
pkg-config has not been configured to support cross-compilation.
Typical offenders: openssl-sys and other crates that rely on pkg-config to locate headers/libs.
Solution: Install OpenSSL + pkg-config in the sysroot and configure the cross pkg-config environment:
Add to .cargo/config.toml:
[]
= "/usr/aarch64-redhat-linux/sys-root/fc42"
= "/usr/aarch64-redhat-linux/sys-root/fc42/usr/lib64/pkgconfig:/usr/aarch64-redhat-linux/sys-root/fc42/usr/share/pkgconfig"
= "1"
Issue 5: Missing Development Symlinks
Error:
/usr/bin/aarch64-linux-gnu-ld: cannot find -lfoo: No such file or directory
Where libfoo.so.X exists in the sysroot but libfoo.so symlink is missing.
Typical offenders: Any crate using cc-rs or direct linking when -devel packages don't create proper symlinks via dnf --installroot.
Root cause: Development packages typically install versioned libraries (e.g., libfoo.so.1.2.3) and a symlink libfoo.so → libfoo.so.1.2.3. However, when using dnf --installroot, sometimes only the versioned library gets installed without the development symlink. The linker (ld) searches for -lfoo by looking for libfoo.so.
Solution: Manually create symlinks in the sysroot:
# Find the actual library version
# Create the symlink
Common libraries requiring symlinks:
libgcc_s.so.1→libgcc_s.solibstdc++.so.6→libstdc++.solibssl.so.3→libssl.so
Issue 6: CMake C++ Compiler Test Fails (Missing libstdc++)
Error:
The C++ compiler "/usr/bin/aarch64-linux-gnu-g++" is not able to compile a simple test program.
/usr/bin/aarch64-linux-gnu-ld: cannot find -lstdc++: No such file or directory
Typical offenders: CMake-based crates (libz-ng-sys, rocksdb, snappy) that invoke the linker directly and need the C++ runtime.
Solution: Install C++ standard library in the sysroot:
Also ensure linker flags are set:
[]
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42"
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42"
= "--sysroot=/usr/aarch64-redhat-linux/sys-root/fc42"
Critical quirk: CMake may invoke the linker (aarch64-linux-gnu-ld) directly instead of through the compiler wrapper. The linker needs unversioned .so files (e.g., libstdc++.so, not libstdc++.so.6). Create symlinks:
Issue 7: CMake Linker Invocation Bypasses Compiler Flags
CMake often bypasses compiler wrappers and invokes the linker directly. This means CFLAGS and CXXFLAGS may not propagate to the linking stage. You must set:
LDFLAGS_<target>- General linker flagsCMAKE_EXE_LINKER_FLAGS_<target>- CMake-specific linker flagsCXXLDFLAGS_<target>- C++ linker flags (for some build systems)
All should include --sysroot=/path/to/sysroot.
Issue 8: Host vs Target Confusion
Build scripts (build.rs) run on the host architecture (x86_64), but they may compile code for the target architecture (aarch64). If a build script requires native tools like clang or cmake, install them on the host system:
If the build script compiles C/C++ for the target, ensure cross-compilers are available and CFLAGS_<target> is set.
Issue 9: pkg-config Cross-Compilation
By default, pkg-config searches /usr/lib64/pkgconfig on the host. For cross-compilation:
- Set
PKG_CONFIG_SYSROOT_DIRto the target sysroot - Set
PKG_CONFIG_PATHto<sysroot>/usr/lib64/pkgconfig - Set
PKG_CONFIG_ALLOW_CROSS=1to bypass sanity checks
Without these, openssl-sys and similar crates will fail.
Issue 10: Binary Compatibility
Binaries built with glibc are not portable across different glibc versions. If your target system runs Fedora 40 with glibc 2.39, but you build against Fedora 42 with glibc 2.41, the binary may fail with "version `GLIBC_2.41' not found".
Solutions:
- Explicitly provide a glibc API version4 - glibc provides versioned symbols
- Match the Fedora version to your deployment target
- Use musl for static binaries (though this has its own challenges)
tl;dr
- Proper sysroot setup with
--installroot
- Installing all native dependencies in the sysroot
- Cargo configuration to pass sysroot to compiler and linker invocations
- Apply specific quirks for your set of libraries
The same principles apply to rust/C/C++ cross-compilation.
Footnotes
See DNF5 Documentation for more on installroot usage.
See Cargo Configuration Reference for all available options.
mold is a fast modern linker with built-in cross-compilation support.
See Specifying glibc Version in Cargo Build for techniques.
Cargo's [env] table only supports exact environment variable names (no per-target variants). See Cargo Configuration Reference.