Skip to main content

cargo_zng/
util.rs

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
14/// Print warning message.
15macro_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
35/// Print error message and flags the current process as failed.
36///
37/// Note that this does not exit the process, use `fatal!` to exit.
38macro_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
50/// Print error message and exit the current process with error code.
51macro_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
62/// Gets if the current process will exit with error code.
63pub fn is_failed_run() -> bool {
64    RUN_FAILED.load(std::sync::atomic::Ordering::SeqCst)
65}
66
67/// Sets if the current process will exit with error code.
68pub fn set_failed_run(failed: bool) {
69    RUN_FAILED.store(failed, std::sync::atomic::Ordering::SeqCst);
70}
71
72/// Exit the current process, with error code `102` if [`is_failed_run`].
73pub fn exit() -> ! {
74    if is_failed_run() {
75        std::process::exit(102)
76    } else {
77        std::process::exit(0)
78    }
79}
80
81/// Run the command with args, inherits stdout and stderr.
82pub fn cmd(line: &str, args: &[&str], env: &[(&str, &str)]) -> io::Result<()> {
83    cmd_impl(line, args, env, false)
84}
85/// Run the command with args.
86pub 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
221/// Workspace crates Cargo.toml paths.
222pub 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
251/// Workspace root and dependencies of manifest_path
252pub 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                    // skip build/dev-dependencies
311                    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 dependencies of dependencies
323                            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
416/// Convert '\' paths to '/' on Windows.
417pub 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}