cargo_zng/
util.rs

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