diff --git a/ninja-xtask/Cargo.lock b/ninja-xtask/Cargo.lock index 754e0cc..905f17e 100644 --- a/ninja-xtask/Cargo.lock +++ b/ninja-xtask/Cargo.lock @@ -66,9 +66,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "cfg-if" @@ -405,14 +405,16 @@ dependencies = [ [[package]] name = "ninja-xtask" -version = "0.3.0" +version = "0.3.1" dependencies = [ "autocfg", + "bitflags", "clap", "clap-cargo", "dircpy", "exit_safely", "ninja-build_rs 0.3.0", + "serde_json", "tempfile", "try_v2", ] @@ -564,9 +566,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", diff --git a/ninja-xtask/Cargo.toml b/ninja-xtask/Cargo.toml index ffc0789..79ba4e5 100644 --- a/ninja-xtask/Cargo.toml +++ b/ninja-xtask/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ninja-xtask" -version = "0.3.0" +version = "0.3.1" edition = "2024" readme = "README.md" description = "xtask utilities that I use in most projects" @@ -19,9 +19,11 @@ path = "src/main.rs" pkg-url = "{ repo }/releases/download/{ name }_v{ version }/{ name }-{ target }-v{ version }{ archive-suffix }" [dependencies] +bitflags = "2.13.1" clap = { version = "4.6.0", features = ["derive"] } clap-cargo = "0.18.3" exit_safely = "0.3.3" +serde_json = "1.0.151" try_v2 = "0.9.2" [dev-dependencies] diff --git a/ninja-xtask/src/cli.rs b/ninja-xtask/src/cli.rs new file mode 100644 index 0000000..6d1f677 --- /dev/null +++ b/ninja-xtask/src/cli.rs @@ -0,0 +1,62 @@ +use std::fmt::Debug; + +use bitflags::bitflags; +use clap::{Parser, Subcommand}; +use clap_cargo::style::CLAP_STYLING as CARGO_STYLING; + +#[derive(Parser)] +#[command(name = "cargo")] +#[command(bin_name = "cargo")] +#[command(styles = CARGO_STYLING)] +pub enum CargoCmd { + #[command(subcommand)] + Ninja(NinjaCommand), +} + +#[derive(Subcommand)] +#[command(version)] +pub enum NinjaCommand { + /// fmt, lint & test then stage everything in git if all is good + Stage { + /// add --deny warnings to clippy invocations + #[arg(long)] + strict: bool, + /// output in json format + #[arg(long)] + json: bool, + }, + /// build (optionally with zigbuild for a given glibc version) + Build { + /// build for a specific glibc version (WSL-Ubuntu is 2.35) + #[arg(short, long)] + glibc: Option, + /// build a release build (default is cargo's default profile, usually debug) + #[arg(short, long)] + release: bool, + /// build for a given target + #[arg(long)] + target: Option, + }, +} + +bitflags! { + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CheckFlags: u32 { + const STRICT = 0b00000001; + const JSON = 0b00000010; + } +} + +impl From<&NinjaCommand> for CheckFlags { + fn from(xtask: &NinjaCommand) -> Self { + match xtask { + NinjaCommand::Stage { strict, json } => { + let mut flags = Self::default(); + flags.set(Self::STRICT, *strict); + flags.set(Self::JSON, *json); + flags + } + NinjaCommand::Build { .. } => CheckFlags::default(), + } + } +} diff --git a/ninja-xtask/src/commands.rs b/ninja-xtask/src/commands/build.rs similarity index 71% rename from ninja-xtask/src/commands.rs rename to ninja-xtask/src/commands/build.rs index 2ed4c74..b6af66d 100644 --- a/ninja-xtask/src/commands.rs +++ b/ninja-xtask/src/commands/build.rs @@ -3,66 +3,7 @@ use std::{ process::{Command, Stdio}, }; -use crate::{Cmd, CmdExt as _, Spawned, SpawnedExt as _}; - -pub fn fmt(root: &Path) -> Cmd { - Command::new("cargo") - .current_dir(root) - .arg("fmt") - .output() - .into_cmd("fmt") -} - -pub fn git_add(root: &Path) -> Cmd { - Command::new("git") - .current_dir(root) - .arg("add") - .arg(".") - .output() - .into_cmd("git add") -} - -pub fn clippy(root: &Path) -> Spawned { - Command::new("cargo") - .current_dir(root) - .arg("clippy") - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .into_spawned("clippy") -} - -pub fn clippy_tests(root: &Path) -> Spawned { - Command::new("cargo") - .current_dir(root) - .arg("clippy") - .arg("--tests") - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .into_spawned("clippy the tests") -} - -pub fn test(root: &Path) -> Spawned { - Command::new("cargo") - .current_dir(root) - .arg("test") - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .into_spawned("tests") -} - -pub fn test_examples(root: &Path) -> Spawned { - Command::new("cargo") - .current_dir(root) - .arg("test") - .arg("--examples") - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .into_spawned("test examples") -} +use super::{Spawned, SpawnedExt as _}; /// Spawn `cargo build` (if no `glibc` specified) / `cargo zigbuild` (if `target` or `glibc` /// specified) optionally performing a release build (default is cargo's default profile). @@ -84,7 +25,7 @@ pub fn build( .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .into_spawned("build") + .into_spawned("build", None) } struct BuildArgs { diff --git a/ninja-xtask/src/commands/mod.rs b/ninja-xtask/src/commands/mod.rs new file mode 100644 index 0000000..aeb96cc --- /dev/null +++ b/ninja-xtask/src/commands/mod.rs @@ -0,0 +1,160 @@ +use std::{ + fmt::Debug, + io, + process::{Child, Output}, +}; + +use serde_json::{Value, json}; + +mod stage; +pub use stage::*; + +mod build; +pub use build::*; + +use crate::{CheckFlags, Exit, WithJson}; + +#[derive(Debug)] +pub struct Cmd { + pub name: &'static str, + pub result: Result, + pub flags: CheckFlags, +} + +pub trait CmdExt { + fn into_cmd(self, name: &'static str, checkflags: Option) -> Cmd; +} + +impl CmdExt for Result { + fn into_cmd(self, name: &'static str, checkflags: Option) -> Cmd { + Cmd { + name, + result: self, + flags: checkflags.unwrap_or_default(), + } + } +} + +impl From for Exit> { + fn from(cmd: Cmd) -> Self { + let Cmd { + name: task, + result: did_it_spawn, + flags, + } = cmd; + + let output = match did_it_spawn { + Ok(output) => output, + Err(err_spawning) => { + let json = flags.contains(CheckFlags::JSON).then(|| { + json!({ + "task": task, + "status": "failed to spawn", + "error": &err_spawning.to_string(), + }) + }); + let msg = format!("{task} failed: {err_spawning}"); + return match json { + Some(json) => Exit::IO(WithJson { + value: String::new(), + json: Some(json), + }), + None => Self::IO(WithJson { + value: msg, + json: None, + }), + }; + } + }; + + let status = output.status; + let json = flags.contains(CheckFlags::JSON).then(|| { + let payload = String::from_utf8_lossy(&output.stdout) + .lines() + .filter(|line| line.starts_with("{")) + .map(serde_json::from_str::) + .map(|json| json.unwrap_or_else(|err| json!({"unparsable": &err.to_string()}))) + .collect::(); + json!({ + "task": task, + "status": &status.to_string(), + "payload": payload, + }) + }); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + match (status.success(), json) { + (true, Some(json)) => Exit::Ok(WithJson { + value: (), + json: Some(json), + }), + (true, None) => { + println!("{task}: OK"); + Self::Ok(WithJson { + value: (), + json: None, + }) + } + (false, Some(json)) => Exit::Error(WithJson { + value: String::new(), + json: Some(json), + }), + (false, None) => Self::Error(WithJson { + value: format!( + "====== {task} exited with {status} ======\n-- stdout: --\n{stdout}\n\n-- stderr: --\n{stderr}", + status = output.status + ), + json: None, + }), + } + } +} + +#[derive(Debug)] +pub struct Spawned { + pub name: &'static str, + pub child: Result, + pub flags: CheckFlags, +} + +impl Spawned { + pub fn wait(self) -> Cmd { + match self.child { + Ok(child) => child + .wait_with_output() + .into_cmd(self.name, Some(self.flags)), + Err(e) => Cmd { + name: self.name, + result: Err(e), + flags: self.flags, + }, + } + } +} + +pub trait SpawnedExt { + fn into_spawned(self, name: &'static str, flags: Option) -> Spawned; +} + +impl SpawnedExt for Result { + fn into_spawned(self, name: &'static str, flags: Option) -> Spawned { + Spawned { + name, + child: self, + flags: flags.unwrap_or_default(), + } + } +} + +impl FromIterator for Exit> { + fn from_iter>(spawns: I) -> Self { + spawns.into_iter().map(Exit::from).collect() + } +} + +impl From for Exit> { + fn from(spawn: Spawned) -> Self { + spawn.wait().into() + } +} diff --git a/ninja-xtask/src/commands/stage.rs b/ninja-xtask/src/commands/stage.rs new file mode 100644 index 0000000..904c6f4 --- /dev/null +++ b/ninja-xtask/src/commands/stage.rs @@ -0,0 +1,96 @@ +use std::{ + path::Path, + process::{Command, Stdio}, +}; + +use super::{Cmd, CmdExt as _, Spawned, SpawnedExt as _}; +use crate::CheckFlags; + +pub fn fmt(root: &Path, flags: CheckFlags) -> Cmd { + Command::new("cargo") + .current_dir(root) + .arg("fmt") + .output() + .into_cmd("fmt", Some(flags)) +} + +pub fn git_add(root: &Path, flags: CheckFlags) -> Cmd { + Command::new("git") + .current_dir(root) + .arg("add") + .arg(".") + .output() + .into_cmd("git add", Some(flags)) +} + +pub fn clippy(root: &Path, flags: CheckFlags) -> Spawned { + let mut clippy = Command::new("cargo"); + clippy + .current_dir(root) + .arg("clippy") + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + if flags.contains(CheckFlags::JSON) { + clippy.arg("--message-format=json"); + } + if flags.contains(CheckFlags::STRICT) { + clippy.args(["--", "--deny", "warnings"]); + } + clippy.spawn().into_spawned("clippy", Some(flags)) +} + +pub fn clippy_tests(root: &Path, flags: CheckFlags) -> Spawned { + let mut clippy = Command::new("cargo"); + clippy + .current_dir(root) + .arg("clippy") + .arg("--tests") + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + if flags.contains(CheckFlags::JSON) { + clippy.arg("--message-format=json"); + } + if flags.contains(CheckFlags::STRICT) { + clippy.args(["--", "--deny", "warnings"]); + } + clippy.spawn().into_spawned("clippy the tests", Some(flags)) +} + +pub fn test(root: &Path, flags: CheckFlags) -> Spawned { + let mut testlib = Command::new("cargo"); + testlib + .current_dir(root) + .arg("test") + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + if flags.contains(CheckFlags::JSON) { + testlib.args([ + "--message-format=json", + "--", + "-Zunstable-options", + "--format", + "json", + ]); + } + testlib.spawn().into_spawned("tests", Some(flags)) +} + +pub fn test_examples(root: &Path, flags: CheckFlags) -> Spawned { + let mut testlib = Command::new("cargo"); + testlib + .current_dir(root) + .arg("test") + .arg("--examples") + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + if flags.contains(CheckFlags::JSON) { + testlib.args([ + "--message-format=json", + "--", + "-Zunstable-options", + "--format", + "json", + ]); + } + testlib.spawn().into_spawned("test examples", Some(flags)) +} diff --git a/ninja-xtask/src/exit_with_json.rs b/ninja-xtask/src/exit_with_json.rs new file mode 100644 index 0000000..954ea37 --- /dev/null +++ b/ninja-xtask/src/exit_with_json.rs @@ -0,0 +1,213 @@ +use std::{ + fmt::{Debug, Display}, + io, + process::Termination as _T, +}; + +use exit_safely::Termination; +use serde_json::Value::{self, Array}; +use try_v2::Try; + +#[derive(Debug, Termination, Try, PartialEq, PartialOrd, Eq, Ord)] +#[FromResidual(Result<_, Self::Residual>)] +#[repr(u8)] +#[must_use] +pub enum Exit { + Ok(T) = 0, + Error(WithJson) = 1, + InvocationError(WithJson) = 2, + IO(WithJson) = 3, +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct WithJson { + pub value: T, + pub json: Option, +} + +impl _T for WithJson +where + T: _T, +{ + fn report(self) -> std::process::ExitCode { + if let Some(json) = self.json { + println!("{json}"); + }; + self.value.report() + } +} + +impl Display for WithJson +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f)?; + if let Some(json) = self.json.clone() { + write!(f, "\n{}", json)?; + }; + Ok(()) + } +} + +impl Ord for WithJson +where + T: Ord, +{ + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.value.cmp(&other.value) { + std::cmp::Ordering::Equal => {} + ord => return ord, + } + Ord::cmp( + &self.json.as_ref().unwrap_or_default().to_string(), + &other.json.as_ref().unwrap_or_default().to_string(), + ) + } +} + +impl PartialOrd for WithJson +where + T: PartialOrd, +{ + fn partial_cmp(&self, other: &Self) -> Option { + match self.value.partial_cmp(&other.value) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + PartialOrd::partial_cmp( + &self.json.as_ref().unwrap_or_default().to_string(), + &other.json.as_ref().unwrap_or_default().to_string(), + ) + } +} + +impl Exit> { + fn message(&self) -> &str { + match self { + Exit::Ok(_) => "", + Exit::Error(WithJson { + value: msg, + json: _, + }) => msg, + Exit::InvocationError(WithJson { + value: msg, + json: _, + }) => msg, + Exit::IO(WithJson { + value: msg, + json: _, + }) => msg, + } + } + + fn replace_message(self, msg: String, jsons: Vec) -> Self { + let json = (!jsons.is_empty()).then(|| jsons.into_iter().collect::()); + match self { + Exit::Ok(_) => Self::Ok(WithJson { value: (), json }), + Exit::Error(_) => Exit::Error(WithJson { value: msg, json }), + Exit::InvocationError(_) => Exit::InvocationError(WithJson { value: msg, json }), + Exit::IO(_) => Exit::IO(WithJson { value: msg, json }), + } + } + + pub fn take_json(&mut self) -> Option { + match self { + Exit::Ok(WithJson { json, .. }) => json.take(), + Exit::Error(WithJson { json, .. }) => json.take(), + Exit::InvocationError(WithJson { json, .. }) => json.take(), + Exit::IO(WithJson { json, .. }) => json.take(), + } + } +} + +impl FromIterator>> for Exit> { + fn from_iter>>>(iter: I) -> Self { + let mut msg = String::new(); + let mut jsons = Vec::::new(); + iter.into_iter() + .map(|mut exit| { + msg.push_str(exit.message()); + match exit.take_json() { + Some(Array(json)) => jsons.extend(json), + Some(json) => jsons.push(json), + None => {} + } + exit + }) + .max() + .map(|highest_exit_code| highest_exit_code.replace_message(msg, jsons)) + .unwrap_or(Exit::Ok(Default::default())) + } +} + +impl From for Exit { + fn from(e: clap::Error) -> Self { + Self::InvocationError(WithJson { + value: e.to_string(), + json: None, + }) + } +} + +impl From for Exit { + fn from(e: io::Error) -> Self { + Self::IO(WithJson { + value: e.to_string(), + json: None, + }) + } +} + +#[cfg(test)] +mod tests { + use std::{assert_matches, io, process::Command}; + + use crate::commands::{Cmd, CmdExt as _}; + + use super::*; + + #[test] + fn exit_from_404() { + let splat: Cmd = Command::new("splat").output().into_cmd("splat", None); + assert_eq!(splat.name, "splat"); + assert!( + matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound)) + ); + let exit: Exit> = Exit::from(splat); + let Exit::IO(WithJson { + value: msg, + json: _, + }) = exit + else { + panic!("not an IO2") + }; + eprintln!("{}", msg); + assert!(msg.starts_with("splat failed: ")); + } + + #[test] + fn collect_exit() { + let exits = [ + Exit::Ok(WithJson { + value: (), + json: None, + }), + Exit::IO(WithJson { + value: "one\n".to_string(), + json: None, + }), + Exit::Error(WithJson { + value: "two\n".to_string(), + json: None, + }), + Exit::Error(WithJson { + value: "three\n".to_string(), + json: None, + }), + ]; + let exit: Exit> = exits.into_iter().collect(); + let expected = "one\ntwo\nthree\n"; + assert_matches!(exit, Exit::IO(s) if s.value == expected); + } +} diff --git a/ninja-xtask/src/lib.rs b/ninja-xtask/src/lib.rs index 3a58e43..6697c81 100644 --- a/ninja-xtask/src/lib.rs +++ b/ninja-xtask/src/lib.rs @@ -2,190 +2,9 @@ #![cfg_attr(unstable_try_trait_v2, feature(try_trait_v2))] #![cfg_attr(unstable_try_trait_v2_residual, feature(try_trait_v2_residual))] -use std::{ - fmt::Debug, - io, - process::{Child, Output, Termination as _T}, -}; - -use exit_safely::Termination; -use try_v2::Try; - +pub mod cli; pub mod commands; +pub mod exit_with_json; -#[derive(Debug, Termination, Try, PartialEq, PartialOrd, Eq, Ord)] -#[FromResidual(Result<_, Self::Residual>)] -#[repr(u8)] -#[must_use] -pub enum Exit { - Ok(T) = 0, - Error(String) = 1, - InvocationError(String) = 2, - IO(String) = 3, -} - -impl Exit<()> { - fn message(&self) -> &str { - match self { - Exit::Ok(_) => "", - Exit::Error(m) => m, - Exit::InvocationError(m) => m, - Exit::IO(m) => m, - } - } - - fn replace_message(self, msg: String) -> Option { - match self { - Exit::Ok(_) => None, - Exit::Error(_) => Some(Exit::Error(msg)), - Exit::InvocationError(_) => Some(Exit::InvocationError(msg)), - Exit::IO(_) => Some(Exit::IO(msg)), - } - } -} - -impl FromIterator> for Exit<()> { - fn from_iter>>(iter: I) -> Self { - let mut msg = String::new(); - iter.into_iter() - .filter_map(|e| { - if let Exit::Ok(_) = e { - None - } else { - msg.push_str(e.message()); - msg.push('\n'); - Some(e) - } - }) - .min() - .and_then(|e| e.replace_message(msg)) - .unwrap_or(Exit::Ok(())) - } -} - -impl From for Exit { - fn from(e: clap::Error) -> Self { - Self::InvocationError(e.to_string()) - } -} - -#[derive(Debug)] -pub struct Cmd { - pub name: &'static str, - pub result: Result, -} - -trait CmdExt { - fn into_cmd(self, name: &'static str) -> Cmd; -} - -impl CmdExt for Result { - fn into_cmd(self, name: &'static str) -> Cmd { - Cmd { name, result: self } - } -} - -// TODO: #10 Provide stdout & error code on failure -impl From for Exit<()> { - fn from(cmd: Cmd) -> Self { - match cmd.result { - Ok(output) => { - if output.status.success() { - println!("{}: OK", cmd.name); - Self::Ok(()) - } else { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - Self::Error(format!( - "====== {} exited with {} ======\n-- stdout: --\n{}\n\n-- stderr: --\n{}", - cmd.name, output.status, stdout, stderr - )) - } - } - Err(e) => { - let msg = format!("{} failed: {}", cmd.name, e); - Self::IO(msg) - } - } - } -} - -#[derive(Debug)] -pub struct Spawned { - pub name: &'static str, - pub child: Result, -} - -impl Spawned { - pub fn wait(self) -> Cmd { - match self.child { - Ok(child) => child.wait_with_output().into_cmd(self.name), - Err(e) => Cmd { - name: self.name, - result: Err(e), - }, - } - } -} - -trait SpawnedExt { - fn into_spawned(self, name: &'static str) -> Spawned; -} - -impl SpawnedExt for Result { - fn into_spawned(self, name: &'static str) -> Spawned { - Spawned { name, child: self } - } -} - -impl FromIterator for Exit<()> { - fn from_iter>(spawns: I) -> Self { - spawns - .into_iter() - .map(|spawn| spawn.wait()) - .map(Exit::from) - .collect() - } -} - -impl From for Exit<()> { - fn from(spawn: Spawned) -> Self { - spawn.wait().into() - } -} - -#[cfg(test)] -mod tests { - use std::process::Command; - - use super::*; - - #[test] - fn exit_from_404() { - let splat: Cmd = Command::new("splat").output().into_cmd("splat"); - assert_eq!(splat.name, "splat"); - assert!( - matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound)) - ); - let exit: Exit<()> = Exit::from(splat); - let Exit::IO(ref msg) = exit else { - panic!("not an IO2") - }; - eprintln!("{}", msg); - assert!(msg.starts_with("splat failed: ")); - } - - #[test] - fn collect_exit() { - let exits = [ - Exit::Ok(()), - Exit::IO("one".to_string()), - Exit::Error("two".to_string()), - Exit::Error("three".to_string()), - ]; - let exit: Exit<()> = exits.into_iter().collect(); - let expected = "one\ntwo\nthree\n"; - dbg!(&exit); - assert!(matches!(exit, Exit::Error(s) if s == expected)); - } -} +pub use cli::*; +pub use exit_with_json::{Exit, WithJson}; diff --git a/ninja-xtask/src/main.rs b/ninja-xtask/src/main.rs index b20a61d..eb232dd 100644 --- a/ninja-xtask/src/main.rs +++ b/ninja-xtask/src/main.rs @@ -1,61 +1,35 @@ use std::path::Path; -use clap::{Parser, Subcommand}; -use clap_cargo::style::CLAP_STYLING as CARGO_STYLING; +use clap::Parser; use ninja_xtask::{ - Exit, + CargoCmd, CheckFlags, Exit, NinjaCommand, WithJson, commands::{build, clippy, clippy_tests, fmt, git_add, test, test_examples}, }; -#[derive(Parser)] -#[command(name = "cargo")] -#[command(bin_name = "cargo")] -#[command(styles = CARGO_STYLING)] -enum CargoCmd { - #[command(subcommand)] - Ninja(Command), -} - -#[derive(Subcommand)] -#[command(version)] -enum Command { - /// fmt, lint & test then stage everything in git if all is good - Stage, - /// build (optionally with zigbuild for a given glibc version) - Build { - /// build for a specific glibc version (WSL-Ubuntu is 2.35) - #[arg(short, long)] - glibc: Option, - /// build a release build (default is cargo's default profile, usually debug) - #[arg(short, long)] - release: bool, - /// build for a given target - #[arg(long)] - target: Option, - }, -} - -fn main() -> Exit<()> { +fn main() -> Exit> { let CargoCmd::Ninja(xtask) = CargoCmd::try_parse()?; let root = Path::new("."); + let checkflags = CheckFlags::from(&xtask); match &xtask { - Command::Stage => { - let fmt = fmt(root); - Exit::from(fmt)?; + NinjaCommand::Stage { .. } => { + let fmt = fmt(root, checkflags); + let fmt = Exit::from(fmt)?; + + let clippy = [clippy(root, checkflags), clippy_tests(root, checkflags)]; + let clippy_result = Exit::from_iter(clippy); + + // Cannot run in parallel with clippy as --json leads to deadlock on build dir + let tests = [test(root, checkflags), test_examples(root, checkflags)]; + let test_result = Exit::from_iter(tests); - let checks = [ - clippy(root), - clippy_tests(root), - test(root), - test_examples(root), - ]; - Exit::from_iter(checks)?; + // But we do want to collect all the results before returning or staging + let checks = Exit::from_iter([Exit::Ok(fmt), clippy_result, test_result])?; - let git = git_add(root); - Exit::from(git) + let git = git_add(root, checkflags); + Exit::from_iter([Exit::Ok(checks), Exit::from(git)]) } - Command::Build { + NinjaCommand::Build { glibc, release, target, diff --git a/ninja-xtask/tests/test_commands.rs b/ninja-xtask/tests/test_commands.rs index 829f563..97b447b 100644 --- a/ninja-xtask/tests/test_commands.rs +++ b/ninja-xtask/tests/test_commands.rs @@ -2,7 +2,7 @@ use std::{fs, path::PathBuf}; use dircpy::copy_dir; use ninja_xtask::{ - Exit, + CheckFlags, Exit, commands::{fmt, test}, }; use tempfile::tempdir; @@ -14,7 +14,7 @@ fn fmt_fixture() { let original = fs::read_to_string("tests/fixture/src/lib.rs").unwrap(); let copied = fs::read_to_string(tmp.path().join("src/lib.rs")).unwrap(); assert_eq!(original, copied); - let cmd = fmt(tmp.path()); + let cmd = fmt(tmp.path(), CheckFlags::default()); let output = cmd.result.expect("`cargo fmt` failed to run"); assert!( output.status.success(), @@ -30,11 +30,12 @@ fn fmt_fixture() { #[test] fn fail_output() { let fixture = PathBuf::from("tests/fixture"); - let run_tests = test(&fixture); + let run_tests = test(&fixture, CheckFlags::default()); let exit = Exit::from(run_tests); let Exit::Error(output) = exit else { panic!("test didn't fail") }; + let output = output.value; assert!(output.contains("====== tests exited with")); assert!(output.contains("test printed to stdout")); assert!(output.contains("test dbg"));