Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement path-bases (RFC 3529) 2/n: support for nested packages #14416

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/cargo/sources/path.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::{self, Debug, Formatter};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::task::Poll;

use crate::core::{Dependency, EitherManifest, Manifest, Package, PackageId, SourceId};
use crate::core::{
find_workspace_root, Dependency, EitherManifest, Manifest, Package, PackageId, SourceId,
};
use crate::ops;
use crate::sources::source::MaybePackage;
use crate::sources::source::QueryKind;
Expand All @@ -14,15 +17,17 @@ use crate::sources::IndexSummary;
use crate::util::errors::CargoResult;
use crate::util::important_paths::find_project_manifest_exact;
use crate::util::internal;
use crate::util::toml::read_manifest;
use crate::util::toml::{lookup_path_base, read_manifest};
use crate::util::GlobalContext;
use anyhow::Context as _;
use anyhow::{anyhow, Context as _};
use cargo_util::paths;
use cargo_util_schemas::manifest::PathBaseName;
use filetime::FileTime;
use gix::bstr::{BString, ByteVec};
use gix::dir::entry::Status;
use gix::index::entry::Stage;
use ignore::gitignore::GitignoreBuilder;
use lazycell::LazyCell;
use tracing::{debug, info, trace, warn};
use walkdir::WalkDir;

Expand Down Expand Up @@ -878,7 +883,7 @@ fn read_packages(
}
}

fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
fn nested_paths(manifest: &Manifest) -> Vec<(PathBuf, Option<PathBaseName>)> {
let mut nested_paths = Vec::new();
let normalized = manifest.normalized_toml();
let dependencies = normalized
Expand Down Expand Up @@ -910,7 +915,7 @@ fn nested_paths(manifest: &Manifest) -> Vec<PathBuf> {
let Some(path) = dep.path.as_ref() else {
continue;
};
nested_paths.push(PathBuf::from(path.as_str()));
nested_paths.push((PathBuf::from(path.as_str()), dep.base.clone()));
}
}
nested_paths
Expand Down Expand Up @@ -1000,8 +1005,31 @@ fn read_nested_packages(
//
// TODO: filesystem/symlink implications?
if !source_id.is_registry() {
for p in nested.iter() {
let path = paths::normalize_path(&path.join(p));
let workspace_root_cell: LazyCell<PathBuf> = LazyCell::new();

for (p, base) in nested.iter() {
let p = if let Some(base) = base {
let workspace_root = || {
workspace_root_cell
.try_borrow_with(|| {
find_workspace_root(&manifest_path, gctx)?
.ok_or_else(|| anyhow!("failed to find a workspace root"))
})
.map(|p| p.as_path())
};
// Pass in `None` for the `cargo-features` not to skip verification: when the
// package is loaded as a dependency, then it will be checked.
match lookup_path_base(base, gctx, &workspace_root, None) {
Ok(base) => Cow::Owned(base.join(p)),
Err(err) => {
errors.push(err);
continue;
}
}
} else {
Cow::Borrowed(p)
};
let path = paths::normalize_path(&path.join(p.as_path()));
Comment on lines +1008 to +1032
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Aren't we operating on a normalized package and don't we resolve path-bases during normaization?

For either of us to verify that is one of the reasons I recommend splitting commits into

  • Test showing the existing behavior
  • Fix/feature with test updates to show how the behavior changed

let result =
read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
// Ignore broken manifests found on git repositories.
Expand Down
21 changes: 12 additions & 9 deletions src/cargo/util/toml/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ fn normalize_toml(
inherit_cell
.try_borrow_with(|| load_inheritable_fields(gctx, manifest_file, &workspace_config))
};
let workspace_root = || inherit().map(|fields| fields.ws_root());
let workspace_root = || inherit().map(|fields| fields.ws_root().as_path());

if let Some(original_package) = original_toml.package() {
let package_name = &original_package.name;
Expand Down Expand Up @@ -538,7 +538,7 @@ fn normalize_toml(
fn normalize_patch<'a>(
gctx: &GlobalContext,
original_patch: Option<&BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>,
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
features: &Features,
) -> CargoResult<Option<BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>> {
if let Some(patch) = original_patch {
Expand Down Expand Up @@ -757,7 +757,7 @@ fn normalize_dependencies<'a>(
activated_opt_deps: &HashSet<&str>,
kind: Option<DepKind>,
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
package_root: &Path,
warnings: &mut Vec<String>,
) -> CargoResult<Option<BTreeMap<manifest::PackageName, manifest::InheritableDependency>>> {
Expand Down Expand Up @@ -839,12 +839,13 @@ fn normalize_dependencies<'a>(
fn normalize_path_dependency<'a>(
gctx: &GlobalContext,
detailed_dep: &mut TomlDetailedDependency,
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
features: &Features,
) -> CargoResult<()> {
if let Some(base) = detailed_dep.base.take() {
if let Some(path) = detailed_dep.path.as_mut() {
let new_path = lookup_path_base(&base, gctx, workspace_root, features)?.join(&path);
let new_path =
lookup_path_base(&base, gctx, workspace_root, Some(features))?.join(&path);
*path = new_path.to_str().unwrap().to_string();
} else {
bail!("`base` can only be used with path dependencies");
Expand Down Expand Up @@ -2225,10 +2226,12 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
pub(crate) fn lookup_path_base<'a>(
base: &PathBaseName,
gctx: &GlobalContext,
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
features: &Features,
workspace_root: &dyn Fn() -> CargoResult<&'a Path>,
features: Option<&Features>,
) -> CargoResult<PathBuf> {
features.require(Feature::path_bases())?;
if let Some(features) = features {
features.require(Feature::path_bases())?;
}

// HACK: The `base` string is user controlled, but building the path is safe from injection
// attacks since the `PathBaseName` type restricts the characters that can be used to exclude `.`
Expand All @@ -2240,7 +2243,7 @@ pub(crate) fn lookup_path_base<'a>(
} else {
// Otherwise, check the built-in bases.
match base.as_str() {
"workspace" => Ok(workspace_root()?.clone()),
"workspace" => Ok(workspace_root()?.to_path_buf()),
_ => bail!(
"path base `{base}` is undefined. \
You must add an entry for `{base}` in the Cargo configuration [path-bases] table."
Expand Down
8 changes: 7 additions & 1 deletion src/doc/src/reference/unstable.md
Original file line number Diff line number Diff line change
Expand Up @@ -1612,7 +1612,7 @@ character, and must not be empty.
If the name of path base used in a dependency is neither in the configuration
nor one of the built-in path base, then Cargo will raise an error.

#### Built-in path bases
### Built-in path bases

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

### Path bases in git dependencies and patches

Configuration files in git dependencies and patches are not loaded by Cargo and
so any path bases used in those packages will need to be defined in some
configuration that is loaded by Cargo.

## lockfile-path
* Original Issue: [#5707](https://github.com/rust-lang/cargo/issues/5707)
* Tracking Issue: [#14421](https://github.com/rust-lang/cargo/issues/14421)
Expand Down
105 changes: 105 additions & 0 deletions tests/testsuite/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use cargo_test_support::registry::Package;
use cargo_test_support::{basic_lib_manifest, basic_manifest, git, main_file, project};
use cargo_test_support::{sleep_ms, str, t, Project};

use crate::config::write_config_at;

#[cargo_test]
fn cargo_compile_simple_git_dep() {
let project = project();
Expand Down Expand Up @@ -380,6 +382,109 @@ hello world
.run();
}

#[cargo_test]
fn dependency_in_submodule_via_path_base() {
// Using a submodule prevents the dependency from being discovered during the directory walk,
// so Cargo will need to follow the path dependency to discover it.

let git_project = git::new("dep1", |project| {
project
.file(
"Cargo.toml",
r#"
cargo-features = ["path-bases"]

[package]
name = "dep1"
version = "0.5.0"
edition = "2015"
authors = ["[email protected]"]

[dependencies]
dep2 = { version = "0.5.0", base = "submodules", path = "dep2" }

[lib]
name = "dep1"
"#,
)
.file(
"src/dep1.rs",
r#"
extern crate dep2;

pub fn hello() -> &'static str {
dep2::hello()
}
"#,
)
});

let git_project2 = git::new("dep2", |project| {
project
.file("Cargo.toml", &basic_lib_manifest("dep2"))
.file(
"src/dep2.rs",
r#"
pub fn hello() -> &'static str {
"hello world"
}
"#,
)
});

let repo = git2::Repository::open(&git_project.root()).unwrap();
let url = git_project2.root().to_url().to_string();
git::add_submodule(&repo, &url, Path::new("submods/dep2"));
git::commit(&repo);

write_config_at(
paths::home().join(".cargo/config.toml"),
r#"
[path-bases]
submodules = '../dep1/submods'
"#,
);
Comment on lines +440 to +446
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo writing path-bases on behalf of git dependencies seems very questionable

  • Constructing the right absolute path is a mess
  • Constructing the right relative path is a mess

If you want to test RecursivePathSource, https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#paths-overrides is another user of that tyoe


let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
version = "0.5.0"
edition = "2015"
authors = ["[email protected]"]

[dependencies]
dep1 = {{ version = "0.5.0", git = '{}' }}

[[bin]]
name = "foo"
"#,
git_project.url()
),
)
.file(
"src/foo.rs",
&main_file(r#""{}", dep1::hello()"#, &["dep1"]),
)
.build();

p.cargo("build")
.masquerade_as_nightly_cargo(&["path-bases"])
.run();

assert!(p.bin("foo").is_file());

p.process(&p.bin("foo"))
.with_stdout_data(str![[r#"
hello world

"#]])
.run();
}

#[cargo_test]
fn cargo_compile_with_malformed_nested_paths() {
let git_project = git::new("dep1", |project| {
Expand Down