1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Copyright (c) The Diem Core Contributors
// SPDX-License-Identifier: Apache-2.0

use crate::{core_config::SubsetConfig, Result, SystemError};
use camino::{Utf8Path, Utf8PathBuf};
use guppy::{
    graph::{
        cargo::{CargoOptions, CargoResolverVersion, CargoSet},
        feature::{FeatureFilter, FeatureSet, StandardFeatures},
        DependencyDirection, PackageGraph, PackageMetadata, PackageSet,
    },
    PackageId,
};
use serde::Deserialize;
use std::{collections::BTreeMap, fs};
use toml::de;

/// Contains information about all the subsets specified in this workspace.
#[derive(Clone, Debug)]
pub struct WorkspaceSubsets<'g> {
    // TODO: default members should become a subset in x.toml
    default_members: WorkspaceSubset<'g>,
    subsets: BTreeMap<String, WorkspaceSubset<'g>>,
}

impl<'g> WorkspaceSubsets<'g> {
    /// Constructs a new store for workspace subsets.
    ///
    /// This is done with respect to a "standard build", which assumes:
    /// * any platform
    /// * v2 resolver
    /// * no dev dependencies
    pub fn new(
        graph: &'g PackageGraph,
        project_root: &Utf8Path,
        config: &BTreeMap<String, SubsetConfig>,
    ) -> Result<Self> {
        let mut cargo_opts = CargoOptions::new();
        cargo_opts
            .set_version(CargoResolverVersion::V2)
            .set_include_dev(false);

        let default_members = Self::read_default_members(project_root)?;

        // Look up default members by path.
        let initial_packages = graph
            .resolve_workspace_paths(&default_members)
            .map_err(|err| SystemError::guppy("querying default members", err))?;
        let default_members =
            WorkspaceSubset::new(&initial_packages, StandardFeatures::Default, &cargo_opts);

        // For each of the subset configs, look up the packages by name.
        let subsets = config
            .iter()
            .map(|(name, config)| {
                let initial_packages = graph
                    .resolve_workspace_names(&config.root_members)
                    .map_err(|err| {
                        SystemError::guppy(format!("querying members for subset '{}'", name), err)
                    })?;
                let subset =
                    WorkspaceSubset::new(&initial_packages, StandardFeatures::Default, &cargo_opts);
                Ok((name.clone(), subset))
            })
            .collect::<Result<_, _>>()?;

        Ok(Self {
            default_members,
            subsets,
        })
    }

    /// Returns information about default members.
    pub fn default_members(&self) -> &WorkspaceSubset<'g> {
        &self.default_members
    }

    /// Returns information about the subset by name.
    pub fn get(&self, name: impl AsRef<str>) -> Option<&WorkspaceSubset<'g>> {
        self.subsets.get(name.as_ref())
    }

    /// Iterate over all named subsets.
    pub fn iter<'a>(&'a self) -> impl Iterator<Item = (&'a str, &'a WorkspaceSubset<'g>)> + 'a {
        self.subsets
            .iter()
            .map(|(name, subset)| (name.as_str(), subset))
    }

    // ---
    // Helper methods
    // ---

    fn read_default_members(project_root: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
        #[derive(Deserialize)]
        struct RootToml {
            workspace: Workspace,
        }

        #[derive(Deserialize)]
        struct Workspace {
            #[serde(rename = "default-members")]
            default_members: Vec<Utf8PathBuf>,
        }

        let root_toml = project_root.join("Cargo.toml");
        let contents =
            fs::read(&root_toml).map_err(|err| SystemError::io("reading root Cargo.toml", err))?;
        let contents: RootToml = de::from_slice(&contents)
            .map_err(|err| SystemError::de("deserializing root Cargo.toml", err))?;
        Ok(contents.workspace.default_members)
    }
}

/// Information collected about a subset of members of a workspace.
///
/// Some subsets of this workspace have special properties that are enforced through linters.
#[derive(Clone, Debug)]
pub struct WorkspaceSubset<'g> {
    build_set: CargoSet<'g>,
    unified_set: FeatureSet<'g>,
}

impl<'g> WorkspaceSubset<'g> {
    /// Creates a new subset by simulating a Cargo build on the specified workspace paths, with
    /// the given feature filter.
    pub fn new<'a>(
        initial_packages: &PackageSet<'g>,
        feature_filter: impl FeatureFilter<'g>,
        cargo_opts: &CargoOptions<'_>,
    ) -> Self {
        // Use the Cargo resolver to figure out which packages will be included.
        let build_set = initial_packages
            .to_feature_set(feature_filter)
            .into_cargo_set(cargo_opts)
            .expect("into_cargo_set should always succeed");
        let unified_set = build_set.host_features().union(build_set.target_features());

        Self {
            build_set,
            unified_set,
        }
    }

    /// Returns the initial members that this subset was constructed from.
    pub fn initials(&self) -> &FeatureSet<'g> {
        self.build_set.initials()
    }

    /// Returns the status of the given package ID in the subset.
    pub fn status_of(&self, package_id: &PackageId) -> WorkspaceStatus {
        if self
            .build_set
            .initials()
            .contains_package(package_id)
            .unwrap_or(false)
        {
            WorkspaceStatus::RootMember
        } else if self
            .unified_set
            .features_for(package_id)
            .unwrap_or(None)
            .is_some()
        {
            WorkspaceStatus::Dependency
        } else {
            WorkspaceStatus::Absent
        }
    }

    /// Returns a list of root packages in this subset, ignoring transitive dependencies.
    pub fn root_members<'a>(&'a self) -> impl Iterator<Item = PackageMetadata<'g>> + 'a {
        self.build_set
            .initials()
            .packages_with_features(DependencyDirection::Forward)
            .map(|f| *f.package())
    }

    /// Returns the set of packages and features that would be built from this subset.
    ///
    /// This contains information about transitive dependencies, both within the workspace and
    /// outside it.
    pub fn build_set(&self) -> &CargoSet<'g> {
        &self.build_set
    }
}

/// The status of a particular package ID in a `WorkspaceSubset`.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum WorkspaceStatus {
    /// This package ID is a root member of the workspace subset.
    RootMember,
    /// This package ID is a dependency of the workspace subset, but not a root member.
    Dependency,
    /// This package ID is not a dependency of the workspace subset.
    Absent,
}