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

Introduction of namespaced features (see #1286) #5300

Merged
merged 8 commits into from
Apr 30, 2018
3 changes: 3 additions & 0 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ features! {

// Overriding profiles for dependencies.
[unstable] profile_overrides: bool,

// Separating the namespaces for features and dependencies
[unstable] namespaced_features: bool,
}
}

Expand Down
220 changes: 181 additions & 39 deletions src/cargo/core/summary.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::mem;
use std::rc::Rc;

Expand Down Expand Up @@ -26,6 +26,7 @@ struct Inner {
features: FeatureMap,
checksum: Option<String>,
links: Option<InternedString>,
namespaced_features: bool,
}

impl Summary {
Expand All @@ -34,9 +35,10 @@ impl Summary {
dependencies: Vec<Dependency>,
features: BTreeMap<String, Vec<String>>,
links: Option<String>,
namespaced_features: bool,
) -> CargoResult<Summary> {
for dep in dependencies.iter() {
if features.get(&*dep.name()).is_some() {
if !namespaced_features && features.get(&*dep.name()).is_some() {
bail!(
"Features and dependencies cannot have the \
same name: `{}`",
Expand All @@ -50,14 +52,15 @@ impl Summary {
)
}
}
let feature_map = build_feature_map(features, &dependencies)?;
let feature_map = build_feature_map(features, &dependencies, namespaced_features)?;
Ok(Summary {
inner: Rc::new(Inner {
package_id: pkg_id,
dependencies,
features: feature_map,
checksum: None,
links: links.map(|l| InternedString::new(&l)),
namespaced_features,
}),
})
}
Expand Down Expand Up @@ -86,6 +89,9 @@ impl Summary {
pub fn links(&self) -> Option<InternedString> {
self.inner.links
}
pub fn namespaced_features(&self) -> bool {
self.inner.namespaced_features
}

pub fn override_id(mut self, id: PackageId) -> Summary {
Rc::make_mut(&mut self.inner).package_id = id;
Expand Down Expand Up @@ -131,55 +137,179 @@ impl PartialEq for Summary {
fn build_feature_map(
features: BTreeMap<String, Vec<String>>,
dependencies: &[Dependency],
namespaced: bool,
) -> CargoResult<FeatureMap> {
use self::FeatureValue::*;
let dep_map: HashMap<_, _> = dependencies
.iter()
.map(|d| (d.name().as_str(), d))
.collect();

let mut map = BTreeMap::new();
for (feature, list) in features.iter() {
// If namespaced features is active and the key is the same as that of an
// optional dependency, that dependency must be included in the values.
// Thus, if a `feature` is found that has the same name as a dependency, we
// (a) bail out if the dependency is non-optional, and (b) we track if the
// feature requirements include the dependency `crate:feature` in the list.
// This is done with the `dependency_found` variable, which can only be
// false if features are namespaced and the current feature key is the same
// as the name of an optional dependency. If so, it gets set to true during
// iteration over the list if the dependency is found in the list.
let mut dependency_found = if namespaced {
match dep_map.get(feature.as_str()) {
Some(ref dep_data) => {
if !dep_data.is_optional() {
bail!(
"Feature `{}` includes the dependency of the same name, but this is \
left implicit in the features included by this feature.\n\
Additionally, the dependency must be marked as optional to be \
included in the feature definition.\n\
Consider adding `crate:{}` to this feature's requirements \
and marking the dependency as `optional = true`",
feature,
feature
)
} else {
false
}
}
None => true,
}
} else {
true
};

let mut values = vec![];
for dep in list {
let val = FeatureValue::build(InternedString::new(dep), |fs| features.contains_key(fs));
if let &Feature(_) = &val {
values.push(val);
continue;
}
let val = FeatureValue::build(
InternedString::new(dep),
|fs| features.contains_key(fs),
namespaced,
);

// Find data for the referenced dependency...
let dep_data = {
let dep_name = match val {
Feature(_) => "",
Crate(ref dep_name) | CrateFeature(ref dep_name, _) => dep_name,
};
dependencies.iter().find(|d| *d.name() == *dep_name)
match val {
Feature(ref dep_name) | Crate(ref dep_name) | CrateFeature(ref dep_name, _) => {
dep_map.get(dep_name.as_str())
}
}
};
let is_optional_dep = dep_data.map_or(false, |d| d.is_optional());
if let FeatureValue::Crate(ref dep_name) = val {
// If we have a dependency value, check if this is the dependency named
// the same as the feature that we were looking for.
if !dependency_found && feature == dep_name.as_str() {
dependency_found = true;
}
}

match (&val, dep_data) {
(&Crate(ref dep), Some(d)) => {
if !d.is_optional() {
bail!(
"Feature `{}` depends on `{}` which is not an \
optional dependency.\nConsider adding \
`optional = true` to the dependency",
feature,
dep
)
match (&val, dep_data.is_some(), is_optional_dep) {
// The value is a feature. If features are namespaced, this just means
// it's not prefixed with `crate:`, so we have to check whether the
// feature actually exist. If the feature is not defined *and* an optional
// dependency of the same name exists, the feature is defined implicitly
// here by adding it to the feature map, pointing to the dependency.
// If features are not namespaced, it's been validated as a feature already
// while instantiating the `FeatureValue` in `FeatureValue::build()`, so
// we don't have to do so here.
(&Feature(feat), _, true) => {
if namespaced && !features.contains_key(&*feat) {
map.insert(feat.to_string(), vec![FeatureValue::Crate(feat)]);
}
}
// If features are namespaced and the value is not defined as a feature
// and there is no optional dependency of the same name, error out.
// If features are not namespaced, there must be an existing feature
// here (checked by `FeatureValue::build()`), so it will always be defined.
(&Feature(feat), dep_exists, false) => {
if namespaced && !features.contains_key(&*feat) {
if dep_exists {
bail!(
"Feature `{}` includes `{}` which is not defined as a feature.\n\
A non-optional dependency of the same name is defined; consider \
adding `optional = true` to its definition",
feature,
feat
)
} else {
bail!(
"Feature `{}` includes `{}` which is not defined as a feature",
feature,
feat
)
}
}
}
(&CrateFeature(ref dep_name, _), None) => bail!(
// The value is a dependency. If features are namespaced, it is explicitly
// tagged as such (`crate:value`). If features are not namespaced, any value
// not recognized as a feature is pegged as a `Crate`. Here we handle the case
// where the dependency exists but is non-optional. It branches on namespaced
// just to provide the correct string for the crate dependency in the error.
(&Crate(ref dep), true, false) => if namespaced {
bail!(
"Feature `{}` includes `crate:{}` which is not an \
optional dependency.\nConsider adding \
`optional = true` to the dependency",
feature,
dep
)
} else {
bail!(
"Feature `{}` depends on `{}` which is not an \
optional dependency.\nConsider adding \
`optional = true` to the dependency",
feature,
dep
)
},
// If namespaced, the value was tagged as a dependency; if not namespaced,
// this could be anything not defined as a feature. This handles the case
// where no such dependency is actually defined; again, the branch on
// namespaced here is just to provide the correct string in the error.
(&Crate(ref dep), false, _) => if namespaced {
bail!(
"Feature `{}` includes `crate:{}` which is not a known \
dependency",
feature,
dep
)
} else {
bail!(
"Feature `{}` includes `{}` which is neither a dependency nor \
another feature",
feature,
dep
)
},
(&Crate(_), true, true) => {}
// If the value is a feature for one of the dependencies, bail out if no such
// dependency is actually defined in the manifest.
(&CrateFeature(ref dep, _), false, _) => bail!(
"Feature `{}` requires a feature of `{}` which is not a \
dependency",
feature,
dep_name
),
(&Crate(ref dep), None) => bail!(
"Feature `{}` includes `{}` which is neither \
a dependency nor another feature",
feature,
dep
),
(&CrateFeature(_, _), Some(_)) | (&Feature(_), _) => {}
(&CrateFeature(_, _), true, _) => {}
}
values.push(val);
}

if !dependency_found {
Copy link
Member

Choose a reason for hiding this comment

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

I keep having a tough time following this dependency_found logic, but is there a reason it needs to span across the match statement above? Could this error be moved up before the match?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a whole bunch of comments, does that help? It spans across the for-loop (which includes the match) as a performance optimization: this way, we don't have to loop over the feature values twice. That's entirely premature (in the sense of not having done any performance measurements), so if the comments don't help enough, I could instead rewrite to check the values list after the loop.

// If we have not found the dependency of the same-named feature, we should
// bail here.
bail!(
"Feature `{}` includes the optional dependency of the \
same name, but this is left implicit in the features \
included by this feature.\nConsider adding \
`crate:{}` to this feature's requirements.",
feature,
feature
)
}

map.insert(feature.clone(), values);
}
Ok(map)
Expand All @@ -200,30 +330,42 @@ pub enum FeatureValue {
}

impl FeatureValue {
fn build<T>(feature: InternedString, is_feature: T) -> FeatureValue
fn build<T>(feature: InternedString, is_feature: T, namespaced: bool) -> FeatureValue
where
T: Fn(&str) -> bool,
{
match feature.find('/') {
Some(pos) => {
match (feature.find('/'), namespaced) {
(Some(pos), _) => {
let (dep, dep_feat) = feature.split_at(pos);
let dep_feat = &dep_feat[1..];
FeatureValue::CrateFeature(InternedString::new(dep), InternedString::new(dep_feat))
}
None if is_feature(&feature) => FeatureValue::Feature(feature),
None => FeatureValue::Crate(feature),
(None, true) if feature.starts_with("crate:") => {
FeatureValue::Crate(InternedString::new(&feature[6..]))
}
(None, true) => FeatureValue::Feature(feature),
(None, false) if is_feature(&feature) => FeatureValue::Feature(feature),
(None, false) => FeatureValue::Crate(feature),
}
}

pub fn new(feature: InternedString, s: &Summary) -> FeatureValue {
Self::build(feature, |fs| s.features().contains_key(fs))
Self::build(
feature,
|fs| s.features().contains_key(fs),
s.namespaced_features(),
)
}

pub fn to_string(&self) -> String {
pub fn to_string(&self, s: &Summary) -> String {
use self::FeatureValue::*;
match *self {
Feature(ref f) => f.to_string(),
Crate(ref c) => c.to_string(),
Crate(ref c) => if s.namespaced_features() {
format!("crate:{}", &c)
} else {
c.to_string()
},
CrateFeature(ref c, ref f) => [c.as_ref(), f.as_ref()].join("/"),
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/cargo/ops/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,14 @@ fn transmit(
return Ok(());
}

let string_features = pkg.summary()
let summary = pkg.summary();
let string_features = summary
.features()
.iter()
.map(|(feat, values)| {
(
feat.clone(),
values.iter().map(|fv| fv.to_string()).collect(),
values.iter().map(|fv| fv.to_string(&summary)).collect(),
)
})
.collect::<BTreeMap<String, Vec<String>>>();
Expand Down
2 changes: 1 addition & 1 deletion src/cargo/sources/registry/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl<'cfg> RegistryIndex<'cfg> {
let deps = deps.into_iter()
.map(|dep| dep.into_dep(&self.source_id))
.collect::<CargoResult<Vec<_>>>()?;
let summary = Summary::new(pkgid, deps, features, links)?;
let summary = Summary::new(pkgid, deps, features, links, false)?;
let summary = summary.set_checksum(cksum.clone());
if self.hashes.contains_key(&name[..]) {
self.hashes.get_mut(&name[..]).unwrap().insert(vers, cksum);
Expand Down
6 changes: 6 additions & 0 deletions src/cargo/util/toml/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,8 @@ pub struct TomlProject {
autoexamples: Option<bool>,
autotests: Option<bool>,
autobenches: Option<bool>,
#[serde(rename = "namespaced-features")]
namespaced_features: Option<bool>,

// package metadata
description: Option<String>,
Expand Down Expand Up @@ -837,12 +839,16 @@ impl TomlManifest {

let exclude = project.exclude.clone().unwrap_or_default();
let include = project.include.clone().unwrap_or_default();
if project.namespaced_features.is_some() {
features.require(Feature::namespaced_features())?;
}

let summary = Summary::new(
pkgid,
deps,
me.features.clone().unwrap_or_else(BTreeMap::new),
project.links.clone(),
project.namespaced_features.unwrap_or(false),
Copy link
Member

Choose a reason for hiding this comment

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

Ah I just remembered, can this be gated behind a feature as well? (setting this key at all)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure - not sure what kind of gate you had in mind?

Copy link
Member

Choose a reason for hiding this comment

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

Oh something like cargo-features = ["namespaced-features"] in Cargo.toml, you can see some other examples of this via src/cargo/core/features.rs I think

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexcrichton in that case, do we still need the separate namespaced-features in the manifest? It seems like those would basically be duplicate values.

Copy link
Member

Choose a reason for hiding this comment

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

For now yeah we'll want that (the Cargo feature basically unlocks the namespaced-features config value).

Long term we may not have both but for the near-term I think we'll want that.

)?;
let metadata = ManifestMetadata {
description: project.description.clone(),
Expand Down
Loading