Skip to content

Commit 3a976c1

Browse files
committed
Auto merge of #8074 - ehuss:package-features2, r=alexcrichton
Extend -Zpackage-features with more capabilities. This is a proposal to extend `-Zpackage-features` with new abilities to change how features are selected on the command-line. See `unstable.md` for documentation on what it does. I've contemplated a variety of ways we could transition this to stable. I tried a few experiments trying to make a "transition with warnings" mode, but I'm just too concerned about breaking backwards compatibility. The current way is just fundamentally different from the new way, and I think it would be a bumpy ride to try to push it. The stabilization story is that the parts of this that add new functionality (feature flags in virtual worskpaces, and `member/feat` syntax) can be stabilized at any time. The change for `cargo build -p member --features feat` in a different member's directory can maybe be part of `-Zfeatures` stabilization, which will need to be opt-in. I've been trying to come up with some transition plan, and I can't think of a way without making it opt-in, and making it part of `-Zfeatures` is an opportunity to simplify things. One concern is that this might be confusing (`--features` flag could behave differently in different workspaces, and documenting the differences), but that seems hard to avoid. Closes #6195 Closes #4753 Closes #5015 Closes #4106 Closes #5362
2 parents 1e6ed94 + 54ace8a commit 3a976c1

File tree

6 files changed

+641
-125
lines changed

6 files changed

+641
-125
lines changed

src/cargo/core/workspace.rs

+134-49
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::cell::RefCell;
22
use std::collections::hash_map::{Entry, HashMap};
3-
use std::collections::{BTreeMap, HashSet};
3+
use std::collections::{BTreeMap, BTreeSet, HashSet};
44
use std::path::{Path, PathBuf};
5+
use std::rc::Rc;
56
use std::slice;
67

