Cargo's New Build Dir Layout Finally Separates What You Built from How You Built It
Source: rust
The target/debug/ directory has always been a lie. What looks like a folder of build outputs is actually a mingling of two completely different categories of files: the final artifacts you care about (your binaries, your libraries) and the enormous pile of intermediate debris that Cargo needs to do its job (incremental compilation data, .fingerprint/ files, .rmeta metadata, build script outputs). These two things have lived together since Cargo’s early days, and the consequences have been accumulating ever since.
The Rust team is now calling for testing of -Zbuild-dir-new-layout, a nightly flag that changes the internal structure of the build directory in a way that finally separates these concerns. If it stabilizes, it will clean up a set of problems that have been quietly frustrating CI engineers, IDE authors, and packaging tool maintainers for years.
Why the Target Directory Became a Problem
The current layout looks roughly like this:
target/debug/
├── mybinary # final output
├── libmycrate.rlib # final output
├── deps/ # intermediate .rlib, .rmeta files for all dependencies
├── .fingerprint/ # incremental rebuild metadata
├── incremental/ # LLVM IR, salsa data (can grow to tens of GB)
├── build/ # build script compiled binaries and their outputs
└── examples/ # compiled examples
Tools that want to find “the binary this project built” have to write defensive code. A naive find target/debug -maxdepth 1 -type f -executable will surface build-script binaries that leak out from build/ subdirectories, plus test binaries, plus proc macro binaries that are themselves executables. A Docker multi-stage build that tries to copy target/debug/ to cache between CI runs copies gigabytes of incremental compilation data along with the handful of files that actually matter.
The problem compounds in workspaces and cross-compilation. For a cross-compiled target, you get target/aarch64-unknown-linux-gnu/debug/ with the same mixed structure, and now you have two massive incremental/ directories to manage instead of one. Tools like cargo-deb and cargo-bundle have to apply increasingly complex heuristics to distinguish “the executable the user wants to ship” from “a build script that happens to be compiled to the same directory.”
The build-dir Concept
The fix proposed in Cargo tracking issue #16147 and partially shipped in Cargo 1.91 is the build-dir configuration option. It lets you separate where Cargo stores intermediate build artifacts from where it stores final outputs:
# .cargo/config.toml
[build]
build-dir = "/fast/nvme/cargo-build"
Or via the environment variable CARGO_BUILD_BUILD_DIR. With this set, intermediate artifacts go to the specified path while the final outputs remain in target/. The practical uses are immediate: point build-dir at a tmpfs for speed, at a CI-optimized cache path, or at any location separate from the outputs you intend to deploy or archive.
You can verify this is working independently of the new layout by running with CARGO_BUILD_BUILD_DIR=build set. If something breaks at that step alone, you have a separate problem from the layout change itself, and that is useful diagnostic information.
What Layout v2 Actually Changes
The build-dir feature alone is necessary but not sufficient. Even with intermediate artifacts redirected elsewhere, the directory structure within the build path still follows the old mixed layout. The new layout v2, enabled with -Zbuild-dir-new-layout on nightly builds from 2026-03-10 or later, restructures things so the output directory genuinely contains only final outputs:
# New layout
target/
├── debug/
│ ├── mybinary # final binary, same path as before
│ └── libmycrate.rlib # final library, same path as before
build/ # or your configured build-dir
└── debug/
├── deps/
├── .fingerprint/
├── incremental/
└── build/
The critical design decision is that final outputs stay at the same paths. target/debug/mybinary is still target/debug/mybinary. Tools that look for binaries there will continue to find them. What changes is that deps/, .fingerprint/, incremental/, and build/ no longer appear inside target/debug/. You can scan target/debug/ and find only final products, without filtering.
This also means the target/ directory can be kept small for caching and deployment purposes. The multi-gigabyte incremental/ data lives separately in build-dir, and CI pipelines that only need to cache final outputs can do so without capturing build state.
The CARGO_BIN_EXE_* Canary
The call-for-testing post specifically mentions CARGO_BIN_EXE_*, and this is worth explaining because it is where a significant portion of ecosystem breakage concentrates.
When Cargo runs integration tests (tests in the tests/ directory), it sets environment variables named CARGO_BIN_EXE_<name> for each binary in the workspace. These expand to the absolute path of the built binary. Integration tests use them like this:
#[test]
fn binary_produces_correct_output() {
let binary_path = env!("CARGO_BIN_EXE_myapp");
let output = std::process::Command::new(binary_path)
.arg("--version")
.output()
.unwrap();
assert!(output.status.success());
}
The env! macro captures the value at compile time. For tests that need the binary location at runtime, std::env::var_os("CARGO_BIN_EXE_myapp") is available from Cargo 1.94 onward. Both forms will continue to work under the new layout because the design explicitly preserves the output paths that CARGO_BIN_EXE_* points to.
The specific failure mode the post warns about is inferring a [[bin]] path from a [[test]] path. If your test binary lands at target/debug/deps/myapp-abc123, some tools strip the hash and navigate up to derive the final binary path. Under the new layout, test binaries and final binaries no longer share a directory tree, so that inference fails entirely. The correct fix is to use CARGO_BIN_EXE_* directly, with the path inference as a fallback for older Cargo versions.
What a Crater Run Misses
The Rust project ran a crater run before this call for testing. Crater is Rust’s large-scale ecosystem compatibility tool: it takes a proposed change, compiles and tests thousands of crates from crates.io, and reports regressions. It is genuinely useful for catching widespread breakage before a change ships.
But crater has structural limits. It runs the test suite for published crates, which means it cannot test:
- Internal CI scripts that shell out to
target/debug/<binary> - Dockerfile build recipes that hardcode paths into
COPYinstructions - Packaging tools like cargo-deb scanning the target directory for executables
- IDE integrations like rust-analyzer reading artifact paths from build metadata
- Build caching infrastructure like sccache that models which files are inputs versus outputs
- Any tool or process that isn’t expressed in the
[dev-dependencies]or[[test]]sections of a publishedCargo.toml
This gap between “what crates.io tests cover” and “what production build pipelines do” is wide enough to hide serious breakage. Crater tells you that the ecosystem’s published test suites still pass; it does not tell you whether the engineer on-call at 2am can still build the Docker image that ships your service.
How to Test It
With a nightly Cargo toolchain from 2026-03-10 or later, run your full build and test workflow with the flag applied:
cargo test -Zbuild-dir-new-layout
cargo build -Zbuild-dir-new-layout
cargo run -Zbuild-dir-new-layout
If you have release processes, CI scripts, or Dockerfiles that invoke Cargo, run those too. The goal is to exercise anything that touches target/ or a configured build-dir. Specifically, look for scripts that construct paths like target/debug/<something> manually, because those paths may now point into what the new layout considers the intermediate build area.
When you find a failure, the debugging path matters. First check whether the failure exists without -Zbuild-dir-new-layout but with CARGO_BUILD_BUILD_DIR=build set. If so, the issue predates the layout change and is in the build-dir routing itself. If the failure only appears with the layout flag, you have found something specific to the path restructuring, and that distinction is useful information for the tracking issue.
Report findings on the tracking issue. The team specifically wants to know about upstream tools that need updating to support both the old and new layouts simultaneously, which matters for any tool that needs to work across Cargo versions during the transition period.
The Broader Significance
The target/ directory is one of those places in the Cargo ecosystem where “internal implementation detail” and “public interface” have blurred together over time. No part of the layout was ever specified as stable, but the tooling ecosystem grew by reading paths that were never formally documented. The Cargo reference describes the directory as a build cache, not as a stable API, but the practical reality is that dozens of tools treat it like one.
The new layout v2 draws a cleaner line: the output directory contains outputs, the build directory contains build state. This is not a new idea in build systems; Bazel, Buck, and CMake have separated these concerns from the beginning. Getting there in Cargo without breaking the existing ecosystem requires careful migration work, which is exactly what the current nightly testing phase is designed to surface.
If your project uses target/debug/ in any way that Cargo does not directly control, now is the time to find out whether your assumptions still hold.