cargo-zng 0.13.2

Zng project manager.
use std::process::Command;

use super::*;

const SH_HELP: &str = r#"
Run a bash script

Script is configured using environment variables (like other tools):

ZR_SOURCE_DIR — Resources directory that is being build.
ZR_TARGET_DIR — Target directory where resources are being built to.
ZR_CACHE_DIR — Dir to use for intermediary data for the specific request.
ZR_WORKSPACE_DIR — Cargo workspace that contains source dir. This is also the working dir.
ZR_REQUEST — Request file that called the tool (.zr-sh).
ZR_REQUEST_DD — Parent dir of the request file.
ZR_TARGET — Target file implied by the request file name.
ZR_TARGET_DD — Parent dir of the target file.

ZR_FINAL — Set if the script previously printed `zng-res::on-final={args}`.

In a Cargo workspace the `zng::env::about` metadata is also set:

ZR_APP_ID — package.metadata.zng.about.app_id or "qualifier.org.app" in snake_case
ZR_APP — package.metadata.zng.about.app or package.name
ZR_ORG — package.metadata.zng.about.org or the first package.authors
ZR_VERSION — package.version
ZR_DESCRIPTION — package.description
ZR_HOMEPAGE — package.homepage
ZR_LICENSE — package.license
ZR_PKG_NAME — package.name
ZR_PKG_AUTHORS — package.authors
ZR_CRATE_NAME — package.name in snake_case
ZR_QUALIFIER — package.metadata.zng.about.qualifier or the first components `ZR_APP_ID` except the last two
ZR_META_* — any other custom string value in package.metadata.zng.about.*

Script can make requests to the resource builder by printing to stdout.
Current supported requests:

zng-res::warning={msg} — Prints the `{msg}` as a warning after the script exits.
zng-res::on-final={args} — Schedule second run with `ZR_FINAL={args}`, on final pass.

If the script fails the entire stderr is printed and the resource build fails. Scripts run with
`set -e` by default.

Tries to run on $ZR_SH, $PROGRAMFILES/Git/bin/bash.exe, bash, sh.
"#;
pub(super) fn sh() {
    help(SH_HELP);
    let script = fs::read_to_string(path(ZR_REQUEST)).unwrap_or_else(|e| fatal!("{e}"));
    sh_run(script, false, None).unwrap_or_else(|e| fatal!("{e}"));
}

fn sh_options() -> Vec<std::ffi::OsString> {
    let mut r = vec![];
    if let Ok(sh) = env::var("ZR_SH")
        && !sh.is_empty()
    {
        let sh = PathBuf::from(sh);
        if sh.exists() {
            r.push(sh.into_os_string());
        }
    }

    #[cfg(windows)]
    if let Ok(pf) = env::var("PROGRAMFILES") {
        let sh = PathBuf::from(pf).join("Git/bin/bash.exe");
        if sh.exists() {
            r.push(sh.into_os_string());
        }
    }
    #[cfg(windows)]
    if let Ok(c) = env::var("SYSTEMDRIVE") {
        let sh = PathBuf::from(c).join("Program Files (x86)/Git/bin/bash.exe");
        if sh.exists() {
            r.push(sh.into_os_string());
        }
    }

    r.push("bash".into());
    r.push("sh".into());

    r
}
pub(crate) fn sh_run(mut script: String, capture: bool, current_dir: Option<&Path>) -> io::Result<String> {
    script.insert_str(0, "set -e\n");

    for opt in sh_options() {
        let r = sh_run_try(&opt, &script, capture, current_dir)?;
        if let Some(r) = r {
            return Ok(r);
        }
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        "cannot find bash, tried $ZR_SH, $PROGRAMFILES/Git/bin/bash.exe, bash, sh",
    ))
}
fn sh_run_try(sh: &std::ffi::OsStr, script: &str, capture: bool, current_dir: Option<&Path>) -> io::Result<Option<String>> {
    let mut sh = Command::new(sh);
    if let Some(d) = current_dir {
        sh.current_dir(d);
    }
    sh.arg("-c").arg(script);
    sh.stdin(std::process::Stdio::null());
    sh.stderr(std::process::Stdio::inherit());
    let r = if capture {
        sh.output().map(|o| (o.status, String::from_utf8_lossy(&o.stdout).into_owned()))
    } else {
        sh.stdout(std::process::Stdio::inherit());
        sh.status().map(|s| (s, String::new()))
    };
    match r {
        Ok((s, o)) => {
            if !s.success() {
                return Err(match s.code() {
                    Some(c) => io::Error::other(format!("script failed, exit code {c}")),
                    None => io::Error::other("script failed"),
                });
            }
            Ok(Some(o))
        }
        Err(e) => {
            if e.kind() == io::ErrorKind::NotFound {
                Ok(None)
            } else {
                Err(e)
            }
        }
    }
}