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