Skip to content

Commit dc172a1

Browse files
committed
Implement base paths (RFC 3529) 2/n: support for nested paths
1 parent beab81a commit dc172a1

File tree

3 files changed

+155
-30
lines changed

3 files changed

+155
-30
lines changed

src/cargo/sources/path.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
use std::borrow::Cow;
12
use std::collections::{HashMap, HashSet};
23
use std::fmt::{self, Debug, Formatter};
34
use std::fs;
45
use std::io;
56
use std::path::{Path, PathBuf};
67
use std::task::Poll;
78

8-
use crate::core::{Dependency, EitherManifest, Manifest, Package, PackageId, SourceId};
9+
use crate::core::{
10+
find_workspace_root, Dependency, EitherManifest, Manifest, Package, PackageId, SourceId,
11+
};
912
use crate::ops;
1013
use crate::sources::source::MaybePackage;
1114
use crate::sources::source::QueryKind;
@@ -14,15 +17,17 @@ use crate::sources::IndexSummary;
1417
use crate::util::errors::CargoResult;
1518
use crate::util::important_paths::find_project_manifest_exact;
1619
use crate::util::internal;
17-
use crate::util::toml::read_manifest;
20+
use crate::util::toml::{lookup_path_base, read_manifest};
1821
use crate::util::GlobalContext;
19-
use anyhow::Context as _;
22+
use anyhow::{anyhow, Context as _};
2023
use cargo_util::paths;
24+
use cargo_util_schemas::manifest::PathBaseName;
2125
use filetime::FileTime;
2226
use gix::bstr::{BString, ByteVec};
2327
use gix::dir::entry::Status;
2428
use gix::index::entry::Stage;
2529
use ignore::gitignore::GitignoreBuilder;
30+
use lazycell::LazyCell;
2631
use tracing::{debug, info, trace, warn};
2732
use walkdir::WalkDir;
2833

