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