How to build a binary you can hand to someone
Short answer: cargo build --release, then add strip = true under [profile.release] so the binary ships without its symbol table. If the person you’re handing it to runs Linux, build for x86_64-unknown-linux-musl and hand over a binary with no dynamic libraries at all.
The release profile
Section titled “The release profile”Start from a new project:
cargo new crunch Creating binary (application) `crunch` packagecargo build builds the dev profile — unoptimised, with debug info, with the runtime checks on. --release builds the release profile instead, and the binary lands in target/release/:
cargo buildcargo build --release Compiling crunch v0.1.0 (/private/tmp/crunch) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s Compiling crunch v0.1.0 (/private/tmp/crunch) Finished `release` profile [optimized] target(s) in 0.14sThe two profiles don’t share artifacts, so the first release build recompiles everything — here that’s one crate and no dependencies. The size difference is the visible part:
ls -lh target/debug/crunch target/release/crunch-rwxr-xr-x@ 1 rango wheel 462K Oct 23 13:38 target/debug/crunch-rwxr-xr-x@ 1 rango wheel 425K Oct 23 13:38 target/release/crunchPaths, timestamps and sizes above are from the machine these ran on; yours will differ. The profile defaults, from the Cargo book:
| setting | dev | release |
|---|---|---|
opt-level |
0 | 3 |
debug |
true | false |
debug-assertions |
true | false |
overflow-checks |
true | false |
incremental |
true | false |
Most of those trade compile time for run time. One changes results: with overflow-checks off, arithmetic that panics in a debug build wraps silently in the release build. A binary you hand over is a release binary, so it behaves that way whether you tested it that way or not.
Stripping
Section titled “Stripping”A release binary still carries the symbol table — the data a debugger uses to map machine code back to names. You’re not shipping a debugger target. One line drops it:
[profile.release]strip = truecargo build --releasels -lh target/release/crunch Compiling crunch v0.1.0 (/private/tmp/crunch) Finished `release` profile [optimized] target(s) in 0.11s-rwxr-xr-x@ 1 rango wheel 335K Oct 23 13:38 target/release/crunchChanging the profile invalidates the build, which is why cargo recompiles: 425 KB becomes 335 KB. strip = true is shorthand for strip = "symbols"; strip = "debuginfo" keeps the symbol table and drops only the debug info, the middle ground when you want readable crash reports. The rustc docs warn that where backtraces lean on the symbol table, stripping it can make a trace incomprehensible — for a CLI that crashes in the field, weigh that against 90 KB.
Stripping removes the local symbol table, not the dynamic imports: nm target/release/crunch still lists U __Unwind_Backtrace and its neighbours.
A static binary for Linux
Section titled “A static binary for Linux”The release binary links the system C library dynamically — libSystem on macOS, glibc on Linux. Hand it to a machine without a compatible one and it won’t start. The musl target links its own C library in, statically:
rustup target add x86_64-unknown-linux-muslinfo: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'RUSTFLAGS="-C linker=rust-lld" cargo build --release --target x86_64-unknown-linux-muslls -lh target/x86_64-unknown-linux-musl/release/crunchfile target/x86_64-unknown-linux-musl/release/crunch Compiling crunch v0.1.0 (/private/tmp/crunch) Finished `release` profile [optimized] target(s) in 0.19s-rwxr-xr-x@ 1 rango wheel 423K Oct 23 13:38 target/x86_64-unknown-linux-musl/release/crunchtarget/x86_64-unknown-linux-musl/release/crunch: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, strippedrustup target add brought the standard library for the target, and rust ships musl and its startup files with it, so the binary needs nothing from the machine it runs on — file reports static-pie linked. Static is not smaller (423 KB against 335 KB); it is fewer things to be missing.
The RUSTFLAGS line is the part that surprises people. rustc drives the link through the platform’s C compiler by default, and on a Mac that driver’s linker only speaks Mach-O, so a musl link needs a linker that reads ELF — the rust-lld that ships with the toolchain. Note also that file says ELF: this binary won’t run on the Mac that built it. The macOS build still runs in place:
cargo run -q --releaseHello, world!What you’re handing over
Section titled “What you’re handing over”target/release/crunch is a file: no toolchain, no crate downloads, nothing to install on the receiving machine. file says arm64 here, and an Intel Mac needs an x86_64-apple-darwin build — a target question rather than a release one, and the next step in this chapter is exactly that question.
- How to test a CLI end to end — the previous step
- Shipping it: release builds and installers — the same job inside a real project, plus packaging
- Back to Building a command-line tool