x_core/
workspace_subset.rs

1// Copyright (c) The Diem Core Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{core_config::SubsetConfig, Result, SystemError};
5use camino::Utf8PathBuf;
6use guppy::{
7    graph::{
8        cargo::{CargoOptions, CargoResolverVersion, CargoSet},
9        feature::{FeatureFilter, FeatureSet, StandardFeatures},
10        DependencyDirection, PackageGraph, PackageMetadata, PackageSet,
11    },
12    PackageId,
13};
14use serde::Deserialize;
15use std::{collections::BTreeMap, fs, path::Path};
16use toml::de;
17
18/// Contains information about all the subsets specified in this workspace.
19#[derive(Clone, Debug)]
20pub struct WorkspaceSubsets<'g> {
21    // TODO: default members should become a subset in x.toml
22    default_members: WorkspaceSubset<'g>,
23    subsets: BTreeMap<String, WorkspaceSubset<'g>>,
24}
25
26impl<'g> WorkspaceSubsets<'g> {
27    /// Constructs a new store for workspace subsets.
28    ///
29    /// This is done with respect to a "standard build", which assumes:
30    /// * any platform
31    /// * v2 resolver
32    /// * no dev dependencies
33    pub fn new(
34        graph: &'g PackageGraph,
35        project_root: &Path,
36        config: &BTreeMap<String, SubsetConfig>,
37    ) -> Result<Self> {
38        let mut cargo_opts = CargoOptions::new();
39        cargo_opts
40            .set_version(CargoResolverVersion::V2)
41            .set_include_dev(false);
42
43        let default_members = Self::read_default_members(project_root)?;
44
45        // Look up default members by path.
46        let initial_packages = graph
47            .resolve_workspace_paths(&default_members)
48            .map_err(|err| SystemError::guppy("querying default members", err))?;
49        let default_members =
50            WorkspaceSubset::new(&initial_packages, StandardFeatures::Default, &cargo_opts);
51
52        // For each of the subset configs, look up the packages by name.
53        let subsets = config
54            .iter()
55            .map(|(name, config)| {
56                let initial_packages = graph
57                    .resolve_workspace_names(&config.root_members)
58                    .map_err(|err| {
59                        SystemError::guppy(format!("querying members for subset '{}'", name), err)
60                    })?;
61                let subset =
62                    WorkspaceSubset::new(&initial_packages, StandardFeatures::Default, &cargo_opts);
63                Ok((name.clone(), subset))
64            })
65            .collect::<Result<_, _>>()?;
66
67        Ok(Self {
68            default_members,
69            subsets,
70        })
71    }
72
73    /// Returns information about default members.
74    pub fn default_members(&self) -> &WorkspaceSubset<'g> {
75        &self.default_members
76    }
77
78    /// Returns information about the subset by name.
79    pub fn get(&self, name: impl AsRef<str>) -> Option<&WorkspaceSubset<'g>> {
80        self.subsets.get(name.as_ref())
81    }
82
83    /// Iterate over all named subsets.
84    pub fn iter<'a>(&'a self) -> impl Iterator<Item = (&'a str, &'a WorkspaceSubset<'g>)> + 'a {
85        self.subsets
86            .iter()
87            .map(|(name, subset)| (name.as_str(), subset))
88    }
89
90    // ---
91    // Helper methods
92    // ---
93
94    fn read_default_members(project_root: &Path) -> Result<Vec<Utf8PathBuf>> {
95        #[derive(Deserialize)]
96        struct RootToml {
97            workspace: Workspace,
98        }
99
100        #[derive(Deserialize)]
101        struct Workspace {
102            #[serde(rename = "default-members")]
103            default_members: Vec<Utf8PathBuf>,
104        }
105
106        let root_toml = project_root.join("Cargo.toml");
107        let contents =
108            fs::read(&root_toml).map_err(|err| SystemError::io("reading root Cargo.toml", err))?;
109        let contents: RootToml = de::from_slice(&contents)
110            .map_err(|err| SystemError::de("deserializing root Cargo.toml", err))?;
111        Ok(contents.workspace.default_members)
112    }
113}
114
115/// Information collected about a subset of members of a workspace.
116///
117/// Some subsets of this workspace have special properties that are enforced through linters.
118#[derive(Clone, Debug)]
119pub struct WorkspaceSubset<'g> {
120    build_set: CargoSet<'g>,
121    unified_set: FeatureSet<'g>,
122}
123
124impl<'g> WorkspaceSubset<'g> {
125    /// Creates a new subset by simulating a Cargo build on the specified workspace paths, with
126    /// the given feature filter.
127    pub fn new<'a>(
128        initial_packages: &PackageSet<'g>,
129        feature_filter: impl FeatureFilter<'g>,
130        cargo_opts: &CargoOptions<'_>,
131    ) -> Self {
132        // Use the Cargo resolver to figure out which packages will be included.
133        let build_set = initial_packages
134            .to_feature_set(feature_filter)
135            .into_cargo_set(cargo_opts)
136            .expect("into_cargo_set should always succeed");
137        let unified_set = build_set.host_features().union(build_set.target_features());
138
139        Self {
140            build_set,
141            unified_set,
142        }
143    }
144
145    /// Returns the initial members that this subset was constructed from.
146    pub fn initials(&self) -> &FeatureSet<'g> {
147        self.build_set.initials()
148    }
149
150    /// Returns the status of the given package ID in the subset.
151    pub fn status_of(&self, package_id: &PackageId) -> WorkspaceStatus {
152        if self
153            .build_set
154            .initials()
155            .contains_package(package_id)
156            .unwrap_or(false)
157        {
158            WorkspaceStatus::RootMember
159        } else if self
160            .unified_set
161            .features_for(package_id)
162            .unwrap_or(None)
163            .is_some()
164        {
165            WorkspaceStatus::Dependency
166        } else {
167            WorkspaceStatus::Absent
168        }
169    }
170
171    /// Returns a list of root packages in this subset, ignoring transitive dependencies.
172    pub fn root_members<'a>(&'a self) -> impl Iterator<Item = PackageMetadata<'g>> + 'a {
173        self.build_set
174            .initials()
175            .packages_with_features(DependencyDirection::Forward)
176            .map(|f| *f.package())
177    }
178
179    /// Returns the set of packages and features that would be built from this subset.
180    ///
181    /// This contains information about transitive dependencies, both within the workspace and
182    /// outside it.
183    pub fn build_set(&self) -> &CargoSet<'g> {
184        &self.build_set
185    }
186}
187
188/// The status of a particular package ID in a `WorkspaceSubset`.
189#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
190pub enum WorkspaceStatus {
191    /// This package ID is a root member of the workspace subset.
192    RootMember,
193    /// This package ID is a dependency of the workspace subset, but not a root member.
194    Dependency,
195    /// This package ID is not a dependency of the workspace subset.
196    Absent,
197}