Skip to content

Commit b75dd6b

Browse files
committed
Implement support for base paths
1 parent fa64658 commit b75dd6b

File tree

7 files changed

+596
-28
lines changed

7 files changed

+596
-28
lines changed

crates/cargo-util-schemas/src/manifest/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,7 @@ pub struct TomlDetailedDependency<P: Clone = String> {
776776
// `path` is relative to the file it appears in. If that's a `Cargo.toml`, it'll be relative to
777777
// that TOML file, and if it's a `.cargo/config` file, it'll be relative to that file.
778778
pub path: Option<P>,
779+
pub base: Option<String>,
779780
pub git: Option<String>,
780781
pub branch: Option<String>,
781782
pub tag: Option<String>,
@@ -815,6 +816,7 @@ impl<P: Clone> Default for TomlDetailedDependency<P> {
815816
registry: Default::default(),
816817
registry_index: Default::default(),
817818
path: Default::default(),
819+
base: Default::default(),
818820
git: Default::default(),
819821
branch: Default::default(),
820822
tag: Default::default(),

src/cargo/core/features.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,7 @@ unstable_cli_options!(
776776
no_index_update: bool = ("Do not update the registry index even if the cache is outdated"),
777777
package_workspace: bool = ("Handle intra-workspace dependencies when packaging"),
778778
panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"),
779+
path_bases: bool = ("Allow paths that resolve relatively to a base specified in the config"),
779780
profile_rustflags: bool = ("Enable the `rustflags` option in profiles in .cargo/config.toml file"),
780781
public_dependency: bool = ("Respect a dependency's `public` field in Cargo.toml to control public/private dependencies"),
781782
publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"),
@@ -1280,6 +1281,7 @@ impl CliUnstable {
12801281
"package-workspace" => self.package_workspace= parse_empty(k, v)?,
12811282
"panic-abort-tests" => self.panic_abort_tests = parse_empty(k, v)?,
12821283
"public-dependency" => self.public_dependency = parse_empty(k, v)?,
1284+
"path-bases" => self.path_bases = parse_empty(k, v)?,
12831285
"profile-rustflags" => self.profile_rustflags = parse_empty(k, v)?,
12841286
"trim-paths" => self.trim_paths = parse_empty(k, v)?,
12851287
"publish-timeout" => self.publish_timeout = parse_empty(k, v)?,

src/cargo/sources/path.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::borrow::Cow;
12
use std::collections::{HashMap, HashSet};
23
use std::fmt::{self, Debug, Formatter};
34
use std::fs;
@@ -14,7 +15,7 @@ use crate::sources::IndexSummary;
1415
use crate::util::errors::CargoResult;
1516
use crate::util::important_paths::find_project_manifest_exact;
1617
use crate::util::internal;
17-
use crate::util::toml::read_manifest;
18+
use crate::util::toml::{lookup_path_base, read_manifest};
1819
use crate::util::GlobalContext;
1920
use anyhow::Context as _;
2021
use cargo_util::paths;
@@ -878,7 +879,7 @@ fn read_packages(
878879
}
879880
}
880881

881-
fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
882+
fn nested_paths(manifest: &Manifest) -> Vec<(String, PathBuf, Option<String>)> {
882883
let mut nested_paths = Vec::new();
883884
let normalized = manifest.normalized_toml();
884885
let dependencies = normalized
@@ -900,7 +901,7 @@ fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
900901
}),
901902
);
902903
for dep_table in dependencies {
903-
for dep in dep_table.values() {
904+
for (name, dep) in dep_table.iter() {
904905
let cargo_util_schemas::manifest::InheritableDependency::Value(dep) = dep else {
905906
continue;
906907
};
@@ -910,7 +911,11 @@ fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
910911
let Some(path) = dep.path.as_ref() else {
911912
continue;
912913
};
913-
nested_paths.push(PathBuf::from(path.as_str()));
914+
nested_paths.push((
915+
name.to_string(),
916+
PathBuf::from(path.as_str()),
917+
dep.base.clone(),
918+
));
914919
}
915920
}
916921
nested_paths
@@ -1000,8 +1005,36 @@ fn read_nested_packages(
10001005
//
10011006
// TODO: filesystem/symlink implications?
10021007
if !source_id.is_registry() {
1003-
for p in nested.iter() {
1004-
let path = paths::normalize_path(&path.join(p));
1008+
let mut manifest_gctx = None;
1009+
1010+
for (name, p, base) in nested.iter() {
1011+
let p = if let Some(base) = base {
1012+
// If the dependency has a path base, then load the global context for the current
1013+
// manifest and use it to resolve the path base.
1014+
let manifest_gctx = match manifest_gctx {
1015+
Some(ref gctx) => gctx,
1016+
None => {
1017+
let mut new_manifest_gctx = match GlobalContext::default() {
1018+
Ok(gctx) => gctx,
1019+
Err(err) => return Err(err),
1020+
};
1021+
if let Err(err) = new_manifest_gctx.reload_rooted_at(&manifest_path) {
1022+
return Err(err);
1023+
}
1024+
manifest_gctx.insert(new_manifest_gctx)
1025+
}
1026+
};
1027+
match lookup_path_base(base, name, manifest_gctx, manifest_path.parent().unwrap()) {
1028+
Ok(base) => Cow::Owned(base.join(p)),
1029+
Err(err) => {
1030+
errors.push(err);
1031+
continue;
1032+
}
1033+
}
1034+
} else {
1035+
Cow::Borrowed(p)
1036+
};
1037+
let path = paths::normalize_path(&path.join(p.as_path()));
10051038
let result =
10061039
read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
10071040
// Ignore broken manifests found on git repositories.
@@ -1019,6 +1052,7 @@ fn read_nested_packages(
10191052
);
10201053
errors.push(err);
10211054
} else {
1055+
trace!("Failed to manifest: {:?}", err);
10221056
return Err(err);
10231057
}
10241058
}

src/cargo/util/toml/mod.rs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -901,13 +901,17 @@ impl InheritableFields {
901901
};
902902
let mut dep = dep.clone();
903903
if let manifest::TomlDependency::Detailed(detailed) = &mut dep {
904-
if let Some(rel_path) = &detailed.path {
905-
detailed.path = Some(resolve_relative_path(
906-
name,
907-
self.ws_root(),
908-
package_root,
909-
rel_path,
910-
)?);
904+
if detailed.base.is_none() {
905+
// If this is a path dependency without a base, then update the path to be relative
906+
// to the workspace root instead.
907+
if let Some(rel_path) = &detailed.path {
908+
detailed.path = Some(resolve_relative_path(
909+
name,
910+
self.ws_root(),
911+
package_root,
912+
rel_path,
913+
)?);
914+
}
911915
}
912916
}
913917
Ok(dep)
@@ -2135,7 +2139,17 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
21352139
// always end up hashing to the same value no matter where it's
21362140
// built from.
21372141
if manifest_ctx.source_id.is_path() {
2138-
let path = manifest_ctx.root.join(path);
2142+
let path = if let Some(base) = orig.base.as_ref() {
2143+
if !manifest_ctx.gctx.cli_unstable().path_bases {
2144+
bail!("usage of path bases requires `-Z path-bases`");
2145+
}
2146+
2147+
lookup_path_base(&base, name_in_toml, manifest_ctx.gctx, manifest_ctx.root)?
2148+
.join(path)
2149+
} else {
2150+
// This is a standard path with no prefix.
2151+
manifest_ctx.root.join(path)
2152+
};
21392153
let path = paths::normalize_path(&path);
21402154
SourceId::for_path(&path)
21412155
} else {
@@ -2151,6 +2165,37 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
21512165
}
21522166
}
21532167

2168+
pub(crate) fn lookup_path_base(
2169+
base: &str,
2170+
name_in_toml: &str,
2171+
gctx: &GlobalContext,
2172+
manifest_root: &Path,
2173+
) -> CargoResult<PathBuf> {
2174+
// Look up the relevant base in the Config and use that as the root.
2175+
if let Some(path_bases) =
2176+
gctx.get::<Option<ConfigRelativePath>>(&format!("path-bases.{base}"))?
2177+
{
2178+
Ok(path_bases.resolve_path(gctx))
2179+
} else {
2180+
// Otherwise, check the built-in bases.
2181+
match base {
2182+
"workspace" => {
2183+
if let Some(workspace_root) = find_workspace_root(manifest_root, gctx)? {
2184+
Ok(workspace_root.parent().unwrap().to_path_buf())
2185+
} else {
2186+
bail!(
2187+
"dependency ({name_in_toml}) is using the `workspace` built-in path base outside of a workspace."
2188+
)
2189+
}
2190+
}
2191+
_ => bail!(
2192+
"dependency ({name_in_toml}) uses an undefined path base `{base}`. \
2193+
You must add an entry for `{base}` in the Cargo configuration [path-bases] table."
2194+
),
2195+
}
2196+
}
2197+
}
2198+
21542199
pub trait ResolveToPath {
21552200
fn resolve(&self, gctx: &GlobalContext) -> PathBuf;
21562201
}
@@ -2865,6 +2910,7 @@ fn prepare_toml_for_publish(
28652910
let mut d = d.clone();
28662911
// Path dependencies become crates.io deps.
28672912
d.path.take();
2913+
d.base.take();
28682914
// Same with git dependencies.
28692915
d.git.take();
28702916
d.branch.take();

tests/testsuite/cargo/z_help/stdout.term.svg

Lines changed: 16 additions & 14 deletions
Loading

tests/testsuite/git.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,107 @@ hello world
380380
.run();
381381
}
382382

383+
#[cargo_test]
384+
fn dependency_in_submodule_via_path_base() {
385+
// Using a submodule prevents the dependency from being discovered during the directory walk,
386+
// so Cargo will need to follow the path dependency to discover it.
387+
388+
let git_project = git::new("dep1", |project| {
389+
project
390+
.file(".cargo/config.toml", "[path-bases]\nsubmodules = 'submods'")
391+
.file(
392+
"Cargo.toml",
393+
r#"
394+
[package]
395+
396+
name = "dep1"
397+
version = "0.5.0"
398+
edition = "2015"
399+
authors = ["[email protected]"]
400+
401+
[dependencies.dep2]
402+
403+
version = "0.5.0"
404+
path = "dep2"
405+
base = "submodules"
406+
407+
[lib]
408+
409+
name = "dep1"
410+
"#,
411+
)
412+
.file(
413+
"src/dep1.rs",
414+
r#"
415+
extern crate dep2;
416+
417+
pub fn hello() -> &'static str {
418+
dep2::hello()
419+
}
420+
"#,
421+
)
422+
});
423+
424+
let git_project2 = git::new("dep2", |project| {
425+
project
426+
.file("Cargo.toml", &basic_lib_manifest("dep2"))
427+
.file(
428+
"src/dep2.rs",
429+
r#"
430+
pub fn hello() -> &'static str {
431+
"hello world"
432+
}
433+
"#,
434+
)
435+
});
436+
437+
let repo = git2::Repository::open(&git_project.root()).unwrap();
438+
let url = git_project2.root().to_url().to_string();
439+
git::add_submodule(&repo, &url, Path::new("submods/dep2"));
440+
git::commit(&repo);
441+
442+
let p = project()
443+
.file(
444+
"Cargo.toml",
445+
&format!(
446+
r#"
447+
[package]
448+
449+
name = "foo"
450+
version = "0.5.0"
451+
edition = "2015"
452+
authors = ["[email protected]"]
453+
454+
[dependencies.dep1]
455+
456+
version = "0.5.0"
457+
git = '{}'
458+
459+
[[bin]]
460+
461+
name = "foo"
462+
"#,
463+
git_project.url()
464+
),
465+
)
466+
.file(
467+
"src/foo.rs",
468+
&main_file(r#""{}", dep1::hello()"#, &["dep1"]),
469+
)
470+
.build();
471+
472+
p.cargo("build").run();
473+
474+
assert!(p.bin("foo").is_file());
475+
476+
p.process(&p.bin("foo"))
477+
.with_stdout_data(str![[r#"
478+
hello world
479+
480+
"#]])
481+
.run();
482+
}
483+
383484
#[cargo_test]
384485
fn cargo_compile_with_malformed_nested_paths() {
385486
let git_project = git::new("dep1", |project| {

0 commit comments

Comments
 (0)