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