Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d5556ae

Browse files
committedNov 4, 2020
Auto merge of #8818 - ehuss:weak-dep-features, r=alexcrichton
Implement weak dependency features. This adds the feature syntax `dep_name?/feat_name` with a `?` to only enable `feat_name` if the optional dependency `dep_name` is enabled through some other means. See `unstable.md` for documentation. This only works with the new feature resolver. I don't think I understand the dependency resolver well enough to implement it there. It would require teaching it to defer activating a feature, but due to the backtracking nature, I don't really know how to accomplish that. I don't think it matters, the main drawback is that the dependency resolver will be slightly more constrained, but in practice I doubt it will ever matter. Closes #3494 **Question** * An alternate syntax I considered was `dep_name?feat_name` (without the slash), what do people think? For some reason the `?/` seems kinda awkward to me.
2 parents 862df61 + 9ffcf69 commit d5556ae

File tree

12 files changed

+809
-59
lines changed

12 files changed

+809
-59
lines changed
 

‎src/bin/cargo/cli.rs

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Available unstable (nightly-only) flags:
4343
-Z doctest-xcompile -- Compile and run doctests for non-host target using runner config
4444
-Z terminal-width -- Provide a terminal width to rustc for error truncation
4545
-Z namespaced-features -- Allow features with `dep:` prefix
46+
-Z weak-dep-features -- Allow `dep_name?/feature` feature syntax
4647
4748
Run with 'cargo -Z [FLAG] [SUBCOMMAND]'"
4849
);

‎src/cargo/core/features.rs

+2
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ pub struct CliUnstable {
358358
pub rustdoc_map: bool,
359359
pub terminal_width: Option<Option<usize>>,
360360
pub namespaced_features: bool,
361+
pub weak_dep_features: bool,
361362
}
362363

363364
fn deserialize_build_std<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
@@ -464,6 +465,7 @@ impl CliUnstable {
464465
"rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
465466
"terminal-width" => self.terminal_width = Some(parse_usize_opt(v)?),
466467
"namespaced-features" => self.namespaced_features = parse_empty(k, v)?,
468+
"weak-dep-features" => self.weak_dep_features = parse_empty(k, v)?,
467469
_ => bail!("unknown `-Z` flag specified: {}", k),
468470
}
469471

‎src/cargo/core/resolver/dep_cache.rs

+4
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,10 @@ impl Requirements<'_> {
494494
dep_name,
495495
dep_feature,
496496
dep_prefix,
497+
// Weak features are always activated in the dependency
498+
// resolver. They will be narrowed inside the new feature
499+
// resolver.
500+
weak: _,
497501
} => self.require_dep_feature(*dep_name, *dep_feature, *dep_prefix)?,
498502
};
499503
Ok(())

‎src/cargo/core/resolver/features.rs

+145-48
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ impl FeatureOpts {
177177
if let ForceAllTargets::Yes = force_all_targets {
178178
opts.ignore_inactive_targets = false;
179179
}
180+
if unstable_flags.weak_dep_features {
181+
// Force this ON because it only works with the new resolver.
182+
opts.new_resolver = true;
183+
}
180184
Ok(opts)
181185
}
182186
}
@@ -311,6 +315,15 @@ pub struct FeatureResolver<'a, 'cfg> {
311315
/// This has to be separate from `FeatureOpts.decouple_host_deps` because
312316
/// `for_host` tracking is also needed for `itarget` to work properly.
313317
track_for_host: bool,
318+
/// `dep_name?/feat_name` features that will be activated if `dep_name` is
319+
/// ever activated.
320+
///
321+
/// The key is the `(package, for_host, dep_name)` of the package whose
322+
/// dependency will trigger the addition of new features. The value is the
323+
/// set of `(feature, dep_prefix)` features to activate (`dep_prefix` is a
324+
/// bool that indicates if `dep:` prefix was used).
325+
deferred_weak_dependencies:
326+
HashMap<(PackageId, bool, InternedString), HashSet<(InternedString, bool)>>,
314327
}
315328

