Skip to main content

xtask/
main.rs

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