cargo/ops/cargo_compile/
packages.rs

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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//! See [`Packages`].

use std::collections::BTreeSet;

use crate::core::{Package, PackageIdSpecQuery};
use crate::core::{PackageIdSpec, Workspace};
use crate::util::restricted_names::is_glob_pattern;
use crate::util::CargoResult;

use anyhow::{bail, Context as _};

/// Represents the selected packages that will be built.
///
/// Generally, it represents the combination of all `-p` flag. When working within
/// a workspace, `--exclude` and `--workspace` flags also contribute to it.
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum Packages {
    /// Packages selected by default. Usually means no flag provided.
    Default,
    /// Opt in all packages.
    ///
    /// As of the time of this writing, it only works on opting in all workspace members.
    All,
    /// Opt out of packages passed in.
    ///
    /// As of the time of this writing, it only works on opting out workspace members.
    OptOut(Vec<String>),
    /// A sequence of hand-picked packages that will be built. Normally done by `-p` flag.
    Packages(Vec<String>),
}

impl Packages {
    /// Creates a `Packages` from flags which are generally equivalent to command line flags.
    pub fn from_flags(all: bool, exclude: Vec<String>, package: Vec<String>) -> CargoResult<Self> {
        Ok(match (all, exclude.len(), package.len()) {
            (false, 0, 0) => Packages::Default,
            (false, 0, _) => Packages::Packages(package),
            (false, _, _) => anyhow::bail!("--exclude can only be used together with --workspace"),
            (true, 0, _) => Packages::All,
            (true, _, _) => Packages::OptOut(exclude),
        })
    }

    /// Converts selected packages to [`PackageIdSpec`]s.
    pub fn to_package_id_specs(&self, ws: &Workspace<'_>) -> CargoResult<Vec<PackageIdSpec>> {
        let specs = match self {
            Packages::All => ws
                .members()
                .map(Package::package_id)
                .map(|id| id.to_spec())
                .collect(),
            Packages::OptOut(opt_out) => {
                let (mut patterns, mut ids) = opt_patterns_and_ids(opt_out)?;
                let specs = ws
                    .members()
                    .filter(|pkg| {
                        let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
                        if let Some(id) = &id {
                            ids.remove(id);
                        }
                        !id.is_some() && !match_patterns(pkg, &mut patterns)
                    })
                    .map(Package::package_id)
                    .map(|id| id.to_spec())
                    .collect();
                let warn = |e| ws.gctx().shell().warn(e);
                let names = ids
                    .into_iter()
                    .map(|id| id.to_string())
                    .collect::<BTreeSet<_>>();
                emit_package_not_found(ws, names, true).or_else(warn)?;
                emit_pattern_not_found(ws, patterns, true).or_else(warn)?;
                specs
            }
            Packages::Packages(packages) if packages.is_empty() => {
                vec![ws.current()?.package_id().to_spec()]
            }
            Packages::Packages(opt_in) => {
                let (mut patterns, mut specs) = opt_patterns_and_ids(opt_in)?;
                if !patterns.is_empty() {
                    let matched_pkgs = ws
                        .members()
                        .filter(|pkg| match_patterns(pkg, &mut patterns))
                        .map(Package::package_id)
                        .map(|id| id.to_spec());
                    specs.extend(matched_pkgs);
                }
                emit_pattern_not_found(ws, patterns, false)?;
                specs.into_iter().collect()
            }
            Packages::Default => ws
                .default_members()
                .map(Package::package_id)
                .map(|id| id.to_spec())
                .collect(),
        };
        if specs.is_empty() {
            if ws.is_virtual() {
                bail!(
                    "manifest path `{}` contains no package: The manifest is virtual, \
                     and the workspace has no members.",
                    ws.root().display()
                )
            }
            bail!("no packages to compile")
        }
        Ok(specs)
    }

