diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 5ed83f0498c..a0ba06ac5e5 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -211,7 +211,6 @@ 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!( @@ -222,6 +221,12 @@ impl<'gctx> Workspace<'gctx> { ws.root_manifest = ws.find_root(manifest_path)?; } + ws.build_dir = gctx.build_dir( + ws.root_manifest + .as_ref() + .unwrap_or(&manifest_path.to_path_buf()), + )?; + ws.custom_metadata = ws .load_workspace_config()? .and_then(|cfg| cfg.custom_metadata); diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 1253bed5e53..6502d4d35fb 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -636,13 +636,37 @@ impl GlobalContext { /// Fallsback to the target directory if not specified. /// /// Callers should prefer [`Workspace::build_dir`] instead. - pub fn build_dir(&self) -> CargoResult> { + pub fn build_dir(&self, manifest_path: &PathBuf) -> 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); + let replacements = vec![ + ( + "{workspace-root}", + manifest_path + .parent() + .unwrap() + .to_str() + .context("workspace root was not valid utf-8")? + .to_string(), + ), + ( + "{cargo-cache}", + self.home() + .as_path_unlocked() + .to_str() + .context("cargo home was not valid utf-8")? + .to_string(), + ), + ( + "{workspace-manifest-path-hash}", + crate::util::hex::short_hash(manifest_path), + ), + ]; + + let path = val.resolve_templated_path(self, replacements); // Check if the target directory is set to an empty string in the config.toml file. if val.raw_value().is_empty() { diff --git a/src/cargo/util/context/path.rs b/src/cargo/util/context/path.rs index 3a6c9e4a397..3e4bab7385b 100644 --- a/src/cargo/util/context/path.rs +++ b/src/cargo/util/context/path.rs @@ -32,6 +32,25 @@ impl ConfigRelativePath { self.0.definition.root(gctx).join(&self.0.val) } + /// Same as [`Self::resolve_path`] but will make string replacements + /// before resolving the path. + /// + /// `replacements` should be an an [`IntoIterator`] of tuples with the "from" and "to" for the + /// string replacement + pub fn resolve_templated_path( + &self, + gctx: &GlobalContext, + replacements: impl IntoIterator, impl AsRef)>, + ) -> PathBuf { + let mut value = self.0.val.clone(); + + for (from, to) in replacements { + value = value.replace(from.as_ref(), to.as_ref()); + } + + self.0.definition.root(gctx).join(&value) + } + /// Resolves this configuration-relative path to either an absolute path or /// something appropriate to execute from `PATH`. /// diff --git a/tests/testsuite/build_dir.rs b/tests/testsuite/build_dir.rs index 74831d228cb..7931bab90ea 100644 --- a/tests/testsuite/build_dir.rs +++ b/tests/testsuite/build_dir.rs @@ -180,6 +180,102 @@ fn verify_build_dir_is_disabled_by_feature_flag() { assert!(!p.root().join("build").exists()); } +mod should_template_build_dir_correctly { + use cargo_test_support::paths; + + use super::*; + + #[cargo_test] + fn workspace_root() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + build-dir = "{workspace-root}/build" + "#, + ) + .build(); + + p.cargo("build -Z unstable-options -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir(p.root().join("build"), "debug", true); + assert_build_dir(p.root().join("target"), "debug", false); + + // Verify the binary was copied to the `target` dir + assert!(p.root().join("target/debug/foo").is_file()); + } + + #[cargo_test] + fn cargo_cache() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + build-dir = "{cargo-cache}/build" + "#, + ) + .build(); + + p.cargo("build -Z unstable-options -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + assert_build_dir(paths::home().join(".cargo/build"), "debug", true); + assert_build_dir(p.root().join("target"), "debug", false); + + // Verify the binary was copied to the `target` dir + assert!(p.root().join("target/debug/foo").is_file()); + } + + #[cargo_test] + fn workspace_manfiest_path_hash() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .file( + ".cargo/config.toml", + r#" + [build] + build-dir = "foo/{workspace-manifest-path-hash}/build" + "#, + ) + .build(); + + p.cargo("build -Z unstable-options -Z build-dir") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + let foo_dir = p.root().join("foo"); + assert!(foo_dir.exists()); + + // Since the hash will change between test runs simply find the first directory in `foo` + // and assume that is the build dir. + let hash_dir = std::fs::read_dir(foo_dir) + .unwrap() + .into_iter() + .next() + .unwrap() + .unwrap(); + + let build_dir = hash_dir.path().join("build"); + assert!(build_dir.exists()); + + assert_build_dir(build_dir, "debug", true); + assert_build_dir(p.root().join("target"), "debug", false); + + // Verify the binary was copied to the `target` dir + assert!(p.root().join("target/debug/foo").is_file()); + } +} + #[track_caller] fn assert_build_dir(path: PathBuf, profile: &str, is_build_dir: bool) { println!("checking if {path:?} is a build directory ({is_build_dir})");