Skip to main content

xtask/
main.rs

1#![feature(variant_count)]
2
3use std::{
4    env,
5    ffi::OsStr,
6    fs, io,
7    mem::variant_count,
8    path::{Path, PathBuf},
9    process::{Command, ExitStatus},
10};
11
12use anyhow::anyhow;
13use cargo_metadata::{
14    camino::{Utf8Path, Utf8PathBuf},
15    Artifact, Message, TargetKind,
16};
17use flux_dev::Suite;
18use flux_sysroot::{
19    default_flux_sysroot_dir, SysrootCrate, SysrootManifest, FLUX_SYSROOT, SYSROOT_MANIFEST,
20};
21
22xflags::xflags! {
23    cmd xtask {
24        /// If true, run all cargo commands with `--offline`
25        optional --offline
26        /// If true, run cargo build commands with --features rust-fixpiont
27        optional --rust-fixpoint
28        /// Run regression tests
29        cmd test {
30            /// Only run tests containing `filter` as a substring.
31            optional filter: String
32            /// Run only the named suite(s). May be repeated. If omitted, all suites run.
33            repeated --suite suite: Suite
34            /// Timeout in seconds for each fixpoint query (passed as `-Ffixpoint-timeout`)
35            optional --fixpoint-timeout seconds: u64
36        }
37        /// Run lean benchmarks: emit lean files for each test in tests/pos/
38        cmd lean-bench {
39            /// Only run tests containing `filter` as a substring.
40            optional filter: String
41        }
42        /// Run the `flux` binary on the given input file.
43        cmd run {
44            /// Input file
45            required input: PathBuf
46            /// Extra options to pass to the `flux` binary, e.g., `cargo x run file.rs -- -Zdump-mir=renumber`
47            repeated opts: String
48            /// Do not build Flux libs for extern specs
49            optional --no-extern-specs
50        }
51        /// Expand Flux macros
52        cmd expand {
53            /// Input file
54            required input: PathBuf
55        }
56        /// Install Flux binaries to `~/.cargo/bin` and precompiled libraries and driver to `~/.flux/sysroot`
57        cmd install {
58            /// Select build profile for the `flux-driver`, either 'release', 'dev', or 'profiling'. Default 'release'
59            optional --profile profile: Profile
60            /// Do not install Flux libs or extern specs
61            optional --no-extern-specs
62        }
63        /// Uninstall Flux binaries and libraries
64        cmd uninstall { }
65        /// Generate precompiled libraries
66        cmd build-sysroot { }
67        /// Build the documentation
68        cmd doc { }
69    }
70}
71
72#[derive(Clone, Copy, Debug)]
73enum Profile {
74    Release,
75    Dev,
76    Profiling,
77}
78
79impl Profile {
80    fn as_str(self) -> &'static str {
81        match self {
82            Profile::Release => "release",
83            Profile::Dev => "dev",
84            Profile::Profiling => "profiling",
85        }
86    }
87}
88
89impl std::str::FromStr for Profile {
90    type Err = &'static str;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        match s {
94            "release" => Ok(Self::Release),
95            "dev" => Ok(Self::Dev),
96            "profiling" => Ok(Self::Profiling),
97            _ => Err("invalid profile"),
98        }
99    }
100}
101
102fn main() -> anyhow::Result<()> {
103    let cmd = match Xtask::from_env() {
104        Ok(cmd) => cmd,
105        Err(err) => {
106            if err.is_help() {
107                println!("{}", Xtask::HELP_);
108                std::process::exit(0);
109            } else {
110                eprintln!("{err}");
111                std::process::exit(2);
112            }
113        }
114    };
115
116    match cmd.subcommand {
117        XtaskCmd::Test(args) => test(args, cmd.rust_fixpoint),
118        XtaskCmd::LeanBench(args) => lean_bench(args, cmd.rust_fixpoint),
119        XtaskCmd::Run(args) => run(args, cmd.rust_fixpoint),
120        XtaskCmd::Install(args) => install(&args, cmd.offline, cmd.rust_fixpoint),
121        XtaskCmd::Doc(args) => doc(args),
122        XtaskCmd::BuildSysroot(_) => {
123            let config = SysrootConfig {
124                profile: Profile::Dev,
125                rust_fixpoint: cmd.rust_fixpoint,
126                dst: local_sysroot_dir()?,
127                build_libs: BuildLibs { force: true, libs: FluxLib::ALL },
128            };
129            install_sysroot(&config)?;
130            Ok(())
131        }
132        XtaskCmd::Uninstall(_) => uninstall(),
133        XtaskCmd::Expand(args) => expand(args),
134    }
135}
136
137fn run_tests(
138    flux_driver: &Utf8Path,
139    sysroot: &Path,
140    suite: &str,
141    filter: Option<&str>,
142    fixpoint_timeout: Option<u64>,
143) -> anyhow::Result<()> {
144    Command::new("cargo")
145        .args(["test", "-p", "tests", "--"])
146        .args(["--flux-driver", flux_driver.as_str()])
147        .args(["--sysroot".as_ref(), sysroot.as_os_str()])
148        .args(["--suite", suite])
149        .args_if_some(filter, |filter| ["--filter", filter])
150        .args_if_some(fixpoint_timeout, |timeout| {
151            ["--fixpoint-timeout".to_string(), timeout.to_string()]
152        })
153        .run()
154}
155
156fn test(args: Test, rust_fixpoint: bool) -> anyhow::Result<()> {
157    let dst = local_sysroot_dir()?;
158
159    let suites: &[Suite] = if args.suite.is_empty() { Suite::ALL } else { &args.suite };
160
161    for suite in suites {
162        let libs = match suite {
163            Suite::Basic => &[FluxLib::FluxAttrs],
164            Suite::WithDeps => FluxLib::ALL,
165        };
166        let config = SysrootConfig {
167            profile: Profile::Dev,
168            rust_fixpoint,
169            dst: dst.clone(),
170            build_libs: BuildLibs { force: false, libs },
171        };
172        let flux_driver = install_sysroot(&config)?;
173        run_tests(&flux_driver, &dst, suite.name(), args.filter.as_deref(), args.fixpoint_timeout)?;
174    }
175    Ok(())
176}
177
178fn lean_bench(args: LeanBench, rust_fixpoint: bool) -> anyhow::Result<()> {
179    use walkdir::WalkDir;
180
181    let config = SysrootConfig {
182        profile: Profile::Dev,
183        rust_fixpoint,
184        dst: local_sysroot_dir()?,
185        build_libs: BuildLibs { force: false, libs: FluxLib::ALL },
186    };
187    let flux_driver = install_sysroot(&config)?;
188
189    let pos_path = PathBuf::from("tests/tests/pos");
190    let lean_bench_dir = PathBuf::from("tests/lean_bench");
191
192    if !pos_path.exists() {
193        return Err(anyhow!("tests/tests/pos directory not found"));
194    }
195
196    // Find all .rs test files
197    let test_files: Vec<PathBuf> = WalkDir::new(&pos_path)
198        .into_iter()
199        .filter_map(|e| e.ok())
200        .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
201        .map(|e| e.path().to_path_buf())
202        .filter(|path| {
203            // Apply filter if specified
204            if let Some(ref filter) = args.filter {
205                path.to_string_lossy().contains(filter)
206            } else {
207                true
208            }
209        })
210        .collect();
211
212    if test_files.is_empty() {
213        if args.filter.is_some() {
214            eprintln!("No test files found matching filter: {:?}", args.filter);
215        } else {
216            eprintln!("No test files found under {:?}", pos_path);
217        }
218        return Ok(());
219    }
220
221    eprintln!("Found {} test files", test_files.len());
222    eprintln!("{}", "-".repeat(60));
223
224    let mut failures: Vec<(PathBuf, String)> = Vec::new();
225    let mut successes = 0;
226
227    for (i, test_path) in test_files.iter().enumerate() {
228        let rel_path = test_path.strip_prefix(&pos_path).unwrap();
229
230        // Create lean output dir: ./tests/lean_bench/<path>/<to>/<file>/
231        let mut lean_dir = lean_bench_dir.clone();
232        if let Some(parent) = rel_path.parent() {
233            if parent != Path::new("") {
234                lean_dir.push(parent);
235            }
236        }
237        if let Some(stem) = rel_path.file_stem() {
238            lean_dir.push(stem);
239        }
240
241        eprint!("[{}/{}] Running: {} ... ", i + 1, test_files.len(), rel_path.display());
242
243        // Create the output directory
244        if let Err(e) = fs::create_dir_all(&lean_dir) {
245            eprintln!("ERROR");
246            failures.push((test_path.clone(), format!("Failed to create directory: {}", e)));
247            continue;
248        }
249
250        // Build rustc flags
251        let mut rustc_flags =
252            flux_dev::default_flags(&config.dst, SysrootManifest::extern_args(&config.dst));
253        rustc_flags.push("-Flean=emit".to_string());
254        rustc_flags.push(format!("-Flean-dir={}", lean_dir.display()));
255
256        // Run the test
257        let result = Command::new(&flux_driver)
258            .args(&rustc_flags)
259            .arg(test_path)
260            .env(FLUX_SYSROOT, &config.dst)
261            .stdout(std::process::Stdio::null())
262            .stderr(std::process::Stdio::piped())
263            .output();
264
265        match result {
266            Ok(output) if output.status.success() => {
267                eprintln!("OK");
268                successes += 1;
269            }
270            Ok(output) => {
271                eprintln!("ERROR");
272                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
273                failures.push((test_path.clone(), stderr));
274            }
275            Err(e) => {
276                eprintln!("ERROR");
277                failures.push((test_path.clone(), e.to_string()));
278            }
279        }
280    }
281
282    // Print summary
283    eprintln!();
284    eprintln!("{}", "=".repeat(60));
285    eprintln!("SUMMARY");
286    eprintln!("{}", "=".repeat(60));
287    eprintln!("Total tests run: {}", test_files.len());
288    eprintln!("Passed: {}", successes);
289    eprintln!("Failed: {}", failures.len());
290
291    if !failures.is_empty() {
292        eprintln!();
293        eprintln!("Failed tests:");
294        for (path, _) in &failures {
295            let rel_path = path.strip_prefix(&pos_path).unwrap_or(path);
296            eprintln!("  - {}", rel_path.display());
297        }
298        eprintln!("{}", "=".repeat(60));
299        return Err(anyhow!("{} test(s) failed", failures.len()));
300    }
301
302    eprintln!("{}", "=".repeat(60));
303    Ok(())
304}
305
306fn run(args: Run, rust_fixpoint: bool) -> anyhow::Result<()> {
307    let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
308    run_inner(
309        args.input,
310        BuildLibs { force: false, libs },
311        ["-Ztrack-diagnostics=y".to_string()]
312            .into_iter()
313            .chain(args.opts),
314        rust_fixpoint,
315    )?;
316    Ok(())
317}
318
319fn expand(args: Expand) -> Result<(), anyhow::Error> {
320    run_inner(
321        args.input,
322        BuildLibs { force: false, libs: &[FluxLib::FluxRs] },
323        ["-Zunpretty=expanded".to_string()],
324        false,
325    )?;
326    Ok(())
327}
328
329fn run_inner(
330    input: PathBuf,
331    build_libs: BuildLibs,
332    flags: impl IntoIterator<Item = String>,
333    rust_fixpoint: bool,
334) -> Result<(), anyhow::Error> {
335    let config = SysrootConfig {
336        profile: Profile::Dev,
337        rust_fixpoint,
338        dst: local_sysroot_dir()?,
339        build_libs,
340    };
341
342    let flux_driver = install_sysroot(&config)?;
343
344    let mut rustc_flags =
345        flux_dev::default_flags(&config.dst, SysrootManifest::extern_args(&config.dst));
346    rustc_flags.extend(flags);
347
348    Command::new(flux_driver)
349        .args(&rustc_flags)
350        .arg(&input)
351        .env(FLUX_SYSROOT, &config.dst)
352        .run()
353}
354
355fn install(args: &Install, offline: bool, rust_fixpoint: bool) -> anyhow::Result<()> {
356    let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
357    let config = SysrootConfig {
358        profile: args.profile(),
359        rust_fixpoint,
360        dst: default_flux_sysroot_dir(),
361        build_libs: BuildLibs { force: false, libs },
362    };
363    install_sysroot(&config)?;
364    // Pass the toolchain explicitly, otherwise `cargo install` warns that the default toolchain
365    // was implicitly overridden.
366    Command::new("cargo")
367        .args_if_some(env::var("RUSTUP_TOOLCHAIN").ok(), |toolchain| [format!("+{toolchain}")])
368        .args(["install", "--path", "crates/flux-bin", "--force"])
369        .args_if(offline, ["--offline"])
370        .run()
371}
372
373fn uninstall() -> anyhow::Result<()> {
374    Command::new("cargo")
375        .args(["uninstall", "-p", "flux-bin"])
376        .run()?;
377    let sysroot = default_flux_sysroot_dir();
378    eprintln!("$ rm -rf {}", sysroot.display());
379    remove_path(&sysroot)?;
380    Ok(())
381}
382
383fn doc(_args: Doc) -> anyhow::Result<()> {
384    Command::new("cargo")
385        .args(["doc", "--workspace", "--document-private-items", "--no-deps"])
386        .env("RUSTDOCFLAGS", "-Zunstable-options --enable-index-page")
387        .run()?;
388    Ok(())
389}
390
391fn build_binary(bin: &str, profile: Profile, rust_fixpoint: bool) -> anyhow::Result<Utf8PathBuf> {
392    Command::new("cargo")
393        .args(["build", "--bin", bin, "--profile", profile.as_str()])
394        .args_if(rust_fixpoint, ["--features", "rust-fixpoint"])
395        .run_with_cargo_metadata()?
396        .into_iter()
397        .find(|artifact| artifact.target.name == bin && artifact.target.is_kind(TargetKind::Bin))
398        .and_then(|artifact| artifact.executable)
399        .ok_or_else(|| anyhow!("cannot find binary: `{bin}`"))
400}
401
402struct SysrootConfig {
403    /// Profile used to build `flux-driver` and libraries
404    profile: Profile,
405    /// Whether rust-fixpoint should be enabled to build `flux-driver`
406    rust_fixpoint: bool,
407    /// Destination path for sysroot artifacts
408    dst: PathBuf,
409    build_libs: BuildLibs,
410}
411
412struct BuildLibs {
413    /// If true, forces a clean build.
414    force: bool,
415    /// List of libraries to install
416    libs: &'static [FluxLib],
417}
418
419#[allow(clippy::enum_variant_names)]
420#[derive(Clone, Copy)]
421enum FluxLib {
422    FluxAlloc,
423    FluxAttrs,
424    FluxCore,
425    FluxRs,
426}
427
428impl FluxLib {
429    const ALL: &[FluxLib] = &[Self::FluxAlloc, Self::FluxAttrs, Self::FluxCore, Self::FluxRs];
430
431    const _ASSERT_ALL: () = { assert!(Self::ALL.len() == variant_count::<Self>()) };
432
433    const fn package_name(self) -> &'static str {
434        match self {
435            FluxLib::FluxAlloc => "flux-alloc",
436            FluxLib::FluxAttrs => "flux-attrs",
437            FluxLib::FluxCore => "flux-core",
438            FluxLib::FluxRs => "flux-rs",
439        }
440    }
441
442    const fn target_name(self) -> &'static str {
443        match self {
444            FluxLib::FluxAlloc => "flux_alloc",
445            FluxLib::FluxAttrs => "flux_attrs",
446            FluxLib::FluxCore => "flux_core",
447            FluxLib::FluxRs => "flux_rs",
448        }
449    }
450
451    /// Whether this lib's specs are injected as force externs under `-Fstd-extern-specs`.
452    const fn is_std_extern_spec(self) -> bool {
453        match self {
454            FluxLib::FluxCore | FluxLib::FluxAlloc => true,
455            FluxLib::FluxAttrs | FluxLib::FluxRs => false,
456        }
457    }
458
459    fn from_artifact(artifact: &Artifact) -> Option<FluxLib> {
460        Self::ALL
461            .iter()
462            .copied()
463            .find(|lib| artifact.target.name == lib.target_name())
464    }
465}
466
467fn install_sysroot(config: &SysrootConfig) -> anyhow::Result<Utf8PathBuf> {
468    remove_path(&config.dst)?;
469    create_dir(&config.dst)?;
470
471    let flux_driver = build_binary("flux-driver", config.profile, config.rust_fixpoint)?;
472    copy_file(&flux_driver, &config.dst)?;
473
474    let cargo_flux = build_binary("cargo-flux", config.profile, config.rust_fixpoint)?;
475
476    if config.build_libs.force {
477        Command::new(&cargo_flux)
478            .args(["flux", "clean"])
479            .env(FLUX_SYSROOT, &config.dst)
480            .run()?;
481    }
482    let artifacts = Command::new(&cargo_flux)
483        .args(["flux", "build"])
484        .args(
485            config
486                .build_libs
487                .libs
488                .iter()
489                .flat_map(|lib| ["-p", lib.package_name()]),
490        )
491        .env(FLUX_SYSROOT, &config.dst)
492        .run_with_cargo_metadata()?;
493    let manifest = copy_artifacts(&artifacts, &config.dst)?;
494    write_sysroot_toml(&manifest, &config.dst)?;
495    Ok(flux_driver)
496}
497
498fn copy_artifacts(artifacts: &[Artifact], sysroot: &Path) -> anyhow::Result<SysrootManifest> {
499    let mut manifest = SysrootManifest::default();
500    for artifact in artifacts {
501        let Some(lib) = FluxLib::from_artifact(artifact) else { continue };
502
503        let entry = manifest
504            .crates
505            .entry(lib.target_name().to_string())
506            .or_default();
507        entry.extern_spec = lib.is_std_extern_spec();
508
509        for filename in &artifact.filenames {
510            copy_artifact(artifact, filename, sysroot, entry)?;
511        }
512    }
513    Ok(manifest)
514}
515
516fn copy_artifact(
517    artifact: &Artifact,
518    filename: &Utf8Path,
519    dst: &Path,
520    entry: &mut SysrootCrate,
521) -> anyhow::Result<()> {
522    let name = sysroot_file_name(artifact, filename)?;
523    copy_file(filename, dst.join(&name))?;
524    record_artifact(entry, &name);
525
526    if filename.extension() == Some("rmeta") {
527        // Flux finds its own metadata by swapping the extension on the resolved `.rmeta` path,
528        // so the two must travel together.
529        let fluxmeta = filename.with_extension("fluxmeta");
530        if fluxmeta.exists() {
531            let name = sysroot_file_name(artifact, &fluxmeta)?;
532            copy_file(&fluxmeta, dst.join(&name))?;
533            record_artifact(entry, &name);
534        }
535    }
536    Ok(())
537}
538
539fn record_artifact(entry: &mut SysrootCrate, name: &str) {
540    let slot = match Utf8Path::new(name).extension() {
541        Some("rlib") => &mut entry.rlib,
542        Some("rmeta") => &mut entry.rmeta,
543        Some("fluxmeta") => &mut entry.fluxmeta,
544        // Proc macros are always built for the host. Anything else cargo reports, like `.pdb`
545        // debug symbols on Windows, is not a crate.
546        Some(extension) if extension == env::consts::DLL_EXTENSION => &mut entry.dylib,
547        _ => return,
548    };
549    *slot = Some(name.to_string());
550}
551
552/// Name an artifact gets inside the sysroot.
553///
554/// Cargo reports an uplifted, hash-free `.rlib` for these crates but a hashed `.rmeta`, and rustc
555/// pairs the two by the infix between `lib<crate>` and the extension, so we cannot reuse cargo's
556/// names. Naming by crate keeps the pairing intact.
557fn sysroot_file_name(artifact: &Artifact, filename: &Utf8Path) -> anyhow::Result<String> {
558    let krate = &artifact.target.name;
559    let name = filename
560        .file_name()
561        .ok_or_else(|| anyhow!("artifact `{filename}` has no file name"))?;
562    let extension = filename
563        .extension()
564        .ok_or_else(|| anyhow!("artifact `{filename}` has no extension"))?;
565    match extension {
566        // rustc hardcodes the `lib` prefix for its own formats on every target.
567        "rlib" | "rmeta" | "fluxmeta" => Ok(format!("lib{krate}.{extension}")),
568        // A dylib's prefix is target-dependent (empty on Windows), so recover it from the name
569        // cargo produced.
570        _ => {
571            let prefix = name
572                .find(krate.as_str())
573                .map(|idx| &name[..idx])
574                .ok_or_else(|| anyhow!("artifact `{name}` does not mention crate `{krate}`"))?;
575            Ok(format!("{prefix}{krate}.{extension}"))
576        }
577    }
578}
579
580fn write_sysroot_toml(manifest: &SysrootManifest, sysroot: &Path) -> anyhow::Result<()> {
581    if manifest.crates.is_empty() {
582        return Ok(());
583    }
584    let content = toml::to_string(manifest)?;
585    let path = sysroot.join(SYSROOT_MANIFEST);
586    eprintln!("$ write {}", path.display());
587    fs::write(&path, &content).map_err(|e| anyhow!("failed to write `{}`: {e}", path.display()))
588}
589
590impl Install {
591    fn profile(&self) -> Profile {
592        self.profile.unwrap_or(Profile::Release)
593    }
594}
595
596fn local_sysroot_dir() -> anyhow::Result<PathBuf> {
597    Ok(Path::new(file!())
598        .canonicalize()?
599        .ancestors()
600        .nth(3)
601        .unwrap()
602        .join("sysroot"))
603}
604
605fn check_status(st: ExitStatus) -> anyhow::Result<()> {
606    if st.success() {
607        return Ok(());
608    }
609    let err = match st.code() {
610        Some(code) => anyhow!("command exited with non-zero code: {code}"),
611        #[cfg(unix)]
612        None => {
613            use std::os::unix::process::ExitStatusExt;
614            match st.signal() {
615                Some(sig) => anyhow!("command was terminated by a signal: {sig}"),
616                None => anyhow!("command was terminated by a signal"),
617            }
618        }
619        #[cfg(not(unix))]
620        None => anyhow!("command was terminated by a signal"),
621    };
622    Err(err)
623}
624
625fn display_command(cmd: &Command) {
626    for var in cmd.get_envs() {
627        if let Some(val) = var.1 {
628            eprintln!("$ export {}={}", var.0.display(), val.display());
629        }
630    }
631
632    let prog = cmd.get_program();
633    eprint!("$ {}", prog.display());
634    for arg in cmd.get_args() {
635        eprint!(" {}", arg.display());
636    }
637    eprintln!();
638}
639
640fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(src: S, dst: D) -> anyhow::Result<()> {
641    let src = src.as_ref();
642    let dst = dst.as_ref();
643    eprintln!("$ cp {} {}", src.display(), dst.display());
644
645    let mut _tmp;
646    let mut dst = dst;
647    if dst.is_dir() {
648        if let Some(file_name) = src.file_name() {
649            _tmp = dst.join(file_name);
650            dst = &_tmp;
651        }
652    }
653    std::fs::copy(src, dst).map_err(|err| {
654        anyhow!("failed to copy `{}` to `{}`: {err}", src.display(), dst.display())
655    })?;
656
657    Ok(())
658}
659
660trait CommandExt {
661    fn run(&mut self) -> anyhow::Result<()>;
662    fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>>;
663    /// Add `args` if `cond` is true.
664    fn args_if<I, S>(&mut self, cond: bool, args: I) -> &mut Self
665    where
666        I: IntoIterator<Item = S>,
667        S: AsRef<OsStr>;
668    /// If `opt` is `Some`, add the arguments computed by `f` from its value.
669    fn args_if_some<T, I, S>(&mut self, opt: Option<T>, f: impl FnOnce(T) -> I) -> &mut Self
670    where
671        I: IntoIterator<Item = S>,
672        S: AsRef<OsStr>;
673}
674
675impl CommandExt for Command {
676    fn args_if<I, S>(&mut self, cond: bool, args: I) -> &mut Self
677    where
678        I: IntoIterator<Item = S>,
679        S: AsRef<OsStr>,
680    {
681        if cond {
682            self.args(args);
683        }
684        self
685    }
686
687    fn args_if_some<T, I, S>(&mut self, opt: Option<T>, f: impl FnOnce(T) -> I) -> &mut Self
688    where
689        I: IntoIterator<Item = S>,
690        S: AsRef<OsStr>,
691    {
692        if let Some(val) = opt {
693            self.args(f(val));
694        }
695        self
696    }
697
698    fn run(&mut self) -> anyhow::Result<()> {
699        display_command(self);
700        let mut child = self.spawn()?;
701        check_status(child.wait()?)
702    }
703
704    fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>> {
705        self.arg("--message-format=json-render-diagnostics")
706            .stdout(std::process::Stdio::piped());
707
708        display_command(self);
709
710        let mut child = self.spawn()?;
711
712        let mut artifacts = vec![];
713        let reader = std::io::BufReader::new(child.stdout.take().unwrap());
714        for message in cargo_metadata::Message::parse_stream(reader) {
715            match message.unwrap() {
716                Message::CompilerMessage(msg) => {
717                    println!("{msg}");
718                }
719                Message::CompilerArtifact(artifact) => {
720                    artifacts.push(artifact);
721                }
722                _ => (),
723            }
724        }
725
726        check_status(child.wait()?)?;
727
728        Ok(artifacts)
729    }
730}
731
732fn remove_path(path: &Path) -> anyhow::Result<()> {
733    match path.metadata() {
734        Ok(meta) => {
735            if meta.is_dir() { remove_dir_all(path) } else { fs::remove_file(path) }
736                .map_err(|err| anyhow!("failed to remove path `{}`: {err}", path.display()))
737        }
738        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
739        Err(err) => Err(anyhow!("failed to remove path `{}`: {err}", path.display())),
740    }
741}
742
743#[cfg(not(windows))]
744fn remove_dir_all(path: &Path) -> io::Result<()> {
745    std::fs::remove_dir_all(path)
746}
747
748// Copied from xshell
749#[cfg(windows)]
750fn remove_dir_all(path: &Path) -> io::Result<()> {
751    for _ in 0..99 {
752        if fs::remove_dir_all(path).is_ok() {
753            return Ok(());
754        }
755        std::thread::sleep(std::time::Duration::from_millis(10))
756    }
757    fs::remove_dir_all(path)
758}
759
760fn create_dir(path: &Path) -> anyhow::Result<()> {
761    match fs::create_dir_all(path) {
762        Ok(()) => Ok(()),
763        Err(err) => Err(anyhow!("failed to create directory `{}`: {err}", path.display())),
764    }
765}