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 run {
34 required input: PathBuf
36 repeated opts: String
38 optional --no-extern-specs
40 }
41 cmd expand {
43 required input: PathBuf
45 }
46 cmd install {
48 optional --profile profile: Profile
50 optional --no-extern-specs
52 }
53 cmd uninstall { }
55 cmd build-sysroot { }
57 cmd doc { }
59 }
60}
61
62#[derive(Clone, Copy, Debug)]
63enum Profile {
64 Release,
65 Dev,
66 Profiling,
67}
68
69impl Profile {
70 fn as_str(self) -> &'static str {
71 match self {
72 Profile::Release => "release",
73 Profile::Dev => "dev",
74 Profile::Profiling => "profiling",
75 }
76 }
77}
78
79impl std::str::FromStr for Profile {
80 type Err = &'static str;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 match s {
84 "release" => Ok(Self::Release),
85 "dev" => Ok(Self::Dev),
86 "profiling" => Ok(Self::Profiling),
87 _ => Err("invalid profile"),
88 }
89 }
90}
91
92fn main() -> anyhow::Result<()> {
93 let cmd = match Xtask::from_env() {
94 Ok(cmd) => cmd,
95 Err(err) => {
96 println!("{}", Xtask::HELP_);
97 if err.is_help() {
98 std::process::exit(0);
99 } else {
100 println!("{}", Xtask::HELP_);
101 std::process::exit(2);
102 }
103 }
104 };
105
106 let mut extra = vec![];
107 if cmd.offline {
108 extra.push("--offline");
109 }
110 match cmd.subcommand {
111 XtaskCmd::Test(args) => test(args, cmd.rust_fixpoint),
112 XtaskCmd::Run(args) => run(args, cmd.rust_fixpoint),
113 XtaskCmd::Install(args) => install(&args, &extra, cmd.rust_fixpoint),
114 XtaskCmd::Doc(args) => doc(args),
115 XtaskCmd::BuildSysroot(_) => {
116 let config = SysrootConfig {
117 profile: Profile::Dev,
118 rust_fixpoint: cmd.rust_fixpoint,
119 dst: local_sysroot_dir()?,
120 build_libs: BuildLibs { force: true, tests: true, libs: FluxLib::ALL },
121 };
122 install_sysroot(&config)?;
123 Ok(())
124 }
125 XtaskCmd::Uninstall(_) => uninstall(),
126 XtaskCmd::Expand(args) => expand(args),
127 }
128}
129
130fn test(args: Test, rust_fixpoint: bool) -> anyhow::Result<()> {
131 let config = SysrootConfig {
132 profile: Profile::Dev,
133 rust_fixpoint,
134 dst: local_sysroot_dir()?,
135 build_libs: BuildLibs { force: false, tests: !args.no_lib_tests, libs: FluxLib::ALL },
136 };
137 let flux = build_binary("flux", config.profile, false)?;
138 install_sysroot(&config)?;
139
140 Command::new("cargo")
141 .args(["test", "-p", "tests", "--"])
142 .args(["--flux", flux.as_str()])
143 .args(["--sysroot".as_ref(), config.dst.as_os_str()])
144 .map_opt(args.filter.as_ref(), |filter, cmd| {
145 cmd.args(["--filter", filter]);
146 })
147 .run()
148}
149
150fn run(args: Run, rust_fixpoint: bool) -> anyhow::Result<()> {
151 let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
152 run_inner(
153 args.input,
154 BuildLibs { force: false, tests: false, libs },
155 ["-Ztrack-diagnostics=y".to_string()]
156 .into_iter()
157 .chain(args.opts),
158 rust_fixpoint,
159 )?;
160 Ok(())
161}
162
163fn expand(args: Expand) -> Result<(), anyhow::Error> {
164 run_inner(
165 args.input,
166 BuildLibs { force: false, tests: false, libs: &[FluxLib::FluxRs] },
167 ["-Zunpretty=expanded".to_string()],
168 false,
169 )?;
170 Ok(())
171}
172
173fn run_inner(
174 input: PathBuf,
175 build_libs: BuildLibs,
176 flags: impl IntoIterator<Item = String>,
177 rust_fixpoint: bool,
178) -> Result<(), anyhow::Error> {
179 let config = SysrootConfig {
180 profile: Profile::Dev,
181 rust_fixpoint,
182 dst: local_sysroot_dir()?,
183 build_libs,
184 };
185
186 install_sysroot(&config)?;
187 let flux = build_binary("flux", config.profile, false)?;
188
189 let mut rustc_flags = tests::default_flags();
190 rustc_flags.extend(flags);
191
192 Command::new(flux)
193 .args(&rustc_flags)
194 .arg(&input)
195 .env(FLUX_SYSROOT, &config.dst)
196 .run()
197}
198
199fn install(args: &Install, extra: &[&str], rust_fixpoint: bool) -> anyhow::Result<()> {
200 let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
201 let config = SysrootConfig {
202 profile: args.profile(),
203 rust_fixpoint,
204 dst: default_sysroot_dir(),
205 build_libs: BuildLibs { force: false, tests: false, libs },
206 };
207 install_sysroot(&config)?;
208 Command::new("cargo")
209 .args(["install", "--path", "crates/flux-bin", "--force"])
210 .args(extra)
211 .run()
212}
213
214fn uninstall() -> anyhow::Result<()> {
215 Command::new("cargo")
216 .args(["uninstall", "-p", "flux-bin"])
217 .run()?;
218 eprintln!("$ rm -rf ~/.flux");
219 remove_path(&default_sysroot_dir())?;
220 Ok(())
221}
222
223fn doc(_args: Doc) -> anyhow::Result<()> {
224 Command::new("cargo")
225 .args(["doc", "--workspace", "--document-private-items", "--no-deps"])
226 .env("RUSTDOCFLAGS", "-Zunstable-options --enable-index-page")
227 .run()?;
228 Ok(())
229}
230
231fn build_binary(bin: &str, profile: Profile, rust_fixpoint: bool) -> anyhow::Result<Utf8PathBuf> {
232 let mut args = vec!["build", "--bin", bin, "--profile", profile.as_str()];
233 if rust_fixpoint {
234 args.extend_from_slice(&["--features", "liquid-fixpoint/rust-fixpoint"]);
235 }
236 Command::new("cargo")
237 .args(&args)
238 .run_with_cargo_metadata()?
239 .into_iter()
240 .find(|artifact| artifact.target.name == bin && artifact.target.is_kind(TargetKind::Bin))
241 .and_then(|artifact| artifact.executable)
242 .ok_or_else(|| anyhow!("cannot find binary: `{bin}`"))
243}
244
245struct SysrootConfig {
246 profile: Profile,
248 rust_fixpoint: bool,
250 dst: PathBuf,
252 build_libs: BuildLibs,
253}
254
255struct BuildLibs {
256 force: bool,
258 tests: bool,
260 libs: &'static [FluxLib],
262}
263
264#[allow(clippy::enum_variant_names)]
265#[derive(Clone, Copy)]
266enum FluxLib {
267 FluxAlloc,
268 FluxAttrs,
269 FluxCore,
270 FluxRs,
271}
272
273impl FluxLib {
274 const ALL: &[FluxLib] = &[Self::FluxAlloc, Self::FluxAttrs, Self::FluxCore, Self::FluxRs];
275
276 const _ASSERT_ALL: () = { assert!(Self::ALL.len() == variant_count::<Self>()) };
277
278 const fn package_name(self) -> &'static str {
279 match self {
280 FluxLib::FluxAlloc => "flux-alloc",
281 FluxLib::FluxAttrs => "flux-attrs",
282 FluxLib::FluxCore => "flux-core",
283 FluxLib::FluxRs => "flux-rs",
284 }
285 }
286
287 const fn target_name(self) -> &'static str {
288 match self {
289 FluxLib::FluxAlloc => "flux_alloc",
290 FluxLib::FluxAttrs => "flux_attrs",
291 FluxLib::FluxCore => "flux_core",
292 FluxLib::FluxRs => "flux_rs",
293 }
294 }
295
296 fn is_flux_lib(artifact: &Artifact) -> bool {
297 Self::ALL
298 .iter()
299 .any(|lib| artifact.target.name == lib.target_name())
300 }
301}
302
303fn install_sysroot(config: &SysrootConfig) -> anyhow::Result<()> {
304 remove_path(&config.dst)?;
305 create_dir(&config.dst)?;
306
307 copy_file(build_binary("flux-driver", config.profile, config.rust_fixpoint)?, &config.dst)?;
308
309 let cargo_flux = build_binary("cargo-flux", config.profile, config.rust_fixpoint)?;
310
311 if config.build_libs.force {
312 Command::new(&cargo_flux)
313 .args(["flux", "clean"])
314 .env(FLUX_SYSROOT, &config.dst)
315 .run()?;
316 }
317
318 let artifacts = Command::new(cargo_flux)
319 .arg("flux")
320 .args(
321 config
322 .build_libs
323 .libs
324 .iter()
325 .flat_map(|lib| ["-p", lib.package_name()]),
326 )
327 .env(FLUX_SYSROOT, &config.dst)
328 .env_if(config.build_libs.tests, FLUX_SYSROOT_TEST, "1")
329 .run_with_cargo_metadata()?;
330
331 copy_artifacts(&artifacts, &config.dst)?;
332 Ok(())
333}
334
335fn copy_artifacts(artifacts: &[Artifact], sysroot: &Path) -> anyhow::Result<()> {
336 for artifact in artifacts {
337 if !FluxLib::is_flux_lib(artifact) {
338 continue;
339 }
340
341 for filename in &artifact.filenames {
342 copy_artifact(filename, sysroot)?;
343 }
344 }
345 Ok(())
346}
347
348fn copy_artifact(filename: &Utf8Path, dst: &Path) -> anyhow::Result<()> {
349 copy_file(filename, dst)?;
350 if filename.extension() == Some("rmeta") {
351 let fluxmeta = filename.with_extension("fluxmeta");
352 if fluxmeta.exists() {
353 copy_file(&fluxmeta, dst)?;
354 }
355 }
356 Ok(())
357}
358
359impl Install {
360 fn profile(&self) -> Profile {
361 self.profile.unwrap_or(Profile::Release)
362 }
363}
364
365fn default_sysroot_dir() -> PathBuf {
366 home::home_dir()
367 .expect("Couldn't find home directory")
368 .join(".flux")
369}
370
371fn local_sysroot_dir() -> anyhow::Result<PathBuf> {
372 Ok(Path::new(file!())
373 .canonicalize()?
374 .ancestors()
375 .nth(3)
376 .unwrap()
377 .join("sysroot"))
378}
379
380fn check_status(st: ExitStatus) -> anyhow::Result<()> {
381 if st.success() {
382 return Ok(());
383 }
384 let err = match st.code() {
385 Some(code) => anyhow!("command exited with non-zero code: {code}"),
386 #[cfg(unix)]
387 None => {
388 use std::os::unix::process::ExitStatusExt;
389 match st.signal() {
390 Some(sig) => anyhow!("command was terminated by a signal: {sig}"),
391 None => anyhow!("command was terminated by a signal"),
392 }
393 }
394 #[cfg(not(unix))]
395 None => anyhow!("command was terminated by a signal"),
396 };
397 Err(err)
398}
399
400fn display_command(cmd: &Command) {
401 for var in cmd.get_envs() {
402 if let Some(val) = var.1 {
403 eprintln!("$ export {}={}", var.0.display(), val.display());
404 }
405 }
406
407 let prog = cmd.get_program();
408 eprint!("$ {}", prog.display());
409 for arg in cmd.get_args() {
410 eprint!(" {}", arg.display());
411 }
412 eprintln!();
413}
414
415fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(src: S, dst: D) -> anyhow::Result<()> {
416 let src = src.as_ref();
417 let dst = dst.as_ref();
418 eprintln!("$ cp {} {}", src.display(), dst.display());
419
420 let mut _tmp;
421 let mut dst = dst;
422 if dst.is_dir() {
423 if let Some(file_name) = src.file_name() {
424 _tmp = dst.join(file_name);
425 dst = &_tmp;
426 }
427 }
428 std::fs::copy(src, dst).map_err(|err| {
429 anyhow!("failed to copy `{}` to `{}`: {err}", src.display(), dst.display())
430 })?;
431
432 Ok(())
433}
434
435trait CommandExt {
436 fn map_opt<T>(&mut self, b: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self;
437 fn run(&mut self) -> anyhow::Result<()>;
438 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
439 where
440 K: AsRef<OsStr>,
441 V: AsRef<OsStr>;
442 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>>;
443}
444
445impl CommandExt for Command {
446 fn map_opt<T>(&mut self, opt: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self {
447 if let Some(v) = opt {
448 f(v, self);
449 }
450 self
451 }
452
453 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
454 where
455 K: AsRef<OsStr>,
456 V: AsRef<OsStr>,
457 {
458 if b {
459 self.env(k, v);
460 }
461 self
462 }
463
464 fn run(&mut self) -> anyhow::Result<()> {
465 display_command(self);
466 let mut child = self.spawn()?;
467 check_status(child.wait()?)
468 }
469
470 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>> {
471 self.arg("--message-format=json-render-diagnostics")
472 .stdout(std::process::Stdio::piped());
473
474 display_command(self);
475
476 let mut child = self.spawn()?;
477
478 let mut artifacts = vec![];
479 let reader = std::io::BufReader::new(child.stdout.take().unwrap());
480 for message in cargo_metadata::Message::parse_stream(reader) {
481 match message.unwrap() {
482 Message::CompilerMessage(msg) => {
483 println!("{msg}");
484 }
485 Message::CompilerArtifact(artifact) => {
486 artifacts.push(artifact);
487 }
488 _ => (),
489 }
490 }
491
492 check_status(child.wait()?)?;
493
494 Ok(artifacts)
495 }
496}
497
498fn remove_path(path: &Path) -> anyhow::Result<()> {
499 match path.metadata() {
500 Ok(meta) => {
501 if meta.is_dir() { remove_dir_all(path) } else { fs::remove_file(path) }
502 .map_err(|err| anyhow!("failed to remove path `{}`: {err}", path.display()))
503 }
504 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
505 Err(err) => Err(anyhow!("failed to remove path `{}`: {err}", path.display())),
506 }
507}
508
509#[cfg(not(windows))]
510fn remove_dir_all(path: &Path) -> io::Result<()> {
511 std::fs::remove_dir_all(path)
512}
513
514#[cfg(windows)]
516fn remove_dir_all(path: &Path) -> io::Result<()> {
517 for _ in 0..99 {
518 if fs::remove_dir_all(path).is_ok() {
519 return Ok(());
520 }
521 std::thread::sleep(std::time::Duration::from_millis(10))
522 }
523 fs::remove_dir_all(path)
524}
525
526fn create_dir(path: &Path) -> anyhow::Result<()> {
527 match fs::create_dir_all(path) {
528 Ok(()) => Ok(()),
529 Err(err) => Err(anyhow!("failed to create directory `{}`: {err}", path.display())),
530 }
531}