Skip to content

Commit a591ec4

Browse files
committed
Implement base paths (RFC 3529) 1/n: path dep and patch support
1 parent 403bc5b commit a591ec4

File tree

8 files changed

+728
-20
lines changed

8 files changed

+728
-20
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
[a2b58c3d...HEAD](https://github.com/rust-lang/cargo/compare/a2b58c3d...HEAD)
55

66
### Added
7+
- Added the `path-bases` feature to support paths that resolve relatively to a
8+
base specified in the config.
9+
[docs](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#path-bases)
10+
[#14360](https://github.com/rust-lang/cargo/pull/14360)
711

812
### Changed
913

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

Lines changed: 12 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<PathBaseName>,
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(),
@@ -1413,6 +1415,16 @@ impl<T: AsRef<str>> FeatureName<T> {
14131415
}
14141416
}
14151417

1418+
str_newtype!(PathBaseName);
1419+
1420+
impl<T: AsRef<str>> PathBaseName<T> {
1421+
/// Validated path base name
1422+
pub fn new(name: T) -> Result<Self, NameValidationError> {
1423+
restricted_names::validate_path_base_name(name.as_ref())?;
1424+
Ok(Self(name))
1425+
}
1426+
}
1427+
14161428
/// Corresponds to a `target` entry, but `TomlTarget` is already used.
14171429
#[derive(Serialize, Deserialize, Debug, Clone)]
14181430
#[serde(rename_all = "kebab-case")]

crates/cargo-util-schemas/src/restricted_names.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,37 @@ pub(crate) fn validate_feature_name(name: &str) -> Result<()> {
238238
Ok(())
239239
}
240240

241+
pub(crate) fn validate_path_base_name(name: &str) -> Result<()> {
242+
let what = "path base name";
243+
let mut chars = name.chars();
244+
245+
let Some(first_char) = chars.next() else {
246+
return Err(ErrorKind::Empty(what).into());
247+
};
248+
249+
if !first_char.is_alphabetic() {
250+
return Err(ErrorKind::InvalidCharacter {
251+
ch: first_char,
252+
what,
253+
name: name.into(),
254+
reason: "first character must be a letter",
255+
}
256+
.into());
257+
}
258+
259+
if let Some(ch) = chars.find(|c| !c.is_alphanumeric() && *c != '_' && *c != '-') {
260+
return Err(ErrorKind::InvalidCharacter {
261+
ch,
262+
what,
263+
name: name.into(),
264+
reason: "allowed characters are letters, numbers, underscore, and hyphen",
265+
}
266+
.into());
267+
}
268+
269+
Ok(())
270+
}
271+
241272
#[cfg(test)]
242273
mod tests {
243274
use super::*;
@@ -264,4 +295,15 @@ mod tests {
264295
assert!(validate_feature_name("a¼").is_err());
265296
assert!(validate_feature_name("").is_err());
266297
}
298+
299+
#[test]
300+
fn validate_path_base_names() {
301+
assert!(validate_path_base_name("foo").is_ok());
302+
assert!(validate_path_base_name("foo12342_42-hello").is_ok());
303+
304+
assert!(validate_path_base_name("").is_err());
305+
assert!(validate_path_base_name("42foo").is_err());
306+
assert!(validate_path_base_name("_foo").is_err());
307+
assert!(validate_path_base_name("foo+bar").is_err());
308+
}
267309
}

src/cargo/core/features.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,9 @@ features! {
513513

514514
/// Allow multiple packages to participate in the same API namespace
515515
(unstable, open_namespaces, "", "reference/unstable.html#open-namespaces"),
516+
517+
/// Allow paths that resolve relatively to a base specified in the config.
518+
(unstable, path_bases, "", "reference/unstable.html#path-bases"),
516519
}
517520

518521
/// Status and metadata for a single unstable feature.

src/cargo/util/toml/mod.rs

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use crate::AlreadyPrintedError;
1010
use anyhow::{anyhow, bail, Context as _};
1111
use cargo_platform::Platform;
1212
use cargo_util::paths::{self, normalize_path};
13-
use cargo_util_schemas::manifest::{self, TomlManifest};
13+
use cargo_util_schemas::manifest::{
14+
self, PackageName, PathBaseName, TomlDependency, TomlDetailedDependency, TomlManifest,
15+
};
1416
use cargo_util_schemas::manifest::{RustVersion, StringOrBool};
1517
use itertools::Itertools;
1618
use lazycell::LazyCell;
@@ -296,7 +298,7 @@ fn normalize_toml(
296298
features: None,
297299
target: None,
298300
replace: original_toml.replace.clone(),
299-
patch: original_toml.patch.clone(),
301+
patch: None,
300302
workspace: original_toml.workspace.clone(),
301303
badges: None,
302304
lints: None,
@@ -305,11 +307,18 @@ fn normalize_toml(
305307

306308
let package_root = manifest_file.parent().unwrap();
307309

308-
let inherit_cell: LazyCell<InheritableFields> = LazyCell::new();
309-
let inherit = || {
310+
let inherit_cell: LazyCell<Option<InheritableFields>> = LazyCell::new();
311+
let load_inherit = || {
310312
inherit_cell
311313
.try_borrow_with(|| load_inheritable_fields(gctx, manifest_file, &workspace_config))
312314
};
315+
let inherit = || {
316+
load_inherit()?
317+
.as_ref()
318+
.ok_or_else(|| anyhow!("failed to find a workspace root"))
319+
};
320+
let workspace_root =
321+
|| load_inherit().map(|inherit| inherit.as_ref().map(|fields| fields.ws_root()));
313322

314323
if let Some(original_package) = original_toml.package() {
315324
let package_name = &original_package.name;
@@ -390,6 +399,7 @@ fn normalize_toml(
390399
&activated_opt_deps,
391400
None,
392401
&inherit,
402+
&workspace_root,
393403
package_root,
394404
warnings,
395405
)?;
@@ -410,6 +420,7 @@ fn normalize_toml(
410420
&activated_opt_deps,
411421
Some(DepKind::Development),
412422
&inherit,
423+
&workspace_root,
413424
package_root,
414425
warnings,
415426
)?;
@@ -430,6 +441,7 @@ fn normalize_toml(
430441
&activated_opt_deps,
431442
Some(DepKind::Build),
432443
&inherit,
444+
&workspace_root,
433445
package_root,
434446
warnings,
435447
)?;
@@ -443,6 +455,7 @@ fn normalize_toml(
443455
&activated_opt_deps,
444456
None,
445457
&inherit,
458+
&workspace_root,
446459
package_root,
447460
warnings,
448461
)?;
@@ -463,6 +476,7 @@ fn normalize_toml(
463476
&activated_opt_deps,
464477
Some(DepKind::Development),
465478
&inherit,
479+
&workspace_root,
466480
package_root,
467481
warnings,
468482
)?;
@@ -483,6 +497,7 @@ fn normalize_toml(
483497
&activated_opt_deps,
484498
Some(DepKind::Build),
485499
&inherit,
500+
&workspace_root,
486501
package_root,
487502
warnings,
488503
)?;
@@ -499,6 +514,13 @@ fn normalize_toml(
499514
}
500515
normalized_toml.target = (!normalized_target.is_empty()).then_some(normalized_target);
501516

517+
normalized_toml.patch = normalize_patch(
518+
gctx,
519+
original_toml.patch.as_ref(),
520+
&workspace_root,
521+
features,
522+
)?;
523+
502524
let normalized_lints = original_toml
503525
.lints
504526
.clone()
@@ -519,6 +541,37 @@ fn normalize_toml(
519541
Ok(normalized_toml)
520542
}
521543

544+
fn normalize_patch<'a>(
545+
gctx: &GlobalContext,
546+
original_patch: Option<&BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>,
547+
workspace_root: &dyn Fn() -> CargoResult<Option<&'a PathBuf>>,
548+
features: &Features,
549+
) -> CargoResult<Option<BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>> {
550+
if let Some(patch) = original_patch {
551+
let mut normalized_patch = BTreeMap::new();
552+
for (name, packages) in patch {
553+
let mut normalized_packages = BTreeMap::new();
554+
for (pkg, dep) in packages {
555+
let dep = if let TomlDependency::Detailed(dep) = dep {
556+
let mut dep = dep.clone();
557+
normalize_path_dependency(gctx, &mut dep, workspace_root, features)
558+
.with_context(|| {
559+
format!("resolving path for patch of ({pkg}) for source ({name})")
560+
})?;
561+
TomlDependency::Detailed(dep)
562+
} else {
563+
dep.clone()
564+
};
565+
normalized_packages.insert(pkg.clone(), dep);
566+
}
567+
normalized_patch.insert(name.clone(), normalized_packages);
568+
}
569+
Ok(Some(normalized_patch))
570+
} else {
571+
Ok(None)
572+
}
573+
}
574+
522575
#[tracing::instrument(skip_all)]
523576
fn normalize_package_toml<'a>(
524577
original_package: &manifest::TomlPackage,
@@ -710,6 +763,7 @@ fn normalize_dependencies<'a>(
710763
activated_opt_deps: &HashSet<&str>,
711764
kind: Option<DepKind>,
712765
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
766+
workspace_root: &dyn Fn() -> CargoResult<Option<&'a PathBuf>>,
713767
package_root: &Path,
714768
warnings: &mut Vec<String>,
715769
) -> CargoResult<Option<BTreeMap<manifest::PackageName, manifest::InheritableDependency>>> {
@@ -768,6 +822,8 @@ fn normalize_dependencies<'a>(
768822
}
769823
}
770824
}
825+
normalize_path_dependency(gctx, d, workspace_root, features)
826+
.with_context(|| format!("resolving path dependency ({name_in_toml})"))?;
771827
}
772828

773829
// if the dependency is not optional, it is always used
@@ -786,13 +842,30 @@ fn normalize_dependencies<'a>(
786842
Ok(Some(deps))
787843
}
788844

