1#![feature(variant_count)]
2
3use std::{
4 ffi::OsStr,
5 fs, io,
6 mem::variant_count,
7 path::{Path, PathBuf},
8 process::{Command, ExitStatus},
9};
10
11use anyhow::anyhow;
12use cargo_metadata::{
13 camino::{Utf8Path, Utf8PathBuf},
14 Artifact, Message, TargetKind,
15};
16use tests::{FLUX_SYSROOT, FLUX_SYSROOT_TEST};
17
18xflags::xflags! {
19 cmd xtask {
20 optional --offline
22 optional --rust-fixpoint
24
25 cmd test {
27 optional filter: String
29 optional --no-lib-tests
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 println!("{}", Xtask::HELP_);
102 if err.is_help() {
103 std::process::exit(0);
104 } else {
105 println!("{}", Xtask::HELP_);
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, tests: 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 test(args: Test, rust_fixpoint: bool) -> anyhow::Result<()> {
137 let config = SysrootConfig {
138 profile: Profile::Dev,
139 rust_fixpoint,
140 dst: local_sysroot_dir()?,
141 build_libs: BuildLibs { force: false, tests: !args.no_lib_tests, libs: FluxLib::ALL },
142 };
143 let flux = build_binary("flux", config.profile, false)?;
144 install_sysroot(&config)?;
145
146 Command::new("cargo")
147 .args(["test", "-p", "tests", "--"])
148 .args(["--flux", flux.as_str()])
149 .args(["--sysroot".as_ref(), config.dst.as_os_str()])
150 .map_opt(args.filter.as_ref(), |filter, cmd| {
151 cmd.args(["--filter", filter]);
152 })
153 .run()
154}
155
156fn lean_bench(args: LeanBench, rust_fixpoint: bool) -> anyhow::Result<()> {
157 use walkdir::WalkDir;
158
159 let config = SysrootConfig {
160 profile: Profile::Dev,
161 rust_fixpoint,
162 dst: local_sysroot_dir()?,
163 build_libs: BuildLibs { force: false, tests: false, libs: FluxLib::ALL },
164 };
165 install_sysroot(&config)?;
166 let flux = build_binary("flux", config.profile, false)?;
167
168 let pos_path = PathBuf::from("tests/tests/pos");
169 let lean_bench_dir = PathBuf::from("tests/lean_bench");
170
171 if !pos_path.exists() {
172 return Err(anyhow!("tests/tests/pos directory not found"));
173 }
174
175 let test_files: Vec<PathBuf> = WalkDir::new(&pos_path)
177 .into_iter()
178 .filter_map(|e| e.ok())
179 .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
180 .map(|e| e.path().to_path_buf())
181 .filter(|path| {
182 if let Some(ref filter) = args.filter {
184 path.to_string_lossy().contains(filter)
185 } else {
186 true
187 }
188 })
189 .collect();
190
191 if test_files.is_empty() {
192 if args.filter.is_some() {
193 eprintln!("No test files found matching filter: {:?}", args.filter);
194 } else {
195 eprintln!("No test files found under {:?}", pos_path);
196 }
197 return Ok(());
198 }
199
200 eprintln!("Found {} test files", test_files.len());
201 eprintln!("{}", "-".repeat(60));
202
203 let mut failures: Vec<(PathBuf, String)> = Vec::new();
204 let mut successes = 0;
205
206 for (i, test_path) in test_files.iter().enumerate() {
207 let rel_path = test_path.strip_prefix(&pos_path).unwrap();
208
209 let mut lean_dir = lean_bench_dir.clone();
211 if let Some(parent) = rel_path.parent() {
212 if parent != Path::new("") {
213 lean_dir.push(parent);
214 }
215 }
216 if let Some(stem) = rel_path.file_stem() {
217 lean_dir.push(stem);
218 }
219
220 eprint!("[{}/{}] Running: {} ... ", i + 1, test_files.len(), rel_path.display());
221
222 if let Err(e) = fs::create_dir_all(&lean_dir) {
224 eprintln!("ERROR");
225 failures.push((test_path.clone(), format!("Failed to create directory: {}", e)));
226 continue;
227 }
228
229 let mut rustc_flags = tests::default_flags();
231 rustc_flags.push("-Flean=emit".to_string());
232 rustc_flags.push(format!("-Flean-dir={}", lean_dir.display()));
233
234 let result = Command::new(&flux)
236 .args(&rustc_flags)
237 .arg(test_path)
238 .env(FLUX_SYSROOT, &config.dst)
239 .stdout(std::process::Stdio::null())
240 .stderr(std::process::Stdio::piped())
241 .output();
242
243 match result {
244 Ok(output) if output.status.success() => {
245 eprintln!("OK");
246 successes += 1;
247 }
248 Ok(output) => {
249 eprintln!("ERROR");
250 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
251 failures.push((test_path.clone(), stderr));
252 }
253 Err(e) => {
254 eprintln!("ERROR");
255 failures.push((test_path.clone(), e.to_string()));
256 }
257 }
258 }
259
260 eprintln!();
262 eprintln!("{}", "=".repeat(60));
263 eprintln!("SUMMARY");
264 eprintln!("{}", "=".repeat(60));
265 eprintln!("Total tests run: {}", test_files.len());
266 eprintln!("Passed: {}", successes);
267 eprintln!("Failed: {}", failures.len());
268
269 if !failures.is_empty() {
270 eprintln!();
271 eprintln!("Failed tests:");
272 for (path, _) in &failures {
273 let rel_path = path.strip_prefix(&pos_path).unwrap_or(path);
274 eprintln!(" - {}", rel_path.display());
275 }
276 eprintln!("{}", "=".repeat(60));
277 return Err(anyhow!("{} test(s) failed", failures.len()));
278 }
279
280 eprintln!("{}", "=".repeat(60));
281 Ok(())
282}
283
284fn run(args: Run, rust_fixpoint: bool) -> anyhow::Result<()> {
285 let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
286 run_inner(
287 args.input,
288 BuildLibs { force: false, tests: false, libs },
289 ["-Ztrack-diagnostics=y".to_string()]
290 .into_iter()
291 .chain(args.opts),
292 rust_fixpoint,
293 )?;
294 Ok(())
295}
296
297fn expand(args: Expand) -> Result<(), anyhow::Error> {
298 run_inner(
299 args.input,
300 BuildLibs { force: false, tests: false, libs: &[FluxLib::FluxRs] },
301 ["-Zunpretty=expanded".to_string()],
302 false,
303 )?;
304 Ok(())
305}
306
307fn run_inner(
308 input: PathBuf,
309 build_libs: BuildLibs,
310 flags: impl IntoIterator<Item = String>,
311 rust_fixpoint: bool,
312) -> Result<(), anyhow::Error> {
313 let config = SysrootConfig {
314 profile: Profile::Dev,
315 rust_fixpoint,
316 dst: local_sysroot_dir()?,
317 build_libs,
318 };
319
320 install_sysroot(&config)?;
321 let flux = build_binary("flux", config.profile, false)?;
322
323 let mut rustc_flags = tests::default_flags();
324 rustc_flags.extend(flags);
325
326 Command::new(flux)
327 .args(&rustc_flags)
328 .arg(&input)
329 .env(FLUX_SYSROOT, &config.dst)
330 .run()
331}
332
333fn install(args: &Install, extra: &[&str], rust_fixpoint: bool) -> anyhow::Result<()> {
334 let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
335 let config = SysrootConfig {
336 profile: args.profile(),
337 rust_fixpoint,
338 dst: default_sysroot_dir(),
339 build_libs: BuildLibs { force: false, tests: false, libs },
340 };
341 install_sysroot(&config)?;
342 Command::new("cargo")
343 .args(["install", "--path", "crates/flux-bin", "--force"])
344 .args(extra)
345 .run()
346}
347
348fn uninstall() -> anyhow::Result<()> {
349 Command::new("cargo")
350 .args(["uninstall", "-p", "flux-bin"])
351 .run()?;
352 eprintln!("$ rm -rf ~/.flux");
353 remove_path(&default_sysroot_dir())?;
354 Ok(())
355}
356
357fn doc(_args: Doc) -> anyhow::Result<()> {
358 Command::new("cargo")
359 .args(["doc", "--workspace", "--document-private-items", "--no-deps"])
360 .env("RUSTDOCFLAGS", "-Zunstable-options --enable-index-page")
361 .run()?;
362 Ok(())
363}
364
365fn build_binary(bin: &str, profile: Profile, rust_fixpoint: bool) -> anyhow::Result<Utf8PathBuf> {
366 let mut args = vec!["build", "--bin", bin, "--profile", profile.as_str()];
367 if rust_fixpoint {
368 args.extend_from_slice(&["--features", "rust-fixpoint"]);
369 }
370 Command::new("cargo")
371 .args(&args)
372 .run_with_cargo_metadata()?
373 .into_iter()
374 .find(|artifact| artifact.target.name == bin && artifact.target.is_kind(TargetKind::Bin))
375 .and_then(|artifact| artifact.executable)
376 .ok_or_else(|| anyhow!("cannot find binary: `{bin}`"))
377}
378
379struct SysrootConfig {
380 profile: Profile,
382 rust_fixpoint: bool,
384 dst: PathBuf,
386 build_libs: BuildLibs,
387}
388
389struct BuildLibs {
390 force: bool,
392 tests: bool,
394 libs: &'static [FluxLib],
396}
397
398#[allow(clippy::enum_variant_names)]
399#[derive(Clone, Copy)]
400enum FluxLib {
401 FluxAlloc,
402 FluxAttrs,
403 FluxCore,
404 FluxRs,
405}
406
407impl FluxLib {
408 const ALL: &[FluxLib] = &[Self::FluxAlloc, Self::FluxAttrs, Self::FluxCore, Self::FluxRs];
409
410 const _ASSERT_ALL: () = { assert!(Self::ALL.len() == variant_count::<Self>()) };
411
412 const fn package_name(self) -> &'static str {
413 match self {
414 FluxLib::FluxAlloc => "flux-alloc",
415 FluxLib::FluxAttrs => "flux-attrs",
416 FluxLib::FluxCore => "flux-core",
417 FluxLib::FluxRs => "flux-rs",
418 }
419 }
420
421 const fn target_name(self) -> &'static str {
422 match self {
423 FluxLib::FluxAlloc => "flux_alloc",
424 FluxLib::FluxAttrs => "flux_attrs",
425 FluxLib::FluxCore => "flux_core",
426 FluxLib::FluxRs => "flux_rs",
427 }
428 }
429
430 fn is_flux_lib(artifact: &Artifact) -> bool {
431 Self::ALL
432 .iter()
433 .any(|lib| artifact.target.name == lib.target_name())
434 }
435}
436
437fn install_sysroot(config: &SysrootConfig) -> anyhow::Result<()> {
438 remove_path(&config.dst)?;
439 create_dir(&config.dst)?;
440
441 copy_file(build_binary("flux-driver", config.profile, config.rust_fixpoint)?, &config.dst)?;
442
443 let cargo_flux = build_binary("cargo-flux", config.profile, config.rust_fixpoint)?;
444
445 if config.build_libs.force {
446 Command::new(&cargo_flux)
447 .args(["flux", "clean"])
448 .env(FLUX_SYSROOT, &config.dst)
449 .run()?;
450 }
451
452 let artifacts = Command::new(cargo_flux)
453 .arg("flux")
454 .args(
455 config
456 .build_libs
457 .libs
458 .iter()
459 .flat_map(|lib| ["-p", lib.package_name()]),
460 )
461 .env(FLUX_SYSROOT, &config.dst)
462 .env_if(config.build_libs.tests, FLUX_SYSROOT_TEST, "1")
463 .run_with_cargo_metadata()?;
464
465 copy_artifacts(&artifacts, &config.dst)?;
466 Ok(())
467}
468
469fn copy_artifacts(artifacts: &[Artifact], sysroot: &Path) -> anyhow::Result<()> {
470 for artifact in artifacts {
471 if !FluxLib::is_flux_lib(artifact) {
472 continue;
473 }
474
475 for filename in &artifact.filenames {
476 if artifact.target.is_kind(TargetKind::ProcMacro)
487 && filename.extension() == Some("rmeta")
488 {
489 continue;
490 }
491 copy_artifact(filename, sysroot)?;
492 }
493 }
494 Ok(())
495}
496
497fn copy_artifact(filename: &Utf8Path, dst: &Path) -> anyhow::Result<()> {
498 copy_file(filename, dst)?;
499 if filename.extension() == Some("rmeta") {
500 let fluxmeta = filename.with_extension("fluxmeta");
501 if fluxmeta.exists() {
502 copy_file(&fluxmeta, dst)?;
503 }
504 }
505 Ok(())
506}
507
508impl Install {
509 fn profile(&self) -> Profile {
510 self.profile.unwrap_or(Profile::Release)
511 }
512}
513
514fn default_sysroot_dir() -> PathBuf {
515 home::home_dir()
516 .expect("Couldn't find home directory")
517 .join(".flux")
518}
519
520fn local_sysroot_dir() -> anyhow::Result<PathBuf> {
521 Ok(Path::new(file!())
522 .canonicalize()?
523 .ancestors()
524 .nth(3)
525 .unwrap()
526 .join("sysroot"))
527}
528
529fn check_status(st: ExitStatus) -> anyhow::Result<()> {
530 if st.success() {
531 return Ok(());
532 }
533 let err = match st.code() {
534 Some(code) => anyhow!("command exited with non-zero code: {code}"),
535 #[cfg(unix)]
536 None => {
537 use std::os::unix::process::ExitStatusExt;
538 match st.signal() {
539 Some(sig) => anyhow!("command was terminated by a signal: {sig}"),
540 None => anyhow!("command was terminated by a signal"),
541 }
542 }
543 #[cfg(not(unix))]
544 None => anyhow!("command was terminated by a signal"),
545 };
546 Err(err)
547}
548
549fn display_command(cmd: &Command) {
550 for var in cmd.get_envs() {
551 if let Some(val) = var.1 {
552 eprintln!("$ export {}={}", var.0.display(), val.display());
553 }
554 }
555
556 let prog = cmd.get_program();
557 eprint!("$ {}", prog.display());
558 for arg in cmd.get_args() {
559 eprint!(" {}", arg.display());
560 }
561 eprintln!();
562}
563
564fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(src: S, dst: D) -> anyhow::Result<()> {
565 let src = src.as_ref();
566 let dst = dst.as_ref();
567 eprintln!("$ cp {} {}", src.display(), dst.display());
568
569 let mut _tmp;
570 let mut dst = dst;
571 if dst.is_dir() {
572 if let Some(file_name) = src.file_name() {
573 _tmp = dst.join(file_name);
574 dst = &_tmp;
575 }
576 }
577 std::fs::copy(src, dst).map_err(|err| {
578 anyhow!("failed to copy `{}` to `{}`: {err}", src.display(), dst.display())
579 })?;
580
581 Ok(())
582}
583
584trait CommandExt {
585 fn map_opt<T>(&mut self, b: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self;
586 fn run(&mut self) -> anyhow::Result<()>;
587 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
588 where
589 K: AsRef<OsStr>,
590 V: AsRef<OsStr>;
591 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>>;
592}
593
594impl CommandExt for Command {
595 fn map_opt<T>(&mut self, opt: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self {
596 if let Some(v) = opt {
597 f(v, self);
598 }
599 self
600 }
601
602 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
603 where
604 K: AsRef<OsStr>,
605 V: AsRef<OsStr>,
606 {
607 if b {
608 self.env(k, v);
609 }
610 self
611 }
612
613 fn run(&mut self) -> anyhow::Result<()> {
614 display_command(self);
615 let mut child = self.spawn()?;
616 check_status(child.wait()?)
617 }
618
619 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>> {
620 self.arg("--message-format=json-render-diagnostics")
621 .stdout(std::process::Stdio::piped());
622
623 display_command(self);
624
625 let mut child = self.spawn()?;
626
627 let mut artifacts = vec![];
628 let reader = std::io::BufReader::new(child.stdout.take().unwrap());
629 for message in cargo_metadata::Message::parse_stream(reader) {
630 match message.unwrap() {
631 Message::CompilerMessage(msg) => {
632 println!("{msg}");
633 }
634 Message::CompilerArtifact(artifact) => {
635 artifacts.push(artifact);
636 }
637 _ => (),
638 }
639 }
640
641 check_status(child.wait()?)?;
642
643 Ok(artifacts)
644 }
645}
646
647fn remove_path(path: &Path) -> anyhow::Result<()> {
648 match path.metadata() {
649 Ok(meta) => {
650 if meta.is_dir() { remove_dir_all(path) } else { fs::remove_file(path) }
651 .map_err(|err| anyhow!("failed to remove path `{}`: {err}", path.display()))
652 }
653 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
654 Err(err) => Err(anyhow!("failed to remove path `{}`: {err}", path.display())),
655 }
656}
657
658#[cfg(not(windows))]
659fn remove_dir_all(path: &Path) -> io::Result<()> {
660 std::fs::remove_dir_all(path)
661}
662
663#[cfg(windows)]
665fn remove_dir_all(path: &Path) -> io::Result<()> {
666 for _ in 0..99 {
667 if fs::remove_dir_all(path).is_ok() {
668 return Ok(());
669 }
670 std::thread::sleep(std::time::Duration::from_millis(10))
671 }
672 fs::remove_dir_all(path)
673}
674
675fn create_dir(path: &Path) -> anyhow::Result<()> {
676 match fs::create_dir_all(path) {
677 Ok(()) => Ok(()),
678 Err(err) => Err(anyhow!("failed to create directory `{}`: {err}", path.display())),
679 }
680}