Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend -Zpackage-features with more capabilities. #8074

Merged
merged 2 commits into from
Apr 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 134 additions & 49 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::cell::RefCell;
use std::collections::hash_map::{Entry, HashMap};
use std::collections::{BTreeMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::slice;

use glob::glob;
Expand All @@ -11,7 +12,7 @@ use url::Url;
use crate::core::features::Features;
use crate::core::registry::PackageRegistry;
use crate::core::resolver::features::RequestedFeatures;
use crate::core::{Dependency, PackageId, PackageIdSpec};
use crate::core::{Dependency, InternedString, PackageId, PackageIdSpec};
use crate::core::{EitherManifest, Package, SourceId, VirtualManifest};
use crate::ops;
use crate::sources::PathSource;
Expand Down Expand Up @@ -878,59 +879,143 @@ impl<'cfg> Workspace<'cfg> {
.collect());
}
if self.config().cli_unstable().package_features {
if specs.len() > 1 && !requested_features.features.is_empty() {
anyhow::bail!("cannot specify features for more than one package");
self.members_with_features_pf(specs, requested_features)
} else {
self.members_with_features_stable(specs, requested_features)
}
}

/// New command-line feature selection with -Zpackage-features.
fn members_with_features_pf(
&self,
specs: &[PackageIdSpec],
requested_features: &RequestedFeatures,
) -> CargoResult<Vec<(&Package, RequestedFeatures)>> {
// Keep track of which features matched *any* member, to produce an error
// if any of them did not match anywhere.
let mut found: BTreeSet<InternedString> = BTreeSet::new();

// Returns the requested features for the given member.
// This filters out any named features that the member does not have.
let mut matching_features = |member: &Package| -> RequestedFeatures {
if requested_features.features.is_empty() || requested_features.all_features {
return requested_features.clone();
}
// Only include features this member defines.
let summary = member.summary();
let member_features = summary.features();
let mut features = BTreeSet::new();

// Checks if a member contains the given feature.
let contains = |feature: InternedString| -> bool {
member_features.contains_key(&feature)
|| summary
.dependencies()
.iter()
.any(|dep| dep.is_optional() && dep.name_in_toml() == feature)
};

for feature in requested_features.features.iter() {
let mut split = feature.splitn(2, '/');
let split = (split.next().unwrap(), split.next());
if let (pkg, Some(pkg_feature)) = split {
let pkg = InternedString::new(pkg);
let pkg_feature = InternedString::new(pkg_feature);
if summary
.dependencies()
.iter()
.any(|dep| dep.name_in_toml() == pkg)
{
// pkg/feat for a dependency.
// Will rely on the dependency resolver to validate `feat`.
features.insert(*feature);
found.insert(*feature);
} else if pkg == member.name() && contains(pkg_feature) {
// member/feat where "feat" is a feature in member.
features.insert(pkg_feature);
found.insert(*feature);
}
} else if contains(*feature) {
// feature exists in this member.
features.insert(*feature);
found.insert(*feature);
}
}
RequestedFeatures {
features: Rc::new(features),
all_features: false,
uses_default_features: requested_features.uses_default_features,
}
};

let members: Vec<(&Package, RequestedFeatures)> = self
.members()
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
.map(|m| (m, matching_features(m)))
.collect();
if members.is_empty() {
// `cargo build -p foo`, where `foo` is not a member.
// Do not allow any command-line flags (defaults only).
if !(requested_features.features.is_empty()
&& !requested_features.all_features
&& requested_features.uses_default_features)
{
anyhow::bail!("cannot specify features for packages outside of workspace");
}
let members: Vec<(&Package, RequestedFeatures)> = self
// Add all members from the workspace so we can ensure `-p nonmember`
// is in the resolve graph.
return Ok(self
.members()
.filter(|m| specs.iter().any(|spec| spec.matches(m.package_id())))
.map(|m| (m, requested_features.clone()))
.map(|m| (m, RequestedFeatures::new_all(false)))
.collect());
}
if *requested_features.features != found {
let missing: Vec<_> = requested_features
.features
.difference(&found)
.copied()
.collect();
if members.is_empty() {
// `cargo build -p foo`, where `foo` is not a member.
// Do not allow any command-line flags (defaults only).
if !(requested_features.features.is_empty()
&& !requested_features.all_features
&& requested_features.uses_default_features)
{
anyhow::bail!("cannot specify features for packages outside of workspace");
// TODO: typo suggestions would be good here.
anyhow::bail!(
"none of the selected packages contains these features: {}",
missing.join(", ")
);
}
Ok(members)
}

/// This is the current "stable" behavior for command-line feature selection.
fn members_with_features_stable(
&self,
specs: &[PackageIdSpec],
requested_features: &RequestedFeatures,
) -> CargoResult<Vec<(&Package, RequestedFeatures)>> {
let ms = self.members().filter_map(|member| {
let member_id = member.package_id();
match self.current_opt() {
// The features passed on the command-line only apply to
// the "current" package (determined by the cwd).
Some(current) if member_id == current.package_id() => {
Some((member, requested_features.clone()))
}
// Add all members from the workspace so we can ensure `-p nonmember`
// is in the resolve graph.
return Ok(self
.members()
.map(|m| (m, RequestedFeatures::new_all(false)))
.collect());
}
Ok(members)
} else {
let ms = self.members().filter_map(|member| {
let member_id = member.package_id();
match self.current_opt() {
// The features passed on the command-line only apply to
// the "current" package (determined by the cwd).
Some(current) if member_id == current.package_id() => {
Some((member, requested_features.clone()))
}
_ => {
// Ignore members that are not enabled on the command-line.
if specs.iter().any(|spec| spec.matches(member_id)) {
// -p for a workspace member that is not the
// "current" one, don't use the local
// `--features`, only allow `--all-features`.
Some((
member,
RequestedFeatures::new_all(requested_features.all_features),
))
} else {
// This member was not requested on the command-line, skip.
None
}
_ => {
// Ignore members that are not enabled on the command-line.
if specs.iter().any(|spec| spec.matches(member_id)) {
// -p for a workspace member that is not the
// "current" one, don't use the local
// `--features`, only allow `--all-features`.
Some((
member,
RequestedFeatures::new_all(requested_features.all_features),
))
} else {
// This member was not requested on the command-line, skip.
None
}
}
});
Ok(ms.collect())
}
}
});
Ok(ms.collect())
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/cargo/ops/cargo_clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
let mut build_config = BuildConfig::new(config, Some(1), &opts.target, CompileMode::Build)?;
build_config.requested_profile = opts.requested_profile;
let target_data = RustcTargetData::new(ws, build_config.requested_kind)?;
let resolve_opts = ResolveOpts::everything();
// Resolve for default features. In the future, `cargo clean` should be rewritten
// so that it doesn't need to guess filename hashes.
let resolve_opts = ResolveOpts::new(
/*dev_deps*/ true,
&[],
/*all features*/ false,
/*default*/ true,
);
let specs = opts
.spec
.iter()
Expand Down
27 changes: 26 additions & 1 deletion src/doc/src/reference/unstable.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ cargo +nightly -Zunstable-options -Zconfig-include --config somefile.toml build

CLI paths are relative to the current working directory.

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

### package-features
* Tracking Issue: [#5364](https://github.com/rust-lang/cargo/issues/5364)

The `-Zpackage-features` flag changes the way features can be passed on the
command-line for a workspace. The normal behavior can be confusing, as the
features passed are always enabled on the package in the current directory,
even if that package is not selected with a `-p` flag. Feature flags also do
not work in the root of a virtual workspace. `-Zpackage-features` tries to
make feature flags behave in a more intuitive manner.

* `cargo build -p other_member --features …` — This now only enables the given
features as defined in `other_member` (ignores whatever is in the current
directory).
* `cargo build -p a -p b --features …` — This now enables the given features
on both `a` and `b`. Not all packages need to define every feature, it only
enables matching features. It is still an error if none of the packages
define a given feature.
* `--features` and `--no-default-features` are now allowed in the root of a
virtual workspace.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm, these flags, when applied in this scenario, would be applied to all workspace members, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

* `member_name/feature_name` syntax may now be used on the command-line to
enable features for a specific member.

The ability to set features for non-workspace members is no longer allowed, as
the resolver fundamentally does not support that ability.

### crate-versions
* Tracking Issue: [#7907](https://github.com/rust-lang/cargo/issues/7907)

Expand Down
74 changes: 0 additions & 74 deletions tests/testsuite/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1358,80 +1358,6 @@ fn many_cli_features_comma_and_space_delimited() {
.run();
}

#[cargo_test]
fn combining_features_and_package() {
Package::new("dep", "1.0.0").publish();

let p = project()
.file(
"Cargo.toml",
r#"
[project]
name = "foo"
version = "0.0.1"
authors = []

[workspace]
members = ["bar"]

[dependencies]
dep = "1"
"#,
)
.file("src/lib.rs", "")
.file(
"bar/Cargo.toml",
r#"
[package]
name = "bar"
version = "0.0.1"
authors = []
[features]
main = []
"#,
)
.file(
"bar/src/main.rs",
r#"
#[cfg(feature = "main")]
fn main() {}
"#,
)
.build();

p.cargo("build -Z package-features --workspace --features main")
.masquerade_as_nightly_cargo()
.with_status(101)
.with_stderr_contains("[ERROR] cannot specify features for more than one package")
.run();

p.cargo("build -Z package-features --package dep --features main")
.masquerade_as_nightly_cargo()
.with_status(101)
.with_stderr_contains("[ERROR] cannot specify features for packages outside of workspace")
.run();
p.cargo("build -Z package-features --package dep --all-features")
.masquerade_as_nightly_cargo()
.with_status(101)
.with_stderr_contains("[ERROR] cannot specify features for packages outside of workspace")
.run();
p.cargo("build -Z package-features --package dep --no-default-features")
.masquerade_as_nightly_cargo()
.with_status(101)
.with_stderr_contains("[ERROR] cannot specify features for packages outside of workspace")
.run();

p.cargo("build -Z package-features --workspace --all-features")
.masquerade_as_nightly_cargo()
.run();
p.cargo("run -Z package-features --package bar --features main")
.masquerade_as_nightly_cargo()
.run();
p.cargo("build -Z package-features --package dep")
.masquerade_as_nightly_cargo()
.run();
}

#[cargo_test]
fn namespaced_invalid_feature() {
let p = project()
Expand Down
1 change: 1 addition & 0 deletions tests/testsuite/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ mod offline;
mod out_dir;
mod owner;
mod package;
mod package_features;
mod patch;
mod path;
mod paths;
Expand Down
Loading