845+
fn normalize_path_dependency<'a>(
846+
gctx: &GlobalContext,
847+
detailed_dep: &mut TomlDetailedDependency,
848+
workspace_root: &dyn Fn() -> CargoResult<Option<&'a PathBuf>>,
849+
features: &Features,
850+
) -> CargoResult<()> {
851+
if let Some(base) = detailed_dep.base.take() {
852+
if let Some(path) = detailed_dep.path.as_mut() {
853+
let new_path = lookup_path_base(&base, gctx, workspace_root, features)?.join(&path);
854+
*path = new_path.to_str().unwrap().to_string();
855+
} else {
856+
bail!("`base` can only be used with path dependencies");
857+
}
858+
}
859+
Ok(())
860+
}
861+
789862
fn load_inheritable_fields(
790863
gctx: &GlobalContext,
791864
normalized_path: &Path,
792865
workspace_config: &WorkspaceConfig,
793-
) -> CargoResult<InheritableFields> {
866+
) -> CargoResult<Option<InheritableFields>> {
794867
match workspace_config {
795-
WorkspaceConfig::Root(root) => Ok(root.inheritable().clone()),
868+
WorkspaceConfig::Root(root) => Ok(Some(root.inheritable().clone())),
796869
WorkspaceConfig::Member {
797870
root: Some(ref path_to_root),
798871
} => {
@@ -802,14 +875,11 @@ fn load_inheritable_fields(
802875
.join(path_to_root)
803876
.join("Cargo.toml");
804877
let root_path = paths::normalize_path(&path);
805-
inheritable_from_path(gctx, root_path)
806-
}
807-
WorkspaceConfig::Member { root: None } => {
808-
match find_workspace_root(&normalized_path, gctx)? {
809-
Some(path_to_root) => inheritable_from_path(gctx, path_to_root),
810-
None => Err(anyhow!("failed to find a workspace root")),
811-
}
878+
inheritable_from_path(gctx, root_path).map(Some)
812879
}
880+
WorkspaceConfig::Member { root: None } => find_workspace_root(&normalized_path, gctx)?
881+
.map(|path_to_root| inheritable_from_path(gctx, path_to_root))
882+
.transpose(),
813883
}
814884
}
815885

@@ -901,13 +971,17 @@ impl InheritableFields {
901971
};
902972
let mut dep = dep.clone();
903973
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-
)?);
974+
if detailed.base.is_none() {
975+
// If this is a path dependency without a base, then update the path to be relative
976+
// to the workspace root instead.
977+
if let Some(rel_path) = &detailed.path {
978+
detailed.path = Some(resolve_relative_path(
979+
name,
980+
self.ws_root(),
981+
package_root,
982+
rel_path,
983+
)?);
984+
}
911985
}
912986
}
913987
Ok(dep)
@@ -2151,6 +2225,41 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
21512225
}
21522226
}
21532227

