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        feature::{
8            ConditionalLinkImpl, FeatureEdge, FeatureGraphImpl, FeatureLabel, FeatureMetadataImpl,
9            FeatureNode, WeakDependencies, WeakIndex,
10        },
11        DepRequiredOrOptional, DependencyReq, FeatureIndexInPackage, FeatureIx, NamedFeatureDep,
12        PackageGraph, PackageIx, PackageLink, PackageMetadata,
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!(
395                "while adding feature edges, missing 'from': {:?}",
396                from_node
397            );
398        });
399
400        let to_nodes_edges = to_nodes_edges.into_iter().collect::<Vec<_>>();
401
402        to_nodes_edges.into_iter().for_each(|(to_node, edge)| {
403            let to_ix = self.lookup_node(&to_node).unwrap_or_else(|| {
404                panic!("while adding feature edges, missing 'to': {:?}", to_node)
405            });
406
407            if from_ix == to_ix {
408                let (package_id, feature_label) = from_node.package_id_and_feature_label(graph);
409                self.warnings.push(FeatureGraphWarning::SelfLoop {
410                    package_id: package_id.clone(),
411                    feature_name: feature_label.to_string(),
412                });
413            }
414
415            match self.graph.find_edge(from_ix, to_ix) {
416                Some(edge_ix) => {
417                    // The edge already exists. This could be an upgrade from a cross link to a
418                    // feature dependency, for example:
419                    //
420                    // [package]
421                    // name = "main"
422                    //
423                    // [dependencies]
424                    // dep = { ..., optional = true }
425                    //
426                    // [features]
427                    // "feat" = ["dep/feat", "dep"]
428                    //
429                    // "dep/feat" causes a cross link to be created from "main/feat" to "main/dep".
430                    // However, the "dep" encountered later upgrades this link to a feature
431                    // dependency.
432                    //
433                    // This could also be an upgrade from a weak to a non-weak dependency:
434                    //
435                    // [features]
436                    // feat = ["dep?/feat", "dep/feat2"]
437                    let old_edge = self
438                        .graph
439                        .edge_weight_mut(edge_ix)
440                        .expect("this edge was just found");
441                    #[allow(clippy::single_match)]
442                    match (old_edge, edge) {
443                        (
444                            FeatureEdge::NamedFeatureWithSlash {
445                                weak_index: old_weak_index,
446                                ..
447                            },
448                            FeatureEdge::NamedFeatureWithSlash { weak_index, .. },
449                        ) => {
450                            if old_weak_index.is_some() && weak_index.is_some() {
451                                debug_assert_eq!(
452                                    *old_weak_index, weak_index,
453                                    "weak indexes should match if some"
454                                );
455                            }
456                            // Upgrade this edge from weak to non-weak.
457                            if weak_index.is_none() {
458                                *old_weak_index = None;
459                            }
460                        }
461                        (
462                            old_edge @ FeatureEdge::NamedFeatureWithSlash { .. },
463                            edge @ FeatureEdge::NamedFeature
464                            | edge @ FeatureEdge::NamedFeatureDepColon(_),
465                        ) => {
466                            // Upgrade this edge from / conditional to dep: conditional or unconditional.
467                            *old_edge = edge;
468                        }
469                        (
470                            old_edge @ FeatureEdge::NamedFeatureDepColon(_),
471                            edge @ FeatureEdge::NamedFeature,
472                        ) => {
473                            // Upgrade this edge from dep: conditional to unconditional.
474                            // XXX: can this ever happen?
475                            *old_edge = edge;
476                        }
477
478                        _ => {
479                            // In all other cases, leave the old edge alone.
480                        }
481                    }
482                }
483                None => {
484                    self.graph.add_edge(from_ix, to_ix, edge);
485                }
486            }
487        })
488    }
489
490    fn lookup_node(&self, node: &FeatureNode) -> Option<NodeIndex<FeatureIx>> {
491        self.map.get(node).map(|metadata| metadata.feature_ix)
492    }
493
494    pub(super) fn build(self) -> FeatureGraphImpl {
495        FeatureGraphImpl {
496            graph: self.graph,
497            base_ixs: self.base_ixs,
498            map: self.map,
499            warnings: self.warnings,
500            sccs: OnceCell::new(),
501            weak: self.weak,
502        }
503    }
504}
505
506#[derive(Debug)]
507struct FeatureReq<'g> {
508    link: PackageLink<'g>,
509    to: PackageMetadata<'g>,
510    edge_ix: EdgeIndex<PackageIx>,
511    to_default_idx: FeatureIndexInPackage,
512    // This will contain any build states that aren't empty.
513    features: AHashMap<FeatureIndexInPackage, DependencyBuildState>,
514}
515
516impl<'g> FeatureReq<'g> {
517    fn new(link: PackageLink<'g>) -> Self {
518        let to = link.to();
519        Self {
520            link,
521            to,
522            edge_ix: link.edge_ix(),
523            to_default_idx: to
524                .get_feature_idx(FeatureLabel::Named("default"))
525                .unwrap_or(FeatureIndexInPackage::Base),
526            features: AHashMap::new(),
527        }
528    }
529
530    fn is_empty(&self) -> bool {
531        // self.features only consists of non-empty build states.
532        self.features.is_empty()
533    }
534
535    fn add_features(
536        &mut self,
537        dep_kind: DependencyKind,
538        req: &DepRequiredOrOptional,
539        warnings: &mut Vec<FeatureGraphWarning>,
540    ) {
541        // Base feature.
542        self.extend(FeatureIndexInPackage::Base, dep_kind, &req.build_if);
543        // Default feature (or base if it isn't present).
544        self.extend(self.to_default_idx, dep_kind, &req.default_features_if);
545
546        for (feature, status) in &req.feature_targets {
547            match self.to.get_feature_idx(FeatureLabel::Named(feature)) {
548                Some(feature_idx) => {
549                    self.extend(feature_idx, dep_kind, status);
550                }
551                None => {
552                    // The destination feature is missing -- this is accepted by cargo
553                    // in some circumstances, so use a warning rather than an error.
554                    warnings.push(FeatureGraphWarning::MissingFeature {
555                        stage: FeatureBuildStage::AddDependencyEdges {
556                            package_id: self.link.from().id().clone(),
557                            dep_name: self.link.dep_name().to_string(),
558                        },
559                        package_id: self.to.id().clone(),
560                        feature_name: feature.to_string(),
561                    });
562                }
563            }
564        }
565    }
566
567    fn extend(
568        &mut self,
569        feature_idx: FeatureIndexInPackage,
570        dep_kind: DependencyKind,
571        status: &PlatformStatusImpl,
572    ) {
573        let package_edge_ix = self.edge_ix;
574        if !status.is_never() {
575            self.features
576                .entry(feature_idx)
577                .or_insert_with(|| DependencyBuildState::new(package_edge_ix))
578                .extend(dep_kind, status);
579        }
580    }
581
582    fn finish(self) -> impl Iterator<Item = (FeatureNode, FeatureEdge)> {
583        let package_ix = self.to.package_ix();
584        self.features
585            .into_iter()
586            .map(move |(feature_idx, build_state)| {
587                // extend ensures that the build states aren't empty. Double-check that.
588                debug_assert!(!build_state.is_empty(), "build states are always non-empty");
589                (
590                    FeatureNode::new(package_ix, feature_idx),
591                    build_state.finish(),
592                )
593            })
594    }
595}
596
597#[derive(Debug)]
598struct DependencyBuildState {
599    package_edge_ix: EdgeIndex<PackageIx>,
600    normal: PlatformStatusImpl,
601    build: PlatformStatusImpl,
602    dev: PlatformStatusImpl,
603}
604
605impl DependencyBuildState {
606    fn new(package_edge_ix: EdgeIndex<PackageIx>) -> Self {
607        Self {
608            package_edge_ix,
609            normal: PlatformStatusImpl::default(),
610            build: PlatformStatusImpl::default(),
611            dev: PlatformStatusImpl::default(),
612        }
613    }
614
615    fn extend(&mut self, dep_kind: DependencyKind, status: &PlatformStatusImpl) {
616        match dep_kind {
617            DependencyKind::Normal => self.normal.extend(status),
618            DependencyKind::Build => self.build.extend(status),
619            DependencyKind::Development => self.dev.extend(status),
620            _ => panic!("unknown dependency kind"),
621        }
622    }
623
624    fn is_empty(&self) -> bool {
625        self.normal.is_never() && self.build.is_never() && self.dev.is_never()
626    }
627
628    fn finish(self) -> FeatureEdge {
629        FeatureEdge::DependenciesSection(ConditionalLinkImpl {
630            package_edge_ix: self.package_edge_ix,
631            normal: self.normal,
632            build: self.build,
633            dev: self.dev,
634        })
635    }
636}