@@ -300,7 +305,7 @@ impl<'gctx> RecursivePathSource<'gctx> {
300305
/// Discovers packages inside this source if it hasn't yet done.
301306
pub fn load(&mut self) -> CargoResult<()> {
302307
if !self.loaded {
303-
self.packages = read_packages(&self.path, self.source_id, self.gctx)?;
308+
self.packages = read_packages(&self.path, self.source_id)?;
304309
self.loaded = true;
305310
}
306311

@@ -808,7 +813,6 @@ fn last_modified_file(
808813
fn read_packages(
809814
path: &Path,
810815
source_id: SourceId,
811-
gctx: &GlobalContext,
812816
) -> CargoResult<HashMap<PackageId, Vec<Package>>> {
813817
let mut all_packages = HashMap::new();
814818
let mut visited = HashSet::<PathBuf>::new();
@@ -844,14 +848,7 @@ fn read_packages(
844848
}
845849

846850
if has_manifest(dir) {
847-
read_nested_packages(
848-
dir,
849-
&mut all_packages,
850-
source_id,
851-
gctx,
852-
&mut visited,
853-
&mut errors,
854-
)?;
851+
read_nested_packages(dir, &mut all_packages, source_id, &mut visited, &mut errors)?;
855852
}
856853
Ok(true)
857854
})?;
@@ -878,7 +875,7 @@ fn read_packages(
878875
}
879876
}
880877

881-
fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
878+
fn nested_paths(manifest: &Manifest) -> Vec<(PathBuf, Option<PathBaseName>)> {
882879
let mut nested_paths = Vec::new();
883880
let normalized = manifest.normalized_toml();
884881
let dependencies = normalized
@@ -910,7 +907,7 @@ fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
910907
let Some(path) = dep.path.as_ref() else {
911908
continue;
912909
};
913-
nested_paths.push(PathBuf::from(path.as_str()));
910+
nested_paths.push((PathBuf::from(path.as_str()), dep.base.clone()));
914911
}
915912
}
916913
nested_paths
@@ -952,7 +949,6 @@ fn read_nested_packages(
952949
path: &Path,
953950
all_packages: &mut HashMap<PackageId, Vec<Package>>,
954951
source_id: SourceId,
955-
gctx: &GlobalContext,
956952
visited: &mut HashSet<PathBuf>,
957953
errors: &mut Vec<anyhow::Error>,
958954
) -> CargoResult<()> {
@@ -961,8 +957,10 @@ fn read_nested_packages(
961957
}
962958

963959
let manifest_path = find_project_manifest_exact(path, "Cargo.toml")?;
960+
let mut manifest_gctx = GlobalContext::default()?;
961+
manifest_gctx.reload_rooted_at(&manifest_path)?;
964962

965-
let manifest = match read_manifest(&manifest_path, source_id, gctx) {
963+
let manifest = match read_manifest(&manifest_path, source_id, &manifest_gctx) {
966964
Err(err) => {
967965
// Ignore malformed manifests found on git repositories
968966
//
@@ -1000,10 +998,32 @@ fn read_nested_packages(
1000998
//
1001999
// TODO: filesystem/symlink implications?
10021000
if !source_id.is_registry() {
1003-
for p in nested.iter() {
1004-
let path = paths::normalize_path(&path.join(p));
1005-
let result =
1006-
read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
1001+
let workspace_root_cell: LazyCell<PathBuf> = LazyCell::new();
1002+
1003+
for (p, base) in nested.iter() {
1004+
let p = if let Some(base) = base {
1005+
let workspace_root = || {
1006+
workspace_root_cell
1007+
.try_borrow_with(|| {
1008+
find_workspace_root(&manifest_path, &manifest_gctx)?
1009+
.ok_or_else(|| anyhow!("failed to find a workspace root"))
1010+
})
1011+
.map(|p| p.as_path())
1012+
};
1013+
// Pass in `None` for the `cargo-features` not to skip verification: when the
1014+
// package is loaded as a dependency, then it will be checked.
1015+
match lookup_path_base(base, &manifest_gctx, &workspace_root, None) {
1016+
Ok(base) => Cow::Owned(base.join(p)),
1017+
Err(err) => {
1018+
errors.push(err);
1019+
continue;
1020+
}
1021+
}
1022+
} else {
1023+
Cow::Borrowed(p)
1024+
};
1025+
let path = paths::normalize_path(&path.join(p.as_path()));
1026+
let result = read_nested_packages(&path, all_packages, source_id, visited, errors);
10071027
// Ignore broken manifests found on git repositories.
10081028
//
10091029
// A well formed manifest might still fail to load due to reasons

src/cargo/util/toml/mod.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ fn normalize_toml(
312312
inherit_cell
313313
.try_borrow_with(|| load_inheritable_fields(gctx, manifest_file, &workspace_config))
314314
};
315-
let workspace_root = || inherit().map(|fields| fields.ws_root());
315+
let workspace_root = || inherit().map(|fields| fields.ws_root().as_path());
316316

317317
if let Some(original_package) = original_toml.package() {
318318
let package_name = &original_package.name;
@@ -538,7 +538,7 @@ fn normalize_toml(
538538
fn normalize_patch<'a>(
539539
gctx: &GlobalContext,
540540
original_patch: Option<&BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>,
541-
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
541+
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
542542
features: &Features,
543543
) -> CargoResult<Option<BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>> {
544544
if let Some(patch) = original_patch {
@@ -757,7 +757,7 @@ fn normalize_dependencies<'a>(
757757
activated_opt_deps: &HashSet<&str>,
758758
kind: Option<DepKind>,
759759
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
760-
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
760+
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
761761
package_root: &Path,
762762
warnings: &mut Vec<String>,
763763
) -> CargoResult<Option<BTreeMap<manifest::PackageName, manifest::InheritableDependency>>> {
@@ -839,12 +839,13 @@ fn normalize_dependencies<'a>(
839839
fn normalize_path_dependency<'a>(
840840
gctx: &GlobalContext,
841841
detailed_dep: &mut TomlDetailedDependency,
842-
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
842+
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
843843
features: &Features,
844844
) -> CargoResult<()> {
845845
if let Some(base) = detailed_dep.base.take() {
846846
if let Some(path) = detailed_dep.path.as_mut() {
847-
let new_path = lookup_path_base(&base, gctx, workspace_root, features)?.join(&path);
847+
let new_path =
848+
lookup_path_base(&base, gctx, workspace_root, Some(features))?.join(&path);
848849
*path = new_path.to_str().unwrap().to_string();
849850
} else {
850851
bail!("`base` can only be used with path dependencies");
@@ -2225,10 +2226,12 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
22252226
pub(crate) fn lookup_path_base<'a>(
22262227
base: &PathBaseName,
22272228
gctx: &GlobalContext,
2228-
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
2229-
features: &Features,
2229+
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
2230+
features: Option<&Features>,
22302231
) -> CargoResult<PathBuf> {
2231-
features.require(Feature::path_bases())?;
2232+
if let Some(features) = features {
2233+
features.require(Feature::path_bases())?;
2234+
}
22322235

22332236
// HACK: The `base` string is user controlled, but building the path is safe from injection
22342237
// attacks since the `PathBaseName` type restricts the characters that can be used to exclude `.`
@@ -2240,7 +2243,7 @@ pub(crate) fn lookup_path_base<'a>(
22402243
} else {
22412244
// Otherwise, check the built-in bases.
22422245
match base.as_str() {
2243-
"workspace" => Ok(workspace_root()?.clone()),
2246+
"workspace" => Ok(workspace_root()?.to_path_buf()),
22442247
_ => bail!(
22452248
"path base `{base}` is undefined. \
22462249
You must add an entry for `{base}` in the Cargo configuration [path-bases] table."

tests/testsuite/git.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,108 @@ 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(
391+
".cargo/config.toml",
392+
r#"
393+
[path-bases]
394+
submodules = 'submods'
395+
"#,
396+
)
397+
.file(
398+
"Cargo.toml",
399+
r#"
400+
cargo-features = ["path-bases"]
401+
402+
[package]
403+
name = "dep1"
404+
version = "0.5.0"
405+
edition = "2015"
406+
authors = ["[email protected]"]
407+
408+
[dependencies]
409+
dep2 = { version = "0.5.0", base = "submodules", path = "dep2" }
410+
411+
[lib]
412+
name = "dep1"
413+
"#,
414+
)
415+
.file(
416+
"src/dep1.rs",
417+
r#"
418+
extern crate dep2;
419+
420+
pub fn hello() -> &'static str {
421+
dep2::hello()
422+
}
423+
"#,
424+
)
425+
});
426+
427+
let git_project2 = git::new("dep2", |project| {
428+
project
429+
.file("Cargo.toml", &basic_lib_manifest("dep2"))
430+
.file(
431+
"src/dep2.rs",
432+
r#"
433+
pub fn hello() -> &'static str {
434+
"hello world"
435+
}
436+
"#,
437+
)
438+
});
439+
440+
let repo = git2::Repository::open(&git_project.root()).unwrap();
441+
let url = git_project2.root().to_url().to_string();
442+
git::add_submodule(&repo, &url, Path::new("submods/dep2"));
443+
git::commit(&repo);
444+
445+
let p = project()
446+
.file(
447+
"Cargo.toml",
448+
&format!(
449+
r#"
450+
[package]
451+
name = "foo"
452+
version = "0.5.0"
453+
edition = "2015"
454+
authors = ["[email protected]"]
455+
456+
[dependencies]
457+
dep1 = {{ version = "0.5.0", git = '{}' }}
458+
459+
[[bin]]
460+
name = "foo"
461+
"#,
462+
git_project.url()
463+
),
464+
)
465+
.file(
466+
"src/foo.rs",
467+
&main_file(r#""{}", dep1::hello()"#, &["dep1"]),
468+
)
469+
.build();
470+
471+
p.cargo("build")
472+
.masquerade_as_nightly_cargo(&["path-bases"])
473+
.run();
474+
475+
assert!(p.bin("foo").is_file());
476+
477+
p.process(&p.bin("foo"))
478+
.with_stdout_data(str![[r#"
479+
hello world
480+
481+
"#]])
482+
.run();
483+
}
484+
383485
#[cargo_test]
384486
fn cargo_compile_with_malformed_nested_paths() {
385487
let git_project = git::new("dep1", |project| {

0 commit comments

Comments
 (0)