xtask/
main.rs

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        /// If true, run all cargo commands with `--offline`
21        optional --offline
22        /// If true, run cargo build commands with --features liquid-fixpoint/rust-fixpiont
23        optional --rust-fixpoint
24
25        /// Run regression tests
26        cmd test {
27            /// Only run tests containing `filter` as a substring.
28            optional filter: String
29            /// Do not check tests in Flux libs.
30            optional --no-lib-tests
31        }
32        /// Run the `flux` binary on the given input file.
33        cmd run {
34            /// Input file
35            required input: PathBuf
36            /// Extra options to pass to the `flux` binary, e.g. `cargo x run file.rs -- -Zdump-mir=y`
37            repeated opts: String
38            /// Do not build Flux libs for extern specs
39            optional --no-extern-specs
40        }
41        /// Expand Flux macros
42        cmd expand {
43            /// Input file
44            required input: PathBuf
45        }
46        /// Install Flux binaries to `~/.cargo/bin` and precompiled libraries and driver to `~/.flux`
47        cmd install {
48            /// Select build profile for the `flux-driver`, either 'release', 'dev', or 'profiling'. Default 'release'
49            optional --profile profile: Profile
50            /// Do not install Flux libs or extern specs
51            optional --no-extern-specs
52        }
53        /// Uninstall Flux binaries and libraries
54        cmd uninstall { }
55        /// Generate precompiled libraries
56        cmd build-sysroot { }
57        /// Build the documentation
58        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 used to build `flux-driver` and libraries
247    profile: Profile,
248    /// Whether liquid-fixpoint/rust-fixpoint should be enabled to build `flux-driver`
249    rust_fixpoint: bool,
250    /// Destination path for sysroot artifacts
251    dst: PathBuf,
252    build_libs: BuildLibs,
253}
254
255struct BuildLibs {
256    /// If true, forces a clean build.
257    force: bool,
258    /// If is true, run library tests.
259    tests: bool,
260    /// List of libraries to install
261    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// Copied from xshell
515#[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}