bevy/tools/ci/src/commands/ci.rs

125 lines
5.2 KiB
Rust
Raw Normal View History

tools: Refactor CI to use `argh` (#12923) # Objective The CI tool currently parses input manually. This has worked fine, but makes it just a bit more difficult to maintain and extend. Additionally, it provides no usage help for devs wanting to run the tool locally. It would be better if parsing was handled by a dedicated CLI library like [`clap`](https://github.com/clap-rs/clap) or [`argh`](https://github.com/google/argh). ## Solution Use `argh` to parse command line input for CI. `argh` was chosen over `clap` and other tools due to being more lightweight and already existing in our dependency tree. Using `argh`, the usage notes are generated automatically: ``` $ cargo run -p ci --quiet -- --help Usage: ci [--keep-going] [<command>] [<args>] The CI command line tool for Bevy. Options: --keep-going continue running commands even if one fails --help display usage information Commands: lints Alias for running the `format` and `clippy` subcommands. doc Alias for running the `doc-test` and `doc-check` subcommands. compile Alias for running the `compile-fail`, `bench-check`, `example-check`, `compile-check`, and `test-check` subcommands. format Check code formatting. clippy Check for clippy warnings and errors. test Runs all tests (except for doc tests). test-check Checks that all tests compile. doc-check Checks that all docs compile. doc-test Runs all doc tests. compile-check Checks that the project compiles. cfg-check Checks that the project compiles using the nightly compiler with cfg checks enabled. compile-fail Runs the compile-fail tests. bench-check Checks that the benches compile. example-check Checks that the examples compile. ``` This PR makes each subcommand more modular, allowing them to be called from other subcommands. This also makes it much easier to extract them out of `main.rs` and into their own dedicated modules. Additionally, this PR improves failure output: ``` $ cargo run -p ci -- lints ... One or more CI commands failed: format: Please run 'cargo fmt --all' to format your code. ``` Including when run with the `--keep-going` flag: ``` $ cargo run -p ci -- --keep-going lints ... One or more CI commands failed: - format: Please run 'cargo fmt --all' to format your code. - clippy: Please fix clippy errors in output above. ``` ### Future Work There are a lot of other things we could possibly clean up. I chose to try and keep the API surface as unchanged as I could (for this PR at least). For example, now that each subcommand is an actual command, we can specify custom arguments for each. The `format` subcommand could include a `--check` (making the default fun `cargo fmt` as normal). Or the `compile-fail` subcommand could include `--ecs`, `--reflect`, and `--macros` flags for specifying which set of compile fail tests to run. The `--keep-going` flag could be split so that it doesn't do double-duty where it also enables `--no-fail-fast` for certain commands. Or at least make it more explicit via renaming or using alternative flags. --- ## Changelog - Improved the CI CLI tool - Now includes usage info with the `--help` option! - [Internal] Cleaned up and refactored the `tools/ci` crate using the `argh` crate ## Migration Guide The CI tool no longer supports running multiple subcommands in a single call. Users who are currently doing so will need to split them across multiple commands: ```bash # BEFORE cargo run -p ci -- lints doc compile # AFTER cargo run -p ci -- lints && cargo run -p ci -- doc && cargo run -p ci -- compile # or cargo run -p ci -- lints; cargo run -p ci -- doc; cargo run -p ci -- compile # or cargo run -p ci -- lints cargo run -p ci -- doc cargo run -p ci -- compile ```
2024-04-13 01:48:37 +00:00
use crate::commands::prepare::{Flag, Prepare, PreparedCommand};
use crate::commands::subcommands;
use argh::FromArgs;
/// The CI command line tool for Bevy.
#[derive(FromArgs)]
pub(crate) struct CI {
#[argh(subcommand)]
command: Option<Commands>,
/// continue running commands even if one fails
#[argh(switch)]
keep_going: bool,
}
impl CI {
/// Runs the specified commands or all commands if none are specified.
///
/// When run locally, results may differ from actual CI runs triggered by `.github/workflows/ci.yml`.
/// This is because the official CI runs latest stable, while local runs use whatever the default Rust is locally.
pub fn run(self) {
let sh = xshell::Shell::new().unwrap();
let prepared_commands = self.prepare(&sh);
let mut failures = vec![];
for command in prepared_commands {
// If the CI test is to be executed in a subdirectory, we move there before running the command.
// This will automatically move back to the original directory once dropped.
let _subdir_hook = command.subdir.map(|path| sh.push_dir(path));
if command.command.envs(command.env_vars).run().is_err() {
let name = command.name;
let message = command.failure_message;
if self.keep_going {
failures.push(format!("- {name}: {message}"));
} else {
failures.push(format!("{name}: {message}"));
break;
}
}
}
if !failures.is_empty() {
let failures = failures.join("\n");
panic!(
"One or more CI commands failed:\n\
{failures}"
);
}
}
fn prepare<'a>(&self, sh: &'a xshell::Shell) -> Vec<PreparedCommand<'a>> {
let mut flags = Flag::empty();
if self.keep_going {
flags |= Flag::KEEP_GOING;
}
match &self.command {
Some(command) => command.prepare(sh, flags),
None => {
// Note that we are running the subcommands directly rather than using any aliases
let mut cmds = vec![];
cmds.append(&mut subcommands::FormatCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::ClippyCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::TestCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::TestCheckCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::DocCheckCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::DocTestCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::CompileCheckCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::CfgCheckCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::CompileFailCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::BenchCheckCommand::default().prepare(sh, flags));
cmds.append(&mut subcommands::ExampleCheckCommand::default().prepare(sh, flags));
cmds
}
}
}
}
/// The subcommands that can be run by the CI script.
#[derive(FromArgs)]
#[argh(subcommand)]
enum Commands {
// Aliases (subcommands that run other subcommands)
Lints(subcommands::LintsCommand),
Doc(subcommands::DocCommand),
Compile(subcommands::CompileCommand),
// Actual subcommands
Format(subcommands::FormatCommand),
Clippy(subcommands::ClippyCommand),
Test(subcommands::TestCommand),
TestCheck(subcommands::TestCheckCommand),
DocCheck(subcommands::DocCheckCommand),
DocTest(subcommands::DocTestCommand),
CompileCheck(subcommands::CompileCheckCommand),
CfgCheck(subcommands::CfgCheckCommand),
CompileFail(subcommands::CompileFailCommand),
BenchCheck(subcommands::BenchCheckCommand),
ExampleCheck(subcommands::ExampleCheckCommand),
}
impl Prepare for Commands {
fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec<PreparedCommand<'a>> {
match self {
Commands::Lints(subcommand) => subcommand.prepare(sh, flags),
Commands::Doc(subcommand) => subcommand.prepare(sh, flags),
Commands::Compile(subcommand) => subcommand.prepare(sh, flags),
Commands::Format(subcommand) => subcommand.prepare(sh, flags),
Commands::Clippy(subcommand) => subcommand.prepare(sh, flags),
Commands::Test(subcommand) => subcommand.prepare(sh, flags),
Commands::TestCheck(subcommand) => subcommand.prepare(sh, flags),
Commands::DocCheck(subcommand) => subcommand.prepare(sh, flags),
Commands::DocTest(subcommand) => subcommand.prepare(sh, flags),
Commands::CompileCheck(subcommand) => subcommand.prepare(sh, flags),
Commands::CfgCheck(subcommand) => subcommand.prepare(sh, flags),
Commands::CompileFail(subcommand) => subcommand.prepare(sh, flags),
Commands::BenchCheck(subcommand) => subcommand.prepare(sh, flags),
Commands::ExampleCheck(subcommand) => subcommand.prepare(sh, flags),
}
}
}