Rust Setup: Install 1.90, Build, Test, and Ship With Cargo
Install Rust 1.90 with rustup, scaffold with cargo new, then build, test, fmt, clippy, and doc your first binary in 30 minutes.
20+ years shipping production backend systems. Everything here is grounded in real deployments.
- ✓Comfortable with the terminal on Linux, macOS, or Windows
- ✓Basic programming experience in any compiled language
- ✓VS Code installed plus a C linker (guide covers setup)
- Rust setup is three layers: rustup manages toolchains, rustc compiles code, Cargo builds, tests, formats, and documents projects
- Install with rustup: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh, then rustup default stable and rustc --version to confirm 1.90 or newer
- Daily Cargo loop: cargo new for scaffolding, cargo check while coding, cargo build for artifacts, cargo test for verification, cargo fmt plus cargo clippy before every commit
- Cargo.toml is the contract: set edition = "2024", rust-version = "1.90", pin dependencies with Cargo.lock, and split dev-dependencies from release code
- Ship trap that bites teams: missing C linker (cc not found), stale PATH without $HOME/.cargo/bin, and mixed stable plus nightly targets that force full rebuilds
Think of setting up Rust the way you would set up a professional kitchen before cooking your first meal. Rustup is the manager who hires and schedules chefs, where each chef is a toolchain version such as stable 1.90 or nightly. Rustc is the chef who actually cooks, turning raw ingredients in the form of .rs files into a finished dish in the form of a binary. Cargo is the head chef plus the inventory system combined: it creates the recipe layout with cargo new, buys exact ingredients listed in Cargo.toml, runs taste tests with cargo test, keeps the kitchen tidy with cargo fmt, inspects quality with cargo clippy, and writes the menu with cargo doc. You do the one-time kitchen setup correctly, then you cook every day with the same five commands and nothing surprises you.
Every Rust beginner hits the same wall in the first hour. The install looks done, then cargo build fails with linker cc not found, or rustc --version reports 1.78 when the tutorial needs edition 2024 features. You've followed three guides and you've got three toolchains fighting over one PATH. It's not your fault. Rust's setup has more moving parts than most languages, and the defaults don't explain themselves.
That confusion costs real time. A new hire burns half a day toggling between system packages, rustup shims, and VS Code errors that say rust-analyzer needs a workspace. A team without a pinned toolchain spends its Fridays debugging CI failures that pass on laptops. You'll avoid all of that if you treat setup as a contract: one installer in rustup, one toolchain file per repo, one Cargo.toml that declares edition and minimum Rust version explicitly.
Here's the good news: the whole workflow fits in your head once you see the layers. Rustup installs and switches compilers. Cargo scaffolds, builds, tests, formats, lints, and documents. Rustc does the compiling underneath, and you'll rarely call it directly. Get those boundaries straight and every error message starts making sense.
Don't expect theory without practice here. You'll run rustup toolchain commands, scaffold a binary with cargo new, push it through check, build, run, test, fmt, clippy, and doc, then wire it into VS Code with rust-analyzer. You'll also fix the classic C-linker failure on Linux, macOS, and Windows so it never blocks you again.
By the end you'll have a repeatable setup you can trust. A fresh laptop goes from zero to tested binary in 30 minutes. An old project builds without mystery rebuilds. Your editor, your terminal, and your CI all agree on the same compiler. That's what we're building together.
Rustup Channels: Stable, Beta, and Nightly Without the Footguns
Rustup looks like a simple installer until you realize it controls which compiler every shell, editor, and CI job sees. Channels are release tracks. Stable ships every six weeks with guarantees you can rely on. Beta previews the next stable so you can catch regressions early. Nightly rebuilds from main every night with experimental flags behind -Z. Most developers should live on stable and visit nightly in a disposable container, not in the repo they ship from. That single habit prevents a whole class of cannot-reproduce failures where code compiles on one laptop and breaks on every other machine.
Installation starts with the official script and ends with PATH verification, not with the script alone. Run the rustup installer, let it append $HOME/.cargo/bin to your shell profile, then open a fresh terminal and prove the shims resolve. rustup show tells you the active toolchain and its origin, whether it came from a default, a directory override, or a rust-toolchain.toml file. rustup toolchain list shows everything installed. When those two disagree with what your editor reports, trust rustup show first because editors inherit whatever terminal launched them, including stale PATH entries from a Homebrew Rust or an old system package.
Overrides are powerful and invisible, which makes them dangerous. rustup default stable sets your global compiler. rustup override set nightly in one directory pins that directory to nightly without touching any file Git tracks. That means a teammate cloning the repo gets stable while you build nightly, and neither of you sees a diff. Prefer rust-toolchain.toml for anything the team must share, and reserve overrides for ten-minute experiments you intend to throw away. Run rustup override list monthly and delete anything you no longer need.
Components ride along with channels. rustfmt, clippy, and rust-docs install per toolchain, so updating stable without adding components leaves cargo fmt broken with component not installed errors. Install them explicitly with rustup component add rustfmt clippy rust-docs, and verify with rustup component list --installed. Targets work the same way for cross-compilation: rustup target add wasm32-unknown-unknown or x86_64-unknown-linux-musl adds the standard library for that platform without disturbing your host builds. Keep host builds on stable 1.90 and isolate cross targets in their own checkouts if nightly is involved.
Updates deserve the same discipline as dependency bumps. rustup update refreshes every installed toolchain, which can move stable from 1.90.0 to 1.91.0 overnight and invalidate your build cache. That invalidation is correct behavior because fingerprints include the compiler version, but it surprises teams that expected a 40-second incremental build and got a 25-minute cold build. Pin releases with rust-toolchain.toml set to channel 1.90.0, update deliberately with rustup toolchain install stable --profile complete, and log rustc --version at the top of every CI run. You get preview access when you want it and reproducible builds when you need them.Offline and enterprise installs deserve a plan before you need one. Download rustup-init for your platform once, mirror the toolchain with rustup toolchain link for air-gapped hosts, and keep a dated copy of the installer beside your runbook. Proxy users should set RUSTUP_DIST_SERVER and unset it after the sync so later updates hit the official endpoint. Record the channel date alongside the version because nightly regressions map to dates, not just numbers. A team that rehearses the offline path finishes machine rebuilds in 15 minutes while others wait on a network they cannot reach.
Profiles trim install size without surprise. Choose minimal for CI images that only compile, default for laptops that need docs and analysis, and complete only when you cross-compile daily. Verify savings with du -sh ~/.rustup/toolchains after each profile so the tradeoff stays visible. Minimal saves roughly 600 MB per toolchain, which matters when runners cache 4 images. Pair the profile choice with CARGO_NET_RETRY and CARGO_NET_TIMEOUT so flaky mirrors fail fast instead of hanging 10-minute jobs.Document the toolchain decision where newcomers look first. A ten-line TOOLCHAIN.md naming the channel, the pin file, and the verify commands answers setup questions before they become tickets. Link it from the readme and the onboarding checklist. Teams with that note onboard juniors in 30 minutes; teams without it answer the same three questions every month.
Verify rustc 1.90: Prove Your Compiler Before You Trust It
Version numbers in Rust are load-bearing. Edition 2024 features, new standard library APIs, and recent diagnostic improvements all gate on the compiler you actually invoke, not the one you think you installed. Rust 1.90 stabilized the pieces this guide relies on, including edition 2024 behavior across the toolchain. Running rustc --version takes half a second and answers the only question that matters: which compiler will read my code today. Make it the first command in every setup checklist, every bug report template, and every CI log header.
The number you see must be traced to its source. which rustc shows the shim path, usually $HOME/.cargo/bin/rustc, while rustup show reveals whether that shim resolves through a default, an override, or a toolchain file. A common surprise is /usr/bin/rustc from an OS package shadowing the rustup shim because PATH order puts /usr/bin first. Another is an IDE launched from Finder or the Start menu inheriting a stale PATH without $HOME/.cargo/bin at all. When rustc --version and rust-analyzer disagree, fix PATH once at the shell profile level instead of patching each tool.
Pinning turns a passing check into a lasting guarantee. rust-toolchain.toml with channel = "1.90.0" plus components rustfmt and clippy makes every checkout converge to the same compiler on first cargo invocation. The rust-version field in Cargo.toml plays a different role: it declares the minimum compiler your crate supports so cargo warns downstream users before they hit obscure errors. Set both. The toolchain file protects your team today, rust-version protects your consumers tomorrow, and together they cut toolchain drift tickets by roughly 80% in teams that adopt them.
Verification in CI must be explicit because runners start cold. Log rustc --version and cargo --version as the first step, fail fast when the hash mismatches the cache key, and store the compiler version as a build artifact alongside your binary. A team that skips this step discovers drift through 40-minute rebuilds and nightly-only lint failures. A team that logs it diagnoses the same problem in 30 seconds by diffing two version strings. The cost is three lines of YAML and the payoff is every cache hit you keep.
Treat upgrades as deliberate events. Moving from 1.90.0 to 1.91.0 changes fingerprints, rebuilds dependencies, and can surface new warnings under -D warnings. Schedule the bump, run cargo update -p sparingly, rebuild with cargo check --workspace first for speed, then promote to full builds and tests. Keep the previous toolchain installed with rustup toolchain list until the new one proves itself in CI. Rollback is then one line in rust-toolchain.toml instead of a Friday-night reinstall.Diagnostics improve with the compiler, so old toolchains cost comprehension as well as features. Rust 1.90 points at the exact argument that mismatched, suggests the correct borrow shape, and links the error index directly. Developers on 1.78 see shorter messages that hide the fix two frames down the stack. That gap shows up in onboarding time: newcomers on current stable resolve borrow errors in minutes while those on stale images file tickets. Keep rustc current and the compiler teaches while it rejects.
Make version proof automatic rather than manual. Add a 5-line shell step that asserts rustc --version starts with 1.90 and fails the job otherwise. Store the full rustc -vV output including host triple and LLVM version as an artifact for post-incident review. When a future regression arrives, you will diff two version blocks instead of guessing which runner moved. Automation turns a half-second check into a permanent guarantee.Teach version triage as a skill, not a chore. When a snippet fails, compare its edition tag against your manifest before editing code. Old answers assume 2021 rules that 2024 tightened. A two-minute edition check resolves half of copy-paste failures without touching logic. Make it step one in every debugging runbook your team writes.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
cargo new to First Run: Scaffold a Binary You Can Keep
Cargo new creates more than files. It declares a package identity, wires edition and resolver defaults, initializes a Git repo, and lays out src/main.rs so cargo run works in under a minute. The binary template differs from the library template in one decisive way: binaries get src/main.rs with fn main while libraries get src/lib.rs with unit test scaffolding. Choose the binary when you ship an executable, the library when others will depend on your code, and a workspace when the project will grow past one crate. Getting this choice right on day one saves a restructuring pass in month three.
Names and paths carry conventions that tooling assumes. Package names use kebab-case such as order-service, while the binary target converts dashes to underscores automatically. Keep the checkout directory equal to the package name so cargo run --bin lookups and editor tasks resolve without extra flags. Run cargo new order-service --bin --vcs git, then inspect Cargo.toml before building. The generated file sets edition, version, and resolver behavior that every later command inherits. Reading those ten lines now prevents confusion when dependency features behave unexpectedly later.
The first run should exercise the full loop, not just compilation. cargo check validates types in seconds, cargo run builds and executes in one step, and cargo run --quiet keeps output clean once you trust the build. Pass arguments after a double dash such as cargo run -- --port 8080 so Cargo forwards them to your binary instead of consuming them. Set RUST_LOG or application flags through the same mechanism you will use in production, because habits formed on the first binary persist across every service you build.
Editors and version control plug in at this stage. Open the folder containing Cargo.toml, not a parent directory, or rust-analyzer will report a missing workspace root. Commit Cargo.toml and Cargo.lock for binaries so every checkout resolves identical dependencies. Add target/ to .gitignore on day one; that directory grows to gigabytes and never belongs in review. A clean scaffold with committed manifests plus ignored artifacts is the difference between onboarding in 10 minutes and debugging phantom diffs for an afternoon.
Measure the baseline before adding code. Record cold build time with time cargo build, warm check time with time cargo check, and binary size with ls -lh target/debug/order-service. Those three numbers become your regression tripwire. When a dependency doubles build time or triples artifact size, you will know exactly which commit caused it because you measured the empty project first. Senior engineers do this instinctively; beginners who adopt it look senior within weeks.Templates scale through cargo-generate once the default scaffold feels thin. Install the subcommand, clone an internal template with logging, config, and CI wired, then generate services with one command instead of copying directories. Templates enforce the layout decisions this section describes: binary versus library split, ignored target directory, committed lockfile, and baseline metrics. New repositories start compliant and reviewers stop commenting on scaffolding.
Binary targets multiply once CLIs grow subcommands. Add [[bin]] entries for admin tools beside src/main.rs, keep shared logic in src/lib.rs so both bins and integration tests import it, and run cargo run --bin admin for the side tool. This split keeps main small, makes logic testable without spawning processes, and mirrors how production services separate serving from maintenance. Plan the split at 500 lines, not 5,000.Standardize names across services so scripts compose. Prefix binaries with the team slug, keep crate names short, and mirror repo names exactly. Automation that loops over checkouts with cargo check --workspace depends on that predictability. Naming discipline pays compound interest across every script you will ever write.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
cargo check, build, and run: The Fast Loop That Saves Hours
Cargo check is the command you will run fifty times a day. It runs name resolution, borrow checking, and type checking without producing machine code, which makes it three to five times faster than cargo build on typical services. Bind it to save in your editor and run it in CI as the first gate. Reserve cargo build for moments when you need an artifact to execute, profile, or ship. Teams that internalize this split keep sub-second feedback while typing and pay full compile cost only when it buys them something.
Build profiles control the tradeoff between speed and optimization. Dev profile prioritizes incremental compilation and debug info; release profile enables opt-level 3, link-time optimization options, and stripped symbols for artifacts that run 10 to 80 times faster on hot loops. Never benchmark a debug binary and never ship one to production. Run cargo build --release for anything customers touch, and record both artifact sizes because release binaries with debug = true can bloat by 40% while aiding crash triage. Choose deliberately per invocation instead of accepting whatever the last command used.
Feature flags and target selection multiply build variants. cargo build --features postgres,redis compiles only the backends you asked for, while --all-features pulls everything including code your binary never executes. Prefer explicit --features lists in CI to keep build graphs small and cache hit rates high. Use --target x86_64-unknown-linux-musl or wasm32-unknown-unknown only after installing the matching rustup target, or you will watch Cargo fail after downloading half the registry. Explicit beats exhaustive in build matrices.
Verbosity and parallelism are your diagnostic levers. CARGO_BUILD_JOBS controls codegen units in parallel; leaving it at default matches CPU count and suits most laptops, while CI runners with 2 vCPUs sometimes benefit from -j 4 with sccache. cargo build --verbose exposes rustc invocations, linker lines, and feature unification when builds behave strangely. Pipe that output to a file because 2,000-line invocations scroll past in seconds. Learn --offline and --frozen now: --offline uses cached crates without network, --frozen refuses to touch Cargo.lock, and both belong in locked-down pipelines.
Running binaries through Cargo differs from running them directly. cargo run rebuilds when sources change, forwards arguments after --, and sets the working directory to the package root. That convenience hides a trap: environment variables and file paths that work under cargo run break when operations runs ./target/release/app from another directory. Test both paths before declaring victory. Execute target/debug/app directly at least once per project so relative paths, config discovery, and asset loading prove themselves outside Cargo's helpful cwd.Caching layers decide whether check stays instant. sccache wraps rustc and shares compiled deps across checkouts, cutting warm rebuilds by 40 to 60% on large graphs. Configure RUSTC_WRAPPER=sccache plus a shared bucket, verify with sccache --show-stats, and watch cache hits climb past 80% within a week. Without shared caching each of 10 developers recompiles the same 300 crates daily, burning hours of CPU that one upload would have saved.
Incremental compilation has knobs worth knowing. The dev profile enables incremental by default, splitting codegen units for parallelism. Setting codegen-units to 256 speeds clean builds on 16-core machines while slowing single-file rebuilds slightly. Measure both directions with hyperfine across three runs before committing the change. Blind tuning helps benchmarks and hurts iteration; measured tuning helps both.Profile continuous benchmarking from the first release binary. Record cargo build --release time plus artifact size per tag in a TRACKING.md file. When either jumps 20%, bisect that week instead of accepting the new normal. Baselines turn performance drift into visible, actionable diffs.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
Cargo.toml in 2024: Edition, rust-version, and Dependencies That Age Well
Cargo.toml is the contract every tool reads. The package section names the crate, versions it with semantic versioning, and declares edition = "2024" to select language semantics plus rust-version = "1.90" to declare minimum compiler support. Those two lines look trivial and control everything from which syntax compiles to whether downstream users get a clear error or a cryptic failure. Set them on day one and never leave them at scaffold defaults without a conscious choice. Reviewers should flag any pull request that bumps dependencies without considering whether rust-version still holds.
Edition 2024 changes rules you can feel. It tightens temporary lifetime capture, adjusts match ergonomics, and aligns unsafe binder behavior with the reference. Code written for edition 2021 compiles under 2024 in most cases, but the reverse fails wherever new keywords or resolver rules apply. That asymmetry matters when you copy snippets from older tutorials: a 2021 example that compiled cleanly can error under 2024 with eye-catching diagnostics about never patterns or gen blocks. Read the diagnostic, check the edition gate in the error index, and port the snippet instead of downgrading the crate.
Dependencies split into three scopes with different lifetimes. Normal dependencies ship in your binary. Dev-dependencies compile only for tests, examples, and benches, which keeps release graphs lean. Build-dependencies run at compile time for codegen or asset preparation. Misplacing a heavy crate such as tokio-test utilities under normal dependencies bloats every consumer. Default features add another axis: most crates enable default = true with batteries included, and disabling them with default-features = false plus explicit features trims compile time by 20 to 50% on large graphs.
Cargo.lock records exact resolution and belongs in Git for binaries. It turns it worked on my machine into it works on every machine by pinning all 300 transitive crates to tested revisions. Libraries traditionally gitignore the lockfile because consumers resolve independently, but binaries and services must commit it. Run cargo update -p serde to bump one crate deliberately, cargo update for a full refresh before a release candidate, and cargo tree -i serde@1.0.210 to explain why a version was selected. Deliberate bumps beat surprise upgrades.
Profiles and workspaces scale these choices. A [profile.release] section with lto = true, codegen-units = 1, and strip = true trades 2x build time for 15 to 30% smaller and faster binaries. A [profile.dev] with opt-level = 0 keeps iteration instant. Workspaces share one lockfile and one target directory across crates, which cuts redundant downloads and lets cargo build --workspace compile 14 crates with one resolution. Get the manifest right in a single crate and workspaces become a straightforward extension instead of a migration headache.Version requirements use caret semantics by default, so serde 1.0 means compatible 1.x, not exactly 1.0.0. Use =1.0.210 only for hotfixes, >= with upper bounds rarely, and wildcard patch pins like 1.90.* solely for toolchain-adjacent crates. Document any pin with a comment naming the incident that motivated it. Future updaters need the reason, not just the constraint, or they will loosen the bound and rediscover the bug.
Patch and replace sections rescue you during upstream outages. A [patch.crates-io] entry pointing at a Git fork unblocks releases when a yanked version breaks resolution. Keep the patch narrow, file the upstream issue the same day, and remove the patch within one sprint. Permanent patches fork your dependency graph silently and confuse every new hire. Temporary patches with expiry dates are engineering; permanent ones are debt.Audit the dependency graph quarterly with cargo tree and cargo audit. Remove crates with single call sites, replace abandoned ones before they block edition bumps, and record each removal reason. A graph that shrinks deliberately stays fast; one that only grows eventually needs a painful purge.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
cargo test on Day One: Your First Green Suite in Minutes
Rust tests live beside the code they verify, which removes every excuse for skipping them. Unit tests sit in a #[cfg(test)] module at the bottom of each source file, integration tests live in tests/ and exercise the public API as external users see it, and doc tests compile every fenced example in your documentation. cargo test runs all three harnesses in one invocation with parallel threads by default. Writing the first assertion before the first feature sets the tone: this project proves its behavior instead of claiming it.
The harness flags shape how you debug. cargo test -- --nocapture shows println output that the harness captures by default, --test-threads=1 serializes tests that fight over ports or files, and test name filters such as cargo test parse_config run only matching cases. Learn those three on day one because the first flaky test arrives within a week. A test that passes alone and fails in parallel is sharing mutable state through a static or a temp file; serializing proves the hypothesis in seconds while you design the real fix.
Assertions deserve the same care as production code. assert_eq! with left and right values prints both sides on failure, assert!(cond, "context") explains the invariant under test, and #[should_panic(expected = "empty")] documents intentional failure modes. Prefer expect over unwrap in tests only when the message names the invariant, because a bare unwrap on line 88 tells the next engineer nothing about what broke. Name tests as behavior specifications such as rejects_negative_port rather than test1, and future readers will thank you during refactors.
Coverage and discipline turn green suites into trustworthy gates. cargo test --locked guarantees CI resolves the same graph developers used, preventing phantom passes from floated dependencies. Dev-dependencies such as tempfile, assert_cmd, and predicates belong in Cargo.toml under [dev-dependencies] so test helpers never bloat release binaries. Run the suite with warnings denied via RUSTFLAGS="-D warnings" cargo test --workspace periodically to catch dead code and unused imports before clippy does. A suite that stays warning-free stays fast to read.
Measure what the suite costs. Time a cold cargo test on a fresh checkout, time a warm rerun, and count tests with cargo test -- --list | wc -l. A project with 40 tests finishing in 9 seconds has a fundamentally different development rhythm than one with 400 tests taking 6 minutes. Split slow integration tests behind cargo test --test integration or a feature flag when they exceed 60 seconds, so the inner loop stays under 10 seconds. Speed is a feature of test suites; slow suites get skipped, then deleted, then missed during the incident they would have prevented.Integration tests in tests/ import your crate as outsiders do, which catches visibility mistakes unit tests miss. Name files by behavior such as cli_startup.rs rather than test.rs, share fixtures through tests/common/mod.rs, and assert on exit codes plus stdout with assert_cmd. A CLI that exits 0 on invalid ports will pass unit tests and fail integration in seconds. That outside-in view is worth the extra directory.
Doc tests deserve the same rigor as any suite. Mark examples that need network or files with no_run so they compile without executing, use ellipsis for long output, and keep examples under 15 lines so readers absorb them. A doc example that drifts from the API fails cargo test just like any regression. Treat documentation builds as test runs and rot disappears.Name async tests clearly and gate slow ones behind features. Tokio tests need flavor attributes that serial runners misread, so document the runtime per file. A suite where every slow test declares itself stays trustworthy at 3 AM. Clarity in test headers prevents midnight misdiagnosis.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
cargo fmt, clippy, and doc: The Quality Gates Pros Never Skip
Formatting ends style debates before they start. cargo fmt rewrites every file to rustfmt rules in milliseconds, and cargo fmt --check verifies without writing so CI can reject unformatted pull requests in 5 seconds. Run fmt before every commit, not during release week, because a 400-file reformat buried in a feature branch destroys blame history and review sanity. Configure your editor to format on save and the gate becomes invisible: code arrives formatted, CI confirms it, and nobody discusses brace placement ever again.
Clippy is the senior reviewer who never sleeps. cargo clippy --all-targets -- -D warnings surfaces 700-plus lints covering needless clones, redundant closures, suspicious arithmetic, and pedantic style. Fix the default lints first, adopt nursery or pedantic groups only when the team agrees, and allow specific lints with #[allow] plus a comment explaining why. A codebase that denies warnings in CI catches real bugs: clippy has flagged reversed comparison operands, needless range loops, and format strings that printed Debug instead of Display in production logs.
Documentation is a build artifact, not a chore. cargo doc --no-deps renders your crate's public API from /// comments into searchable HTML under target/doc, while cargo doc --open previews it locally. Doc tests embedded in those comments compile and run under cargo test, which means examples double as regression tests. Write /// with # Examples sections on every public function, link types with [MyType] intra-doc syntax, and add //! crate overviews in lib.rs. Broken links fail loudly with RUSTDOCFLAGS="-D warnings" cargo doc, so gate them in CI.
The three gates compose into a two-minute pre-push ritual. Run cargo fmt --check for style, cargo clippy --all-targets -- -D warnings for lints, RUSTDOCFLAGS="-D warnings" cargo doc --no-deps for docs, then cargo test --locked for behavior. That sequence catches 90% of review nits before a human reads the diff. Encode it as a justfile recipe, a cargo alias in .cargo/config.toml, or a pre-push hook. Rituals beat reminders because they run the same way when you are tired, rushed, or onboarding.
Enforcement must live in CI, not in wiki pages. A fmt job that runs cargo fmt --check --all, a lint job with clippy deny warnings, and a doc job with -D warnings each take under 2 minutes on warm caches and block merges independently. Keep them separate so a formatting nit does not hide behind a 12-minute test run. Teams that split the gates fix violations in 3 minutes on average; teams that bundle them wait 20 minutes to learn a brace was misplaced. Fast, isolated feedback is the whole point.Custom rustfmt options belong in rustfmt.toml and deserve restraint. Set max_width to 100 only when the team agrees, keep imports_granularity at default until the codebase exceeds 50 modules, and never reformat unrelated files in feature branches. Review the formatting diff separately from logic changes so reviewers trust both. Consistency within the repo beats alignment with any external style.
Clippy configuration lives in clippy.toml beside lint levels in Cargo.toml. Set cognitive-complexity-threshold to 30 for services with branching handlers, allow-list generated code paths, and document each allowance with the tracking issue. Run cargo clippy --fix --allow-dirty sparingly and always review its diff because automated fixes can restructure borrows. Lints guide judgment; they never replace it.Publish the alias set your team actually uses in .cargo/config.toml and the readme. Shortcuts like cargo xcheck for workspace checks spread good habits faster than wiki pages. Review aliases yearly and delete stale ones. Shared shortcuts encode team judgment into muscle memory.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded. Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
VS Code and rust-analyzer: An Editor That Actually Understands Rust
VS Code plus rust-analyzer is the standard Rust editing environment for good reason: it compiles your code incrementally as you type and surfaces borrow errors before you run cargo check. Install the rust-lang.rust-analyzer extension from the marketplace, open the crate root containing Cargo.toml, and verify the status bar shows the toolchain version. When completions, go-to-definition, and inline errors appear within seconds, the wiring is correct. When the editor shows unresolved import on code that cargo check accepts, the wiring is wrong and the fix is structural, not a reinstall.
Settings determine whether the experience feels instant or broken. Set rust-analyzer.check.command to check for speed or clippy for deeper lints, enable rust-analyzer.cargo.allTargets so tests and benches resolve, and point rust-analyzer.server.path at the rustup shim only when debugging the extension itself. Keep files.exclude for target/ so search stays fast across 50,000 generated files. A five-line settings.json tuned once pays back every hour you edit: diagnostics arrive in under a second on warm checkouts instead of timing out.
Workspace layout decides success more than any setting. Opening /repo when the crate lives in /repo/crates/api leaves rust-analyzer guessing at the workspace root and produces phantom errors. Open /repo only when Cargo.toml at its root declares [workspace] members; otherwise open the specific crate folder. Multi-root workspaces in one window work when each root has its own manifest. Reload the window after switching toolchains because the server caches the compiler path from launch time.
Debugging and running integrate through standard extensions. CodeLLDB with a launch.json that points at target/debug/order-service gives breakpoints, variable inspection, and call stacks without leaving the editor. Tasks that invoke cargo check --workspace --all-targets surface diagnostics in the Problems panel with click-to-jump. Format on save via editor.formatOnSave plus rust-analyzer as the default formatter keeps diffs clean. These three integrations turn the editor from a text box into a Rust workstation.
When diagnostics lie, diagnose the server before the code. Run rust-analyzer: Status in the command palette to see the workspace roots and cargo metadata time. Delete a corrupt target/.rust-analyzer lock only when metadata hangs, never as routine maintenance. Compare cargo check output in the terminal with editor squiggles: agreement means the code needs fixing, disagreement means the server needs a reload. That 30-second triage resolves 9 out of 10 editor tickets without touching the toolchain.Multi-crate workspaces need explicit linkedProjects when the root manifest stays minimal. List each member path so the server loads metadata per crate instead of guessing. Exclude generated protobuf or OpenAPI output from watching to keep file handles under OS limits on large repos. These two settings prevent the spinning progress indicator that developers mistake for a hang.
Remote development over SSH works well when the toolchain lives on the remote host. Install rust-analyzer remotely, open the remote crate root, and let checking run where target/ already warms. Avoid syncing target/ back over the network; it spans gigabytes and invalidates on every host switch. Remote-first layout keeps laptop fans quiet and builds server-fast.Keep one golden laptop image or container with the full toolchain warm. New hires pull it and run cargo check in 2 minutes instead of installing linkers all afternoon. Refresh the image monthly with current stable. Golden images turn setup from a day into a coffee break.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded. Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
Fix the C Linker: build-essential, Xcode CLT, Targets, and cargo clean
The most common Rust setup failure is not Rust at all. rustc lowers code through LLVM then calls a C linker, usually cc, to produce the final binary. Fresh Docker images, minimal cloud VMs, and new Macs often lack that linker, so cargo build fails with linker cc not found or linking with cc failed after compiling 200 crates successfully. The error looks catastrophic and the fix takes 2 minutes once you recognize the pattern: install the platform toolchain, verify cc resolves, and rebuild only what the missing linker blocked.
Linux needs build-essential plus pkg-config. On Debian and Ubuntu run sudo apt update followed by sudo apt install -y build-essential pkg-config libssl-dev, on Fedora sudo dnf install -y gcc pkg-config openssl-devel, on Arch sudo pacman -S base-devel. Verify with which cc, cc --version, and pkg-config --version before rerunning Cargo. Containers should bake these into the Dockerfile base layer so every cargo build starts warm; installing them per job wastes 90 seconds on each of 100 daily runs. That single Dockerfile line eliminates the top setup ticket for Linux teams.
macOS needs Xcode Command Line Tools, not full Xcode. Run xcode-select --install, accept the dialog, then verify with xcode-select -p and cc --version. After major macOS upgrades rerun sudo xcode-select --reset because the tools path drifts and the linker error returns with a different message about missing SDK headers. Apple Silicon versus Intel rarely matters for the linker itself, but the target triple does: aarch64-apple-darwin is native on M-series, x86_64-apple-darwin needs Rosetta only when you deliberately cross-compile. Match rustc --version --verbose host output to your hardware before adding targets.
Windows needs the MSVC build tools or a MinGW alternative. Install Visual Studio Build Tools with the Desktop development with C++ workload, then use the stable-x86_64-pc-windows-msvc default toolchain. The gnu alternative with MinGW works but mixes CRT behavior in ways that surprise native dependency builds. Verify with where link.exe and rustc --version --verbose. Windows developers who skip the C++ workload see linking with link.exe failed on every build; the fix is always the installer checkbox, never a Rust reinstall.
Targets and cargo clean finish the picture. rustup target list shows installed triples, rustup target add fills gaps for musl or wasm, and rustup show confirms the active host. Reach for cargo clean only as a scalpel: cargo clean -p broken-crate clears one fingerprint, while bare cargo clean wipes gigabytes of cache and forces a 20-minute rebuild. When builds fail after a toolchain or CLT update, try cargo check first, then per-package clean, then full clean as the last resort. In that order you preserve the cache that saves 70% of iteration time.Musl static builds add a second linker path worth mastering. Install musl-tools, add the x86_64-unknown-linux-musl target, and set RUSTFLAGS to link statically for scratch containers. Verify with file target/.../order-service reporting statically linked and ldd confirming no dynamic deps. Static binaries grow 20% larger yet deploy to distroless images without library drift. Choose static for minimal images and dynamic for hosts you control.
Windows GNU versus MSVC deserves a deliberate pick per repo. MSVC matches native SDKs and links fastest with link.exe. GNU with MinGW suits cross builds from Linux CI but needs matching libgcc runtimes. Record the choice in the readme so contributors install the right workload first. Mixed toolchains in one team produce linker tickets weekly; one declared toolchain produces none.Sign release tags and verify them in CI before building artifacts. A signed tag plus locked build proves what shipped and from which source. Store signatures beside hashes in the release notes. Provenance questions during audits then take minutes, not days.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
Ship It: Layout, CI, and Docs That Survive Real Teams
A shippable project layout separates what you edit from what you generate. Keep src/main.rs or src/lib.rs plus modules under src/, integration tests in tests/, examples in examples/, and benches in benches/. Put workspace manifests at the root with members = ["crates/*"] when the repo grows past one crate. Generated output lives in target/ and stays out of Git. This layout is not aesthetic preference; every tool from rust-analyzer to docs.rs assumes it, and deviating means fighting defaults on every command.
CI should verify the same gates you run locally, in the same order, on the same compiler. Start jobs from rust:1.90-slim or dtolnay/rust-toolchain@stable with rust-toolchain.toml checked in, log rustc and cargo versions first, then run fmt check, clippy deny warnings, doc with -D warnings, and test locked as parallel jobs. Cache ~/.cargo/registry, ~/.cargo/git, and target/ with keys that hash rust-toolchain.toml plus Cargo.lock. A warm pipeline finishes in 4 to 6 minutes; a cold one takes 25. That gap is entirely cache keys and toolchain pins, not code size.
Release discipline turns binaries into products. Bump version in Cargo.toml deliberately, update CHANGELOG.md in the same commit, tag with git tag v0.3.0, and build with cargo build --release --locked. Record artifact hashes with sha256sum target/release/order-service and store them beside the binary. Strip symbols only when crash triage tooling supports it; stripped binaries are 30% smaller but produce less useful backtraces. Ship debug info to your symbol server and stripped binaries to customers when both matter.
Documentation closes the loop between code and users. Crate-level //! overviews, per-item /// with examples, and cargo doc --no-deps output published to GitHub Pages or docs.rs give every release a readable reference. Doc tests run under cargo test, so examples cannot rot without failing the suite. Enforce RUSTDOCFLAGS="-D warnings" cargo doc in CI and broken intra-doc links block the pull request instead of embarrassing the release. Teams that treat docs as artifacts ship fewer support tickets because users answer their own questions.
The final checklist fits on an index card. rustup show reports 1.90.0 from the toolchain file, cargo fmt --check passes, cargo clippy denies warnings, cargo test --locked is green, cargo doc builds without warnings, and cargo build --release produces a hashed artifact. Run that sequence before every tag and onboarding stays at 30 minutes, CI stays under 6 minutes warm, and releases stop slipping on Fridays. Setup done right disappears; you think about features while the toolchain quietly holds its contract.Feature flags gate risky rollout paths without branching. Ship new parsers behind --features preview-parser, enable them in staging first, and promote by flipping the deploy flag rather than merging code. Measure binary size per flag combination because two large backends can double artifacts. Flags turn deploys into decisions instead of events.
Supply-chain hygiene finishes the release story. Run cargo audit in CI to flag CVEs across the 400-crate graph, cargo deny to enforce license allow-lists, and cargo outdated monthly to schedule deliberate bumps. Pin each tool version so audits reproduce. A 10-minute audit job replaces the quarterly scramble when a critical advisory lands on a Friday.Rotate a setup owner each quarter to keep the template honest. The owner rebuilds from scratch, updates pins, and prunes dead docs. Fresh eyes catch the drift that daily users stop noticing. Ownership rotation keeps setup guides alive instead of archived.Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded. Keep a dated log of every toolchain change with build time before and after. A five-line entry per bump turns folklore into data: channel version, cache hit rate, cold versus warm minutes, and the reason for the move. Future upgrades then reference evidence instead of memory, and rollbacks take one line because the prior state stays recorded.
Friday 6 PM: Nightly Toolchain Shipped to 14 Crates, CI Burned 3 Hours
- Pin the toolchain in the repo with rust-toolchain.toml and rust-version, then log rustc --version on every CI run. An invisible rustup override can recompile 14 crates and burn 3 hours while looking like a dependency outage.
- Never mix nightly experiments with stable release paths. Nightly flags, preview lints, and different codegen hashes poison shared caches and produce binaries you cannot reproduce on stable 1.90.
- Make setup self-verifying: fmt check, clippy deny warnings, and locked test runs before artifacts. A 2-minute gate catches toolchain drift before it becomes a Friday-evening release slip.
Type] intra-doc links instead of raw URLs, and run cargo doc --no-deps to isolate your crate from 300 dependency pages. Gate releases with cargo doc --no-deps --workspace in CI so a broken link blocks the pull request, not the release train.| File | Command / Code | Purpose |
|---|---|---|
| terminal | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y | Rustup Channels |
| terminal | which rustc | Verify rustc 1.90 |
| terminal | cargo new order-service --bin --vcs git | cargo new to First Run |
| terminal | cargo check | cargo check, build, and run |
| terminal | cat Cargo.toml | Cargo.toml in 2024 |
| src | fn parse_port(raw: &str) -> Result<u16, String> { | cargo test on Day One |
| terminal | cargo fmt --all | cargo fmt, clippy, and doc |
| terminal | code --list-extensions | grep -i rust | VS Code and rust-analyzer |
| terminal | which cc; cc --version | Fix the C Linker |
| src | pub fn parse_port(raw: &str) -> Result<u16, String> { | Ship It |
Key takeaways
Common mistakes to avoid
7 patternsInstalling Rust from OS packages instead of rustup
Leaving directory overrides that shadow the pinned toolchain
Running cargo build for every keystroke instead of cargo check
Forgetting the C linker on fresh images
Ignoring Cargo.lock for binaries or skipping --locked in CI
Opening the wrong folder so rust-analyzer reports phantom errors
Skipping fmt, clippy, and doc gates until release week
Interview Questions on This Topic
What do rustup, rustc, and Cargo each own, and why does that split matter?
Frequently Asked Questions
20+ years shipping production backend systems. Everything here is grounded in real deployments.
That's Setup. Mark it forged?
27 min read · try the examples if you haven't