Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a931659
bitflags
MusicalNinjaDad Aug 24, 2026
f0265b9
use flags in commands
MusicalNinjaDad Aug 24, 2026
2daff2b
fix deadlock
MusicalNinjaDad Aug 24, 2026
8e961ac
store flags in Spawned
MusicalNinjaDad Aug 24, 2026
f83b59f
simplify FromIterator<Spawned> for Exit<()>
MusicalNinjaDad Aug 24, 2026
7ed33a6
output my stuff as json
MusicalNinjaDad Aug 24, 2026
e70d63c
for all cmds
MusicalNinjaDad Aug 24, 2026
b54c464
use serde_json
MusicalNinjaDad Aug 24, 2026
662b159
fix payload
MusicalNinjaDad Aug 24, 2026
4288d34
try only parsing lines which start with a {
MusicalNinjaDad Aug 24, 2026
c3aa963
and store json with errors too
MusicalNinjaDad Aug 24, 2026
3037846
actually output the json on error
MusicalNinjaDad Aug 24, 2026
55533a4
fix tests
MusicalNinjaDad Aug 24, 2026
5a33158
don't push null jsons
MusicalNinjaDad Aug 24, 2026
18382b2
flatten json nesting
MusicalNinjaDad Aug 24, 2026
1ec95c5
rm json example output
MusicalNinjaDad Aug 24, 2026
4dc1959
unbreak test
MusicalNinjaDad Aug 24, 2026
8ab30f6
mod exit_with_json
MusicalNinjaDad Aug 24, 2026
f089764
commands as dir
MusicalNinjaDad Aug 24, 2026
61ab187
move commands to own files
MusicalNinjaDad Aug 24, 2026
36dc5c4
Cmd & Spawned -> commands
MusicalNinjaDad Aug 24, 2026
ca178fc
cli -> cli
MusicalNinjaDad Aug 24, 2026
772ea29
From<io::Error> for Exit<T>
MusicalNinjaDad Aug 24, 2026
593d660
fmt
MusicalNinjaDad Aug 24, 2026
4bdb4a6
readable impl From<Cmd> for Exit<WithJson<()>>
MusicalNinjaDad Aug 24, 2026
3cdca68
early return
MusicalNinjaDad Aug 25, 2026
f90531d
ewrly return with correct output on err
MusicalNinjaDad Aug 25, 2026
7204952
fmt
MusicalNinjaDad Aug 25, 2026
5fbecfa
v bump
MusicalNinjaDad Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions ninja-xtask/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion ninja-xtask/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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]
Expand Down
62 changes: 62 additions & 0 deletions ninja-xtask/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
/// 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<String>,
},
}

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(),
}
}
}
63 changes: 2 additions & 61 deletions ninja-xtask/src/commands.rs → ninja-xtask/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -84,7 +25,7 @@ pub fn build(
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.into_spawned("build")
.into_spawned("build", None)
}

struct BuildArgs {
Expand Down
160 changes: 160 additions & 0 deletions ninja-xtask/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Output, io::Error>,
pub flags: CheckFlags,
}

pub trait CmdExt {
fn into_cmd(self, name: &'static str, checkflags: Option<CheckFlags>) -> Cmd;
}

impl CmdExt for Result<Output, io::Error> {
fn into_cmd(self, name: &'static str, checkflags: Option<CheckFlags>) -> Cmd {
Cmd {
name,
result: self,
flags: checkflags.unwrap_or_default(),
}
}
}

impl From<Cmd> for Exit<WithJson<()>> {
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::<Value>)
.map(|json| json.unwrap_or_else(|err| json!({"unparsable": &err.to_string()})))
.collect::<Value>();
json!({
"task": task,
"status": &status.to_string(),
"payload": payload,
})
Comment thread
MusicalNinjaDad marked this conversation as resolved.
});
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<Child, io::Error>,
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<CheckFlags>) -> Spawned;
}

impl SpawnedExt for Result<Child, io::Error> {
fn into_spawned(self, name: &'static str, flags: Option<CheckFlags>) -> Spawned {
Spawned {
name,
child: self,
flags: flags.unwrap_or_default(),
}
}
}

impl FromIterator<Spawned> for Exit<WithJson<()>> {
fn from_iter<I: IntoIterator<Item = Spawned>>(spawns: I) -> Self {
spawns.into_iter().map(Exit::from).collect()
}
}

impl From<Spawned> for Exit<WithJson<()>> {
fn from(spawn: Spawned) -> Self {
spawn.wait().into()
}
}
Loading