Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ab1f02e
skeleton
MusicalNinjaDad Apr 12, 2026
4dc7555
basic shape for main
MusicalNinjaDad Apr 12, 2026
cdd462c
let main compile
MusicalNinjaDad Apr 12, 2026
e806f85
skeleton how fmt could be tested
MusicalNinjaDad Apr 12, 2026
80e95fc
implement & test fmt
MusicalNinjaDad Apr 12, 2026
e22eae8
run fmt
MusicalNinjaDad Apr 12, 2026
3d662fe
fmt
MusicalNinjaDad Apr 12, 2026
bc11819
check output of fmt then run git add
MusicalNinjaDad Apr 12, 2026
cd23320
provide feedback if task OK
MusicalNinjaDad Apr 12, 2026
4d26d0e
run clippy in spawned process
MusicalNinjaDad Apr 12, 2026
29574fd
and clippy the tests
MusicalNinjaDad Apr 12, 2026
9520f98
note that we fail fast
MusicalNinjaDad Apr 12, 2026
7002b72
something like join tasks
MusicalNinjaDad Apr 12, 2026
b06b6d6
and run the tests
MusicalNinjaDad Apr 12, 2026
13f4cf2
add xtask to default members for test, clippy etc
MusicalNinjaDad Apr 13, 2026
c9d1c6d
move commands to module
MusicalNinjaDad Apr 13, 2026
756f2be
move Exit to lib
MusicalNinjaDad Apr 13, 2026
9bf4d81
start to store the io::Error in the Cmd
MusicalNinjaDad Apr 13, 2026
1737b66
handle to IOError on Exit::from
MusicalNinjaDad Apr 13, 2026
0c9731b
complete from Cmd_
MusicalNinjaDad Apr 13, 2026
426eff5
return Cmd_
MusicalNinjaDad Apr 13, 2026
00b59b8
manual half impl FromIterator (Debug not Display)
MusicalNinjaDad Apr 13, 2026
91d4d89
hacky from ITerator
MusicalNinjaDad Apr 13, 2026
bb13e08
store strings not boxed errors
MusicalNinjaDad Apr 13, 2026
7664f69
no need for second IO variant
MusicalNinjaDad Apr 13, 2026
f8666f6
derive PartialEq&Ord
MusicalNinjaDad Apr 13, 2026
32151a4
add message function
MusicalNinjaDad Apr 13, 2026
4c94c7d
from Iterator looking better
MusicalNinjaDad Apr 13, 2026
78b3c1d
update the test
MusicalNinjaDad Apr 13, 2026
f299f40
add Spawned_
MusicalNinjaDad Apr 13, 2026
337da96
SpawnedExt doesnt need to be generic over E
MusicalNinjaDad Apr 13, 2026
da23c91
add into_spawned
MusicalNinjaDad Apr 13, 2026
931ecce
fmt
MusicalNinjaDad Apr 13, 2026
73c4cab
now From Vec Spawned is nice
MusicalNinjaDad Apr 13, 2026
edb83c8
remove the old Cmd & Spawned
MusicalNinjaDad Apr 13, 2026
88a9898
and rename the new ones
MusicalNinjaDad Apr 13, 2026
d14fe1d
handle let_chains stabilisation
MusicalNinjaDad Apr 13, 2026
34a00a3
remove unused direct conversion from io::Error
MusicalNinjaDad Apr 13, 2026
945c5c8
reorder for easier reading
MusicalNinjaDad Apr 13, 2026
3bbdc44
bump exit_safely to fix msrv
MusicalNinjaDad Apr 13, 2026
752c0ac
don't need build.rs for xtasks
MusicalNinjaDad Apr 13, 2026
bb5c6fe
better handling of stable_feature probe
MusicalNinjaDad Apr 13, 2026
9874770
add cargo aliases for xtasks
MusicalNinjaDad Apr 13, 2026
52eee1e
check fmt runs OK
MusicalNinjaDad Apr 13, 2026
91e64ff
dbg tmp path - to help find CI issue
MusicalNinjaDad Apr 13, 2026
6b36343
safer channel settings for stability checks
MusicalNinjaDad Apr 13, 2026
bb6379a
try again to set default profile
MusicalNinjaDad Apr 13, 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
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[alias]
x = "run --package try_v2_xtasks --"
stage = "run --package try_v2_xtasks -- add"
8 changes: 4 additions & 4 deletions .github/workflows/rust-stability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: update rust
run: rustup update
run: rustup set profile default && rustup update && rustup default ${{ matrix.channel }}
- name: Run tests
run: cargo +${{ matrix.channel }} test --no-fail-fast
run: cargo test --no-fail-fast

lint:
strategy:
Expand All @@ -39,9 +39,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: update rust
run: rustup update && rustup component add --toolchain ${{ matrix.channel }} clippy
run: rustup set profile default && rustup update && rustup default ${{ matrix.channel }}
- name: clippy
run: cargo +${{ matrix.channel }} clippy -- --deny warnings
run: cargo clippy -- --deny warnings

report:
if: ${{ always() }}
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ trybuild = "1.0.116"
autocfg = {workspace = true }

