Skip to main content

greentic_dev/
passthrough.rs

1use anyhow::{Context, Result, anyhow, bail};
2use semver::Version;
3use std::env;
4use std::ffi::OsString;
5use std::path::{Path, PathBuf};
6use std::process::{Command, ExitStatus, Stdio};
7
8/// Resolve a binary by name using env override, then PATH.
9pub fn resolve_binary(name: &str) -> Result<PathBuf> {
10    let env_key = format!("GREENTIC_DEV_BIN_{}", name.replace('-', "_").to_uppercase());
11    if let Ok(path) = env::var(&env_key) {
12        let pb = PathBuf::from(path);
13        if pb.exists() {
14            return Ok(pb);
15        }
16        bail!("{env_key} points to non-existent binary: {}", pb.display());
17    }
18
19    if let Ok(path) = which::which(name) {
20        return Ok(path);
21    }
22
23    bail!(
24        "failed to find `{name}` in PATH; set {env_key}, install `{name}` with cargo binstall, or run `greentic-dev install tools` (`--latest` to force-refresh)"
25    )
26}
27
28pub fn run_passthrough(bin: &Path, args: &[OsString], verbose: bool) -> Result<ExitStatus> {
29    if verbose {
30        eprintln!("greentic-dev passthrough -> {} {:?}", bin.display(), args);
31        let _ = Command::new(bin)
32            .arg("--version")
33            .stdout(Stdio::inherit())
34            .stderr(Stdio::inherit())
35            .status();
36    }
37
38    Command::new(bin)
39        .args(args)
40        .stdin(Stdio::inherit())
41        .stdout(Stdio::inherit())
42        .stderr(Stdio::inherit())
43        .status()
44        .map_err(|e| anyhow!("failed to execute {}: {e}", bin.display()))
45}
46
47#[derive(Clone, Copy)]
48struct InstallSpec {
49    crate_name: &'static str,
50    bin_name: &'static str,
51}
52
53const DELEGATED_INSTALL_SPECS: [InstallSpec; 7] = [
54    InstallSpec {
55        crate_name: "greentic-component",
56        bin_name: "greentic-component",
57    },
58    InstallSpec {
59        crate_name: "greentic-flow",
60        bin_name: "greentic-flow",
61    },
62    InstallSpec {
63        crate_name: "greentic-pack",
64        bin_name: "greentic-pack",
65    },
66    InstallSpec {
67        crate_name: "greentic-runner",
68        bin_name: "greentic-runner",
69    },
70    InstallSpec {
71        crate_name: "greentic-runner",
72        bin_name: "greentic-runner-cli",
73    },
74    InstallSpec {
75        crate_name: "greentic-gui",
76        bin_name: "greentic-gui",
77    },
78    InstallSpec {
79        crate_name: "greentic-secrets",
80        bin_name: "greentic-secrets",
81    },
82];
83
84pub fn install_all_delegated_tools(latest: bool) -> Result<()> {
85    ensure_cargo_binstall()?;
86    for spec in DELEGATED_INSTALL_SPECS {
87        install_with_binstall(spec, latest)?;
88    }
89    Ok(())
90}
91
92fn install_with_binstall(spec: InstallSpec, force_latest: bool) -> Result<()> {
93    eprintln!(
94        "greentic-dev: installing `{}` from crate `{}` via cargo binstall...",
95        spec.bin_name, spec.crate_name
96    );
97
98    let mut cmd = Command::new("cargo");
99    cmd.arg("binstall")
100        .arg("-y")
101        .arg("--locked")
102        .arg(spec.crate_name)
103        .arg("--bin")
104        .arg(spec.bin_name);
105    if force_latest {
106        cmd.arg("--force");
107    }
108
109    let status = cmd
110        .stdin(Stdio::inherit())
111        .stdout(Stdio::inherit())
112        .stderr(Stdio::inherit())
113        .status()
114        .with_context(|| "failed to execute `cargo binstall`")?;
115
116    if status.success() {
117        Ok(())
118    } else {
119        bail!(
120            "`cargo binstall` failed while installing `{}` (crate `{}`), exit code {:?}",
121            spec.bin_name,
122            spec.crate_name,
123            status.code()
124        );
125    }
126}
127
128fn ensure_cargo_binstall() -> Result<()> {
129    let installed_version = installed_cargo_binstall_version()?;
130    if installed_version.is_none() {
131        eprintln!("greentic-dev: installing `cargo-binstall` via cargo...");
132        return install_cargo_binstall();
133    }
134
135    let installed_version = installed_version.expect("checked is_some above");
136    match latest_cargo_binstall_version() {
137        Ok(latest_version) => {
138            if installed_version >= latest_version {
139                return Ok(());
140            }
141
142            eprintln!(
143                "greentic-dev: updating `cargo-binstall` from {} to {} via cargo...",
144                installed_version, latest_version
145            );
146            install_cargo_binstall()
147        }
148        Err(err) => {
149            eprintln!(
150                "greentic-dev: failed to check latest `cargo-binstall` version ({err}); continuing with installed version {installed_version}."
151            );
152            Ok(())
153        }
154    }
155}
156
157fn install_cargo_binstall() -> Result<()> {
158    let status = Command::new("cargo")
159        .arg("install")
160        .arg("cargo-binstall")
161        .arg("--locked")
162        .stdin(Stdio::inherit())
163        .stdout(Stdio::inherit())
164        .stderr(Stdio::inherit())
165        .status()
166        .with_context(|| "failed to execute `cargo install cargo-binstall --locked`")?;
167
168    if status.success() {
169        Ok(())
170    } else {
171        bail!(
172            "failed to install cargo-binstall; `cargo install cargo-binstall --locked` exit code {:?}",
173            status.code()
174        );
175    }
176}
177
178fn installed_cargo_binstall_version() -> Result<Option<Version>> {
179    let output = Command::new("cargo")
180        .arg("binstall")
181        .arg("--version")
182        .stdin(Stdio::null())
183        .stderr(Stdio::null())
184        .output();
185    let output = match output {
186        Ok(output) => output,
187        Err(_) => return Ok(None),
188    };
189    if !output.status.success() {
190        return Ok(None);
191    }
192
193    let stdout = String::from_utf8(output.stdout)
194        .context("`cargo binstall --version` returned non-UTF8 output")?;
195    parse_installed_cargo_binstall_version(&stdout)
196}
197
198fn latest_cargo_binstall_version() -> Result<Version> {
199    let output = Command::new("cargo")
200        .arg("search")
201        .arg("cargo-binstall")
202        .arg("--limit")
203        .arg("1")
204        .stdin(Stdio::null())
205        .stderr(Stdio::null())
206        .output()
207        .with_context(|| "failed to execute `cargo search cargo-binstall --limit 1`")?;
208    if !output.status.success() {
209        bail!(
210            "`cargo search cargo-binstall --limit 1` failed with exit code {:?}",
211            output.status.code()
212        );
213    }
214
215    let stdout = String::from_utf8(output.stdout)
216        .context("`cargo search cargo-binstall --limit 1` returned non-UTF8 output")?;
217    parse_latest_cargo_binstall_version(&stdout)
218}
219
220fn parse_installed_cargo_binstall_version(stdout: &str) -> Result<Option<Version>> {
221    let line = stdout.lines().next().unwrap_or_default();
222    let maybe_version = line
223        .split_whitespace()
224        .find_map(|token| Version::parse(token.trim_start_matches('v')).ok());
225    Ok(maybe_version)
226}
227
228fn parse_latest_cargo_binstall_version(stdout: &str) -> Result<Version> {
229    let first_line = stdout
230        .lines()
231        .find(|line| !line.trim().is_empty())
232        .ok_or_else(|| anyhow!("`cargo search cargo-binstall --limit 1` returned no results"))?;
233    let (_, rhs) = first_line
234        .split_once('=')
235        .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
236    let quoted = rhs
237        .split('#')
238        .next()
239        .map(str::trim)
240        .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
241    let version_text = quoted.trim_matches('"');
242    Version::parse(version_text)
243        .with_context(|| format!("failed to parse cargo-binstall version from `{first_line}`"))
244}
245
246#[cfg(test)]
247mod tests {
248    use super::{
249        DELEGATED_INSTALL_SPECS, parse_installed_cargo_binstall_version,
250        parse_latest_cargo_binstall_version,
251    };
252
253    #[test]
254    fn delegated_install_specs_include_runner_cli() {
255        let found = DELEGATED_INSTALL_SPECS.iter().any(|spec| {
256            spec.bin_name == "greentic-runner-cli" && spec.crate_name == "greentic-runner"
257        });
258        assert!(found);
259    }
260
261    #[test]
262    fn parse_installed_binstall_version_line() {
263        let parsed = parse_installed_cargo_binstall_version("cargo-binstall 1.15.7\n")
264            .expect("parse should succeed")
265            .expect("version should exist");
266        assert_eq!(parsed.to_string(), "1.15.7");
267    }
268
269    #[test]
270    fn parse_latest_binstall_version_line() {
271        let parsed = parse_latest_cargo_binstall_version(
272            "cargo-binstall = \"1.15.7\"    # Binary installation for rust projects\n",
273        )
274        .expect("parse should succeed");
275        assert_eq!(parsed.to_string(), "1.15.7");
276    }
277}