316329
impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
@@ -353,6 +366,7 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
353366
activated_dependencies: HashMap::new(),
354367
processed_deps: HashSet::new(),
355368
track_for_host,
369+
deferred_weak_dependencies: HashMap::new(),
356370
};
357371
r.do_resolve(specs, requested_features)?;
358372
log::debug!("features={:#?}", r.activated_features);
@@ -378,7 +392,7 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
378392
for (member, requested_features) in &member_features {
379393
let fvs = self.fvs_from_requested(member.package_id(), requested_features);
380394
let for_host = self.track_for_host && self.is_proc_macro(member.package_id());
381-
self.activate_pkg(member.package_id(), &fvs, for_host)?;
395+
self.activate_pkg(member.package_id(), for_host, &fvs)?;
382396
if for_host {
383397
// Also activate without for_host. This is needed if the
384398
// proc-macro includes other targets (like binaries or tests),
@@ -387,7 +401,7 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
387401
// `--workspace`), this forces feature unification with normal
388402
// dependencies. This is part of the bigger problem where
389403
// features depend on which packages are built.
390-
self.activate_pkg(member.package_id(), &fvs, false)?;
404+
self.activate_pkg(member.package_id(), false, &fvs)?;
391405
}
392406
}
393407
Ok(())
@@ -396,17 +410,18 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
396410
fn activate_pkg(
397411
&mut self,
398412
pkg_id: PackageId,
399-
fvs: &[FeatureValue],
400413
for_host: bool,
414+
fvs: &[FeatureValue],
401415
) -> CargoResult<()> {
416+
log::trace!("activate_pkg {} {}", pkg_id.name(), for_host);
402417
// Add an empty entry to ensure everything is covered. This is intended for
403418
// finding bugs where the resolver missed something it should have visited.
404419
// Remove this in the future if `activated_features` uses an empty default.
405420
self.activated_features
406421
.entry((pkg_id, self.opts.decouple_host_deps && for_host))
407422
.or_insert_with(BTreeSet::new);
408423
for fv in fvs {
409-
self.activate_fv(pkg_id, fv, for_host)?;
424+
self.activate_fv(pkg_id, for_host, fv)?;
410425
}
411426
if !self.processed_deps.insert((pkg_id, for_host)) {
412427
// Already processed dependencies. There's no need to process them
@@ -433,7 +448,7 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
433448
}
434449
// Recurse into the dependency.
435450
let fvs = self.fvs_from_dependency(dep_pkg_id, dep);
436-
self.activate_pkg(dep_pkg_id, &fvs, dep_for_host)?;
451+
self.activate_pkg(dep_pkg_id, dep_for_host, &fvs)?;
437452
}
438453
}
439454
Ok(())
@@ -443,59 +458,31 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
443458
fn activate_fv(
444459
&mut self,
445460
pkg_id: PackageId,
446-
fv: &FeatureValue,
447461
for_host: bool,
462+
fv: &FeatureValue,
448463
) -> CargoResult<()> {
464+
log::trace!("activate_fv {} {} {}", pkg_id.name(), for_host, fv);
449465
match fv {
450466
FeatureValue::Feature(f) => {
451-
self.activate_rec(pkg_id, *f, for_host)?;
467+
self.activate_rec(pkg_id, for_host, *f)?;
452468
}
453469
FeatureValue::Dep { dep_name } => {
454-
// Mark this dependency as activated.
455-
self.activated_dependencies
456-
.entry((pkg_id, self.opts.decouple_host_deps && for_host))
457-
.or_default()
458-
.insert(*dep_name);
459-
// Activate the optional dep.
460-
for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
461-
for (dep, dep_for_host) in deps {
462-
if dep.name_in_toml() != *dep_name {
463-
continue;
464-
}
465-
let fvs = self.fvs_from_dependency(dep_pkg_id, dep);
466-
self.activate_pkg(dep_pkg_id, &fvs, dep_for_host)?;
467-
}
468-
}
470+
self.activate_dependency(pkg_id, for_host, *dep_name)?;
469471
}
470472
FeatureValue::DepFeature {
471473
dep_name,
472474
dep_feature,
473475
dep_prefix,
476+
weak,
474477
} => {
475-
// Activate a feature within a dependency.
476-
for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
477-
for (dep, dep_for_host) in deps {
478-
if dep.name_in_toml() != *dep_name {
479-
continue;
480-
}
481-
if dep.is_optional() {
482-
// Activate the dependency on self.
483-
let fv = FeatureValue::Dep {
484-
dep_name: *dep_name,
485-
};
486-
self.activate_fv(pkg_id, &fv, for_host)?;
487-
if !dep_prefix {
488-
// To retain compatibility with old behavior,
489-
// this also enables a feature of the same
490-
// name.
491-
self.activate_rec(pkg_id, *dep_name, for_host)?;
492-
}
493-
}
494-
// Activate the feature on the dependency.
495-
let fv = FeatureValue::new(*dep_feature);
496-
self.activate_fv(dep_pkg_id, &fv, dep_for_host)?;
497-
}
498-
}
478+
self.activate_dep_feature(
479+
pkg_id,
480+
for_host,
481+
*dep_name,
482+
*dep_feature,
483+
*dep_prefix,
484+
*weak,
485+
)?;
499486
}
500487
}
501488
Ok(())
@@ -506,9 +493,15 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
506493
fn activate_rec(
507494
&mut self,
508495
pkg_id: PackageId,
509-
feature_to_enable: InternedString,
510496
for_host: bool,
497+
feature_to_enable: InternedString,
511498
) -> CargoResult<()> {
499+
log::trace!(
500+
"activate_rec {} {} feat={}",
501+
pkg_id.name(),
502+
for_host,
503+
feature_to_enable
504+
);
512505
let enabled = self
513506
.activated_features
514507
.entry((pkg_id, self.opts.decouple_host_deps && for_host))
@@ -534,7 +527,111 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
534527
}
535528
};
536529
for fv in fvs {
537-
self.activate_fv(pkg_id, fv, for_host)?;
530+
self.activate_fv(pkg_id, for_host, fv)?;
531+
}
532+
Ok(())
533+
}
534+
535+
/// Activate a dependency (`dep:dep_name` syntax).
536+
fn activate_dependency(
537+
&mut self,
538+
pkg_id: PackageId,
539+
for_host: bool,
540+
dep_name: InternedString,
541+
) -> CargoResult<()> {
542+
// Mark this dependency as activated.
543+
let save_for_host = self.opts.decouple_host_deps && for_host;
544+
self.activated_dependencies
545+
.entry((pkg_id, save_for_host))
546+
.or_default()
547+
.insert(dep_name);
548+
// Check for any deferred features.
549+
let to_enable = self
550+
.deferred_weak_dependencies
551+
.remove(&(pkg_id, for_host, dep_name));
552+
// Activate the optional dep.
553+
for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
554+
for (dep, dep_for_host) in deps {
555+
if dep.name_in_toml() != dep_name {
556+
continue;
557+
}
558+
if let Some(to_enable) = &to_enable {
559+
for (dep_feature, dep_prefix) in to_enable {
560+
log::trace!(
561+
"activate deferred {} {} -> {}/{}",
562+
pkg_id.name(),
563+
for_host,
564+
dep_name,
565+
dep_feature
566+
);
567+
if !dep_prefix {
568+
self.activate_rec(pkg_id, for_host, dep_name)?;
569+
}
570+
let fv = FeatureValue::new(*dep_feature);
571+
self.activate_fv(dep_pkg_id, dep_for_host, &fv)?;
572+
}
573+
}
574+
let fvs = self.fvs_from_dependency(dep_pkg_id, dep);
575+
self.activate_pkg(dep_pkg_id, dep_for_host, &fvs)?;
576+
}
577+
}
578+
Ok(())
579+
}
580+
581+
/// Activate a feature within a dependency (`dep_name/feat_name` syntax).
582+
fn activate_dep_feature(
583+
&mut self,
584+
pkg_id: PackageId,
585+
for_host: bool,
586+
dep_name: InternedString,
587+
dep_feature: InternedString,
588+
dep_prefix: bool,
589+
weak: bool,
590+
) -> CargoResult<()> {
591+
for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
592+
for (dep, dep_for_host) in deps {
593+
if dep.name_in_toml() != dep_name {
594+
continue;
595+
}
596+
if dep.is_optional() {
597+
let save_for_host = self.opts.decouple_host_deps && for_host;
598+
if weak
599+
&& !self
600+
.activated_dependencies
601+
.get(&(pkg_id, save_for_host))
602+
.map(|deps| deps.contains(&dep_name))
603+
.unwrap_or(false)
604+
{
605+
// This is weak, but not yet activated. Defer in case
606+
// something comes along later and enables it.
607+
log::trace!(
608+
"deferring feature {} {} -> {}/{}",
609+
pkg_id.name(),
610+
for_host,
611+
dep_name,
612+
dep_feature
613+
);
614+
self.deferred_weak_dependencies
615+
.entry((pkg_id, for_host, dep_name))
616+
.or_default()
617+
.insert((dep_feature, dep_prefix));
618+
continue;
619+
}
620+
621+
// Activate the dependency on self.
622+
let fv = FeatureValue::Dep { dep_name };
623+
self.activate_fv(pkg_id, for_host, &fv)?;
624+
if !dep_prefix {
625+
// To retain compatibility with old behavior,
626+
// this also enables a feature of the same
627+
// name.
628+
self.activate_rec(pkg_id, for_host, dep_name)?;
629+
}
630+
}
631+
// Activate the feature on the dependency.
632+
let fv = FeatureValue::new(dep_feature);
633+
self.activate_fv(dep_pkg_id, dep_for_host, &fv)?;
634+
}
538635
}
539636
Ok(())
540637
}

