guppy/graph/cargo/
build.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    DependencyKind, Error,
6    graph::{
7        DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageSet,
8        cargo::{
9            CargoIntermediateSet, CargoOptions, CargoResolverVersion, CargoSet, InitialsPlatform,
10        },
11        feature::{ConditionalLink, FeatureLabel, FeatureQuery, FeatureSet, StandardFeatures},
12    },
13    platform::{EnabledTernary, PlatformSpec},
14    sorted_set::SortedSet,
15};
16use fixedbitset::FixedBitSet;
17use petgraph::{prelude::*, visit::VisitMap};
18
19pub(super) struct CargoSetBuildState<'a> {
20    opts: &'a CargoOptions<'a>,
21    omitted_packages: SortedSet<NodeIndex<PackageIx>>,
22}
23
24impl<'a> CargoSetBuildState<'a> {
25    pub(super) fn new<'g>(
26        graph: &'g PackageGraph,
27        opts: &'a CargoOptions<'a>,
28    ) -> Result<Self, Error> {
29        let omitted_packages: SortedSet<_> =
30            graph.package_ixs(opts.omitted_packages.iter().copied())?;
31
32        Ok(Self {
33            opts,
34            omitted_packages,
35        })
36    }
37
38    pub(super) fn build<'g>(
39        self,
40        initials: FeatureSet<'g>,
41        features_only: FeatureSet<'g>,
42    ) -> CargoSet<'g> {
43        match self.opts.resolver {
44            CargoResolverVersion::V1 => self.new_v1(initials, features_only, false),
45            CargoResolverVersion::V1Install => {
46                let avoid_dev_deps = !self.opts.include_dev;
47                self.new_v1(initials, features_only, avoid_dev_deps)
48            }
49            // V2 and V3 do the same feature resolution.
50            CargoResolverVersion::V2 | CargoResolverVersion::V3 => {
51                self.new_v2(initials, features_only)
52            }
53        }
54    }
55
56    pub(super) fn build_intermediate(self, query: FeatureQuery) -> CargoIntermediateSet {
57        match self.opts.resolver {
58            CargoResolverVersion::V1 => self.new_v1_intermediate(query, false),
59            CargoResolverVersion::V1Install => {
60                let avoid_dev_deps = !self.opts.include_dev;
61                self.new_v1_intermediate(query, avoid_dev_deps)
62            }
63            CargoResolverVersion::V2 | CargoResolverVersion::V3 => self.new_v2_intermediate(query),
64        }
65    }
66
67    fn new_v1<'g>(
68        self,
69        initials: FeatureSet<'g>,
70        features_only: FeatureSet<'g>,
71        avoid_dev_deps: bool,
72    ) -> CargoSet<'g> {
73        self.build_set(initials, features_only, |query| {
74            self.new_v1_intermediate(query, avoid_dev_deps)
75        })
76    }
77
78    fn new_v2<'g>(self, initials: FeatureSet<'g>, features_only: FeatureSet<'g>) -> CargoSet<'g> {
79        self.build_set(initials, features_only, |query| {
80            self.new_v2_intermediate(query)
81        })
82    }
83
84    // ---
85    // Helper methods
86    // ---
87
88    fn is_omitted(&self, package_ix: NodeIndex<PackageIx>) -> bool {
89        self.omitted_packages.contains(&package_ix)
90    }
91
92    fn build_set<'g>(
93        &self,
94        initials: FeatureSet<'g>,
95        features_only: FeatureSet<'g>,
96        intermediate_fn: impl FnOnce(FeatureQuery<'g>) -> CargoIntermediateSet<'g>,
97    ) -> CargoSet<'g> {
98        // Prepare a package query for step 2.
99        let graph = *initials.graph();
100        // Note that currently, proc macros specified in initials are built on both the target and
101        // the host.
102        let mut host_ixs = Vec::new();
103        let target_ixs: Vec<_> = initials
104            .ixs_unordered()
105            .filter_map(|feature_ix| {
106                let metadata = graph.metadata_for_ix(feature_ix);
107                let package_ix = metadata.package_ix();
108                match self.opts.initials_platform {
109                    InitialsPlatform::Host => {
110                        // Always build on the host.
111                        host_ixs.push(package_ix);
112                        None
113                    }
114                    InitialsPlatform::Standard => {
115                        // Proc macros on the host platform, everything else on the target platform.
116                        if metadata.package().is_proc_macro() {
117                            host_ixs.push(package_ix);
118                            None
119                        } else {
120                            Some(package_ix)
121                        }
122                    }
123                    InitialsPlatform::ProcMacrosOnTarget => {
124                        // Proc macros on both the host and the target platforms, everything else
125                        // on the target platform.
126                        if metadata.package().is_proc_macro() {
127                            host_ixs.push(package_ix);
128                        }
129                        Some(package_ix)
130                    }
131                }
132            })
133            .collect();
134        let target_query = graph
135            .package_graph
136            .query_from_parts(SortedSet::new(target_ixs), DependencyDirection::Forward);
137
138        // 1. Build the intermediate set containing the features for any possible package that can
139        // be built, including features-only packages.
140        let initials_plus_features_only = initials.union(&features_only);
141        let intermediate_set = intermediate_fn(
142            initials_plus_features_only.to_feature_query(DependencyDirection::Forward),
143        );
144        let (target_set, host_set) = intermediate_set.target_host_sets();
145
146        // While doing traversal 2 below, record any packages discovered along build edges for use
147        // in host ixs, to prepare for step 3. This will also include proc-macros.
148
149        // This list will contain proc-macro edges out of target packages.
150        let mut proc_macro_edge_ixs = Vec::new();
151        // This list will contain build dep edges out of target packages.
152        let mut build_dep_edge_ixs = Vec::new();
153
154        let is_enabled = |feature_set: &FeatureSet<'_>,
155                          link: &PackageLink<'_>,
156                          kind: DependencyKind,
157                          platform_spec: &PlatformSpec| {
158            let (from, to) = link.endpoints();
159            let req_status = link.req_for_kind(kind).status();
160            // Check the complete set to figure out whether we look at required_on or
161            // enabled_on.
162            let consider_optional = feature_set
163                .contains((from.id(), FeatureLabel::OptionalDependency(link.dep_name())))
164                .unwrap_or_else(|_| {
165                    // If the feature ID isn't present, it means the dependency wasn't declared
166                    // as optional. In that case the value doesn't matter.
167                    debug_assert!(
168                        req_status.optional_status().is_never(),
169                        "for {} -> {}, dep '{}' not declared as optional",
170                        from.name(),
171                        to.name(),
172                        link.dep_name()
173                    );
174                    false
175                });
176
177            if consider_optional {
178                req_status.enabled_on(platform_spec) != EnabledTernary::Disabled
179            } else {
180                req_status.required_on(platform_spec) != EnabledTernary::Disabled
181            }
182        };
183
184        // Record workspace + direct third-party deps in these sets.
185        let mut target_direct_deps =
186            FixedBitSet::with_capacity(graph.package_graph.package_count());
187        let mut host_direct_deps = FixedBitSet::with_capacity(graph.package_graph.package_count());
188
189        // 2. Figure out what packages will be included on the target platform, i.e. normal + dev
190        // (if requested).
191        let target_platform = &self.opts.target_platform;
192        let host_platform = &self.opts.host_platform;
193
194        let target_packages = target_query.resolve_with_fn(|query, link| {
195            let (from, to) = link.endpoints();
196
197            if from.in_workspace() {
198                // Mark initials in target_direct_deps.
199                target_direct_deps.visit(from.package_ix());
200            }
201
202            if self.is_omitted(to.package_ix()) {
203                // Pretend that the omitted set doesn't exist.
204                return false;
205            }
206
207            // Dev-dependencies are only considered if `from` is an initial.
208            let consider_dev =
209                self.opts.include_dev && query.starts_from(from.id()).expect("valid ID");
210            // Build dependencies are only considered if there's a build script.
211            let consider_build = from.has_build_script();
212
213            let mut follow_target =
214                is_enabled(target_set, &link, DependencyKind::Normal, target_platform)
215                    || (consider_dev
216                        && is_enabled(
217                            target_set,
218                            &link,
219                            DependencyKind::Development,
220                            target_platform,
221                        ));
222
223            // Proc macros build on the host, so for normal/dev dependencies redirect it to the host
224            // instead.
225            let proc_macro_redirect = follow_target && to.is_proc_macro();
226
227            // Build dependencies are evaluated against the host platform.
228            let build_dep_redirect = consider_build
229                && is_enabled(target_set, &link, DependencyKind::Build, host_platform);
230
231            // Finally, process what needs to be done.
232            if build_dep_redirect || proc_macro_redirect {
233                if from.in_workspace() {
234                    // The 'to' node is either in the workspace or a direct dependency [a].
235                    host_direct_deps.visit(to.package_ix());
236                }
237                host_ixs.push(to.package_ix());
238            }
239            if build_dep_redirect {
240                build_dep_edge_ixs.push(link.edge_ix());
241            }
242            if proc_macro_redirect {
243                proc_macro_edge_ixs.push(link.edge_ix());
244                follow_target = false;
245            }
246
247            if from.in_workspace() && follow_target {
248                // The 'to' node is either in the workspace or a direct dependency.
249                target_direct_deps.visit(to.package_ix());
250            }
251
252            follow_target
253        });
254
255        // 3. Figure out what packages will be included on the host platform.
256        let host_ixs = SortedSet::new(host_ixs);
257        let host_packages = graph
258            .package_graph
259            .query_from_parts(host_ixs, DependencyDirection::Forward)
260            .resolve_with_fn(|query, link| {
261                let (from, to) = link.endpoints();
262                if self.is_omitted(to.package_ix()) {
263                    // Pretend that the omitted set doesn't exist.
264                    return false;
265                }
266
267                // All relevant nodes in host_ixs have already been added to host_direct_deps at [a].
268
269                // Dev-dependencies are only considered if `from` is an initial.
270                let consider_dev =
271                    self.opts.include_dev && query.starts_from(from.id()).expect("valid ID");
272                let consider_build = from.has_build_script();
273
274                // Only normal and build dependencies are typically considered. Dev-dependencies of
275                // initials are also considered.
276                let res = is_enabled(host_set, &link, DependencyKind::Normal, host_platform)
277                    || (consider_build
278                        && is_enabled(host_set, &link, DependencyKind::Build, host_platform))
279                    || (consider_dev
280                        && is_enabled(host_set, &link, DependencyKind::Development, host_platform));
281
282                if res {
283                    if from.in_workspace() {
284                        // The 'to' node is either in the workspace or a direct dependency.
285                        host_direct_deps.visit(to.package_ix());
286                    }
287                    true
288                } else {
289                    false
290                }
291            });
292
293        // Finally, the features are whatever packages were selected, intersected with whatever
294        // features were selected.
295        let target_features = target_packages
296            .to_feature_set(StandardFeatures::All)
297            .intersection(target_set);
298        let host_features = host_packages
299            .to_feature_set(StandardFeatures::All)
300            .intersection(host_set);
301
302        // Also construct the direct dep sets.
303        let target_direct_deps =
304            PackageSet::from_included(graph.package_graph(), target_direct_deps);
305        let host_direct_deps = PackageSet::from_included(graph.package_graph, host_direct_deps);
306
307        CargoSet {
308            initials,
309            features_only,
310            target_features,
311            host_features,
312            target_direct_deps,
313            host_direct_deps,
314            proc_macro_edge_ixs: SortedSet::new(proc_macro_edge_ixs),
315            build_dep_edge_ixs: SortedSet::new(build_dep_edge_ixs),
316        }
317    }
318
319    fn new_v1_intermediate<'g>(
320        &self,
321        query: FeatureQuery<'g>,
322        avoid_dev_deps: bool,
323    ) -> CargoIntermediateSet<'g> {
324        // Perform a "complete" feature query. This will provide more packages than will be
325        // included in the final build, but for each package it will have the correct feature set.
326        let complete_set = query.resolve_with_fn(|query, link| {
327            if self.is_omitted(link.to().package_ix()) {
328                // Pretend that the omitted set doesn't exist.
329                false
330            } else if !avoid_dev_deps
331                && query
332                    .starts_from(link.from().feature_id())
333                    .expect("valid ID")
334            {
335                // Follow everything for initials.
336                true
337            } else {
338                // Follow normal and build edges for everything else.
339                !link.dev_only()
340            }
341        });
342
343        CargoIntermediateSet::Unified(complete_set)
344    }
345
346    fn new_v2_intermediate<'g>(&self, query: FeatureQuery<'g>) -> CargoIntermediateSet<'g> {
347        let graph = *query.graph();
348        // Note that proc macros specified in initials take part in feature resolution
349        // for both target and host ixs. If they didn't, then the query would be partitioned into
350        // host and target ixs instead.
351        // https://github.com/rust-lang/cargo/issues/8312
352        let mut host_ixs: Vec<_> = query
353            .params
354            .initials()
355            .iter()
356            .filter_map(|feature_ix| {
357                let metadata = graph.metadata_for_ix(*feature_ix);
358                if self.opts.initials_platform == InitialsPlatform::Host
359                    || metadata.package().is_proc_macro()
360                {
361                    // Proc macros are always unified on the host.
362                    Some(metadata.feature_ix())
363                } else {
364                    // Everything else is built on the target.
365                    None
366                }
367            })
368            .collect();
369
370        let is_enabled =
371            |link: &ConditionalLink<'_>, kind: DependencyKind, platform_spec: &PlatformSpec| {
372                let platform_status = link.status_for_kind(kind);
373                platform_status.enabled_on(platform_spec) != EnabledTernary::Disabled
374            };
375
376        let target_query = if self.opts.initials_platform == InitialsPlatform::Host {
377            // Empty query on the target.
378            graph.query_from_parts(SortedSet::new(vec![]), DependencyDirection::Forward)
379        } else {
380            query
381        };
382
383        // Keep a copy of the target query for use in step 2.
384        let target_query_2 = target_query.clone();
385
386        // 1. Perform a feature query for the target.
387        let target_platform = &self.opts.target_platform;
388        let host_platform = &self.opts.host_platform;
389        let target = target_query.resolve_with_fn(|query, link| {
390            let (from, to) = link.endpoints();
391
392            if self.is_omitted(to.package_ix()) {
393                // Pretend that the omitted set doesn't exist.
394                return false;
395            }
396
397            let consider_dev =
398                self.opts.include_dev && query.starts_from(from.feature_id()).expect("valid ID");
399            // This resolver doesn't check for whether this package has a build script.
400            let mut follow_target = is_enabled(&link, DependencyKind::Normal, target_platform)
401                || (consider_dev
402                    && is_enabled(&link, DependencyKind::Development, target_platform));
403
404            // Proc macros build on the host, so for normal/dev dependencies redirect it to the host
405            // instead.
406            let proc_macro_redirect = follow_target && to.package().is_proc_macro();
407
408            // Build dependencies are evaluated against the host platform.
409            let build_dep_redirect = {
410                // If this is a dependency like:
411                //
412                // ```
413                // [build-dependencies]
414                // cc = { version = "1.0", optional = true }
415                //
416                // [features]
417                // bundled = ["cc"]
418                // ```
419                //
420                // Then, there is an implicit named feature here called "cc" on the target platform,
421                // which enables the optional dependency "cc". But this does not mean that this
422                // package itself is built on the host platform!
423                //
424                // Detect this situation by ensuring that the package ID of the `from` and `to`
425                // nodes are different.
426                from.package_id() != to.package_id()
427                    && is_enabled(&link, DependencyKind::Build, host_platform)
428            };
429
430            // Finally, process what needs to be done.
431            if build_dep_redirect || proc_macro_redirect {
432                host_ixs.push(to.feature_ix());
433            }
434            if proc_macro_redirect {
435                follow_target = false;
436            }
437
438            follow_target
439        });
440
441        // 2. Perform a feature query for the host.
442        let host = graph
443            .query_from_parts(SortedSet::new(host_ixs), DependencyDirection::Forward)
444            .resolve_with_fn(|_, link| {
445                let (from, to) = link.endpoints();
446                if self.is_omitted(to.package_ix()) {
447                    // Pretend that the omitted set doesn't exist.
448                    return false;
449                }
450                // During feature resolution, the v2 resolver doesn't check for whether this package
451                // has a build script. It also unifies dev dependencies of initials, even on the
452                // host platform.
453                let consider_dev = self.opts.include_dev
454                    && target_query_2
455                        .starts_from(from.feature_id())
456                        .expect("valid ID");
457
458                is_enabled(&link, DependencyKind::Normal, host_platform)
459                    || is_enabled(&link, DependencyKind::Build, host_platform)
460                    || (consider_dev
461                        && is_enabled(&link, DependencyKind::Development, host_platform))
462            });
463
464        CargoIntermediateSet::TargetHost { target, host }
465    }
466}