← Table of contents

9.3 Forged

The artifact has passed the gate. Tests pass, the port proves itself, the ledger is kept, the door closes cleanly. All of this - in the workshop, with a debug build: no optimizations, function names and line numbers that the debugger needs and the user does not. The workshop is not the world. What goes out into it is assembled by different rules.

What You Need

cargo build is a debug build. The compiler is in a hurry: no optimizations, everything is kept. The tq binary in target/debug/ weighs 21 megabytes. Inside - the names of every function, source line numbers, data for the debugger. The user needs none of it.

cargo build --release activates a different profile. The compiler stops hurrying: it applies optimizations, removes dead code. The profile can be configured in the root Cargo.toml - that is where you define what “release” means for this project. Two settings have the largest effect.

lto = true - link-time optimization. Normally each crate compiles separately; the linker joins the object files afterward. With lto = true, the compiler sees the entire program graph at link time: it removes functions nobody calls, inlines small functions, eliminates branches that never execute. The binary is smaller and runs faster.

strip = true - discards debug symbols from the final binary. Function names, source paths, line numbers - none of it is needed by the user.

The Build

Add a section to the root Cargo.toml after [workspace]:

[profile.release]
lto = true
strip = true

In the Makefile, add a target in the BUILD section, before clean:

## release: build optimized stripped binaries
.PHONY: release
release:
	@cargo build --release -p tq-cli -p tq-api
	@ls -lh target/release/tq target/release/tq-api

The Result

$ make release
    Finished `release` profile [optimized] target(s) in 53.72s
-rwxrwxr-x 1 user user 1.1M target/release/tq
-rwxrwxr-x 1 user user 1.7M target/release/tq-api

target/debug/tq weighed 21 megabytes. target/release/tq - 1.1. Twenty times smaller. The debug binary is a draft with margin notes. What leaves the workshop is the finished piece.

The finished artifact runs without Rust:

$ ./target/release/tq list
$ ./target/release/tq-api

tasks.json persists between runs - data from earlier chapters is still there.

make ci verifies correctness. make release builds what gets shipped.

The complete tq code for this chapter is in 9-shippable/03-forged/.


Lore: What the Debug Build Contains

In a debug build the binary contains a .debug_info section - data in DWARF format. It holds variable and function names, source file paths, line numbers, types. The debugger (gdb, lldb) reads this section to show “you stopped at line 42 in store.rs” instead of “you stopped at address 0x4a3f12”. strip = true removes this section; the program’s behavior does not change.

You can remove only part of it: strip = "debuginfo" removes the debug data but keeps symbol names - useful if you need to profile a release build.

[profile.release] accepts many settings: opt-level, codegen-units, panic, overflow-checks, and others. The right combination depends on the task - an embedded system, a server application, and a CLI each require different trade-offs. The full list with explanations is in the Cargo documentation.