From 9c0cf274a33afd73d834a0b85f29dc469c4af3a1 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 17 Oct 2025 00:47:54 +0200 Subject: [PATCH] chore(workspace): add cargo xtask crate for common ops --- Cargo.toml | 1 + xtask/Cargo.toml | 9 +++ xtask/src/main.rs | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index ad75879..67711e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/mcp/code-server", "crates/mcp/prompt-server", "crates/owlen-markdown", + "xtask", ] exclude = [] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..ca07ab0 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "xtask" +version = "0.1.0" +edition.workspace = true +publish = false + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..0b1d1ec --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,162 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result, bail}; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(author, version, about = "Owlen developer tasks", long_about = None)] +struct Xtask { + #[command(subcommand)] + command: Task, +} + +#[derive(Subcommand)] +enum Task { + /// Format the workspace (use --check to verify without writing). + Fmt { + #[arg(long, help = "Run rustfmt in check mode")] + check: bool, + }, + /// Run clippy with all warnings elevated to errors. + Lint, + /// Execute the full workspace test suite. + Test, + /// Run coverage via cargo-llvm-cov (requires the tool to be installed). + Coverage, + /// Launch the default Owlen CLI binary (owlen) with optional args. + DevRun { + #[arg(last = true, help = "Arguments forwarded to `owlen`")] + args: Vec, + }, + /// Composite release validation (fmt --check, clippy, test). + ReleaseCheck, + /// Regenerate docs/repo-map.md (accepts optional output path). + GenRepoMap { + #[arg(long, value_name = "PATH", help = "Override the repo map output path")] + output: Option, + }, +} + +fn main() -> Result<()> { + let cli = Xtask::parse(); + + match cli.command { + Task::Fmt { check } => fmt(check), + Task::Lint => lint(), + Task::Test => test(), + Task::Coverage => coverage(), + Task::DevRun { args } => dev_run(args), + Task::ReleaseCheck => release_check(), + Task::GenRepoMap { output } => gen_repo_map(output), + } +} + +fn fmt(check: bool) -> Result<()> { + let mut args = vec!["fmt".to_string(), "--all".to_string()]; + if check { + args.push("--".to_string()); + args.push("--check".to_string()); + } + run_cargo(args) +} + +fn lint() -> Result<()> { + run_cargo(vec![ + "clippy".into(), + "--workspace".into(), + "--all-features".into(), + "--".into(), + "-D".into(), + "warnings".into(), + ]) +} + +fn test() -> Result<()> { + run_cargo(vec![ + "test".into(), + "--workspace".into(), + "--all-features".into(), + ]) +} + +fn coverage() -> Result<()> { + run_cargo(vec![ + "llvm-cov".into(), + "--workspace".into(), + "--all-features".into(), + "--summary-only".into(), + ]) + .with_context(|| "install `cargo llvm-cov` to use the coverage task".to_string()) +} + +fn dev_run(args: Vec) -> Result<()> { + let mut command_args = vec![ + "run".into(), + "-p".into(), + "owlen-cli".into(), + "--bin".into(), + "owlen".into(), + ]; + if !args.is_empty() { + command_args.push("--".into()); + command_args.extend(args); + } + run_cargo(command_args) +} + +fn release_check() -> Result<()> { + fmt(true)?; + lint()?; + test()?; + Ok(()) +} + +fn gen_repo_map(output: Option) -> Result<()> { + let script = workspace_root().join("scripts/gen-repo-map.sh"); + if !script.exists() { + bail!("repo map script not found at {}", script.display()); + } + + let mut cmd = Command::new(&script); + cmd.current_dir(workspace_root()); + if let Some(path) = output { + cmd.arg(path); + } + let status = cmd + .status() + .with_context(|| format!("failed to run {}", script.display()))?; + if !status.success() { + bail!( + "{} exited with status {}", + script.display(), + status.code().unwrap_or_default() + ); + } + Ok(()) +} + +fn run_cargo(args: Vec) -> Result<()> { + let mut cmd = Command::new("cargo"); + cmd.current_dir(workspace_root()); + cmd.args(&args); + + let status = cmd + .status() + .with_context(|| format!("failed to run cargo {}", args.join(" ")))?; + if !status.success() { + bail!( + "`cargo {}` exited with status {}", + args.join(" "), + status.code().unwrap_or_default() + ); + } + Ok(()) +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("xtask has a parent directory") + .to_path_buf() +}