Shipping it: release builds and installers
This step finishes: the track. Eleven steps in, hit parses arguments, sends requests, colours its output, reads ~/.hit.toml, replays saved requests, runs batches concurrently, and guards all of it with an offline test suite. What it can’t do yet is leave your machine. This step makes the binary people actually run: a release build instead of a debug one, debug symbols stripped, an Intel build from an Apple Silicon Mac, and two paths to installation — cargo install for Rust users, and cargo-dist-generated CI that attaches archives for every major platform to a GitHub Release.
You need the project as the previous article left it. If you are starting from scratch, that state is tagged cli-ch11 in the examples repository:
git clone https://github.com/hackingwithrust/examples.gitcd examples/cli-toolgit checkout cli-ch11The release profile
Section titled “The release profile”Everything so far has run cargo build or cargo run, which build the dev profile: unoptimized, with debug info, compiled fast so the edit-run loop stays short. The binary people install is a different build:
cargo build --release Finished `release` profile [optimized] target(s) in 27.21sThe first release build recompiles every dependency with optimisations on, so expect it to take much longer than any dev build this track has done; that time is cached, and later builds are quick. The output lands in target/release/, not target/debug/ — the two profiles never share artifacts. The size difference is not subtle:
ls -lh target/debug/hit target/release/hit-rwxr-xr-x@ 1 rango staff 25M Oct 17 11:05 target/debug/hit-rwxr-xr-x@ 1 rango staff 7.8M Oct 17 11:07 target/release/hit25 MB of debug binary becomes 7.8 MB of release binary (8,231,680 bytes here; your exact numbers will differ a little by platform). cargo run --release -- get https://example.org runs the optimised binary without installing it.
Stripping debug symbols
Section titled “Stripping debug symbols”Even the release build carries debug symbols — the data a debugger uses to map machine code back to source lines. You are not shipping a debugger target; you are shipping a program. One line in Cargo.toml drops them:
[profile.release]strip = trueRebuild, and the binary loses another 1.5 MB:
cargo build --releasels -lh target/release/hit-rwxr-xr-x@ 1 rango staff 6.4M Oct 17 11:07 target/release/hit6,673,872 bytes on this machine, down from 8,231,680. strip = true removes all symbols; strip = "debuginfo" keeps the symbol table but drops debug info, which still lets tools like sample name functions in a stack trace. If you ever need to profile a user’s crash report, that middle setting is the one to reach for.
It is tempting to keep tuning here. Measured on this project, lto = "thin" in the same profile recompiled the whole graph and saved 26 KB (6,673,872 → 6,647,488). For a network client that spends its life waiting on sockets, that is nothing. Profile settings are worth measuring, not assuming.
Building for other machines
Section titled “Building for other machines”A macOS binary runs on macOS. The --target flag builds for a named platform — a target triple — and cross-compiling to another Mac needs nothing but the standard library for that architecture:
rustup target add x86_64-apple-darwininfo: downloading component 'rust-std' for 'x86_64-apple-darwin'info: installing component 'rust-std' for 'x86_64-apple-darwin'cargo build --release --target x86_64-apple-darwin Compiling hit v0.1.0 (/Users/rango/www/hackingwithrust/code/cli-tool) Finished `release` profile [optimized] target(s) in 23.67sThe output goes to target/x86_64-apple-darwin/release/ — each target gets its own directory, so the two builds coexist. file reads the architecture header and says what each one is:
file target/release/hit target/x86_64-apple-darwin/release/hittarget/release/hit: Mach-O 64-bit executable arm64target/x86_64-apple-darwin/release/hit: Mach-O 64-bit executable x86_64The Intel build is 7.3 MB (7,631,248 bytes). On a Mac without Rosetta it refuses to run — Bad CPU type in executable — which is the honest limit of local cross-compiling: an Intel Mac or a Rosetta install can run it, this machine can’t. macOS-to-macOS is the easy case. Cross-compiling to Linux from macOS pulls in a different linker and C library, and the usual answers (cross, a musl target, a Linux CI runner) are each their own afternoon.
That is the gap cargo-dist exists to close: instead of one machine pretending to be five platforms, let CI machines build natively for theirs.
Let CI build every platform
Section titled “Let CI build every platform”cargo-dist turns “build for every platform” into a GitHub Actions workflow it generates for you. Version 0.33.0 is what this article uses; install it with brew install cargo-dist or cargo install cargo-dist, then, from the cli-tool directory:
cargo dist init -ylet's setup your dist config...
✔ added [dist] to your root dist-workspace.toml✔ dist is setup!
running 'dist generate' to apply any changes
generated Github CI to /Users/rango/www/hackingwithrust/code/.github/workflows/release.yml(The -y accepts the recommended defaults; without it, dist init asks the same choices as prompts.)
It wrote three things. Cargo.toml gains a profile for its builds — the repository field above it is how dist found the GitHub repo to target:
repository = "https://github.com/hackingwithrust/examples"# The profile that 'dist' will build with[profile.dist]inherits = "release"lto = "thin"A dist-workspace.toml next to Cargo.toml holds the settings, including the five platforms it will build:
[workspace]members = ["cargo:."]
# Config for 'dist'[dist]# The preferred dist version to use in CI (Cargo.toml SemVer syntax)cargo-dist-version = "0.33.0"# CI backends to supportci = "github"# The installers to generate for each appinstallers = []# Target platforms to build apps for (Rust target-triple syntax)targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]And a .github/workflows/release.yml that publishes a GitHub Release whenever a tag that looks like a version is pushed:
on: pull_request: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*'The workflow’s jobs plan the release, build on a matrix of runners — one per platform in targets, natively — and attach the resulting archives to a GitHub Release. The tag and the crate version have to agree: with hit at 0.1.0 in Cargo.toml, --tag=v0.9.0 gets rejected before anything builds:
× This workspace doesn't have anything for dist to Release! help: You may need to pass the current version as --tag, or need to give all your packages the same versionSo the release flow is: bump version in Cargo.toml, commit, tag v0.1.0, push the tag. GitHub builds the archives; the Release page carries them.
Before trusting the CI, build the same thing locally — dist build uses the dist profile and produces the exact archives CI will:
cargo dist buildannouncing v0.1.0 hit 0.1.0 /Users/rango/www/hackingwithrust/code/cli-tool/target/distrib/source.tar.gz [checksum] /Users/rango/www/hackingwithrust/code/cli-tool/target/distrib/source.tar.gz.sha256 /Users/rango/www/hackingwithrust/code/cli-tool/target/distrib/sha256.sum /Users/rango/www/hackingwithrust/code/cli-tool/target/distrib/hit-aarch64-apple-darwin.tar.xz [bin] hit [misc] README.md [checksum] /Users/rango/www/hackingwithrust/code/cli-tool/target/distrib/hit-aarch64-apple-darwin.tar.xz.sha256Locally it can only build the host platform; CI fills in the other four. The archive itself is plain tar:
tar tzf target/distrib/hit-aarch64-apple-darwin.tar.xzhit-aarch64-apple-darwin/hit-aarch64-apple-darwin/README.mdhit-aarch64-apple-darwin/hitOne caveat, because this repository is unusual. GitHub only reads workflow files from a repository’s root .github/workflows/, and dist init writes there — correct for the normal single-project repo, and what a reader’s checkout should have. The examples repository holds six tracks, so a live release workflow at its root would run on unrelated tags and PRs. This repo keeps the generated file inside cli-tool/.github/workflows/ next to the CI workflow from the testing step, and tells dist not to mind:
allow-dirty = ["ci"]If you are following along in your own repo, leave the file where dist put it and skip that line.
installers = [] is the default — archives only. Setting it to ["shell"] or ["homebrew"] makes dist also generate a curl-able install script or a Homebrew formula that fetches the right archive per platform; the workflow regenerates on the next dist generate.
Install it with cargo install
Section titled “Install it with cargo install”The other path needs no GitHub Release at all. cargo install builds a crate in release mode and puts the binary on your PATH:
cargo install --path . Installing /Users/rango/.cargo/bin/hit Installed package `hit v0.1.0 (/Users/rango/www/hackingwithrust/code/cli-tool)` (executable `hit`)~/.cargo/bin is on PATH wherever rustup is installed, so hit works from any directory:
which hithit --version/Users/rango/.cargo/bin/hithit 0.1.0This is how every Rust developer already installs tools — ripgrep, fd, bat all arrive this way — and it is the lowest-friction distribution the language has. For everyone else, the dist archives from the previous section are the fallback. Published crates (after cargo publish) install with plain cargo install hit; a path install like this one is the local-repo version of the same mechanism.
Where the code lives
Section titled “Where the code lives”This step’s end state is tagged
cli-ch12 in the
examples repository — the project exactly as this
article leaves it:
git clone https://github.com/hackingwithrust/examples.gitcd examples/cli-toolgit checkout cli-ch12If your terminal shows something different from what this article describes, checking out the tag and diffing it against your own tree shows which line drifted.
There is no next step — the track is done. hit started as an empty directory twelve articles ago and ended as a tested, coloured, configured, concurrent, installable command-line HTTP client. The tag chain from cli-ch02 to cli-ch12 is its complete history; check out any step to see the project as that article left it.
The tool is yours to extend now. Body-aware post, request timeouts per saved request, shell completions via clap_complete — none of it changes the shape of what you have: a main.rs that fits on two screens and a test suite that runs offline.