1use std::ffi::{OsStr, OsString};
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use std::str::FromStr;
24use std::{env, fmt, fs, io};
25
26use crate::diagnostics::TidyCtx;
27
28mod rustdoc_js;
29
30#[cfg(test)]
31mod tests;
32
33const MIN_PY_REV: (u32, u32) = (3, 9);
34const MIN_PY_REV_STR: &str = "≥3.9";
35
36#[cfg(target_os = "windows")]
38const REL_PY_PATH: &[&str] = &["Scripts", "python3.exe"];
39#[cfg(not(target_os = "windows"))]
40const REL_PY_PATH: &[&str] = &["bin", "python3"];
41
42const RUFF_CONFIG_PATH: &[&str] = &["src", "tools", "tidy", "config", "ruff.toml"];
43const RUFF_CACHE_PATH: &[&str] = &["cache", "ruff_cache"];
45const PIP_REQ_PATH: &[&str] = &["src", "tools", "tidy", "config", "requirements.txt"];
46
47const SPELLCHECK_DIRS: &[&str] = &["compiler", "library", "src/bootstrap", "src/librustdoc"];
48const SPELLCHECK_VER: &str = "1.38.1";
49
50pub fn check(
51 root_path: &Path,
52 outdir: &Path,
53 librustdoc_path: &Path,
54 tools_path: &Path,
55 npm: &Path,
56 cargo: &Path,
57 extra_checks: Option<Vec<String>>,
58 pos_args: Vec<String>,
59 tidy_ctx: TidyCtx,
60) {
61 let mut lint_args = match extra_checks {
63 Some(s) => s
64 .iter()
65 .map(|s| {
66 if s == "spellcheck:fix" {
67 eprintln!("warning: `spellcheck:fix` is no longer valid, use `--extra-checks=spellcheck --bless`");
68 }
69 (ExtraCheckArg::from_str(s), s)
70 })
71 .filter_map(|(res, src)| match res {
72 Ok(arg) => {
73 Some(arg)
74 }
75 Err(err) => {
76 eprintln!("warning: bad extra check argument {src:?}: {err:?}");
78 None
79 }
80 })
81 .collect(),
82 None => vec![],
83 };
84 lint_args.retain(|ck| ck.is_non_if_installed_or_matches(root_path, outdir));
85 if lint_args.iter().any(|ck| ck.auto) {
86 crate::files_modified_batch_filter(
87 &tidy_ctx.base_commit,
88 tidy_ctx.is_running_on_ci(),
89 &mut lint_args,
90 |ck, path| ck.is_non_auto_or_matches(path),
91 );
92 }
93
94 macro_rules! extra_check {
95 ($lang:ident, $kind:ident) => {
96 lint_args.iter().any(|arg| arg.matches(ExtraCheckLang::$lang, ExtraCheckKind::$kind))
97 };
98 }
99
100 let python_lint = extra_check!(Py, Lint);
101 let python_fmt = extra_check!(Py, Fmt);
102 let shell_lint = extra_check!(Shell, Lint);
103 let cpp_fmt = extra_check!(Cpp, Fmt);
104 let spellcheck = extra_check!(Spellcheck, None);
105 let js_lint = extra_check!(Js, Lint);
106 let js_typecheck = extra_check!(Js, Typecheck);
107
108 let mut py_path = None;
109
110 let (cfg_args, file_args): (Vec<_>, Vec<_>) = pos_args
111 .iter()
112 .map(OsStr::new)
113 .partition(|arg| arg.to_str().is_some_and(|s| s.starts_with('-')));
114
115 if python_lint || python_fmt || cpp_fmt {
116 let p = py_prepare(root_path, outdir, &tidy_ctx);
118 if p.is_none() {
119 return;
120 }
121 py_path = p;
122 }
123
124 if python_lint {
125 check_python_lint(
126 root_path,
127 outdir,
128 &cfg_args,
129 &file_args,
130 py_path.as_ref().unwrap(),
131 &tidy_ctx,
132 );
133 }
134
135 if python_fmt {
136 check_python_fmt(
137 root_path,
138 outdir,
139 &cfg_args,
140 &file_args,
141 py_path.as_ref().unwrap(),
142 &tidy_ctx,
143 );
144 }
145
146 if cpp_fmt {
147 check_cpp_fmt(root_path, &cfg_args, &file_args, py_path.as_ref().unwrap(), &tidy_ctx);
148 }
149
150 if shell_lint {
151 check_shell_lint(root_path, &cfg_args, &file_args, &tidy_ctx);
152 }
153
154 if spellcheck {
155 check_spellcheck(root_path, outdir, cargo, &tidy_ctx);
156 }
157
158 if js_lint || js_typecheck {
159 if js_prepare(root_path, outdir, npm, &tidy_ctx).is_none() {
161 return;
162 }
163 }
164
165 if js_lint {
166 check_js_lint(outdir, librustdoc_path, tools_path, &tidy_ctx);
167 }
168
169 if js_typecheck {
170 check_js_typecheck(outdir, librustdoc_path, &tidy_ctx);
171 }
172}
173
174fn py_prepare(root_path: &Path, outdir: &Path, tidy_ctx: &TidyCtx) -> Option<PathBuf> {
175 let mut check = tidy_ctx.start_check("extra_checks:py_prepare");
176
177 let venv_path = outdir.join("venv");
178 let mut reqs_path = root_path.to_owned();
179 reqs_path.extend(PIP_REQ_PATH);
180
181 match get_or_create_venv(&venv_path, &reqs_path) {
182 Ok(p) => Some(p),
183 Err(e) => {
184 check.error(e);
185 None
186 }
187 }
188}
189
190fn js_prepare(root_path: &Path, outdir: &Path, npm: &Path, tidy_ctx: &TidyCtx) -> Option<()> {
191 let mut check = tidy_ctx.start_check("extra_checks:js_prepare");
192
193 if let Err(e) = rustdoc_js::npm_install(root_path, outdir, npm) {
194 check.error(e.to_string());
195 return None;
196 }
197
198 Some(())
199}
200
201fn show_bless_help(mode: &str, action: &str, bless: bool) {
202 if !bless {
203 eprintln!("rerun tidy with `--extra-checks={mode} --bless` to {action}");
204 }
205}
206
207fn show_diff() -> bool {
208 std::env::var("TIDY_PRINT_DIFF").is_ok_and(|v| v.eq_ignore_ascii_case("true") || v == "1")
209}
210
211fn check_spellcheck(root_path: &Path, outdir: &Path, cargo: &Path, tidy_ctx: &TidyCtx) {
212 let mut check = tidy_ctx.start_check("extra_checks:spellcheck");
213
214 let bless = tidy_ctx.is_bless_enabled();
215
216 let config_path = root_path.join("typos.toml");
217 let mut args = vec!["-c", config_path.as_os_str().to_str().unwrap()];
218 args.extend_from_slice(SPELLCHECK_DIRS);
219
220 if bless {
221 eprintln!("spellchecking files and fixing typos");
222 args.push("--write-changes");
223 } else {
224 eprintln!("spellchecking files");
225 }
226
227 if let Err(e) =
228 spellcheck_runner(root_path, &outdir, &cargo, &args, tidy_ctx.is_running_on_ci())
229 {
230 show_bless_help("spellcheck", "fix typos", bless);
231 check.error(e);
232 }
233}
234
235fn check_js_lint(outdir: &Path, librustdoc_path: &Path, tools_path: &Path, tidy_ctx: &TidyCtx) {
236 let mut check = tidy_ctx.start_check("extra_checks:js_lint");
237
238 let bless = tidy_ctx.is_bless_enabled();
239
240 if bless {
241 eprintln!("linting javascript files and applying suggestions");
242 } else {
243 eprintln!("linting javascript files");
244 }
245
246 if let Err(e) = rustdoc_js::lint(outdir, librustdoc_path, tools_path, bless) {
247 show_bless_help("js:lint", "apply esplint suggestion", bless);
248 check.error(e);
249 return;
250 }
251
252 if let Err(e) = rustdoc_js::es_check(outdir, librustdoc_path) {
253 check.error(e);
254 }
255}
256
257fn check_js_typecheck(outdir: &Path, librustdoc_path: &Path, tidy_ctx: &TidyCtx) {
258 let mut check = tidy_ctx.start_check("extra_checks:js_typecheck");
259
260 eprintln!("typechecking javascript files");
261 if let Err(e) = rustdoc_js::typecheck(outdir, librustdoc_path) {
262 check.error(e);
263 }
264}
265
266fn check_shell_lint(
267 root_path: &Path,
268 cfg_args: &Vec<&OsStr>,
269 file_args: &Vec<&OsStr>,
270 tidy_ctx: &TidyCtx,
271) {
272 let mut check = tidy_ctx.start_check("extra_checks:shell_lint");
273
274 eprintln!("linting shell files");
275
276 let mut file_args_shc = file_args.clone();
277 let files;
278 if file_args.is_empty() {
279 match find_with_extension(root_path, None, &[OsStr::new("sh")]) {
280 Ok(f) => files = f,
281 Err(e) => {
282 check.error(e);
283 return;
284 }
285 }
286
287 file_args_shc.extend(files.iter().map(|p| p.as_os_str()));
288 }
289
290 if let Err(e) = shellcheck_runner(&merge_args(&cfg_args, &file_args_shc)) {
291 check.error(e);
292 }
293}
294
295fn check_python_lint(
296 root_path: &Path,
297 outdir: &Path,
298 cfg_args: &Vec<&OsStr>,
299 file_args: &Vec<&OsStr>,
300 py_path: &Path,
301 tidy_ctx: &TidyCtx,
302) {
303 let mut check = tidy_ctx.start_check("extra_checks:python_lint");
304
305 let bless = tidy_ctx.is_bless_enabled();
306
307 let args: &[&OsStr] = if bless {
308 eprintln!("linting python files and applying suggestions");
309 &["check".as_ref(), "--fix".as_ref()]
310 } else {
311 eprintln!("linting python files");
312 &["check".as_ref()]
313 };
314
315 let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, args);
316
317 if res.is_err() && !bless && show_diff() {
318 eprintln!("\npython linting failed! Printing diff suggestions:");
319
320 let diff_res = run_ruff(
321 root_path,
322 outdir,
323 py_path,
324 &cfg_args,
325 &file_args,
326 &["check".as_ref(), "--diff".as_ref()],
327 );
328 if diff_res.is_err() {
330 show_bless_help("py:lint", "apply ruff suggestions", bless);
331 }
332 }
333 if let Err(e) = res {
334 check.error(e);
335 }
336}
337
338fn check_python_fmt(
339 root_path: &Path,
340 outdir: &Path,
341 cfg_args: &Vec<&OsStr>,
342 file_args: &Vec<&OsStr>,
343 py_path: &Path,
344 tidy_ctx: &TidyCtx,
345) {
346 let mut check = tidy_ctx.start_check("extra_checks:python_fmt");
347
348 let bless = tidy_ctx.is_bless_enabled();
349
350 let mut args: Vec<&OsStr> = vec!["format".as_ref()];
351 if bless {
352 eprintln!("formatting python files");
353 } else {
354 eprintln!("checking python file formatting");
355 args.push("--check".as_ref());
356 }
357
358 let res = run_ruff(root_path, outdir, py_path, &cfg_args, &file_args, &args);
359
360 if res.is_err() && !bless {
361 if show_diff() {
362 eprintln!("\npython formatting does not match! Printing diff:");
363
364 let _ = run_ruff(
365 root_path,
366 outdir,
367 py_path,
368 &cfg_args,
369 &file_args,
370 &["format".as_ref(), "--diff".as_ref()],
371 );
372 }
373 show_bless_help("py:fmt", "reformat Python code", bless);
374 }
375
376 if let Err(e) = res {
377 check.error(e);
378 }
379}
380
381fn check_cpp_fmt(
382 root_path: &Path,
383 cfg_args: &Vec<&OsStr>,
384 file_args: &Vec<&OsStr>,
385 py_path: &Path,
386 tidy_ctx: &TidyCtx,
387) {
388 let mut check = tidy_ctx.start_check("extra_checks:cpp_fmt");
389
390 let bless = tidy_ctx.is_bless_enabled();
391
392 let mut cfg_args_clang_format = cfg_args.clone();
393 let mut file_args_clang_format = file_args.clone();
394 let config_path = root_path.join(".clang-format");
395 let mut config_file_arg = OsString::from("file:");
396 config_file_arg.push(&config_path);
397 cfg_args_clang_format.extend(&["--style".as_ref(), config_file_arg.as_ref()]);
398 if bless {
399 eprintln!("formatting C++ files");
400 cfg_args_clang_format.push("-i".as_ref());
401 } else {
402 eprintln!("checking C++ file formatting");
403 cfg_args_clang_format.extend(&["--dry-run".as_ref(), "--Werror".as_ref()]);
404 }
405 let files;
406 if file_args_clang_format.is_empty() {
407 let llvm_wrapper = root_path.join("compiler/rustc_llvm/llvm-wrapper");
408 match find_with_extension(
409 root_path,
410 Some(llvm_wrapper.as_path()),
411 &[OsStr::new("h"), OsStr::new("cpp")],
412 ) {
413 Ok(f) => files = f,
414 Err(e) => {
415 check.error(e);
416 return;
417 }
418 }
419 file_args_clang_format.extend(files.iter().map(|p| p.as_os_str()));
420 }
421 let args = merge_args(&cfg_args_clang_format, &file_args_clang_format);
422 let res = py_runner(py_path, false, None, "clang-format", &args);
423
424 if res.is_err() && !bless && show_diff() {
425 eprintln!("\nclang-format linting failed! Printing diff suggestions:");
426
427 let mut cfg_args_diff = cfg_args.clone();
428 cfg_args_diff.extend(&["--style".as_ref(), config_file_arg.as_ref()]);
429 for file in file_args {
430 let mut formatted = String::new();
431 let mut diff_args = cfg_args_diff.clone();
432 diff_args.push(file);
433 let _ = py_runner(py_path, false, Some(&mut formatted), "clang-format", &diff_args);
434 if formatted.is_empty() {
435 eprintln!(
436 "failed to obtain the formatted content for '{}'",
437 file.to_string_lossy()
438 );
439 continue;
440 }
441 let actual = std::fs::read_to_string(file).unwrap_or_else(|e| {
442 panic!("failed to read the C++ file at '{}' due to '{e}'", file.to_string_lossy())
443 });
444 if formatted != actual {
445 let diff = similar::TextDiff::from_lines(&actual, &formatted);
446 eprintln!(
447 "{}",
448 diff.unified_diff().context_radius(4).header(
449 &format!("{} (actual)", file.to_string_lossy()),
450 &format!("{} (formatted)", file.to_string_lossy())
451 )
452 );
453 }
454 }
455 show_bless_help("cpp:fmt", "reformat C++ code", bless);
456 }
457
458 if let Err(e) = res {
459 check.error(e);
460 }
461}
462
463fn run_ruff(
464 root_path: &Path,
465 outdir: &Path,
466 py_path: &Path,
467 cfg_args: &[&OsStr],
468 file_args: &[&OsStr],
469 ruff_args: &[&OsStr],
470) -> Result<(), Error> {
471 let mut cfg_args_ruff = cfg_args.to_vec();
472 let mut file_args_ruff = file_args.to_vec();
473
474 let mut cfg_path = root_path.to_owned();
475 cfg_path.extend(RUFF_CONFIG_PATH);
476 let mut cache_dir = outdir.to_owned();
477 cache_dir.extend(RUFF_CACHE_PATH);
478
479 cfg_args_ruff.extend([
480 "--config".as_ref(),
481 cfg_path.as_os_str(),
482 "--cache-dir".as_ref(),
483 cache_dir.as_os_str(),
484 ]);
485
486 if file_args_ruff.is_empty() {
487 file_args_ruff.push(root_path.as_os_str());
488 }
489
490 let mut args: Vec<&OsStr> = ruff_args.to_vec();
491 args.extend(merge_args(&cfg_args_ruff, &file_args_ruff));
492 py_runner(py_path, true, None, "ruff", &args)
493}
494
495fn merge_args<'a>(cfg_args: &[&'a OsStr], file_args: &[&'a OsStr]) -> Vec<&'a OsStr> {
497 let mut args = cfg_args.to_owned();
498 args.push("--".as_ref());
499 args.extend(file_args);
500 args
501}
502
503fn py_runner(
507 py_path: &Path,
508 as_module: bool,
509 stdout: Option<&mut String>,
510 bin: &'static str,
511 args: &[&OsStr],
512) -> Result<(), Error> {
513 let mut cmd = Command::new(py_path);
514 if as_module {
515 cmd.arg("-m").arg(bin).args(args);
516 } else {
517 let bin_path = py_path.with_file_name(bin);
518 cmd.arg(bin_path).args(args);
519 }
520 let status = if let Some(stdout) = stdout {
521 let output = cmd.output()?;
522 if let Ok(s) = std::str::from_utf8(&output.stdout) {
523 stdout.push_str(s);
524 }
525 output.status
526 } else {
527 cmd.status()?
528 };
529 if status.success() { Ok(()) } else { Err(Error::FailedCheck(bin)) }
530}
531
532fn get_or_create_venv(venv_path: &Path, src_reqs_path: &Path) -> Result<PathBuf, Error> {
535 let mut py_path = venv_path.to_owned();
536 py_path.extend(REL_PY_PATH);
537
538 if !has_py_tools(venv_path, src_reqs_path)? {
539 let dst_reqs_path = venv_path.join("requirements.txt");
540 eprintln!("removing old virtual environment");
541 if venv_path.is_dir() {
542 fs::remove_dir_all(venv_path).unwrap_or_else(|_| {
543 panic!("failed to remove directory at {}", venv_path.display())
544 });
545 }
546 create_venv_at_path(venv_path)?;
547 install_requirements(&py_path, src_reqs_path, &dst_reqs_path)?;
548 }
549
550 verify_py_version(&py_path)?;
551 Ok(py_path)
552}
553
554fn has_py_tools(venv_path: &Path, src_reqs_path: &Path) -> Result<bool, Error> {
555 let dst_reqs_path = venv_path.join("requirements.txt");
556 if let Ok(req) = fs::read_to_string(&dst_reqs_path) {
557 if req == fs::read_to_string(src_reqs_path)? {
558 return Ok(true);
559 }
560 eprintln!("requirements.txt file mismatch");
561 }
562
563 Ok(false)
564}
565
566fn create_venv_at_path(path: &Path) -> Result<(), Error> {
569 const TRY_PY: &[&str] = &[
572 "python3.13",
573 "python3.12",
574 "python3.11",
575 "python3.10",
576 "python3.9",
577 "python3",
578 "python",
579 "python3.14",
580 ];
581
582 let mut sys_py = None;
583 let mut found = Vec::new();
584
585 for py in TRY_PY {
586 match verify_py_version(Path::new(py)) {
587 Ok(_) => {
588 sys_py = Some(*py);
589 break;
590 }
591 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => (),
593 Err(Error::Version { installed, .. }) => found.push(installed),
595 Err(e) => eprintln!("note: error running '{py}': {e}"),
597 }
598 }
599
600 let Some(sys_py) = sys_py else {
601 let ret = if found.is_empty() {
602 Error::MissingReq("python3", "python file checks", None)
603 } else {
604 found.sort();
605 found.dedup();
606 Error::Version {
607 program: "python3",
608 required: MIN_PY_REV_STR,
609 installed: found.join(", "),
610 }
611 };
612 return Err(ret);
613 };
614
615 if try_create_venv(sys_py, path, "venv").is_ok() {
619 return Ok(());
620 }
621 try_create_venv(sys_py, path, "virtualenv")
622}
623
624fn try_create_venv(python: &str, path: &Path, module: &str) -> Result<(), Error> {
625 eprintln!(
626 "creating virtual environment at '{}' using '{python}' and '{module}'",
627 path.display()
628 );
629 let out = Command::new(python).args(["-m", module]).arg(path).output().unwrap();
630
631 if out.status.success() {
632 return Ok(());
633 }
634
635 let stderr = String::from_utf8_lossy(&out.stderr);
636 let err = if stderr.contains(&format!("No module named {module}")) {
637 Error::Generic(format!(
638 r#"{module} not found: you may need to install it:
639`{python} -m pip install {module}`
640If you see an error about "externally managed environment" when running the above command,
641either install `{module}` using your system package manager
642(e.g. `sudo apt-get install {python}-{module}`) or create a virtual environment manually, install
643`{module}` in it and then activate it before running tidy.
644"#
645 ))
646 } else {
647 Error::Generic(format!(
648 "failed to create venv at '{}' using {python} -m {module}: {stderr}",
649 path.display()
650 ))
651 };
652 Err(err)
653}
654
655fn verify_py_version(py_path: &Path) -> Result<(), Error> {
658 let out = Command::new(py_path).arg("--version").output()?;
659 let outstr = String::from_utf8_lossy(&out.stdout);
660 let vers = outstr.trim().split_ascii_whitespace().nth(1).unwrap().trim();
661 let mut vers_comps = vers.split('.');
662 let major: u32 = vers_comps.next().unwrap().parse().unwrap();
663 let minor: u32 = vers_comps.next().unwrap().parse().unwrap();
664
665 if (major, minor) < MIN_PY_REV {
666 Err(Error::Version {
667 program: "python",
668 required: MIN_PY_REV_STR,
669 installed: vers.to_owned(),
670 })
671 } else {
672 Ok(())
673 }
674}
675
676fn install_requirements(
677 py_path: &Path,
678 src_reqs_path: &Path,
679 dst_reqs_path: &Path,
680) -> Result<(), Error> {
681 let stat = Command::new(py_path)
682 .args(["-m", "pip", "install", "--upgrade", "pip"])
683 .status()
684 .expect("failed to launch pip");
685 if !stat.success() {
686 return Err(Error::Generic(format!("pip install failed with status {stat}")));
687 }
688
689 let stat = Command::new(py_path)
690 .args(["-m", "pip", "install", "--quiet", "--require-hashes", "-r"])
691 .arg(src_reqs_path)
692 .status()?;
693 if !stat.success() {
694 return Err(Error::Generic(format!(
695 "failed to install requirements at {}",
696 src_reqs_path.display()
697 )));
698 }
699 fs::copy(src_reqs_path, dst_reqs_path)?;
700 assert_eq!(
701 fs::read_to_string(src_reqs_path).unwrap(),
702 fs::read_to_string(dst_reqs_path).unwrap()
703 );
704 Ok(())
705}
706
707fn has_shellcheck() -> Result<(), Error> {
709 match Command::new("shellcheck").arg("--version").status() {
710 Ok(_) => Ok(()),
711 Err(e) if e.kind() == io::ErrorKind::NotFound => Err(Error::MissingReq(
712 "shellcheck",
713 "shell file checks",
714 Some(
715 "see <https://github.com/koalaman/shellcheck#installing> \
716 for installation instructions"
717 .to_owned(),
718 ),
719 )),
720 Err(e) => Err(e.into()),
721 }
722}
723
724fn shellcheck_runner(args: &[&OsStr]) -> Result<(), Error> {
726 has_shellcheck()?;
727
728 let status = Command::new("shellcheck").args(args).status()?;
729 if status.success() { Ok(()) } else { Err(Error::FailedCheck("shellcheck")) }
730}
731
732fn spellcheck_runner(
734 src_root: &Path,
735 outdir: &Path,
736 cargo: &Path,
737 args: &[&str],
738 is_ci: bool,
739) -> Result<(), Error> {
740 let bin_path = ensure_version_or_cargo_install(
741 outdir,
742 cargo,
743 "typos-cli",
744 "typos",
745 SPELLCHECK_VER,
746 is_ci,
747 )?;
748 match Command::new(bin_path).current_dir(src_root).args(args).status() {
749 Ok(status) => {
750 if status.success() {
751 Ok(())
752 } else {
753 Err(Error::FailedCheck("typos"))
754 }
755 }
756 Err(err) => Err(Error::Generic(format!("failed to run typos tool: {err:?}"))),
757 }
758}
759
760fn find_with_extension(
762 root_path: &Path,
763 find_dir: Option<&Path>,
764 extensions: &[&OsStr],
765) -> Result<Vec<PathBuf>, Error> {
766 let stat_output =
769 Command::new("git").arg("-C").arg(root_path).args(["status", "--short"]).output()?.stdout;
770
771 if String::from_utf8_lossy(&stat_output).lines().filter(|ln| ln.starts_with('?')).count() > 0 {
772 eprintln!("found untracked files, ignoring");
773 }
774
775 let mut output = Vec::new();
776 let binding = {
777 let mut command = Command::new("git");
778 command.arg("-C").arg(root_path).args(["ls-files"]);
779 if let Some(find_dir) = find_dir {
780 command.arg(find_dir);
781 }
782 command.output()?
783 };
784 let tracked = String::from_utf8_lossy(&binding.stdout);
785
786 for line in tracked.lines() {
787 let line = line.trim();
788 let path = Path::new(line);
789
790 let Some(ref extension) = path.extension() else {
791 continue;
792 };
793 if extensions.contains(extension) {
794 output.push(root_path.join(path));
795 }
796 }
797
798 Ok(output)
799}
800
801fn ensure_version(build_dir: &Path, bin_name: &str, version: &str) -> Result<PathBuf, Error> {
803 let bin_path = build_dir.join("misc-tools").join("bin").join(bin_name);
804
805 match Command::new(&bin_path).arg("--version").output() {
806 Ok(output) => {
807 let Some(v) = str::from_utf8(&output.stdout).unwrap().trim().split_whitespace().last()
808 else {
809 return Err(Error::Generic("version check failed".to_string()));
810 };
811
812 if v != version {
813 return Err(Error::Version { program: "", required: "", installed: v.to_string() });
814 }
815 Ok(bin_path)
816 }
817 Err(e) => Err(Error::Io(e)),
818 }
819}
820
821fn ensure_version_or_cargo_install(
824 build_dir: &Path,
825 cargo: &Path,
826 pkg_name: &str,
827 bin_name: &str,
828 version: &str,
829 is_ci: bool,
830) -> Result<PathBuf, Error> {
831 if let Ok(bin_path) = ensure_version(build_dir, bin_name, version) {
832 return Ok(bin_path);
833 }
834
835 eprintln!("building external tool {bin_name} from package {pkg_name}@{version}");
836
837 let tool_root_dir = build_dir.join("misc-tools");
838 let tool_bin_dir = tool_root_dir.join("bin");
839 let bin_path = tool_bin_dir.join(bin_name).with_extension(env::consts::EXE_EXTENSION);
840
841 let mut cmd = Command::new(cargo);
845 cmd.args(["install", "--locked", "--force", "--quiet"])
846 .arg("--root")
847 .arg(&tool_root_dir)
848 .arg("--target-dir")
849 .arg(tool_root_dir.join("target"))
850 .arg(format!("{pkg_name}@{version}"))
851 .env(
852 "PATH",
853 env::join_paths(
854 env::split_paths(&env::var("PATH").unwrap())
855 .chain(std::iter::once(tool_bin_dir.clone())),
856 )
857 .expect("build dir contains invalid char"),
858 );
859
860 if is_ci {
864 cmd.env("RUSTFLAGS", "-Copt-level=0");
865 }
866
867 let cargo_exit_code = cmd.spawn()?.wait()?;
868 if !cargo_exit_code.success() {
869 return Err(Error::Generic("cargo install failed".to_string()));
870 }
871 assert!(
872 matches!(bin_path.try_exists(), Ok(true)),
873 "cargo install did not produce the expected binary"
874 );
875 eprintln!("finished building tool {bin_name}");
876 Ok(bin_path)
877}
878
879#[derive(Debug)]
880enum Error {
881 Io(io::Error),
882 MissingReq(&'static str, &'static str, Option<String>),
884 FailedCheck(&'static str),
886 Generic(String),
888 Version {
890 program: &'static str,
891 required: &'static str,
892 installed: String,
893 },
894}
895
896impl fmt::Display for Error {
897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898 match self {
899 Self::MissingReq(a, b, ex) => {
900 write!(
901 f,
902 "{a} is required to run {b} but it could not be located. Is it installed?"
903 )?;
904 if let Some(s) = ex {
905 write!(f, "\n{s}")?;
906 };
907 Ok(())
908 }
909 Self::Version { program, required, installed } => write!(
910 f,
911 "insufficient version of '{program}' to run external tools: \
912 {required} required but found {installed}",
913 ),
914 Self::Generic(s) => f.write_str(s),
915 Self::Io(e) => write!(f, "IO error: {e}"),
916 Self::FailedCheck(s) => write!(f, "checks with external tool '{s}' failed"),
917 }
918 }
919}
920
921impl From<io::Error> for Error {
922 fn from(value: io::Error) -> Self {
923 Self::Io(value)
924 }
925}
926
927#[derive(Debug, PartialEq)]
928enum ExtraCheckParseError {
929 #[allow(dead_code, reason = "shown through Debug")]
930 UnknownKind(String),
931 #[allow(dead_code)]
932 UnknownLang(String),
933 UnsupportedKindForLang,
934 TooManyParts,
936 Empty,
938 AutoRequiresLang,
940 IfInstalledRequiresLang,
942}
943
944#[derive(PartialEq, Debug)]
945struct ExtraCheckArg {
946 auto: bool,
948 if_installed: bool,
950 lang: ExtraCheckLang,
951 kind: Option<ExtraCheckKind>,
953}
954
955impl ExtraCheckArg {
956 fn matches(&self, lang: ExtraCheckLang, kind: ExtraCheckKind) -> bool {
957 self.lang == lang && self.kind.map(|k| k == kind).unwrap_or(true)
958 }
959
960 fn is_non_if_installed_or_matches(&self, root_path: &Path, build_dir: &Path) -> bool {
961 if !self.if_installed {
962 return true;
963 }
964
965 match self.lang {
966 ExtraCheckLang::Spellcheck => {
967 match ensure_version(build_dir, "typos", SPELLCHECK_VER) {
968 Ok(_) => true,
969 Err(Error::Version { installed, .. }) => {
970 eprintln!(
971 "warning: the tool `typos` is detected, but version {installed} doesn't match with the expected version {SPELLCHECK_VER}"
972 );
973 false
974 }
975 _ => false,
976 }
977 }
978 ExtraCheckLang::Shell => has_shellcheck().is_ok(),
979 ExtraCheckLang::Js => {
980 match self.kind {
981 Some(ExtraCheckKind::Lint) => {
982 rustdoc_js::has_tool(build_dir, "eslint")
984 && rustdoc_js::has_tool(build_dir, "es-check")
985 }
986 Some(ExtraCheckKind::Typecheck) => {
987 rustdoc_js::has_tool(build_dir, "tsc")
989 }
990 None => {
991 rustdoc_js::has_tool(build_dir, "eslint")
993 && rustdoc_js::has_tool(build_dir, "es-check")
994 && rustdoc_js::has_tool(build_dir, "tsc")
995 }
996 Some(_) => unreachable!("js shouldn't have other type of ExtraCheckKind"),
997 }
998 }
999 ExtraCheckLang::Py | ExtraCheckLang::Cpp => {
1000 let venv_path = build_dir.join("venv");
1001 let mut reqs_path = root_path.to_owned();
1002 reqs_path.extend(PIP_REQ_PATH);
1003 let Ok(v) = has_py_tools(&venv_path, &reqs_path) else {
1004 return false;
1005 };
1006
1007 v
1008 }
1009 }
1010 }
1011
1012 fn is_non_auto_or_matches(&self, filepath: &str) -> bool {
1014 if !self.auto {
1015 return true;
1016 }
1017 let exts: &[&str] = match self.lang {
1018 ExtraCheckLang::Py => &[".py"],
1019 ExtraCheckLang::Cpp => &[".cpp"],
1020 ExtraCheckLang::Shell => &[".sh"],
1021 ExtraCheckLang::Js => &[".js", ".ts"],
1022 ExtraCheckLang::Spellcheck => {
1023 if SPELLCHECK_DIRS.iter().any(|dir| Path::new(filepath).starts_with(dir)) {
1024 return true;
1025 }
1026 &[]
1027 }
1028 };
1029 exts.iter().any(|ext| filepath.ends_with(ext))
1030 }
1031
1032 fn has_supported_kind(&self) -> bool {
1033 let Some(kind) = self.kind else {
1034 return true;
1036 };
1037 use ExtraCheckKind::*;
1038 let supported_kinds: &[_] = match self.lang {
1039 ExtraCheckLang::Py => &[Fmt, Lint],
1040 ExtraCheckLang::Cpp => &[Fmt],
1041 ExtraCheckLang::Shell => &[Lint],
1042 ExtraCheckLang::Spellcheck => &[],
1043 ExtraCheckLang::Js => &[Lint, Typecheck],
1044 };
1045 supported_kinds.contains(&kind)
1046 }
1047}
1048
1049impl FromStr for ExtraCheckArg {
1050 type Err = ExtraCheckParseError;
1051
1052 fn from_str(s: &str) -> Result<Self, Self::Err> {
1053 let mut auto = false;
1054 let mut if_installed = false;
1055 let mut parts = s.split(':');
1056 let mut first = match parts.next() {
1057 Some("") | None => return Err(ExtraCheckParseError::Empty),
1058 Some(part) => part,
1059 };
1060
1061 loop {
1064 match (first, auto, if_installed) {
1065 ("auto", false, _) => {
1066 let Some(part) = parts.next() else {
1067 return Err(ExtraCheckParseError::AutoRequiresLang);
1068 };
1069 auto = true;
1070 first = part;
1071 }
1072 ("if-installed", _, false) => {
1073 let Some(part) = parts.next() else {
1074 return Err(ExtraCheckParseError::IfInstalledRequiresLang);
1075 };
1076 if_installed = true;
1077 first = part;
1078 }
1079 _ => break,
1080 }
1081 }
1082 let second = parts.next();
1083 if parts.next().is_some() {
1084 return Err(ExtraCheckParseError::TooManyParts);
1085 }
1086 let arg = Self {
1087 auto,
1088 if_installed,
1089 lang: first.parse()?,
1090 kind: second.map(|s| s.parse()).transpose()?,
1091 };
1092 if !arg.has_supported_kind() {
1093 return Err(ExtraCheckParseError::UnsupportedKindForLang);
1094 }
1095
1096 Ok(arg)
1097 }
1098}
1099
1100#[derive(PartialEq, Copy, Clone, Debug)]
1101enum ExtraCheckLang {
1102 Py,
1103 Shell,
1104 Cpp,
1105 Spellcheck,
1106 Js,
1107}
1108
1109impl FromStr for ExtraCheckLang {
1110 type Err = ExtraCheckParseError;
1111
1112 fn from_str(s: &str) -> Result<Self, Self::Err> {
1113 Ok(match s {
1114 "py" => Self::Py,
1115 "shell" => Self::Shell,
1116 "cpp" => Self::Cpp,
1117 "spellcheck" => Self::Spellcheck,
1118 "js" => Self::Js,
1119 _ => return Err(ExtraCheckParseError::UnknownLang(s.to_string())),
1120 })
1121 }
1122}
1123
1124#[derive(PartialEq, Copy, Clone, Debug)]
1125enum ExtraCheckKind {
1126 Lint,
1127 Fmt,
1128 Typecheck,
1129 None,
1132}
1133
1134impl FromStr for ExtraCheckKind {
1135 type Err = ExtraCheckParseError;
1136
1137 fn from_str(s: &str) -> Result<Self, Self::Err> {
1138 Ok(match s {
1139 "lint" => Self::Lint,
1140 "fmt" => Self::Fmt,
1141 "typecheck" => Self::Typecheck,
1142 _ => return Err(ExtraCheckParseError::UnknownKind(s.to_string())),
1143 })
1144 }
1145}