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 optional --suggestions
26
27 cmd test {
29 optional filter: String
31 repeated --suite suite: Suite
33 }
34 cmd lean-bench {
36 optional filter: String
38 }
39 cmd run {
41 required input: PathBuf
43 repeated opts: String
45 optional --no-extern-specs
47 }
48 cmd expand {
50 required input: PathBuf
52 }
53 cmd install {
55 optional --profile profile: Profile
57 optional --no-extern-specs
59 }
60 cmd uninstall { }
62 cmd build-sysroot { }
64 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 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 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 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 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 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 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 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: Profile,
422 rust_fixpoint: bool,
424 suggestions: bool,
426 dst: PathBuf,
428 build_libs: BuildLibs,
429}
430
431struct BuildLibs {
432 force: bool,
434 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 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#[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}