Skip to content

Commit 6df401f

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

File tree

4 files changed

+159
-17
lines changed

4 files changed

+159
-17
lines changed

src/cargo/sources/path.rs

+35-7
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

@@ -878,7 +883,7 @@ fn read_packages(
878883
}
879884
}
880885

881-
fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
886+
fn nested_paths(manifest: &Manifest) -> Vec<(PathBuf, Option<PathBaseName>)> {
882887
let mut nested_paths = Vec::new();
883888
let normalized = manifest.normalized_toml();
884889
let dependencies = normalized
@@ -910,7 +915,7 @@ fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
910915
let Some(path) = dep.path.as_ref() else {
911916
continue;
912917
};
913-
nested_paths.push(PathBuf::from(path.as_str()));
918+
nested_paths.push((PathBuf::from(path.as_str()), dep.base.clone()));
914919
}
915920
}
916921
nested_paths
@@ -1000,8 +1005,31 @@ 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 workspace_root_cell: LazyCell<PathBuf> = LazyCell::new();
1009+
1010+
for (p, base) in nested.iter() {
1011+
let p = if let Some(base) = base {
1012+
let workspace_root = || {
1013+
workspace_root_cell
1014+
.try_borrow_with(|| {
1015+
find_workspace_root(&manifest_path, gctx)?
1016+
.ok_or_else(|| anyhow!("failed to find a workspace root"))
1017+
})
1018+
.map(|p| p.as_path())
1019+
};
1020+
// Pass in `None` for the `cargo-features` not to skip verification: when the
1021+
// package is loaded as a dependency, then it will be checked.
1022+
match lookup_path_base(base, gctx, &workspace_root, None) {
1023+
Ok(base) => Cow::Owned(base.join(p)),
1024+
Err(err) => {
1025+
errors.push(err);
1026+
continue;
1027+
}
1028+
}
1029+
} else {
1030+
Cow::Borrowed(p)
1031+
};
1032+
let path = paths::normalize_path(&path.join(p.as_path()));
10051033
let result =
10061034
read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
10071035
// Ignore broken manifests found on git repositories.

src/cargo/util/toml/mod.rs

+12-9
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."

src/doc/src/reference/unstable.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1611,7 +1611,7 @@ character, and must not be empty.
16111611
If the name of path base used in a dependency is neither in the configuration
16121612
nor one of the built-in path base, then Cargo will raise an error.
16131613

1614-
#### Built-in path bases
1614+
### Built-in path bases
16151615

16161616
Cargo provides implicit path bases that can be used without the need to specify
16171617
them in a `[path-bases]` table.
@@ -1625,6 +1625,12 @@ will prefer the value in the configuration. The allows Cargo to add new built-in
16251625
path bases without compatibility issues (as existing uses will shadow the
16261626
built-in name).
16271627

1628+
### Path bases in git dependencies and patches
1629+
1630+
Configuration files in git dependencies and patches are not loaded by Cargo and
1631+
so any path bases used in those packages will need to be defined in some
1632+
configuration that is loaded by Cargo.
1633+
16281634
# Stabilized and removed features
16291635

16301636
## Compile progress

tests/testsuite/git.rs

+105
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use cargo_test_support::registry::Package;
1717
use cargo_test_support::{basic_lib_manifest, basic_manifest, git, main_file, project};
1818
use cargo_test_support::{sleep_ms, str, t, Project};
1919

20+
use crate::config::write_config_at;
21+
2022
#[cargo_test]
2123
fn cargo_compile_simple_git_dep() {
2224
let project = project();
@@ -380,6 +382,109 @@ hello world
380382
.run();
381383
}
382384

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

0 commit comments

Comments
 (0)