Skip to main content

bootstrap/core/builder/
cli_paths.rs

1//! Various pieces of code for dealing with "paths" passed to bootstrap on the
2//! command-line, extracted from `core/builder/mod.rs` because that file is
3//! large and hard to navigate.
4
5use std::collections::{HashMap, HashSet};
6use std::hash::Hash;
7use std::path::{Path, PathBuf};
8
9use crate::core::builder::{Builder, CommandLineStepDescription, Kind, PathSet, ShouldRun};
10use crate::utils::helpers;
11
12#[cfg(test)]
13mod tests;
14
15/// Combines a [`CommandLineStepDescription`] with its corresponding [`ShouldRun`].
16struct StepExtra<'a> {
17    desc: &'a CommandLineStepDescription,
18    should_run: ShouldRun<'a>,
19}
20
21pub(crate) fn match_paths_to_steps_and_run(
22    builder: &Builder<'_>,
23    step_descs: &[CommandLineStepDescription],
24    paths: &[PathBuf],
25) {
26    // Obtain `ShouldRun` information for each step, so that we know which
27    // paths to match it against.
28    let steps = step_descs
29        .iter()
30        .map(|desc| StepExtra { desc, should_run: (desc.should_run)(ShouldRun::new(builder)) })
31        .collect::<Vec<_>>();
32
33    // FIXME(Zalathar): This particular check isn't related to path-to-step
34    // matching, and should probably be hoisted to somewhere much earlier.
35    if builder.download_rustc() && (builder.kind == Kind::Dist || builder.kind == Kind::Install) {
36        eprintln!(
37            "ERROR: '{}' subcommand is incompatible with `rust.download-rustc`.",
38            builder.kind.as_str()
39        );
40        helpers::exit_process(1);
41    }
42
43    // sanity checks on rules
44    for StepExtra { desc, should_run } in &steps {
45        assert!(!should_run.paths.is_empty(), "{:?} should have at least one pathset", desc.name);
46    }
47
48    // Run default steps if appropriate.
49    if paths.is_empty() || builder.config.include_default_paths {
50        for StepExtra { desc, should_run } in &steps {
51            if (desc.is_default_step_fn)(builder) {
52                let default_pathsets = should_run.default_pathsets();
53                desc.maybe_run(builder, default_pathsets);
54            }
55        }
56    }
57
58    // Normalize command-line selectors to account for absolute and dot-relative paths.
59    let paths = paths.iter().map(|path| normalize_selector(builder, path)).collect::<Vec<_>>();
60
61    // If any absolute paths couldn't be made relative, stop now and report them.
62    let bad_abs_paths = paths.iter().filter(|path| path.is_absolute()).collect::<Vec<_>>();
63    if !bad_abs_paths.is_empty() {
64        eprintln!(
65            "ERROR: the following paths do not exist on disk or point outside the source directory: {bad_abs_paths:#?}"
66        );
67        helpers::exit_process(1);
68    }
69
70    // When matching selectors to steps, we want to balance two conflicting goals:
71    // - Ideally, steps should run in the order specified by command-line arguments.
72    // - A selected step should be invoked only once, not multiple times.
73    //
74    // We therefore build up:
75    // - An ordered list of steps to run, each represented by its index in `steps`.
76    // - For each step (by index), the list of its anchors that were matched.
77    let mut step_queue = Vec::<usize>::with_capacity(paths.len());
78    let mut step_anchors = HashMap::<usize, Vec<&PathSet>>::with_capacity(steps.len());
79    let mut unmatched_paths = vec![];
80
81    // For each command-line selector, enqueue the steps that it matches.
82    for path in &paths {
83        let mut path_matched = false;
84
85        for (step_ix, step) in steps.iter().enumerate() {
86            let matched_anchors = step
87                .should_run
88                .paths
89                .iter()
90                .filter(|anchor| {
91                    // The extra `starts_with` here allows an argument like
92                    // `tests/ui/asm/cfg.rs` to select the suite anchor `tests/ui`.
93                    anchor.has(path)
94                        || matches!(anchor, PathSet::Suite(suite) if path.starts_with(&suite.path))
95                })
96                .collect::<Vec<_>>();
97
98            if !matched_anchors.is_empty() {
99                step_queue.push(step_ix);
100                step_anchors.entry(step_ix).or_default().extend(matched_anchors);
101                path_matched = true;
102            }
103        }
104
105        if !path_matched {
106            unmatched_paths.push(path);
107        }
108    }
109
110    if !unmatched_paths.is_empty() {
111        eprintln!("ERROR: no `{}` rules matched {unmatched_paths:?}", builder.kind.as_str());
112        eprintln!(
113            "HELP: run `x.py {} --help --verbose` to show a list of available paths",
114            builder.kind.as_str()
115        );
116        eprintln!(
117            "NOTE: if you are adding a new Step to bootstrap itself, make sure you register it with `describe!`"
118        );
119        helpers::exit_process(1);
120    }
121
122    fn dedup_vec<T: Copy + Eq + Hash>(vec: &mut Vec<T>) {
123        let mut seen = HashSet::<T>::with_capacity(vec.len());
124        vec.retain(|&x| seen.insert(x));
125    }
126
127    // Deduplicate the queue of steps to run, and the list of anchors to run for each step.
128    dedup_vec(&mut step_queue);
129    for anchors in step_anchors.values_mut() {
130        dedup_vec(anchors);
131    }
132
133    // Run the steps that were selected, in (roughly) command-line order.
134    // For each step, pass all of its matched anchors, regardless of position.
135    for &step_ix in &step_queue {
136        let step = &steps[step_ix];
137        let anchors = step_anchors[&step_ix].iter().map(|p| PathSet::clone(p)).collect::<Vec<_>>();
138        step.desc.maybe_run(builder, anchors);
139    }
140}
141
142/// Normalize command-line arguments that happen to be paths, e.g.:
143/// - `/home/ferris/rust/tests/ui/asm/cfg.rs` => `tests/ui/asm/cfg.rs`
144/// - `./tests/ui/asm/cfg.rs` => `tests/ui/asm/cfg.rs`
145///
146/// Normalization is performed relative to the _repository source root_,
147/// not the working directory.
148///
149/// We take care to only modify selectors that specifically resemble paths,
150/// and to only modify selectors that actually correspond to a file on disk.
151/// This avoids incorrect conversions such as:
152/// - `/home/ferris/rust/ui` =X=> `ui`
153/// - `./tidyselftest` =X=> `tidyselftest`
154fn normalize_selector<'a>(builder: &Builder<'_>, path: &'a Path) -> &'a Path {
155    // Note that `Path::strip_prefix` strips path _segments_, not substrings.
156    // So this turns `./foo` into `foo`, but ignores `../foo` entirely.
157    if let Ok(without_dot) = Path::strip_prefix(path, ".")
158        && builder.src.join(path).exists()
159    {
160        without_dot
161    } else if path.is_absolute()
162        && path.exists()
163        && let Ok(relative) = path.strip_prefix(&builder.src)
164    {
165        relative
166    } else {
167        path
168    }
169}