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 locale = crate::i18n::select_locale(None);
11    let env_key = format!("GREENTIC_DEV_BIN_{}", name.replace('-', "_").to_uppercase());
12    if let Ok(path) = env::var(&env_key) {
13        let pb = PathBuf::from(path);
14        if pb.exists() {
15            return Ok(pb);
16        }
17        bail!(
18            "{}",
19            crate::i18n::tf(
20                &locale,
21                "runtime.passthrough.error.env_binary_missing",
22                &[
23                    ("env_key", env_key.clone()),
24                    ("path", pb.display().to_string()),
25                ],
26            )
27        );
28    }
29
30    if let Ok(path) = which::which(name) {
31        return Ok(path);
32    }
33
34    bail!(
35        "{}",
36        crate::i18n::tf(
37            &locale,
38            "runtime.passthrough.error.binary_not_found",
39            &[("name", name.to_string()), ("env_key", env_key)],
40        )
41    )
42}
43
44pub fn run_passthrough(bin: &Path, args: &[OsString], verbose: bool) -> Result<ExitStatus> {
45    let locale = crate::i18n::select_locale(None);
46    if verbose {
47        eprintln!(
48            "{}",
49            crate::i18n::tf(
50                &locale,
51                "runtime.passthrough.debug.exec",
52                &[
53                    ("bin", bin.display().to_string()),
54                    ("args", format!("{args:?}")),
55                ],
56            )
57        );
58        let _ = Command::new(bin)
59            .arg("--version")
60            .stdout(Stdio::inherit())
61            .stderr(Stdio::inherit())
62            .status();
63    }
64
65    Command::new(bin)
66        .args(args)
67        .stdin(Stdio::inherit())
68        .stdout(Stdio::inherit())
69        .stderr(Stdio::inherit())
70        .status()
71        .map_err(|e| {
72            anyhow!(crate::i18n::tf(
73                &locale,
74                "runtime.passthrough.error.execute",
75                &[("bin", bin.display().to_string()), ("error", e.to_string())],
76            ))
77        })
78}
79
80#[derive(Clone, Copy)]
81struct InstallSpec {
82    crate_name: &'static str,
83    bin_name: &'static str,
84}
85
86const DELEGATED_INSTALL_SPECS: [InstallSpec; 8] = [
87    InstallSpec {
88        crate_name: "greentic-component",
89        bin_name: "greentic-component",
90    },
91    InstallSpec {
92        crate_name: "greentic-flow",
93        bin_name: "greentic-flow",
94    },
95    InstallSpec {
96        crate_name: "greentic-pack",
97        bin_name: "greentic-pack",
98    },
99    InstallSpec {
100        crate_name: "greentic-runner",
101        bin_name: "greentic-runner",
102    },
103    InstallSpec {
104        crate_name: "greentic-runner",
105        bin_name: "greentic-runner-cli",
106    },
107    InstallSpec {
108        crate_name: "greentic-gui",
109        bin_name: "greentic-gui",
110    },
111    InstallSpec {
112        crate_name: "greentic-secrets",
113        bin_name: "greentic-secrets",
114    },
115    InstallSpec {
116        crate_name: "greentic-mcp",
117        bin_name: "greentic-mcp",
118    },
119];
120
121pub fn install_all_delegated_tools(latest: bool, locale: &str) -> Result<()> {
122    ensure_cargo_binstall()?;
123    for spec in DELEGATED_INSTALL_SPECS {
124        install_with_binstall(spec, latest, locale)?;
125    }
126    Ok(())
127}
128
129fn install_with_binstall(spec: InstallSpec, force_latest: bool, locale: &str) -> Result<()> {
130    eprintln!(
131        "{}",
132        crate::i18n::tf(
133            locale,
134            "runtime.tools.install.installing",
135            &[
136                ("bin_name", spec.bin_name.to_string()),
137                ("crate_name", spec.crate_name.to_string()),
138            ],
139        )
140    );
141
142    let mut cmd = Command::new("cargo");
143    cmd.arg("binstall")
144        .arg("-y")
145        .arg("--locked")
146        .arg(spec.crate_name)
147        .arg("--bin")
148        .arg(spec.bin_name);
149    if force_latest {
150        cmd.arg("--force");
151    }
152
153    let status = cmd
154        .stdin(Stdio::inherit())
155        .stdout(Stdio::inherit())
156        .stderr(Stdio::inherit())
157        .status()
158        .with_context(|| crate::i18n::t(locale, "runtime.tools.install.error.execute_binstall"))?;
159
160    if status.success() {
161        Ok(())
162    } else {
163        bail!(
164            "{}",
165            crate::i18n::tf(
166                locale,
167                "runtime.tools.install.error.binstall_failed",
168                &[
169                    ("bin_name", spec.bin_name.to_string()),
170                    ("crate_name", spec.crate_name.to_string()),
171                    ("exit_code", format!("{:?}", status.code())),
172                ],
173            )
174        );
175    }
176}
177
178fn ensure_cargo_binstall() -> Result<()> {
179    let locale = crate::i18n::select_locale(None);
180    let installed_version = installed_cargo_binstall_version()?;
181    if installed_version.is_none() {
182        eprintln!(
183            "{}",
184            crate::i18n::t(&locale, "runtime.tools.install.installing_binstall")
185        );
186        return install_cargo_binstall();
187    }
188
189    let installed_version = installed_version.expect("checked is_some above");
190    match latest_cargo_binstall_version() {
191        Ok(latest_version) => {
192            if installed_version >= latest_version {
193                return Ok(());
194            }
195
196            eprintln!(
197                "{}",
198                crate::i18n::tf(
199                    &locale,
200                    "runtime.tools.install.updating_binstall",
201                    &[
202                        ("installed_version", installed_version.to_string()),
203                        ("latest_version", latest_version.to_string()),
204                    ],
205                )
206            );
207            install_cargo_binstall()
208        }
209        Err(err) => {
210            eprintln!(
211                "{}",
212                crate::i18n::tf(
213                    &locale,
214                    "runtime.tools.install.warn.latest_check_failed",
215                    &[
216                        ("error", err.to_string()),
217                        ("installed_version", installed_version.to_string()),
218                    ],
219                )
220            );
221            Ok(())
222        }
223    }
224}
225
226fn install_cargo_binstall() -> Result<()> {
227    let status = Command::new("cargo")
228        .arg("install")
229        .arg("cargo-binstall")
230        .arg("--locked")
231        .stdin(Stdio::inherit())
232        .stdout(Stdio::inherit())
233        .stderr(Stdio::inherit())
234        .status()
235        .with_context(|| {
236            crate::i18n::t(
237                &crate::i18n::select_locale(None),
238                "runtime.tools.install.error.execute_install_binstall",
239            )
240        })?;
241
242    if status.success() {
243        Ok(())
244    } else {
245        let locale = crate::i18n::select_locale(None);
246        bail!(
247            "{}",
248            crate::i18n::tf(
249                &locale,
250                "runtime.tools.install.error.install_binstall_failed",
251                &[("exit_code", format!("{:?}", status.code()))],
252            )
253        );
254    }
255}
256
257fn installed_cargo_binstall_version() -> Result<Option<Version>> {
258    let output = Command::new("cargo")
259        .arg("binstall")
260        .arg("-V")
261        .stdin(Stdio::null())
262        .stderr(Stdio::null())
263        .output();
264    let output = match output {
265        Ok(output) => output,
266        Err(_) => return Ok(None),
267    };
268    if !output.status.success() {
269        return Ok(None);
270    }
271
272    let stdout =
273        String::from_utf8(output.stdout).context("`cargo binstall -V` returned non-UTF8 output")?;
274    parse_installed_cargo_binstall_version(&stdout)
275}
276
277fn latest_cargo_binstall_version() -> Result<Version> {
278    let output = Command::new("cargo")
279        .arg("search")
280        .arg("cargo-binstall")
281        .arg("--limit")
282        .arg("1")
283        .stdin(Stdio::null())
284        .stderr(Stdio::null())
285        .output()
286        .with_context(|| "failed to execute `cargo search cargo-binstall --limit 1`")?;
287    if !output.status.success() {
288        bail!(
289            "`cargo search cargo-binstall --limit 1` failed with exit code {:?}",
290            output.status.code()
291        );
292    }
293
294    let stdout = String::from_utf8(output.stdout)
295        .context("`cargo search cargo-binstall --limit 1` returned non-UTF8 output")?;
296    parse_latest_cargo_binstall_version(&stdout)
297}
298
299fn parse_installed_cargo_binstall_version(stdout: &str) -> Result<Option<Version>> {
300    let line = stdout.lines().next().unwrap_or_default();
301    let maybe_version = line
302        .split_whitespace()
303        .find_map(|token| Version::parse(token.trim_start_matches('v')).ok());
304    Ok(maybe_version)
305}
306
307fn parse_latest_cargo_binstall_version(stdout: &str) -> Result<Version> {
308    let first_line = stdout
309        .lines()
310        .find(|line| !line.trim().is_empty())
311        .ok_or_else(|| anyhow!("`cargo search cargo-binstall --limit 1` returned no results"))?;
312    let (_, rhs) = first_line
313        .split_once('=')
314        .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
315    let quoted = rhs
316        .split('#')
317        .next()
318        .map(str::trim)
319        .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
320    let version_text = quoted.trim_matches('"');
321    Version::parse(version_text)
322        .with_context(|| format!("failed to parse cargo-binstall version from `{first_line}`"))
323}
324
325#[cfg(test)]
326mod tests {
327    use super::{
328        DELEGATED_INSTALL_SPECS, parse_installed_cargo_binstall_version,
329        parse_latest_cargo_binstall_version,
330    };
331
332    #[test]
333    fn delegated_install_specs_include_runner_cli() {
334        let found = DELEGATED_INSTALL_SPECS.iter().any(|spec| {
335            spec.bin_name == "greentic-runner-cli" && spec.crate_name == "greentic-runner"
336        });
337        assert!(found);
338    }
339
340    #[test]
341    fn parse_installed_binstall_version_line() {
342        let parsed = parse_installed_cargo_binstall_version("cargo-binstall 1.15.7\n")
343            .expect("parse should succeed")
344            .expect("version should exist");
345        assert_eq!(parsed.to_string(), "1.15.7");
346    }
347
348    #[test]
349    fn parse_latest_binstall_version_line() {
350        let parsed = parse_latest_cargo_binstall_version(
351            "cargo-binstall = \"1.15.7\"    # Binary installation for rust projects\n",
352        )
353        .expect("parse should succeed");
354        assert_eq!(parsed.to_string(), "1.15.7");
355    }
356}