[workspace]
members = ["tests/compilation"]
members = ["tests/compilation", "xtask"]
default-members = ["xtask"]

[workspace.dependencies]
try_v2 = { path = "."}
Expand Down
11 changes: 9 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ fn main() {

fn stable_feature(ac: &AutoCfg, feature: &'static str) {
let cfg = format!("stable_{feature}");
let code = format!(
let deny = format!(
r#"
#![deny(stable_features)]
#![feature({feature})]
"#
);

let allow = format!(
r#"
#![allow(stable_features)]
#![feature({feature})]
"#
);

autocfg::emit_possibility(&cfg);
if ac.probe_raw(&code).is_err() {
if ac.probe_raw(&deny).is_err() && ac.probe_raw(&allow).is_ok() {
autocfg::emit(&cfg);
}
}
Expand Down
17 changes: 17 additions & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "try_v2_xtasks"
version = "0.0.1"
edition = "2024"
publish = false

[dependencies]
exit_safely = "0.2.1"
try_v2 = { workspace = true }
clap = { version = "4.6.0", features = ["derive"] }

[dev-dependencies]
dircpy = "0.3.20"
tempfile = "3.27.0"

[build-dependencies]
autocfg = { workspace = true }
54 changes: 54 additions & 0 deletions xtask/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::{
path::Path,
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")
}
179 changes: 179 additions & 0 deletions xtask/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#![feature(never_type)]
#![feature(try_trait_v2)]
#![feature(try_trait_v2_residual)]

use std::{
fmt::Debug,
io,
process::{Child, Output, Termination as _T},
};

use exit_safely::Termination;
use try_v2::{Try, Try_ConvertResult};

pub mod commands;

#[derive(Debug, Termination, Try, Try_ConvertResult, PartialEq, PartialOrd, Eq, Ord)]
#[repr(u8)]
#[must_use]
pub enum Exit<T: _T> {
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<Self> {
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<Exit<()>> for Exit<()> {
fn from_iter<I: IntoIterator<Item = Exit<()>>>(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<T: _T> From<clap::Error> for Exit<T> {
fn from(e: clap::Error) -> Self {
Self::InvocationError(e.to_string())
}
}

#[derive(Debug)]
pub struct Cmd {
pub name: &'static str,
pub result: Result<Output, io::Error>,
}

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

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

impl From<Cmd> for Exit<()> {
fn from(cmd: Cmd) -> Self {
match cmd.result {
Ok(output) => {
if output.status.success() {
println!("{}: OK", cmd.name);
Self::Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Self::Error(stderr.to_string())
}
}
Err(e) => {
let msg = format!("{} failed: {}", cmd.name, e);
Self::IO(msg)
}
}
}
}

#[derive(Debug)]
pub struct Spawned {
pub name: &'static str,
pub child: Result<Child, io::Error>,
}

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<Child, io::Error> {
fn into_spawned(self, name: &'static str) -> Spawned {
Spawned { name, child: self }
}
}

impl From<Vec<Spawned>> for Exit<()> {
fn from(spawns: Vec<Spawned>) -> Self {
spawns
.into_iter()
.map(|spawn| spawn.wait())
.map(Exit::from)
.collect()
}
}

#[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));
}
}
39 changes: 39 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::path::Path;

use clap::{Parser, Subcommand};
use try_v2_xtasks::{
Exit,
commands::{clippy, clippy_tests, fmt, git_add, test},
};

#[derive(Parser)]
#[command(version)]
struct XTask {
#[command(subcommand)]
command: Command,
}

#[derive(Subcommand)]
enum Command {
/// git add if all is good
Add,
}

fn main() -> Exit<()> {
let xtask = XTask::try_parse()?;

match &xtask.command {
Command::Add => {
let root = Path::new(".");
let fmt = fmt(root);
Exit::from(fmt)?;
let clippy = clippy(root);
let clippy_tests = clippy_tests(root);
let tests = test(root);
let checks = vec![clippy, clippy_tests, tests];
Exit::from(checks)?;
let git = git_add(root);
Exit::from(git)
}
}
}
5 changes: 5 additions & 0 deletions xtask/tests/fixture/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "try_v2_xtasks_fixture"
version = "0.0.1"
edition = "2024"
publish = false
1 change: 1 addition & 0 deletions xtask/tests/fixture/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum foo { bar }
25 changes: 25 additions & 0 deletions xtask/tests/test_fmt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::fs;

use dircpy::copy_dir;
use tempfile::tempdir;
use try_v2_xtasks::commands::fmt;

#[test]
fn fmt_fixture() {
let tmp = tempdir().expect("couldn't create temp dir for test");
copy_dir("tests/fixture", tmp.path()).expect("couldn't copy 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 output = cmd.result.expect("`cargo fmt` failed to run");
assert!(
output.status.success(),
"`cargo fmt` exited with status {:?}\nstderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let formatted = fs::read_to_string(tmp.path().join("src/lib.rs")).unwrap();
assert_ne!(original, formatted);
dbg!(tmp.path());
}
Loading