hakari/explain/
mod.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Information about why a dependency is in the workspace-hack.
5//!
6//! [`HakariExplain`] instances are produced by [`Hakari::explain`]. The current API is limited
7//! to displaying these instances if the `cli-support` feature is enabled.
8
9#[cfg(feature = "cli-support")]
10mod display;
11mod simplify;
12
13#[cfg(feature = "cli-support")]
14pub use display::HakariExplainDisplay;
15
16use crate::{explain::simplify::*, Hakari};
17use guppy::{
18    graph::{cargo::BuildPlatform, feature::StandardFeatures, PackageGraph, PackageMetadata},
19    PackageId,
20};
21use std::{
22    collections::{BTreeMap, BTreeSet},
23    sync::Arc,
24};
25use target_spec::Platform;
26
27/// The result of a Hakari explain query.
28///
29/// Generated by [`Hakari::explain`].
30#[derive(Clone, Debug)]
31pub struct HakariExplain<'g, 'a> {
32    #[cfg_attr(not(feature = "cli-support"), allow(dead_code))]
33    graph: &'g PackageGraph,
34    metadata: PackageMetadata<'g>,
35    #[cfg_attr(not(feature = "cli-support"), allow(dead_code))]
36    platforms: &'a [Arc<Platform>],
37    target_map: ExplainMap<'g, 'a>,
38    host_map: ExplainMap<'g, 'a>,
39}
40
41type ExplainMap<'g, 'a> = BTreeMap<&'a BTreeSet<&'g str>, ExplainInner<'g>>;
42
43#[derive(Clone, Debug, Default)]
44struct ExplainInner<'g> {
45    #[cfg_attr(not(feature = "cli-support"), allow(dead_code))]
46    workspace_packages: BTreeMap<&'g PackageId, ExplainInnerValue<'g>>,
47    #[cfg_attr(not(feature = "cli-support"), allow(dead_code))]
48    fixup_platforms: Vec<Simple<Option<usize>>>,
49}
50
51#[derive(Clone, Debug)]
52#[allow(clippy::type_complexity)]
53struct ExplainInnerValue<'g> {
54    #[cfg_attr(not(feature = "cli-support"), allow(dead_code))]
55    metadata: PackageMetadata<'g>,
56    #[cfg_attr(not(feature = "cli-support"), allow(dead_code))]
57    sets: Vec<(
58        Simple<bool>,
59        Simple<StandardFeatures>,
60        Simple<Option<usize>>,
61    )>,
62}
63
64impl<'g, 'a> HakariExplain<'g, 'a> {
65    pub(crate) fn new(hakari: &'a Hakari<'g>, dep_id: &PackageId) -> Result<Self, guppy::Error> {
66        let graph = hakari.builder.graph();
67        let metadata = hakari.builder.graph().metadata(dep_id)?;
68        let intermediate = ExplainIntermediate::new(hakari, metadata.id())?;
69
70        let target_map = Self::simplify_map(hakari, intermediate.target_map);
71        let host_map = Self::simplify_map(hakari, intermediate.host_map);
72
73        Ok(Self {
74            graph,
75            metadata,
76            platforms: &hakari.builder.platforms,
77            target_map,
78            host_map,
79        })
80    }
81
82    fn simplify_map(hakari: &'a Hakari<'g>, map: IntermediateMap<'g, 'a>) -> ExplainMap<'g, 'a> {
83        const STANDARD_FEATURES_COUNT: usize = 3;
84        const INCLUDE_DEV_COUNT: usize = 2;
85        // +1 for the None case
86        let platform_count = hakari.builder.platforms.len() + 1;
87
88        map.into_iter()
89            .map(|(features, inner)| {
90                let workspace_packages = inner
91                    .workspace_packages
92                    .into_iter()
93                    .map(|(package_id, IntermediateInnerValue { metadata, sets })| {
94                        let sets = simplify3(
95                            &sets,
96                            (INCLUDE_DEV_COUNT, STANDARD_FEATURES_COUNT, platform_count),
97                        );
98                        (package_id, ExplainInnerValue { metadata, sets })
99                    })
100                    .collect();
101                let fixup_platforms = simplify1(&inner.fixup_platforms, platform_count);
102                (
103                    features,
104                    ExplainInner {
105                        workspace_packages,
106                        fixup_platforms,
107                    },
108                )
109            })
110            .collect()
111    }
112
113    /// Returns [`PackageMetadata`] for the dependency associated with this `HakariExplain`
114    /// instance.
115    pub fn dependency(&self) -> PackageMetadata<'g> {
116        self.metadata
117    }
118
119    /// Returns a displayer for the output.
120    #[cfg(feature = "cli-support")]
121    pub fn display<'explain>(&'explain self) -> HakariExplainDisplay<'g, 'a, 'explain> {
122        HakariExplainDisplay::new(self)
123    }
124
125    // Used by the display module.
126    #[allow(dead_code)]
127    fn explain_maps(&self) -> [(BuildPlatform, &ExplainMap<'g, 'a>); 2] {
128        [
129            (BuildPlatform::Target, &self.target_map),
130            (BuildPlatform::Host, &self.host_map),
131        ]
132    }
133}
134
135/// Pre-simplification map.
136#[derive(Debug)]
137struct ExplainIntermediate<'g, 'a> {
138    target_map: IntermediateMap<'g, 'a>,
139    host_map: IntermediateMap<'g, 'a>,
140}
141
142type IntermediateMap<'g, 'a> = BTreeMap<&'a BTreeSet<&'g str>, IntermediateInner<'g>>;
143
144#[derive(Debug, Default)]
145struct IntermediateInner<'g> {
146    workspace_packages: BTreeMap<&'g PackageId, IntermediateInnerValue<'g>>,
147    fixup_platforms: BTreeSet<Option<usize>>,
148}
149
150#[derive(Debug)]
151struct IntermediateInnerValue<'g> {
152    metadata: PackageMetadata<'g>,
153    sets: BTreeSet<(bool, StandardFeatures, Option<usize>)>,
154}
155
156impl<'g, 'a> ExplainIntermediate<'g, 'a> {
157    fn new(hakari: &'a Hakari<'g>, dep_id: &'g PackageId) -> Result<Self, guppy::Error> {
158        let mut target_map: IntermediateMap<'g, 'a> = BTreeMap::new();
159        let mut host_map: IntermediateMap<'g, 'a> = BTreeMap::new();
160
161        // Look at the computed map to figure out which packages are built.
162        let platform_idxs =
163            std::iter::once(None).chain((0..=hakari.builder.platforms.len()).map(Some));
164        let map_keys = platform_idxs.map(|idx| (idx, dep_id));
165
166        for (platform, package_id) in map_keys {
167            let computed_value = match hakari.computed_map.get(&(platform, package_id)) {
168                Some(v) => v,
169                None => continue,
170            };
171
172            for (build_platform, inner_map) in computed_value.inner_maps() {
173                let map = match build_platform {
174                    BuildPlatform::Target => &mut target_map,
175                    BuildPlatform::Host => &mut host_map,
176                };
177
178                for (features, inner_value) in inner_map {
179                    for &(workspace_package, standard_features, include_dev) in
180                        &inner_value.workspace_packages
181                    {
182                        map.entry(features)
183                            .or_default()
184                            .workspace_packages
185                            .entry(workspace_package.id())
186                            .or_insert_with(|| IntermediateInnerValue {
187                                metadata: workspace_package,
188                                sets: BTreeSet::new(),
189                            })
190                            .sets
191                            .insert((include_dev, standard_features, platform));
192                    }
193
194                    if inner_value.fixed_up {
195                        map.entry(features)
196                            .or_default()
197                            .fixup_platforms
198                            .insert(platform);
199                    }
200                }
201            }
202        }
203
204        if target_map.is_empty() && host_map.is_empty() {
205            return Err(guppy::Error::UnknownPackageId(dep_id.clone()));
206        }
207
208        Ok(Self {
209            target_map,
210            host_map,
211        })
212    }
213}