xtask/
main.rs

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