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