1use std::{
2 collections::HashMap,
3 fs,
4 io::{self, BufReader, Read},
5 path::{Path, PathBuf},
6 process::{Command, Stdio},
7 sync::atomic::AtomicBool,
8};
9
10use semver::{Version, VersionReq};
11use serde::Deserialize;
12
13macro_rules! warn {
15 ($($format_args:tt)*) => {
16 if $crate::util::deny_warnings() {
17 error!($($format_args)*);
18 }else {
19 eprintln!("{} {}", $crate::util::WARN_PREFIX, format_args!($($format_args)*));
20 }
21 };
22}
23
24pub fn deny_warnings() -> bool {
25 std::env::var("RUSTFLAGS")
26 .map(|f| {
27 ["--deny=warnings", "-Dwarnings", "-D warnings", "--deny warnings"]
28 .iter()
29 .any(|d| f.contains(d))
30 })
31 .unwrap_or(false)
32}
33
34macro_rules! error {
38 ($($format_args:tt)*) => {
39 {
40 $crate::util::set_failed_run(true);
41 eprintln!("{} {}", $crate::util::ERROR_PREFIX, format_args!($($format_args)*));
42 }
43 };
44}
45
46pub static WARN_PREFIX: &str = color_print::cstr!("<bold><yellow>warning</yellow>:</bold>");
47pub static ERROR_PREFIX: &str = color_print::cstr!("<bold><red>error</red>:</bold>");
48
49macro_rules! fatal {
51 ($($format_args:tt)*) => {
52 {
53 error!($($format_args)*);
54 $crate::util::exit();
55 }
56 };
57}
58
59static RUN_FAILED: AtomicBool = AtomicBool::new(false);
60
61pub fn is_failed_run() -> bool {
63 RUN_FAILED.load(std::sync::atomic::Ordering::SeqCst)
64}
65
66pub fn set_failed_run(failed: bool) {
68 RUN_FAILED.store(failed, std::sync::atomic::Ordering::SeqCst);
69}
70
71pub fn exit() -> ! {
73 if is_failed_run() {
74 std::process::exit(102)
75 } else {
76 std::process::exit(0)
77 }
78}
79
80pub fn cmd(line: &str, args: &[&str], env: &[(&str, &str)]) -> io::Result<()> {
82 cmd_impl(line, args, env, false)
83}
84pub fn cmd_silent(line: &str, args: &[&str], env: &[(&str, &str)]) -> io::Result<()> {
86 cmd_impl(line, args, env, true)
87}
88fn cmd_impl(line: &str, args: &[&str], env: &[(&str, &str)], silent: bool) -> io::Result<()> {
89 let mut line_parts = line.split(' ');
90 let program = line_parts.next().expect("expected program to run");
91 let mut cmd = Command::new(program);
92 cmd.args(
93 line_parts
94 .map(|a| {
95 let a = a.trim();
96 if a.starts_with('"') { a.trim_matches('"') } else { a }
97 })
98 .filter(|a| !a.is_empty()),
99 );
100 cmd.args(args.iter().filter(|a| !a.is_empty()));
101 for (key, val) in env.iter() {
102 cmd.env(key, val);
103 }
104
105 if silent {
106 let output = cmd.output()?;
107 if output.status.success() {
108 Ok(())
109 } else {
110 let mut cmd = format!("cmd failed: {line}");
111 for arg in args {
112 cmd.push(' ');
113 cmd.push_str(arg);
114 }
115 cmd.push('\n');
116 cmd.push_str(&String::from_utf8_lossy(&output.stderr));
117 Err(io::Error::new(io::ErrorKind::Other, cmd))
118 }
119 } else {
120 let status = cmd.status()?;
121 if status.success() {
122 Ok(())
123 } else {
124 let mut cmd = format!("cmd failed: {line}");
125 for arg in args {
126 cmd.push(' ');
127 cmd.push_str(arg);
128 }
129 Err(io::Error::new(io::ErrorKind::Other, cmd))
130 }
131 }
132}
133
134pub fn workspace_dir() -> Option<PathBuf> {
135 let output = std::process::Command::new("cargo")
136 .arg("locate-project")
137 .arg("--workspace")
138 .arg("--message-format=plain")
139 .output()
140 .ok()?;
141
142 if output.status.success() {
143 let cargo_path = Path::new(std::str::from_utf8(&output.stdout).unwrap().trim());
144 Some(cargo_path.parent().unwrap().to_owned())
145 } else {
146 None
147 }
148}
149
150pub fn ansi_enabled() -> bool {
151 std::env::var("NO_COLOR").is_err()
152}
153
154pub fn clean_value(value: &str, required: bool) -> io::Result<String> {
155 let mut first_char = false;
156 let clean_value: String = value
157 .chars()
158 .filter(|c| {
159 if first_char {
160 first_char = c.is_ascii_alphabetic();
161 first_char
162 } else {
163 *c == ' ' || *c == '-' || *c == '_' || c.is_ascii_alphanumeric()
164 }
165 })
166 .collect();
167 let clean_value = clean_value.trim().to_owned();
168
169 if required && clean_value.is_empty() {
170 if clean_value.is_empty() {
171 return Err(io::Error::new(
172 io::ErrorKind::InvalidInput,
173 format!("cannot derive clean value from `{value}`, must contain at least one ascii alphabetic char"),
174 ));
175 }
176 if clean_value.len() > 62 {
177 return Err(io::Error::new(
178 io::ErrorKind::InvalidInput,
179 format!("cannot derive clean value from `{value}`, must contain <= 62 ascii alphanumeric chars"),
180 ));
181 }
182 }
183 Ok(clean_value)
184}
185
186pub fn manifest_path_from_package(package: &str) -> Option<String> {
187 let metadata = match Command::new("cargo")
188 .args(["metadata", "--format-version", "1", "--no-deps"])
189 .stderr(Stdio::inherit())
190 .output()
191 {
192 Ok(m) => {
193 if !m.status.success() {
194 fatal!("cargo metadata error")
195 }
196 String::from_utf8_lossy(&m.stdout).into_owned()
197 }
198 Err(e) => fatal!("cargo metadata error, {e}"),
199 };
200
201 #[derive(Deserialize)]
202 struct Metadata {
203 packages: Vec<Package>,
204 }
205 #[derive(Deserialize)]
206 struct Package {
207 name: String,
208 manifest_path: String,
209 }
210 let metadata: Metadata = serde_json::from_str(&metadata).unwrap_or_else(|e| fatal!("unexpected cargo metadata format, {e}"));
211
212 for p in metadata.packages {
213 if p.name == package {
214 return Some(p.manifest_path);
215 }
216 }
217 None
218}
219
220pub fn workspace_manifest_paths() -> Vec<PathBuf> {
222 let metadata = match Command::new("cargo")
223 .args(["metadata", "--format-version", "1", "--no-deps"])
224 .stderr(Stdio::inherit())
225 .output()
226 {
227 Ok(m) => {
228 if !m.status.success() {
229 fatal!("cargo metadata error")
230 }
231 String::from_utf8_lossy(&m.stdout).into_owned()
232 }
233 Err(e) => fatal!("cargo metadata error, {e}"),
234 };
235
236 #[derive(Deserialize)]
237 struct Metadata {
238 packages: Vec<Package>,
239 }
240 #[derive(Debug, Deserialize)]
241 struct Package {
242 manifest_path: PathBuf,
243 }
244
245 let metadata: Metadata = serde_json::from_str(&metadata).unwrap_or_else(|e| fatal!("unexpected cargo metadata format, {e}"));
246
247 metadata.packages.into_iter().map(|p| p.manifest_path).collect()
248}
249
250pub fn dependencies(manifest_path: &str) -> (PathBuf, Vec<DependencyManifest>) {
252 let metadata = match Command::new("cargo")
253 .args(["metadata", "--format-version", "1", "--manifest-path"])
254 .arg(manifest_path)
255 .stderr(Stdio::inherit())
256 .output()
257 {
258 Ok(m) => {
259 if !m.status.success() {
260 fatal!("cargo metadata error")
261 }
262 String::from_utf8_lossy(&m.stdout).into_owned()
263 }
264 Err(e) => fatal!("cargo metadata error, {e}"),
265 };
266
267 #[derive(Deserialize)]
268 struct Metadata {
269 packages: Vec<Package>,
270 workspace_root: PathBuf,
271 }
272 #[derive(Debug, Deserialize)]
273 struct Package {
274 name: String,
275 version: Version,
276 dependencies: Vec<Dependency>,
277 manifest_path: String,
278 }
279 #[derive(Debug, Deserialize)]
280 struct Dependency {
281 name: String,
282 kind: Option<String>,
283 req: VersionReq,
284 }
285
286 let metadata: Metadata = serde_json::from_str(&metadata).unwrap_or_else(|e| fatal!("unexpected cargo metadata format, {e}"));
287
288 let manifest_path = dunce::canonicalize(manifest_path).unwrap();
289
290 let mut dependencies: &[Dependency] = &[];
291
292 for pkg in &metadata.packages {
293 let pkg_path = Path::new(&pkg.manifest_path);
294 if pkg_path == manifest_path {
295 dependencies = &pkg.dependencies;
296 break;
297 }
298 }
299 if !dependencies.is_empty() {
300 let mut map = HashMap::new();
301 for pkg in &metadata.packages {
302 map.entry(pkg.name.as_str()).or_insert_with(Vec::new).push((&pkg.version, pkg));
303 }
304
305 let mut r = vec![];
306 fn collect(map: &mut HashMap<&str, Vec<(&Version, &Package)>>, dependencies: &[Dependency], r: &mut Vec<DependencyManifest>) {
307 for dep in dependencies {
308 if dep.kind.is_some() {
309 continue;
311 }
312 if let Some(versions) = map.remove(dep.name.as_str()) {
313 for (version, pkg) in versions.iter() {
314 if dep.req.comparators.is_empty() || dep.req.matches(version) {
315 r.push(DependencyManifest {
316 name: pkg.name.clone(),
317 version: pkg.version.clone(),
318 manifest_path: pkg.manifest_path.as_str().into(),
319 });
320
321 collect(map, &pkg.dependencies, r)
323 }
324 }
325 }
326 }
327 }
328 collect(&mut map, dependencies, &mut r);
329 return (metadata.workspace_root, r);
330 }
331
332 (metadata.workspace_root, vec![])
333}
334
335pub struct DependencyManifest {
336 pub name: String,
337 pub version: Version,
338 pub manifest_path: PathBuf,
339}
340
341pub fn check_or_create_dir(check: bool, path: impl AsRef<Path>) -> io::Result<()> {
342 if check {
343 let path = path.as_ref();
344 if !path.is_dir() {
345 fatal!("expected `{}` dir", path.display());
346 }
347 Ok(())
348 } else {
349 fs::create_dir(path)
350 }
351}
352
353pub fn check_or_create_dir_all(check: bool, path: impl AsRef<Path>) -> io::Result<()> {
354 if check {
355 let path = path.as_ref();
356 if !path.is_dir() {
357 fatal!("expected `{}` dir", path.display());
358 }
359 Ok(())
360 } else {
361 fs::create_dir_all(path)
362 }
363}
364
365pub fn check_or_write(check: bool, path: impl AsRef<Path>, contents: impl AsRef<[u8]>, verbose: bool) -> io::Result<()> {
366 let path = path.as_ref();
367 let contents = contents.as_ref();
368 if check {
369 if !path.is_file() {
370 fatal!("expected `{}` file", path.display());
371 }
372 let file = fs::File::open(path).unwrap_or_else(|e| fatal!("cannot read `{}`, {e}", path.display()));
373 let mut bytes = vec![];
374 BufReader::new(file)
375 .read_to_end(&mut bytes)
376 .unwrap_or_else(|e| fatal!("cannot read `{}`, {e}", path.display()));
377
378 if bytes != contents {
379 fatal!("file `{}` contents changed", path.display());
380 } else if verbose {
381 println!("file `{}` contents did not change", path.display());
382 }
383
384 Ok(())
385 } else {
386 if verbose {
387 println!("writing `{}`", path.display());
388 }
389 fs::write(path, contents)
390 }
391}
392
393pub fn check_or_copy(check: bool, from: impl AsRef<Path>, to: impl AsRef<Path>, verbose: bool) -> io::Result<u64> {
394 let from = from.as_ref();
395 let to = to.as_ref();
396 if check {
397 if !to.is_file() {
398 fatal!("expected `{}` file", to.display());
399 }
400
401 let mut bytes = vec![];
402 for path in [from, to] {
403 let file = fs::File::open(path).unwrap_or_else(|e| fatal!("cannot read `{}`, {e}", path.display()));
404 let mut b = vec![];
405 BufReader::new(file)
406 .read_to_end(&mut b)
407 .unwrap_or_else(|e| fatal!("cannot read `{}`, {e}", path.display()));
408
409 bytes.push(b);
410 }
411
412 if bytes[0] != bytes[1] {
413 fatal!("file `{}` contents changed", to.display());
414 } else if verbose {
415 println!("file `{}` contents did not change", to.display());
416 }
417
418 Ok(bytes[1].len() as u64)
419 } else {
420 if verbose {
421 println!("copying\n from: `{}`\n to: `{}`", from.display(), to.display());
422 }
423 fs::copy(from, to)
424 }
425}