cargo/ops/cargo_compile/
packages.rs

1//! See [`Packages`].
2
3use std::collections::BTreeSet;
4
5use crate::core::{Package, PackageIdSpecQuery};
6use crate::core::{PackageIdSpec, Workspace};
7use crate::util::restricted_names::is_glob_pattern;
8use crate::util::CargoResult;
9
10use anyhow::{bail, Context as _};
11
12/// Represents the selected packages that will be built.
13///
14/// Generally, it represents the combination of all `-p` flag. When working within
15/// a workspace, `--exclude` and `--workspace` flags also contribute to it.
16#[derive(PartialEq, Eq, Debug, Clone)]
17pub enum Packages {
18    /// Packages selected by default. Usually means no flag provided.
19    Default,
20    /// Opt in all packages.
21    ///
22    /// As of the time of this writing, it only works on opting in all workspace members.
23    /// Keeps the packages passed in to verify that they exist in the workspace.
24    All(Vec<String>),
25    /// Opt out of packages passed in.
26    ///
27    /// As of the time of this writing, it only works on opting out workspace members.
28    OptOut(Vec<String>),
29    /// A sequence of hand-picked packages that will be built. Normally done by `-p` flag.
30    Packages(Vec<String>),
31}
32
33impl Packages {
34    /// Creates a `Packages` from flags which are generally equivalent to command line flags.
35    pub fn from_flags(all: bool, exclude: Vec<String>, package: Vec<String>) -> CargoResult<Self> {
36        Ok(match (all, exclude.len(), package.len()) {
37            (false, 0, 0) => Packages::Default,
38            (false, 0, _) => Packages::Packages(package),
39            (false, _, _) => anyhow::bail!("--exclude can only be used together with --workspace"),
40            (true, 0, _) => Packages::All(package),
41            (true, _, _) => Packages::OptOut(exclude),
42        })
43    }
44
45    /// Converts selected packages to [`PackageIdSpec`]s.
46    pub fn to_package_id_specs(&self, ws: &Workspace<'_>) -> CargoResult<Vec<PackageIdSpec>> {
47        let specs = match self {
48            Packages::All(packages) => {
49                emit_packages_not_found_within_workspace(ws, packages)?;
50                ws.members()
51                    .map(Package::package_id)
52                    .map(|id| id.to_spec())
53                    .collect()
54            }
55            Packages::OptOut(opt_out) => {
56                let (mut patterns, mut ids) = opt_patterns_and_ids(opt_out)?;
57                let specs = ws
58                    .members()
59                    .filter(|pkg| {
60                        let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
61                        if let Some(id) = &id {
62                            ids.remove(id);
63                        }
64                        !id.is_some() && !match_patterns(pkg, &mut patterns)
65                    })
66                    .map(Package::package_id)
67                    .map(|id| id.to_spec())
68                    .collect();
69                let warn = |e| ws.gctx().shell().warn(e);
70                let names = ids
71                    .into_iter()
72                    .map(|id| id.to_string())
73                    .collect::<BTreeSet<_>>();
74                emit_package_not_found(ws, names, true).or_else(warn)?;
75                emit_pattern_not_found(ws, patterns, true).or_else(warn)?;
76                specs
77            }
78            Packages::Packages(packages) if packages.is_empty() => {
79                vec![ws.current()?.package_id().to_spec()]
80            }
81            Packages::Packages(opt_in) => {
82                let (mut patterns, mut specs) = opt_patterns_and_ids(opt_in)?;
83                if !patterns.is_empty() {
84                    let matched_pkgs = ws
85                        .members()
86                        .filter(|pkg| match_patterns(pkg, &mut patterns))
87                        .map(Package::package_id)
88                        .map(|id| id.to_spec());
89                    specs.extend(matched_pkgs);
90                }
91                emit_pattern_not_found(ws, patterns, false)?;
92                specs.into_iter().collect()
93            }
94            Packages::Default => ws
95                .default_members()
96                .map(Package::package_id)
97                .map(|id| id.to_spec())
98                .collect(),
99        };
100        if specs.is_empty() {
101            if ws.is_virtual() {
102                bail!(
103                    "manifest path `{}` contains no package: The manifest is virtual, \
104                     and the workspace has no members.",
105                    ws.root().display()
106                )
107            }
108            bail!("no packages to compile")
109        }
110        Ok(specs)
111    }
112
113    /// Gets a list of selected [`Package`]s.
114    pub fn get_packages<'ws>(&self, ws: &'ws Workspace<'_>) -> CargoResult<Vec<&'ws Package>> {
115        let packages: Vec<_> = match self {
116            Packages::Default => ws.default_members().collect(),
117            Packages::All(packages) => {
118                emit_packages_not_found_within_workspace(ws, packages)?;
119                ws.members().collect()
120            }
121            Packages::OptOut(opt_out) => {
122                let (mut patterns, mut ids) = opt_patterns_and_ids(opt_out)?;
123                let packages = ws
124                    .members()
125                    .filter(|pkg| {
126                        let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
127                        if let Some(id) = &id {
128                            ids.remove(id);
129                        }
130                        !id.is_some() && !match_patterns(pkg, &mut patterns)
131                    })
132                    .collect();
133                let names = ids
134                    .into_iter()
135                    .map(|id| id.to_string())
136                    .collect::<BTreeSet<_>>();
137                emit_package_not_found(ws, names, true)?;
138                emit_pattern_not_found(ws, patterns, true)?;
139                packages
140            }
141            Packages::Packages(opt_in) => {
142                let (mut patterns, mut ids) = opt_patterns_and_ids(opt_in)?;
143                let packages = ws
144                    .members()
145                    .filter(|pkg| {
146                        let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
147                        if let Some(id) = &id {
148                            ids.remove(id);
149                        }
150                        id.is_some() || match_patterns(pkg, &mut patterns)
151                    })
152                    .collect();
153                let names = ids
154                    .into_iter()
155                    .map(|id| id.to_string())
156                    .collect::<BTreeSet<_>>();
157                emit_package_not_found(ws, names, false)?;
158                emit_pattern_not_found(ws, patterns, false)?;
159                packages
160            }
161        };
162        Ok(packages)
163    }
164
165    /// Returns whether or not the user needs to pass a `-p` flag to target a
166    /// specific package in the workspace.
167    pub fn needs_spec_flag(&self, ws: &Workspace<'_>) -> bool {
168        match self {
169            Packages::Default => ws.default_members().count() > 1,
170            Packages::All(_) => ws.members().count() > 1,
171            Packages::Packages(_) => true,
172            Packages::OptOut(_) => true,
173        }
174    }
175}
176
177/// Emits "package not found" error.
178fn emit_package_not_found(
179    ws: &Workspace<'_>,
180    opt_names: BTreeSet<String>,
181    opt_out: bool,
182) -> CargoResult<()> {
183    if !opt_names.is_empty() {
184        anyhow::bail!(
185            "{}package(s) `{}` not found in workspace `{}`",
186            if opt_out { "excluded " } else { "" },
187            opt_names.into_iter().collect::<Vec<_>>().join(", "),
188            ws.root().display(),
189        )
190    }
191    Ok(())
192}
193
194/// Emits "glob pattern not found" error.
195fn emit_pattern_not_found(
196    ws: &Workspace<'_>,
197    opt_patterns: Vec<(glob::Pattern, bool)>,
198    opt_out: bool,
199) -> CargoResult<()> {
200    let not_matched = opt_patterns
201        .iter()
202        .filter(|(_, matched)| !*matched)
203        .map(|(pat, _)| pat.as_str())
204        .collect::<Vec<_>>();
205    if !not_matched.is_empty() {
206        anyhow::bail!(
207            "{}package pattern(s) `{}` not found in workspace `{}`",
208            if opt_out { "excluded " } else { "" },
209            not_matched.join(", "),
210            ws.root().display(),
211        )
212    }
213    Ok(())
214}
215
216fn emit_packages_not_found_within_workspace(
217    ws: &Workspace<'_>,
218    packages: &[String],
219) -> CargoResult<()> {
220    let (mut patterns, mut ids) = opt_patterns_and_ids(packages)?;
221    let _: Vec<_> = ws
222        .members()
223        .filter(|pkg| {
224            let id = ids.iter().find(|id| id.matches(pkg.package_id())).cloned();
225            if let Some(id) = &id {
226                ids.remove(id);
227            }
228            !id.is_some() && !match_patterns(pkg, &mut patterns)
229        })
230        .map(Package::package_id)
231        .map(|id| id.to_spec())
232        .collect();
233    let names = ids
234        .into_iter()
235        .map(|id| id.to_string())
236        .collect::<BTreeSet<_>>();
237    emit_package_not_found(ws, names, false)?;
238    emit_pattern_not_found(ws, patterns, false)?;
239    Ok(())
240}
241
242/// Given a list opt-in or opt-out package selection strings, generates two
243/// collections that represent glob patterns and package id specs respectively.
244fn opt_patterns_and_ids(
245    opt: &[String],
246) -> CargoResult<(Vec<(glob::Pattern, bool)>, BTreeSet<PackageIdSpec>)> {
247    let mut opt_patterns = Vec::new();
248    let mut opt_ids = BTreeSet::new();
249    for x in opt.iter() {
250        match PackageIdSpec::parse(x) {
251            Ok(spec) => {
252                opt_ids.insert(spec);
253            }
254            Err(_) if is_glob_pattern(x) => opt_patterns.push((build_glob(x)?, false)),
255            Err(e) => return Err(e.into()),
256        }
257    }
258    Ok((opt_patterns, opt_ids))
259}
260
261/// Checks whether a package matches any of a list of glob patterns generated
262/// from `opt_patterns_and_names`.
263fn match_patterns(pkg: &Package, patterns: &mut Vec<(glob::Pattern, bool)>) -> bool {
264    patterns.iter_mut().any(|(m, matched)| {
265        let is_matched = m.matches(pkg.name().as_str());
266        *matched |= is_matched;
267        is_matched
268    })
269}
270
271/// Build [`glob::Pattern`] with informative context.
272pub fn build_glob(pat: &str) -> CargoResult<glob::Pattern> {
273    glob::Pattern::new(pat).with_context(|| format!("cannot build glob pattern from `{}`", pat))
274}