    /// Gets a list of selected [`Package`]s.
    pub fn get_packages<'ws>(&self, ws: &'ws Workspace<'_>) -> CargoResult<Vec<&'ws Package>> {
        let packages: Vec<_> = match self {
            Packages::Default => ws.default_members().collect(),
            Packages::All => ws.members().collect(),
            Packages::OptOut(opt_out) => {
                let (mut patterns, mut ids) = opt_patterns_and_ids(opt_out)?;
                let packages = ws
                    .members()
                    .filter(|pkg| {
                        let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
                        if let Some(id) = &id {
                            ids.remove(id);
                        }
                        !id.is_some() && !match_patterns(pkg, &mut patterns)
                    })
                    .collect();
                let names = ids
                    .into_iter()
                    .map(|id| id.to_string())
                    .collect::<BTreeSet<_>>();
                emit_package_not_found(ws, names, true)?;
                emit_pattern_not_found(ws, patterns, true)?;
                packages
            }
            Packages::Packages(opt_in) => {
                let (mut patterns, mut ids) = opt_patterns_and_ids(opt_in)?;
                let packages = ws
                    .members()
                    .filter(|pkg| {
                        let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
                        if let Some(id) = &id {
                            ids.remove(id);
                        }
                        id.is_some() || match_patterns(pkg, &mut patterns)
                    })
                    .collect();
                let names = ids
                    .into_iter()
                    .map(|id| id.to_string())
                    .collect::<BTreeSet<_>>();
                emit_package_not_found(ws, names, false)?;
                emit_pattern_not_found(ws, patterns, false)?;
                packages
            }
        };
        Ok(packages)
    }

    /// Returns whether or not the user needs to pass a `-p` flag to target a
    /// specific package in the workspace.
    pub fn needs_spec_flag(&self, ws: &Workspace<'_>) -> bool {
        match self {
            Packages::Default => ws.default_members().count() > 1,
            Packages::All => ws.members().count() > 1,
            Packages::Packages(_) => true,
            Packages::OptOut(_) => true,
        }
    }
}

/// Emits "package not found" error.
fn emit_package_not_found(
    ws: &Workspace<'_>,
    opt_names: BTreeSet<String>,
    opt_out: bool,
) -> CargoResult<()> {
    if !opt_names.is_empty() {
        anyhow::bail!(
            "{}package(s) `{}` not found in workspace `{}`",
            if opt_out { "excluded " } else { "" },
            opt_names.into_iter().collect::<Vec<_>>().join(", "),
            ws.root().display(),
        )
    }
    Ok(())
}

/// Emits "glob pattern not found" error.
fn emit_pattern_not_found(
    ws: &Workspace<'_>,
    opt_patterns: Vec<(glob::Pattern, bool)>,
    opt_out: bool,
) -> CargoResult<()> {
    let not_matched = opt_patterns
        .iter()
        .filter(|(_, matched)| !*matched)
        .map(|(pat, _)| pat.as_str())
        .collect::<Vec<_>>();
    if !not_matched.is_empty() {
        anyhow::bail!(
            "{}package pattern(s) `{}` not found in workspace `{}`",
            if opt_out { "excluded " } else { "" },
            not_matched.join(", "),
            ws.root().display(),
        )
    }
    Ok(())
}

/// Given a list opt-in or opt-out package selection strings, generates two
/// collections that represent glob patterns and package id specs respectively.
fn opt_patterns_and_ids(
    opt: &[String],
) -> CargoResult<(Vec<(glob::Pattern, bool)>, BTreeSet<PackageIdSpec>)> {
    let mut opt_patterns = Vec::new();
    let mut opt_ids = BTreeSet::new();
    for x in opt.iter() {
        match PackageIdSpec::parse(x) {
            Ok(spec) => {
                opt_ids.insert(spec);
            }
            Err(_) if is_glob_pattern(x) => opt_patterns.push((build_glob(x)?, false)),
            Err(e) => return Err(e.into()),
        }
    }
    Ok((opt_patterns, opt_ids))
}

/// Checks whether a package matches any of a list of glob patterns generated
/// from `opt_patterns_and_names`.
fn match_patterns(pkg: &Package, patterns: &mut Vec<(glob::Pattern, bool)>) -> bool {
    patterns.iter_mut().any(|(m, matched)| {
        let is_matched = m.matches(pkg.name().as_str());
        *matched |= is_matched;
        is_matched
    })
}

/// Build [`glob::Pattern`] with informative context.
pub fn build_glob(pat: &str) -> CargoResult<glob::Pattern> {
    glob::Pattern::new(pat).with_context(|| format!("cannot build glob pattern from `{}`", pat))
}