Skip to content

Commit f5f34e8

Browse files
committed
Allow reexporting of features between packages
As pointed in #633, it's currently not possible for a package to reexport the feature of another package due to the limitations of how features are defined. This commit adds support for this ability by allowing features of the form `foo/bar` in the `features` section of the manifest. This form indicates that the dependency `foo` should have its `bar` feature enabled. Additionally, it is not required that `foo` is an optional dependency. This does not allow features of the form `foo/bar` in a `[dependencies]` features section as dependencies shouldn't be enabling features for other dependencies. Closes #633 Closes #674
1 parent 9788700 commit f5f34e8

File tree

5 files changed

+253
-58
lines changed

5 files changed

+253
-58
lines changed

src/cargo/core/resolver.rs

+122-52
Original file line numberDiff line numberDiff line change
@@ -318,40 +318,7 @@ fn resolve_deps<'a, R: Registry>(parent: &Summary,
318318
method: ResolveMethod,
319319
ctx: &mut Context<'a, R>)
320320
-> CargoResult<()> {
321-
let dev_deps = match method {
322-
ResolveEverything => true,
323-
ResolveRequired(dev_deps, _, _) => dev_deps,
324-
};
325-
326-
// First, filter by dev-dependencies
327-
let deps = parent.get_dependencies();
328-
let deps = deps.iter().filter(|d| d.is_transitive() || dev_deps);
329-
330-
// Second, weed out optional dependencies, but not those required
331-
let (mut feature_deps, used_features) = try!(build_features(parent, method));
332-
let deps = deps.filter(|d| {
333-
!d.is_optional() || feature_deps.remove(&d.get_name().to_string())
334-
}).collect::<Vec<&Dependency>>();
335-
336-
// All features can only point to optional dependencies, in which case they
337-
// should have all been weeded out by the above iteration. Any remaining
338-
// features are bugs in that the package does not actually have those
339-
// features.
340-
if feature_deps.len() > 0 {
341-
let features = feature_deps.iter().map(|s| s.as_slice())
342-
.collect::<Vec<&str>>().connect(", ");
343-
return Err(human(format!("Package `{}` does not have these features: \
344-
`{}`", parent.get_package_id(), features)))
345-
}
346-
347-
// Record what list of features is active for this package.
348-
{
349-
let pkgid = parent.get_package_id().clone();
350-
match ctx.resolve.features.entry(pkgid) {
351-
Occupied(entry) => entry.into_mut(),
352-
Vacant(entry) => entry.set(HashSet::new()),
353-
}.extend(used_features.into_iter());
354-
}
321+
let (deps, features) = try!(resolve_features(parent, method, ctx));
355322

356323
// Recursively resolve all dependencies
357324
for &dep in deps.iter() {
@@ -409,8 +376,15 @@ fn resolve_deps<'a, R: Registry>(parent: &Summary,
409376
depends on itself",
410377
summary.get_package_id())))
411378
}
379+
380+
// The list of enabled features for this dependency are both those
381+
// listed in the dependency itself as well as any of our own features
382+
// which enabled a feature of this package.
383+
let features = features.find_equiv(&dep.get_name())
384+
.map(|v| v.as_slice())
385+
.unwrap_or(&[]);
412386
try!(resolve_deps(summary,
413-
ResolveRequired(false, dep.get_features(),
387+
ResolveRequired(false, features,
414388
dep.uses_default_features()),
415389
ctx));
416390
if dep.is_transitive() {
@@ -421,10 +395,81 @@ fn resolve_deps<'a, R: Registry>(parent: &Summary,
421395
Ok(())
422396
}
423397

398+
fn resolve_features<'a, R>(parent: &'a Summary, method: ResolveMethod,
399+
ctx: &mut Context<R>)
400+
-> CargoResult<(Vec<&'a Dependency>,
401+
HashMap<&'a str, Vec<String>>)> {
402+
let dev_deps = match method {
403+
ResolveEverything => true,
404+
ResolveRequired(dev_deps, _, _) => dev_deps,
405+
};
406+
407+
// First, filter by dev-dependencies
408+
let deps = parent.get_dependencies();
409+
let deps = deps.iter().filter(|d| d.is_transitive() || dev_deps);
410+
411+
// Second, weed out optional dependencies, but not those required
412+
let (mut feature_deps, used_features) = try!(build_features(parent, method));
413+
let mut ret = HashMap::new();
414+
let deps = deps.filter(|d| {
415+
!d.is_optional() || feature_deps.contains_key_equiv(&d.get_name())
416+
}).collect::<Vec<&Dependency>>();
417+
418+
// Next, sanitize all requested features by whitelisting all the requested
419+
// features that correspond to optional dependencies
420+
for dep in deps.iter() {
421+
let mut base = feature_deps.pop_equiv(&dep.get_name())
422+
.unwrap_or(Vec::new());
423+
for feature in dep.get_features().iter() {
424+
base.push(feature.clone());
425+
if feature.as_slice().contains("/") {
426+
return Err(human(format!("features in dependencies \
427+
cannot enable features in \
428+
other dependencies: `{}`",
429+
feature)));
430+
}
431+
}
432+
ret.insert(dep.get_name(), base);
433+
}
434+
435+
// All features can only point to optional dependencies, in which case they
436+
// should have all been weeded out by the above iteration. Any remaining
437+
// features are bugs in that the package does not actually have those
438+
// features.
439+
if feature_deps.len() > 0 {
440+
let unknown = feature_deps.keys().map(|s| s.as_slice())
441+
.collect::<Vec<&str>>();
442+
if unknown.len() > 0 {
443+
let features = unknown.connect(", ");
444+
return Err(human(format!("Package `{}` does not have these features: \
445+
`{}`", parent.get_package_id(), features)))
446+
}
447+
}
448+
449+
// Record what list of features is active for this package.
450+
{
451+
let pkgid = parent.get_package_id().clone();
452+
match ctx.resolve.features.entry(pkgid) {
453+
Occupied(entry) => entry.into_mut(),
454+
Vacant(entry) => entry.set(HashSet::new()),
455+
}.extend(used_features.into_iter());
456+
}
457+
458+
Ok((deps, ret))
459+
}
460+
424461
// Returns a pair of (feature dependencies, all used features)
462+
//
463+
// The feature dependencies map is a mapping of package name to list of features
464+
// enabled. Each package should be enabled, and each package should have the
465+
// specified set of features enabled.
466+
//
467+
// The all used features set is the set of features which this local package had
468+
// enabled, which is later used when compiling to instruct the code what
469+
// features were enabled.
425470
fn build_features(s: &Summary, method: ResolveMethod)
426-
-> CargoResult<(HashSet<String>, HashSet<String>)> {
427-
let mut deps = HashSet::new();
471+
-> CargoResult<(HashMap<String, Vec<String>>, HashSet<String>)> {
472+
let mut deps = HashMap::new();
428473
let mut used = HashSet::new();
429474
let mut visited = HashSet::new();
430475
match method {
@@ -454,26 +499,51 @@ fn build_features(s: &Summary, method: ResolveMethod)
454499
return Ok((deps, used));
455500

456501
fn add_feature(s: &Summary, feat: &str,
457-
deps: &mut HashSet<String>,
502+
deps: &mut HashMap<String, Vec<String>>,
458503
used: &mut HashSet<String>,
459504
visited: &mut HashSet<String>) -> CargoResult<()> {
460-
if feat.is_empty() {
461-
return Ok(())
462-
}
463-
if !visited.insert(feat.to_string()) {
464-
return Err(human(format!("Cyclic feature dependency: feature `{}` \
465-
depends on itself", feat)))
466-
}
467-
used.insert(feat.to_string());
468-
match s.get_features().find_equiv(&feat) {
469-
Some(recursive) => {
470-
for f in recursive.iter() {
471-
try!(add_feature(s, f.as_slice(), deps, used, visited));
505+
if feat.is_empty() { return Ok(()) }
506+
507+
// If this feature is of the form `foo/bar`, then we just lookup package
508+
// `foo` and enable its feature `bar`. Otherwise this feature is of the
509+
// form `foo` and we need to recurse to enable the feature `foo` for our
510+
// own package, which may end up enabling more features or just enabling
511+
// a dependency.
512+
let mut parts = feat.splitn(1, '/');
513+
let feat_or_package = parts.next().unwrap();
514+
match parts.next() {
515+
Some(feat) => {
516+
let package = feat_or_package;
517+
match deps.entry(package.to_string()) {
518+
Occupied(e) => e.into_mut(),
519+
Vacant(e) => e.set(Vec::new()),
520+
}.push(feat.to_string());
521+
}
522+
None => {
523+
let feat = feat_or_package;
524+
if !visited.insert(feat.to_string()) {
525+
return Err(human(format!("Cyclic feature dependency: \
526+
feature `{}` depends on itself",
527+
feat)))
528+
}
529+
used.insert(feat.to_string());
530+
match s.get_features().find_equiv(&feat) {
531+
Some(recursive) => {
532+
for f in recursive.iter() {
533+
try!(add_feature(s, f.as_slice(), deps, used,
534+
visited));
535+
}
536+
}
537+
None => {
538+
match deps.entry(feat.to_string()) {
539+
Occupied(..) => {} // already activated
540+
Vacant(e) => { e.set(Vec::new()); }
541+
}
542+
}
472543
}
544+
visited.remove(&feat.to_string());
473545
}
474-
None => { deps.insert(feat.to_string()); }
475546
}
476-
visited.remove(&feat.to_string());
477547
Ok(())
478548
}
479549
}

src/cargo/core/summary.rs

+11-6
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,24 @@ impl Summary {
3030
}
3131
for (feature, list) in features.iter() {
3232
for dep in list.iter() {
33-
if features.find_equiv(&dep.as_slice()).is_some() { continue }
34-
let d = dependencies.iter().find(|d| {
35-
d.get_name() == dep.as_slice()
36-
});
37-
match d {
33+
let mut parts = dep.as_slice().splitn(1, '/');
34+
let dep = parts.next().unwrap();
35+
let is_reexport = parts.next().is_some();
36+
if !is_reexport && features.find_equiv(&dep).is_some() { continue }
37+
match dependencies.iter().find(|d| d.get_name() == dep) {
3838
Some(d) => {
39-
if d.is_optional() { continue }
39+
if d.is_optional() || is_reexport { continue }
4040
return Err(human(format!("Feature `{}` depends on `{}` \
4141
which is not an optional \
4242
dependency.\nConsider adding \
4343
`optional = true` to the \
4444
dependency", feature, dep)))
4545
}
46+
None if is_reexport => {
47+
return Err(human(format!("Feature `{}` requires `{}` \
48+
which is not an optional \
49+
dependency", feature, dep)))
50+
}
4651
None => {
4752
return Err(human(format!("Feature `{}` includes `{}` \
4853
which is neither a dependency \

src/doc/manifest.md

+5
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ default = ["jquery", "uglifier"]
157157
# requirements to the feature in the future.
158158
secure-password = ["bcrypt"]
159159

160+
# Features can be used to reexport features of other packages.
161+
# The `session` feature of package `awesome` will ensure that the
162+
# `session` feature of the package `cookie` is also enabled.
163+
session = ["cookie/session"]
164+
160165
[dependencies]
161166

162167
# These packages are mandatory and form the core of this

src/snapshots.txt

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
2014-10-16
2+
linux-i386 61417861716cd41d8f372be36bb0572e4f29dec8
3+
linux-x86_64 59be4ff9f547f1ba47ad133ab74151a48bc2659b
4+
macos-i386 cb5267d2e7df8406c26bb0337b1c2e80b125e2cb
5+
macos-x86_64 9283adb4dfd1b60c7bfe38ef755f9187fe7d5580
6+
winnt-i386 88deb2950fa2b73358bc15763e6373ade6325f53
7+
winnt-x86_64 0143d4b0e4b20e84dbb27a4440b4b55d369f4456
8+
19
2014-09-19
210
linux-i386 c92895421e6fa170dbd713e74334b8c3cf22b817
311
linux-x86_64 66ee4126f9e4820cd82e78181931f8ea365904de

tests/test_cargo_features.rs

+107
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,76 @@ Dev-dependencies are not allowed to be optional: `bar`
137137
").as_slice()));
138138
})
139139

140+
test!(invalid6 {
141+
let p = project("foo")
142+
.file("Cargo.toml", r#"
143+
[project]
144+
name = "foo"
145+
version = "0.0.1"
146+
authors = []
147+
148+
[features]
149+
foo = ["bar/baz"]
150+
"#)
151+
.file("src/main.rs", "");
152+
153+
assert_that(p.cargo_process("build").arg("--features").arg("foo"),
154+
execs().with_status(101).with_stderr(format!("\
155+
Cargo.toml is not a valid manifest
156+
157+
Feature `foo` requires `bar` which is not an optional dependency
158+
").as_slice()));
159+
})
160+
161+
test!(invalid7 {
162+
let p = project("foo")
163+
.file("Cargo.toml", r#"
164+
[project]
165+
name = "foo"
166+
version = "0.0.1"
167+
authors = []
168+
169+
[features]
170+
foo = ["bar/baz"]
171+
bar = []
172+
"#)
173+
.file("src/main.rs", "");
174+
175+
assert_that(p.cargo_process("build").arg("--features").arg("foo"),
176+
execs().with_status(101).with_stderr(format!("\
177+
Cargo.toml is not a valid manifest
178+
179+
Feature `foo` requires `bar` which is not an optional dependency
180+
").as_slice()));
181+
})
182+
183+
test!(invalid8 {
184+
let p = project("foo")
185+
.file("Cargo.toml", r#"
186+
[project]
187+
name = "foo"
188+
version = "0.0.1"
189+
authors = []
190+
191+
[dependencies.bar]
192+
path = "bar"
193+
features = ["foo/bar"]
194+
"#)
195+
.file("src/main.rs", "")
196+
.file("bar/Cargo.toml", r#"
197+
[package]
198+
name = "bar"
199+
version = "0.0.1"
200+
authors = []
201+
"#)
202+
.file("bar/src/lib.rs", "");
203+
204+
assert_that(p.cargo_process("build").arg("--features").arg("foo"),
205+
execs().with_status(101).with_stderr(format!("\
206+
features in dependencies cannot enable features in other dependencies: `foo/bar`
207+
").as_slice()));
208+
})
209+
140210
test!(no_feature_doesnt_build {
141211
let p = project("foo")
142212
.file("Cargo.toml", r#"
@@ -477,3 +547,40 @@ test!(empty_features {
477547
assert_that(p.cargo_process("build").arg("--features").arg(""),
478548
execs().with_status(0));
479549
})
550+
551+
// Tests that all cmd lines work with `--features ""`
552+
test!(transitive_features {
553+
let p = project("foo")
554+
.file("Cargo.toml", r#"
555+
[project]
556+
name = "foo"
557+
version = "0.0.1"
558+
authors = []
559+
560+
[features]
561+
foo = ["bar/baz"]
562+
563+
[dependencies.bar]
564+
path = "bar"
565+
"#)
566+
.file("src/main.rs", "
567+
extern crate bar;
568+
fn main() { bar::baz(); }
569+
")
570+
.file("bar/Cargo.toml", r#"
571+
[package]
572+
name = "bar"
573+
version = "0.0.1"
574+
authors = []
575+
576+
[features]
577+
baz = []
578+
"#)
579+
.file("bar/src/lib.rs", r#"
580+
#[cfg(feature = "baz")]
581+
pub fn baz() {}
582+
"#);
583+
584+
assert_that(p.cargo_process("build").arg("--features").arg("foo"),
585+
execs().with_status(0));
586+
})

0 commit comments

Comments
 (0)