· 5 min read
Warnings are build policy
Cargo 1.97 can deny warnings for local packages without smuggling CI policy through RUSTFLAGS into every rustc invocation.
A Rust CI job that refuses warnings has had a wonderfully blunt spell available for years: RUSTFLAGS="-D warnings". It works, which is why you keep seeing it. The odd part is what the command says underneath: take a repository policy about diagnostics and inject a raw compiler argument into the environment.
Cargo 1.97 finally gives that policy a name of its own. Set build.warnings = "deny" in Cargo configuration, or CARGO_BUILD_WARNINGS=deny in CI, and Cargo raises adjustable warnings to errors for local packages. That sounds like a small convenience. I think the important bit is that Cargo now knows whose warnings you meant.
env: RUSTFLAGS: -D warningssteps: - run: cargo test --workspaceThe old spelling asks rustc for strictness. Cargo's own documentation says RUSTFLAGS is passed to every compiler invocation when you are building for the host without an explicit target, including build scripts and proc macros. Cargo even warns that these low-level flags can conflict with settings it manages itself. A warning policy should not need that escape hatch.
A compiler flag cannot see ownership#
The distinction Cargo can make is mundane and useful. Its new build.warnings setting changes lint warnings for local packages. Registry dependencies do not suddenly become your cleanup project, and non-lint diagnostics stay whatever they were. If several local crates warn, Cargo's docs point to --keep-going when you want the run to expose more than the first failing crate.
That ownership boundary already exists elsewhere in Cargo. The manifest documentation explains that lint settings on a package apply to that package, while Cargo suppresses ordinary lints from non-path dependencies with mechanisms such as --cap-lints. Build-script warnings follow a similar local bias: Cargo normally displays cargo::warning output for path dependencies you are actively working on and hides dependency noise unless you ask for very verbose output.
RUSTFLAGS cannot express any of that intent because it is an opaque string delivered below the package graph. Cargo's build-cache documentation points out that the variable is shared with every rustc invocation in the ordinary host build. I have not measured how much needless recompilation a CI-only -D warnings causes across real caches, and I would not pretend the cost is always large. The stranger part is simpler: Cargo has to carry a policy flag through compilers that cannot decide whether the crate in front of them belongs to you.
This problem has been sitting in public for a while. A 2017 Cargo issue about warning control shows people reaching for RUSTFLAGS to quiet development warnings and, in the other direction, asking for a clean way to turn warnings into CI errors without making dependencies part of the bargain. That workaround stayed in circulation for nine years before Cargo got a first-class build setting.
There are three different decisions here#
Rust projects already gained a first-class [lints] table in manifests. That solves a different problem. If unsafe_code = "forbid" is part of the package's contract, putting it in Cargo.toml makes sense because developers, CI, and downstream builds should all see the same choice. “CI fails on any warning” is often an environment rule instead: useful at the gate, irritating while you are halfway through a refactor.
| [lints] | build.warnings | RUSTFLAGS | |
|---|---|---|---|
| Lives in | Cargo.toml | .cargo/config.toml or CARGO_BUILD_WARNINGS | Environment or Cargo config |
| Controls | Named rustc and Clippy lint levels | The effective level of adjustable warnings | Raw rustc command-line flags |
| Scope | The package that declares or inherits them | Local packages in this build | Compiler invocations selected by Cargo's target mode |
| Good fit | Source policy that should travel with the crate | Environment policy such as a warning-free CI gate | Low-level compiler options with no first-class Cargo setting |
The new setting is deliberately coarse. It offers warn, allow, or deny for warnings as a class, leaving named lint policy to the manifest. I like the boundary. Cargo can enforce the build's tolerance while source files and [lints] keep describing which diagnostics are meaningful in the first place.
Cargo's own 1.97 changelog describes build.warnings as useful for enforcing a warning-free CI build and explicitly calls it a replacement for -Dwarnings. The feature became stable on July 9, 2026. Cargo's config reference marks 1.97 as the minimum version for this setting, so repositories supporting older Cargo cannot rely on it yet.
env: CARGO_BUILD_WARNINGS: denysteps: - run: cargo test --workspaceThe resulting diff is almost comically small. One environment variable disappears and another takes its place. The useful change is the label on the wire: CARGO_BUILD_WARNINGS=deny reaches Cargo as build policy, and Cargo can apply it only to the packages the build actually owns.