Skip to main content

hakari/
hakari.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    CargoTomlError, HakariCargoToml, TomlOutError,
6    explain::HakariExplain,
7    registry::Registry,
8    toml_name_map,
9    toml_out::{HakariOutputOptions, TomlNameEntry, write_toml},
10};
11use debug_ignore::DebugIgnore;
12use guppy::{
13    PackageId,
14    errors::TargetSpecError,
15    graph::{
16        DependencyDirection, PackageGraph, PackageMetadata,
17        cargo::{BuildPlatform, CargoOptions, CargoResolverVersion, CargoSet, InitialsPlatform},
18        feature::{FeatureId, FeatureLabel, FeatureSet, StandardFeatures, named_feature_filter},
19    },
20    platform::{Platform, PlatformSpec, TargetFeatures},
21};
22use iddqd::{BiHashMap, IdOrdMap};
23use rayon::prelude::*;
24use std::{
25    borrow::Cow,
26    collections::{BTreeMap, BTreeSet, HashSet},
27    fmt,
28    sync::Arc,
29};
30
31/// Configures and constructs [`Hakari`](Hakari) instances.
32///
33/// This struct provides a number of options that determine how `Hakari` instances are generated.
34#[derive(Clone, Debug)]
35pub struct HakariBuilder<'g> {
36    graph: DebugIgnore<&'g PackageGraph>,
37    hakari_package: Option<PackageMetadata<'g>>,
38    pub(crate) platforms: Vec<Arc<Platform>>,
39    resolver: CargoResolverVersion,
40    pub(crate) verify_mode: bool,
41    pub(crate) traversal_excludes: HashSet<&'g PackageId>,
42    final_excludes: HashSet<&'g PackageId>,
43    pub(crate) registries: BiHashMap<Registry, ahash::RandomState>,
44    unify_target_host: UnifyTargetHost,
45    output_single_feature: bool,
46    pub(crate) dep_format_version: DepFormatVersion,
47    pub(crate) workspace_hack_line_style: WorkspaceHackLineStyle,
48}
49
50impl<'g> HakariBuilder<'g> {
51    /// Creates a new `HakariBuilder` instance from a `PackageGraph`.
52    ///
53    /// The Hakari package itself is usually present in the workspace. If so, specify its
54    /// package ID, otherwise pass in `None`.
55    ///
56    /// Returns an error if a Hakari package ID is specified but it isn't known to the graph, or
57    /// isn't in the workspace.
58    pub fn new(
59        graph: &'g PackageGraph,
60        hakari_id: Option<&PackageId>,
61    ) -> Result<Self, guppy::Error> {
62        let hakari_package = hakari_id
63            .map(|package_id| {
64                let package = graph.metadata(package_id)?;
65                if !package.in_workspace() {
66                    return Err(guppy::Error::UnknownWorkspaceName(
67                        package.name().to_string(),
68                    ));
69                }
70                Ok(package)
71            })
72            .transpose()?;
73
74        Ok(Self {
75            graph: DebugIgnore(graph),
76            hakari_package,
77            platforms: vec![],
78            resolver: CargoResolverVersion::V2,
79            verify_mode: false,
80            traversal_excludes: HashSet::new(),
81            final_excludes: HashSet::new(),
82            registries: BiHashMap::default(),
83            unify_target_host: UnifyTargetHost::default(),
84            output_single_feature: false,
85            dep_format_version: DepFormatVersion::default(),
86            workspace_hack_line_style: WorkspaceHackLineStyle::default(),
87        })
88    }
89
90    /// Returns the `PackageGraph` used to construct this `Hakari` instance.
91    pub fn graph(&self) -> &'g PackageGraph {
92        // This is a spurious clippy lint on Rust 1.65.0
93        #[allow(clippy::explicit_auto_deref)]
94        *self.graph
95    }
96
97    /// Returns the Hakari package, or `None` if it wasn't passed into [`new`](Self::new).
98    pub fn hakari_package(&self) -> Option<&PackageMetadata<'g>> {
99        self.hakari_package.as_ref()
100    }
101
102    /// Reads the existing TOML file for the Hakari package from disk, returning a
103    /// `HakariCargoToml`.
104    ///
105    /// This can be used with [`Hakari::to_toml_string`](Hakari::to_toml_string) to manage the
106    /// contents of the Hakari package's TOML file on disk.
107    ///
108    /// Returns an error if there was an issue reading the TOML file from disk, or `None` if
109    /// this builder was created without a Hakari package.
110    pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
111        let hakari_package = self.hakari_package()?;
112        let workspace_path = hakari_package
113            .source()
114            .workspace_path()
115            .expect("hakari_package is in workspace");
116        Some(HakariCargoToml::new_relative(
117            self.graph.workspace().root(),
118            workspace_path,
119        ))
120    }
121
122    /// Sets a list of platforms for `hakari` to use.
123    ///
124    /// By default, `hakari` unifies features that are always enabled across all platforms. If
125    /// builds are commonly performed on a few platforms, `hakari` can output platform-specific
126    /// instructions for those builds.
127    ///
128    /// This currently supports target triples only, without further customization around
129    /// target features or flags. In the future, this may support `cfg()` expressions using
130    /// an [SMT solver](https://en.wikipedia.org/wiki/Satisfiability_modulo_theories).
131    ///
132    /// Call `set_platforms` with an empty list to reset to default behavior.
133    ///
134    /// Returns an error if a platform wasn't known to [`target_spec`], the library `hakari` uses
135    /// to resolve platforms.
136    pub fn set_platforms(
137        &mut self,
138        platforms: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
139    ) -> Result<&mut Self, TargetSpecError> {
140        self.platforms = platforms
141            .into_iter()
142            .map(|s| Ok(Arc::new(Platform::new(s.into(), TargetFeatures::Unknown)?)))
143            .collect::<Result<Vec<_>, _>>()?;
144        Ok(self)
145    }
146
147    /// Returns the platforms set through `set_platforms`, or an empty list if no platforms are
148    /// set.
149    pub fn platforms(&self) -> impl ExactSizeIterator<Item = &str> + '_ {
150        self.platforms.iter().map(|platform| platform.triple_str())
151    }
152
153    /// Sets the Cargo resolver version.
154    ///
155    /// By default, `HakariBuilder` uses [version 2](CargoResolverVersion::V2) of the Cargo
156    /// resolver. For more about Cargo resolvers, see the documentation for
157    /// [`CargoResolverVersion`](CargoResolverVersion).
158    pub fn set_resolver(&mut self, resolver: CargoResolverVersion) -> &mut Self {
159        self.resolver = resolver;
160        self
161    }
162
163    /// Returns the current Cargo resolver version.
164    pub fn resolver(&self) -> CargoResolverVersion {
165        self.resolver
166    }
167
168    /// Pretends that the provided packages don't exist during graph traversals.
169    ///
170    /// Users may wish to not consider certain packages while figuring out the unified feature set.
171    /// Setting this option prevents those packages from being considered.
172    ///
173    /// Practically, this means that:
174    /// * If a workspace package is specified, Cargo build simulations for it will not be run.
175    /// * If a third-party package is specified, it will not be present in the output, nor will
176    ///   any transitive dependencies or features enabled by it that aren't enabled any other way.
177    ///   In other words, any packages excluded during traversal are also [excluded from the final
178    ///   output](Self::add_final_excludes).
179    ///
180    /// Returns an error if any package IDs specified aren't known to the graph.
181    pub fn add_traversal_excludes<'b>(
182        &mut self,
183        excludes: impl IntoIterator<Item = &'b PackageId>,
184    ) -> Result<&mut Self, guppy::Error> {
185        let traversal_exclude: Vec<&'g PackageId> = excludes
186            .into_iter()
187            .map(|package_id| Ok(self.graph.metadata(package_id)?.id()))
188            .collect::<Result<_, _>>()?;
189        self.traversal_excludes.extend(traversal_exclude);
190        Ok(self)
191    }
192
193    /// Returns the packages currently excluded during graph traversals.
194    ///
195    /// Also returns the Hakari package if specified. This is because the Hakari package is treated
196    /// as excluded while performing unification.
197    pub fn traversal_excludes<'b>(&'b self) -> impl Iterator<Item = &'g PackageId> + 'b {
198        let excludes = self.make_traversal_excludes();
199        excludes.iter()
200    }
201
202    /// Returns true if a package ID is currently excluded during traversal.
203    ///
204    /// Also returns true for the Hakari package if specified. This is because the Hakari package is
205    /// treated as excluded by the algorithm.
206    ///
207    /// Returns an error if this package ID isn't known to the underlying graph.
208    pub fn is_traversal_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
209        self.graph.metadata(package_id)?;
210
211        let excludes = self.make_traversal_excludes();
212        Ok(excludes.is_excluded(package_id))
213    }
214
215    /// Adds packages to be removed from the final output.
216    ///
217    /// Unlike [`traversal_excludes`](Self::traversal_excludes), these packages are considered
218    /// during traversals, but removed at the end.
219    ///
220    /// Returns an error if any package IDs specified aren't known to the graph.
221    pub fn add_final_excludes<'b>(
222        &mut self,
223        excludes: impl IntoIterator<Item = &'b PackageId>,
224    ) -> Result<&mut Self, guppy::Error> {
225        let final_excludes: Vec<&'g PackageId> = excludes
226            .into_iter()
227            .map(|package_id| Ok(self.graph.metadata(package_id)?.id()))
228            .collect::<Result<_, _>>()?;
229        self.final_excludes.extend(final_excludes);
230        Ok(self)
231    }
232
233    /// Returns the packages to be removed from the final output.
234    pub fn final_excludes<'b>(&'b self) -> impl Iterator<Item = &'g PackageId> + 'b {
235        self.final_excludes.iter().copied()
236    }
237
238    /// Returns true if a package ID is currently excluded from the final output.
239    ///
240    /// Returns an error if this package ID isn't known to the underlying graph.
241    pub fn is_final_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
242        self.graph.metadata(package_id)?;
243        Ok(self.final_excludes.contains(package_id))
244    }
245
246    /// Returns true if a package ID is excluded from either the traversal or the final output.
247    ///
248    /// Also returns true for the Hakari package if specified. This is because the Hakari package is
249    /// treated as excluded by the algorithm.
250    ///
251    /// This does not cover [structural excludes](Hakari::structural_excludes).
252    ///
253    /// Returns an error if this package ID isn't known to the underlying graph.
254    #[inline]
255    pub fn is_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
256        Ok(self.is_traversal_excluded(package_id)? || self.is_final_excluded(package_id)?)
257    }
258
259    /// Returns true if `package` is a workspace member that hakari manages,
260    /// i.e. one that should depend on the hakari package.
261    ///
262    /// This consists of workspace packages that satisfy all of the following
263    /// criteria:
264    ///
265    /// * Not the hakari package itself.
266    /// * Not part of traversal excludes.
267    /// * Not part of final excludes.
268    ///
269    /// Returns false if no hakari package was specified, since nothing is
270    /// managed in that case.
271    pub(crate) fn is_managed_member(&self, package: &PackageMetadata<'g>) -> bool {
272        debug_assert!(
273            std::ptr::eq(package.graph(), *self.graph),
274            "package is from the same graph as this builder"
275        );
276        let Some(hakari_package) = self.hakari_package else {
277            return false;
278        };
279        // In verify mode, make_traversal_excludes leaves the hakari package in,
280        // so is_excluded alone is not enough here -- we have to check
281        // explicitly for the hakari package ID.
282        let is_hakari_package = hakari_package.id() == package.id();
283        package.in_workspace()
284            && !is_hakari_package
285            && !self
286                .is_excluded(package.id())
287                .expect("package is from the same graph as this builder")
288    }
289
290    /// Add alternate registries by (name, URL) pairs.
291    ///
292    /// This is a temporary workaround until [Cargo issue #9052](https://github.com/rust-lang/cargo/issues/9052)
293    /// is resolved.
294    pub fn add_registries(
295        &mut self,
296        registries: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
297    ) -> &mut Self {
298        self.registries
299            .extend(registries.into_iter().map(|(name, url)| Registry {
300                name: name.into(),
301                url: url.into(),
302            }));
303        self
304    }
305
306    /// Whether and how to unify feature sets across target and host platforms.
307    ///
308    /// This is an advanced feature that most users don't need to set. For more information about
309    /// this option, see the documentation for [`UnifyTargetHost`](UnifyTargetHost).
310    pub fn set_unify_target_host(&mut self, unify_target_host: UnifyTargetHost) -> &mut Self {
311        self.unify_target_host = unify_target_host;
312        self
313    }
314
315    /// Returns the current value of `unify_target_host`.
316    pub fn unify_target_host(&self) -> UnifyTargetHost {
317        self.unify_target_host
318    }
319
320    /// Whether to unify feature sets for all dependencies.
321    ///
322    /// By default, Hakari only produces output for dependencies that are built with more
323    /// than one feature set. If set to true, Hakari will produce outputs for all dependencies,
324    /// including those that don't need to be unified.
325    ///
326    /// This is rarely needed in production, and is most useful for testing and debugging scenarios.
327    pub fn set_output_single_feature(&mut self, output_single_feature: bool) -> &mut Self {
328        self.output_single_feature = output_single_feature;
329        self
330    }
331
332    /// Returns the current value of `output_single_feature`.
333    pub fn output_single_feature(&self) -> bool {
334        self.output_single_feature
335    }
336
337    /// Version of hakari data to output.
338    ///
339    /// For more, see the documentation for [`DepFormatVersion`](DepFormatVersion).
340    pub fn set_dep_format_version(&mut self, dep_format_version: DepFormatVersion) -> &mut Self {
341        self.dep_format_version = dep_format_version;
342        self
343    }
344
345    /// Returns the current value of `dep_format_version`.
346    pub fn dep_format_version(&self) -> DepFormatVersion {
347        self.dep_format_version
348    }
349
350    /// Kind of `workspace-hack = ...` lines to output.
351    ///
352    /// For more, see the documentation for [`WorkspaceHackLineStyle`].
353    pub fn set_workspace_hack_line_style(
354        &mut self,
355        line_style: WorkspaceHackLineStyle,
356    ) -> &mut Self {
357        self.workspace_hack_line_style = line_style;
358        self
359    }
360
361    /// Returns the current value of `workspace_hack_line_style`.
362    pub fn workspace_hack_line_style(&self) -> WorkspaceHackLineStyle {
363        self.workspace_hack_line_style
364    }
365
366    /// Computes the `Hakari` for this builder.
367    pub fn compute(self) -> Hakari<'g> {
368        Hakari::build(self)
369    }
370
371    // ---
372    // Helper methods
373    // ---
374
375    #[cfg(feature = "cli-support")]
376    pub(crate) fn traversal_excludes_only<'b>(
377        &'b self,
378    ) -> impl Iterator<Item = &'g PackageId> + 'b {
379        self.traversal_excludes.iter().copied()
380    }
381
382    fn make_traversal_excludes<'b>(&'b self) -> TraversalExcludes<'g, 'b> {
383        let hakari_package = if self.verify_mode {
384            None
385        } else {
386            self.hakari_package.map(|package| package.id())
387        };
388
389        TraversalExcludes {
390            excludes: &self.traversal_excludes,
391            hakari_package,
392        }
393    }
394
395    pub(crate) fn make_structural_excludes(&self) -> StructuralExcludes<'g> {
396        let cycle_forming = match &self.hakari_package {
397            Some(hakari_package) => {
398                let roots = std::iter::once(hakari_package.id()).chain(
399                    self.graph
400                        .workspace()
401                        .iter()
402                        .filter(|member| self.is_managed_member(member))
403                        .map(|member| member.id()),
404                );
405                self.graph
406                    .query_reverse(roots)
407                    .expect("roots are package IDs from this graph")
408                    .resolve_with_fn(|_, link| !link.dev_only())
409                    // The direction of the package IDs here doesn't matter since we
410                    // collect into a set anyway.
411                    .packages(DependencyDirection::Reverse)
412                    .filter(|package| !package.in_workspace())
413                    .map(|package| package.id())
414                    .collect()
415            }
416            None => BTreeSet::new(),
417        };
418
419        StructuralExcludes { cycle_forming }
420    }
421
422    fn make_features_only<'b>(&'b self) -> FeatureSet<'g> {
423        if self.verify_mode {
424            match &self.hakari_package {
425                Some(package) => package.to_package_set(),
426                None => self.graph.resolve_none(),
427            }
428            .to_feature_set(StandardFeatures::Default)
429        } else {
430            self.graph.feature_graph().resolve_none()
431        }
432    }
433}
434
435#[cfg(feature = "cli-support")]
436mod summaries {
437    use super::*;
438    use crate::summaries::HakariBuilderSummary;
439    use guppy::platform::TargetFeatures;
440
441    impl<'g> HakariBuilder<'g> {
442        /// Constructs a `HakariBuilder` from a `PackageGraph` and a serialized summary.
443        ///
444        /// Requires the `cli-support` feature to be enabled.
445        ///
446        /// Returns an error if the summary references a package that's not present, or if there was
447        /// some other issue while creating a `HakariBuilder` from the summary.
448        pub fn from_summary(
449            graph: &'g PackageGraph,
450            summary: &HakariBuilderSummary,
451        ) -> Result<Self, guppy::Error> {
452            let hakari_package = summary
453                .hakari_package
454                .as_ref()
455                .map(|name| graph.workspace().member_by_name(name))
456                .transpose()?;
457            let platforms = summary
458                .platforms
459                .iter()
460                .map(|triple_str| {
461                    let platform = Platform::new(triple_str.clone(), TargetFeatures::Unknown)
462                        .map_err(|err| {
463                            guppy::Error::TargetSpecError(
464                                "while resolving hakari config or summary".to_owned(),
465                                err,
466                            )
467                        })?;
468                    Ok(platform.into())
469                })
470                .collect::<Result<Vec<_>, _>>()?;
471
472            let registries: BiHashMap<_, ahash::RandomState> = summary
473                .registries
474                .iter()
475                .map(|(name, url)| Registry {
476                    name: name.clone(),
477                    url: url.clone(),
478                })
479                .collect();
480
481            let traversal_excludes = summary
482                .traversal_excludes
483                .to_package_set_registry(
484                    graph,
485                    |name| registries.get1(name).map(|registry| registry.url.as_str()),
486                    "resolving hakari traversal-excludes",
487                )?
488                .package_ids(DependencyDirection::Forward)
489                .collect();
490            let final_excludes = summary
491                .final_excludes
492                .to_package_set_registry(
493                    graph,
494                    |name| registries.get1(name).map(|registry| registry.url.as_str()),
495                    "resolving hakari final-excludes",
496                )?
497                .package_ids(DependencyDirection::Forward)
498                .collect();
499
500            Ok(Self {
501                graph: DebugIgnore(graph),
502                hakari_package,
503                resolver: summary.resolver,
504                verify_mode: false,
505                unify_target_host: summary.unify_target_host,
506                output_single_feature: summary.output_single_feature,
507                dep_format_version: summary.dep_format_version,
508                workspace_hack_line_style: summary.workspace_hack_line_style,
509                platforms,
510                registries,
511                traversal_excludes,
512                final_excludes,
513            })
514        }
515    }
516}
517
518/// Whether to unify feature sets for a given dependency across target and host platforms.
519///
520/// Consider a dependency that is built as both normally (on the target platform) and in a build
521/// script or proc macro. The normal dependency is considered to be built on the *target platform*,
522/// and is represented in the `[dependencies]` section in the generated `Cargo.toml`.
523/// The build dependency is built on the *host platform*, represented in the `[build-dependencies]`
524/// section.
525///
526/// Now consider that the target and host platforms need two different sets of features:
527///
528/// ```toml
529/// ## feature set on target platform
530/// [dependencies]
531/// my-dep = { version = "1.0", features = ["a", "b"] }
532///
533/// ## feature set on host platform
534/// [build-dependencies]
535/// my-dep = { version = "1.0", features = ["b", "c"] }
536/// ```
537///
538/// Should hakari unify the feature sets across the `[dependencies]` and `[build-dependencies]`
539/// feature sets?
540///
541/// Call `HakariBuilder::set_unify_target_host` to configure this option.
542#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
543#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
544#[cfg_attr(feature = "cli-support", derive(serde::Serialize, serde::Deserialize))]
545#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
546#[non_exhaustive]
547pub enum UnifyTargetHost {
548    /// Perform no unification across the target and host feature sets.
549    ///
550    /// This is the most conservative option, but it means that some dependencies may be built with
551    /// two different sets of features. In this mode, Hakari will likely be significantly less
552    /// efficient.
553    None,
554
555    /// Automatically choose between the [`UnifyIfBoth`](Self::UnifyIfBoth) and the
556    /// [`ReplicateTargetOnHost`](Self::ReplicateTargetOnHost) options:
557    /// * If the workspace contains proc macros, or crates that are build dependencies of other
558    ///   crates, choose the `ReplicateTargetAsHost` strategy.
559    /// * Otherwise, choose the `UnifyIfBoth` strategy.
560    ///
561    /// This is the default behavior.
562    Auto,
563
564    /// Perform unification across target and host feature sets, but only if a dependency is built
565    /// on both the target and the host.
566    ///
567    /// This is useful if cross-compilations are uncommon and one wishes to avoid the same package
568    /// being built two different ways: once for the target and once for the host.
569    UnifyIfBoth,
570
571    /// Perform unification across target and host feature sets, and also replicate all target-only
572    /// lines to the host.
573    ///
574    /// This is most useful if some workspace packages are proc macros or build dependencies
575    /// used by other packages.
576    ReplicateTargetOnHost,
577}
578
579/// The default for `UnifyTargetHost`: automatically choose unification strategy based on the
580/// workspace.
581impl Default for UnifyTargetHost {
582    #[inline]
583    fn default() -> Self {
584        UnifyTargetHost::Auto
585    }
586}
587
588/// Format version for hakari.
589///
590/// Older versions are kept around for backwards compatibility.
591#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
592#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
593#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
594#[non_exhaustive]
595#[derive(Default)]
596pub enum DepFormatVersion {
597    /// `workspace-hack = { path = ...}`. (Note the lack of a trailing space.)
598    ///
599    /// This was used until `cargo hakari 0.9.6`.
600    #[cfg_attr(feature = "cli-support", serde(rename = "1"))]
601    #[default]
602    V1,
603
604    /// `workspace-hack = { version = "0.1", path = ... }`. This was introduced in
605    /// `cargo hakari 0.9.8`.
606    #[cfg_attr(feature = "cli-support", serde(rename = "2"))]
607    V2,
608
609    /// Elides build metadata. This was introduced in `cargo hakari 0.9.18`.
610    #[cfg_attr(feature = "cli-support", serde(rename = "3"))]
611    V3,
612
613    /// Sorts dependency names alphabetically. This was introduced in `cargo hakari 0.9.22`.
614    ///
615    /// (Dependency names were usually produced in sorted order before V4, but there are
616    /// some edge cases where they weren't: see [issue
617    /// #65](https://github.com/guppy-rs/guppy/issues/65).
618    #[cfg_attr(feature = "cli-support", serde(rename = "4"))]
619    V4,
620}
621
622impl DepFormatVersion {
623    /// Returns the highest format version supported by this version of `cargo hakari`.
624    #[inline]
625    pub fn latest() -> Self {
626        DepFormatVersion::V4
627    }
628}
629
630impl fmt::Display for DepFormatVersion {
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        match self {
633            DepFormatVersion::V1 => write!(f, "1"),
634            DepFormatVersion::V2 => write!(f, "2"),
635            DepFormatVersion::V3 => write!(f, "3"),
636            DepFormatVersion::V4 => write!(f, "4"),
637        }
638    }
639}
640
641/// Style of `workspace-hack = ...` lines to output.
642#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
643#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
644#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
645#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
646#[non_exhaustive]
647#[derive(Default)]
648pub enum WorkspaceHackLineStyle {
649    /// `workspace-hack = { version = "0.1", path = ... }`.
650    #[default]
651    Full,
652
653    /// `workspace-hack = { version = "0.1" }`.
654    VersionOnly,
655
656    /// `workspace-hack.workspace = true`
657    WorkspaceDotted,
658}
659
660/// A key representing a platform and host/target. Returned by `Hakari`.
661#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
662pub struct OutputKey {
663    /// The index of the build platform for this key, or `None` if the computation was done in a
664    /// platform-independent manner.
665    pub platform_idx: Option<usize>,
666
667    /// The build platform: target or host.
668    pub build_platform: BuildPlatform,
669}
670
671/// The result of a Hakari computation.
672///
673/// This contains all the data required to generate a workspace package.
674///
675/// Produced by [`HakariBuilder::compute`](HakariBuilder::compute).
676#[derive(Clone, Debug)]
677#[non_exhaustive]
678pub struct Hakari<'g> {
679    pub(crate) builder: HakariBuilder<'g>,
680
681    /// The map built by Hakari of dependencies that need to be unified.
682    ///
683    /// This map is used to construct the TOML output. Public access is provided in case some
684    /// post-processing needs to be done.
685    pub output_map: OutputMap<'g>,
686
687    /// The complete map of dependency build results built by Hakari.
688    ///
689    /// The map does not include workspace packages or the packages reported by
690    /// [`structural_excludes`](Self::structural_excludes).
691    ///
692    /// This map is not used to generate the TOML output.
693    pub computed_map: ComputedMap<'g>,
694
695    structural_excludes: StructuralExcludes<'g>,
696}
697
698impl<'g> Hakari<'g> {
699    /// Returns the `HakariBuilder` used to create this instance.
700    pub fn builder(&self) -> &HakariBuilder<'g> {
701        &self.builder
702    }
703
704    /// Returns the *structural excludes*: third-party packages that depend on
705    /// the Hakari package, or on a workspace member that Hakari manages,
706    /// directly or transitively through normal or build dependencies.
707    ///
708    /// Unlike traversal and final excludes, which come from configuration,
709    /// structural excludes are determined by the shape of the dependency graph
710    /// and can't be configured away.
711    ///
712    /// These packages are never added to the Hakari package, because doing so
713    /// would form a dependency cycle (assuming the `manage-deps` command says
714    /// the workspace is up-to-date). This typically happens when a workspace
715    /// member is also published on a registry such as crates.io, and a
716    /// `[patch]` directive redirects the published version's Hakari dependency
717    /// back into the workspace.
718    ///
719    /// * Dev-only dependencies are not followed, because Cargo permits cycles
720    ///   through them.
721    /// * Target-specific dependencies are followed, no matter which platforms
722    ///   this builder is configured with, because Cargo rejects these cycles
723    ///   even on platforms where the dependency isn't enabled.
724    ///
725    /// Unlike [`traversal_excludes`](HakariBuilder::traversal_excludes), these packages
726    /// are still considered while simulating builds, so their own dependencies
727    /// are unified as usual.
728    ///
729    /// Returns an empty iterator if the builder had no Hakari package
730    /// specified.
731    pub fn structural_excludes(&self) -> impl Iterator<Item = &'g PackageId> + '_ {
732        self.structural_excludes.cycle_forming.iter().copied()
733    }
734
735    /// Returns true if `package_id` is one of the
736    /// [`structural_excludes`](Self::structural_excludes).
737    ///
738    /// Note that this returns `Ok(false)` for workspace members, even though
739    /// those are structurally excluded as well.
740    ///
741    /// Returns an error if this package ID isn't known to the underlying graph.
742    pub fn is_structural_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
743        self.builder.graph().metadata(package_id)?;
744        Ok(self.structural_excludes.cycle_forming.contains(package_id))
745    }
746
747    /// Reads the existing TOML file for the Hakari package from disk, returning a
748    /// `HakariCargoToml`.
749    ///
750    /// This can be used with [`to_toml_string`](Self::to_toml_string) to manage the contents of
751    /// the given TOML file on disk.
752    ///
753    /// Returns an error if there was an issue reading the TOML file from disk, or `None` if
754    /// the builder's [`hakari_package`](HakariBuilder::hakari_package) is `None`.
755    pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
756        self.builder.read_toml()
757    }
758
759    /// Writes `[dependencies]` and other `Cargo.toml` lines to the given `fmt::Write` instance.
760    ///
761    /// `&mut String` and `fmt::Formatter` both implement `fmt::Write`.
762    pub fn write_toml(
763        &self,
764        options: &HakariOutputOptions,
765        out: impl fmt::Write,
766    ) -> Result<(), TomlOutError> {
767        write_toml(
768            &self.builder,
769            &self.output_map,
770            options,
771            self.builder.dep_format_version,
772            out,
773        )
774    }
775
776    /// Returns a map of dependency names as present in the workspace-hack's `Cargo.toml` to their
777    /// corresponding [`PackageMetadata`].
778    ///
779    /// Packages which have one version are present as their original names, while packages with
780    /// more than one version have a hash appended to them. The map is ordered by name.
781    pub fn toml_name_map(&self) -> IdOrdMap<TomlNameEntry<'g>> {
782        toml_name_map(&self.output_map, self.builder.dep_format_version)
783    }
784
785    /// Returns a `HakariExplain`, which can be used to print out why a specific package is
786    /// in the workspace-hack's `Cargo.toml`.
787    ///
788    /// Returns an error if the package ID was not found in the output.
789    pub fn explain(
790        &self,
791        package_id: &'g PackageId,
792    ) -> Result<HakariExplain<'g, '_>, guppy::Error> {
793        HakariExplain::new(self, package_id)
794    }
795
796    /// A convenience method around `write_toml` that returns a new string with `Cargo.toml` lines.
797    ///
798    /// The returned string is guaranteed to be valid TOML, and can be provided to
799    /// a [`HakariCargoToml`](crate::HakariCargoToml) obtained from [`read_toml`](Self::read_toml).
800    pub fn to_toml_string(&self, options: &HakariOutputOptions) -> Result<String, TomlOutError> {
801        let mut out = String::new();
802        self.write_toml(options, &mut out)?;
803        Ok(out)
804    }
805
806    // ---
807    // Helper methods
808    // ---
809
810    fn build(builder: HakariBuilder<'g>) -> Self {
811        let graph = *builder.graph;
812        let mut computed_map_build = ComputedMapBuild::new(&builder);
813        let platform_specs: Vec<_> = builder
814            .platforms
815            .iter()
816            .map(|platform| PlatformSpec::from(platform.clone()))
817            .collect();
818
819        let unify_target_host = builder.unify_target_host.to_impl(graph);
820
821        // Collect all the dependencies that need to be unified, by platform and build type.
822        let mut map_build: OutputMapBuild<'g> = OutputMapBuild::new(graph);
823        map_build.insert_all(
824            computed_map_build.iter(),
825            builder.output_single_feature,
826            unify_target_host,
827        );
828
829        if !builder.output_single_feature {
830            // Adding packages might cause different feature sets for some dependencies. Simulate
831            // further builds with the given target and host features, and use that to add in any
832            // extra features that need to be considered.
833            loop {
834                let mut add_extra = HashSet::new();
835                for (output_key, features) in map_build.iter_feature_sets() {
836                    let initials_platform = match output_key.build_platform {
837                        BuildPlatform::Target => InitialsPlatform::Standard,
838                        BuildPlatform::Host => InitialsPlatform::Host,
839                    };
840
841                    let mut cargo_opts = CargoOptions::new();
842                    let platform_spec = match output_key.platform_idx {
843                        Some(idx) => platform_specs[idx].clone(),
844                        None => PlatformSpec::Always,
845                    };
846                    // Third-party dependencies are built without including dev.
847                    cargo_opts
848                        .set_include_dev(false)
849                        .set_initials_platform(initials_platform)
850                        .set_platform(platform_spec)
851                        .set_resolver(builder.resolver)
852                        .add_omitted_packages(computed_map_build.traversal_excludes.iter());
853                    let cargo_set = features
854                        .into_cargo_set(&cargo_opts)
855                        .expect("into_cargo_set processed successfully");
856
857                    // Check the features for the cargo set to see if any further dependencies were
858                    // built with a different result and weren't included in the hakari map
859                    // originally.
860                    for &(build_platform, feature_set) in cargo_set.all_features().iter() {
861                        for feature_list in
862                            feature_set.packages_with_features(DependencyDirection::Forward)
863                        {
864                            let dep = feature_list.package();
865                            if computed_map_build.structural_excludes.never_unified(dep) {
866                                continue;
867                            }
868                            let dep_id = dep.id();
869                            // This is "get or insert" because we could be adding whole new
870                            // dependencies here rather than just new features to existing
871                            // dependencies.
872                            let v_mut = computed_map_build
873                                .get_or_insert_mut(output_key.platform_idx, dep_id);
874
875                            // Is it already present in the output?
876                            let new_key = OutputKey {
877                                platform_idx: output_key.platform_idx,
878                                build_platform,
879                            };
880
881                            if map_build.is_inserted(new_key, dep_id) {
882                                continue;
883                            }
884
885                            let this_list: BTreeSet<_> = feature_list.named_features().collect();
886
887                            let already_present = v_mut.contains(build_platform, &this_list);
888                            if !already_present {
889                                // The feature list added by this dependency is non-unique.
890                                v_mut.mark_fixed_up(build_platform, this_list);
891                                add_extra.insert((output_key.platform_idx, dep_id));
892                            }
893                        }
894                    }
895                }
896
897                if add_extra.is_empty() {
898                    break;
899                }
900
901                map_build.insert_all(
902                    add_extra.iter().map(|&(platform_idx, dep_id)| {
903                        let v = computed_map_build
904                            .get(platform_idx, dep_id)
905                            .expect("full value should be present");
906                        (platform_idx, dep_id, v)
907                    }),
908                    builder.output_single_feature,
909                    unify_target_host,
910                );
911            }
912        }
913
914        let ComputedMapBuild {
915            structural_excludes,
916            computed_map,
917            ..
918        } = computed_map_build;
919        let output_map = map_build.finish(
920            &builder.final_excludes,
921            builder.dep_format_version,
922            builder.output_single_feature,
923        );
924
925        Self {
926            builder,
927            output_map,
928            computed_map,
929            structural_excludes,
930        }
931    }
932}
933
934/// The map used by Hakari to generate output TOML.
935///
936/// This is a two-level `BTreeMap`, where:
937/// * the top-level keys are [`OutputKey`](OutputKey) instances.
938/// * the inner map is keyed by dependency [`PackageId`](PackageId) instances, and the values are
939///   the corresponding [`PackageMetadata`](PackageMetadata) for this dependency, and the set of
940///   features enabled for this package.
941///
942/// This is an alias for the type of [`Hakari::output_map`](Hakari::output_map).
943pub type OutputMap<'g> =
944    BTreeMap<OutputKey, BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>>;
945
946/// The map of all build results computed by Hakari.
947///
948/// The keys are the platform index and the dependency's package ID, and the values are
949/// [`ComputedValue`](ComputedValue) instances that represent the different feature sets this
950/// dependency is built with on both the host and target platforms.
951///
952/// The values that are most interesting are the ones where maps have two elements or more: they
953/// indicate dependencies with features that need to be unified.
954///
955/// This is an alias for the type of [`Hakari::computed_map`](Hakari::computed_map).
956pub type ComputedMap<'g> = BTreeMap<(Option<usize>, &'g PackageId), ComputedValue<'g>>;
957
958/// The values of a [`ComputedMap`](ComputedMap).
959///
960/// This represents a pair of `ComputedInnerMap` instances: one for the target platform and one for
961/// the host. For more about the values, see the documentation for
962/// [`ComputedInnerMap`](ComputedInnerMap).
963#[derive(Clone, Debug, Default)]
964pub struct ComputedValue<'g> {
965    /// The feature sets built on the target platform.
966    pub target_inner: ComputedInnerMap<'g>,
967
968    /// The feature sets built on the host platform.
969    pub host_inner: ComputedInnerMap<'g>,
970}
971
972/// A target map or a host map in a [`ComputedValue`](ComputedValue).
973///
974/// * The keys are sets of feature names (or empty for no features).
975/// * The values are [`ComputedInnerValue`] instances.
976pub type ComputedInnerMap<'g> = BTreeMap<BTreeSet<&'g str>, ComputedInnerValue<'g>>;
977
978/// The values of [`ComputedInnerMap`].
979#[derive(Clone, Debug, Default)]
980pub struct ComputedInnerValue<'g> {
981    /// The workspace packages, selected features, and include dev that cause the key in
982    /// `ComputedMap` to be built with the feature set that forms the key of `ComputedInnerMap`.
983    /// They are not defined to be in any particular order.
984    pub workspace_packages: Vec<(PackageMetadata<'g>, StandardFeatures, bool)>,
985
986    /// Whether at least one post-computation fixup was performed with this feature set.
987    pub fixed_up: bool,
988}
989
990impl<'g> ComputedInnerValue<'g> {
991    fn extend(&mut self, other: ComputedInnerValue<'g>) {
992        self.workspace_packages.extend(other.workspace_packages);
993        self.fixed_up |= other.fixed_up;
994    }
995
996    #[inline]
997    fn push(
998        &mut self,
999        package: PackageMetadata<'g>,
1000        features: StandardFeatures,
1001        include_dev: bool,
1002    ) {
1003        self.workspace_packages
1004            .push((package, features, include_dev));
1005    }
1006}
1007
1008#[derive(Debug)]
1009struct TraversalExcludes<'g, 'b> {
1010    excludes: &'b HashSet<&'g PackageId>,
1011    hakari_package: Option<&'g PackageId>,
1012}
1013
1014impl<'g, 'b> TraversalExcludes<'g, 'b> {
1015    fn iter(&self) -> impl Iterator<Item = &'g PackageId> + 'b + use<'g, 'b> {
1016        self.excludes.iter().copied().chain(self.hakari_package)
1017    }
1018
1019    fn is_excluded(&self, package_id: &PackageId) -> bool {
1020        self.hakari_package == Some(package_id) || self.excludes.contains(package_id)
1021    }
1022}
1023
1024/// Packages that can never be unified into the Hakari package, regardless of
1025/// configuration.
1026///
1027/// For the definition, and how these differ from [`TraversalExcludes`], see
1028/// [`Hakari::structural_excludes`].
1029#[derive(Clone, Debug)]
1030pub(crate) struct StructuralExcludes<'g> {
1031    /// The third-party packages reported by [`Hakari::structural_excludes`].
1032    ///
1033    /// Workspace packages (including the Hakari package itself) are never in
1034    /// this set; [`Self::never_unified`] handles them.
1035    pub(crate) cycle_forming: BTreeSet<&'g PackageId>,
1036}
1037
1038impl<'g> StructuralExcludes<'g> {
1039    /// Returns true if `package` must never be unified into the Hakari
1040    /// package: it is a workspace package (Hakari only unifies third-party
1041    /// dependencies), or a member of [`Self::cycle_forming`].
1042    fn never_unified(&self, package: &PackageMetadata<'g>) -> bool {
1043        package.in_workspace() || self.cycle_forming.contains(package.id())
1044    }
1045}
1046
1047/// Intermediate build state used by Hakari.
1048#[derive(Debug)]
1049struct ComputedMapBuild<'g, 'b> {
1050    traversal_excludes: TraversalExcludes<'g, 'b>,
1051    structural_excludes: StructuralExcludes<'g>,
1052    computed_map: ComputedMap<'g>,
1053}
1054
1055impl<'g, 'b> ComputedMapBuild<'g, 'b> {
1056    fn new(builder: &'b HakariBuilder<'g>) -> Self {
1057        // This was just None or All for a bit under the theory that feature sets are additive only,
1058        // but unfortunately we cannot exploit this property because it doesn't account for the fact
1059        // that some dependencies might not be built *at all*, under certain feature combinations.
1060        //
1061        // That's also why we simulate builds with and without dev-only dependencies in all cases.
1062        //
1063        // For example, for:
1064        //
1065        // ```toml
1066        // [dependencies]
1067        // dep = { version = "1", optional = true }
1068        //
1069        // [dev-dependencies]
1070        // dep = { version = "1", optional = true, features = ["dev-feature"] }
1071        //
1072        // [features]
1073        // default = ["dep"]
1074        // extra = ["dep/extra", "dep/dev-feature"]
1075        // ```
1076        //
1077        // | feature set | include dev | dep status         |
1078        // | ----------- | ----------- | ------------------ |
1079        // | none        | no          | not built          |
1080        // | none        | yes         | not built          |
1081        // | default     | no          | no features        |
1082        // | default     | yes         | dev-feature        |
1083        // | all         | no          | extra, dev-feature |
1084        // | all         | yes         | extra, dev-feature |
1085        //
1086        // (And there's further complexity possible with transitive deps as well.)
1087        let features_include_dev = [
1088            (StandardFeatures::None, false),
1089            (StandardFeatures::None, true),
1090            (StandardFeatures::Default, false),
1091            (StandardFeatures::Default, true),
1092            (StandardFeatures::All, false),
1093            (StandardFeatures::All, true),
1094        ];
1095
1096        // Features for the "always" platform spec.
1097        let always_features = features_include_dev
1098            .iter()
1099            .map(|&(features, include_dev)| (None, PlatformSpec::Always, features, include_dev));
1100
1101        // Features for specified platforms.
1102        let specified_features =
1103            features_include_dev
1104                .iter()
1105                .flat_map(|&(features, include_dev)| {
1106                    builder
1107                        .platforms
1108                        .iter()
1109                        .enumerate()
1110                        .map(move |(idx, platform)| {
1111                            (
1112                                Some(idx),
1113                                PlatformSpec::from(platform.clone()),
1114                                features,
1115                                include_dev,
1116                            )
1117                        })
1118                });
1119        let platforms_features: Vec<_> = always_features.chain(specified_features).collect();
1120
1121        let workspace = builder.graph.workspace();
1122        let traversal_excludes = builder.make_traversal_excludes();
1123        let structural_excludes = builder.make_structural_excludes();
1124        let features_only = builder.make_features_only();
1125        let traversal_excludes_ref = &traversal_excludes;
1126        let structural_excludes_ref = &structural_excludes;
1127        let features_only_ref = &features_only;
1128
1129        let computed_map: ComputedMap<'g> = platforms_features
1130            .into_par_iter()
1131            // The cargo_set computation in the inner iterator is the most expensive part of the
1132            // process, so use flat_map instead of flat_map_iter.
1133            .flat_map(|(idx, platform_spec, feature_filter, include_dev)| {
1134                let mut cargo_options = CargoOptions::new();
1135                cargo_options
1136                    .set_include_dev(include_dev)
1137                    .set_resolver(builder.resolver)
1138                    .set_platform(platform_spec)
1139                    .add_omitted_packages(traversal_excludes.iter());
1140
1141                workspace.par_iter().map(move |workspace_package| {
1142                    if traversal_excludes_ref.is_excluded(workspace_package.id()) {
1143                        // Skip this package since it was excluded during traversal.
1144                        return BTreeMap::new();
1145                    }
1146
1147                    let initials = workspace_package
1148                        .to_package_set()
1149                        .to_feature_set(feature_filter);
1150                    let cargo_set =
1151                        CargoSet::new(initials, features_only_ref.clone(), &cargo_options)
1152                            .expect("cargo resolution should succeed");
1153
1154                    let all_features = cargo_set.all_features();
1155
1156                    let values = all_features.iter().flat_map(|&(build_platform, features)| {
1157                        features
1158                            .packages_with_features(DependencyDirection::Forward)
1159                            .filter_map(move |feature_list| {
1160                                let dep = feature_list.package();
1161                                if structural_excludes_ref.never_unified(dep) {
1162                                    return None;
1163                                }
1164
1165                                let features: BTreeSet<&'g str> =
1166                                    feature_list.named_features().collect();
1167                                Some((
1168                                    idx,
1169                                    build_platform,
1170                                    dep.id(),
1171                                    features,
1172                                    workspace_package,
1173                                    feature_filter,
1174                                    include_dev,
1175                                ))
1176                            })
1177                    });
1178
1179                    let mut map = ComputedMap::new();
1180                    for (
1181                        platform_idx,
1182                        build_platform,
1183                        package_id,
1184                        features,
1185                        package,
1186                        feature_filter,
1187                        include_dev,
1188                    ) in values
1189                    {
1190                        // Accumulate the features and package for each key.
1191                        map.entry((platform_idx, package_id)).or_default().insert(
1192                            build_platform,
1193                            features,
1194                            package,
1195                            feature_filter,
1196                            include_dev,
1197                        );
1198                    }
1199
1200                    map
1201                })
1202            })
1203            .reduce(ComputedMap::new, |mut acc, map| {
1204                // Accumulate across all threads.
1205                for (k, v) in map {
1206                    acc.entry(k).or_default().merge(v);
1207                }
1208                acc
1209            });
1210
1211        Self {
1212            traversal_excludes,
1213            structural_excludes,
1214            computed_map,
1215        }
1216    }
1217
1218    fn get(
1219        &self,
1220        platform_idx: Option<usize>,
1221        package_id: &'g PackageId,
1222    ) -> Option<&ComputedValue<'g>> {
1223        self.computed_map.get(&(platform_idx, package_id))
1224    }
1225
1226    fn get_or_insert_mut(
1227        &mut self,
1228        platform_idx: Option<usize>,
1229        package_id: &'g PackageId,
1230    ) -> &mut ComputedValue<'g> {
1231        self.computed_map
1232            .entry((platform_idx, package_id))
1233            .or_default()
1234    }
1235
1236    fn iter<'a>(
1237        &'a self,
1238    ) -> impl Iterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)> + 'a {
1239        self.computed_map
1240            .iter()
1241            .map(move |(&(platform_idx, package_id), v)| (platform_idx, package_id, v))
1242    }
1243}
1244
1245impl<'g> ComputedValue<'g> {
1246    /// Returns both the inner maps along with the build platforms they represent.
1247    pub fn inner_maps(&self) -> [(BuildPlatform, &ComputedInnerMap<'g>); 2] {
1248        [
1249            (BuildPlatform::Target, &self.target_inner),
1250            (BuildPlatform::Host, &self.host_inner),
1251        ]
1252    }
1253
1254    /// Converts `self` into [`ComputedInnerMap`] instances, along with the build platforms they
1255    /// represent.
1256    pub fn into_inner_maps(self) -> [(BuildPlatform, ComputedInnerMap<'g>); 2] {
1257        [
1258            (BuildPlatform::Target, self.target_inner),
1259            (BuildPlatform::Host, self.host_inner),
1260        ]
1261    }
1262
1263    /// Returns a reference to the inner map corresponding to the given build platform.
1264    pub fn get_inner(&self, build_platform: BuildPlatform) -> &ComputedInnerMap<'g> {
1265        match build_platform {
1266            BuildPlatform::Target => &self.target_inner,
1267            BuildPlatform::Host => &self.host_inner,
1268        }
1269    }
1270
1271    /// Returns a mutable reference to the inner map corresponding to the given build platform.
1272    pub fn get_inner_mut(&mut self, build_platform: BuildPlatform) -> &mut ComputedInnerMap<'g> {
1273        match build_platform {
1274            BuildPlatform::Target => &mut self.target_inner,
1275            BuildPlatform::Host => &mut self.host_inner,
1276        }
1277    }
1278
1279    /// Adds all the instances in `other` to `self`.
1280    fn merge(&mut self, other: ComputedValue<'g>) {
1281        for (features, details) in other.target_inner {
1282            self.target_inner
1283                .entry(features)
1284                .or_default()
1285                .extend(details);
1286        }
1287        for (features, details) in other.host_inner {
1288            self.host_inner.entry(features).or_default().extend(details);
1289        }
1290    }
1291
1292    fn contains(&mut self, build_platform: BuildPlatform, features: &BTreeSet<&'g str>) -> bool {
1293        self.get_inner(build_platform).contains_key(features)
1294    }
1295
1296    fn insert(
1297        &mut self,
1298        build_platform: BuildPlatform,
1299        features: BTreeSet<&'g str>,
1300        package: PackageMetadata<'g>,
1301        feature_filter: StandardFeatures,
1302        include_dev: bool,
1303    ) {
1304        self.get_inner_mut(build_platform)
1305            .entry(features)
1306            .or_default()
1307            .push(package, feature_filter, include_dev);
1308    }
1309
1310    fn mark_fixed_up(&mut self, build_platform: BuildPlatform, features: BTreeSet<&'g str>) {
1311        self.get_inner_mut(build_platform)
1312            .entry(features)
1313            .or_default()
1314            .fixed_up = true;
1315    }
1316
1317    fn describe<'a>(&'a self) -> ValueDescribe<'g, 'a> {
1318        match (self.target_inner.len(), self.host_inner.len()) {
1319            (0, 0) => ValueDescribe::None,
1320            (0, 1) => ValueDescribe::SingleHost(&self.host_inner),
1321            (1, 0) => ValueDescribe::SingleTarget(&self.target_inner),
1322            (1, 1) => {
1323                let target_features = self.target_inner.keys().next().expect("1 element");
1324                let host_features = self.host_inner.keys().next().expect("1 element");
1325                if target_features == host_features {
1326                    ValueDescribe::SingleMatchingBoth {
1327                        target_inner: &self.target_inner,
1328                        host_inner: &self.host_inner,
1329                    }
1330                } else {
1331                    ValueDescribe::SingleNonMatchingBoth {
1332                        target_inner: &self.target_inner,
1333                        host_inner: &self.host_inner,
1334                    }
1335                }
1336            }
1337            (_m, 0) => ValueDescribe::MultiTarget(&self.target_inner),
1338            (_m, 1) => ValueDescribe::MultiTargetSingleHost {
1339                target_inner: &self.target_inner,
1340                host_inner: &self.host_inner,
1341            },
1342            (0, _n) => ValueDescribe::MultiHost(&self.host_inner),
1343            (1, _n) => ValueDescribe::MultiHostSingleTarget {
1344                target_inner: &self.target_inner,
1345                host_inner: &self.host_inner,
1346            },
1347            (_m, _n) => ValueDescribe::MultiBoth {
1348                target_inner: &self.target_inner,
1349                host_inner: &self.host_inner,
1350            },
1351        }
1352    }
1353}
1354
1355#[derive(Copy, Clone, Debug)]
1356enum ValueDescribe<'g, 'a> {
1357    None,
1358    SingleTarget(&'a ComputedInnerMap<'g>),
1359    SingleHost(&'a ComputedInnerMap<'g>),
1360    MultiTarget(&'a ComputedInnerMap<'g>),
1361    MultiHost(&'a ComputedInnerMap<'g>),
1362    SingleMatchingBoth {
1363        target_inner: &'a ComputedInnerMap<'g>,
1364        host_inner: &'a ComputedInnerMap<'g>,
1365    },
1366    SingleNonMatchingBoth {
1367        target_inner: &'a ComputedInnerMap<'g>,
1368        host_inner: &'a ComputedInnerMap<'g>,
1369    },
1370    MultiTargetSingleHost {
1371        target_inner: &'a ComputedInnerMap<'g>,
1372        host_inner: &'a ComputedInnerMap<'g>,
1373    },
1374    MultiHostSingleTarget {
1375        target_inner: &'a ComputedInnerMap<'g>,
1376        host_inner: &'a ComputedInnerMap<'g>,
1377    },
1378    MultiBoth {
1379        target_inner: &'a ComputedInnerMap<'g>,
1380        host_inner: &'a ComputedInnerMap<'g>,
1381    },
1382}
1383
1384impl<'g, 'a> ValueDescribe<'g, 'a> {
1385    #[allow(dead_code)]
1386    fn description(self) -> &'static str {
1387        match self {
1388            ValueDescribe::None => "None",
1389            ValueDescribe::SingleTarget(_) => "SingleTarget",
1390            ValueDescribe::SingleHost(_) => "SingleHost",
1391            ValueDescribe::MultiTarget(_) => "MultiTarget",
1392            ValueDescribe::MultiHost(_) => "MultiHost",
1393            ValueDescribe::SingleMatchingBoth { .. } => "SingleMatchingBoth",
1394            ValueDescribe::SingleNonMatchingBoth { .. } => "SingleNonMatchingBoth",
1395            ValueDescribe::MultiTargetSingleHost { .. } => "MultiTargetSingleHost",
1396            ValueDescribe::MultiHostSingleTarget { .. } => "MultiHostSingleTarget",
1397            ValueDescribe::MultiBoth { .. } => "MultiBoth",
1398        }
1399    }
1400
1401    fn insert(
1402        self,
1403        output_single_feature: bool,
1404        unify_target_host: UnifyTargetHostImpl,
1405        mut insert_cb: impl FnMut(BuildPlatform, &'a ComputedInnerMap<'g>),
1406    ) {
1407        use BuildPlatform::*;
1408
1409        match self {
1410            ValueDescribe::None => {
1411                // Empty, ignore. (This should probably never happen anyway.)
1412            }
1413            ValueDescribe::SingleTarget(target_inner) => {
1414                // Just one way to unify these.
1415                if output_single_feature {
1416                    insert_cb(Target, target_inner);
1417                    if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1418                        insert_cb(Host, target_inner);
1419                    }
1420                }
1421            }
1422            ValueDescribe::SingleHost(host_inner) => {
1423                // Just one way to unify other.
1424                if output_single_feature {
1425                    insert_cb(Host, host_inner);
1426                }
1427            }
1428            ValueDescribe::MultiTarget(target_inner) => {
1429                // Unify features for target.
1430                insert_cb(Target, target_inner);
1431                if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1432                    insert_cb(Host, target_inner);
1433                }
1434            }
1435            ValueDescribe::MultiHost(host_inner) => {
1436                // Unify features for host.
1437                insert_cb(Host, host_inner);
1438            }
1439            ValueDescribe::SingleMatchingBoth {
1440                target_inner,
1441                host_inner,
1442            } => {
1443                // Just one way to unify across both.
1444                if output_single_feature {
1445                    insert_cb(Target, target_inner);
1446                    insert_cb(Host, host_inner);
1447                }
1448            }
1449            ValueDescribe::SingleNonMatchingBoth {
1450                target_inner,
1451                host_inner,
1452            } => {
1453                // Unify features for both across both.
1454                insert_cb(Target, target_inner);
1455                insert_cb(Host, host_inner);
1456                if unify_target_host != UnifyTargetHostImpl::None {
1457                    insert_cb(Target, host_inner);
1458                    insert_cb(Host, target_inner);
1459                }
1460            }
1461            ValueDescribe::MultiTargetSingleHost {
1462                target_inner,
1463                host_inner,
1464            } => {
1465                // Unify features for both across both.
1466                insert_cb(Target, target_inner);
1467                insert_cb(Host, host_inner);
1468                if unify_target_host != UnifyTargetHostImpl::None {
1469                    insert_cb(Target, host_inner);
1470                    insert_cb(Host, target_inner);
1471                }
1472            }
1473            ValueDescribe::MultiHostSingleTarget {
1474                target_inner,
1475                host_inner,
1476            } => {
1477                // Unify features for both across both.
1478                insert_cb(Target, target_inner);
1479                insert_cb(Host, host_inner);
1480                if unify_target_host != UnifyTargetHostImpl::None {
1481                    insert_cb(Target, host_inner);
1482                    insert_cb(Host, target_inner);
1483                }
1484            }
1485            ValueDescribe::MultiBoth {
1486                target_inner,
1487                host_inner,
1488            } => {
1489                // Unify features for both across both.
1490                insert_cb(Target, target_inner);
1491                insert_cb(Host, host_inner);
1492                if unify_target_host != UnifyTargetHostImpl::None {
1493                    insert_cb(Target, host_inner);
1494                    insert_cb(Host, target_inner);
1495                }
1496            }
1497        }
1498    }
1499}
1500
1501#[derive(Debug)]
1502struct OutputMapBuild<'g> {
1503    graph: &'g PackageGraph,
1504    output_map: OutputMap<'g>,
1505}
1506
1507impl<'g> OutputMapBuild<'g> {
1508    fn new(graph: &'g PackageGraph) -> Self {
1509        Self {
1510            graph,
1511            output_map: OutputMap::new(),
1512        }
1513    }
1514
1515    fn is_inserted(&self, output_key: OutputKey, package_id: &'g PackageId) -> bool {
1516        match self.output_map.get(&output_key) {
1517            Some(inner_map) => inner_map.contains_key(package_id),
1518            None => false,
1519        }
1520    }
1521
1522    #[allow(dead_code)]
1523    fn get(
1524        &self,
1525        output_key: OutputKey,
1526        package_id: &'g PackageId,
1527    ) -> Option<&(PackageMetadata<'g>, BTreeSet<&'g str>)> {
1528        match self.output_map.get(&output_key) {
1529            Some(inner_map) => inner_map.get(package_id),
1530            None => None,
1531        }
1532    }
1533
1534    fn insert_all<'a>(
1535        &mut self,
1536        values: impl IntoIterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)>,
1537        output_single_feature: bool,
1538        unify_target_host: UnifyTargetHostImpl,
1539    ) where
1540        'g: 'a,
1541    {
1542        for (platform_idx, dep_id, v) in values {
1543            let describe = v.describe();
1544            describe.insert(
1545                output_single_feature,
1546                unify_target_host,
1547                |build_platform, inner| {
1548                    self.insert_inner(platform_idx, build_platform, dep_id, inner);
1549                },
1550            );
1551        }
1552    }
1553
1554    fn insert_inner(
1555        &mut self,
1556        platform_idx: Option<usize>,
1557        build_platform: BuildPlatform,
1558        package_id: &'g PackageId,
1559        inner: &ComputedInnerMap<'g>,
1560    ) {
1561        let output_key = OutputKey {
1562            platform_idx,
1563            build_platform,
1564        };
1565        self.insert(
1566            output_key,
1567            package_id,
1568            inner.keys().flat_map(|f| f.iter().copied()),
1569        )
1570    }
1571
1572    fn insert(
1573        &mut self,
1574        output_key: OutputKey,
1575        package_id: &'g PackageId,
1576        features: impl IntoIterator<Item = &'g str>,
1577    ) {
1578        let map = self.output_map.entry(output_key).or_default();
1579        let graph = self.graph;
1580        let (_, inner) = map.entry(package_id).or_insert_with(|| {
1581            (
1582                graph.metadata(package_id).expect("valid package ID"),
1583                BTreeSet::new(),
1584            )
1585        });
1586        inner.extend(features);
1587    }
1588
1589    fn iter_feature_sets<'a>(&'a self) -> impl Iterator<Item = (OutputKey, FeatureSet<'g>)> + 'a {
1590        self.output_map.iter().map(move |(&output_key, deps)| {
1591            let feature_ids = deps.iter().flat_map(|(&package_id, (_, features))| {
1592                features
1593                    .iter()
1594                    .map(move |&feature| FeatureId::new(package_id, FeatureLabel::Named(feature)))
1595            });
1596            (
1597                output_key,
1598                self.graph
1599                    .feature_graph()
1600                    .resolve_ids(feature_ids)
1601                    .expect("specified feature IDs are valid"),
1602            )
1603        })
1604    }
1605
1606    fn finish(
1607        mut self,
1608        final_excludes: &HashSet<&'g PackageId>,
1609        dep_format: DepFormatVersion,
1610        output_single_feature: bool,
1611    ) -> OutputMap<'g> {
1612        // Remove all features that are already unified in the "always" set.
1613        for &build_platform in BuildPlatform::VALUES {
1614            let always_key = OutputKey {
1615                platform_idx: None,
1616                build_platform,
1617            };
1618
1619            // Temporarily remove the set to avoid &mut issues.
1620            let mut always_map = match self.output_map.remove(&always_key) {
1621                Some(always_map) => always_map,
1622                None => {
1623                    // No features unified for the always set.
1624                    continue;
1625                }
1626            };
1627
1628            if dep_format >= DepFormatVersion::V3 {
1629                Self::filter_root_features(&mut always_map, output_single_feature);
1630            }
1631
1632            for (key, inner_map) in &mut self.output_map {
1633                // Treat the host and target maps as separate.
1634                if key.build_platform != build_platform {
1635                    continue;
1636                }
1637                if dep_format >= DepFormatVersion::V3 {
1638                    Self::filter_root_features(inner_map, output_single_feature);
1639                }
1640
1641                for (package_id, (_always_package, always_features)) in &always_map {
1642                    let (package, remaining_features) = {
1643                        let (package, features) = match inner_map.get(package_id) {
1644                            Some(v) => v,
1645                            None => {
1646                                // The package ID isn't present in the platform-specific map --
1647                                // nothing to be done.
1648                                continue;
1649                            }
1650                        };
1651                        (*package, features - always_features)
1652                    };
1653                    if remaining_features.is_empty() {
1654                        // No features left.
1655                        inner_map.remove(package_id);
1656                    } else {
1657                        inner_map.insert(package_id, (package, remaining_features));
1658                    }
1659                }
1660            }
1661
1662            // Put always_map back into the output map.
1663            self.output_map.insert(always_key, always_map);
1664        }
1665
1666        // Remove final-excludes, and get rid of any maps that are empty.
1667        self.output_map.retain(|_, inner_map| {
1668            for package_id in final_excludes {
1669                inner_map.remove(package_id);
1670            }
1671            !inner_map.is_empty()
1672        });
1673
1674        self.output_map
1675    }
1676
1677    /// Removes all features from the map that aren't at the root of the provided feature graph.
1678    ///
1679    /// Many crates have a notion of public and private features. Private features are not
1680    /// intended to be used by consumers of the crate, and are only used by the crate itself.
1681    ///
1682    /// As a heuristic, we assume that all root features are public.
1683    ///
1684    /// There aren't any platform-related considerations here, because internal feature dependencies
1685    /// aren't platform-specific.
1686    fn filter_root_features(
1687        inner_map: &mut BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>,
1688        output_single_feature: bool,
1689    ) {
1690        inner_map.retain(|_, (package, features)| {
1691            let feature_set = package.to_feature_set(named_feature_filter(
1692                StandardFeatures::None,
1693                features.iter().copied(),
1694            ));
1695
1696            let root_features: BTreeSet<_> = feature_set
1697                .root_ids(DependencyDirection::Forward)
1698                .filter_map(|f| match f.label() {
1699                    FeatureLabel::Named(name) => Some(name),
1700                    FeatureLabel::Base => None,
1701                    FeatureLabel::OptionalDependency(name) => {
1702                        debug_assert!(
1703                            false,
1704                            "root features must be named or base, found optional dependency {name}",
1705                        );
1706                        None
1707                    }
1708                })
1709                .collect();
1710
1711            if root_features.is_empty() {
1712                // No features left -- remove it from the map if output_single_feature is false. If
1713                // it's true, then we might be tracking a feature set that was originally provided
1714                // as empty to us.
1715                output_single_feature && features.is_empty()
1716            } else {
1717                *features = root_features;
1718                true
1719            }
1720        });
1721    }
1722}
1723
1724#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1725enum UnifyTargetHostImpl {
1726    None,
1727    UnifyIfBoth,
1728    ReplicateTargetOnHost,
1729}
1730
1731impl UnifyTargetHost {
1732    fn to_impl(self, graph: &PackageGraph) -> UnifyTargetHostImpl {
1733        match self {
1734            UnifyTargetHost::None => UnifyTargetHostImpl::None,
1735            UnifyTargetHost::UnifyIfBoth => UnifyTargetHostImpl::UnifyIfBoth,
1736            UnifyTargetHost::ReplicateTargetOnHost => UnifyTargetHostImpl::ReplicateTargetOnHost,
1737            UnifyTargetHost::Auto => {
1738                let workspace_set = graph.resolve_workspace();
1739                // Is any package a proc macro?
1740                if workspace_set
1741                    .packages(DependencyDirection::Forward)
1742                    .any(|package| package.is_proc_macro())
1743                {
1744                    return UnifyTargetHostImpl::ReplicateTargetOnHost;
1745                }
1746
1747                // Is any package a build dependency of any other?
1748                if workspace_set
1749                    .links(DependencyDirection::Forward)
1750                    .any(|link| link.build().is_present())
1751                {
1752                    return UnifyTargetHostImpl::ReplicateTargetOnHost;
1753                }
1754
1755                UnifyTargetHostImpl::UnifyIfBoth
1756            }
1757        }
1758    }
1759}
1760
1761#[cfg(test)]
1762mod tests {
1763    use super::*;
1764    use crate::UnifyTargetHost;
1765    use fixtures::json::*;
1766
1767    #[test]
1768    fn unify_target_host_auto() {
1769        // Test that this "guppy" fixture (which does not have internal proc macros or build deps)
1770        // turns into "unify if both".
1771        let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_guppy_78cb7e8().graph());
1772        assert_eq!(
1773            res,
1774            UnifyTargetHostImpl::UnifyIfBoth,
1775            "no proc macros => unify if both"
1776        );
1777
1778        // Test that this "libra" fixture (which has internal proc macros) turns into "replicate
1779        // target on host".
1780        let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_libra_9ffd93b().graph());
1781        assert_eq!(
1782            res,
1783            UnifyTargetHostImpl::ReplicateTargetOnHost,
1784            "proc macros => replicate target on host"
1785        );
1786
1787        // Test that the "builddep" fixture (which has an internal build dependency) turns into
1788        // "replicate target on host".
1789        let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_builddep().graph());
1790        assert_eq!(
1791            res,
1792            UnifyTargetHostImpl::ReplicateTargetOnHost,
1793            "internal build deps => replicate target on host"
1794        );
1795    }
1796
1797    #[test]
1798    fn fixpoint_never_adds_workspace_packages() {
1799        // This fixture has third-party-to-member edges:
1800        //
1801        // * hrd-via-member-dev -> hrd-member-dev
1802        // * hrd-via-member-unlinked -> hrd-member-unlinked
1803        // * hrd-via-member-published -> hrd-member-published
1804        //
1805        // so the fixpoint loop reaches workspace members.
1806        let mut builder = reverse_dep_builder();
1807        let graph = builder.graph();
1808        // The fixpoint loop only runs when this is false. (This is the default,
1809        // but let's be explicit anyway.)
1810        builder.set_output_single_feature(false);
1811
1812        let member_dev_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV);
1813        let member_unlinked_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_UNLINKED);
1814        builder
1815            .add_final_excludes([&member_dev_id, &member_unlinked_id])
1816            .expect("final excludes are known to the graph");
1817        let hakari = builder.compute();
1818
1819        // Guard against the test becoming vacuous -- the fixpoint loop iterates
1820        // over the output feature sets, so ensure the corresponding packages
1821        // are present in the output map.
1822        let target_key = OutputKey {
1823            platform_idx: None,
1824            build_platform: BuildPlatform::Target,
1825        };
1826        let output_ids: BTreeSet<&PackageId> =
1827            hakari.output_map[&target_key].keys().copied().collect();
1828        for bridge_id in [
1829            METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_DEV,
1830            METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_UNLINKED,
1831        ] {
1832            assert!(
1833                output_ids.contains(&PackageId::new(bridge_id)),
1834                "{bridge_id} depends on a workspace member and is in the output \
1835                 map, so the fixpoint loop reaches that member"
1836            );
1837        }
1838
1839        for &(platform_idx, package_id) in hakari.computed_map.keys() {
1840            let package = graph.metadata(package_id).expect("package is in the graph");
1841            assert!(
1842                !package.in_workspace(),
1843                "the computed map only has third-party packages, but {} \
1844                 is a workspace member (platform_idx: {platform_idx:?})",
1845                package.name()
1846            );
1847        }
1848
1849        for (output_key, inner_map) in &hakari.output_map {
1850            for (package, _) in inner_map.values() {
1851                assert!(
1852                    !package.in_workspace(),
1853                    "the output map only has third-party packages, but {} \
1854                     is a workspace member (output key: {output_key:?})",
1855                    package.name()
1856                );
1857            }
1858        }
1859    }
1860
1861    #[test]
1862    fn hakari_reverse_deps_not_unified() {
1863        let builder = reverse_dep_builder();
1864        let graph = builder.graph();
1865        let leaf_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_LEAF);
1866        let hakari = builder.compute();
1867
1868        let expected_excludes = expected_structural_excludes();
1869        let structural_excludes: BTreeSet<PackageId> =
1870            hakari.structural_excludes().cloned().collect();
1871        assert_eq!(
1872            structural_excludes, expected_excludes,
1873            "structural excludes are exactly the third-party packages that would form a cycle"
1874        );
1875        for package_id in &expected_excludes {
1876            assert!(
1877                hakari
1878                    .is_structural_excluded(package_id)
1879                    .expect("package ID is known"),
1880                "{package_id} is structurally excluded"
1881            );
1882        }
1883        assert!(
1884            !hakari
1885                .is_structural_excluded(&leaf_id)
1886                .expect("package ID is known"),
1887            "{leaf_id} is not structurally excluded"
1888        );
1889        for package in graph.workspace().iter() {
1890            assert!(
1891                !hakari
1892                    .is_structural_excluded(package.id())
1893                    .expect("package ID is known"),
1894                "workspace package {} is not structurally excluded (workspace packages are \
1895                 handled separately)",
1896                package.name()
1897            );
1898        }
1899        assert!(
1900            hakari
1901                .is_structural_excluded(&PackageId::new("unknown-package 0.1.0"))
1902                .is_err(),
1903            "unknown package IDs are an error"
1904        );
1905
1906        let computed_ids: BTreeSet<&PackageId> = hakari
1907            .computed_map
1908            .keys()
1909            .map(|(_, package_id)| *package_id)
1910            .collect();
1911        for package_id in &expected_excludes {
1912            assert!(
1913                !computed_ids.contains(package_id),
1914                "{package_id} is structurally excluded so it is never computed: {computed_ids:?}"
1915            );
1916        }
1917        for package in graph.workspace().iter() {
1918            assert!(
1919                !computed_ids.contains(package.id()),
1920                "workspace package {} is never computed",
1921                package.name()
1922            );
1923        }
1924
1925        let target_key = OutputKey {
1926            platform_idx: None,
1927            build_platform: BuildPlatform::Target,
1928        };
1929        let host_key = OutputKey {
1930            platform_idx: None,
1931            build_platform: BuildPlatform::Host,
1932        };
1933        let output_keys: BTreeSet<OutputKey> = hakari.output_map.keys().copied().collect();
1934        assert_eq!(
1935            output_keys,
1936            [target_key, host_key].into_iter().collect(),
1937            "hrd-member-build's build-dependency on the hakari package makes \
1938             UnifyTargetHost::Auto resolve to replicate-target-on-host, so leaf's target \
1939             entry is replicated under the always/host key alongside always/target"
1940        );
1941        for key in [target_key, host_key] {
1942            let map = &hakari.output_map[&key];
1943            let output_ids: BTreeSet<&PackageId> = map.keys().copied().collect();
1944            let expected_ids: BTreeSet<&PackageId> = [&leaf_id].into_iter().collect();
1945            assert_eq!(
1946                output_ids, expected_ids,
1947                "[{key:?}] only leaf is unified: its features differ across workspace \
1948                 members, and every other third-party package would form a cycle"
1949            );
1950            assert_eq!(
1951                map[&leaf_id].1,
1952                ["feat1", "feat2"].into_iter().collect::<BTreeSet<_>>(),
1953                "[{key:?}] leaf features unified"
1954            );
1955        }
1956
1957        // The fixture's workspace-hack is up-to-date, so verify should pass.
1958        if let Err(errs) = hakari.builder().clone().verify() {
1959            panic!("verify failed for packages: {:?}", errs.dependency_ids);
1960        }
1961    }
1962
1963    #[test]
1964    fn unmanaged_members_are_not_cycle_roots() {
1965        let member_dev_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV);
1966        let member_unlinked_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_UNLINKED);
1967
1968        let mut final_excluded = reverse_dep_builder();
1969        final_excluded
1970            .add_final_excludes([&member_dev_id, &member_unlinked_id])
1971            .expect("final excludes are known to the graph");
1972        assert_unmanaged_members_unified(final_excluded, "final-excluded");
1973
1974        let mut traversal_excluded = reverse_dep_builder();
1975        traversal_excluded
1976            .add_traversal_excludes([&member_dev_id, &member_unlinked_id])
1977            .expect("traversal excludes are known to the graph");
1978        assert_unmanaged_members_unified(traversal_excluded, "traversal-excluded");
1979    }
1980
1981    fn assert_unmanaged_members_unified(builder: HakariBuilder<'_>, scenario: &str) {
1982        let via_member_published_id =
1983            PackageId::new(METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_PUBLISHED);
1984        let via_member_dev_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_DEV);
1985        let via_member_unlinked_id =
1986            PackageId::new(METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_UNLINKED);
1987        let leaf_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_LEAF);
1988        let hakari = builder.compute();
1989
1990        // member-dev is no longer managed, and its only link to the hakari
1991        // package is a dev-dependency; member-unlinked is no longer managed
1992        // and has no link to the hakari package at all.
1993        for package_id in [&via_member_dev_id, &via_member_unlinked_id] {
1994            assert!(
1995                !hakari
1996                    .is_structural_excluded(package_id)
1997                    .expect("package ID is known"),
1998                "[{scenario}] {package_id} only reaches unmanaged members, so it is \
1999                 not structurally excluded"
2000            );
2001        }
2002        // member-published is still managed, so via-member-published would
2003        // still form a cycle.
2004        assert!(
2005            hakari
2006                .is_structural_excluded(&via_member_published_id)
2007                .expect("package ID is known"),
2008            "[{scenario}] {via_member_published_id} reaches managed member hrd-member-published, so it \
2009             is structurally excluded"
2010        );
2011
2012        let target_key = OutputKey {
2013            platform_idx: None,
2014            build_platform: BuildPlatform::Target,
2015        };
2016        let target_map = &hakari.output_map[&target_key];
2017        let output_ids: BTreeSet<&PackageId> = target_map.keys().copied().collect();
2018        let expected_ids: BTreeSet<&PackageId> =
2019            [&leaf_id, &via_member_dev_id, &via_member_unlinked_id]
2020                .into_iter()
2021                .collect();
2022        assert_eq!(
2023            output_ids, expected_ids,
2024            "[{scenario}] via-member-dev and via-member-unlinked are unified now that they can't \
2025             form a cycle"
2026        );
2027    }
2028
2029    #[test]
2030    fn hakari_reverse_deps_verify_failure() {
2031        let fixture = JsonFixture::metadata_hakari_reverse_dep();
2032        let hakari_id = fixture
2033            .details()
2034            .hakari_package()
2035            .expect("fixture declares a hakari package");
2036
2037        // Make the fixture's workspace-hack stale by dropping the features it
2038        // requests from hrd-leaf. This will make verify fail for hrd-leaf.
2039        let leaf_dep_unified = r#""name":"hrd-leaf","source":null,"req":"*","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":["feat1","feat2"]"#;
2040        let leaf_dep_stale = leaf_dep_unified.replace(r#"["feat1","feat2"]"#, "[]");
2041        let json = fixture.json();
2042        assert_eq!(
2043            json.matches(leaf_dep_unified).count(),
2044            1,
2045            "hakari package's dependency on hrd-leaf occurs exactly once"
2046        );
2047        let stale_json = json.replace(leaf_dep_unified, &leaf_dep_stale);
2048        let graph = PackageGraph::from_json(stale_json).expect("stale fixture parsed");
2049
2050        let builder = HakariBuilder::new(&graph, Some(hakari_id)).expect("builder created");
2051        let structural_excludes: BTreeSet<PackageId> = builder
2052            .clone()
2053            .compute()
2054            .structural_excludes()
2055            .cloned()
2056            .collect();
2057        let errs = builder
2058            .verify()
2059            .expect_err("stale hakari package fails verification");
2060
2061        let leaf_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_LEAF);
2062        assert_eq!(
2063            errs.dependency_ids,
2064            [&leaf_id].into_iter().collect::<BTreeSet<_>>(),
2065            "only hrd-leaf is reported: the structurally excluded packages are \
2066             built with more than one feature set too, but verify skips them"
2067        );
2068        assert_eq!(
2069            structural_excludes,
2070            expected_structural_excludes(),
2071            "the cycle-forming packages really are structurally excluded here, so the check \
2072             above isn't tautological"
2073        );
2074    }
2075
2076    // The third-party packages in the metadata_hakari_reverse_dep fixture that
2077    // reach the hakari package or a managed member. See the fixture's
2078    // definition for the graph and why each one counts.
2079    fn expected_structural_excludes() -> BTreeSet<PackageId> {
2080        [
2081            METADATA_HAKARI_REVERSE_DEP_NORMAL_ON_HACK,
2082            METADATA_HAKARI_REVERSE_DEP_VIA_NORMAL_ON_HACK,
2083            METADATA_HAKARI_REVERSE_DEP_BUILD_ON_HACK,
2084            METADATA_HAKARI_REVERSE_DEP_CFG_ON_HACK,
2085            METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_PUBLISHED,
2086            METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_DEV,
2087            METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_UNLINKED,
2088        ]
2089        .into_iter()
2090        .map(PackageId::new)
2091        .collect()
2092    }
2093
2094    fn reverse_dep_builder() -> HakariBuilder<'static> {
2095        let fixture = JsonFixture::metadata_hakari_reverse_dep();
2096        let hakari_id = fixture
2097            .details()
2098            .hakari_package()
2099            .expect("hakari-reverse-dep fixture names a hakari package");
2100        HakariBuilder::new(fixture.graph(), Some(hakari_id)).expect("hakari builder is created")
2101    }
2102}