guppy/graph/feature/build.rs
1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5 errors::{FeatureBuildStage, FeatureGraphWarning},
6 graph::{
7 DepRequiredOrOptional, DependencyReq, FeatureIndexInPackage, FeatureIx, NamedFeatureDep,
8 PackageGraph, PackageIx, PackageLink, PackageMetadata,
9 feature::{
10 ConditionalLinkImpl, FeatureEdge, FeatureGraphImpl, FeatureLabel, FeatureMetadataImpl,
11 FeatureNode, WeakDependencies, WeakIndex,
12 },
13 },
14 platform::PlatformStatusImpl,
15};
16use ahash::AHashMap;
17use cargo_metadata::DependencyKind;
18use once_cell::sync::OnceCell;
19use petgraph::{prelude::*, visit::IntoEdgeReferences};
20use smallvec::SmallVec;
21use std::iter;
22
23pub(super) type FeaturePetgraph = Graph<FeatureNode, FeatureEdge, Directed, FeatureIx>;
24pub(super) type FeatureEdgeReference<'g> = <&'g FeaturePetgraph as IntoEdgeReferences>::EdgeRef;
25
26#[derive(Debug)]
27pub(super) struct FeatureGraphBuildState {
28 graph: FeaturePetgraph,
29 // Map from package ixs to the base (first) feature for each package.
30 base_ixs: Vec<NodeIndex<FeatureIx>>,
31 map: AHashMap<FeatureNode, FeatureMetadataImpl>,
32 weak: WeakDependencies,
33 warnings: Vec<FeatureGraphWarning>,
34}
35
36impl FeatureGraphBuildState {
37 pub(super) fn new(package_graph: &PackageGraph) -> Self {
38 let package_count = package_graph.package_count();
39 Self {
40 // Each package corresponds to at least one feature ID.
41 graph: Graph::with_capacity(package_count, package_count),
42 // Each package corresponds to exactly one base feature ix, and there's one last ix at
43 // the end.
44 base_ixs: Vec::with_capacity(package_count + 1),
45 map: AHashMap::with_capacity(package_count),
46 weak: WeakDependencies::new(),
47 warnings: vec![],
48 }
49 }
50
51 /// Add nodes for every feature in this package + the base package, and add edges from every
52 /// feature to the base package.
53 pub(super) fn add_nodes(&mut self, package: PackageMetadata<'_>) {
54 let base_node = FeatureNode::base(package.package_ix());
55 let base_ix = self.add_node(base_node);
56 self.base_ixs.push(base_ix);
57 FeatureNode::named_features(package)
58 .chain(FeatureNode::optional_deps(package))
59 .for_each(|feature_node| {
60 let feature_ix = self.add_node(feature_node);
61 self.graph
62 .update_edge(feature_ix, base_ix, FeatureEdge::FeatureToBase);
63 });
64 }
65
66 /// Mark the end of adding nodes.
67 pub(super) fn end_nodes(&mut self) {
68 self.base_ixs.push(NodeIndex::new(self.graph.node_count()));
69 }
70
71 pub(super) fn add_named_feature_edges(&mut self, metadata: PackageMetadata<'_>) {
72 let dep_name_to_link: AHashMap<_, _> = metadata
73 .direct_links()
74 .map(|link| (link.dep_name(), link))
75 .collect();
76
77 metadata
78 .named_features_full()
79 .for_each(|(n, from_feature, feature_deps)| {
80 let from_node = FeatureNode::new(metadata.package_ix(), n);
81
82 let to_nodes_edges: Vec<_> = feature_deps
83 .iter()
84 .flat_map(|feature_dep| {
85 self.nodes_for_named_feature_dep(
86 metadata,
87 from_feature,
88 feature_dep,
89 &dep_name_to_link,
90 )
91 })
92 // The flat_map above holds an &mut reference to self, which is why it needs to
93 // be collected.
94 .collect();
95
96 // Don't create a map to the base 'from' node since it is already created in
97 // add_nodes.
98 self.add_edges(from_node, to_nodes_edges, metadata.graph());
99 })
100 }
101
102 fn nodes_for_named_feature_dep(
103 &mut self,
104 metadata: PackageMetadata<'_>,
105 from_named_feature: &str,
106 feature_dep: &NamedFeatureDep,
107 dep_name_to_link: &AHashMap<&str, PackageLink>,
108 ) -> SmallVec<[(FeatureNode, FeatureEdge); 3]> {
109 let from_label = FeatureLabel::Named(from_named_feature);
110 let mut nodes_edges: SmallVec<[(FeatureNode, FeatureEdge); 3]> = SmallVec::new();
111
112 match feature_dep {
113 NamedFeatureDep::DependencyNamedFeature {
114 dep_name,
115 feature,
116 weak,
117 } => {
118 if let Some(link) = dep_name_to_link.get(dep_name.as_ref()) {
119 let weak_index = weak.then(|| self.weak.insert(link.edge_ix()));
120
121 // Dependency from (`main`, `a`) to (`dep, `foo`)
122 if let Some(cross_node) = self.make_named_feature_node(
123 &metadata,
124 from_label,
125 &link.to(),
126 FeatureLabel::Named(feature.as_ref()),
127 true,
128 ) {
129 // This is a cross-package link. The platform-specific
130 // requirements still apply, so grab them from the
131 // PackageLink.
132 nodes_edges.push((
133 cross_node,
134 Self::make_named_feature_cross_edge(link, weak_index),
135 ));
136 };
137
138 // If the package is present as an optional dependency, it is
139 // implicitly activated by the feature:
140 // from (`main`, `a`) to (`main`, `dep:dep`)
141 if let Some(same_node) = self.make_named_feature_node(
142 &metadata,
143 from_label,
144 &metadata,
145 FeatureLabel::OptionalDependency(dep_name),
146 // Don't warn if this dep isn't optional.
147 false,
148 ) {
149 nodes_edges.push((
150 same_node,
151 Self::make_named_feature_cross_edge(link, weak_index),
152 ));
153 }
154
155 // Finally, (`main`, `a`) to (`main`, `dep`) -- if this is a non-weak dependency
156 // and a named feature by this name is present, it also gets activated (even if
157 // the named feature has no relation to the optional dependency).
158 //
159 // For example:
160 //
161 // server = ["hyper/server"]
162 //
163 // will also activate the named feature `hyper`.
164 //
165 // One thing to be careful of here is that we don't want to insert self-edges.
166 // For example:
167 //
168 // tokio = ["dep:tokio", "tokio/net"]
169 //
170 // should not insert a self-edge from `tokio` to `tokio`. The second condition
171 // checks this.
172 if !*weak && &**dep_name != from_named_feature {
173 if let Some(same_named_feature_node) = self.make_named_feature_node(
174 &metadata,
175 from_label,
176 &metadata,
177 FeatureLabel::Named(dep_name),
178 // Don't warn if this dep isn't optional.
179 false,
180 ) {
181 nodes_edges.push((
182 same_named_feature_node,
183 Self::make_named_feature_cross_edge(link, None),
184 ));
185 }
186 }
187 }
188 }
189 NamedFeatureDep::NamedFeature(feature_name) => {
190 if let Some(same_node) = self.make_named_feature_node(
191 &metadata,
192 from_label,
193 &metadata,
194 FeatureLabel::Named(feature_name.as_ref()),
195 true,
196 ) {
197 nodes_edges.push((same_node, FeatureEdge::NamedFeature));
198 }
199 }
200 NamedFeatureDep::OptionalDependency(dep_name) => {
201 if let Some(same_node) = self.make_named_feature_node(
202 &metadata,
203 from_label,
204 &metadata,
205 FeatureLabel::OptionalDependency(dep_name.as_ref()),
206 true,
207 ) {
208 if let Some(link) = dep_name_to_link.get(dep_name.as_ref()) {
209 nodes_edges.push((
210 same_node,
211 FeatureEdge::NamedFeatureDepColon(
212 Self::make_full_conditional_link_impl(link),
213 ),
214 ));
215 }
216 }
217 }
218 };
219
220 nodes_edges
221 }
222
223 fn make_named_feature_node(
224 &mut self,
225 from_package: &PackageMetadata<'_>,
226 from_label: FeatureLabel<'_>,
227 to_package: &PackageMetadata<'_>,
228 to_label: FeatureLabel<'_>,
229 warn: bool,
230 ) -> Option<FeatureNode> {
231 match to_package.get_feature_idx(to_label) {
232 Some(idx) => Some(FeatureNode::new(to_package.package_ix(), idx)),
233 None => {
234 // It is possible to specify a feature that doesn't actually exist, and cargo will
235 // accept that if the feature isn't resolved. One example is the cfg-if crate, where
236 // version 0.1.9 has the `rustc-dep-of-std` feature commented out, and several
237 // crates try to enable that feature:
238 // https://github.com/alexcrichton/cfg-if/issues/22
239 //
240 // Since these aren't fatal errors, it seems like the best we can do is to store
241 // such issues as warnings.
242 if warn {
243 self.warnings.push(FeatureGraphWarning::MissingFeature {
244 stage: FeatureBuildStage::AddNamedFeatureEdges {
245 package_id: from_package.id().clone(),
246 from_feature: from_label.to_string(),
247 },
248 package_id: to_package.id().clone(),
249 feature_name: to_label.to_string(),
250 });
251 }
252 None
253 }
254 }
255 }
256
257 /// Creates the cross link for situations like:
258 ///
259 /// ```toml
260 /// [features]
261 /// a = ["dep/foo"]
262 /// ```
263 ///
264 /// (a link (`from`, `a`) to (`dep`, `foo`) is created.
265 ///
266 /// If `dep` is optional, the edge (`from`, `a`) to (`from`, `dep`) is also a
267 /// `NamedFeatureWithSlash` edge.
268 fn make_named_feature_cross_edge(
269 link: &PackageLink<'_>,
270 weak_index: Option<WeakIndex>,
271 ) -> FeatureEdge {
272 // This edge is enabled if the feature is enabled, which means the union of (required,
273 // optional) build conditions.
274 FeatureEdge::NamedFeatureWithSlash {
275 link: Self::make_full_conditional_link_impl(link),
276 weak_index,
277 }
278 }
279
280 // Creates a "full" conditional link, unifying requirements across all dependency lines.
281 // This should not be used in add_dependency_edges below!
282 fn make_full_conditional_link_impl(link: &PackageLink<'_>) -> ConditionalLinkImpl {
283 // This edge is enabled if the feature is enabled, which means the union of (required,
284 // optional) build conditions.
285 fn combine_req_opt(req: DependencyReq<'_>) -> PlatformStatusImpl {
286 let mut required = req.inner.required.build_if.clone();
287 required.extend(&req.inner.optional.build_if);
288 required
289 }
290
291 ConditionalLinkImpl {
292 package_edge_ix: link.edge_ix(),
293 normal: combine_req_opt(link.normal()),
294 build: combine_req_opt(link.build()),
295 dev: combine_req_opt(link.dev()),
296 }
297 }
298
299 pub(super) fn add_dependency_edges(&mut self, link: PackageLink<'_>) {
300 let from = link.from();
301
302 // Sometimes the same package is depended on separately in different sections like so:
303 //
304 // bar/Cargo.toml:
305 //
306 // [dependencies]
307 // foo = { version = "1", features = ["a"] }
308 //
309 // [build-dependencies]
310 // foo = { version = "1", features = ["b"] }
311 //
312 // Now if you have a crate 'baz' with:
313 //
314 // [dependencies]
315 // bar = { path = "../bar" }
316 //
317 // ... what features would you expect foo to be built with? You might expect it to just
318 // be built with "a", but as it turns out Cargo actually *unifies* the features, such
319 // that foo is built with both "a" and "b".
320 //
321 // Also, feature unification is impacted by whether the dependency is optional.
322 //
323 // [dependencies]
324 // foo = { version = "1", features = ["a"] }
325 //
326 // [build-dependencies]
327 // foo = { version = "1", optional = true, features = ["b"] }
328 //
329 // This will include 'foo' as a normal dependency but *not* as a build dependency by
330 // default.
331 // * Without '--features foo', the `foo` dependency will be built with "a".
332 // * With '--features foo', `foo` will be both a normal and a build dependency, with
333 // features "a" and "b" in both instances.
334 //
335 // This means that up to two separate edges have to be represented:
336 // * a 'required edge', which will be from the base node for 'from' to the feature nodes
337 // for each required feature in 'to'.
338 // * an 'optional edge', which will be from the feature node (from, dep_name) to the
339 // feature nodes for each optional feature in 'to'. This edge is only added if at least
340 // one line is optional.
341
342 let unified_metadata = iter::once((DependencyKind::Normal, link.normal()))
343 .chain(iter::once((DependencyKind::Build, link.build())))
344 .chain(iter::once((DependencyKind::Development, link.dev())));
345
346 let mut required_req = FeatureReq::new(link);
347 let mut optional_req = FeatureReq::new(link);
348 for (kind, dependency_req) in unified_metadata {
349 required_req.add_features(kind, &dependency_req.inner.required, &mut self.warnings);
350 optional_req.add_features(kind, &dependency_req.inner.optional, &mut self.warnings);
351 }
352
353 // Add the required edges (base -> features).
354 self.add_edges(
355 FeatureNode::base(from.package_ix()),
356 required_req.finish(),
357 link.from().graph(),
358 );
359
360 if !optional_req.is_empty() {
361 // This means that there is at least one instance of this dependency with optional =
362 // true. The dep name should have been added as an optional dependency node to the
363 // package metadata.
364 let from_node = FeatureNode::new(
365 from.package_ix(),
366 from.get_feature_idx(FeatureLabel::OptionalDependency(link.dep_name()))
367 .unwrap_or_else(|| {
368 panic!(
369 "while adding feature edges, for package '{}', optional dep '{}' missing",
370 from.id(),
371 link.dep_name(),
372 );
373 }),
374 );
375 self.add_edges(from_node, optional_req.finish(), link.from().graph());
376 }
377 }
378
379 fn add_node(&mut self, feature_id: FeatureNode) -> NodeIndex<FeatureIx> {
380 let feature_ix = self.graph.add_node(feature_id);
381 self.map
382 .insert(feature_id, FeatureMetadataImpl { feature_ix });
383 feature_ix
384 }
385
386 fn add_edges(
387 &mut self,
388 from_node: FeatureNode,
389 to_nodes_edges: impl IntoIterator<Item = (FeatureNode, FeatureEdge)>,
390 graph: &PackageGraph,
391 ) {
392 // The from node should always be present because it is a known node.
393 let from_ix = self.lookup_node(&from_node).unwrap_or_else(|| {
394 panic!("while adding feature edges, missing 'from': {from_node:?}");
395 });
396
397 let to_nodes_edges = to_nodes_edges.into_iter().collect::<Vec<_>>();
398
399 to_nodes_edges.into_iter().for_each(|(to_node, edge)| {
400 let to_ix = self
401 .lookup_node(&to_node)
402 .unwrap_or_else(|| panic!("while adding feature edges, missing 'to': {to_node:?}"));
403
404 if from_ix == to_ix {
405 // Only named-feature self-loops are user errors -- e.g.,
406 // `[features] a = ["a"]`. Self-loops arising from a
407 // package's dependency declarations (such as a `path`
408 // dev-dependency on the package's own crate) are
409 // legitimate Cargo constructs and must not produce a
410 // warning.
411 let is_named_feature_loop = match &edge {
412 FeatureEdge::NamedFeature
413 | FeatureEdge::NamedFeatureDepColon(_)
414 | FeatureEdge::NamedFeatureWithSlash { .. } => true,
415 FeatureEdge::DependenciesSection(_) | FeatureEdge::FeatureToBase => false,
416 };
417 if is_named_feature_loop {
418 let (package_id, feature_label) = from_node.package_id_and_feature_label(graph);
419 self.warnings.push(FeatureGraphWarning::SelfLoop {
420 package_id: package_id.clone(),
421 feature_name: feature_label.to_string(),
422 });
423 }
424 }
425
426 match self.graph.find_edge(from_ix, to_ix) {
427 Some(edge_ix) => {
428 // The edge already exists. This could be an upgrade from a cross link to a
429 // feature dependency, for example:
430 //
431 // [package]
432 // name = "main"
433 //
434 // [dependencies]
435 // dep = { ..., optional = true }
436 //
437 // [features]
438 // "feat" = ["dep/feat", "dep"]
439 //
440 // "dep/feat" causes a cross link to be created from "main/feat" to "main/dep".
441 // However, the "dep" encountered later upgrades this link to a feature
442 // dependency.
443 //
444 // This could also be an upgrade from a weak to a non-weak dependency:
445 //
446 // [features]
447 // feat = ["dep?/feat", "dep/feat2"]
448 let old_edge = self
449 .graph
450 .edge_weight_mut(edge_ix)
451 .expect("this edge was just found");
452 #[allow(clippy::single_match)]
453 match (old_edge, edge) {
454 (
455 FeatureEdge::NamedFeatureWithSlash {
456 weak_index: old_weak_index,
457 ..
458 },
459 FeatureEdge::NamedFeatureWithSlash { weak_index, .. },
460 ) => {
461 if old_weak_index.is_some() && weak_index.is_some() {
462 debug_assert_eq!(
463 *old_weak_index, weak_index,
464 "weak indexes should match if some"
465 );
466 }
467 // Upgrade this edge from weak to non-weak.
468 if weak_index.is_none() {
469 *old_weak_index = None;
470 }
471 }
472 (
473 old_edge @ FeatureEdge::NamedFeatureWithSlash { .. },
474 edge @ FeatureEdge::NamedFeature
475 | edge @ FeatureEdge::NamedFeatureDepColon(_),
476 ) => {
477 // Upgrade this edge from / conditional to dep: conditional or unconditional.
478 *old_edge = edge;
479 }
480 (
481 old_edge @ FeatureEdge::NamedFeatureDepColon(_),
482 edge @ FeatureEdge::NamedFeature,
483 ) => {
484 // Upgrade this edge from dep: conditional to unconditional.
485 // XXX: can this ever happen?
486 *old_edge = edge;
487 }
488
489 _ => {
490 // In all other cases, leave the old edge alone.
491 }
492 }
493 }
494 None => {
495 self.graph.add_edge(from_ix, to_ix, edge);
496 }
497 }
498 })
499 }
500
501 fn lookup_node(&self, node: &FeatureNode) -> Option<NodeIndex<FeatureIx>> {
502 self.map.get(node).map(|metadata| metadata.feature_ix)
503 }
504
505 pub(super) fn build(self) -> FeatureGraphImpl {
506 FeatureGraphImpl {
507 graph: self.graph,
508 base_ixs: self.base_ixs,
509 map: self.map,
510 warnings: self.warnings,
511 sccs: OnceCell::new(),
512 weak: self.weak,
513 }
514 }
515}
516
517#[derive(Debug)]
518struct FeatureReq<'g> {
519 link: PackageLink<'g>,
520 to: PackageMetadata<'g>,
521 edge_ix: EdgeIndex<PackageIx>,
522 to_default_idx: FeatureIndexInPackage,
523 // This will contain any build states that aren't empty.
524 features: AHashMap<FeatureIndexInPackage, DependencyBuildState>,
525}
526
527impl<'g> FeatureReq<'g> {
528 fn new(link: PackageLink<'g>) -> Self {
529 let to = link.to();
530 Self {
531 link,
532 to,
533 edge_ix: link.edge_ix(),
534 to_default_idx: to
535 .get_feature_idx(FeatureLabel::Named("default"))
536 .unwrap_or(FeatureIndexInPackage::Base),
537 features: AHashMap::new(),
538 }
539 }
540
541 fn is_empty(&self) -> bool {
542 // self.features only consists of non-empty build states.
543 self.features.is_empty()
544 }
545
546 fn add_features(
547 &mut self,
548 dep_kind: DependencyKind,
549 req: &DepRequiredOrOptional,
550 warnings: &mut Vec<FeatureGraphWarning>,
551 ) {
552 // Base feature.
553 self.extend(FeatureIndexInPackage::Base, dep_kind, &req.build_if);
554 // Default feature (or base if it isn't present).
555 self.extend(self.to_default_idx, dep_kind, &req.default_features_if);
556
557 for (feature, status) in &req.feature_targets {
558 match self.to.get_feature_idx(FeatureLabel::Named(feature)) {
559 Some(feature_idx) => {
560 self.extend(feature_idx, dep_kind, status);
561 }
562 None => {
563 // The destination feature is missing -- this is accepted by cargo
564 // in some circumstances, so use a warning rather than an error.
565 warnings.push(FeatureGraphWarning::MissingFeature {
566 stage: FeatureBuildStage::AddDependencyEdges {
567 package_id: self.link.from().id().clone(),
568 dep_name: self.link.dep_name().to_string(),
569 },
570 package_id: self.to.id().clone(),
571 feature_name: feature.to_string(),
572 });
573 }
574 }
575 }
576 }
577
578 fn extend(
579 &mut self,
580 feature_idx: FeatureIndexInPackage,
581 dep_kind: DependencyKind,
582 status: &PlatformStatusImpl,
583 ) {
584 let package_edge_ix = self.edge_ix;
585 if !status.is_never() {
586 self.features
587 .entry(feature_idx)
588 .or_insert_with(|| DependencyBuildState::new(package_edge_ix))
589 .extend(dep_kind, status);
590 }
591 }
592
593 fn finish(self) -> impl Iterator<Item = (FeatureNode, FeatureEdge)> + use<> {
594 let package_ix = self.to.package_ix();
595 self.features
596 .into_iter()
597 .map(move |(feature_idx, build_state)| {
598 // extend ensures that the build states aren't empty. Double-check that.
599 debug_assert!(!build_state.is_empty(), "build states are always non-empty");
600 (
601 FeatureNode::new(package_ix, feature_idx),
602 build_state.finish(),
603 )
604 })
605 }
606}
607
608#[derive(Debug)]
609struct DependencyBuildState {
610 package_edge_ix: EdgeIndex<PackageIx>,
611 normal: PlatformStatusImpl,
612 build: PlatformStatusImpl,
613 dev: PlatformStatusImpl,
614}
615
616impl DependencyBuildState {
617 fn new(package_edge_ix: EdgeIndex<PackageIx>) -> Self {
618 Self {
619 package_edge_ix,
620 normal: PlatformStatusImpl::default(),
621 build: PlatformStatusImpl::default(),
622 dev: PlatformStatusImpl::default(),
623 }
624 }
625
626 fn extend(&mut self, dep_kind: DependencyKind, status: &PlatformStatusImpl) {
627 match dep_kind {
628 DependencyKind::Normal => self.normal.extend(status),
629 DependencyKind::Build => self.build.extend(status),
630 DependencyKind::Development => self.dev.extend(status),
631 _ => panic!("unknown dependency kind"),
632 }
633 }
634
635 fn is_empty(&self) -> bool {
636 self.normal.is_never() && self.build.is_never() && self.dev.is_never()
637 }
638
639 fn finish(self) -> FeatureEdge {
640 FeatureEdge::DependenciesSection(ConditionalLinkImpl {
641 package_edge_ix: self.package_edge_ix,
642 normal: self.normal,
643 build: self.build,
644 dev: self.dev,
645 })
646 }
647}