xtask/
main.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! HvLite repo-specific automation.
5//!
6//! If you're thinking of writing a bash script, write an xtask instead!
7//!
8//! Follows the xtask workflow/convention, as described at
9//! <https://github.com/matklad/cargo-xtask>
10
11use anyhow::Context;
12use clap::Parser;
13use clap::Subcommand;
14use std::path::Path;
15use std::path::PathBuf;
16
17mod completions;
18pub mod fs_helpers;
19pub mod tasks;
20
21/// Default location to maintain a `xtask-path` file
22///
23/// This file contains a fully-resolved path to the actual `xtask` binary,
24/// allowing other tooling (e.g: git hook) to invoke the xtask without having to
25/// go through `cargo`.
26pub const XTASK_PATH_FILE: &str = "./target/xtask-path";
27
28/// Common context passed into every Xtask
29#[derive(Clone)]
30pub struct XtaskCtx {
31    /// Project root directory
32    pub root: PathBuf,
33    /// xtask is running within a hook
34    pub in_git_hook: bool,
35    /// xtask was invoked as part of a "run on save" operation
36    pub in_run_on_save: bool,
37}
38
39/// Common trait implemented by all Xtask subcommands.
40pub trait Xtask: Parser {
41    /// Run the Xtask.
42    ///
43    /// For consistency and simplicity, `Xtask` implementations are allowed to
44    /// assume that they are being run from the root of the repo's filesystem.
45    /// Callers of `Xtask::run` should take care to ensure
46    /// [`std::env::set_current_dir`] was called prior to invoking `Xtask::run`.
47    fn run(self, ctx: XtaskCtx) -> anyhow::Result<()>;
48}
49
50#[derive(Parser)]
51#[clap(name = "xtask", about = "HvLite repo automation")]
52struct Cli {
53    #[clap(subcommand)]
54    command: Commands,
55
56    /// Specify a custom project root directory.
57    ///
58    /// Can be used to ensure consistent formatting between the OpenVMM base
59    /// repo, and any custom out-of-tree overlay repos.
60    #[clap(long)]
61    custom_root: Option<PathBuf>,
62
63    /// Signal that this `xtask` is being invoked as part of a "run on save"
64    /// operation.
65    ///
66    /// When set, certain tasks may choose to skip certain expensive checks in
67    /// order to execute as quickly as possible.
68    ///
69    /// e.g: instead of calling `cargo run` in order to execute a project-local
70    /// tool, `xtask` may instead attempt to find and use an existing pre-built
71    /// binary, if one is available. This will be faster, but may run the risk
72    /// of executing a slightly stale binary.
73    #[clap(long)]
74    run_on_save: bool,
75}
76
77#[expect(clippy::large_enum_variant)]
78#[derive(Subcommand)]
79enum Commands {
80    #[clap(hide = true)]
81    Hook(tasks::RunGitHook),
82    #[clap(hide = true)]
83    Complete(clap_dyn_complete::Complete),
84    Completions(completions::Completions),
85
86    // deprecated
87    #[clap(hide = true)]
88    BuildIgvm(tasks::BuildIgvm),
89
90    Fmt(tasks::Fmt),
91    Fuzz(tasks::Fuzz),
92    GuestTest(tasks::GuestTest),
93    InstallGitHooks(tasks::InstallGitHooks),
94    VerifySize(tasks::VerifySize),
95}
96
97fn main() {
98    ci_logger::init("XTASK_LOG").unwrap();
99
100    if let Err(e) = try_main() {
101        log::error!("Error: {:#}", e);
102        std::process::exit(-1);
103    }
104}
105
106fn try_main() -> anyhow::Result<()> {
107    let cli = Cli::parse();
108
109    let orig_root = Path::new(&env!("CARGO_MANIFEST_DIR"))
110        .ancestors()
111        .nth(1)
112        .unwrap()
113        .to_path_buf();
114
115    let root = cli
116        .custom_root
117        .map(std::path::absolute)
118        .transpose()?
119        .unwrap_or(orig_root.clone());
120
121    // for consistency, always run xtasks as though they were run from the root
122    std::env::set_current_dir(&root)?;
123
124    // drop the path to the xtask binary in an easy-to-find place. this gets
125    // used by the pre-commit hook, as well as the fmt-on-save dev-flow to avoid
126    // rebuilding the xtask.
127    if let Ok(path) = std::env::current_exe() {
128        if let Err(e) = fs_err::write(orig_root.join(XTASK_PATH_FILE), path.display().to_string()) {
129            log::debug!("Unable to create XTASK_PATH_FILE: {:#}", e)
130        }
131    }
132
133    if !matches!(cli.command, Commands::Complete(..)) {
134        tasks::update_hooks(&root).context("failed to update git hooks")?;
135    }
136
137    let ctx = XtaskCtx {
138        root,
139        in_git_hook: matches!(cli.command, Commands::Hook(..)),
140        in_run_on_save: cli.run_on_save,
141    };
142
143    match cli.command {
144        Commands::Hook(task) => task.run(ctx),
145        Commands::Completions(task) => task.run(),
146        Commands::Complete(task) => {
147            futures::executor::block_on(task.println_to_stub_script::<Cli>(
148                Some("cargo"),
149                completions::XtaskCompleteFactory { ctx },
150            ));
151            Ok(())
152        }
153
154        Commands::BuildIgvm(task) => task.run(ctx),
155        Commands::Fmt(task) => task.run(ctx),
156        Commands::Fuzz(task) => task.run(ctx),
157        Commands::GuestTest(task) => task.run(ctx),
158        Commands::InstallGitHooks(task) => task.run(ctx),
159        Commands::VerifySize(task) => task.run(ctx),
160    }
161}