Skip to content
Select theme

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.

Start from a new project:

Terminal window
cargo new crunch
Creating binary (application) `crunch` package

cargo 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/:

Terminal window
cargo build
cargo 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.14s

The 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:

Terminal window
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/crunch

Paths, 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.

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:

Cargo.toml
[profile.release]
strip = true
Terminal window
cargo build --release
ls -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/crunch

Changing 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.

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:

Terminal window
rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'
Terminal window
RUSTFLAGS="-C linker=rust-lld" cargo build --release --target x86_64-unknown-linux-musl
ls -lh target/x86_64-unknown-linux-musl/release/crunch
file 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/crunch
target/x86_64-unknown-linux-musl/release/crunch: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped

rustup 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:

Terminal window
cargo run -q --release
Hello, world!

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.