Rust in Practice #7 Release Optimization and Cross-Compilation: Small, Fast Binaries That Run Anywhere
With the safety net of tests (Part 6) in place, we now build binaries to hand to other people. This part has two goals: tightening the release build one more turn to make it small and fast, and producing binaries for operating systems and architectures other than the machine in front of you.
Tightening the release profile #
Part 4 measured the effect of --release; that release still has screws left to turn. Add a profile section to Cargo.toml:
[profile.release]
lto = true # link-time optimization: inlining and elimination across crate boundaries
codegen-units = 1 # give up parallel codegen for maximum optimization quality
strip = true # remove debug symbols to shrink the binaryNone of these options is free — each is a trade.
- lto (link-time optimization): optimization normally ends at crate granularity; lto revisits the whole program at link time, inlining across crate boundaries and removing dead code. The price is longer compile times.
- codegen-units = 1: the default splits code generation into chunks for parallel compilation, and optimization opportunities die at the chunk boundaries. At 1, quality is maximal and compilation slowest.
- strip: removes debug symbols for a large size reduction. The price is poorer backtraces from the shipped binary.
In short, a setting that trades slower development iteration for shipping quality — and because it lives only in the release profile, everyday cargo run and cargo test are untouched. Even at loglens scale, strip alone routinely takes a binary from several MB down to the 1 MB range. One caution: panic = "abort" (terminate immediately, no unwinding), which appears on every size-reduction checklist, deserves hesitation. It shrinks the binary but sacrifices cleanup on panic and backtrace quality — if your tool needs usable bug reports from users, the default is the safer choice.
Cross-compilation: the notion of a target #
Development happened on a Mac, but loglens will run on Linux servers. Rust names compile targets with “triples” that look like x86_64-unknown-linux-gnu (architecture-vendor-OS-libc), and rustup can add targets:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslChoosing the -musl target is this part’s key decision. Binaries for the default target (-gnu) link dynamically against the system’s glibc — and if the build environment’s glibc is newer than the server’s, execution is refused with the infamous GLIBC_2.xx not found. The musl target produces a single file with the C library statically linked in, so it runs on old distributions and Alpine containers alike by copying the file, with nothing else required. It completes the CLI virtue of “one file, no installation,” which is why it is the de facto standard choice for Rust CLI distribution on Linux.
The linker problem, and cross #
Run that command on a Mac, though, and it usually stops at the link step. The Rust compiler can generate code for any target, but the linker that assembles the final binary must belong to the target platform. The orthodox fix is installing and configuring a linker per target; the widely used detour is cross.
cargo install cross
cross build --release --target x86_64-unknown-linux-muslcross runs cargo build for you inside a Docker container that already has the target’s toolchain. Its command interface mirrors cargo, so the learning cost is near zero, and it steps over the linker-configuration swamp entirely. The single prerequisite is Docker. And as the next part shows, release automation solves this differently again: when each OS’s runner builds its own binaries, cross-compilation itself mostly disappears. cross earns its place as the tool for “I need this built here, now.”
Binaries for macOS and Windows users follow the same principle: build aarch64-apple-darwin (Apple Silicon), x86_64-apple-darwin (Intel Mac), and x86_64-pc-windows-msvc (Windows) to fill out the distribution list. That list becomes next part’s release matrix.
Summary #
- lto, codegen-units = 1, and strip trade compile time for speed and size. They apply only to the release profile, so development iteration is unaffected.
panic = "abort"shrinks further but sacrifices backtrace quality. For tools that need bug reports, the default is safer.- Cross-compilation is
rustup target addplus--target. For Linux distribution, musl static linking — escaping glibc version hell — is the de facto standard. - When compilation succeeds but the linker balks, cross substitutes an entire Docker-hosted toolchain.
- The target list (Linux musl, two Macs, Windows) is next part’s raw material: crates.io publication and GitHub Releases automation close out the series.