Skip to content

Commit b22bb51

Browse files
committed
Implement support for base paths
1 parent 403bc5b commit b22bb51

File tree

62 files changed

+1374
-76
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1374
-76
lines changed

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

+2
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/bin/cargo/commands/add.rs

+8
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ Example uses:
100100
.help("Filesystem path to local crate to add")
101101
.group("selected")
102102
.conflicts_with("git"),
103+
clap::Arg::new("base")
104+
.long("base")
105+
.action(ArgAction::Set)
106+
.value_name("BASE")
107+
.help("The path base to use when adding from a local crate.")
108+
.requires("path"),
103109
clap::Arg::new("git")
104110
.long("git")
105111
.action(ArgAction::Set)
@@ -223,6 +229,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
223229

224230
fn parse_dependencies(gctx: &GlobalContext, matches: &ArgMatches) -> CargoResult<Vec<DepOp>> {
225231
let path = matches.get_one::<String>("path");
232+
let base = matches.get_one::<String>("base");
226233
let git = matches.get_one::<String>("git");
227234
let branch = matches.get_one::<String>("branch");
228235
let rev = matches.get_one::<String>("rev");
@@ -328,6 +335,7 @@ fn parse_dependencies(gctx: &GlobalContext, matches: &ArgMatches) -> CargoResult
328335
public,
329336
registry: registry.clone(),
330337
path: path.map(String::from),
338+
base: base.map(String::from),
331339
git: git.map(String::from),
332340
branch: branch.map(String::from),
333341
rev: rev.map(String::from),

src/cargo/core/features.rs

+2
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/ops/cargo_add/mod.rs

+23-5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::core::Workspace;
2828
use crate::sources::source::QueryKind;
2929
use crate::util::cache_lock::CacheLockMode;
3030
use crate::util::style;
31+
use crate::util::toml::lookup_path_base;
3132
use crate::util::toml_mut::dependency::Dependency;
3233
use crate::util::toml_mut::dependency::GitSource;
3334
use crate::util::toml_mut::dependency::MaybeWorkspace;
@@ -270,8 +271,11 @@ pub struct DepOp {
270271
/// Registry for looking up dependency version
271272
pub registry: Option<String>,
272273

273-
/// Git repo for dependency
274+
/// File system path for dependency
274275
pub path: Option<String>,
276+
/// Specify a named base for a path dependency
277+
pub base: Option<String>,
278+
275279
/// Git repo for dependency
276280
pub git: Option<String>,
277281
/// Specify an alternative git branch
@@ -331,10 +335,19 @@ fn resolve_dependency(
331335
};
332336
selected
333337
} else if let Some(raw_path) = &arg.path {
334-
let path = paths::normalize_path(&std::env::current_dir()?.join(raw_path));
335-
let src = PathSource::new(&path);
338+
let (path, base_name_and_value) = if let Some(base_name) = &arg.base {
339+
let base_value = lookup_path_base(base_name, gctx, manifest.path.parent().unwrap())?;
340+
(
341+
base_value.join(raw_path),
342+
Some((base_name.clone(), base_value)),
343+
)
344+
} else {
345+
(std::env::current_dir()?.join(raw_path), None)
346+
};
347+
let path = paths::normalize_path(&path);
348+
let src = PathSource::new(path);
336349

337-
let selected = if let Some(crate_spec) = &crate_spec {
350+
let mut selected = if let Some(crate_spec) = &crate_spec {
338351
if let Some(v) = crate_spec.version_req() {
339352
// crate specifier includes a version (e.g. `[email protected]`)
340353
anyhow::bail!("cannot specify a path (`{raw_path}`) with a version (`{v}`).");
@@ -349,10 +362,15 @@ fn resolve_dependency(
349362
}
350363
selected
351364
} else {
352-
let mut source = crate::sources::PathSource::new(&path, src.source_id()?, gctx);
365+
let mut source = crate::sources::PathSource::new(&src.path, src.source_id()?, gctx);
353366
let package = source.root_package()?;
354367
Dependency::from(package.summary())
355368
};
369+
if let Some(selected_source) = selected.source.as_mut() {
370+
if let Source::Path(selected_source) = selected_source {
371+
selected_source.base_name_and_value = base_name_and_value;
372+
}
373+
}
356374
selected
357375
} else if let Some(crate_spec) = &crate_spec {
358376
crate_spec.to_dependency()?

src/cargo/sources/path.rs

+35-5
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<(PathBuf, Option<String>)> {
882883
let mut nested_paths = Vec::new();
883884
let normalized = manifest.normalized_toml();
884885
let dependencies = normalized
@@ -910,7 +911,7 @@ 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((PathBuf::from(path.as_str()), dep.base.clone()));
914915
}
915916
}
916917
nested_paths
@@ -1000,8 +1001,36 @@ fn read_nested_packages(
10001001
//
10011002
// TODO: filesystem/symlink implications?
10021003
if !source_id.is_registry() {
1003-
for p in nested.iter() {
1004-
let path = paths::normalize_path(&path.join(p));
1004+
let mut manifest_gctx = None;
1005+
1006+
for (p, base) in nested.iter() {
1007+
let p = if let Some(base) = base {
1008+
// If the dependency has a path base, then load the global context for the current
1009+
// manifest and use it to resolve the path base.
1010+
let manifest_gctx = match manifest_gctx {
1011+
Some(ref gctx) => gctx,
1012+
None => {
1013+
let mut new_manifest_gctx = match GlobalContext::default() {
1014+
Ok(gctx) => gctx,
1015+
Err(err) => return Err(err),
1016+
};
1017+
if let Err(err) = new_manifest_gctx.reload_rooted_at(&manifest_path) {
1018+
return Err(err);
1019+
}
1020+
manifest_gctx.insert(new_manifest_gctx)
1021+
}
1022+
};
1023+
match lookup_path_base(base, manifest_gctx, manifest_path.parent().unwrap()) {
1024+
Ok(base) => Cow::Owned(base.join(p)),
1025+
Err(err) => {
1026+
errors.push(err);
1027+
continue;
1028+
}
1029+
}
1030+
} else {
1031+
Cow::Borrowed(p)
1032+
};
1033+
let path = paths::normalize_path(&path.join(p.as_path()));
10051034
let result =
10061035
read_nested_packages(&path, all_packages, source_id, gctx, visited, errors);
10071036
// Ignore broken manifests found on git repositories.
@@ -1019,6 +1048,7 @@ fn read_nested_packages(
10191048
);
10201049
errors.push(err);
10211050
} else {
1051+
trace!("Failed to manifest: {:?}", err);
10221052
return Err(err);
10231053
}
10241054
}

src/cargo/util/toml/mod.rs

+70-8
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,20 @@ 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, manifest_ctx.gctx, manifest_ctx.root)
2148+
.with_context(|| {
2149+
format!("resolving path base for dependency ({name_in_toml})")
2150+
})?
2151+
.join(path)
2152+
} else {
2153+
// This is a standard path with no prefix.
2154+
manifest_ctx.root.join(path)
2155+
};
21392156
let path = paths::normalize_path(&path);
21402157
SourceId::for_path(&path)
21412158
} else {
@@ -2151,6 +2168,50 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
21512168
}
21522169
}
21532170

2171+
pub(crate) fn lookup_path_base(
2172+
base: &str,
2173+
gctx: &GlobalContext,
2174+
manifest_root: &Path,
2175+
) -> CargoResult<PathBuf> {
2176+
// Validate the path base name.
2177+
if base.is_empty()
2178+
|| !base.chars().next().unwrap().is_alphabetic()
2179+
|| base
2180+
.chars()
2181+
.skip(1)
2182+
.any(|c| !c.is_alphanumeric() && c != '_' && c != '-')
2183+
{
2184+
bail!(
2185+
"invalid path base name `{base}`. \
2186+
Path base names must start with a letter and contain only letters, numbers, hyphens, and underscores."
2187+
);
2188+
}
2189+
2190+
// Look up the relevant base in the Config and use that as the root.
2191+
if let Some(path_bases) =
2192+
gctx.get::<Option<ConfigRelativePath>>(&format!("path-bases.{base}"))?
2193+
{
2194+
Ok(path_bases.resolve_path(gctx))
2195+
} else {
2196+
// Otherwise, check the built-in bases.
2197+
match base {
2198+
"workspace" => {
2199+
if let Some(workspace_root) = find_workspace_root(manifest_root, gctx)? {
2200+
Ok(workspace_root.parent().unwrap().to_path_buf())
2201+
} else {
2202+
bail!(
2203+
"the `workspace` built-in path base cannot be used outside of a workspace."
2204+
)
2205+
}
2206+
}
2207+
_ => bail!(
2208+
"path base `{base}` is undefined. \
2209+
You must add an entry for `{base}` in the Cargo configuration [path-bases] table."
2210+
),
2211+
}
2212+
}
2213+
}
2214+
21542215
pub trait ResolveToPath {
21552216
fn resolve(&self, gctx: &GlobalContext) -> PathBuf;
21562217
}
@@ -2865,6 +2926,7 @@ fn prepare_toml_for_publish(
28652926
let mut d = d.clone();
28662927
// Path dependencies become crates.io deps.
28672928
d.path.take();
2929+
d.base.take();
28682930
// Same with git dependencies.
28692931
d.git.take();
28702932
d.branch.take();

src/cargo/util/toml_mut/dependency.rs

+29-5
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,18 @@ impl Dependency {
412412
table.insert("version", src.version.as_str().into());
413413
}
414414
Some(Source::Path(src)) => {
415-
let relpath = path_field(crate_root, &src.path);
416415
if let Some(r) = src.version.as_deref() {
417416
table.insert("version", r.into());
418417
}
418+
let relative_to = if let Some((base_name, base_value)) =
419+
src.base_name_and_value.as_ref()
420+
{
421+
table.insert("base", base_name.into());
422+
base_value
423+
} else {
424+
crate_root
425+
};
426+
let relpath = path_field(relative_to, &src.path);
419427
table.insert("path", relpath.into());
420428
}
421429
Some(Source::Git(src)) => {
@@ -493,12 +501,20 @@ impl Dependency {
493501
Some(Source::Registry(src)) => {
494502
overwrite_value(table, "version", src.version.as_str());
495503

496-
for key in ["path", "git", "branch", "tag", "rev", "workspace"] {
504+
for key in ["path", "git", "branch", "tag", "rev", "workspace", "base"] {
497505
table.remove(key);
498506
}
499507
}
500508
Some(Source::Path(src)) => {
501-
let relpath = path_field(crate_root, &src.path);
509+
let relative_to =
510+
if let Some((base_name, base_value)) = src.base_name_and_value.as_ref() {
511+
overwrite_value(table, "base", base_name);
512+
base_value
513+
} else {
514+
table.remove("base");
515+
crate_root
516+
};
517+
let relpath = path_field(relative_to, &src.path);
502518
overwrite_value(table, "path", relpath);
503519
if let Some(r) = src.version.as_deref() {
504520
overwrite_value(table, "version", r);
@@ -533,7 +549,7 @@ impl Dependency {
533549
table.remove("version");
534550
}
535551

536-
for key in ["path", "workspace"] {
552+
for key in ["path", "workspace", "base"] {
537553
table.remove(key);
538554
}
539555
}
@@ -552,6 +568,7 @@ impl Dependency {
552568
"rev",
553569
"package",
554570
"default-features",
571+
"base",
555572
] {
556573
table.remove(key);
557574
}
@@ -812,6 +829,8 @@ impl std::fmt::Display for RegistrySource {
812829
pub struct PathSource {
813830
/// Local, absolute path.
814831
pub path: PathBuf,
832+
/// If using a named base, the base and relative path.
833+
pub base_name_and_value: Option<(String, PathBuf)>,
815834
/// Version requirement for when published.
816835
pub version: Option<String>,
817836
}
@@ -821,6 +840,7 @@ impl PathSource {
821840
pub fn new(path: impl Into<PathBuf>) -> Self {
822841
Self {
823842
path: path.into(),
843+
base_name_and_value: None,
824844
version: None,
825845
}
826846
}
@@ -843,7 +863,11 @@ impl PathSource {
843863

844864
impl std::fmt::Display for PathSource {
845865
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846-
self.path.display().fmt(f)
866+
if let Some((base_name, _)) = &self.base_name_and_value {
867+
write!(f, "{} (@{base_name})", self.path.display())
868+
} else {
869+
self.path.display().fmt(f)
870+
}
847871
}
848872
}
849873

0 commit comments

Comments
 (0)