Skip to content

Commit ff6df29

Browse files
committed
Auto merge of #14360 - dpaoliello:basepath1, r=epage
Implement base paths (RFC 3529) 1/n: path dep and patch support RFC: rust-lang/rfcs#3529 Tracking Issue: #14355 This PR add support in path dependencies and `patch` table and the `workspace` built-in path base.
2 parents 3a20ea7 + 502c74e commit ff6df29

File tree

7 files changed

+840
-9
lines changed

7 files changed

+840
-9
lines changed

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

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

+4
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ 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+
validate_name(name, "path base name")
243+
}
244+
241245
#[cfg(test)]
242246
mod tests {
243247
use super::*;

src/cargo/core/features.rs

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

+108-9
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,
@@ -310,6 +312,7 @@ fn normalize_toml(
310312
inherit_cell
311313
.try_borrow_with(|| load_inheritable_fields(gctx, manifest_file, &workspace_config))
312314
};
315+
let workspace_root = || inherit().map(|fields| fields.ws_root());
313316

314317
if let Some(original_package) = original_toml.package() {
315318
let package_name = &original_package.name;
@@ -390,6 +393,7 @@ fn normalize_toml(
390393
&activated_opt_deps,
391394
None,
392395
&inherit,
396+
&workspace_root,
393397
package_root,
394398
warnings,
395399
)?;
@@ -410,6 +414,7 @@ fn normalize_toml(
410414
&activated_opt_deps,
411415
Some(DepKind::Development),
412416
&inherit,
417+
&workspace_root,
413418
package_root,
414419
warnings,
415420
)?;
@@ -430,6 +435,7 @@ fn normalize_toml(
430435
&activated_opt_deps,
431436
Some(DepKind::Build),
432437
&inherit,
438+
&workspace_root,
433439
package_root,
434440
warnings,
435441
)?;
@@ -443,6 +449,7 @@ fn normalize_toml(
443449
&activated_opt_deps,
444450
None,
445451
&inherit,
452+
&workspace_root,
446453
package_root,
447454
warnings,
448455
)?;
@@ -463,6 +470,7 @@ fn normalize_toml(
463470
&activated_opt_deps,
464471
Some(DepKind::Development),
465472
&inherit,
473+
&workspace_root,
466474
package_root,
467475
warnings,
468476
)?;
@@ -483,6 +491,7 @@ fn normalize_toml(
483491
&activated_opt_deps,
484492
Some(DepKind::Build),
485493
&inherit,
494+
&workspace_root,
486495
package_root,
487496
warnings,
488497
)?;
@@ -499,6 +508,13 @@ fn normalize_toml(
499508
}
500509
normalized_toml.target = (!normalized_target.is_empty()).then_some(normalized_target);
501510

511+
normalized_toml.patch = normalize_patch(
512+
gctx,
513+
original_toml.patch.as_ref(),
514+
&workspace_root,
515+
features,
516+
)?;
517+
502518
let normalized_lints = original_toml
503519
.lints
504520
.clone()
@@ -519,6 +535,37 @@ fn normalize_toml(
519535
Ok(normalized_toml)
520536
}
521537

538+
fn normalize_patch<'a>(
539+
gctx: &GlobalContext,
540+
original_patch: Option<&BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>,
541+
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
542+
features: &Features,
543+
) -> CargoResult<Option<BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>> {
544+
if let Some(patch) = original_patch {
545+
let mut normalized_patch = BTreeMap::new();
546+
for (name, packages) in patch {
547+
let mut normalized_packages = BTreeMap::new();
548+
for (pkg, dep) in packages {
549+
let dep = if let TomlDependency::Detailed(dep) = dep {
550+
let mut dep = dep.clone();
551+
normalize_path_dependency(gctx, &mut dep, workspace_root, features)
552+
.with_context(|| {
553+
format!("resolving path for patch of ({pkg}) for source ({name})")
554+
})?;
555+
TomlDependency::Detailed(dep)
556+
} else {
557+
dep.clone()
558+
};
559+
normalized_packages.insert(pkg.clone(), dep);
560+
}
561+
normalized_patch.insert(name.clone(), normalized_packages);
562+
}
563+
Ok(Some(normalized_patch))
564+
} else {
565+
Ok(None)
566+
}
567+
}
568+
522569
#[tracing::instrument(skip_all)]
523570
fn normalize_package_toml<'a>(
524571
original_package: &manifest::TomlPackage,
@@ -710,6 +757,7 @@ fn normalize_dependencies<'a>(
710757
activated_opt_deps: &HashSet<&str>,
711758
kind: Option<DepKind>,
712759
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
760+
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
713761
package_root: &Path,
714762
warnings: &mut Vec<String>,
715763
) -> CargoResult<Option<BTreeMap<manifest::PackageName, manifest::InheritableDependency>>> {
@@ -768,6 +816,8 @@ fn normalize_dependencies<'a>(
768816
}
769817
}
770818
}
819+
normalize_path_dependency(gctx, d, workspace_root, features)
820+
.with_context(|| format!("resolving path dependency {name_in_toml}"))?;
771821
}
772822

