cargo/core/resolver/features.rs
1//! Feature resolver.
2//!
3//! This is a new feature resolver that runs independently of the main
4//! dependency resolver. It is intended to make it easier to experiment with
5//! new behaviors. When `-Zfeatures` is not used, it will fall back to using
6//! the original `Resolve` feature computation. With `-Zfeatures` enabled,
7//! this will walk the dependency graph and compute the features using a
8//! different algorithm.
9//!
10//! One of its key characteristics is that it can avoid unifying features for
11//! shared dependencies in some situations. See `FeatureOpts` for the
12//! different behaviors that can be enabled. If no extra options are enabled,
13//! then it should behave exactly the same as the dependency resolver's
14//! feature resolution. This can be verified by setting the
15//! `__CARGO_FORCE_NEW_FEATURES=compare` environment variable and running
16//! Cargo's test suite (or building other projects), and checking if it
17//! panics. Note: the `features2` tests will fail because they intentionally
18//! compare the old vs new behavior, so forcing the old behavior will
19//! naturally fail the tests.
20//!
21//! The preferred way to engage this new resolver is via
22//! `resolve_ws_with_opts`.
23//!
24//! This does not *replace* feature resolution in the dependency resolver, but
25//! instead acts as a second pass which can *narrow* the features selected in
26//! the dependency resolver. The dependency resolver still needs to do its own
27//! feature resolution in order to avoid selecting optional dependencies that
28//! are never enabled. The dependency resolver could, in theory, just assume
29//! all optional dependencies on all packages are enabled (and remove all
30//! knowledge of features), but that could introduce new requirements that
31//! might change old behavior or cause conflicts. Maybe some day in the future
32//! we could experiment with that, but it seems unlikely to work or be all
33//! that helpful.
34//!
35//! There are many assumptions made about the dependency resolver. This
36//! feature resolver assumes validation has already been done on the feature
37//! maps, and doesn't do any validation itself. It assumes dev-dependencies
38//! within a dependency have been removed. There are probably other
39//! assumptions that I am forgetting.
40
41use crate::core::compiler::{CompileKind, RustcTargetData};
42use crate::core::dependency::{DepKind, Dependency};
43use crate::core::resolver::types::FeaturesSet;
44use crate::core::resolver::Resolve;
45use crate::core::{FeatureValue, InternedString, PackageId, PackageIdSpec, PackageSet, Workspace};
46use crate::util::{CargoResult, Config};
47use std::collections::{BTreeSet, HashMap, HashSet};
48use std::rc::Rc;
49
50/// Map of activated features.
51///
52/// The key is `(PackageId, bool)` where the bool is `true` if these
53/// are features for a build dependency or proc-macro.
54type ActivateMap = HashMap<(PackageId, bool), BTreeSet<InternedString>>;
55
56/// Set of all activated features for all packages in the resolve graph.
57pub struct ResolvedFeatures {
58 activated_features: ActivateMap,
59 /// This is only here for legacy support when `-Zfeatures` is not enabled.
60 legacy: Option<HashMap<PackageId, Vec<InternedString>>>,
61 opts: FeatureOpts,
62}
63
64/// Options for how the feature resolver works.
65#[derive(Default)]
66struct FeatureOpts {
67 /// -Zpackage-features, changes behavior of feature flags in a workspace.
68 package_features: bool,
69 /// -Zfeatures is enabled, use new resolver.
70 new_resolver: bool,
71 /// Build deps and proc-macros will not share share features with other dep kinds.
72 decouple_host_deps: bool,
73 /// Dev dep features will not be activated unless needed.
74 decouple_dev_deps: bool,
75 /// Targets that are not in use will not activate features.
76 ignore_inactive_targets: bool,
77 /// If enabled, compare against old resolver (for testing).
78 compare: bool,
79}
80
81/// Flag to indicate if Cargo is building *any* dev units (tests, examples, etc.).
82///
83/// This disables decoupling of dev dependencies. It may be possible to relax
84/// this in the future, but it will require significant changes to how unit
85/// dependencies are computed, and can result in longer build times with
86/// `cargo test` because the lib may need to be built 3 times instead of
87/// twice.
88#[derive(Copy, Clone, PartialEq)]
89pub enum HasDevUnits {
90 Yes,
91 No,
92}
93
94/// Flag to indicate if features are requested for a build dependency or not.
95#[derive(Debug, PartialEq)]
96pub enum FeaturesFor {
97 NormalOrDev,
98 /// Build dependency or proc-macro.
99 HostDep,
100}
101
102impl FeatureOpts {
103 fn new(config: &Config, has_dev_units: HasDevUnits) -> CargoResult<FeatureOpts> {
104 let mut opts = FeatureOpts::default();
105 let unstable_flags = config.cli_unstable();
106 opts.package_features = unstable_flags.package_features;
107 let mut enable = |feat_opts: &Vec<String>| {
108 opts.new_resolver = true;
109 for opt in feat_opts {
110 match opt.as_ref() {
111 "build_dep" | "host_dep" => opts.decouple_host_deps = true,
112 "dev_dep" => opts.decouple_dev_deps = true,
113 "itarget" => opts.ignore_inactive_targets = true,
114 "all" => {
115 opts.decouple_host_deps = true;
116 opts.decouple_dev_deps = true;
117 opts.ignore_inactive_targets = true;
118 }
119 "compare" => opts.compare = true,
120 "ws" => unimplemented!(),
121 s => anyhow::bail!("-Zfeatures flag `{}` is not supported", s),
122 }
123 }
124 Ok(())
125 };
126 if let Some(feat_opts) = unstable_flags.features.as_ref() {
127 enable(feat_opts)?;
128 }
129 // This env var is intended for testing only.
130 if let Ok(env_opts) = std::env::var("__CARGO_FORCE_NEW_FEATURES") {
131 if env_opts == "1" {
132 opts.new_resolver = true;
133 } else {
134 let env_opts = env_opts.split(',').map(|s| s.to_string()).collect();
135 enable(&env_opts)?;
136 }
137 }
138 if let HasDevUnits::Yes = has_dev_units {
139 opts.decouple_dev_deps = false;
140 }
141 Ok(opts)
142 }
143}
144
145/// Features flags requested for a package.
146#[derive(Debug, Clone, Eq, PartialEq, Hash)]
147pub struct RequestedFeatures {
148 pub features: FeaturesSet,
149 pub all_features: bool,
150 pub uses_default_features: bool,
151}
152
153impl RequestedFeatures {
154 /// Creates a new RequestedFeatures from the given command-line flags.
155 pub fn from_command_line(
156 features: &[String],
157 all_features: bool,
158 uses_default_features: bool,
159 ) -> RequestedFeatures {
160 RequestedFeatures {
161 features: Rc::new(RequestedFeatures::split_features(features)),
162 all_features,
163 uses_default_features,
164 }
165 }
166
167 /// Creates a new RequestedFeatures with the given `all_features` setting.
168 pub fn new_all(all_features: bool) -> RequestedFeatures {
169 RequestedFeatures {
170 features: Rc::new(BTreeSet::new()),
171 all_features,
172 uses_default_features: true,
173 }
174 }
175
176 fn split_features(features: &[String]) -> BTreeSet<InternedString> {
177 features
178 .iter()
179 .flat_map(|s| s.split_whitespace())
180 .flat_map(|s| s.split(','))
181 .filter(|s| !s.is_empty())
182 .map(InternedString::new)
183 .collect::<BTreeSet<InternedString>>()
184 }
185}
186
187impl ResolvedFeatures {
188 /// Returns the list of features that are enabled for the given package.
189 pub fn activated_features(
190 &self,
191 pkg_id: PackageId,
192 features_for: FeaturesFor,
193 ) -> Vec<InternedString> {
194 self.activated_features_int(pkg_id, features_for, true)
195 }
196
197 /// Variant of `activated_features` that returns an empty Vec if this is
198 /// not a valid pkg_id/is_build combination. Used in places which do
199 /// not know which packages are activated (like `cargo clean`).
200 pub fn activated_features_unverified(
201 &self,
202 pkg_id: PackageId,
203 features_for: FeaturesFor,
204 ) -> Vec<InternedString> {
205 self.activated_features_int(pkg_id, features_for, false)
206 }
207
208 fn activated_features_int(
209 &self,
210 pkg_id: PackageId,
211 features_for: FeaturesFor,
212 verify: bool,
213 ) -> Vec<InternedString> {
214 if let Some(legacy) = &self.legacy {
215 legacy.get(&pkg_id).map_or_else(Vec::new, |v| v.clone())
216 } else {
217 let is_build = self.opts.decouple_host_deps && features_for == FeaturesFor::HostDep;
218 if let Some(fs) = self.activated_features.get(&(pkg_id, is_build)) {
219 fs.iter().cloned().collect()
220 } else if verify {
221 panic!("features did not find {:?} {:?}", pkg_id, is_build)
222 } else {
223 Vec::new()
224 }
225 }
226 }
227}
228
229pub struct FeatureResolver<'a, 'cfg> {
230 ws: &'a Workspace<'cfg>,
231 target_data: &'a RustcTargetData,
232 /// The platform to build for, requested by the user.
233 requested_target: CompileKind,
234 resolve: &'a Resolve,
235 package_set: &'a PackageSet<'cfg>,
236 /// Options that change how the feature resolver operates.
237 opts: FeatureOpts,
238 /// Map of features activated for each package.
239 activated_features: ActivateMap,
240 /// Keeps track of which packages have had its dependencies processed.
241 /// Used to avoid cycles, and to speed up processing.
242 processed_deps: HashSet<(PackageId, bool)>,
243}
244
245impl<'a, 'cfg> FeatureResolver<'a, 'cfg> {
246 /// Runs the resolution algorithm and returns a new `ResolvedFeatures`
247 /// with the result.
248 pub fn resolve(
249 ws: &Workspace<'cfg>,
250 target_data: &RustcTargetData,
251 resolve: &Resolve,
252 package_set: &'a PackageSet<'cfg>,
253 requested_features: &RequestedFeatures,
254 specs: &[PackageIdSpec],
255 requested_target: CompileKind,
256 has_dev_units: HasDevUnits,
257 ) -> CargoResult<ResolvedFeatures> {
258 use crate::util::profile;
259 let _p = profile::start("resolve features");
260
261 let opts = FeatureOpts::new(ws.config(), has_dev_units)?;
262 if !opts.new_resolver {
263 // Legacy mode.
264 return Ok(ResolvedFeatures {
265 activated_features: HashMap::new(),
266 legacy: Some(resolve.features_clone()),
267 opts,
268 });
269 }
270 let mut r = FeatureResolver {
271 ws,
272 target_data,
273 requested_target,
274 resolve,
275 package_set,
276 opts,
277 activated_features: HashMap::new(),
278 processed_deps: HashSet::new(),
279 };
280 r.do_resolve(specs, requested_features)?;
281 log::debug!("features={:#?}", r.activated_features);
282 if r.opts.compare {
283 r.compare();
284 }
285 Ok(ResolvedFeatures {
286 activated_features: r.activated_features,
287 legacy: None,
288 opts: r.opts,
289 })
290 }
291
292 /// Performs the process of resolving all features for the resolve graph.
293 fn do_resolve(
294 &mut self,
295 specs: &[PackageIdSpec],
296 requested_features: &RequestedFeatures,
297 ) -> CargoResult<()> {
298 let member_features = self.ws.members_with_features(specs, requested_features)?;
299 for (member, requested_features) in &member_features {
300 let fvs = self.fvs_from_requested(member.package_id(), requested_features);
301 let for_host = self.opts.decouple_host_deps && self.is_proc_macro(member.package_id());
302 self.activate_pkg(member.package_id(), &fvs, for_host)?;
303 if for_host {
304 // Also activate without for_host. This is needed if the
305 // proc-macro includes other targets (like binaries or tests),
306 // or running in `cargo test`. Note that in a workspace, if
307 // the proc-macro is selected on the command like (like with
308 // `--workspace`), this forces feature unification with normal
309 // dependencies. This is part of the bigger problem where
310 // features depend on which packages are built.
311 self.activate_pkg(member.package_id(), &fvs, false)?;
312 }
313 }
314 Ok(())
315 }
316
317 fn activate_pkg(
318 &mut self,
319 pkg_id: PackageId,
320 fvs: &[FeatureValue],
321 for_host: bool,
322 ) -> CargoResult<()> {
323 // Add an empty entry to ensure everything is covered. This is intended for
324 // finding bugs where the resolver missed something it should have visited.
325 // Remove this in the future if `activated_features` uses an empty default.
326 self.activated_features
327 .entry((pkg_id, for_host))
328 .or_insert_with(BTreeSet::new);
329 for fv in fvs {
330 self.activate_fv(pkg_id, fv, for_host)?;
331 }
332 if !self.processed_deps.insert((pkg_id, for_host)) {
333 // Already processed dependencies. There's no need to process them
334 // again. This is primarily to avoid cycles, but also helps speed
335 // things up.
336 //
337 // This is safe because if another package comes along and adds a
338 // feature on this package, it will immediately add it (in
339 // `activate_fv`), and recurse as necessary right then and there.
340 // For example, consider we've already processed our dependencies,
341 // and another package comes along and enables one of our optional
342 // dependencies, it will do so immediately in the
343 // `FeatureValue::CrateFeature` branch, and then immediately
344 // recurse into that optional dependency. This also holds true for
345 // features that enable other features.
346 return Ok(());
347 }
348 for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
349 for (dep, dep_for_host) in deps {
350 if dep.is_optional() {
351 // Optional dependencies are enabled in `activate_fv` when
352 // a feature enables it.
353 continue;
354 }
355 // Recurse into the dependency.
356 let fvs = self.fvs_from_dependency(dep_pkg_id, dep);
357 self.activate_pkg(dep_pkg_id, &fvs, dep_for_host)?;
358 }
359 }
360 Ok(())
361 }
362
363 /// Activate a single FeatureValue for a package.
364 fn activate_fv(
365 &mut self,
366 pkg_id: PackageId,
367 fv: &FeatureValue,
368 for_host: bool,
369 ) -> CargoResult<()> {
370 match fv {
371 FeatureValue::Feature(f) => {
372 self.activate_rec(pkg_id, *f, for_host)?;
373 }
374 FeatureValue::Crate(dep_name) => {
375 // Activate the feature name on self.
376 self.activate_rec(pkg_id, *dep_name, for_host)?;
377 // Activate the optional dep.
378 for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
379 for (dep, dep_for_host) in deps {
380 if dep.name_in_toml() != *dep_name {
381 continue;
382 }
383 let fvs = self.fvs_from_dependency(dep_pkg_id, dep);
384 self.activate_pkg(dep_pkg_id, &fvs, dep_for_host)?;
385 }
386 }
387 }
388 FeatureValue::CrateFeature(dep_name, dep_feature) => {
389 // Activate a feature within a dependency.
390 for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) {
391 for (dep, dep_for_host) in deps {
392 if dep.name_in_toml() != *dep_name {
393 continue;
394 }
395 if dep.is_optional() {
396 // Activate the crate on self.
397 let fv = FeatureValue::Crate(*dep_name);
398 self.activate_fv(pkg_id, &fv, for_host)?;
399 }
400 // Activate the feature on the dependency.
401 let summary = self.resolve.summary(dep_pkg_id);
402 let fv = FeatureValue::new(*dep_feature, summary);
403 self.activate_fv(dep_pkg_id, &fv, dep_for_host)?;
404 }
405 }
406 }
407 }
408 Ok(())
409 }
410
411 /// Activate the given feature for the given package, and then recursively
412 /// activate any other features that feature enables.
413 fn activate_rec(
414 &mut self,
415 pkg_id: PackageId,
416 feature_to_enable: InternedString,
417 for_host: bool,
418 ) -> CargoResult<()> {
419 let enabled = self
420 .activated_features
421 .entry((pkg_id, for_host))
422 .or_insert_with(BTreeSet::new);
423 if !enabled.insert(feature_to_enable) {
424 // Already enabled.
425 return Ok(());
426 }
427 let summary = self.resolve.summary(pkg_id);
428 let feature_map = summary.features();
429 let fvs = match feature_map.get(&feature_to_enable) {
430 Some(fvs) => fvs,
431 None => {
432 // TODO: this should only happen for optional dependencies.
433 // Other cases should be validated by Summary's `build_feature_map`.
434 // Figure out some way to validate this assumption.
435 log::debug!(
436 "pkg {:?} does not define feature {}",
437 pkg_id,
438 feature_to_enable
439 );
440 return Ok(());
441 }
442 };
443 for fv in fvs {
444 self.activate_fv(pkg_id, fv, for_host)?;
445 }
446 Ok(())
447 }
448
449 /// Returns Vec of FeatureValues from a Dependency definition.
450 fn fvs_from_dependency(&self, dep_id: PackageId, dep: &Dependency) -> Vec<FeatureValue> {
451 let summary = self.resolve.summary(dep_id);
452 let feature_map = summary.features();
453 let mut result: Vec<FeatureValue> = dep
454 .features()
455 .iter()
456 .map(|f| FeatureValue::new(*f, summary))
457 .collect();
458 let default = InternedString::new("default");
459 if dep.uses_default_features() && feature_map.contains_key(&default) {
460 result.push(FeatureValue::Feature(default));
461 }
462 result
463 }
464
465 /// Returns Vec of FeatureValues from a set of command-line features.
466 fn fvs_from_requested(
467 &self,
468 pkg_id: PackageId,
469 requested_features: &RequestedFeatures,
470 ) -> Vec<FeatureValue> {
471 let summary = self.resolve.summary(pkg_id);
472 let feature_map = summary.features();
473 if requested_features.all_features {
474 let mut fvs: Vec<FeatureValue> = feature_map
475 .keys()
476 .map(|k| FeatureValue::Feature(*k))
477 .collect();
478 // Add optional deps.
479 // Top-level requested features can never apply to
480 // build-dependencies, so for_host is `false` here.
481 for (_dep_pkg_id, deps) in self.deps(pkg_id, false) {
482 for (dep, _dep_for_host) in deps {
483 if dep.is_optional() {
484 // This may result in duplicates, but that should be ok.
485 fvs.push(FeatureValue::Crate(dep.name_in_toml()));
486 }
487 }
488 }
489 fvs
490 } else {
491 let mut result: Vec<FeatureValue> = requested_features
492 .features
493 .as_ref()
494 .iter()
495 .map(|f| FeatureValue::new(*f, summary))
496 .collect();
497 let default = InternedString::new("default");
498 if requested_features.uses_default_features && feature_map.contains_key(&default) {
499 result.push(FeatureValue::Feature(default));
500 }
501 result
502 }
503 }
504
505 /// Returns the dependencies for a package, filtering out inactive targets.
506 fn deps(
507 &self,
508 pkg_id: PackageId,
509 for_host: bool,
510 ) -> Vec<(PackageId, Vec<(&'a Dependency, bool)>)> {
511 // Helper for determining if a platform is activated.
512 let platform_activated = |dep: &Dependency| -> bool {
513 // We always care about build-dependencies, and they are always
514 // Host. If we are computing dependencies "for a build script",
515 // even normal dependencies are host-only.
516 if for_host || dep.is_build() {
517 return self
518 .target_data
519 .dep_platform_activated(dep, CompileKind::Host);
520 }
521 // Not a build dependency, and not for a build script, so must be Target.
522 self.target_data
523 .dep_platform_activated(dep, self.requested_target)
524 };
525 self.resolve
526 .deps(pkg_id)
527 .map(|(dep_id, deps)| {
528 let is_proc_macro = self.is_proc_macro(dep_id);
529 let deps = deps
530 .iter()
531 .filter(|dep| {
532 if dep.platform().is_some()
533 && self.opts.ignore_inactive_targets
534 && !platform_activated(dep)
535 {
536 return false;
537 }
538 if self.opts.decouple_dev_deps && dep.kind() == DepKind::Development {
539 return false;
540 }
541 true
542 })
543 .map(|dep| {
544 let dep_for_host = for_host
545 || (self.opts.decouple_host_deps && (dep.is_build() || is_proc_macro));
546 (dep, dep_for_host)
547 })
548 .collect::<Vec<_>>();
549 (dep_id, deps)
550 })
551 .filter(|(_id, deps)| !deps.is_empty())
552 .collect()
553 }
554
555 /// Compare the activated features to the resolver. Used for testing.
556 fn compare(&self) {
557 let mut found = false;
558 for ((pkg_id, dep_kind), features) in &self.activated_features {
559 let r_features = self.resolve.features(*pkg_id);
560 if !r_features.iter().eq(features.iter()) {
561 eprintln!(
562 "{}/{:?} features mismatch\nresolve: {:?}\nnew: {:?}\n",
563 pkg_id, dep_kind, r_features, features
564 );
565 found = true;
566 }
567 }
568 if found {
569 panic!("feature mismatch");
570 }
571 }
572
573 fn is_proc_macro(&self, package_id: PackageId) -> bool {
574 self.package_set
575 .get_one(package_id)
576 .expect("packages downloaded")
577 .proc_macro()
578 }
579}