rust-lang/cargo · critical
artifact-dir was not locked during clean
Error message
artifact-dir was not locked during clean
What it means
This is an internal Cargo invariant assertion (panic) in `clean_packages` (src/ops/cargo_clean.rs), reached while computing which uplifted artifact files to delete for each package target. `Layout::artifact_dir()` returns `Option` because Cargo only populates it when the layout was created with `must_take_artifact_dir_lock = true` (src/compiler/layout.rs:294-318); `cargo clean` always constructs its layouts with that flag set (cargo_clean.rs:200,206), so the expect firing means Cargo's own invariant broke — i.e., a Cargo bug, most plausibly in the `-Zbuild-dir-new-layout` code path that guards this block (cargo_clean.rs:278), not anything in your project.
Solutions
- Bypass cargo entirely: `rm -rf target` (or `cargo +stable clean`) removes the same directories without hitting the buggy path.
- Stop enabling the experimental flag: drop `-Zbuild-dir-new-layout` / `build-dir-new-layout` from your invocation, `CARGO_UNSTABLE_*` env, or `.cargo/config.toml` `[unstable]` table, then retry `cargo clean`.
- Update the nightly (`rustup update nightly`) — invariants like this are usually fixed quickly during the migration — and retest in a scratch copy of the workspace.
- If it still reproduces, report it to https://github.com/rust-lang/cargo/issues including `cargo +nightly version --verbose`, the exact command, and whether multiple cargo versions share the target dir.
Example fix
# before: panics with `artifact-dir was not locked during clean` cargo +nightly -Zbuild-dir-new-layout clean # after: same effect, no buggy code path rm -rf target # or cargo +stable clean
Defensive patterns
Strategy: fallback
Validate before calling
# Guard scripts: refuse `cargo clean` while the experimental layout flag is on if cargo +nightly -Zhelp 2>/dev/null | grep -q build-dir-new-layout \ && grep -RqsE 'build-dir-new-layout|min-publish-age' .cargo/config.toml ~/.cargo/config.toml; then echo "unstable flags present — falling back to rm -rf target" rm -rf target else cargo clean fi
Prevention
- Give each cargo/toolchain version its own target directory (e.g. per-toolchain CARGO_TARGET_DIR in CI) instead of sharing one target/ across versions.
- Run `cargo clean` with the same toolchain that produced the artifacts; add `rust-toolchain.toml` so every invocation, including IDEs, agrees.
- In scripts, prefer `rm -rf target` as the documented fallback when `cargo clean` exits non-zero or panics.
- Keep nightlies current (`rustup update`) when opting into `-Z` flags — these invariants move fast during migrations.
- Wrap CI clean steps with `set +e` on the clean command and a manual directory removal fallback so a clean panic cannot fail unrelated jobs.
When it happens
Trigger: Running `cargo clean` (optionally `-p <pkg>`) on a nightly toolchain with `-Z build-dir-new-layout` enabled (via CLI flag, `CARGO_UNSTABLE_BUILD_DIR_NEW_LAYOUT=1`, or an `[unstable]` config table), where a code regression or a layout constructed without the artifact-dir lock reaches the per-target uplift-removal loop at lines 299-306. Not reachable through normal user configuration of a released stable cargo.
Common situations: Nightly users experimenting with the build-directory re-layout migration sharing one `target/` dir across several cargo versions; cargo built from source or patched; IDE-embedded cargo (rust-analyzer) invoking a mismatched cargo on a workspace created by a different version; interrupted cleans leaving lock state half-updated.
Related errors
- target must support `bin`
- not implemented
- parsing ` ` requires `-Zscript
- parsing ` ` requires `-Zscript
- argument for --color must be auto, always, or never, but…
AI-assisted analysis of rust-lang/cargo@98a09e7e7d (2026-08-21).
Data as JSON: /api/errors/4d571bcf1d0ce156.
Report an issue: GitHub.
Appendix: source
Thrown at src/ops/cargo_clean.rs:307
// Remove the uplifted copy.
for target in pkg.targets() {
if target.is_custom_build() {
continue;
}
let crate_name: Rc<str> = target.crate_name().into();
for &mode in &[
CompileMode::Build,
CompileMode::Test,
CompileMode::Check { test: false },
] {
for (compile_kind, layout) in &layouts {
let triple = target_data.short_name(compile_kind);
let (file_types, _unsupported) = target_data
.info(*compile_kind)
.rustc_outputs(mode, target.kind(), triple)?;
let artifact_dir = layout
.artifact_dir()
.expect("artifact-dir was not locked during clean");
let uplift_dir = match target.kind() {
TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
Some(artifact_dir.examples())
}
// Tests/benchmarks are never uplifted.
TargetKind::Test | TargetKind::Bench => None,
_ => Some(artifact_dir.dest()),
};
if let Some(uplift_dir) = uplift_dir {
for file_type in file_types {
let uplifted_filename = file_type.uplift_filename(target);
// Dep-info generated by Cargo itself.
let dep_info = Path::new(&uplifted_filename)
.with_extension("d")
.to_string_lossy()
.into_owned();
View on GitHub (pinned to 98a09e7e7d)