logix_xtask/
lib.rs

1#![deny(warnings, clippy::all)]
2
3use std::{
4    collections::VecDeque,
5    path::{Path, PathBuf},
6    process::Command,
7};
8
9struct Vars {
10    cwd: PathBuf,
11    target_dir: PathBuf,
12    verbose: bool,
13}
14
15impl Vars {
16    fn verbose_arg(&self) -> &'static [&'static str] {
17        if self.verbose {
18            &["--verbose"]
19        } else {
20            &[]
21        }
22    }
23}
24
25enum Action<'a> {
26    Cargo(&'a str, &'a [&'a str]),
27    Call(&'static (dyn Fn(&Vars) + Sync)),
28    Run(&'a str),
29}
30
31use Action::*;
32
33static ACTIONS: &[(&str, &[Action])] = &[
34    (
35        "before-pr",
36        &[
37            Cargo("update", &[]),
38            Run("lints"),
39            Run("build-all"),
40            Run("all-tests"),
41            Run("all-checks"),
42        ],
43    ),
44    (
45        "all-checks",
46        &[
47            Cargo("deny", &["check"]),
48            Cargo("semver-checks", &[]),
49            Cargo("outdated", &["--exit-code", "1"]),
50            // TODO(2023.10): NightlyCargo("udeps", &[]),
51            // TODO(2023.10): Cargo("audit", &[]),
52            // TODO(2023.10): Cargo("pants", &[]),
53        ],
54    ),
55    (
56        "lints",
57        &[
58            Cargo("fmt", &["--check"]),
59            Cargo("clippy", &["--workspace"]),
60        ],
61    ),
62    (
63        "build-all",
64        &[
65            Cargo("build", &["--workspace"]),
66            Cargo("build", &["--workspace", "--tests"]),
67            Cargo("build", &["--workspace", "--release"]),
68        ],
69    ),
70    ("all-tests", &[Cargo("test", &["--workspace"])]),
71    ("lcov-coverage", &[Call(&run_lcov_coverage)]),
72    ("html-coverage", &[Call(&run_html_coverage)]),
73];
74
75fn grcov(target_dir: &Path, format: &str, build_type: &str) {
76    let ret = Command::new("grcov")
77        .args(["."])
78        .args([
79            "--binary-path",
80            target_dir
81                .join(format!("{build_type}/deps"))
82                .to_str()
83                .unwrap(),
84        ])
85        .args(["-s", "."])
86        .args(["-t", format])
87        .args(["--branch"])
88        .args(["--ignore-not-existing"])
89        .args(["-o", target_dir.join(format).to_str().unwrap()])
90        .args(["--keep-only", "src/*"])
91        .args(["--keep-only", "derive/src/*"])
92        .status()
93        .expect("Perhaps you need to run 'cargo install grcov'")
94        .success();
95    assert!(ret);
96}
97
98fn run_lcov_coverage(vars: &Vars) {
99    code_coverage(vars, "lcov")
100}
101
102fn run_html_coverage(vars: &Vars) {
103    code_coverage(vars, "html")
104}
105
106fn code_coverage(vars: &Vars, format: &str) {
107    let build_type = "debug";
108    let target_dir = vars.target_dir.join(format!("coverage-{format}"));
109
110    if target_dir.is_dir() {
111        std::fs::remove_dir_all(&target_dir)
112            .unwrap_or_else(|e| panic!("Failed to delete {target_dir:?}: {e}"));
113    }
114
115    let ret = Command::new("cargo")
116        .env("CARGO_TARGET_DIR", &target_dir)
117        .env("CARGO_INCREMENTAL", "0")
118        .env("RUSTFLAGS", "-Cinstrument-coverage")
119        .env(
120            "LLVM_PROFILE_FILE",
121            target_dir.join("cargo-test-%p-%m.profraw"),
122        )
123        .arg("test")
124        .arg("--workspace")
125        .args(match build_type {
126            "release" => vec!["--release"],
127            "debug" => vec![],
128            _ => unreachable!("{build_type:?}"),
129        })
130        .status()
131        .unwrap_or_else(|e| panic!("Failed to run cargo: {e}"))
132        .success();
133    assert!(ret);
134
135    match format {
136        "html" => {
137            grcov(&target_dir, "html", build_type);
138            grcov(&target_dir, "lcov", build_type);
139
140            let ret = Command::new("genhtml")
141                .args(["-o", target_dir.join("html2").to_str().unwrap()])
142                .args(["--show-details"])
143                .args(["--highlight"])
144                .args(["--ignore-errors", "source"])
145                .args(["--legend", target_dir.join("lcov").to_str().unwrap()])
146                .status()
147                .unwrap_or_else(|e| panic!("Failed to run genhtml: {e}"))
148                .success();
149            assert!(ret);
150
151            println!("Now open:");
152            println!(
153                "  file://{}/html/index.html",
154                vars.cwd.join(&target_dir).display()
155            );
156            println!(
157                "  file://{}/html2/index.html",
158                vars.cwd.join(&target_dir).display()
159            );
160        }
161        "lcov" => {
162            grcov(&target_dir, "lcov", build_type);
163        }
164        _ => panic!("Unknown format {format:?}"),
165    }
166}
167
168fn cargo_cmd(command: &str, args: &[&str], vars: &Vars) {
169    print!("Running cargo {command}");
170    for arg in args.iter() {
171        print!(" {arg}");
172    }
173    println!();
174
175    let ret = Command::new("cargo")
176        .args(vars.verbose_arg())
177        .arg(command)
178        .args(args)
179        .status()
180        .unwrap_or_else(|e| panic!("Failed to run cargo: {e}"))
181        .success();
182    assert!(ret);
183}
184
185pub fn run_xtask() {
186    let mut vars = Vars {
187        cwd: std::env::current_dir().unwrap().canonicalize().unwrap(),
188        target_dir: std::env::var_os("CARGO_TARGET_DIR")
189            .map(PathBuf::from)
190            .unwrap_or_else(|| "target".into())
191            .canonicalize()
192            .unwrap(),
193        verbose: false,
194    };
195
196    let mut tasks = VecDeque::new();
197
198    for arg in std::env::args().skip(1) {
199        if arg == "--verbose" || arg == "-v" {
200            vars.verbose = true;
201        } else if let Some((_, actions)) = ACTIONS.iter().find(|&&(t, _)| arg == t) {
202            tasks.extend(actions.iter());
203        } else {
204            eprintln!("Invalid argument {arg:?}");
205            std::process::exit(1);
206        }
207    }
208
209    if tasks.is_empty() {
210        eprint!("Missing action, use one of ");
211        for (i, &(t, _)) in ACTIONS.iter().enumerate() {
212            if i != 0 {
213                eprint!(", {t}");
214            } else {
215                eprint!("{t}");
216            }
217        }
218        eprintln!();
219        std::process::exit(1);
220    }
221
222    while let Some(action) = tasks.pop_front() {
223        match *action {
224            Action::Cargo(cmd, args) => cargo_cmd(cmd, args, &vars),
225            Action::Call(clb) => clb(&vars),
226            Action::Run(name) => {
227                tasks.extend(
228                    ACTIONS
229                        .iter()
230                        .find(|&&(t, _)| name == t)
231                        .unwrap_or_else(|| panic!("Unknown action {name}"))
232                        .1
233                        .iter(),
234                );
235            }
236        }
237    }
238}