refactor: move xtask commands to small modules (#1620)

This commit is contained in:
Josh McKinney
2025-01-17 01:40:22 -08:00
committed by GitHub
parent e7831aedd4
commit a195d59a47
11 changed files with 458 additions and 421 deletions

View File

@@ -16,15 +16,15 @@ command = ["cargo", "xtask", "check", "--all-features"]
need_stdout = false
[jobs.check-crossterm]
command = ["cargo", "xtask", "check-crossterm"]
command = ["cargo", "xtask", "check-backend", "crossterm"]
need_stdout = false
[jobs.check-termion]
command = ["cargo", "xtask", "check-termion"]
command = ["cargo", "xtask", "check-backend", "termion"]
need_stdout = false
[jobs.check-termwiz]
command = ["cargo", "xtask", "check-termwiz"]
command = ["cargo", "xtask", "check-backend", "termwiz"]
need_stdout = false
[jobs.clippy-all]
@@ -52,7 +52,7 @@ need_stdout = false
command = ["cargo", "xtask", "coverage"]
[jobs.coverage-unit-tests-only]
command = ["cargo", "xtask", "coverage-unit"]
command = ["cargo", "xtask", "coverage", "--lib"]
[jobs.hack]
command = ["cargo", "xtask", "hack"]

177
xtask/src/commands.rs Normal file
View File

@@ -0,0 +1,177 @@
use std::fmt::Debug;
use backend::{Backend, TestBackend};
use clap::Subcommand;
use color_eyre::Result;
use coverage::Coverage;
use duct::cmd;
use rdme::Readme;
use self::{
backend::CheckBackend, check::Check, clippy::Clippy, docs::Docs, format::Format, typos::Typos,
};
use crate::{run_cargo, ExpressionExt, Run};
mod backend;
mod check;
mod clippy;
mod coverage;
mod docs;
mod format;
mod rdme;
mod typos;
#[derive(Clone, Debug, Subcommand)]
pub enum Command {
/// Run CI checks (lint, build, test)
CI,
/// Lint formatting, typos, clippy, and docs
#[command(visible_alias = "l")]
Lint,
/// Build the project
#[command(visible_alias = "b")]
Build,
#[command(visible_alias = "c")]
Check(Check),
/// Run tests
#[command(visible_alias = "t")]
Test,
/// Check backend
#[command(visible_alias = "cb")]
CheckBackend(CheckBackend),
/// Check if README.md is up-to-date (using cargo-rdme)
#[command(visible_alias = "cr", alias = "rdme")]
Readme(Readme),
/// Generate code coverage report
#[command(visible_alias = "cov")]
Coverage(Coverage),
/// Run clippy on the project
#[command(visible_alias = "cl")]
Clippy(Clippy),
/// Check documentation for errors and warnings
#[command(name = "docs", visible_alias = "d")]
Docs(Docs),
/// Check for formatting issues in the project
#[command(visible_aliases = ["fmt", "f"])]
Format(Format),
/// Lint markdown files
#[command(visible_alias = "md")]
LintMarkdown,
/// Check for typos in the project
#[command(visible_alias = "ty")]
Typos(Typos),
/// Test backend
#[command(visible_alias = "tb")]
TestBackend(TestBackend),
/// Run doc tests
#[command(visible_alias = "td")]
TestDocs,
/// Run lib tests
#[command(visible_alias = "tl")]
TestLibs,
/// Run cargo hack to test each feature in isolation
#[command(visible_alias = "h")]
Hack,
}
impl Run for Command {
fn run(self) -> crate::Result<()> {
match self {
Command::CI => ci(),
Command::Build => build(),
Command::Check(command) => command.run(),
Command::CheckBackend(command) => command.run(),
Command::Readme(command) => command.run(),
Command::Coverage(command) => command.run(),
Command::Lint => lint(),
Command::Clippy(command) => command.run(),
Command::Docs(command) => command.run(),
Command::Format(command) => command.run(),
Command::Typos(command) => command.run(),
Command::LintMarkdown => lint_markdown(),
Command::Test => test(),
Command::TestBackend(command) => command.run(),
Command::TestDocs => test_docs(),
Command::TestLibs => test_libs(),
Command::Hack => hack(),
}
}
}
/// Run CI checks (lint, build, test)
fn ci() -> Result<()> {
lint()?;
build()?;
test()?;
Ok(())
}
/// Build the project
fn build() -> Result<()> {
run_cargo(vec!["build", "--all-targets", "--all-features"])
}
/// Lint formatting, typos, clippy, and docs (and a soft fail on markdown)
fn lint() -> Result<()> {
Clippy { fix: false }.run()?;
Docs { open: false }.run()?;
Format { check: true }.run()?;
Typos { fix: false }.run()?;
if let Err(err) = lint_markdown() {
tracing::warn!("known issue: markdownlint is currently noisy and can be ignored: {err}");
}
Ok(())
}
/// Lint markdown files using [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)
fn lint_markdown() -> Result<()> {
cmd!("markdownlint-cli2", "**/*.md", "!target").run_with_trace()?;
Ok(())
}
/// Run tests for libs, backends, and docs
fn test() -> Result<()> {
test_libs()?;
for backend in [Backend::Crossterm, Backend::Termion, Backend::Termwiz] {
TestBackend { backend }.run()?;
}
test_docs()?; // run last because it's slow
Ok(())
}
/// Run doc tests for the workspace's default packages
fn test_docs() -> Result<()> {
run_cargo(vec!["test", "--doc", "--all-features"])
}
/// Run lib tests for the workspace's default packages
fn test_libs() -> Result<()> {
run_cargo(vec!["test", "--lib", "--all-targets", "--all-features"])
}
/// Run cargo hack to test each feature in isolation
fn hack() -> Result<()> {
run_cargo(vec![
"hack",
"test",
"--lib",
"--each-feature",
"--workspace",
])
}

View File

@@ -0,0 +1,65 @@
use clap::ValueEnum;
use color_eyre::Result;
use crate::{run_cargo, Run};
/// Check backend
#[derive(Clone, Debug, clap::Args)]
pub struct CheckBackend {
/// Backend to check
pub backend: Backend,
}
/// Test backend
#[derive(Clone, Debug, clap::Args)]
pub struct TestBackend {
/// Backend to test
pub backend: Backend,
}
#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
pub enum Backend {
Crossterm,
Termion,
Termwiz,
}
impl Run for CheckBackend {
fn run(self) -> Result<()> {
if cfg!(windows) && self.backend == Backend::Termion {
tracing::error!("termion backend is not supported on Windows");
}
let backend = match self.backend {
Backend::Crossterm => "crossterm",
Backend::Termion => "termion",
Backend::Termwiz => "termwiz",
};
run_cargo(vec![
"check",
"--all-targets",
"--no-default-features",
"--features",
backend,
])
}
}
impl Run for TestBackend {
fn run(self) -> Result<()> {
if cfg!(windows) && self.backend == Backend::Termion {
tracing::error!("termion backend is not supported on Windows");
}
let backend = match self.backend {
Backend::Crossterm => "crossterm",
Backend::Termion => "termion",
Backend::Termwiz => "termwiz",
};
run_cargo(vec![
"test",
"--all-targets",
"--no-default-features",
"--features",
backend,
])
}
}

View File

@@ -0,0 +1,21 @@
use color_eyre::Result;
use crate::{run_cargo, Run};
/// Run cargo check
#[derive(Clone, Debug, clap::Args)]
pub struct Check {
/// Check all features
#[arg(long, visible_alias = "all")]
all_features: bool,
}
impl Run for Check {
fn run(self) -> Result<()> {
if self.all_features {
run_cargo(vec!["check", "--all-targets", "--all-features"])
} else {
run_cargo(vec!["check", "--all-targets"])
}
}
}

View File

@@ -0,0 +1,30 @@
use color_eyre::Result;
use crate::{run_cargo, Run};
/// Run clippy on the project
#[derive(Clone, Debug, clap::Args)]
pub struct Clippy {
/// Fix clippy warnings
#[arg(long)]
pub fix: bool,
}
impl Run for Clippy {
fn run(self) -> Result<()> {
let mut args = vec![
"clippy",
"--all-targets",
"--all-features",
"--tests",
"--benches",
"--",
"-D",
"warnings",
];
if self.fix {
args.push("--fix");
}
run_cargo(args)
}
}

View File

@@ -0,0 +1,27 @@
use color_eyre::Result;
use crate::{run_cargo, Run};
/// Generate code coverage report
#[derive(Clone, Debug, clap::Args)]
pub struct Coverage {
/// Only generate coverage for unit tests
#[arg(long)]
pub lib: bool,
}
impl Run for Coverage {
fn run(self) -> Result<()> {
let mut args = vec![
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--all-features",
];
if self.lib {
args.push("--lib");
}
run_cargo(args)
}
}

View File

@@ -0,0 +1,26 @@
use color_eyre::Result;
use itertools::{Itertools, Position};
use crate::{run_cargo_nightly, workspace_libs, Run};
/// Check documentation for errors and warnings
#[derive(Clone, Debug, clap::Args)]
pub struct Docs {
/// Open the documentation in the browser
#[arg(long)]
pub open: bool,
}
impl Run for Docs {
fn run(self) -> Result<()> {
let packages = workspace_libs()?;
for (position, package) in packages.iter().with_position() {
let mut args = vec!["docs-rs", "--package", &package];
if self.open && matches!(position, Position::Last | Position::Only) {
args.push("--open");
}
run_cargo_nightly(args)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,40 @@
use color_eyre::Result;
use duct::cmd;
use crate::{run_cargo_nightly, ExpressionExt, Run};
/// Check for formatting issues in the project
#[derive(Clone, Debug, clap::Args)]
pub struct Format {
/// Check formatting issues
#[arg(long)]
pub check: bool,
}
impl Run for Format {
fn run(self) -> Result<()> {
self.run_rustfmt()?;
self.run_taplo()?;
Ok(())
}
}
impl Format {
fn run_rustfmt(&self) -> Result<(), color_eyre::eyre::Error> {
let mut args = vec!["fmt", "--all"];
if self.check {
args.push("--check");
}
run_cargo_nightly(args)?;
Ok(())
}
fn run_taplo(&self) -> Result<(), color_eyre::eyre::Error> {
let mut args = vec!["format", "--colors", "always"];
if self.check {
args.push("--check");
}
cmd("taplo", args).run_with_trace()?;
Ok(())
}
}

View File

@@ -0,0 +1,32 @@
use color_eyre::Result;
use crate::{run_cargo, workspace_libs, Run};
/// Check if README.md is up-to-date (using cargo-rdme)
#[derive(Clone, Debug, clap::Args)]
pub struct Readme {
/// Check if README.md is up-to-date
#[arg(long)]
check: bool,
}
impl Run for Readme {
fn run(self) -> Result<()> {
let args = if self.check {
vec!["rdme", "--check"]
} else {
vec!["rdme"]
};
for package in workspace_libs()? {
if package == "ratatui" {
// Skip the main crate as we removed rdme
continue;
}
let mut package_args = args.clone();
package_args.push("--workspace-project");
package_args.push(&package);
run_cargo(package_args)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,23 @@
use color_eyre::Result;
use duct::cmd;
use crate::{ExpressionExt, Run};
/// Check for typos in the project
#[derive(Clone, Debug, clap::Args)]
pub struct Typos {
/// Fix typos
#[arg(long)]
pub fix: bool,
}
impl Run for Typos {
fn run(self) -> Result<()> {
if self.fix {
cmd!("typos", "--write-changes").run_with_trace()?;
} else {
cmd!("typos").run_with_trace()?;
}
Ok(())
}
}

View File

@@ -4,14 +4,20 @@
//!
//! Run `cargo xtask --help` for more information
use std::{fmt::Debug, io, process::Output, vec};
use std::{io, process::Output};
use cargo_metadata::{MetadataCommand, TargetKind};
use clap::{Parser, Subcommand, ValueEnum};
use clap::Parser;
use clap_verbosity_flag::{InfoLevel, Verbosity};
use color_eyre::{eyre::Context, Result};
use commands::Command;
use duct::cmd;
use itertools::{Itertools, Position};
mod commands;
pub trait Run {
fn run(self) -> Result<()>;
}
fn main() -> Result<()> {
color_eyre::install()?;
@@ -21,7 +27,7 @@ fn main() -> Result<()> {
.without_time()
.init();
match args.run() {
match args.command.run() {
Ok(_) => (),
Err(err) => {
tracing::error!("{err}");
@@ -41,430 +47,20 @@ struct Args {
verbosity: Verbosity<InfoLevel>,
}
impl Args {
fn run(self) -> Result<()> {
self.command.run()
}
}
#[derive(Clone, Debug, Subcommand)]
enum Command {
/// Run CI checks (lint, build, test)
CI,
/// Build the project
#[command(visible_alias = "b")]
Build,
#[command(visible_alias = "c")]
Check(CheckCommand),
/// Run cargo check with crossterm feature
#[command(visible_alias = "cc")]
CheckCrossterm,
/// Run cargo check with termion feature
#[command(visible_alias = "ct")]
CheckTermion,
/// Run cargo check with termwiz feature
#[command(visible_alias = "cw")]
CheckTermwiz,
/// Check if README.md is up-to-date (using cargo-rdme)
#[command(visible_alias = "cr", alias = "rdme")]
Readme(ReadmeCommand),
/// Generate code coverage report
#[command(visible_alias = "cov")]
Coverage,
/// Generate code coverage for unit tests only
#[command(visible_alias = "covu")]
CoverageUnit,
/// Lint formatting, typos, clippy, and docs
#[command(visible_alias = "l")]
Lint,
/// Run clippy on the project
#[command(visible_alias = "cl")]
Clippy(ClippyCommand),
/// Check documentation for errors and warnings
#[command(name = "docs", visible_alias = "d")]
Docs(DocsCommand),
/// Check for formatting issues in the project
#[command(visible_aliases = ["fmt", "f"])]
Format(FormatCommand),
/// Lint markdown files
#[command(visible_alias = "md")]
LintMarkdown,
/// Check for typos in the project
#[command(visible_alias = "ty")]
Typos(TyposCommand),
/// Run tests
#[command(visible_alias = "t")]
Test,
/// Test backend
#[command(visible_alias = "tb")]
TestBackend { backend: Backend },
/// Run doc tests
#[command(visible_alias = "td")]
TestDocs,
/// Run lib tests
#[command(visible_alias = "tl")]
TestLibs,
/// Run cargo hack to test each feature in isolation
#[command(visible_alias = "h")]
Hack,
}
/// Run cargo check
#[derive(Clone, Debug, clap::Args)]
struct CheckCommand {
/// Check all features
#[arg(long, visible_alias = "all")]
all_features: bool,
}
/// Check documentation for errors and warnings
#[derive(Clone, Debug, clap::Args)]
struct DocsCommand {
/// Open the documentation in the browser
#[arg(long)]
open: bool,
}
/// Check for formatting issues in the project
#[derive(Clone, Debug, clap::Args)]
struct FormatCommand {
/// Check formatting issues
#[arg(long)]
check: bool,
}
/// Run clippy on the project
#[derive(Clone, Debug, clap::Args)]
struct ClippyCommand {
/// Fix clippy warnings
#[arg(long)]
fix: bool,
}
/// Check if README.md is up-to-date (using cargo-rdme)
#[derive(Clone, Debug, clap::Args)]
struct ReadmeCommand {
/// Check if README.md is up-to-date
#[arg(long)]
check: bool,
}
/// Check for typos in the project
#[derive(Clone, Debug, clap::Args)]
struct TyposCommand {
/// Fix typos
#[arg(long)]
fix: bool,
}
#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
enum Backend {
Crossterm,
Termion,
Termwiz,
}
impl Command {
fn run(self) -> Result<()> {
match self {
Command::CI => ci(),
Command::Build => build(),
Command::Check(command) => command.run(),
Command::CheckCrossterm => check_crossterm(),
Command::CheckTermion => check_termion(),
Command::CheckTermwiz => check_termwiz(),
Command::Readme(command) => command.run(),
Command::Coverage => coverage(),
Command::CoverageUnit => coverage_unit(),
Command::Lint => lint(),
Command::Clippy(command) => command.run(),
Command::Docs(command) => command.run(),
Command::Format(command) => command.run(),
Command::Typos(command) => command.run(),
Command::LintMarkdown => lint_markdown(),
Command::Test => test(),
Command::TestBackend { backend } => test_backend(backend),
Command::TestDocs => test_docs(),
Command::TestLibs => test_libs(),
Command::Hack => hack(),
}
}
}
/// Run CI checks (lint, build, test)
fn ci() -> Result<()> {
lint()?;
build()?;
test()?;
Ok(())
}
/// Build the project
fn build() -> Result<()> {
run_cargo(vec!["build", "--all-targets", "--all-features"])
}
impl CheckCommand {
fn run(self) -> Result<()> {
if self.all_features {
run_cargo(vec!["check", "--all-targets", "--all-features"])
} else {
run_cargo(vec!["check", "--all-targets"])
}
}
}
/// Run cargo check with crossterm feature
fn check_crossterm() -> Result<()> {
run_cargo(vec![
"check",
"--all-targets",
"--all-features",
"--no-default-features",
"--features",
"crossterm",
])
}
/// Run cargo check with termion feature
fn check_termion() -> Result<()> {
run_cargo(vec![
"check",
"--all-targets",
"--all-features",
"--no-default-features",
"--features",
"termion",
])
}
/// Run cargo check with termwiz feature
fn check_termwiz() -> Result<()> {
run_cargo(vec![
"check",
"--all-targets",
"--all-features",
"--no-default-features",
"--features",
"termwiz",
])
}
impl ReadmeCommand {
fn run(self) -> Result<()> {
let args = if self.check {
vec!["rdme", "--check"]
} else {
vec!["rdme"]
};
for package in workspace_packages(TargetKind::Lib)? {
if package == "ratatui" {
// Skip the main crate as we removed rdme
continue;
}
let mut package_args = args.clone();
package_args.push("--workspace-project");
package_args.push(&package);
run_cargo(package_args)?;
}
Ok(())
}
}
/// Generate code coverage report
fn coverage() -> Result<()> {
run_cargo(vec![
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--all-features",
])
}
/// Generate code coverage for unit tests only
fn coverage_unit() -> Result<()> {
run_cargo(vec![
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov-unit.info",
"--all-features",
"--lib",
])
}
/// Lint formatting, typos, clippy, and docs (and a soft fail on markdown)
fn lint() -> Result<()> {
ClippyCommand { fix: false }.run()?;
DocsCommand { open: false }.run()?;
FormatCommand { check: true }.run()?;
TyposCommand { fix: false }.run()?;
if let Err(err) = lint_markdown() {
tracing::warn!("known issue: markdownlint is currently noisy and can be ignored: {err}");
}
Ok(())
}
impl ClippyCommand {
fn run(self) -> Result<()> {
let mut args = vec![
"clippy",
"--all-targets",
"--all-features",
"--tests",
"--benches",
"--",
"-D",
"warnings",
];
if self.fix {
args.push("--fix");
}
run_cargo(args)
}
}
impl DocsCommand {
fn run(self) -> Result<()> {
let packages = workspace_packages(TargetKind::Lib)?;
for (position, package) in packages.iter().with_position() {
let mut args = vec!["docs-rs", "--package", &package];
if self.open && matches!(position, Position::Last | Position::Only) {
args.push("--open");
}
run_cargo_nightly(args)?;
}
Ok(())
}
}
/// Return the available packages in the workspace
fn workspace_packages(kind: TargetKind) -> Result<Vec<String>> {
/// Return the available libs in the workspace
fn workspace_libs() -> Result<Vec<String>> {
let meta = MetadataCommand::new()
.exec()
.wrap_err("failed to get cargo metadata")?;
let packages = meta
.workspace_packages()
.iter()
.filter(|v| v.targets.iter().any(|t| t.kind.contains(&kind)))
.filter(|v| v.targets.iter().any(|t| t.kind.contains(&TargetKind::Lib)))
.map(|v| v.name.clone())
.collect();
Ok(packages)
}
impl FormatCommand {
fn run(self) -> Result<()> {
self.run_rustfmt()?;
self.run_taplo()?;
Ok(())
}
fn run_rustfmt(&self) -> Result<(), color_eyre::eyre::Error> {
let mut args = vec!["fmt", "--all"];
if self.check {
args.push("--check");
}
run_cargo_nightly(args)?;
Ok(())
}
fn run_taplo(&self) -> Result<(), color_eyre::eyre::Error> {
let mut args = vec!["format", "--colors", "always"];
if self.check {
args.push("--check");
}
cmd("taplo", args).run_with_trace()?;
Ok(())
}
}
/// Lint markdown files using [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)
fn lint_markdown() -> Result<()> {
cmd!("markdownlint-cli2", "**/*.md", "!target").run_with_trace()?;
Ok(())
}
impl TyposCommand {
fn run(self) -> Result<()> {
if self.fix {
cmd!("typos", "--write-changes").run_with_trace()?;
} else {
cmd!("typos").run_with_trace()?;
}
Ok(())
}
}
/// Run tests for libs, backends, and docs
fn test() -> Result<()> {
test_libs()?;
test_backend(Backend::Crossterm)?;
test_backend(Backend::Termion)?;
test_backend(Backend::Termwiz)?;
test_docs()?; // run last because it's slow
Ok(())
}
/// Run tests for the specified backend
fn test_backend(backend: Backend) -> Result<()> {
if cfg!(windows) && backend == Backend::Termion {
tracing::error!("termion backend is not supported on Windows");
}
let backend = match backend {
Backend::Crossterm => "crossterm",
Backend::Termion => "termion",
Backend::Termwiz => "termwiz",
};
run_cargo(vec![
"test",
"--all-targets",
"--no-default-features",
"--features",
backend,
])
}
/// Run doc tests for the workspace's default packages
fn test_docs() -> Result<()> {
run_cargo(vec!["test", "--doc", "--all-features"])
}
/// Run lib tests for the workspace's default packages
fn test_libs() -> Result<()> {
run_cargo(vec!["test", "--lib", "--all-targets", "--all-features"])
}
/// Run cargo hack to test each feature in isolation
fn hack() -> Result<()> {
run_cargo(vec![
"hack",
"test",
"--lib",
"--each-feature",
"--workspace",
])
}
/// Run a cargo subcommand with the default toolchain
fn run_cargo(args: Vec<&str>) -> Result<()> {
cmd("cargo", args).run_with_trace()?;