Skip to content

Commit

Permalink
Added templating support to build.build-dir
Browse files Browse the repository at this point in the history
This commit add the ability to use predefined variables when specifying
the `build.build-dir` in the Cargo configuration.

The currently supported template variables are:

* `{workspace-root}` which will expand to the Cargo workspace root
* `{cargo-cache}` which will expand to `CARGO_HOME` but will likely
  change in the future.
* `{workspace-manifest-path-hash}` which will expand to a short hash of
  the workspace manifest's path.
  • Loading branch information
ranger-ross committed Feb 2, 2025
1 parent 6eb89af commit f53007f
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 3 deletions.
7 changes: 6 additions & 1 deletion src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ impl<'gctx> Workspace<'gctx> {
pub fn new(manifest_path: &Path, gctx: &'gctx GlobalContext) -> CargoResult<Workspace<'gctx>> {
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!(
Expand All @@ -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);
Expand Down
28 changes: 26 additions & 2 deletions src/cargo/util/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Filesystem>> {
pub fn build_dir(&self, manifest_path: &PathBuf) -> CargoResult<Option<Filesystem>> {
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() {
Expand Down
19 changes: 19 additions & 0 deletions src/cargo/util/context/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = (impl AsRef<str>, impl AsRef<str>)>,
) -> 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`.
///
Expand Down
96 changes: 96 additions & 0 deletions tests/testsuite/build_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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})");
Expand Down

0 comments on commit f53007f

Please sign in to comment.