Skip to content

Commit c17bfcb

Browse files
committed
Implement -Zpackage-features2
1 parent 805462e commit c17bfcb

File tree

7 files changed

+602
-53
lines changed

7 files changed

+602
-53
lines changed

src/cargo/core/features.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ pub struct CliUnstable {
330330
pub avoid_dev_deps: bool,
331331
pub minimal_versions: bool,
332332
pub package_features: bool,
333+
pub package_features2: bool,
333334
pub advanced_env: bool,
334335
pub config_include: bool,
335336
pub dual_proc_macros: bool,
@@ -404,6 +405,7 @@ impl CliUnstable {
404405
"avoid-dev-deps" => self.avoid_dev_deps = parse_empty(k, v)?,
405406
"minimal-versions" => self.minimal_versions = parse_empty(k, v)?,
406407
"package-features" => self.package_features = parse_empty(k, v)?,
408+
"package-features2" => self.package_features2 = parse_empty(k, v)?,
407409
"advanced-env" => self.advanced_env = parse_empty(k, v)?,
408410
"config-include" => self.config_include = parse_empty(k, v)?,
409411
"dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,

src/cargo/core/workspace.rs

Lines changed: 145 additions & 50 deletions
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;
@@ -877,60 +878,154 @@ impl<'cfg> Workspace<'cfg> {
877878
.map(|m| (m, RequestedFeatures::new_all(true)))
878879
.collect());
879880
}
880-
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");
881+
if self.config().cli_unstable().package_features
882+
|| self.config().cli_unstable().package_features2
883+
{
884+
self.members_with_features_pf(specs, requested_features)
885+
} else {
886+
self.members_with_features_stable(specs, requested_features)
887+
}
888+
}
889+
890+
/// New command-line feature selection with -Zpackage-features or -Zpackage-features2.
891+
fn members_with_features_pf(
892+
&self,
893+
specs: &[PackageIdSpec],
894+
requested_features: &RequestedFeatures,
895+
) -> CargoResult<Vec<(&Package, RequestedFeatures)>> {
896+
let pf2 = self.config().cli_unstable().package_features2;
897+
if specs.len() > 1 && !requested_features.features.is_empty() && !pf2 {
898+
anyhow::bail!("cannot specify features for more than one package");
899+
}
900+
// Keep track of which features matched *any* member, to produce an error
901+
// if any of them did not match anywhere.
902+
let mut found: BTreeSet<InternedString> = BTreeSet::new();
903+
904+
// Returns the requested features for the given member.
905+
// This filters out any named features that the member does not have.
906+
let mut matching_features = |member: &Package| -> RequestedFeatures {
907+
// This new behavior is only enabled for -Zpackage-features2
908+
if !pf2 {
909+
return requested_features.clone();
910+
}
911+
if requested_features.features.is_empty() || requested_features.all_features {
912+
return requested_features.clone();
883913
}
884-
let members: Vec<(&Package, RequestedFeatures)> = self
914+
// Only include features this member defines.
915+
let summary = member.summary();
916+
let member_features = summary.features();
917+
let mut features = BTreeSet::new();
918+
919+
// Checks if a member contains the given feature.
920+
let contains = |feature: InternedString| -> bool {
921+
member_features.contains_key(&feature)
922+
|| summary
923+
.dependencies()
924+
.iter()
925+
.any(|dep| dep.is_optional() && dep.name_in_toml() == feature)
926+
};
927+
928+
for feature in requested_features.features.iter() {
929+
let mut split = feature.splitn(2, '/');
930+
let split = (split.next().unwrap(), split.next());
931+
if let (pkg, Some(pkg_feature)) = split {
932+
let pkg = InternedString::new(pkg);
933+
let pkg_feature = InternedString::new(pkg_feature);
934+
if summary
935+
.dependencies()
936+
.iter()
937+
.any(|dep| dep.name_in_toml() == pkg)
938+
{
939+
// pkg/feat for a dependency.
940+
// Will rely on the dependency resolver to validate `feat`.
941+
features.insert(*feature);
942+
found.insert(*feature);
943+
} else if pkg == member.name() && contains(pkg_feature) {
944+
// member/feat where "feat" is a feature in member.
945+
features.insert(pkg_feature);
946+
found.insert(*feature);
947+
}
948+
} else if contains(*feature) {
949+
// feature exists in this member.
950+
features.insert(*feature);
951+
found.insert(*feature);
952+
}
953+
}
954+
RequestedFeatures {
955+
features: Rc::new(features),
956+
all_features: false,
957+
uses_default_features: requested_features.uses_default_features,
958+
}
959+
};
960+
961+
let members: Vec<(&Package, RequestedFeatures)> = self
962+
.members()
963+
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
964+
.map(|m| (m, matching_features(m)))
965+
.collect();
966+
if members.is_empty() {
967+
// `cargo build -p foo`, where `foo` is not a member.
968+
// Do not allow any command-line flags (defaults only).
969+
if !(requested_features.features.is_empty()
970+
&& !requested_features.all_features
971+
&& requested_features.uses_default_features)
972+
{
973+
anyhow::bail!("cannot specify features for packages outside of workspace");
974+
}
975+
// Add all members from the workspace so we can ensure `-p nonmember`
976+
// is in the resolve graph.
977+
return Ok(self
885978
.members()
886-
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
887-
.map(|m| (m, requested_features.clone()))
979+
.map(|m| (m, RequestedFeatures::new_all(false)))
980+
.collect());
981+
}
982+
if pf2 && *requested_features.features != found {
983+
let missing: Vec<_> = requested_features
984+
.features
985+
.difference(&found)
986+
.copied()
888987
.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");
988+
// TODO: typo suggestions would be good here.
989+
anyhow::bail!(
990+
"none of the selected packages contains these features: {}",
991+
missing.join(", ")
992+
);
993+
}
994+
Ok(members)
995+
}
996+
997+
/// This is the current "stable" behavior for command-line feature selection.
998+
fn members_with_features_stable(
999+
&self,
1000+
specs: &[PackageIdSpec],
1001+
requested_features: &RequestedFeatures,
1002+
) -> CargoResult<Vec<(&Package, RequestedFeatures)>> {
1003+
let ms = self.members().filter_map(|member| {
1004+
let member_id = member.package_id();
1005+
match self.current_opt() {
1006+
// The features passed on the command-line only apply to
1007+
// the "current" package (determined by the cwd).
1008+
Some(current) if member_id == current.package_id() => {
1009+
Some((member, requested_features.clone()))
8971010
}
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-
}
1011+
_ => {
1012+
// Ignore members that are not enabled on the command-line.
1013+
if specs.iter().any(|spec| spec.matches(member_id)) {
1014+
// -p for a workspace member that is not the
1015+
// "current" one, don't use the local
1016+
// `--features`, only allow `--all-features`.
1017+
Some((
1018+
member,
1019+
RequestedFeatures::new_all(requested_features.all_features),
1020+
))
1021+
} else {
1022+
// This member was not requested on the command-line, skip.
1023+
None
9291024
}
9301025
}
931-
});
932-
Ok(ms.collect())
933-
}
1026+
}
1027+
});
1028+
Ok(ms.collect())
9341029
}
9351030
}
9361031

src/cargo/ops/cargo_clean.rs

Lines changed: 8 additions & 1 deletion
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/cargo/util/command_prelude.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ pub trait ArgMatchesExt {
288288
if config.cli_unstable().avoid_dev_deps {
289289
ws.set_require_optional_deps(false);
290290
}
291-
if ws.is_virtual() && !config.cli_unstable().package_features {
291+
let unstable =
292+
config.cli_unstable().package_features || config.cli_unstable().package_features2;
293+
if ws.is_virtual() && !unstable {
292294
// --all-features is actually honored. In general, workspaces and
293295
// feature flags are a bit of a mess right now.
294296
for flag in &["features", "no-default-features"] {

src/doc/src/reference/unstable.md

Lines changed: 26 additions & 1 deletion
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-features2
553+
* Tracking Issue: TODO
554+
555+
The `-Zpackage-features2` 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-features2` 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/main.rs

Lines changed: 1 addition & 0 deletions
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_features2;
7677
mod patch;
7778
mod path;
7879
mod paths;

0 commit comments

Comments
 (0)