diff --git a/src/cargo/core/compiler/build_runner/compilation_files.rs b/src/cargo/core/compiler/build_runner/compilation_files.rs index 8c7ddff7f89..29a5458ca90 100644 --- a/src/cargo/core/compiler/build_runner/compilation_files.rs +++ b/src/cargo/core/compiler/build_runner/compilation_files.rs @@ -217,7 +217,7 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { } else if unit.target.is_custom_build() { self.build_script_dir(unit) } else if unit.target.is_example() { - self.layout(unit.kind).examples().to_path_buf() + self.layout(unit.kind).build_examples().to_path_buf() } else if unit.artifact.is_true() { self.artifact_dir(unit) } else { diff --git a/src/cargo/core/compiler/custom_build.rs b/src/cargo/core/compiler/custom_build.rs index 9f7287baace..8928c93be10 100644 --- a/src/cargo/core/compiler/custom_build.rs +++ b/src/cargo/core/compiler/custom_build.rs @@ -1124,7 +1124,7 @@ fn prepare_metabuild( let path = unit .pkg .manifest() - .metabuild_path(build_runner.bcx.ws.target_dir()); + .metabuild_path(build_runner.bcx.ws.build_dir()); paths::create_dir_all(path.parent().unwrap())?; paths::write_if_changed(path, &output)?; Ok(()) diff --git a/src/cargo/core/compiler/fingerprint/dep_info.rs b/src/cargo/core/compiler/fingerprint/dep_info.rs index 746c0832035..294942d43d2 100644 --- a/src/cargo/core/compiler/fingerprint/dep_info.rs +++ b/src/cargo/core/compiler/fingerprint/dep_info.rs @@ -51,9 +51,9 @@ pub struct RustcDepInfo { pub enum DepInfoPathType { /// src/, e.g. src/lib.rs PackageRootRelative, - /// target/debug/deps/lib... + /// {build-dir}/debug/deps/lib... /// or an absolute path /.../sysroot/... - TargetRootRelative, + BuildRootRelative, } /// Same as [`RustcDepInfo`] except avoids absolute paths as much as possible to @@ -126,7 +126,7 @@ impl EncodedDepInfo { for _ in 0..nfiles { let ty = match read_u8(bytes)? { 0 => DepInfoPathType::PackageRootRelative, - 1 => DepInfoPathType::TargetRootRelative, + 1 => DepInfoPathType::BuildRootRelative, _ => return None, }; let path_bytes = read_bytes(bytes)?; @@ -210,7 +210,7 @@ impl EncodedDepInfo { for (ty, file, checksum_info) in self.files.iter() { match ty { DepInfoPathType::PackageRootRelative => dst.push(0), - DepInfoPathType::TargetRootRelative => dst.push(1), + DepInfoPathType::BuildRootRelative => dst.push(1), } write_bytes(dst, paths::path2bytes(file)?); write_bool(dst, checksum_info.is_some()); @@ -292,14 +292,14 @@ pub fn translate_dep_info( cargo_dep_info: &Path, rustc_cwd: &Path, pkg_root: &Path, - target_root: &Path, + build_root: &Path, rustc_cmd: &ProcessBuilder, allow_package: bool, env_config: &Arc>, ) -> CargoResult<()> { let depinfo = parse_rustc_dep_info(rustc_dep_info)?; - let target_root = crate::util::try_canonicalize(target_root)?; + let build_root = crate::util::try_canonicalize(build_root)?; let pkg_root = crate::util::try_canonicalize(pkg_root)?; let mut on_disk_info = EncodedDepInfo::default(); on_disk_info.env = depinfo.env; @@ -351,8 +351,8 @@ pub fn translate_dep_info( let canon_file = crate::util::try_canonicalize(&abs_file).unwrap_or_else(|_| abs_file.clone()); - let (ty, path) = if let Ok(stripped) = canon_file.strip_prefix(&target_root) { - (DepInfoPathType::TargetRootRelative, stripped) + let (ty, path) = if let Ok(stripped) = canon_file.strip_prefix(&build_root) { + (DepInfoPathType::BuildRootRelative, stripped) } else if let Ok(stripped) = canon_file.strip_prefix(&pkg_root) { if !allow_package { return None; @@ -362,7 +362,7 @@ pub fn translate_dep_info( // It's definitely not target root relative, but this is an absolute path (since it was // joined to rustc_cwd) and as such re-joining it later to the target root will have no // effect. - (DepInfoPathType::TargetRootRelative, &*abs_file) + (DepInfoPathType::BuildRootRelative, &*abs_file) }; Some((ty, path.to_owned())) }; @@ -472,7 +472,7 @@ pub fn parse_rustc_dep_info(rustc_dep_info: &Path) -> CargoResult /// indicates that the crate should likely be rebuilt. pub fn parse_dep_info( pkg_root: &Path, - target_root: &Path, + build_root: &Path, dep_info: &Path, ) -> CargoResult> { let Ok(data) = paths::read_bytes(dep_info) else { @@ -487,7 +487,7 @@ pub fn parse_dep_info( ret.files .extend(info.files.into_iter().map(|(ty, path, checksum_info)| { ( - make_absolute_path(ty, pkg_root, target_root, path), + make_absolute_path(ty, pkg_root, build_root, path), checksum_info.and_then(|(file_len, checksum)| { Checksum::from_str(&checksum).ok().map(|c| (file_len, c)) }), @@ -499,13 +499,13 @@ pub fn parse_dep_info( fn make_absolute_path( ty: DepInfoPathType, pkg_root: &Path, - target_root: &Path, + build_root: &Path, path: PathBuf, ) -> PathBuf { match ty { DepInfoPathType::PackageRootRelative => pkg_root.join(path), // N.B. path might be absolute here in which case the join will have no effect - DepInfoPathType::TargetRootRelative => target_root.join(path), + DepInfoPathType::BuildRootRelative => build_root.join(path), } } @@ -678,7 +678,7 @@ mod encoded_dep_info { fn gen_test(checksum: bool) { let checksum = checksum.then_some((768, "c01efc669f09508b55eced32d3c88702578a7c3e".into())); let lib_rs = ( - DepInfoPathType::TargetRootRelative, + DepInfoPathType::BuildRootRelative, PathBuf::from("src/lib.rs"), checksum.clone(), ); @@ -691,7 +691,7 @@ mod encoded_dep_info { assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo); let mod_rs = ( - DepInfoPathType::TargetRootRelative, + DepInfoPathType::BuildRootRelative, PathBuf::from("src/mod.rs"), checksum.clone(), ); diff --git a/src/cargo/core/compiler/fingerprint/mod.rs b/src/cargo/core/compiler/fingerprint/mod.rs index b35a592ab11..b409b7aac35 100644 --- a/src/cargo/core/compiler/fingerprint/mod.rs +++ b/src/cargo/core/compiler/fingerprint/mod.rs @@ -839,7 +839,7 @@ impl LocalFingerprint { mtime_cache: &mut HashMap, checksum_cache: &mut HashMap, pkg: &Package, - target_root: &Path, + build_root: &Path, cargo_exe: &Path, gctx: &GlobalContext, ) -> CargoResult> { @@ -852,8 +852,8 @@ impl LocalFingerprint { // the `dep_info` file itself whose mtime represents the start of // rustc. LocalFingerprint::CheckDepInfo { dep_info, checksum } => { - let dep_info = target_root.join(dep_info); - let Some(info) = parse_dep_info(pkg_root, target_root, &dep_info)? else { + let dep_info = build_root.join(dep_info); + let Some(info) = parse_dep_info(pkg_root, build_root, &dep_info)? else { return Ok(Some(StaleItem::MissingFile(dep_info))); }; for (key, previous) in info.env.iter() { @@ -910,7 +910,7 @@ impl LocalFingerprint { LocalFingerprint::RerunIfChanged { output, paths } => Ok(find_stale_file( mtime_cache, checksum_cache, - &target_root.join(output), + &build_root.join(output), paths.iter().map(|p| (pkg_root.join(p), None)), false, )), @@ -1153,7 +1153,7 @@ impl Fingerprint { mtime_cache: &mut HashMap, checksum_cache: &mut HashMap, pkg: &Package, - target_root: &Path, + build_root: &Path, cargo_exe: &Path, gctx: &GlobalContext, ) -> CargoResult<()> { @@ -1261,7 +1261,7 @@ impl Fingerprint { mtime_cache, checksum_cache, pkg, - target_root, + build_root, cargo_exe, gctx, )? { @@ -1449,13 +1449,13 @@ fn calculate(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult // After we built the initial `Fingerprint` be sure to update the // `fs_status` field of it. - let target_root = target_root(build_runner); + let build_root = build_root(build_runner); let cargo_exe = build_runner.bcx.gctx.cargo_exe()?; fingerprint.check_filesystem( &mut build_runner.mtime_cache, &mut build_runner.checksum_cache, &unit.pkg, - &target_root, + &build_root, cargo_exe, build_runner.bcx.gctx, )?; @@ -1493,7 +1493,7 @@ fn calculate_normal( }; // Afterwards calculate our own fingerprint information. - let target_root = target_root(build_runner); + let build_root = build_root(build_runner); let local = if unit.mode.is_doc() || unit.mode.is_doc_scrape() { // rustdoc does not have dep-info files. let fingerprint = pkg_fingerprint(build_runner.bcx, &unit.pkg).with_context(|| { @@ -1505,7 +1505,7 @@ fn calculate_normal( vec![LocalFingerprint::Precalculated(fingerprint)] } else { let dep_info = dep_info_loc(build_runner, unit); - let dep_info = dep_info.strip_prefix(&target_root).unwrap().to_path_buf(); + let dep_info = dep_info.strip_prefix(&build_root).unwrap().to_path_buf(); vec![LocalFingerprint::CheckDepInfo { dep_info, checksum: build_runner.bcx.gctx.cli_unstable().checksum_freshness, @@ -1714,7 +1714,7 @@ fn build_script_local_fingerprints( // longstanding bug, in Cargo. Recent refactorings just made it painfully // obvious. let pkg_root = unit.pkg.root().to_path_buf(); - let target_dir = target_root(build_runner); + let build_dir = build_root(build_runner); let env_config = Arc::clone(build_runner.bcx.gctx.env_config()?); let calculate = move |deps: &BuildDeps, pkg_fingerprint: Option<&dyn Fn() -> CargoResult>| { @@ -1747,7 +1747,7 @@ fn build_script_local_fingerprints( // them all here. Ok(Some(local_fingerprints_deps( deps, - &target_dir, + &build_dir, &pkg_root, &env_config, ))) @@ -1783,7 +1783,7 @@ fn build_script_override_fingerprint( /// [`RunCustomBuild`]: crate::core::compiler::CompileMode::RunCustomBuild fn local_fingerprints_deps( deps: &BuildDeps, - target_root: &Path, + build_root: &Path, pkg_root: &Path, env_config: &Arc>, ) -> Vec { @@ -1796,7 +1796,7 @@ fn local_fingerprints_deps( // absolute prefixes from them. let output = deps .build_script_output - .strip_prefix(target_root) + .strip_prefix(build_root) .unwrap() .to_path_buf(); let paths = deps @@ -1854,10 +1854,10 @@ pub fn dep_info_loc(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Path build_runner.files().fingerprint_file_path(unit, "dep-") } -/// Returns an absolute path that target directory. +/// Returns an absolute path that build directory. /// All paths are rewritten to be relative to this. -fn target_root(build_runner: &BuildRunner<'_, '_>) -> PathBuf { - build_runner.bcx.ws.target_dir().into_path_unlocked() +fn build_root(build_runner: &BuildRunner<'_, '_>) -> PathBuf { + build_runner.bcx.ws.build_dir().into_path_unlocked() } /// Reads the value from the old fingerprint hash file and compare. diff --git a/src/cargo/core/compiler/future_incompat.rs b/src/cargo/core/compiler/future_incompat.rs index abe3de3e2a8..22c6ad4e913 100644 --- a/src/cargo/core/compiler/future_incompat.rs +++ b/src/cargo/core/compiler/future_incompat.rs @@ -91,7 +91,7 @@ pub struct Diagnostic { pub level: String, } -/// The filename in the top-level `target` directory where we store +/// The filename in the top-level `build-dir` directory where we store /// the report const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json"; /// Max number of reports to save on disk. @@ -166,7 +166,7 @@ impl OnDiskReports { } let on_disk = serde_json::to_vec(&self).unwrap(); if let Err(e) = ws - .target_dir() + .build_dir() .open_rw_exclusive_create( FUTURE_INCOMPAT_FILE, ws.gctx(), @@ -191,7 +191,7 @@ impl OnDiskReports { /// Loads the on-disk reports. pub fn load(ws: &Workspace<'_>) -> CargoResult { - let report_file = match ws.target_dir().open_ro_shared( + let report_file = match ws.build_dir().open_ro_shared( FUTURE_INCOMPAT_FILE, ws.gctx(), "Future incompatible report", diff --git a/src/cargo/core/compiler/layout.rs b/src/cargo/core/compiler/layout.rs index 30062164d3c..3638887a5c0 100644 --- a/src/cargo/core/compiler/layout.rs +++ b/src/cargo/core/compiler/layout.rs @@ -128,6 +128,8 @@ pub struct Layout { fingerprint: PathBuf, /// The directory for examples: `$dest/examples` examples: PathBuf, + /// The directory for pre-uplifted examples: `$build-dir/debug/examples` + build_examples: PathBuf, /// The directory for rustdoc output: `$root/doc` doc: PathBuf, /// The directory for temporary data of integration tests and benches: `$dest/tmp` @@ -135,6 +137,11 @@ pub struct Layout { /// The lockfile for a build (`.cargo-lock`). Will be unlocked when this /// struct is `drop`ped. _lock: FileLock, + /// Same as `_lock` but for the build directory. + /// + /// Will be `None` when the build-dir and target-dir are the same path as we cannot + /// lock the same path twice. + _build_lock: Option, } impl Layout { @@ -150,15 +157,22 @@ impl Layout { dest: &str, ) -> CargoResult { let mut root = ws.target_dir(); + let mut build_root = ws.build_dir(); if let Some(target) = target { root.push(target.short_name()); + build_root.push(target.short_name()); } + let build_dest = build_root.join(dest); let dest = root.join(dest); // If the root directory doesn't already exist go ahead and create it // here. Use this opportunity to exclude it from backups as well if the // system supports it since this is a freshly created folder. // paths::create_dir_all_excluded_from_backups_atomic(root.as_path_unlocked())?; + if root != build_root { + paths::create_dir_all_excluded_from_backups_atomic(build_root.as_path_unlocked())?; + } + // Now that the excluded from backups target root is created we can create the // actual destination (sub)subdirectory. paths::create_dir_all(dest.as_path_unlocked())?; @@ -167,23 +181,37 @@ impl Layout { // directory, so just lock the entire thing for the duration of this // compile. let lock = dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?; + + let build_lock = if root != build_root { + Some(build_dest.open_rw_exclusive_create( + ".cargo-lock", + ws.gctx(), + "build directory", + )?) + } else { + None + }; let root = root.into_path_unlocked(); + let build_root = build_root.into_path_unlocked(); let dest = dest.into_path_unlocked(); - let deps = dest.join("deps"); + let build_dest = build_dest.as_path_unlocked(); + let deps = build_dest.join("deps"); let artifact = deps.join("artifact"); Ok(Layout { deps, - build: dest.join("build"), + build: build_dest.join("build"), artifact, - incremental: dest.join("incremental"), - fingerprint: dest.join(".fingerprint"), + incremental: build_dest.join("incremental"), + fingerprint: build_dest.join(".fingerprint"), examples: dest.join("examples"), + build_examples: build_dest.join("examples"), doc: root.join("doc"), - tmp: root.join("tmp"), + tmp: build_root.join("tmp"), root, dest, _lock: lock, + _build_lock: build_lock, }) } @@ -193,6 +221,7 @@ impl Layout { paths::create_dir_all(&self.incremental)?; paths::create_dir_all(&self.fingerprint)?; paths::create_dir_all(&self.examples)?; + paths::create_dir_all(&self.build_examples)?; paths::create_dir_all(&self.build)?; Ok(()) @@ -210,6 +239,10 @@ impl Layout { pub fn examples(&self) -> &Path { &self.examples } + /// Fetch the build examples path. + pub fn build_examples(&self) -> &Path { + &self.build_examples + } /// Fetch the doc path. pub fn doc(&self) -> &Path { &self.doc diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 277039685d6..1b15f450351 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -297,7 +297,7 @@ fn rustc( let exec = exec.clone(); let root_output = build_runner.files().host_dest().to_path_buf(); - let target_dir = build_runner.bcx.ws.target_dir().into_path_unlocked(); + let build_dir = build_runner.bcx.ws.build_dir().into_path_unlocked(); let pkg_root = unit.pkg.root().to_path_buf(); let cwd = rustc .get_cwd() @@ -455,7 +455,7 @@ fn rustc( &dep_info_loc, &cwd, &pkg_root, - &target_dir, + &build_dir, &rustc, // Do not track source files in the fingerprint for registry dependencies. is_local, @@ -555,7 +555,7 @@ fn link_targets( let path = unit .pkg .manifest() - .metabuild_path(build_runner.bcx.ws.target_dir()); + .metabuild_path(build_runner.bcx.ws.build_dir()); target.set_src_path(TargetSourcePath::Path(path)); } diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 64d37016b2a..7cf4225e5ac 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -759,6 +759,7 @@ unstable_cli_options!( avoid_dev_deps: bool = ("Avoid installing dev-dependencies if possible"), binary_dep_depinfo: bool = ("Track changes to dependency artifacts"), bindeps: bool = ("Allow Cargo packages to depend on bin, cdylib, and staticlib crates, and use the artifacts built by those crates"), + build_dir: bool = ("Enable the `build.build-dir` option in .cargo/config.toml file"), #[serde(deserialize_with = "deserialize_comma_separated_list")] build_std: Option> = ("Enable Cargo to compile the standard library itself as part of a crate graph compilation"), #[serde(deserialize_with = "deserialize_comma_separated_list")] @@ -1264,6 +1265,7 @@ impl CliUnstable { "avoid-dev-deps" => self.avoid_dev_deps = parse_empty(k, v)?, "binary-dep-depinfo" => self.binary_dep_depinfo = parse_empty(k, v)?, "bindeps" => self.bindeps = parse_empty(k, v)?, + "build-dir" => self.build_dir = parse_empty(k, v)?, "build-std" => self.build_std = Some(parse_list(v)), "build-std-features" => self.build_std_features = Some(parse_list(v)), "cargo-lints" => self.cargo_lints = parse_empty(k, v)?, diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 277af3587d7..455b3df36c8 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -67,6 +67,10 @@ pub struct Workspace<'gctx> { /// `None` if the default path of `root/target` should be used. target_dir: Option, + /// Shared build directory for intermediate build artifacts. + /// This directory may be shared between multiple workspaces. + build_dir: Option, + /// List of members in this workspace with a listing of all their manifest /// paths. The packages themselves can be looked up through the `packages` /// set above. @@ -209,6 +213,7 @@ impl<'gctx> Workspace<'gctx> { pub fn new(manifest_path: &Path, gctx: &'gctx GlobalContext) -> CargoResult> { let mut ws = Workspace::new_default(manifest_path.to_path_buf(), gctx); ws.target_dir = gctx.target_dir()?; + ws.build_dir = gctx.build_dir()?; if manifest_path.is_relative() { bail!( @@ -238,6 +243,7 @@ impl<'gctx> Workspace<'gctx> { }, root_manifest: None, target_dir: None, + build_dir: None, members: Vec::new(), member_ids: HashSet::new(), default_members: Vec::new(), @@ -282,6 +288,7 @@ impl<'gctx> Workspace<'gctx> { } else { ws.gctx.target_dir()? }; + ws.build_dir = ws.target_dir.clone(); ws.members.push(ws.current_manifest.clone()); ws.member_ids.insert(id); ws.default_members.push(ws.current_manifest.clone()); @@ -418,6 +425,13 @@ impl<'gctx> Workspace<'gctx> { .unwrap_or_else(|| self.default_target_dir()) } + pub fn build_dir(&self) -> Filesystem { + if !self.gctx().cli_unstable().build_dir { + return self.target_dir(); + } + self.build_dir.clone().unwrap_or_else(|| self.target_dir()) + } + fn default_target_dir(&self) -> Filesystem { if self.root_maybe().is_embedded() { let hash = crate::util::hex::short_hash(&self.root_manifest().to_string_lossy()); diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 45519bf2a74..dc16676fed2 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -41,6 +41,7 @@ pub struct CleanContext<'gctx> { /// Cleans various caches. pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { let mut target_dir = ws.target_dir(); + let mut build_dir = ws.build_dir(); let gctx = opts.gctx; let mut clean_ctx = CleanContext::new(gctx); clean_ctx.dry_run = opts.dry_run; @@ -67,6 +68,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { // that profile. let dir_name = profiles.get_dir_name(); target_dir = target_dir.join(dir_name); + build_dir = build_dir.join(dir_name); } // If we have a spec, then we need to delete some packages, otherwise, just @@ -75,7 +77,15 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { // Note that we don't bother grabbing a lock here as we're just going to // blow it all away anyway. if opts.spec.is_empty() { - clean_ctx.remove_paths(&[target_dir.into_path_unlocked()])?; + let paths: &[PathBuf] = if gctx.cli_unstable().build_dir && build_dir != target_dir { + &[ + target_dir.into_path_unlocked(), + build_dir.into_path_unlocked(), + ] + } else { + &[target_dir.into_path_unlocked()] + }; + clean_ctx.remove_paths(paths)?; } else { clean_specs( &mut clean_ctx, @@ -222,7 +232,7 @@ fn clean_specs( .rustc_outputs(mode, target.kind(), triple)?; let (dir, uplift_dir) = match target.kind() { TargetKind::ExampleBin | TargetKind::ExampleLib(..) => { - (layout.examples(), Some(layout.examples())) + (layout.build_examples(), Some(layout.examples())) } // Tests/benchmarks are never uplifted. TargetKind::Test | TargetKind::Bench => (layout.deps(), None), diff --git a/src/cargo/ops/cargo_package/mod.rs b/src/cargo/ops/cargo_package/mod.rs index 5e5f4ff191d..86cdd15923d 100644 --- a/src/cargo/ops/cargo_package/mod.rs +++ b/src/cargo/ops/cargo_package/mod.rs @@ -217,7 +217,7 @@ fn do_package<'a>( }; let mut local_reg = if ws.gctx().cli_unstable().package_workspace { - let reg_dir = ws.target_dir().join("package").join("tmp-registry"); + let reg_dir = ws.build_dir().join("package").join("tmp-registry"); sid.map(|sid| TmpRegistry::new(ws.gctx(), reg_dir, sid)) .transpose()? } else { diff --git a/src/cargo/ops/cargo_package/verify.rs b/src/cargo/ops/cargo_package/verify.rs index 794e5d30581..0abcd76b00e 100644 --- a/src/cargo/ops/cargo_package/verify.rs +++ b/src/cargo/ops/cargo_package/verify.rs @@ -45,9 +45,11 @@ pub fn run_verify( tar.file().seek(SeekFrom::Start(0))?; let f = GzDecoder::new(tar.file()); - let dst = tar - .parent() - .join(&format!("{}-{}", pkg.name(), pkg.version())); + let dst = ws.build_dir().as_path_unlocked().join(&format!( + "package/{}-{}", + pkg.name(), + pkg.version() + )); if dst.exists() { paths::remove_dir_all(&dst)?; } @@ -63,7 +65,14 @@ pub fn run_verify( let mut src = PathSource::new(&dst, id, ws.gctx()); let new_pkg = src.root_package()?; let pkg_fingerprint = hash_all(&dst)?; - let mut ws = Workspace::ephemeral(new_pkg, gctx, None, true)?; + + let target_dir = if gctx.cli_unstable().build_dir { + Some(ws.build_dir()) + } else { + None + }; + + let mut ws = Workspace::ephemeral(new_pkg, gctx, target_dir, true)?; if let Some(local_reg) = local_reg { ws.add_local_overlay( local_reg.upstream, diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 0b388978d69..b74450c202e 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -421,11 +421,8 @@ impl GlobalContext { /// Gets the path to the `rustc` executable. pub fn load_global_rustc(&self, ws: Option<&Workspace<'_>>) -> CargoResult { - let cache_location = ws.map(|ws| { - ws.target_dir() - .join(".rustc_info.json") - .into_path_unlocked() - }); + let cache_location = + ws.map(|ws| ws.build_dir().join(".rustc_info.json").into_path_unlocked()); let wrapper = self.maybe_get_tool("rustc_wrapper", &self.build_config()?.rustc_wrapper); let rustc_workspace_wrapper = self.maybe_get_tool( "rustc_workspace_wrapper", @@ -603,7 +600,7 @@ impl GlobalContext { /// /// Returns `None` if the user has not chosen an explicit directory. /// - /// Callers should prefer `Workspace::target_dir` instead. + /// Callers should prefer [`Workspace::target_dir`] instead. pub fn target_dir(&self) -> CargoResult> { if let Some(dir) = &self.target_dir { Ok(Some(dir.clone())) @@ -634,6 +631,34 @@ impl GlobalContext { } } + /// The directory to use for intermediate build artifacts. + /// + /// Falls back to the target directory if not specified. + /// + /// Callers should prefer [`Workspace::build_dir`] instead. + pub fn build_dir(&self) -> CargoResult> { + if !self.cli_unstable().build_dir { + return self.target_dir(); + } + if let Some(val) = &self.build_config()?.build_dir { + let path = val.resolve_path(self); + + // Check if the target directory is set to an empty string in the config.toml file. + if val.raw_value().is_empty() { + bail!( + "the build directory is set to an empty string in {}", + val.value().definition + ) + } + + Ok(Some(Filesystem::new(path))) + } else { + // For now, fallback to the previous implementation. + // This will change in the future. + return self.target_dir(); + } + } + /// Get a configuration value by key. /// /// This does NOT look at environment variables. See `get_cv_with_env` for @@ -2653,6 +2678,7 @@ pub struct CargoBuildConfig { pub pipelining: Option, pub dep_info_basedir: Option, pub target_dir: Option, + pub build_dir: Option, pub incremental: Option, pub target: Option, pub jobs: Option, diff --git a/src/cargo/util/flock.rs b/src/cargo/util/flock.rs index 1c144b1d49a..dcb3e05c39f 100644 --- a/src/cargo/util/flock.rs +++ b/src/cargo/util/flock.rs @@ -142,7 +142,7 @@ impl Drop for FileLock { /// /// [`flock`]: https://linux.die.net/man/2/flock /// [`LockFileEx`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Filesystem { root: PathBuf, } diff --git a/src/cargo/util/workspace.rs b/src/cargo/util/workspace.rs index 9039cae8439..efc7aa7d8dd 100644 --- a/src/cargo/util/workspace.rs +++ b/src/cargo/util/workspace.rs @@ -114,7 +114,7 @@ pub fn print_available_tests(ws: &Workspace<'_>, options: &CompileOptions) -> Ca pub fn path_args(ws: &Workspace<'_>, unit: &Unit) -> (PathBuf, PathBuf) { let src = match unit.target.src_path() { TargetSourcePath::Path(path) => path.to_path_buf(), - TargetSourcePath::Metabuild => unit.pkg.manifest().metabuild_path(ws.target_dir()), + TargetSourcePath::Metabuild => unit.pkg.manifest().metabuild_path(ws.build_dir()), }; assert!(src.is_absolute()); if unit.pkg.package_id().source_id().is_path() { diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 6cbad3c49d3..da2ece53686 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -78,6 +78,7 @@ Each new feature described below should explain how to use it. * [feature-unification](#feature-unification) --- Enable new feature unification modes in workspaces * Output behavior * [artifact-dir](#artifact-dir) --- Adds a directory where artifacts are copied to. + * [build-dir](#build-dir) --- Adds a directory where intermediate build artifacts are stored. * [Different binary name](#different-binary-name) --- Assign a name to the built binary that is separate from the crate name. * [root-dir](#root-dir) --- Controls the root directory relative to which paths are printed * Compile behavior @@ -238,6 +239,27 @@ This can also be specified in `.cargo/config.toml` files. artifact-dir = "out" ``` +## build-dir +* Original Issue: [#14125](https://github.com/rust-lang/cargo/issues/14125) +* Tracking Issue: [#14125](https://github.com/rust-lang/cargo/issues/14125) + +The directory where intermediate build artifacts will be stored. +Intermediate artifacts are produced by Rustc/Cargo during the build process. + +```toml +[build] +build-dir = "out" +``` + +### `build.build-dir` + +* Type: string (path) +* Default: Defaults to the value of `build.target-dir` +* Environment: `CARGO_BUILD_BUILD_DIR` + +The path to where internal files used as part of the build are placed. + + ## root-dir * Original Issue: [#9887](https://github.com/rust-lang/cargo/issues/9887) * Tracking Issue: None (not currently slated for stabilization) diff --git a/tests/testsuite/build_dir.rs b/tests/testsuite/build_dir.rs new file mode 100644 index 00000000000..bc9c27d0ffa --- /dev/null +++ b/tests/testsuite/build_dir.rs @@ -0,0 +1,602 @@ +//! Tests for `build.build-dir` config property. +//! +//! The testing strategy for build-dir functionality is primarily checking if directories / files +//! are in the expected locations. +//! The rational is that other tests will verify each individual feature, while the tests in this +//! file verify the files saved to disk are in the correct locations according to the `build-dir` +//! configuration. +//! +//! Tests check if directories match some "layout" by using [`assert_build_dir_layout`] and +//! [`assert_artifact_dir_layout`]. + +use std::path::PathBuf; + +use cargo_test_support::prelude::*; +use cargo_test_support::project; +use std::env::consts::{DLL_PREFIX, DLL_SUFFIX, EXE_SUFFIX}; + +#[cargo_test] +fn verify_build_dir_is_disabled_by_feature_flag() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("target"), "debug"); + assert_exists(&p.root().join(format!("target/debug/foo{EXE_SUFFIX}"))); + assert_exists(&p.root().join("target/debug/foo.d")); + assert_not_exists(&p.root().join("build-dir")); +} + +#[cargo_test] +fn binary_with_debug() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + assert_artifact_dir_layout(p.root().join("target-dir"), "debug"); + assert_exists_patterns_with_base_dir( + &p.root(), + &[ + // Check the pre-uplifted binary in the build-dir + &format!("build-dir/debug/deps/foo*{EXE_SUFFIX}"), + "build-dir/debug/deps/foo*.d", + // Verify the binary was copied to the target-dir + &format!("target-dir/debug/foo{EXE_SUFFIX}"), + "target-dir/debug/foo.d", + ], + ); + assert_not_exists(&p.root().join("target")); +} + +#[cargo_test] +fn binary_with_release() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build --release -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "release"); + assert_exists(&p.root().join(format!("target-dir/release/foo{EXE_SUFFIX}"))); + assert_exists_patterns_with_base_dir( + &p.root(), + &[ + // Check the pre-uplifted binary in the build-dir + &format!("build-dir/release/deps/foo*{EXE_SUFFIX}"), + "build-dir/release/deps/foo*.d", + // Verify the binary was copied to the target-dir + &format!("target-dir/release/foo{EXE_SUFFIX}"), + "target-dir/release/foo.d", + ], + ); +} + +#[cargo_test] +fn libs() { + // https://doc.rust-lang.org/reference/linkage.html#r-link.staticlib + let (staticlib_prefix, staticlib_suffix) = + if cfg!(target_os = "windows") && cfg!(target_env = "msvc") { + ("", ".lib") + } else { + ("lib", ".a") + }; + + // (crate-type, list of final artifacts) + let lib_types = [ + ("lib", ["libfoo.rlib", "libfoo.d"]), + ( + "dylib", + [ + &format!("{DLL_PREFIX}foo{DLL_SUFFIX}"), + &format!("{DLL_PREFIX}foo.d"), + ], + ), + ( + "cdylib", + [ + &format!("{DLL_PREFIX}foo{DLL_SUFFIX}"), + &format!("{DLL_PREFIX}foo.d"), + ], + ), + ( + "staticlib", + [ + &format!("{staticlib_prefix}foo{staticlib_suffix}"), + &format!("{staticlib_prefix}foo.d"), + ], + ), + ]; + + for (lib_type, expected_files) in lib_types { + let p = project() + .file("src/lib.rs", r#"fn foo() { println!("Hello, World!") }"#) + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + edition = "2015" + + [lib] + crate-type = ["{lib_type}"] + "# + ), + ) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + + // Verify lib artifacts were copied into the artifact dir + assert_exists_patterns_with_base_dir(&p.root().join("target-dir/debug"), &expected_files); + } +} + +#[cargo_test] +fn should_default_to_target() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("target"), "debug"); + assert_exists(&p.root().join(format!("target/debug/foo{EXE_SUFFIX}"))); +} + +#[cargo_test] +fn should_respect_env_var() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .env("CARGO_BUILD_BUILD_DIR", "build-dir") + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + assert_exists(&p.root().join(format!("target/debug/foo{EXE_SUFFIX}"))); +} + +#[cargo_test] +fn build_script_should_output_to_build_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + "build.rs", + r#" + fn main() { + std::fs::write( + format!("{}/foo.txt", std::env::var("OUT_DIR").unwrap()), + "Hello, world!", + ) + .unwrap(); + } + "#, + ) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + assert_exists_patterns_with_base_dir( + &p.root(), + &[ + &format!("build-dir/debug/build/foo-*/build-script-build{EXE_SUFFIX}"), + "build-dir/debug/build/foo-*/out/foo.txt", // Verify OUT_DIR + ], + ); +} + +#[cargo_test] +fn cargo_tmpdir_should_output_to_build_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + "tests/foo.rs", + r#" + #[test] + fn test() { + std::fs::write( + format!("{}/foo.txt", env!("CARGO_TARGET_TMPDIR")), + "Hello, world!", + ) + .unwrap(); + } + "#, + ) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("test -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + assert_exists(&p.root().join(format!("build-dir/tmp/foo.txt"))); +} + +#[cargo_test] +fn examples_should_output_to_build_dir_and_uplift_to_target_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file("examples/foo.rs", r#"fn main() { }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build --examples -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + assert_exists_patterns_with_base_dir( + &p.root(), + &[ + // uplifted (target-dir) + &format!("target-dir/debug/examples/foo{EXE_SUFFIX}"), + "target-dir/debug/examples/foo.d", + // pre-uplifted (build-dir) + &format!("build-dir/debug/examples/foo*{EXE_SUFFIX}"), + "build-dir/debug/examples/foo*.d", + ], + ); +} + +#[cargo_test] +fn benches_should_output_to_build_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file("benches/foo.rs", r#"fn main() { }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build --bench=foo -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + assert_exists_patterns_with_base_dir( + &p.root(), + &[ + &format!("build-dir/debug/deps/foo*{EXE_SUFFIX}"), + "build-dir/debug/deps/foo*.d", + ], + ); +} + +#[cargo_test] +fn cargo_doc_should_output_to_target_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("doc -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + let docs_dir = p.root().join("target-dir/doc"); + + assert_exists(&docs_dir); + assert_exists(&docs_dir.join("foo/index.html")); +} + +#[cargo_test] +fn cargo_package_should_build_in_build_dir_and_output_to_target_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("package -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + + let package_artifact_dir = p.root().join("target-dir/package"); + assert_exists(&package_artifact_dir); + assert_exists(&package_artifact_dir.join("foo-0.0.1.crate")); + assert!(package_artifact_dir.join("foo-0.0.1.crate").is_file()); + + let package_build_dir = p.root().join("build-dir/package"); + assert_exists(&package_build_dir); + assert_exists(&package_build_dir.join("foo-0.0.1")); + assert!(package_build_dir.join("foo-0.0.1").is_dir()); +} + +#[cargo_test] +fn cargo_clean_should_clean_the_target_dir_and_build_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir_layout(p.root().join("build-dir"), "debug"); + + p.cargo("clean -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_not_exists(&p.root().join("build-dir")); + assert_not_exists(&p.root().join("target-dir")); +} + +#[cargo_test] +fn timings_report_should_output_to_target_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build --timings -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_exists(&p.root().join("target-dir/cargo-timings/cargo-timing.html")); +} + +#[cargo_test( + nightly, + reason = "-Zfuture-incompat-test requires nightly (permanently)" +)] +fn future_incompat_should_output_to_build_dir() { + let p = project() + .file("src/main.rs", r#"fn main() { let x = 1; }"#) + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("build -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .arg("--future-incompat-report") + .env("RUSTFLAGS", "-Zfuture-incompat-test") + .run(); + + assert_exists(&p.root().join("build-dir/.future-incompat-report.json")); +} + +#[track_caller] +fn assert_build_dir_layout(path: PathBuf, profile: &str) { + assert_dir_layout(path, profile, true); +} + +#[allow(dead_code)] +#[track_caller] +fn assert_artifact_dir_layout(path: PathBuf, profile: &str) { + assert_dir_layout(path, profile, false); +} + +#[track_caller] +fn assert_dir_layout(path: PathBuf, profile: &str, is_build_dir: bool) { + println!("checking if {path:?} is a build directory ({is_build_dir})"); + // For things that are in both `target` and the build directory we only check if they are + // present if `is_build_dir` is true. + if is_build_dir { + assert_eq!( + is_build_dir, + path.join(profile).is_dir(), + "Expected {:?} to exist and be a directory", + path.join(profile) + ); + } + + let error_message = |dir: &str| { + if is_build_dir { + format!("`{dir}` dir was expected but not found") + } else { + format!("`{dir}` dir was not expected but was found") + } + }; + + if is_build_dir { + assert_exists(&path.join(".rustc_info.json")); + } else { + assert_not_exists(&path.join(".rustc_info.json")); + } + + assert_eq!( + is_build_dir, + path.join(profile).join("deps").is_dir(), + "{}", + error_message("deps") + ); + assert_eq!( + is_build_dir, + path.join(profile).join("build").is_dir(), + "{}", + error_message("build") + ); + assert_eq!( + is_build_dir, + path.join(profile).join("incremental").is_dir(), + "{}", + error_message("incremental") + ); + assert_eq!( + is_build_dir, + path.join(profile).join(".fingerprint").is_dir(), + "{}", + error_message(".fingerprint") + ); +} + +#[track_caller] +fn assert_exists(path: &PathBuf) { + assert!( + path.exists(), + "Expected `{}` to exist but was not found.", + path.display() + ); +} + +#[track_caller] +fn assert_not_exists(path: &PathBuf) { + assert!( + !path.exists(), + "Expected `{}` to NOT exist but was found.", + path.display() + ); +} + +#[track_caller] +fn assert_exists_patterns_with_base_dir(base: &PathBuf, patterns: &[&str]) { + let root = base.to_str().unwrap(); + let p: Vec<_> = patterns.iter().map(|p| format!("{root}/{p}")).collect(); + let p: Vec<&str> = p.iter().map(|v| v.as_str()).collect(); + assert_exists_patterns(&p); +} + +#[track_caller] +fn assert_exists_patterns(patterns: &[&str]) { + for p in patterns { + assert_exists_pattern(p); + } +} + +#[track_caller] +fn assert_exists_pattern(pattern: &str) { + use glob::glob; + + let mut z = glob(pattern).unwrap(); + + assert!( + z.next().is_some(), + "Expected `{pattern}` to match existing file but was not found.", + ) +} diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index 45700298477..e0a828959fd 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +