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 optional --offline
22 optional --rust-fixpoint
24
25 cmd test {
27 optional filter: String
29 repeated --suite suite: Suite
31 }
32 cmd lean-bench {
34 optional filter: String
36 }
37 cmd run {
39 required input: PathBuf
41 repeated opts: String
43 optional --no-extern-specs
45 }
46 cmd expand {
48 required input: PathBuf
50 }
51 cmd install {
53 optional --profile profile: Profile
55 optional --no-extern-specs
57 }
58 cmd uninstall { }
60 cmd build-sysroot { }
62 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 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 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 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 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 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 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 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: Profile,
399 rust_fixpoint: bool,
401 dst: PathBuf,
403 build_libs: BuildLibs,
404}
405
406struct BuildLibs {
407 force: bool,
409 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 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#[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}