78
use glob::glob;
@@ -11,7 +12,7 @@ use url::Url;
1112
use crate::core::features::Features;
1213
use crate::core::registry::PackageRegistry;
1314
use crate::core::resolver::features::RequestedFeatures;
14-
use crate::core::{Dependency, PackageId, PackageIdSpec};
15+
use crate::core::{Dependency, InternedString, PackageId, PackageIdSpec};
1516
use crate::core::{EitherManifest, Package, SourceId, VirtualManifest};
1617
use crate::ops;
1718
use crate::sources::PathSource;
@@ -878,59 +879,143 @@ impl<'cfg> Workspace<'cfg> {
878879
.collect());
879880
}
880881
if self.config().cli_unstable().package_features {
881-
if specs.len() > 1 && !requested_features.features.is_empty() {
882-
anyhow::bail!("cannot specify features for more than one package");
882+
self.members_with_features_pf(specs, requested_features)
883+
} else {
884+
self.members_with_features_stable(specs, requested_features)
885+
}
886+
}
887+
888+
/// New command-line feature selection with -Zpackage-features.
889+
fn members_with_features_pf(
890+
&self,
891+
specs: &[PackageIdSpec],
892+
requested_features: &RequestedFeatures,
893+
) -> CargoResult<Vec<(&Package, RequestedFeatures)>> {
894+
// Keep track of which features matched *any* member, to produce an error
895+
// if any of them did not match anywhere.
896+
let mut found: BTreeSet<InternedString> = BTreeSet::new();
897+
898+
// Returns the requested features for the given member.
899+
// This filters out any named features that the member does not have.
900+
let mut matching_features = |member: &Package| -> RequestedFeatures {
901+
if requested_features.features.is_empty() || requested_features.all_features {
902+
return requested_features.clone();
903+
}
904+
// Only include features this member defines.
905+
let summary = member.summary();
906+
let member_features = summary.features();
907+
let mut features = BTreeSet::new();
908+
909+
// Checks if a member contains the given feature.
910+
let contains = |feature: InternedString| -> bool {
911+
member_features.contains_key(&feature)
912+
|| summary
913+
.dependencies()
914+
.iter()
915+
.any(|dep| dep.is_optional() && dep.name_in_toml() == feature)
916+
};
917+
918+
for feature in requested_features.features.iter() {
919+
let mut split = feature.splitn(2, '/');
920+
let split = (split.next().unwrap(), split.next());
921+
if let (pkg, Some(pkg_feature)) = split {
922+
let pkg = InternedString::new(pkg);
923+
let pkg_feature = InternedString::new(pkg_feature);
924+
if summary
925+
.dependencies()
926+
.iter()
927+
.any(|dep| dep.name_in_toml() == pkg)
928+
{
929+
// pkg/feat for a dependency.
930+
// Will rely on the dependency resolver to validate `feat`.
931+
features.insert(*feature);
932+
found.insert(*feature);
933+
} else if pkg == member.name() && contains(pkg_feature) {
934+
// member/feat where "feat" is a feature in member.
935+
features.insert(pkg_feature);
936+
found.insert(*feature);
937+
}
938+
} else if contains(*feature) {
939+
// feature exists in this member.
940+
features.insert(*feature);
941+
found.insert(*feature);
942+
}
943+
}
944+
RequestedFeatures {
945+
features: Rc::new(features),
946+
all_features: false,
947+
uses_default_features: requested_features.uses_default_features,
948+
}
949+
};
950+
951+
let members: Vec<(&Package, RequestedFeatures)> = self
952+
.members()
953+
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
954+
.map(|m| (m, matching_features(m)))
955+
.collect();
956+
if members.is_empty() {
957+
// `cargo build -p foo`, where `foo` is not a member.
958+
// Do not allow any command-line flags (defaults only).
959+
if !(requested_features.features.is_empty()
960+
&& !requested_features.all_features
961+
&& requested_features.uses_default_features)
962+
{
963+
anyhow::bail!("cannot specify features for packages outside of workspace");
883964
}
884-
let members: Vec<(&Package, RequestedFeatures)> = self
965+
// Add all members from the workspace so we can ensure `-p nonmember`
966+
// is in the resolve graph.
967+
return Ok(self
885968
.members()
886-
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
887-
.map(|m| (m, requested_features.clone()))
969+
.map(|m| (m, RequestedFeatures::new_all(false)))
970+
.collect());
971+
}
972+
if *requested_features.features != found {
973+
let missing: Vec<_> = requested_features
974+
.features
975+
.difference(&found)
976+
.copied()
888977
.collect();
889-
if members.is_empty() {
890-
// `cargo build -p foo`, where `foo` is not a member.
891-
// Do not allow any command-line flags (defaults only).
892-
if !(requested_features.features.is_empty()
893-
&& !requested_features.all_features
894-
&& requested_features.uses_default_features)
895-
{
896-
anyhow::bail!("cannot specify features for packages outside of workspace");
978+
// TODO: typo suggestions would be good here.
979+
anyhow::bail!(
980+
"none of the selected packages contains these features: {}",
981+
missing.join(", ")
982+
);
983+
}
984+
Ok(members)
985+
}
986+
987+
/// This is the current "stable" behavior for command-line feature selection.
988+
fn members_with_features_stable(
989+
&self,
990+
specs: &[PackageIdSpec],
991+
requested_features: &RequestedFeatures,
992+
) -> CargoResult<Vec<(&Package, RequestedFeatures)>> {
993+
let ms = self.members().filter_map(|member| {
994+
let member_id = member.package_id();
995+
match self.current_opt() {
996+
// The features passed on the command-line only apply to
997+
// the "current" package (determined by the cwd).
998+
Some(current) if member_id == current.package_id() => {
999+
Some((member, requested_features.clone()))
8971000
}
898-
// Add all members from the workspace so we can ensure `-p nonmember`
899-
// is in the resolve graph.
900-
return Ok(self
901-
.members()
902-
.map(|m| (m, RequestedFeatures::new_all(false)))
903-
.collect());
904-
}
905-
Ok(members)
906-
} else {
907-
let ms = self.members().filter_map(|member| {
908-
let member_id = member.package_id();
909-
match self.current_opt() {
910-
// The features passed on the command-line only apply to
911-
// the "current" package (determined by the cwd).
912-
Some(current) if member_id == current.package_id() => {
913-
Some((member, requested_features.clone()))
914-
}
915-
_ => {
916-
// Ignore members that are not enabled on the command-line.
917-
if specs.iter().any(|spec| spec.matches(member_id)) {
918-
// -p for a workspace member that is not the
919-
// "current" one, don't use the local
920-
// `--features`, only allow `--all-features`.
921-
Some((
922-
member,
923-
RequestedFeatures::new_all(requested_features.all_features),
924-
))
925-
} else {
926-
// This member was not requested on the command-line, skip.
927-
None
928-
}
1001+
_ => {
1002+
// Ignore members that are not enabled on the command-line.
1003+
if specs.iter().any(|spec| spec.matches(member_id)) {
1004+
// -p for a workspace member that is not the
1005+
// "current" one, don't use the local
1006+
// `--features`, only allow `--all-features`.
1007+
Some((
1008+
member,
1009+
RequestedFeatures::new_all(requested_features.all_features),
1010+
))
1011+
} else {
1012+
// This member was not requested on the command-line, skip.
1013+
None
9291014
}
9301015
}
931-
});
932-
Ok(ms.collect())
933-
}
1016+
}
1017+
});
1018+
Ok(ms.collect())
9341019
}
9351020
}
9361021

src/cargo/ops/cargo_clean.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,14 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
6262
let mut build_config = BuildConfig::new(config, Some(1), &opts.target, CompileMode::Build)?;
6363
build_config.requested_profile = opts.requested_profile;
6464
let target_data = RustcTargetData::new(ws, build_config.requested_kind)?;
65-
let resolve_opts = ResolveOpts::everything();
65+
// Resolve for default features. In the future, `cargo clean` should be rewritten
66+
// so that it doesn't need to guess filename hashes.
67+
let resolve_opts = ResolveOpts::new(
68+
/*dev_deps*/ true,
69+
&[],
70+
/*all features*/ false,
71+
/*default*/ true,
72+
);
6673
let specs = opts
6774
.spec
6875
.iter()

src/doc/src/reference/unstable.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ cargo +nightly -Zunstable-options -Zconfig-include --config somefile.toml build
474474

475475
CLI paths are relative to the current working directory.
476476

477-
## Features
477+
### Features
478478
* Tracking Issues:
479479
* [itarget #7914](https://github.com/rust-lang/cargo/issues/7914)
480480
* [build_dep #7915](https://github.com/rust-lang/cargo/issues/7915)
@@ -549,6 +549,31 @@ The available options are:
549549
* `compare` — This option compares the resolved features to the old resolver,
550550
and will print any differences.
551551

552+
### package-features
553+
* Tracking Issue: [#5364](https://github.com/rust-lang/cargo/issues/5364)
554+
555+
The `-Zpackage-features` flag changes the way features can be passed on the
556+
command-line for a workspace. The normal behavior can be confusing, as the
557+
features passed are always enabled on the package in the current directory,
558+
even if that package is not selected with a `-p` flag. Feature flags also do
559+
not work in the root of a virtual workspace. `-Zpackage-features` tries to
560+
make feature flags behave in a more intuitive manner.
561+
562+
* `cargo build -p other_member --features …` — This now only enables the given
563+
features as defined in `other_member` (ignores whatever is in the current
564+
directory).
565+
* `cargo build -p a -p b --features …` — This now enables the given features
566+
on both `a` and `b`. Not all packages need to define every feature, it only
567+
enables matching features. It is still an error if none of the packages
568+
define a given feature.
569+
* `--features` and `--no-default-features` are now allowed in the root of a
570+
virtual workspace.
571+
* `member_name/feature_name` syntax may now be used on the command-line to
572+
enable features for a specific member.
573+
574+
The ability to set features for non-workspace members is no longer allowed, as
575+
the resolver fundamentally does not support that ability.
576+
552577
### crate-versions
553578
* Tracking Issue: [#7907](https://github.com/rust-lang/cargo/issues/7907)
554579

tests/testsuite/features.rs

-74
Original file line numberDiff line numberDiff line change
@@ -1358,80 +1358,6 @@ fn many_cli_features_comma_and_space_delimited() {
13581358
.run();
13591359
}
13601360

1361-
#[cargo_test]
1362-
fn combining_features_and_package() {
1363-
Package::new("dep", "1.0.0").publish();
1364-
1365-
let p = project()
1366-
.file(
1367-
"Cargo.toml",
1368-
r#"
1369-
[project]
1370-
name = "foo"
1371-
version = "0.0.1"
1372-
authors = []
1373-
1374-
[workspace]
1375-
members = ["bar"]
1376-
1377-
[dependencies]
1378-
dep = "1"
1379-
"#,
1380-
)
1381-
.file("src/lib.rs", "")
1382-
.file(
1383-
"bar/Cargo.toml",
1384-
r#"
1385-
[package]
1386-
name = "bar"
1387-
version = "0.0.1"
1388-
authors = []
1389-
[features]
1390-
main = []
1391-
"#,
1392-
)
1393-
.file(
1394-
"bar/src/main.rs",
1395-
r#"
1396-
#[cfg(feature = "main")]
1397-
fn main() {}
1398-
"#,
1399-
)
1400-
.build();
1401-
1402-
p.cargo("build -Z package-features --workspace --features main")
1403-
.masquerade_as_nightly_cargo()
1404-
.with_status(101)
1405-
.with_stderr_contains("[ERROR] cannot specify features for more than one package")
1406-
.run();
1407-
1408-
p.cargo("build -Z package-features --package dep --features main")
1409-
.masquerade_as_nightly_cargo()
1410-
.with_status(101)
1411-
.with_stderr_contains("[ERROR] cannot specify features for packages outside of workspace")
1412-
.run();
1413-
p.cargo("build -Z package-features --package dep --all-features")
1414-
.masquerade_as_nightly_cargo()
1415-
.with_status(101)
1416-
.with_stderr_contains("[ERROR] cannot specify features for packages outside of workspace")
1417-
.run();
1418-
p.cargo("build -Z package-features --package dep --no-default-features")
1419-
.masquerade_as_nightly_cargo()
1420-
.with_status(101)
1421-
.with_stderr_contains("[ERROR] cannot specify features for packages outside of workspace")
1422-
.run();
1423-
1424-
p.cargo("build -Z package-features --workspace --all-features")
1425-
.masquerade_as_nightly_cargo()
1426-
.run();
1427-
p.cargo("run -Z package-features --package bar --features main")
1428-
.masquerade_as_nightly_cargo()
1429-
.run();
1430-
p.cargo("build -Z package-features --package dep")
1431-
.masquerade_as_nightly_cargo()
1432-
.run();
1433-
}
1434-
14351361
#[cargo_test]
14361362
fn namespaced_invalid_feature() {
14371363
let p = project()

tests/testsuite/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ mod offline;
7373
mod out_dir;
7474
mod owner;
7575
mod package;
76+
mod package_features;
7677
mod patch;
7778
mod path;
7879
mod paths;

0 commit comments

Comments
 (0)