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