tidy/
lib.rs

1//! Library used by tidy and other tools.
2//!
3//! This library contains the tidy lints and exposes it
4//! to be used by tools.
5
6use std::ffi::OsStr;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::{env, io};
10
11use build_helper::ci::CiEnv;
12use build_helper::git::{GitConfig, get_closest_upstream_commit};
13use build_helper::stage0_parser::{Stage0Config, parse_stage0_file};
14
15use crate::diagnostics::{DiagCtx, RunningCheck};
16
17macro_rules! static_regex {
18    ($re:literal) => {{
19        static RE: ::std::sync::LazyLock<::regex::Regex> =
20            ::std::sync::LazyLock::new(|| ::regex::Regex::new($re).unwrap());
21        &*RE
22    }};
23}
24
25/// A helper macro to `unwrap` a result except also print out details like:
26///
27/// * The expression that failed
28/// * The error itself
29/// * (optionally) a path connected to the error (e.g. failure to open a file)
30#[macro_export]
31macro_rules! t {
32    ($e:expr, $p:expr) => {
33        match $e {
34            Ok(e) => e,
35            Err(e) => panic!("{} failed on {} with {}", stringify!($e), ($p).display(), e),
36        }
37    };
38
39    ($e:expr) => {
40        match $e {
41            Ok(e) => e,
42            Err(e) => panic!("{} failed with {}", stringify!($e), e),
43        }
44    };
45}
46
47pub struct CiInfo {
48    pub git_merge_commit_email: String,
49    pub nightly_branch: String,
50    pub base_commit: Option<String>,
51    pub ci_env: CiEnv,
52}
53
54impl CiInfo {
55    pub fn new(diag_ctx: DiagCtx) -> Self {
56        let mut check = diag_ctx.start_check("CI history");
57
58        let stage0 = parse_stage0_file();
59        let Stage0Config { nightly_branch, git_merge_commit_email, .. } = stage0.config;
60
61        let mut info = Self {
62            nightly_branch,
63            git_merge_commit_email,
64            ci_env: CiEnv::current(),
65            base_commit: None,
66        };
67        let base_commit = match get_closest_upstream_commit(None, &info.git_config(), info.ci_env) {
68            Ok(Some(commit)) => Some(commit),
69            Ok(None) => {
70                info.error_if_in_ci("no base commit found", &mut check);
71                None
72            }
73            Err(error) => {
74                info.error_if_in_ci(
75                    &format!("failed to retrieve base commit: {error}"),
76                    &mut check,
77                );
78                None
79            }
80        };
81        info.base_commit = base_commit;
82        info
83    }
84
85    pub fn git_config(&self) -> GitConfig<'_> {
86        GitConfig {
87            nightly_branch: &self.nightly_branch,
88            git_merge_commit_email: &self.git_merge_commit_email,
89        }
90    }
91
92    pub fn error_if_in_ci(&self, msg: &str, check: &mut RunningCheck) {
93        if self.ci_env.is_running_in_ci() {
94            check.error(msg);
95        } else {
96            check.warning(format!("{msg}. Some checks will be skipped."));
97        }
98    }
99}
100
101pub fn git_diff<S: AsRef<OsStr>>(base_commit: &str, extra_arg: S) -> Option<String> {
102    let output = Command::new("git").arg("diff").arg(base_commit).arg(extra_arg).output().ok()?;
103    Some(String::from_utf8_lossy(&output.stdout).into())
104}
105
106/// Similar to `files_modified`, but only involves a single call to `git`.
107///
108/// removes all elements from `items` that do not cause any match when `pred` is called with the list of modifed files.
109///
110/// if in CI, no elements will be removed.
111pub fn files_modified_batch_filter<T>(
112    ci_info: &CiInfo,
113    items: &mut Vec<T>,
114    pred: impl Fn(&T, &str) -> bool,
115) {
116    if CiEnv::is_ci() {
117        // assume everything is modified on CI because we really don't want false positives there.
118        return;
119    }
120    let Some(base_commit) = &ci_info.base_commit else {
121        eprintln!("No base commit, assuming all files are modified");
122        return;
123    };
124    match crate::git_diff(base_commit, "--name-status") {
125        Some(output) => {
126            let modified_files: Vec<_> = output
127                .lines()
128                .filter_map(|ln| {
129                    let (status, name) = ln
130                        .trim_end()
131                        .split_once('\t')
132                        .expect("bad format from `git diff --name-status`");
133                    if status == "M" { Some(name) } else { None }
134                })
135                .collect();
136            items.retain(|item| {
137                for modified_file in &modified_files {
138                    if pred(item, modified_file) {
139                        // at least one predicate matches, keep this item.
140                        return true;
141                    }
142                }
143                // no predicates matched, remove this item.
144                false
145            });
146        }
147        None => {
148            eprintln!("warning: failed to run `git diff` to check for changes");
149            eprintln!("warning: assuming all files are modified");
150        }
151    }
152}
153
154/// Returns true if any modified file matches the predicate, if we are in CI, or if unable to list modified files.
155pub fn files_modified(ci_info: &CiInfo, pred: impl Fn(&str) -> bool) -> bool {
156    let mut v = vec![()];
157    files_modified_batch_filter(ci_info, &mut v, |_, p| pred(p));
158    !v.is_empty()
159}
160
161/// If the given executable is installed with the given version, use that,
162/// otherwise install via cargo.
163pub fn ensure_version_or_cargo_install(
164    build_dir: &Path,
165    cargo: &Path,
166    pkg_name: &str,
167    bin_name: &str,
168    version: &str,
169) -> io::Result<PathBuf> {
170    let tool_root_dir = build_dir.join("misc-tools");
171    let tool_bin_dir = tool_root_dir.join("bin");
172    let bin_path = tool_bin_dir.join(bin_name).with_extension(env::consts::EXE_EXTENSION);
173
174    // ignore the process exit code here and instead just let the version number check fail.
175    // we also importantly don't return if the program wasn't installed,
176    // instead we want to continue to the fallback.
177    'ck: {
178        // FIXME: rewrite as if-let chain once this crate is 2024 edition.
179        let Ok(output) = Command::new(&bin_path).arg("--version").output() else {
180            break 'ck;
181        };
182        let Ok(s) = str::from_utf8(&output.stdout) else {
183            break 'ck;
184        };
185        let Some(v) = s.trim().split_whitespace().last() else {
186            break 'ck;
187        };
188        if v == version {
189            return Ok(bin_path);
190        }
191    }
192
193    eprintln!("building external tool {bin_name} from package {pkg_name}@{version}");
194    // use --force to ensure that if the required version is bumped, we update it.
195    // use --target-dir to ensure we have a build cache so repeated invocations aren't slow.
196    // modify PATH so that cargo doesn't print a warning telling the user to modify the path.
197    let mut cmd = Command::new(cargo);
198    cmd.args(["install", "--locked", "--force", "--quiet"])
199        .arg("--root")
200        .arg(&tool_root_dir)
201        .arg("--target-dir")
202        .arg(tool_root_dir.join("target"))
203        .arg(format!("{pkg_name}@{version}"))
204        .env(
205            "PATH",
206            env::join_paths(
207                env::split_paths(&env::var("PATH").unwrap())
208                    .chain(std::iter::once(tool_bin_dir.clone())),
209            )
210            .expect("build dir contains invalid char"),
211        );
212
213    // On CI, we set opt-level flag for quicker installation.
214    // Since lower opt-level decreases the tool's performance,
215    // we don't set this option on local.
216    if CiEnv::is_ci() {
217        cmd.env("RUSTFLAGS", "-Copt-level=0");
218    }
219
220    let cargo_exit_code = cmd.spawn()?.wait()?;
221    if !cargo_exit_code.success() {
222        return Err(io::Error::other("cargo install failed"));
223    }
224    assert!(
225        matches!(bin_path.try_exists(), Ok(true)),
226        "cargo install did not produce the expected binary"
227    );
228    eprintln!("finished building tool {bin_name}");
229    Ok(bin_path)
230}
231
232pub mod alphabetical;
233pub mod bins;
234pub mod debug_artifacts;
235pub mod deps;
236pub mod diagnostics;
237pub mod edition;
238pub mod error_codes;
239pub mod extdeps;
240pub mod extra_checks;
241pub mod features;
242pub mod filenames;
243pub mod fluent_alphabetical;
244pub mod fluent_lowercase;
245pub mod fluent_period;
246mod fluent_used;
247pub mod gcc_submodule;
248pub(crate) mod iter_header;
249pub mod known_bug;
250pub mod mir_opt_tests;
251pub mod pal;
252pub mod rustdoc_css_themes;
253pub mod rustdoc_gui_tests;
254pub mod rustdoc_json;
255pub mod rustdoc_templates;
256pub mod style;
257pub mod target_policy;
258pub mod target_specific_tests;
259pub mod tests_placement;
260pub mod tests_revision_unpaired_stdout_stderr;
261pub mod triagebot;
262pub mod ui_tests;
263pub mod unit_tests;
264pub mod unknown_revision;
265pub mod unstable_book;
266pub mod walk;
267pub mod x_version;