Skip to main contentrelease, cross compilation, and binary size | IoT Worker

release, cross compilation, and binary size

Before delivering a C project, “it compiles” is not enough.

You still check:

whether release optimization is enabled
whether architecture and ABI match the target
whether linked libraries are target libraries or host libraries
whether the binary carries unnecessary symbols
whether runtime dynamic dependencies exist on the target
whether size fits device limits
whether crashes can still be symbolized

Rust delivery for CLI tools, system libraries, or C/Rust boundary artifacts has the same concerns. Cargo simplifies the build command, but it does not remove release checks.

release build Is Not Just Another Directory

Development builds usually use:

cargo build

Delivery usually uses:

cargo build --release

The output moves from:

target/debug/

to:

target/release/

But the difference is not just the directory. The release profile enables optimization, takes longer to compile, and usually runs faster.

Configure it in Cargo.toml:

[profile.release]
opt-level = 3
debug = "line-tables-only"
lto = "thin"
codegen-units = 1
panic = "abort"
strip = "symbols"

Do not copy this mechanically. Each option has a cost:

  • opt-level affects speed and size
  • lto may reduce size or improve speed, but increases link time
  • codegen-units = 1 helps optimization, but slows compilation
  • panic = "abort" may reduce size, but disables unwinding
  • strip reduces binary size, but affects debugging
  • debug controls how much symbolization information remains

Release configuration is a delivery strategy, not a contest to be maximally aggressive.

Measure Binary Size Before Tuning

Do not tune by guessing. First inspect the current artifact:

ls -lh target/release/device-cli
file target/release/device-cli

Look at ELF section sizes:

size target/release/device-cli

Check dynamic dependencies:

ldd target/release/device-cli

Inspect symbols:

nm -S --size-sort target/release/device-cli

Many size problems are ordinary linking problems, not Rust-specific problems:

  • static linking pulls in large libraries
  • unnecessary features are enabled
  • debug info was not handled
  • panic/unwind code remains
  • LTO is off, so cross-crate optimization is weaker
  • dependencies bring in large formats, TLS, logging, or regex stacks

Find the source before changing profiles, features, or dependencies.

opt-level Is Not Always Smaller When Higher

Rust release builds usually optimize for speed. Small devices or CLI tools may care more about size.

Try:

[profile.release]
opt-level = "z"

or:

[profile.release]
opt-level = "s"

Roughly:

3   -> speed-oriented
s   -> size-oriented
z   -> more size-oriented

But measure the result. Some programs are smaller with opt-level = 3 because inlining and optimization remove more code. Do not replace measurement with rules of thumb.

panic Strategy Affects Size and Semantics

The default panic strategy may unwind. For libraries and FFI, panic should not cross a C ABI anyway.

If the program or boundary can accept abort, configure:

[profile.release]
panic = "abort"

This often reduces size and makes panic behavior direct.

But the cost is real:

panic terminates the process
destructors on unwound stack frames do not run
not suitable for code that expects catch_unwind recovery

CLI tools often accept this. Dynamic libraries embedded into an existing host process need more care, because abort may kill the host process.

Balance strip and Debug Information

Release artifacts are often stripped:

[profile.release]
strip = "symbols"

or after build:

strip target/release/device-cli

But production crash analysis needs symbols. A common delivery model:

ship a stripped binary
keep the unstripped symbol file on the build server
record version, commit, and build id
symbolize crash addresses with the matching symbol file

Do not save a few hundred KB by destroying all debugging ability. Field failures are much harder when addresses cannot be mapped back to source.

Cross Compilation Starts with the Target Triple

Rust cross compilation uses a target triple:

rustup target add aarch64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu

The target triple carries architecture, system, and ABI information:

x86_64-unknown-linux-gnu
aarch64-unknown-linux-gnu
armv7-unknown-linux-gnueabihf
x86_64-unknown-linux-musl

For C developers, this is not just “CPU architecture.” It also implies ABI and C library ecosystem.

gnu usually means a glibc-style dynamic-linking environment. musl is often used for more self-contained static linking. The right choice depends on the target system.

Cross Compilation Success Does Not Mean It Runs

Before copying the binary to the device, inspect it:

file target/aarch64-unknown-linux-gnu/release/device-cli
readelf -h target/aarch64-unknown-linux-gnu/release/device-cli

Check:

ELF machine matches the target CPU
32-bit or 64-bit matches
dynamic linker path exists on the target
required .so files exist in the target rootfs
floating-point ABI matches
target kernel and C library versions support the binary

For dynamically linked programs:

readelf -d target/.../device-cli

If the target board can run it:

ldd ./device-cli

Do not stop at “it compiled.” Cross-build failures often happen at build time; cross-run failures often happen at ABI, dynamic linker, rootfs, or system library boundaries.

C Dependencies Must Come from the Target sysroot

The previous article covered build.rs and pkg-config. During cross compilation, C dependencies must come from the target sysroot.

Wrong model:

build.rs runs on x86_64 host
pkg-config finds host /usr/lib
final artifact targets an ARM device

Correct direction:

Rust target is ARM
C headers and libraries come from the ARM sysroot
pkg-config reads target .pc files
the linker uses the target toolchain

This is not a Rust-language issue. It is cross-compilation fundamentals placed inside the Cargo workflow.

features Affect Size

When size is large, inspect enabled features.

A CLI tool may pull in TLS, compression, JSON, regex, or a large logging stack through default features.

Dependencies can disable default features:

[dependencies]
some-crate = { version = "1", default-features = false, features = ["std"] }

Do not disable blindly. Default features may affect functionality, platform support, or security.

A practical size-tuning order:

confirm release profile
inspect dependencies and features
decide dynamic vs static linking
then tune LTO, strip, panic, opt-level
finally replace heavy dependencies or split functionality

Libraries and CLI Tools Need Different Release Policies

CLI tools can often be more aggressive:

panic = abort
strip symbols
opt-level = z or s
single-file deployment

Dynamic libraries called by C need more care:

do not let panic abort the host unless the contract says so
keep enough symbols for crash analysis
maintain ABI stability
confirm dynamic dependencies exist on the target

Different artifacts in the same workspace can have different delivery policies. Do not blindly apply a CLI size profile to an FFI dynamic library.

What to Keep

Rust release and cross compilation map roughly like this:

cargo build --release          -> use release profile
[profile.release]              -> optimization, LTO, strip, panic strategy
--target <triple>               -> target architecture/system/ABI
file/readelf                    -> inspect ELF, architecture, interpreter, dynamic section
ldd                             -> check runtime dynamic dependencies
size/nm                         -> inspect section sizes and large symbols
default-features = false        -> control dependency default capabilities
strip/debug/build id            -> balance size and debugging ability

Before delivery, do not only ask whether it compiles. Ask whether the binary targets the right system, whether dependencies exist on the target, where size comes from, and whether field crashes can still be diagnosed.

The next article covers publishing CLI tools, crates, and versioning. Release builds produce an artifact; publishing strategy defines how others install, upgrade, and rely on it over time.