Added symlink resolution for workspace-path-hash (#15400)

### What does this PR try to resolve?

This PR adds logic to resolve symlinks before hashing the
`workspace-path-hash` template variable for `build.build-dir`.
See
https://github.com/rust-lang/cargo/issues/14125#issuecomment-2751658701

cc: #14125

Note: The behavior on unix systems is unchanged, as the manifest_path
was already canonicalized (at least on my system and in CI). However,
the Windows behavior did not do this previous.

### How should we test and review this PR?

I added a test which runs `cargo build` twice, once from the real
directory and once from inside of a symlinked directory, then verifies
that hashes match.

The change is only a few lines. Most of the diffs are testing code

r? @epage
This commit is contained in:
Weihang Lo 2025-04-08 15:31:34 +00:00 committed by GitHub
commit 7ade57b15f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 3 deletions

View File

@ -672,7 +672,8 @@ impl GlobalContext {
.to_string(),
),
("{workspace-path-hash}", {
let hash = crate::util::hex::short_hash(&workspace_manifest_path);
let real_path = std::fs::canonicalize(workspace_manifest_path)?;
let hash = crate::util::hex::short_hash(&real_path);
format!("{}{}{}", &hash[0..2], std::path::MAIN_SEPARATOR, &hash[2..])
}),
];

View File

@ -11,8 +11,8 @@
use std::path::PathBuf;
use cargo_test_support::prelude::*;
use cargo_test_support::{paths, project, str};
use cargo_test_support::{prelude::*, Project};
use std::env::consts::{DLL_PREFIX, DLL_SUFFIX, EXE_SUFFIX};
#[cargo_test]
@ -569,7 +569,7 @@ fn template_cargo_cache_home() {
}
#[cargo_test]
fn template_workspace_manfiest_path_hash() {
fn template_workspace_path_hash() {
let p = project()
.file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#)
.file(
@ -609,6 +609,81 @@ fn template_workspace_manfiest_path_hash() {
assert_exists(&p.root().join(&format!("target-dir/debug/foo{EXE_SUFFIX}")));
}
/// Verify that the {workspace-path-hash} does not changes if cargo is run from inside of
/// a symlinked directory.
/// The test approach is to build a project twice from the non-symlinked directory and a symlinked
/// directory and then compare the build-dir paths.
#[cargo_test]
fn template_workspace_path_hash_should_handle_symlink() {
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(windows)]
use std::os::windows::fs::symlink_dir as symlink;
let p = project()
.file("src/lib.rs", "")
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "1.0.0"
authors = []
edition = "2015"
"#,
)
.file(
".cargo/config.toml",
r#"
[build]
build-dir = "foo/{workspace-path-hash}/build-dir"
"#,
)
.build();
// Build from the non-symlinked directory
p.cargo("check -Z build-dir")
.masquerade_as_nightly_cargo(&["build-dir"])
.enable_mac_dsym()
.run();
// Parse and verify the hash dir created from the non-symlinked dir
let foo_dir = p.root().join("foo");
assert_exists(&foo_dir);
let original_hash_dir = parse_workspace_manifest_path_hash(&foo_dir);
verify_layouts(&p, &original_hash_dir);
// Create a symlink of the project root.
let mut symlinked_dir = p.root().clone();
symlinked_dir.pop();
symlinked_dir = symlinked_dir.join("symlink-dir");
symlink(p.root(), &symlinked_dir).unwrap();
// Remove the foo dir (which contains the build-dir) before we rebuild from a symlinked dir.
foo_dir.rm_rf();
// Run cargo from the symlinked dir
p.cargo("check -Z build-dir")
.cwd(&symlinked_dir)
.masquerade_as_nightly_cargo(&["build-dir"])
.enable_mac_dsym()
.run();
// Parse and verify the hash created from the symlinked dir
assert_exists(&foo_dir);
let symlink_hash_dir = parse_workspace_manifest_path_hash(&foo_dir);
verify_layouts(&p, &symlink_hash_dir);
// Verify the hash dir created from the symlinked and non-symlinked dirs are the same.
assert_eq!(original_hash_dir, symlink_hash_dir);
fn verify_layouts(p: &Project, build_dir_parent: &PathBuf) {
let build_dir = build_dir_parent.as_path().join("build-dir");
assert_build_dir_layout(build_dir, "debug");
assert_artifact_dir_layout(p.root().join("target"), "debug");
}
}
fn parse_workspace_manifest_path_hash(hash_dir: &PathBuf) -> PathBuf {
// Since the hash will change between test runs simply find the first directories and assume
// that is the hash dir. The format is a 2 char directory followed by the remaining hash in the