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