Skip to content

Commit 3fcbb10

Browse files
authored
Auto merge of #2821 - bennofs:no-transitive-dep-feature, r=alexcrichton
Disallow specifying features of transitive deps Before this commit, it was possible to activate a feature in a transtive dependency, using a Cargo.toml like the following one: ... [features] # this will enable feature fast in package bar, which is a # dependency of foo default = [ foo/bar/fast ] This is a bug, and was never intended, and it is checked in other places already. The behavior was possible because `build_features::add_feature` treats the specification "foo/bar/fast" as just another feature. So when we require the feature "foo/bar/fast", add_feature for foo will generate a dependency on "foo" requiring that feature "bar/fast" is enabled. Then, when resolving foo, add_feature will find that "bar/fast" is a required feature, so it'll happily add "fast" as the required feature for the dependency "foo". The fix for this is to make sure that the `add_feature` function does not treat `a/b` specifications as just another feature. Instead, it now handles that case without recursion directly when it encounters it. We can see how this resolves the above problem: when resolving foo, add_feature for the required feature "bar/fast" will be called. Because add_feature no longer treats such specifciations differently at the top level, it will try to enable a feature with the exact name "bar/fast", and Context::resolve_features will later find that no such feature exists for package foo. To give a friendlier error message, we also check in Context::resolve_features that we never ever require a feature with a slash in a name from a dependency.
2 parents 5716f32 + 848f8b5 commit 3fcbb10

File tree

2 files changed

+97
-35
lines changed

2 files changed

+97
-35
lines changed

src/cargo/core/resolver/mod.rs

+45-34
Original file line numberDiff line numberDiff line change
@@ -658,41 +658,53 @@ fn build_features(s: &Summary, method: &Method)
658658
visited: &mut HashSet<String>) -> CargoResult<()> {
659659
if feat.is_empty() { return Ok(()) }
660660

661-
// If this feature is of the form `foo/bar`, then we just lookup package
662-
// `foo` and enable its feature `bar`. Otherwise this feature is of the
663-
// form `foo` and we need to recurse to enable the feature `foo` for our
664-
// own package, which may end up enabling more features or just enabling
665-
// a dependency.
666-
let mut parts = feat.splitn(2, '/');
667-
let feat_or_package = parts.next().unwrap();
668-
match parts.next() {
669-
Some(feat) => {
670-
let package = feat_or_package;
671-
used.insert(package.to_string());
672-
deps.entry(package.to_string())
673-
.or_insert(Vec::new())
674-
.push(feat.to_string());
675-
}
676-
None => {
677-
let feat = feat_or_package;
678-
if !visited.insert(feat.to_string()) {
679-
bail!("Cyclic feature dependency: feature `{}` depends \
680-
on itself", feat)
681-
}
682-
used.insert(feat.to_string());
683-
match s.features().get(feat) {
684-
Some(recursive) => {
685-
for f in recursive {
686-
try!(add_feature(s, f, deps, used, visited));
661+
if !visited.insert(feat.to_string()) {
662+
bail!("Cyclic feature dependency: feature `{}` depends \
663+
on itself", feat)
664+
}
665+
666+
used.insert(feat.to_string());
667+
668+
// The lookup of here may fail if the feature corresponds to an optional
669+
// dependency. If that is the case, we simply enable the optional dependency.
670+
//
671+
// Otherwise, we check what other features the feature wants and recursively
672+
// add them.
673+
match s.features().get(feat) {
674+
Some(wanted_features) => {
675+
for entry in wanted_features {
676+
// If the entry is of the form `foo/bar`, then we just lookup package
677+
// `foo` and enable its feature `bar`. We also add `foo` to the used
678+
// set because `foo` might have been an optional dependency.
679+
//
680+
// Otherwise the entry refers to another feature of our current package,
681+
// so we recurse by calling add_feature again, which may end up enabling
682+
// more features or just enabling a dependency (remember, optional
683+
// dependencies create an implicit feature with the same name).
684+
let mut parts = entry.splitn(2, '/');
685+
let feat_or_package = parts.next().unwrap();
686+
match parts.next() {
687+
Some(feat) => {
688+
let package = feat_or_package;
689+
used.insert(package.to_string());
690+
deps.entry(package.to_string())
691+
.or_insert(Vec::new())
692+
.push(feat.to_string());
693+
}
694+
None => {
695+
let feat = feat_or_package;
696+
try!(add_feature(s, feat, deps, used, visited));
687697
}
688-
}
689-
None => {
690-
deps.entry(feat.to_string()).or_insert(Vec::new());
691698
}
692699
}
693-
visited.remove(&feat.to_string());
700+
}
701+
None => {
702+
deps.entry(feat.to_string()).or_insert(Vec::new());
694703
}
695704
}
705+
706+
visited.remove(&feat.to_string());
707+
696708
Ok(())
697709
}
698710
}
@@ -843,11 +855,10 @@ impl<'a> Context<'a> {
843855
continue
844856
}
845857
let mut base = feature_deps.remove(dep.name()).unwrap_or(vec![]);
846-
for feature in dep.features().iter() {
847-
base.push(feature.clone());
858+
base.extend(dep.features().iter().map(|x| x.clone()));
859+
for feature in base.iter() {
848860
if feature.contains("/") {
849-
bail!("features in dependencies cannot enable features in \
850-
other dependencies: `{}`", feature)
861+
bail!("feature names may not contain slashes: `{}`", feature);
851862
}
852863
}
853864
ret.push((dep.clone(), base));

tests/features.rs

+52-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,58 @@ fn invalid8() {
220220

221221
assert_that(p.cargo_process("build").arg("--features").arg("foo"),
222222
execs().with_status(101).with_stderr("\
223-
[ERROR] features in dependencies cannot enable features in other dependencies: `foo/bar`
223+
[ERROR] feature names may not contain slashes: `foo/bar`
224+
"));
225+
}
226+
227+
#[test]
228+
fn no_transitive_dep_feature_requirement() {
229+
let p = project("foo")
230+
.file("Cargo.toml", r#"
231+
[project]
232+
name = "foo"
233+
version = "0.0.1"
234+
authors = []
235+
236+
[dependencies.derived]
237+
path = "derived"
238+
239+
[features]
240+
default = ["derived/bar/qux"]
241+
"#)
242+
.file("src/main.rs", r#"
243+
extern crate derived;
244+
fn main() { derived::test(); }
245+
"#)
246+
.file("derived/Cargo.toml", r#"
247+
[package]
248+
name = "derived"
249+
version = "0.0.1"
250+
authors = []
251+
252+
[dependencies.bar]
253+
path = "../bar"
254+
"#)
255+
.file("derived/src/lib.rs", r#"
256+
extern crate bar;
257+
pub use bar::test;
258+
"#)
259+
.file("bar/Cargo.toml", r#"
260+
[package]
261+
name = "bar"
262+
version = "0.0.1"
263+
authors = []
264+
265+
[features]
266+
qux = []
267+
"#)
268+
.file("bar/src/lib.rs", r#"
269+
#[cfg(feature = "qux")]
270+
pub fn test() { print!("test"); }
271+
"#);
272+
assert_that(p.cargo_process("build"),
273+
execs().with_status(101).with_stderr("\
274+
[ERROR] feature names may not contain slashes: `bar/qux`
224275
"));
225276
}
226277

0 commit comments

Comments
 (0)