‎src/cargo/core/summary.rs

+45-9
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ impl Summary {
8686

8787
/// Returns an error if this Summary is using an unstable feature that is
8888
/// not enabled.
89-
pub fn unstable_gate(&self, namespaced_features: bool) -> CargoResult<()> {
89+
pub fn unstable_gate(
90+
&self,
91+
namespaced_features: bool,
92+
weak_dep_features: bool,
93+
) -> CargoResult<()> {
9094
if !namespaced_features {
9195
if self.inner.has_namespaced_features {
9296
bail!(
@@ -101,6 +105,22 @@ impl Summary {
101105
)
102106
}
103107
}
108+
if !weak_dep_features {
109+
for (feat_name, features) in self.features() {
110+
for fv in features {
111+
if matches!(fv, FeatureValue::DepFeature{weak: true, ..}) {
112+
bail!(
113+
"optional dependency features with `?` syntax are only \
114+
allowed on the nightly channel and requires the \
115+
`-Z weak-dep-features` flag on the command line\n\
116+
Feature `{}` had feature value `{}`.",
117+
feat_name,
118+
fv
119+
);
120+
}
121+
}
122+
}
123+
}
104124
Ok(())
105125
}
106126

@@ -293,7 +313,7 @@ fn build_feature_map(
293313
);
294314
}
295315
}
296-
DepFeature { dep_name, .. } => {
316+
DepFeature { dep_name, weak, .. } => {
297317
// Validation of the feature name will be performed in the resolver.
298318
if !is_any_dep {
299319
bail!(
@@ -303,6 +323,12 @@ fn build_feature_map(
303323
dep_name
304324
);
305325
}
326+
if *weak && !is_optional_dep {
327+
bail!("feature `{}` includes `{}` with a `?`, but `{}` is not an optional dependency\n\
328+
A non-optional dependency of the same name is defined; \
329+
consider removing the `?` or changing the dependency to be optional",
330+
feature, fv, dep_name);
331+
}
306332
}
307333
}
308334
}
@@ -346,6 +372,10 @@ pub enum FeatureValue {
346372
/// If this is true, then the feature used the `dep:` prefix, which
347373
/// prevents enabling the feature named `dep_name`.
348374
dep_prefix: bool,
375+
/// If `true`, indicates the `?` syntax is used, which means this will
376+
/// not automatically enable the dependency unless the dependency is
377+
/// activated through some other means.
378+
weak: bool,
349379
},
350380
}
351381