773823
// if the dependency is not optional, it is always used
@@ -786,6 +836,23 @@ fn normalize_dependencies<'a>(
786836
Ok(Some(deps))
787837
}
788838

839+
fn normalize_path_dependency<'a>(
840+
gctx: &GlobalContext,
841+
detailed_dep: &mut TomlDetailedDependency,
842+
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
843+
features: &Features,
844+
) -> CargoResult<()> {
845+
if let Some(base) = detailed_dep.base.take() {
846+
if let Some(path) = detailed_dep.path.as_mut() {
847+
let new_path = lookup_path_base(&base, gctx, workspace_root, features)?.join(&path);
848+
*path = new_path.to_str().unwrap().to_string();
849+
} else {
850+
bail!("`base` can only be used with path dependencies");
851+
}
852+
}
853+
Ok(())
854+
}
855+
789856
fn load_inheritable_fields(
790857
gctx: &GlobalContext,
791858
normalized_path: &Path,
@@ -901,13 +968,17 @@ impl InheritableFields {
901968
};
902969
let mut dep = dep.clone();
903970
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-
)?);
971+
if detailed.base.is_none() {
972+
// If this is a path dependency without a base, then update the path to be relative
973+
// to the workspace root instead.
974+
if let Some(rel_path) = &detailed.path {
975+
detailed.path = Some(resolve_relative_path(
976+
name,
977+
self.ws_root(),
978+
package_root,
979+
rel_path,
980+
)?);
981+
}
911982
}
912983
}
913984
Ok(dep)
@@ -2151,6 +2222,33 @@ fn to_dependency_source_id<P: ResolveToPath + Clone>(
21512222
}
21522223
}
21532224

