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 ],
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}