@@ -360,10 +390,16 @@ impl FeatureValue {
360390
} else {
361391
(dep, false)
362392
};
393+
let (dep, weak) = if let Some(dep) = dep.strip_suffix('?') {
394+
(dep, true)
395+
} else {
396+
(dep, false)
397+
};
363398
FeatureValue::DepFeature {
364399
dep_name: InternedString::new(dep),
365400
dep_feature: InternedString::new(dep_feat),
366401
dep_prefix,
402+
weak,
367403
}
368404
}
369405
None => {
@@ -393,13 +429,13 @@ impl fmt::Display for FeatureValue {
393429
DepFeature {
394430
dep_name,
395431
dep_feature,
396-
dep_prefix: true,
397-
} => write!(f, "dep:{}/{}", dep_name, dep_feature),
398-
DepFeature {
399-
dep_name,
400-
dep_feature,
401-
dep_prefix: false,
402-
} => write!(f, "{}/{}", dep_name, dep_feature),
432+
dep_prefix,
433+
weak,
434+
} => {
435+
let dep_prefix = if *dep_prefix { "dep:" } else { "" };
436+
let weak = if *weak { "?" } else { "" };
437+
write!(f, "{}{}{}/{}", dep_prefix, dep_name, weak, dep_feature)
438+
}
403439
}
404440
}
405441
}

