Skip to main content

cargo_zng/res/built_in/
sh.rs

1use std::process::Command;
2
3use super::*;
4
5const SH_HELP: &str = r#"
6Run a bash script
7
8Script is configured using environment variables (like other tools):
9
10ZR_SOURCE_DIR — Resources directory that is being build.
11ZR_TARGET_DIR — Target directory where resources are being built to.
12ZR_CACHE_DIR — Dir to use for intermediary data for the specific request.
13ZR_WORKSPACE_DIR — Cargo workspace that contains source dir. Also the working dir.
14ZR_REQUEST — Request file that called the tool (.zr-sh).
15ZR_REQUEST_DD — Parent dir of the request file.
16ZR_TARGET — Target file implied by the request file name.
17ZR_TARGET_DD — Parent dir of the target file.
18
19ZR_FINAL — Set if the script previously printed `zng-res::on-final={args}`.
20
21In a Cargo workspace the `zng::env::about` metadata is also set:
22
23ZR_APP_ID — package.metadata.zng.about.app_id or "qualifier.org.app" in snake_case
24ZR_APP — package.metadata.zng.about.app or package.name
25ZR_ORG — package.metadata.zng.about.org or the first package.authors
26ZR_VERSION — package.version
27ZR_DESCRIPTION — package.description
28ZR_HOMEPAGE — package.homepage
29ZR_LICENSE — package.license
30ZR_PKG_NAME — package.name
31ZR_PKG_AUTHORS — package.authors
32ZR_CRATE_NAME — package.name in snake_case
33ZR_QUALIFIER — package.metadata.zng.about.qualifier or the first components `ZR_APP_ID` except the last two
34ZR_META_* — any other custom string value in package.metadata.zng.about.*
35
36Script can make requests to the resource builder by printing to stdout.
37Current supported requests:
38
39zng-res::warning={msg} — Prints the `{msg}` as a warning after the script exits.
40zng-res::on-final={args} — Schedule second run with `ZR_FINAL={args}`, on final pass.
41
42If the script fails the entire stderr is printed and the resource build fails. Scripts run with
43`set -e` by default.
44
45Tries to run on $ZR_SH, $PROGRAMFILES/Git/bin/bash.exe, bash, sh.
46"#;
47pub(super) fn sh() {
48    help(SH_HELP);
49    let script = fs::read_to_string(path(ZR_REQUEST)).unwrap_or_else(|e| fatal!("{e}"));
50    sh_run(script, false, None).unwrap_or_else(|e| fatal!("{e}"));
51}
52
53fn sh_options() -> Vec<std::ffi::OsString> {
54    let mut r = vec![];
55    if let Ok(sh) = env::var("ZR_SH")
56        && !sh.is_empty()
57    {
58        let sh = PathBuf::from(sh);
59        if sh.exists() {
60            r.push(sh.into_os_string());
61        }
62    }
63
64    #[cfg(windows)]
65    if let Ok(pf) = env::var("PROGRAMFILES") {
66        let sh = PathBuf::from(pf).join("Git/bin/bash.exe");
67        if sh.exists() {
68            r.push(sh.into_os_string());
69        }
70    }
71    #[cfg(windows)]
72    if let Ok(c) = env::var("SYSTEMDRIVE") {
73        let sh = PathBuf::from(c).join("Program Files (x86)/Git/bin/bash.exe");
74        if sh.exists() {
75            r.push(sh.into_os_string());
76        }
77    }
78
79    r.push("bash".into());
80    r.push("sh".into());
81
82    r
83}
84pub(crate) fn sh_run(mut script: String, capture: bool, current_dir: Option<&Path>) -> io::Result<String> {
85    script.insert_str(0, "set -e\n");
86
87    for opt in sh_options() {
88        let r = sh_run_try(&opt, &script, capture, current_dir)?;
89        if let Some(r) = r {
90            return Ok(r);
91        }
92    }
93    Err(io::Error::new(
94        io::ErrorKind::NotFound,
95        "cannot find bash, tried $ZR_SH, $PROGRAMFILES/Git/bin/bash.exe, bash, sh",
96    ))
97}
98fn sh_run_try(sh: &std::ffi::OsStr, script: &str, capture: bool, current_dir: Option<&Path>) -> io::Result<Option<String>> {
99    let mut sh = Command::new(sh);
100    if let Some(d) = current_dir {
101        sh.current_dir(d);
102    }
103    sh.arg("-c").arg(script);
104    sh.stdin(std::process::Stdio::null());
105    sh.stderr(std::process::Stdio::inherit());
106    let r = if capture {
107        sh.output().map(|o| (o.status, String::from_utf8_lossy(&o.stdout).into_owned()))
108    } else {
109        sh.stdout(std::process::Stdio::inherit());
110        sh.status().map(|s| (s, String::new()))
111    };
112    match r {
113        Ok((s, o)) => {
114            if !s.success() {
115                return Err(match s.code() {
116                    Some(c) => io::Error::other(format!("script failed, exit code {c}")),
117                    None => io::Error::other("script failed"),
118                });
119            }
120            Ok(Some(o))
121        }
122        Err(e) => {
123            if e.kind() == io::ErrorKind::NotFound {
124                Ok(None)
125            } else {
126                Err(e)
127            }
128        }
129    }
130}