diff --git a/Cargo.lock b/Cargo.lock index 2be61a658aa8..a5b3703ba094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,7 +461,7 @@ dependencies = [ [[package]] name = "cargo-util-schemas" -version = "0.5.1" +version = "0.6.0" dependencies = [ "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index fc47f7ade375..9dc40f90a8ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ cargo-platform = { path = "crates/cargo-platform", version = "0.1.5" } cargo-test-macro = { version = "0.3.0", path = "crates/cargo-test-macro" } cargo-test-support = { version = "0.4.0", path = "crates/cargo-test-support" } cargo-util = { version = "0.2.14", path = "crates/cargo-util" } -cargo-util-schemas = { version = "0.5.0", path = "crates/cargo-util-schemas" } +cargo-util-schemas = { version = "0.6.0", path = "crates/cargo-util-schemas" } cargo_metadata = "0.18.1" clap = "4.5.11" color-print = "0.3.6" diff --git a/crates/cargo-util-schemas/Cargo.toml b/crates/cargo-util-schemas/Cargo.toml index f960213f7ded..b95026ac3be3 100644 --- a/crates/cargo-util-schemas/Cargo.toml +++ b/crates/cargo-util-schemas/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-util-schemas" -version = "0.5.1" +version = "0.6.0" rust-version = "1.80" # MSRV:1 edition.workspace = true license.workspace = true diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index fe954f0f4ca0..b2e50bddaef3 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -767,6 +767,7 @@ pub struct TomlDetailedDependency { // `path` is relative to the file it appears in. If that's a `Cargo.toml`, it'll be relative to // that TOML file, and if it's a `.cargo/config` file, it'll be relative to that file. pub path: Option

, + pub base: Option, pub git: Option, pub branch: Option, pub tag: Option, @@ -806,6 +807,7 @@ impl Default for TomlDetailedDependency

{ registry: Default::default(), registry_index: Default::default(), path: Default::default(), + base: Default::default(), git: Default::default(), branch: Default::default(), tag: Default::default(), diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index c44b8d51eb4e..66eaa501bae9 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -776,6 +776,7 @@ unstable_cli_options!( no_index_update: bool = ("Do not update the registry index even if the cache is outdated"), package_workspace: bool = ("Handle intra-workspace dependencies when packaging"), panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"), + path_bases: bool = ("Allow paths that resolve relatively to a base specified in the config"), profile_rustflags: bool = ("Enable the `rustflags` option in profiles in .cargo/config.toml file"), public_dependency: bool = ("Respect a dependency's `public` field in Cargo.toml to control public/private dependencies"), publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"), @@ -1280,6 +1281,7 @@ impl CliUnstable { "package-workspace" => self.package_workspace= parse_empty(k, v)?, "panic-abort-tests" => self.panic_abort_tests = parse_empty(k, v)?, "public-dependency" => self.public_dependency = parse_empty(k, v)?, + "path-bases" => self.path_bases = parse_empty(k, v)?, "profile-rustflags" => self.profile_rustflags = parse_empty(k, v)?, "trim-paths" => self.trim_paths = parse_empty(k, v)?, "publish-timeout" => self.publish_timeout = parse_empty(k, v)?, diff --git a/src/cargo/sources/path.rs b/src/cargo/sources/path.rs index 85036c8101f5..27b7ec830559 100644 --- a/src/cargo/sources/path.rs +++ b/src/cargo/sources/path.rs @@ -910,6 +910,11 @@ fn nested_paths(manifest: &Manifest) -> Vec { let Some(path) = dep.path.as_ref() else { continue; }; + if dep.base.is_some() { + // Skip dependencies with base paths, if they are part of the directory tree then + // the outer walk will find them. + continue; + } nested_paths.push(PathBuf::from(path.as_str())); } } diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 76892b9d773b..1593f3e7b58f 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -901,13 +901,17 @@ impl InheritableFields { }; let mut dep = dep.clone(); if let manifest::TomlDependency::Detailed(detailed) = &mut dep { - if let Some(rel_path) = &detailed.path { - detailed.path = Some(resolve_relative_path( - name, - self.ws_root(), - package_root, - rel_path, - )?); + if detailed.base.is_none() { + // If this is a path dependency without a base, then update the path to be relative + // to the workspace root instead. + if let Some(rel_path) = &detailed.path { + detailed.path = Some(resolve_relative_path( + name, + self.ws_root(), + package_root, + rel_path, + )?); + } } } Ok(dep) @@ -2129,7 +2133,40 @@ fn to_dependency_source_id( // always end up hashing to the same value no matter where it's // built from. if manifest_ctx.source_id.is_path() { - let path = manifest_ctx.root.join(path); + let path = if let Some(base) = orig.base.as_ref() { + if !manifest_ctx.gctx.cli_unstable().path_bases { + bail!("usage of path bases requires `-Z path-bases`"); + } + + // Look up the relevant base in the Config and use that as the root. + if let Some(path_bases) = manifest_ctx + .gctx + .get::>(&format!("path-bases.{base}"))? + { + let path_base = path_bases.resolve_path(manifest_ctx.gctx); + path_base.join(path) + } else { + // Otherwise, check the build-in bases. + match base.as_str() { + "workspace" => { + if let Some(workspace_root) = find_workspace_root(manifest_ctx.root, manifest_ctx.gctx)? { + workspace_root.parent().unwrap().join(path) + } else { + bail!( + "dependency ({name_in_toml}) is using the `workspace` built-in path base outside of a workspace." + ) + } + }, + _ => bail!( + "dependency ({name_in_toml}) uses an undefined path base `{base}`. \ + You must add an entry for `{base}` in the Cargo configuration [path-bases] table." + ) + } + } + } else { + // This is a standard path with no prefix. + manifest_ctx.root.join(path) + }; let path = paths::normalize_path(&path); SourceId::for_path(&path) } else { @@ -2855,6 +2892,7 @@ fn prepare_toml_for_publish( let mut d = d.clone(); // Path dependencies become crates.io deps. d.path.take(); + d.base.take(); // Same with git dependencies. d.git.take(); d.branch.take(); diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index e5386620e464..2549003e5180 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -66,33 +66,35 @@ -Z panic-abort-tests Enable support to run tests with -Cpanic=abort - -Z profile-rustflags Enable the `rustflags` option in profiles in .cargo/config.toml file + -Z path-bases Allow paths that resolve relatively to a base specified in the config - -Z public-dependency Respect a dependency's `public` field in Cargo.toml to control public/private dependencies + -Z profile-rustflags Enable the `rustflags` option in profiles in .cargo/config.toml file - -Z publish-timeout Enable the `publish.timeout` key in .cargo/config.toml file + -Z public-dependency Respect a dependency's `public` field in Cargo.toml to control public/private dependencies - -Z rustdoc-map Allow passing external documentation mappings to rustdoc + -Z publish-timeout Enable the `publish.timeout` key in .cargo/config.toml file - -Z rustdoc-scrape-examples Allows Rustdoc to scrape code examples from reverse-dependencies + -Z rustdoc-map Allow passing external documentation mappings to rustdoc - -Z script Enable support for single-file, `.rs` packages + -Z rustdoc-scrape-examples Allows Rustdoc to scrape code examples from reverse-dependencies - -Z target-applies-to-host Enable the `target-applies-to-host` key in the .cargo/config.toml file + -Z script Enable support for single-file, `.rs` packages - -Z trim-paths Enable the `trim-paths` option in profiles + -Z target-applies-to-host Enable the `target-applies-to-host` key in the .cargo/config.toml file - -Z unstable-options Allow the usage of unstable options + -Z trim-paths Enable the `trim-paths` option in profiles - + -Z unstable-options Allow the usage of unstable options - Run with `cargo -Z [FLAG] [COMMAND]` + - + Run with `cargo -Z [FLAG] [COMMAND]` - See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html for more information about these flags. + - + See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html for more information about these flags. + + diff --git a/tests/testsuite/path.rs b/tests/testsuite/path.rs index 17179a520cc8..bb15d62faa8f 100644 --- a/tests/testsuite/path.rs +++ b/tests/testsuite/path.rs @@ -589,6 +589,387 @@ Caused by: .run(); } +#[cargo_test] +fn path_bases_not_stable() { + let bar = project() + .at("bar") + .file("Cargo.toml", &basic_manifest("bar", "0.5.0")) + .file("src/lib.rs", "") + .build(); + + let p = project() + .file( + ".cargo/config.toml", + &format!( + "[path-bases]\ntest = '{}'", + bar.root().parent().unwrap().display() + ), + ) + .file( + "Cargo.toml", + r#" + [package] + + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies.bar] + path = 'bar' + base = 'test' + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("build") + .with_status(101) + .with_stderr_data( + "\ +[ERROR] failed to parse manifest at `[..]/foo/Cargo.toml` + +Caused by: + usage of path bases requires `-Z path-bases` +", + ) + .run(); +} + +#[cargo_test] +fn patch_with_base() { + let bar = project() + .at("bar") + .file("Cargo.toml", &basic_manifest("bar", "0.5.0")) + .file("src/lib.rs", "pub fn hello() {}") + .build(); + Package::new("bar", "0.5.0").publish(); + + let p = project() + .file( + ".cargo/config.toml", + &format!( + "[path-bases]\ntest = '{}'", + bar.root().parent().unwrap().display() + ), + ) + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + edition = "2018" + + [dependencies] + bar = "0.5.0" + + [patch.crates-io.bar] + path = 'bar' + base = 'test' + "#, + ) + .file("src/lib.rs", "use bar::hello as _;") + .build(); + + p.cargo("build -v -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .run(); +} + +#[cargo_test] +fn path_with_base() { + let bar = project() + .at("bar") + .file("Cargo.toml", &basic_manifest("bar", "0.5.0")) + .file("src/lib.rs", "") + .build(); + + let p = project() + .file( + ".cargo/config.toml", + &format!( + "[path-bases]\ntest = '{}'", + bar.root().parent().unwrap().display() + ), + ) + .file( + "Cargo.toml", + r#" + [package] + + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies.bar] + path = 'bar' + base = 'test' + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("build -v -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .run(); +} + +#[cargo_test] +fn workspace_with_base() { + let bar = project() + .at("dep_with_base") + .file("Cargo.toml", &basic_manifest("dep_with_base", "0.5.0")) + .file("src/lib.rs", "") + .build(); + + let p = project() + .file( + ".cargo/config.toml", + &format!( + "[path-bases]\ntest = '{}'", + bar.root().parent().unwrap().display() + ), + ) + .file( + "Cargo.toml", + r#" + [package] + name = "parent" + version = "0.1.0" + authors = [] + + [workspace] + members = ["child"] + + [workspace.dependencies.dep_with_base] + path = 'dep_with_base' + base = 'test' + "#, + ) + .file("src/main.rs", "fn main() {}") + .file( + "child/Cargo.toml", + r#" + [package] + name = "child" + version = "0.1.0" + authors = [] + workspace = ".." + + [dependencies.dep_with_base] + workspace = true + "#, + ) + .file("child/src/main.rs", "fn main() {}"); + let p = p.build(); + + p.cargo("build -v -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .run(); +} + +#[cargo_test] +fn path_with_relative_base() { + project() + .at("shared_proj/bar") + .file("Cargo.toml", &basic_manifest("bar", "0.5.0")) + .file("src/lib.rs", "") + .build(); + + let p = project() + .file( + "../.cargo/config.toml", + "[path-bases]\ntest = 'shared_proj'", + ) + .file( + "Cargo.toml", + r#" + [package] + + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies.bar] + path = 'bar' + base = 'test' + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("build -v -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .run(); +} + +#[cargo_test] +fn workspace_builtin_base() { + project() + .at("dep_with_base") + .file("Cargo.toml", &basic_manifest("dep_with_base", "0.5.0")) + .file("src/lib.rs", "") + .build(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "parent" + version = "0.1.0" + authors = [] + + [workspace] + members = ["child"] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file( + "child/Cargo.toml", + r#" + [package] + name = "child" + version = "0.1.0" + authors = [] + workspace = ".." + + [dependencies.dep_with_base] + path = '../dep_with_base' + base = 'workspace' + "#, + ) + .file("child/src/main.rs", "fn main() {}"); + let p = p.build(); + + p.cargo("build -v -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .run(); +} + +#[cargo_test] +fn shadow_workspace_builtin_base() { + let bar = project() + .at("dep_with_base") + .file("Cargo.toml", &basic_manifest("dep_with_base", "0.5.0")) + .file("src/lib.rs", "") + .build(); + + let p = project() + .file( + ".cargo/config.toml", + &format!( + "[path-bases]\nworkspace = '{}/subdir'", + bar.root().parent().unwrap().display() + ), + ) + .file( + "Cargo.toml", + r#" + [package] + name = "parent" + version = "0.1.0" + authors = [] + + [workspace] + members = ["child"] + + [workspace.dependencies.dep_with_base] + path = '../dep_with_base' + base = 'workspace' + "#, + ) + .file("src/main.rs", "fn main() {}") + .file( + "child/Cargo.toml", + r#" + [package] + name = "child" + version = "0.1.0" + authors = [] + workspace = ".." + + [dependencies.dep_with_base] + workspace = true + "#, + ) + .file("child/src/main.rs", "fn main() {}"); + let p = p.build(); + + p.cargo("build -v -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .run(); +} + +#[cargo_test] +fn unknown_base() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies.bar] + path = 'bar' + base = 'test' + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("build -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .with_status(101) + .with_stderr_data( + "\ +[ERROR] failed to parse manifest at `[..]/foo/Cargo.toml` + +Caused by: + dependency (bar) uses an undefined path base `test`. You must add an entry for `test` in the Cargo configuration [path-bases] table. +", + ) + .run(); +} + +#[cargo_test] +fn workspace_builtin_base_not_a_workspace() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies.bar] + path = 'bar' + base = 'workspace' + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("build -Zpath-bases") + .masquerade_as_nightly_cargo(&["path-bases"]) + .with_status(101) + .with_stderr_data( + "\ +[ERROR] failed to parse manifest at `[..]/foo/Cargo.toml` + +Caused by: + dependency (bar) is using the `workspace` built-in path base outside of a workspace. +", + ) + .run(); +} + #[cargo_test] fn override_relative() { let bar = project()