‎src/cargo/ops/cargo_compile.rs

+9
Original file line numberDiff line numberDiff line change
@@ -1082,11 +1082,20 @@ fn validate_required_features(
10821082
target_name
10831083
);
10841084
}
1085+
FeatureValue::DepFeature { weak: true, .. } => {
1086+
anyhow::bail!(
1087+
"invalid feature `{}` in required-features of target `{}`: \
1088+
optional dependency with `?` is not allowed in required-features",
1089+
fv,
1090+
target_name
1091+
);
1092+
}
10851093
// Handling of dependent_crate/dependent_crate_feature syntax
10861094
FeatureValue::DepFeature {
10871095
dep_name,
10881096
dep_feature,
10891097
dep_prefix: false,
1098+
weak: false,
10901099
} => {
10911100
match resolve
10921101
.deps(summary.package_id())

‎src/cargo/ops/tree/graph.rs

+5
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,11 @@ fn add_feature_rec(
568568
dep_name,
569569
dep_feature,
570570
dep_prefix,
571+
// `weak` is ignored, because it will be skipped if the
572+
// corresponding dependency is not found in the map, which
573+
// means it wasn't activated. Skipping is handled by
574+
// `is_dep_activated` when the graph was built.
575+
weak: _,
571576
} => {
572577
let dep_indexes = match graph.dep_name_map[&package_index].get(dep_name) {
573578
Some(indexes) => indexes.clone(),

‎src/cargo/sources/registry/index.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ impl<'cfg> RegistryIndex<'cfg> {
272272
let source_id = self.source_id;
273273
let config = self.config;
274274
let namespaced_features = self.config.cli_unstable().namespaced_features;
275+
let weak_dep_features = self.config.cli_unstable().weak_dep_features;
275276

276277
// First up actually parse what summaries we have available. If Cargo
277278
// has run previously this will parse a Cargo-specific cache file rather
@@ -299,7 +300,11 @@ impl<'cfg> RegistryIndex<'cfg> {
299300
}
300301
},
301302
)
302-
.filter(move |is| is.summary.unstable_gate(namespaced_features).is_ok()))
303+
.filter(move |is| {
304+
is.summary
305+
.unstable_gate(namespaced_features, weak_dep_features)
306+
.is_ok()
307+
}))
303308
}
304309

305310
fn load_summaries(

‎src/cargo/util/toml/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,8 @@ impl TomlManifest {
11971197
me.features.as_ref().unwrap_or(&empty_features),
11981198
project.links.as_deref(),
11991199
)?;
1200-
summary.unstable_gate(config.cli_unstable().namespaced_features)?;
1200+
let unstable = config.cli_unstable();
1201+
summary.unstable_gate(unstable.namespaced_features, unstable.weak_dep_features)?;
12011202

12021203
let metadata = ManifestMetadata {
12031204
description: project.description.clone(),

‎src/doc/src/reference/unstable.md

+23
Original file line numberDiff line numberDiff line change
@@ -906,3 +906,26 @@ error[E0308]: mismatched types
906906
907907
error: aborting due to previous error
908908
```
909+
910+
### Weak dependency features
911+
* Tracking Issue: [#8832](https://github.com/rust-lang/cargo/issues/8832)
912+
913+
The `-Z weak-dep-features` command-line options enables the ability to use
914+
`dep_name?/feat_name` syntax in the `[features]` table. The `?` indicates that
915+
the optional dependency `dep_name` will not be automatically enabled. The
916+
feature `feat_name` will only be added if something else enables the
917+
`dep_name` dependency.
918+
919+
Example:
920+
921+
```toml
922+
[dependencies]
923+
serde = { version = "1.0.117", optional = true, default-features = false }
924+
925+
[features]
926+
std = ["serde?/std"]
927+
```
928+
929+
In this example, the `std` feature enables the `std` feature on the `serde`
930+
dependency. However, unlike the normal `serde/std` syntax, it will not enable
931+
the optional dependency `serde` unless something else has included it.

‎tests/testsuite/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ mod vendor;
120120
mod verify_project;
121121
mod version;
122122
mod warn_on_failure;
123+
mod weak_dep_features;
123124
mod workspaces;
124125
mod yank;
125126

‎tests/testsuite/weak_dep_features.rs

+566
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.