2225+
pub(crate) fn lookup_path_base<'a>(
2226+
base: &PathBaseName,
2227+
gctx: &GlobalContext,
2228+
workspace_root: &dyn Fn() -> CargoResult<&'a PathBuf>,
2229+
features: &Features,
2230+
) -> CargoResult<PathBuf> {
2231+
features.require(Feature::path_bases())?;
2232+
2233+
// HACK: The `base` string is user controlled, but building the path is safe from injection
2234+
// attacks since the `PathBaseName` type restricts the characters that can be used to exclude `.`
2235+
let base_key = format!("path-bases.{base}");
2236+
2237+
// Look up the relevant base in the Config and use that as the root.
2238+
if let Some(path_bases) = gctx.get::<Option<ConfigRelativePath>>(&base_key)? {
2239+
Ok(path_bases.resolve_path(gctx))
2240+
} else {
2241+
// Otherwise, check the built-in bases.
2242+
match base.as_str() {
2243+
"workspace" => Ok(workspace_root()?.clone()),
2244+
_ => bail!(
2245+
"path base `{base}` is undefined. \
2246+
You must add an entry for `{base}` in the Cargo configuration [path-bases] table."
2247+
),
2248+
}
2249+
}
2250+
}
2251+
21542252
pub trait ResolveToPath {
21552253
fn resolve(&self, gctx: &GlobalContext) -> PathBuf;
21562254
}
@@ -2865,6 +2963,7 @@ fn prepare_toml_for_publish(
28652963
let mut d = d.clone();
28662964
// Path dependencies become crates.io deps.
28672965
d.path.take();
2966+
d.base.take();
28682967
// Same with git dependencies.
28692968
d.git.take();
28702969
d.branch.take();

src/doc/src/reference/unstable.md

+55
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Each new feature described below should explain how to use it.
101101
* [Edition 2024](#edition-2024) — Adds support for the 2024 Edition.
102102
* [Profile `trim-paths` option](#profile-trim-paths-option) --- Control the sanitization of file paths in build outputs.
103103
* [`[lints.cargo]`](#lintscargo) --- Allows configuring lints for Cargo.
104+
* [path bases](#path-bases) --- Named base directories for path dependencies.
104105
* Information and metadata
105106
* [Build-plan](#build-plan) --- Emits JSON information on which commands will be run.
106107
* [unit-graph](#unit-graph) --- Emits JSON for Cargo's internal graph structure.
@@ -1570,6 +1571,60 @@ implicit-features = "warn"
15701571
workspace = true
15711572
```
15721573

1574+
## Path Bases
1575+
1576+
* Tracking Issue: [#14355](https://github.com/rust-lang/cargo/issues/14355)
1577+
1578+
A `path` dependency may optionally specify a base by setting the `base` key to
1579+
the name of a path base from the `[path-bases]` table in either the
1580+
[configuration](config.md) or one of the [built-in path bases](#built-in-path-bases).
1581+
The value of that path base is prepended to the `path` value (along with a path
1582+
separator if necessary) to produce the actual location where Cargo will look for
1583+
the dependency.
1584+
1585+
For example, if the `Cargo.toml` contains:
1586+
1587+
```toml
1588+
cargo-features = ["path-bases"]
1589+
1590+
[dependencies]
1591+
foo = { base = "dev", path = "foo" }
1592+
```
1593+
1594+
Given a `[path-bases]` table in the configuration that contains:
1595+
1596+
```toml
1597+
[path-bases]
1598+
dev = "/home/user/dev/rust/libraries/"
1599+
```
1600+
1601+
This will produce a `path` dependency `foo` located at
1602+
`/home/user/dev/rust/libraries/foo`.
1603+
1604+
Path bases can be either absolute or relative. Relative path bases are relative
1605+
to the parent directory of the configuration file that declared that path base.
1606+
1607+
The name of a path base must use only [alphanumeric](https://doc.rust-lang.org/std/primitive.char.html#method.is_alphanumeric)
1608+
characters or `-` or `_`, must start with an [alphabetic](https://doc.rust-lang.org/std/primitive.char.html#method.is_alphabetic)
1609+
character, and must not be empty.
1610+
1611+
If the name of path base used in a dependency is neither in the configuration
1612+
nor one of the built-in path base, then Cargo will raise an error.
1613+
1614+
#### Built-in path bases
1615+
1616+
Cargo provides implicit path bases that can be used without the need to specify
1617+
them in a `[path-bases]` table.
1618+
1619+
* `workspace` - If a project is [a workspace or workspace member](workspaces.md)
1620+
then this path base is defined as the parent directory of the root `Cargo.toml`
1621+
of the workspace.
1622+
1623+
If a built-in path base name is also declared in the configuration, then Cargo
1624+
will prefer the value in the configuration. The allows Cargo to add new built-in
1625+
path bases without compatibility issues (as existing uses will shadow the
1626+
built-in name).
1627+
15731628
# Stabilized and removed features
15741629

15751630
## Compile progress

0 commit comments

Comments
 (0)