xtask/
main.rs

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