xtask/
main.rs

1use std::{
2    env,
3    ffi::OsStr,
4    path::{Path, PathBuf},
5    process::{Command, ExitStatus},
6};
7
8use anyhow::anyhow;
9use cargo_metadata::{
10    camino::{Utf8Path, Utf8PathBuf},
11    Artifact, Message, TargetKind,
12};
13use tests::{FLUX_SYSROOT, FLUX_SYSROOT_TEST};
14use xshell::{cmd, Shell};
15
16xflags::xflags! {
17    cmd xtask {
18        /// If true, run all cargo commands with `--offline`
19        optional --offline
20
21        /// Run regression tests
22        cmd test {
23            /// Only run tests containing `filter` as a substring.
24            optional filter: String
25            /// Do not check tests in Flux libs.
26            optional --no-lib-tests
27        }
28        /// Run the `flux` binary on the given input file.
29        cmd run {
30            /// Input file
31            required input: PathBuf
32            /// Extra options to pass to the `flux` binary, e.g. `cargo x run file.rs -- -Zdump-mir=y`
33            repeated opts: String
34        }
35        /// Expand Flux macros
36        cmd expand {
37            /// Input file
38            required input: PathBuf
39        }
40        /// Install Flux binaries to `~/.cargo/bin` and precompiled libraries and driver to `~/.flux`
41        cmd install {
42            /// Select build profile for the `flux-driver`, either 'release', 'dev', or 'profiling'. Default 'release'
43            optional --profile profile: Profile
44            /// Do not build Flux libs
45            optional --no-libs
46        }
47        /// Uninstall Flux binaries and libraries
48        cmd uninstall { }
49        /// Generate precompiled libraries
50        cmd build-sysroot { }
51        /// Build the documentation
52        cmd doc {
53            optional -o,--open
54        }
55    }
56}
57
58#[derive(Clone, Copy, Debug)]
59enum Profile {
60    Release,
61    Dev,
62    Profiling,
63}
64
65impl Profile {
66    fn as_str(self) -> &'static str {
67        match self {
68            Profile::Release => "release",
69            Profile::Dev => "dev",
70            Profile::Profiling => "profiling",
71        }
72    }
73}
74
75impl std::str::FromStr for Profile {
76    type Err = &'static str;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s {
80            "release" => Ok(Self::Release),
81            "dev" => Ok(Self::Dev),
82            "profiling" => Ok(Self::Profiling),
83            _ => Err("invalid profile"),
84        }
85    }
86}
87
88fn main() -> anyhow::Result<()> {
89    let cmd = match Xtask::from_env() {
90        Ok(cmd) => cmd,
91        Err(err) => {
92            println!("{}", Xtask::HELP_);
93            if err.is_help() {
94                std::process::exit(0);
95            } else {
96                println!("{}", Xtask::HELP_);
97                std::process::exit(2);
98            }
99        }
100    };
101
102    let sh = Shell::new()?;
103    sh.change_dir(project_root());
104
105    let mut extra = vec![];
106    if cmd.offline {
107        extra.push("--offline");
108    }
109    match cmd.subcommand {
110        XtaskCmd::Test(args) => test(sh, args),
111        XtaskCmd::Run(args) => run(sh, args),
112        XtaskCmd::Install(args) => install(&sh, &args, &extra),
113        XtaskCmd::Doc(args) => doc(args),
114        XtaskCmd::BuildSysroot(_) => {
115            let config = SysrootConfig {
116                profile: Profile::Dev,
117                dst: local_sysroot_dir()?,
118                build_libs: BuildLibs::Yes { force: true, tests: true },
119            };
120            install_sysroot(&sh, &config)?;
121            Ok(())
122        }
123        XtaskCmd::Uninstall(_) => uninstall(&sh),
124        XtaskCmd::Expand(args) => expand(&sh, args),
125    }
126}
127
128fn test(sh: Shell, args: Test) -> anyhow::Result<()> {
129    let config = SysrootConfig {
130        profile: Profile::Dev,
131        dst: local_sysroot_dir()?,
132        build_libs: BuildLibs::Yes { force: false, tests: !args.no_lib_tests },
133    };
134    let flux = build_binary("flux", config.profile)?;
135    install_sysroot(&sh, &config)?;
136
137    Command::new("cargo")
138        .args(["test", "-p", "tests", "--"])
139        .args(["--flux", flux.as_str()])
140        .args(["--sysroot".as_ref(), config.dst.as_os_str()])
141        .map_opt(args.filter.as_ref(), |filter, cmd| {
142            cmd.args(["--filter", filter]);
143        })
144        .run()
145}
146
147fn run(sh: Shell, args: Run) -> anyhow::Result<()> {
148    run_inner(
149        &sh,
150        args.input,
151        ["-Ztrack-diagnostics=y".to_string()]
152            .into_iter()
153            .chain(args.opts),
154    )?;
155    Ok(())
156}
157
158fn expand(sh: &Shell, args: Expand) -> Result<(), anyhow::Error> {
159    run_inner(sh, args.input, ["-Zunpretty=expanded".to_string()])?;
160    Ok(())
161}
162
163fn run_inner(
164    sh: &Shell,
165    input: PathBuf,
166    flags: impl IntoIterator<Item = String>,
167) -> Result<(), anyhow::Error> {
168    let config = SysrootConfig {
169        profile: Profile::Dev,
170        dst: local_sysroot_dir()?,
171        build_libs: BuildLibs::Yes { force: false, tests: false },
172    };
173
174    install_sysroot(sh, &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(sh: &Shell, args: &Install, extra: &[&str]) -> anyhow::Result<()> {
188    let config = SysrootConfig {
189        profile: args.profile(),
190        dst: default_sysroot_dir(),
191        build_libs: if args.no_libs {
192            BuildLibs::No
193        } else {
194            BuildLibs::Yes { force: false, tests: false }
195        },
196    };
197    install_sysroot(sh, &config)?;
198    Command::new("cargo")
199        .args(["install", "--path", "crates/flux-bin", "--force"])
200        .args(extra)
201        .run()
202}
203
204fn uninstall(sh: &Shell) -> anyhow::Result<()> {
205    cmd!(sh, "cargo uninstall -p flux-bin").run()?;
206    eprintln!("$ rm -rf ~/.flux");
207    sh.remove_path(default_sysroot_dir())?;
208    Ok(())
209}
210
211fn doc(args: Doc) -> anyhow::Result<()> {
212    Command::new("cargo")
213        .args(["doc", "--workspace", "--document-private-items", "--no-deps"])
214        .env("RUSTDOCFLAGS", "-Zunstable-options --enable-index-page")
215        .run()?;
216    if args.open {
217        opener::open("target/doc/index.html")?;
218    }
219    Ok(())
220}
221
222fn project_root() -> PathBuf {
223    Path::new(
224        &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()),
225    )
226    .ancestors()
227    .nth(1)
228    .unwrap()
229    .to_path_buf()
230}
231
232fn build_binary(bin: &str, profile: Profile) -> anyhow::Result<Utf8PathBuf> {
233    Command::new("cargo")
234        .args(["build", "--bin", bin, "--profile", profile.as_str()])
235        .run_with_cargo_metadata()?
236        .into_iter()
237        .find(|artifact| artifact.target.name == bin && artifact.target.is_kind(TargetKind::Bin))
238        .and_then(|artifact| artifact.executable)
239        .ok_or_else(|| anyhow!("cannot find binary: `{bin}`"))
240}
241
242struct SysrootConfig {
243    /// Profile used to build `flux-driver` and libraries
244    profile: Profile,
245    /// Destination path for sysroot artifacts
246    dst: PathBuf,
247    build_libs: BuildLibs,
248}
249
250/// Whether to build Flux's libs
251enum BuildLibs {
252    /// If `force` is true, forces a clean build. If `tests` is true, check library tests.
253    Yes { force: bool, tests: bool },
254    /// Do not build libs
255    No,
256}
257
258fn install_sysroot(sh: &Shell, config: &SysrootConfig) -> anyhow::Result<()> {
259    sh.remove_path(&config.dst)?;
260    sh.create_dir(&config.dst)?;
261
262    copy_file(sh, build_binary("flux-driver", config.profile)?, &config.dst)?;
263
264    let cargo_flux = build_binary("cargo-flux", config.profile)?;
265
266    if let BuildLibs::Yes { force, tests } = config.build_libs {
267        if force {
268            Command::new(&cargo_flux)
269                .args(["flux", "clean"])
270                .env(FLUX_SYSROOT, &config.dst)
271                .run()?;
272        }
273
274        let artifacts = Command::new(cargo_flux)
275            .args(["flux", "-p", "flux-rs", "-p", "flux-core"])
276            .env(FLUX_SYSROOT, &config.dst)
277            .env_if(tests, FLUX_SYSROOT_TEST, "1")
278            .run_with_cargo_metadata()?;
279
280        copy_artifacts(sh, &artifacts, &config.dst)?;
281    }
282    Ok(())
283}
284
285fn copy_artifacts(sh: &Shell, artifacts: &[Artifact], sysroot: &Path) -> anyhow::Result<()> {
286    for artifact in artifacts {
287        if !is_flux_lib(artifact) {
288            continue;
289        }
290
291        for filename in &artifact.filenames {
292            copy_artifact(sh, filename, sysroot)?;
293        }
294    }
295    Ok(())
296}
297
298fn copy_artifact(sh: &Shell, filename: &Utf8Path, dst: &Path) -> anyhow::Result<()> {
299    copy_file(sh, filename, dst)?;
300    if filename.extension() == Some("rmeta") {
301        let fluxmeta = filename.with_extension("fluxmeta");
302        if sh.path_exists(&fluxmeta) {
303            copy_file(sh, &fluxmeta, dst)?;
304        }
305    }
306    Ok(())
307}
308
309fn is_flux_lib(artifact: &Artifact) -> bool {
310    matches!(&artifact.target.name[..], "flux_rs" | "flux_attrs" | "flux_core")
311}
312
313impl Install {
314    fn profile(&self) -> Profile {
315        self.profile.unwrap_or(Profile::Release)
316    }
317}
318
319fn default_sysroot_dir() -> PathBuf {
320    home::home_dir()
321        .expect("Couldn't find home directory")
322        .join(".flux")
323}
324
325fn local_sysroot_dir() -> anyhow::Result<PathBuf> {
326    Ok(Path::new(file!())
327        .canonicalize()?
328        .ancestors()
329        .nth(3)
330        .unwrap()
331        .join("sysroot"))
332}
333
334fn check_status(st: ExitStatus) -> anyhow::Result<()> {
335    if st.success() {
336        return Ok(());
337    }
338    let err = match st.code() {
339        Some(code) => anyhow!("command exited with non-zero code: {code}"),
340        #[cfg(unix)]
341        None => {
342            use std::os::unix::process::ExitStatusExt;
343            match st.signal() {
344                Some(sig) => anyhow!("command was terminated by a signal: {sig}"),
345                None => anyhow!("command was terminated by a signal"),
346            }
347        }
348        #[cfg(not(unix))]
349        None => anyhow!("command was terminated by a signal"),
350    };
351    Err(err)
352}
353
354fn display_command(cmd: &Command) {
355    for var in cmd.get_envs() {
356        if let Some(val) = var.1 {
357            eprintln!("$ export {}={}", var.0.to_string_lossy(), val.to_string_lossy());
358        }
359    }
360
361    let prog = cmd.get_program();
362    eprint!("$ {}", prog.to_string_lossy());
363    for arg in cmd.get_args() {
364        eprint!(" {}", arg.to_string_lossy());
365    }
366    eprintln!();
367}
368
369fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(sh: &Shell, src: S, dst: D) -> anyhow::Result<()> {
370    let src = src.as_ref();
371    let dst = dst.as_ref();
372    eprintln!("$ cp {} {}", src.to_string_lossy(), dst.to_string_lossy());
373    sh.copy_file(src, dst)?;
374    Ok(())
375}
376
377trait CommandExt {
378    fn map_opt<T>(&mut self, b: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self;
379    fn run(&mut self) -> anyhow::Result<()>;
380    fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
381    where
382        K: AsRef<OsStr>,
383        V: AsRef<OsStr>;
384    fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>>;
385}
386
387impl CommandExt for Command {
388    fn map_opt<T>(&mut self, opt: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self {
389        if let Some(v) = opt {
390            f(v, self);
391        }
392        self
393    }
394
395    fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
396    where
397        K: AsRef<OsStr>,
398        V: AsRef<OsStr>,
399    {
400        if b {
401            self.env(k, v);
402        }
403        self
404    }
405
406    fn run(&mut self) -> anyhow::Result<()> {
407        display_command(self);
408        let mut child = self.spawn()?;
409        check_status(child.wait()?)
410    }
411
412    fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>> {
413        self.arg("--message-format=json-render-diagnostics")
414            .stdout(std::process::Stdio::piped());
415
416        display_command(self);
417
418        let mut child = self.spawn()?;
419
420        let mut artifacts = vec![];
421        let reader = std::io::BufReader::new(child.stdout.take().unwrap());
422        for message in cargo_metadata::Message::parse_stream(reader) {
423            match message.unwrap() {
424                Message::CompilerMessage(msg) => {
425                    println!("{msg}");
426                }
427                Message::CompilerArtifact(artifact) => {
428                    artifacts.push(artifact);
429                }
430                _ => (),
431            }
432        }
433
434        check_status(child.wait()?)?;
435
436        Ok(artifacts)
437    }
438}