Repository navigation
Description
For design, see #14125 (comment)
For tracking the artifact-dir side of this, see #6790
Documentation: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-dir
Testing instructions: #14125 (comment)
FCP: #14125 (comment)
Implementation
- unstable build-dir support (Implemented
build.build-dirconfig option #15104) - build-dir templating (Added
build.build_dirtemplating support #15236) -
cargo metadatasupport (Addedbuild_directoryfield to cargo metadata output #15377) - Document
build_directorycargo metadatafield (docs(metadata): Added build_directory to cargo metadata documentation #15410) -
workspace-path-hashshould have symlinks resolved (Added symlink resolution forworkspace-path-hash#15400) - error on unmatched
{,}(Added validation for unmatched brackets in build-dir template #15414) - append
closest_msgto unknown variable error (and maybe list all variables if no match is found?) (Improved error message when build-dir template var is invalid #15418) - call for testing (Redefine
CARGO_TARGET_DIRto be only an artifacts directory #14125 (comment)) - stabilize build-dir
Stabilizing this would also resolve
Known issues:
Decisions
cargo cleanwill clean bothtarget-dirandbuild-dir- Skipping including a
CARGO_BUILD_DIRshortcut forCARGO_BUILD_BUILD_DIR(likeCARGO_TARGET_DIRis a shortcut forCARGO_BUILD_TARGET_DIR)
artifact-dir files:
- build
- binary executable for bin crates
- binary executable for examples
- depinfo files (
.dfiles) for third party build-system integrations (see https://github.com/rust-lang/cargo/blob/master/src/cargo/core/compiler/fingerprint/mod.rs#L194) - Cargo's
--timingsHTML report
cargo package's generated.cratefilescargo docoutput (html/css/js/etc)
build-dir files (intermediate artifacts, build state, caches):
- build
- "pre and non uplifted" binary executables. (ie. bins for
examplesthat contain the hash in the name, bins forbenches, proc macros, build scripts) - other depinfo files (generated by rustc, fingerprint, etc. See https://github.com/rust-lang/cargo/blob/master/src/cargo/core/compiler/fingerprint/mod.rs#L164)
- rlibs and debug info from dependencies
- build script
OUT_DIR - output from proc macros (previously stored in
target/build) - incremental build output from rustc
- fingerprint files used by Cargo for rebuild detection
- Cache of rustc invocations (
.rustc_info.json)
- "pre and non uplifted" binary executables. (ie. bins for
cargo package's scratchpad used for the verify stepCARGO_TARGET_TMPDIRfiles (see rational for this here)- reports generated from
cargo reportlike future-incompat-report
Open questions
- Are we good with the name
build-dir?- Yes, discussed on 2025-03-18
- Should we offer
CARGO_BUILD_DIRas a shortcut toCARGO_BUILD_BUILD_DIR, likeCARGO_TARGET_DIRis forCARGO_BUILD_TARGET_DIR? see Implementedbuild.build-dirconfig option #15104 (comment)- No, discussed on 2025-03-18
- End-users are most likely to set this globally in config
- Env variable is most likely in CI
- Length and "cleaner name" aren't priorities for the target audience, CI
- Should we stick with the name
{workspace-manifest-path-hash}and what should it include? Should we shorten to{workspace-hash}or even just{hash}?- We should drop
manifest, unsure about dropping eitherworkspaceorpath. Users may be confused if they don't realize the path is a part of the hash - Renamed to
workspace-path-hashin fix(build-dir): Renamed workspace-manifest-path-hash to workspace-path-hash #15334
- We should drop
-
Should we include the Cargo version indeferred{workspace-manifest-path-hash}so we get unique whole-target directories for easier cleanup (Garbage collect wholetarget/#13136) - Should we resolve symlinks in workspace manifest-path before generating
{workspace-manifest-path-hash}? See Addedbuild.build_dirtemplating support #15236 (comment)- The cargo team leans towards doing so right now
- As the hash is unspecified, we feel this is a two-way door
- Added symlink resolution for
workspace-path-hash#15400
- Should we keep erroring on unknown template variables? Should we start erroring on unmatch
{? See Addedbuild.build_dirtemplating support #15236 (comment)- Yes, we should error on unknown template variables. It is the least surprising, even if annoying, and provides better error messages (229489e)
- Yes, we should error on unmatched
{. This includes{{; escaping has been deferred (Added validation for unmatched brackets in build-dir template #15414)
- Should we take the advantage to self-ignore
build.build-dir? Make target dir self-ignoring #15061 - Include in
cargo metadata? What about that people need to matchcargo metadataversion againstcargoto get it?- Yes, it should be included in
cargo metadatabut only the current one. Changing the hash on every cargo version has been deferred to reduce the need to providebuild-dirfor all cargo versions - Added
build_directoryfield to cargo metadata output #15377, docs(metadata): Added build_directory to cargo metadata documentation #15410
- Yes, it should be included in
- Should we offer
CARGO_BUILD_TMPDIR, deprecatingCARGO_TARGET_TMPDIR? - should we offer the workspace base name either as a prefix to the hash or as its own variable for making it easier for humans to find delete specific build dirs
Deferred
{workspace-manifest-hash}also hashes the cargo version- Excludes release channel information, including which nightly
- Allows easier cleanup on
rustup update(Garbage collect wholetarget/#13136) - Makes very clear that the hash's meaning is unstable
- Blocked on Garbage collect whole
target/#13136 (maybe more?) to reduce the need for third-party clean up tools which will need to know the path to everybuild-dirfor a workspace
- In templates,
{{is an escaped{- With us erroring on
{, we have the room to allow escaping in the future - We'd like escaping support to be based on use cases and not speculatively developed as
{in paths is rare and this isn't a general path
- With us erroring on
Original Issue:
Problem
There are a couple of issues with the CARGO_TARGET_DIR that are seemingly in conflict with each other:
-
Multiple locations of
targetdirs complicate excluding them from backups and full-disk search, cleanup of the temp files, moving temp files to dedicated partitions, out of slow network drives or container mounts, etc. Users don't like that thetargetdir is huge, and multiple instances of it add up to lot of disk space. Users would prefer a central location to ease management of the temp files, and also to dedupe/reuse dependencies across many projects. -
People (and tools) are relying on a relative
./targetdirectory being present to copy or run built files out of there. Additionally, users may not want to configure a sharedCARGO_TARGET_DIRdue to risk of file name conflicts between projects.
However, the dilemma between 1 and 2 exists only because Cargo uses CARGO_TARGET_DIR for two different roles:
- A cache for all intermediate build products (a place where crates.io crates are built, where compiler-private temp files are) which aren't project-specific, and/or files that users don't need to access directly.
- A location for user-facing final build products (artifacts) that users expect to be there and need to access.
Proposed Solution
So to satisfy both uses, I suggest to change the thinking about what the role of CARGO_TARGET_DIR should be. Instead of thinking where to put the same huge all-purpose mixed CARGO_TARGET_DIR, think how to deduplicate and slim CARGO_TARGET_DIR, and move everything non-user-facing out of it.
Instead of merging or sharding the CARGO_TARGET_DIR as-is with all of its current content, and adding --artifact-dir as a separate place where final products are being copied to — make CARGO_TARGET_DIR to be the artifact dir (without copying).
As long as the CARGO_TARGET_DIR dir is the place for all of the build files, of all crates including all the crates.io and local builds, with all the caches, all the temp junk, then this is going to be a problematic large directory that needs to be managed. But if the purpose of the ./target dir was changed to be only for user-facing files (files that users can name, and would access via ./target path themselves), then this directory would be relatively small, with a good reason to stay workspace-relative.
What isn't an intermediate build product? (and should stay in ./target)
- linked (and stripped) binaries of the current workspace, including binaries for the examples,
- libraries of the current workspace as
.a/.so, wherelib.crate-typecalls for them. Possibly.rlib/.rmetain the future if there's a stable ABI. - linked binaries for tests and benches of the current workspace (to make it easy to launch them under a debugger/profiler, and so they can use relative file paths to read workspace assets).
- debug symbols for all of the above.
.dfiles for all of the above (so that IDEs and other build systems know when to rebuild the artifacts).- if Cargo adds some "staging" directory (a non-private
OUT_DIRforbuild.rs, see Allow build scripts to stage final artifacts #13663), then for build scripts belonging to the current workspace it would be inside./targetas well.
So generally files that users build intentionally, and may want to access directly (run themselves, or package up for distribution) and files that users may need configure their IDE and debugger to find inside the project.
Crates in [patch.crates-io] with a path are a gray area, an might also have their artifacts included in the ./target dir (but in some way that avoids clobbering workspaces' files).
What isn't a final build product, and doesn't belong to ./target:
- anything related to building crates from crates.io, or any other registry (packages with
source = "registry+…") - all
.fingerprintandincrementaldir content of all crates. These are implementation details of the compiler, and nobody should be accessing these directly via./target/…. .ofiles. Users are not supposed to use them directly either (Rust has static libs for this).- proc macro libs. They're not useful without rustc present.
All of these should be built in some other shared build cache dir (one that is not inside CARGO_TARGET_DIR), configurable by a new option/env var.
Registry dependencies would get unique paths derived from rustc version + package IDs + enabled features (so that different crates using different features don't invalidate each others' caches all the time). This would enable sharing built crates.io dependencies across all projects for the same local user, without also causing local workspaces to clobber each others' CARGO_TARGET_DIR/profile/product paths. Temp directories for local projects would need some hashed paths in the shared build/temp dir too.
Advantages
- Such split removes about 90% of the weight from
./targetdirs (forcargoitself, it makes./target/debugwith binaries and tests take 415MB, instead of 4.2GB). This makes cleanup of all the scatteredtargetdirs less of a pressing problem. - The
./targetkeeps relatively few files, and removes high-frequency-churning files out of it, which makes it less of a problem for real-time disk indexing (like search and backups on macOS). - I/O latency of
./targetstops being critical for build speeds, unlike I/O of the incremental cache and rewrites of thousands of.ofiles. It becomes feasible to have project directory on a network drive without overridingCARGO_TARGET_DIR(network filesystems are used by non-Linux systems where tools like Vagrant and Docker have to run full-fat VMs, and can't cheaply share the file system). - It makes
./targetcontain only workspace-unique files, which makes it justified for every workspace to have one. - It enables moving registry deps to a shared build directory, without side effect of local projects overwriting each others' files. Sharing of dependencies matches users' expectation that the same dependencies shouldn't be redundantly rebuilt for each local project.
- It's almost entirely backwards compatible. Users can get the benefits without breaking their existing workflows, post-build scripts, and integrations. It doesn't invalidate documentation/books/tutorials that refer to
target/release/exeetc. - It could be the default behavior, so it could benefit all users without friction of adding
--artifact-diror.cargo/config.
Notes
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
kornelski commentedon Jun 21, 2024
In terms of incompatibilities, only a few come to my mind:
Hacks that recursively search
./targetlooking for a file built by some build script in itsOUT_DIR. Ideally this should be solved by adding proper support for building arbitrary assets in Cargo, but for the sake of keeping compatibility withCARGO_TARGET_DIR, this could be made to work by keepingOUT_DIRs as subdirectories or symlinks in./target.Binaries trying to link to
.so/.dylibof their Rust dependencies, where the deps are built by Cargo, and are not installed on the system. This is currently basically unsupported in Cargo, because there's no way to setrpathto anything that works (absolute path is too build-specific, and relative doesn't work for tests and examples that are in a different dir than bins). But if users are hacking around it with custom post-build steps, they may expect to find the 3rd party shared libs in./target. This again would be better solved by having Rust/Cargo aware of dylib dependencies, and copy/hardlink dylibs as needed and set rpath accordingly. Not many deps havecrate-type = "cdylib", so these also could be kept/symlinked in./targetfor back-compat.Build containers could have very strict read-only filesystems with only
CARGO_TARGET_DIRandCARGO_HOMEsubdirs being writeable. This could be made backwards-compatible by reverting back to the current everything-in-one-place behavior whenCARGO_TARGET_DIRis set, and picking other env vars for configuring locations of the trimmed./targetand the intermediate products cache dir.epage commentedon Jun 22, 2024
@poliorcetics from rust-lang/rfcs#3664 (comment)
epage commentedon Jun 22, 2024
Overall, I see this as a solution alternative to #6790 and had recommended we have that conversation there (or on internals).
This is likely going to be the most difficult topic to work through and we'll need to make sure we get wide input on this from #6790 users and others.
imo this is out of scope for this proposal (see #5931) and we should keep this focused so as not to get distracted.
epage commentedon Jun 22, 2024
I see this as a re-framing of the problem, addressing #6790 and rust-lang/rfcs#3371
Instead of us defining a new
artifact-dir, we saytarget-diris the artifact directory and move everything else out into a "working directory".--artifact-dirbecause controlling of the location of the "working directory" is too low level for the CLI and should be reserved for config--artfact-diris thattarget-diris not the final directory files are put under but the parent directory. This is especially annoying forcargo buildvscargo build --target, dealing with profile names, etcPotential names
working-dir(arbitrarily chose this one to refer to the concept moving forward)build-dirCargo script would default its
target-diras itsworking-dirThis would need input from
epage commentedon Jun 22, 2024
This would need an audit of ways we publicly treat the target dir as a working dir, like exposing
CARGO_TARGET_TMPDIRensc commentedon Jun 23, 2024
Sharing sources over NFS adds another incompatibility when the ability to move
./targetout of the sources is eliminated.Every modern buildsystem allows to keep sources and build results separated (and users and tools do not have problems with it). I do not think that cargo should go the way back and enforce a fixed
./targetdirectory.kornelski commentedon Jun 24, 2024
I'm not suggesting to force it to always be
./target. TheCARGO_TARGET_DIRcan continue to move this directory elsewhere.The main point is to reduce severity of problems that the current high-churn high-volume content of this dir causes.
epage commentedon Jul 30, 2024
We talked about this in today's Cargo team meeting.
Our care abouts include
target-dirfor dealing with final artifacts, so we likely want to preserve thattarget-diras that will push people down one path moving intermediate artifacts and then we completely change it on the user what path they should go, rather than having a stable story for how to handle thisWhile we acknowledged the potential for user confusion with
CARGO_TARGET_TMPDIR, we were fine with it not being associated withtarget-dirThe general shape of what we proposed in the meeting is...
Shiny future
target-build-dir: Home of intermediate artifactstarget-build-dirtarget-dir)target-build-dirconfigtarget-build-dirdefault ("{cargo-cache}/target/{workspace-manifest-path-hash}")target-artifact-dir: Home of final artifactstargetprefix)--artifact-dirCLI flag is added{platform}or{legacy-platform}is not present with multi-target buildstarget-artifact-dirconfig / clitarget-dirconfig / clitarget-artifact-dirdefault ("{workspace-root}/target/{legacy-platform}/{profile}")Legacy
target-dir--target-diris hidden on CLI. Maybe shows up in man pagesOther
CARGO_TARGET_TMPDIRpoints withintarget-build-dirPath to Shiny Future
target-build-dirIn theory, we could trivially do this by
target-build-dirandtarget-dir(if different)target-build-dirand only when we do the "hardlink or cp" do we referencetarget-dirInitial default is
"{workspace-root}/target"Template supports
{workspace-root}{cargo-cache}(pointing toCARGO_HOMEfor now){workspace-manifest-path-hash}Steps
Notes:
.cargo/config.tomltarget-build-dirfieldtarget-artifact-dirAssumption:
target-build-dirtakes some pressure off oftarget-artifact-dirDefaulted to final location (
"{workspace-root}/target/{legacy-platform}/{profile}")Template supports
{workspace-root}{cargo-cache}(pointing toCARGO_HOMEfor now){workspace-manifest-path-hash}{platform}{legacy-platform}(elides host-target)Needs all of the details in the tracking issue to be finalized.
Contingencies
If
target-build-dirtakes more than N time (1 year?) to stabilize, then we re-evaluate approving rust-lang/rfcs#3371. This is to try to balance the needs of the people who want something like rust-lang/rfcs#3371 now vs (1) the long-term inapplicability of that RFC and (2) the lack of stable "blessed" workflow for users (telling users to use solution X for several months and then telling them that is no longer "right" and they need to use solution Y).Alternatives
target-diras the "artifact base dir" and provide a query command (likebuck2 analyze) to ask what the "artifact dir" istarget-artifact-dirin CLI, leaving it for more advanced usestarget-dirandtarget-artifact-dirtarget-dirfor final artifacts. While we eventually de-emphasizetarget-dir, those workflows will work just finetarget-dirandtarget-build-dir{platform}/{profile}) if no templates are available{dl}but that is a more limited / off in the weeds use case rather than front and center for userstarget-diriftarget-build-dirortarget-artifact-diris presentepage commentedon Aug 1, 2024
Something we overlooked in the above analysis is other "artifacts". In particular, I'm thinking of
cargo packagewhich places files in$CARGO_TARGET_DIR/packages. I'm assuming at least the.crates location is part our stable API. We'd need to decide about the files laid out on disk next to it.Ways of solving this
examples/, we could make the profile and target template dirs blank and always put this underpackages/{default}variable that is artifact-specific.clarfonthey commentedon Sep 24, 2024
Just commenting here as I'm dealing with my own issues regarding the target dir, but personally, while it's nice for
targetto contain final build products only by default, I still will want all of these build products out of the target directory for the sake of excluding them from backups and snapshotting.For some context, I use ZFS snapshots as a form of fast local backups; not long-term backups in case of hardware or extreme software failure, but decent short-term backups in case I accidentally delete a file or mess up an update. However, I explicitly go out of my way to exclude as many things as possible from auto-snapshotting that qualify as "cache" because they can very quickly clog up my disk if I'm not careful.
(Also: since snapshotting is a filesystem-level feature, I can't just say "don't save files of this type in snapshots" since snapshotting works by instantly freezing the state of the FS into a snapshot, and doesn't copy files over like a long-term backup would.)
For example, today I just deleted 200 GiB of snapshots of target directories. Not the current target directories, but past versions of them from previous snapshots. Snapshots are good for incremental stuff like code because they're copy-on-write, but the contents of a binary are effectively random to any snapshotting tool and they'll end up being fully duplicated every time they're snapshotted, and that means you can end up with several times that amount of data in snapshots until everything eventually gets old enough to be deleted. The "effectively random" part also applies especially to the final products, since while crates that don't change won't change in their compiled artifacts, the final linked products definitely will.
So, as far as I'm concerned, moving the final build products back into the workspace without also having the option to keep them out effectively un-solves the problem that moving the target directory was meant to solve. After all, the final build products, modulo LTO (which isn't really going to happen for debug builds) will effectively be the same size as all the intermediate products, so, that means that about half the disk usage will not be saved. (I'm extremely approximating here; the point is that it's a considerable amount of the disk usage, even if it's not half. Even 10% of the size is still a lot when you consider that these are being multiplied across several snapshots.)
And note that yes, other languages like Node and Python also have this exact same problem, but I don't think that other languages' inability to solve this problem forgives Rust not solving it. Also, even though
node_modulescan be massive, hundreds of GiB of binary artifacts is pretty hard to beat.I love the idea of keeping intermediate products deduplicated and in one place. I just don't want that to obscure the goal of having the final products also somewhere else too.
kornelski commentedon Sep 24, 2024
No, the intermediate products are usually many many times larger. Not just double, they can be 1000× larger! On the project I'm currently working, a clean debug build of a 20MB executable creates 2300MB of junk in
target/. After working on it for a while, it grows to 13GB of temp data for a 20MB result.There are often many duplicate copies of libstd and other dependencies in each
.rlibfile. There is a lot of duplication across code units. There's plenty of completely unused objects included in the dependencies, and stripped even without LTO (rust relies on--as-neededflag). There are also often separate copies for build dependencies, builds withcfg(test), and incremental build cache.epage commentedon Sep 24, 2024
@clarfonthey the plan calls for both
target-work-dirandtarget-artifact-dirto be templated so you can move their content out. It does not call out templating oftarget-diras it calls for phasing that out. If we wanted to templatize it as a convenience way of setting both of the above, we'd likely want to wait for the above so we set the precedence for what people are generally expected to work with, rather than shifting expectations around on the user.mathstuf commentedon Sep 27, 2024
FWIW, I've had decent results with
targetbeing a symlink to somewhere that is not subject to backups/snapshotting.cargo cleanwill remove the symlink, but everything else I've used is largely fine. Note that this puts intermediate and final artifacts into the same bucket.100 remaining items
kornelski commentedon Jul 27, 2025
I'm afraid that all the tooling which uses
cargo metadata, Cargo's internal API, or invokescargo buildcommand by itself is undertested, because settingCARGO_BUILD_BUILD_DIRor passing-Zbuild-diris simply ignored in these scenarios.CARGO_BUILD_BUILD_DIR=x cargo asm -Zbuild-dirdoesn't use thebuild-dirfeature.CARGO_BUILD_BUILD_DIR=x cargo deb -- -Zbuild-dirdoesn't use thebuild-dirfeature.CARGO_BUILD_BUILD_DIR=x cargo xcodedoesn't use thebuild-dirfeature, and doesn't support passing-Zbuild-dirCARGO_BUILD_BUILD_DIR=x cargo tauri devdoesn't use thebuild-dirfeature, and doesn't support passing-Zbuild-dirrfcbot commentedon Jul 27, 2025
The final comment period, with a disposition to merge, as per the review above, is now complete.
As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.
This will be merged soon.
ranger-ross commentedon Jul 28, 2025
While preparing the stabilization PR I noticed some of the packaging tests started failing (this one for example)
I noticed we had one less
[COMPILING] whatever 0.0.1 ...for a few of thecargo packagerelated tests.I did some digging and I came across a behavior change that was not discussed or noticed in the original implementation pull request.
During the post package "verify" step the
.crateis untarred and built to verify it is able to compile.Prior to splitting
targetandbuild-dir, during the build iftarget-dirwas not set the build target directory would be set astarget/package/{name}-{version}/target, due toWorkspace::root()callingPathBuf::parent()on the untarred manifest which is inside of thetarget/package/{name}-{version}directory.This causes all of the dependencies to be re-built as the
target-dir(which is nowtarget/package/{name}-{version}/target) is empty.It also means that for every crate in the workspace, will recompile all dependencies during package verification.
It's notable that by setting
CARGO_TARGET_DIRthis behavior changes.The workspace
target-dirwill be used during package verification meaning that if a crate has already been compiled with an unchanged fingerprint Cargo will skip compiling.Below is a simplified example.
With the build-dir changes, the default behavior changes to as
CARGO_TARGET_DIRwas always set, expect we will be using thebuild-dirinstead of thetarget-dir.I would argue this is a positive change as it reduces the amount of recompiling needed during packaging/publishing.
epage commentedon Jul 28, 2025
If so, we should have an explicit PR to set this as the behavior, including tests.
I always took the current behavior as intended to ensure the correctness in place of build caches being poisoned in some way. At least for local packages, they would still be fingerprinted differently than the original workspace members, do maybe less so. I wonder if there are also problems with conflicting final artifacts.
With us stabilizing workspace publishing, I would be open to us sharing a build directory between published packages. A part of me worries about sharing final artifacts.
perf(package): Always reuse the workspace's target-dir (#15783)
Stabilize `build.build-dir` (#15833)
fix(publish): Don't generate final artifacts (#15910)
fix(script): Store cargo script lockfiles in build-dir (#16087)
Refactor `Layout` into `BuildDirLayout` and `ArtifactDirLayout` (#16092)