diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index eb48c5c4197..8458ca0e2a6 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -676,7 +676,16 @@ impl GlobalContext { .to_string(), ), ("{workspace-path-hash}", { - let hash = crate::util::hex::short_hash(&workspace_manifest_path); + // We include the version in the hash to prevent external tools from relying on + // our hash implmentation. Note that we explictly do not include the prerelease + // and build parts of the semver to avoid a new hash for every nightly version. + let version = crate::version::version().semver(); + let hash = crate::util::hex::short_hash(&( + version.major, + version.minor, + version.patch, + workspace_manifest_path, + )); format!("{}{}{}", &hash[0..2], std::path::MAIN_SEPARATOR, &hash[2..]) }), ]; diff --git a/src/cargo/version.rs b/src/cargo/version.rs index ca5bb8b2e7c..2dfdaf85856 100644 --- a/src/cargo/version.rs +++ b/src/cargo/version.rs @@ -35,6 +35,13 @@ pub struct VersionInfo { pub description: Option, } +impl VersionInfo { + /// The Cargo version as a [`semver::Version`]. + pub fn semver(&self) -> semver::Version { + semver::Version::parse(&self.version).expect("Cargo version was not a valid semver version") + } +} + impl fmt::Display for VersionInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.version)?; @@ -58,28 +65,30 @@ pub fn version() -> VersionInfo { }; } - // This is the version set in bootstrap, which we use to match rustc. - let version = option_env_str!("CFG_RELEASE").unwrap_or_else(|| { - // If cargo is not being built by bootstrap, then we just use the - // version from cargo's own `Cargo.toml`. - // - // There are two versions at play here: - // - version of cargo-the-binary, which you see when you type `cargo --version` - // - version of cargo-the-library, which you download from crates.io for use - // in your packages. - // - // The library is permanently unstable, so it always has a 0 major - // version. However, the CLI now reports a stable 1.x version - // (starting in 1.26) which stays in sync with rustc's version. - // - // Coincidentally, the minor version for cargo-the-library is always - // +1 of rustc's minor version (that is, `rustc 1.11.0` corresponds to - // `cargo `0.12.0`). The versions always get bumped in lockstep, so - // this should continue to hold. - let minor = env!("CARGO_PKG_VERSION_MINOR").parse::().unwrap() - 1; - let patch = env!("CARGO_PKG_VERSION_PATCH").parse::().unwrap(); - format!("1.{}.{}", minor, patch) - }); + let version = version_for_testing() + // This is the version set in bootstrap, which we use to match rustc. + .or_else(|| option_env_str!("CFG_RELEASE")) + .unwrap_or_else(|| { + // If cargo is not being built by bootstrap, then we just use the + // version from cargo's own `Cargo.toml`. + // + // There are two versions at play here: + // - version of cargo-the-binary, which you see when you type `cargo --version` + // - version of cargo-the-library, which you download from crates.io for use + // in your packages. + // + // The library is permanently unstable, so it always has a 0 major + // version. However, the CLI now reports a stable 1.x version + // (starting in 1.26) which stays in sync with rustc's version. + // + // Coincidentally, the minor version for cargo-the-library is always + // +1 of rustc's minor version (that is, `rustc 1.11.0` corresponds to + // `cargo `0.12.0`). The versions always get bumped in lockstep, so + // this should continue to hold. + let minor = env!("CARGO_PKG_VERSION_MINOR").parse::().unwrap() - 1; + let patch = env!("CARGO_PKG_VERSION_PATCH").parse::().unwrap(); + format!("1.{}.{}", minor, patch) + }); let release_channel = option_env_str!("CFG_RELEASE_CHANNEL"); let commit_info = option_env_str!("CARGO_COMMIT_HASH").map(|commit_hash| CommitInfo { @@ -96,3 +105,11 @@ pub fn version() -> VersionInfo { description, } } + +/// Provides a way for tests to inject an abitrary version for testing purposes. +/// +// __CARGO_TEST_CARGO_VERSION should not be relied on outside of tests. +#[allow(clippy::disallowed_methods)] +fn version_for_testing() -> Option { + std::env::var("__CARGO_TEST_CARGO_VERSION").ok() +} diff --git a/tests/testsuite/build_dir.rs b/tests/testsuite/build_dir.rs index bb0abe33da8..a585c9f5700 100644 --- a/tests/testsuite/build_dir.rs +++ b/tests/testsuite/build_dir.rs @@ -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,65 @@ fn template_workspace_manfiest_path_hash() { assert_exists(&p.root().join(&format!("target-dir/debug/foo{EXE_SUFFIX}"))); } +#[cargo_test] +fn template_workspace_path_hash_should_change_between_cargo_versions() { + let p = project() + .file("src/main.rs", r#"fn main() { println!("Hello, World!") }"#) + .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" + target-dir = "target-dir" + "#, + ) + .build(); + + p.cargo("build -Z build-dir") + .env("__CARGO_TEST_CARGO_VERSION", "1.0.0") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + let foo_dir = p.root().join("foo"); + assert_exists(&foo_dir); + let hash_dir = parse_workspace_manifest_path_hash(&foo_dir); + + let first_run_build_dir = hash_dir.as_path().join("build-dir"); + assert_build_dir_layout(first_run_build_dir.clone(), "debug"); + + // Remove the build-dir since we already know the hash and it makes + // finding the directory in the next run simpler. + foo_dir.as_path().rm_rf(); + + // Run Cargo build again with a different Cargo version + p.cargo("build -Z build-dir") + .env("__CARGO_TEST_CARGO_VERSION", "1.1.0") + .masquerade_as_nightly_cargo(&["build-dir"]) + .enable_mac_dsym() + .run(); + + let hash_dir = parse_workspace_manifest_path_hash(&foo_dir); + let second_run_build_dir = hash_dir.as_path().join("build-dir"); + assert_build_dir_layout(second_run_build_dir.clone(), "debug"); + + // Finally check that the build-dir is in different location between both Cargo versions + assert_ne!( + first_run_build_dir, second_run_build_dir, + "The workspace path hash generated between 2 Cargo versions matched when it should not" + ); +} + 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