2228+
pub(crate) fn lookup_path_base<'a>(
2229+
base: &PathBaseName,
2230+
gctx: &GlobalContext,
2231+
workspace_root: &dyn Fn() -> CargoResult<Option<&'a PathBuf>>,
2232+
features: &Features,
2233+
) -> CargoResult<PathBuf> {
2234+
features.require(Feature::path_bases())?;
2235+
2236+
// Look up the relevant base in the Config and use that as the root.
2237+
// NOTE: The `base` string is user controlled, but building the path is safe from injection
2238+
// attacks since the `PathBaseName` type restricts the characters that can be used.
2239+
if let Some(path_bases) =
2240+
gctx.get::<Option<ConfigRelativePath>>(&format!("path-bases.{base}"))?
2241+
{
2242+
Ok(path_bases.resolve_path(gctx))
2243+
} else {
2244+
// Otherwise, check the built-in bases.
2245+
match base.as_str() {
2246+
"workspace" => {
2247+
if let Some(workspace_root) = workspace_root()? {
2248+
Ok(workspace_root.clone())
2249+
} else {
2250+
bail!(
2251+
"the `workspace` built-in path base cannot be used outside of a workspace."
2252+
)
2253+
}
2254+
}
2255+
_ => bail!(
2256+
"path base `{base}` is undefined. \
2257+
You must add an entry for `{base}` in the Cargo configuration [path-bases] table."
2258+
),
2259+
}
2260+
}
2261+
}
2262+
21542263
pub trait ResolveToPath {
21552264
fn resolve(&self, gctx: &GlobalContext) -> PathBuf;
21562265
}
@@ -2865,6 +2974,7 @@ fn prepare_toml_for_publish(
28652974
let mut d = d.clone();
28662975
// Path dependencies become crates.io deps.
28672976
d.path.take();
2977+
d.base.take();
28682978
// Same with git dependencies.
28692979
d.git.take();
28702980
d.branch.take();

0 commit comments

Comments
 (0)