Skip to main content

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