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