Skip to content

Commit edfce08

Browse files
add xtask (#66)
1 parent 0d0cf11 commit edfce08

11 files changed

Lines changed: 338 additions & 7 deletions

File tree

.cargo/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[alias]
2+
x = "run --package try_v2_xtasks --"
3+
stage = "run --package try_v2_xtasks -- add"

.github/workflows/rust-stability.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ jobs:
2626
steps:
2727
- uses: actions/checkout@v6
2828
- name: update rust
29-
run: rustup update
29+
run: rustup set profile default && rustup update && rustup default ${{ matrix.channel }}
3030
- name: Run tests
31-
run: cargo +${{ matrix.channel }} test --no-fail-fast
31+
run: cargo test --no-fail-fast
3232

3333
lint:
3434
strategy:
@@ -39,9 +39,9 @@ jobs:
3939
steps:
4040
- uses: actions/checkout@v6
4141
- name: update rust
42-
run: rustup update && rustup component add --toolchain ${{ matrix.channel }} clippy
42+
run: rustup set profile default && rustup update && rustup default ${{ matrix.channel }}
4343
- name: clippy
44-
run: cargo +${{ matrix.channel }} clippy -- --deny warnings
44+
run: cargo clippy -- --deny warnings
4545

4646
report:
4747
if: ${{ always() }}

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ trybuild = "1.0.116"
2929
autocfg = {workspace = true }
3030

3131
[workspace]
32-
members = ["tests/compilation"]
32+
members = ["tests/compilation", "xtask"]
33+
default-members = ["xtask"]
3334

3435
[workspace.dependencies]
3536
try_v2 = { path = "."}

build.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,22 @@ fn main() {
1515

1616
fn stable_feature(ac: &AutoCfg, feature: &'static str) {
1717
let cfg = format!("stable_{feature}");
18-
let code = format!(
18+
let deny = format!(
1919
r#"
2020
#![deny(stable_features)]
2121
#![feature({feature})]
2222
"#
2323
);
2424

25+
let allow = format!(
26+
r#"
27+
#![allow(stable_features)]
28+
#![feature({feature})]
29+
"#
30+
);
31+
2532
autocfg::emit_possibility(&cfg);
26-
if ac.probe_raw(&code).is_err() {
33+
if ac.probe_raw(&deny).is_err() && ac.probe_raw(&allow).is_ok() {
2734
autocfg::emit(&cfg);
2835
}
2936
}

xtask/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "try_v2_xtasks"
3+
version = "0.0.1"
4+
edition = "2024"
5+
publish = false
6+
7+
[dependencies]
8+
exit_safely = "0.2.1"
9+
try_v2 = { workspace = true }
10+
clap = { version = "4.6.0", features = ["derive"] }
11+
12+
[dev-dependencies]
13+
dircpy = "0.3.20"
14+
tempfile = "3.27.0"
15+
16+
[build-dependencies]
17+
autocfg = { workspace = true }

xtask/src/commands.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use std::{
2+
path::Path,
3+
process::{Command, Stdio},
4+
};
5+
6+
use crate::{Cmd, CmdExt as _, Spawned, SpawnedExt as _};
7+
8+
pub fn fmt(root: &Path) -> Cmd {
9+
Command::new("cargo")
10+
.current_dir(root)
11+
.arg("fmt")
12+
.output()
13+
.into_cmd("fmt")
14+
}
15+
16+
pub fn git_add(root: &Path) -> Cmd {
17+
Command::new("git")
18+
.current_dir(root)
19+
.arg("add")
20+
.arg(".")
21+
.output()
22+
.into_cmd("git add")
23+
}
24+
25+
pub fn clippy(root: &Path) -> Spawned {
26+
Command::new("cargo")
27+
.current_dir(root)
28+
.arg("clippy")
29+
.stderr(Stdio::piped())
30+
.stdout(Stdio::piped())
31+
.spawn()
32+
.into_spawned("clippy")
33+
}
34+
35+
pub fn clippy_tests(root: &Path) -> Spawned {
36+
Command::new("cargo")
37+
.current_dir(root)
38+
.arg("clippy")
39+
.arg("--tests")
40+
.stderr(Stdio::piped())
41+
.stdout(Stdio::piped())
42+
.spawn()
43+
.into_spawned("clippy the tests")
44+
}
45+
46+
pub fn test(root: &Path) -> Spawned {
47+
Command::new("cargo")
48+
.current_dir(root)
49+
.arg("test")
50+
.stderr(Stdio::piped())
51+
.stdout(Stdio::piped())
52+
.spawn()
53+
.into_spawned("tests")
54+
}

xtask/src/lib.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#![feature(never_type)]
2+
#![feature(try_trait_v2)]
3+
#![feature(try_trait_v2_residual)]
4+
5+
use std::{
6+
fmt::Debug,
7+
io,
8+
process::{Child, Output, Termination as _T},
9+
};
10+
11+
use exit_safely::Termination;
12+
use try_v2::{Try, Try_ConvertResult};
13+
14+
pub mod commands;
15+
16+
#[derive(Debug, Termination, Try, Try_ConvertResult, PartialEq, PartialOrd, Eq, Ord)]
17+
#[repr(u8)]
18+
#[must_use]
19+
pub enum Exit<T: _T> {
20+
Ok(T) = 0,
21+
Error(String) = 1,
22+
InvocationError(String) = 2,
23+
IO(String) = 3,
24+
}
25+
26+
impl Exit<()> {
27+
fn message(&self) -> &str {
28+
match self {
29+
Exit::Ok(_) => "",
30+
Exit::Error(m) => m,
31+
Exit::InvocationError(m) => m,
32+
Exit::IO(m) => m,
33+
}
34+
}
35+
36+
fn replace_message(self, msg: String) -> Option<Self> {
37+
match self {
38+
Exit::Ok(_) => None,
39+
Exit::Error(_) => Some(Exit::Error(msg)),
40+
Exit::InvocationError(_) => Some(Exit::InvocationError(msg)),
41+
Exit::IO(_) => Some(Exit::IO(msg)),
42+
}
43+
}
44+
}
45+
46+
impl FromIterator<Exit<()>> for Exit<()> {
47+
fn from_iter<I: IntoIterator<Item = Exit<()>>>(iter: I) -> Self {
48+
let mut msg = String::new();
49+
iter.into_iter()
50+
.filter_map(|e| {
51+
if let Exit::Ok(_) = e {
52+
None
53+
} else {
54+
msg.push_str(e.message());
55+
msg.push('\n');
56+
Some(e)
57+
}
58+
})
59+
.min()
60+
.and_then(|e| e.replace_message(msg))
61+
.unwrap_or(Exit::Ok(()))
62+
}
63+
}
64+
65+
impl<T: _T> From<clap::Error> for Exit<T> {
66+
fn from(e: clap::Error) -> Self {
67+
Self::InvocationError(e.to_string())
68+
}
69+
}
70+
71+
#[derive(Debug)]
72+
pub struct Cmd {
73+
pub name: &'static str,
74+
pub result: Result<Output, io::Error>,
75+
}
76+
77+
trait CmdExt {
78+
fn into_cmd(self, name: &'static str) -> Cmd;
79+
}
80+
81+
impl CmdExt for Result<Output, io::Error> {
82+
fn into_cmd(self, name: &'static str) -> Cmd {
83+
Cmd { name, result: self }
84+
}
85+
}
86+
87+
impl From<Cmd> for Exit<()> {
88+
fn from(cmd: Cmd) -> Self {
89+
match cmd.result {
90+
Ok(output) => {
91+
if output.status.success() {
92+
println!("{}: OK", cmd.name);
93+
Self::Ok(())
94+
} else {
95+
let stderr = String::from_utf8_lossy(&output.stderr);
96+
Self::Error(stderr.to_string())
97+
}
98+
}
99+
Err(e) => {
100+
let msg = format!("{} failed: {}", cmd.name, e);
101+
Self::IO(msg)
102+
}
103+
}
104+
}
105+
}
106+
107+
#[derive(Debug)]
108+
pub struct Spawned {
109+
pub name: &'static str,
110+
pub child: Result<Child, io::Error>,
111+
}
112+
113+
impl Spawned {
114+
pub fn wait(self) -> Cmd {
115+
match self.child {
116+
Ok(child) => child.wait_with_output().into_cmd(self.name),
117+
Err(e) => Cmd {
118+
name: self.name,
119+
result: Err(e),
120+
},
121+
}
122+
}
123+
}
124+
125+
trait SpawnedExt {
126+
fn into_spawned(self, name: &'static str) -> Spawned;
127+
}
128+
129+
impl SpawnedExt for Result<Child, io::Error> {
130+
fn into_spawned(self, name: &'static str) -> Spawned {
131+
Spawned { name, child: self }
132+
}
133+
}
134+
135+
impl From<Vec<Spawned>> for Exit<()> {
136+
fn from(spawns: Vec<Spawned>) -> Self {
137+
spawns
138+
.into_iter()
139+
.map(|spawn| spawn.wait())
140+
.map(Exit::from)
141+
.collect()
142+
}
143+
}
144+
145+
#[cfg(test)]
146+
mod tests {
147+
use std::process::Command;
148+
149+
use super::*;
150+
151+
#[test]
152+
fn exit_from_404() {
153+
let splat: Cmd = Command::new("splat").output().into_cmd("splat");
154+
assert_eq!(splat.name, "splat");
155+
assert!(
156+
matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound))
157+
);
158+
let exit: Exit<()> = Exit::from(splat);
159+
let Exit::IO(ref msg) = exit else {
160+
panic!("not an IO2")
161+
};
162+
eprintln!("{}", msg);
163+
assert!(msg.starts_with("splat failed: "));
164+
}
165+
166+
#[test]
167+
fn collect_exit() {
168+
let exits = [
169+
Exit::Ok(()),
170+
Exit::IO("one".to_string()),
171+
Exit::Error("two".to_string()),
172+
Exit::Error("three".to_string()),
173+
];
174+
let exit: Exit<()> = exits.into_iter().collect();
175+
let expected = "one\ntwo\nthree\n";
176+
dbg!(&exit);
177+
assert!(matches!(exit, Exit::Error(s) if s == expected));
178+
}
179+
}

xtask/src/main.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::path::Path;
2+
3+
use clap::{Parser, Subcommand};
4+
use try_v2_xtasks::{
5+
Exit,
6+
commands::{clippy, clippy_tests, fmt, git_add, test},
7+
};
8+
9+
#[derive(Parser)]
10+
#[command(version)]
11+
struct XTask {
12+
#[command(subcommand)]
13+
command: Command,
14+
}
15+
16+
#[derive(Subcommand)]
17+
enum Command {
18+
/// git add if all is good
19+
Add,
20+
}
21+
22+
fn main() -> Exit<()> {
23+
let xtask = XTask::try_parse()?;
24+
25+
match &xtask.command {
26+
Command::Add => {
27+
let root = Path::new(".");
28+
let fmt = fmt(root);
29+
Exit::from(fmt)?;
30+
let clippy = clippy(root);
31+
let clippy_tests = clippy_tests(root);
32+
let tests = test(root);
33+
let checks = vec![clippy, clippy_tests, tests];
34+
Exit::from(checks)?;
35+
let git = git_add(root);
36+
Exit::from(git)
37+
}
38+
}
39+
}

xtask/tests/fixture/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[package]
2+
name = "try_v2_xtasks_fixture"
3+
version = "0.0.1"
4+
edition = "2024"
5+
publish = false

xtask/tests/fixture/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enum foo { bar }

0 commit comments

Comments
 (0)