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; 7] = [
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];
116
117pub fn install_all_delegated_tools(latest: bool, locale: &str) -> Result<()> {
118    ensure_cargo_binstall()?;
119    for spec in DELEGATED_INSTALL_SPECS {
120        install_with_binstall(spec, latest, locale)?;
121    }
122    Ok(())
123}
124
125fn install_with_binstall(spec: InstallSpec, force_latest: bool, locale: &str) -> Result<()> {
126    eprintln!(
127        "{}",
128        crate::i18n::tf(
129            locale,
130            "runtime.tools.install.installing",
131            &[
132                ("bin_name", spec.bin_name.to_string()),
133                ("crate_name", spec.crate_name.to_string()),
134            ],
135        )
136    );
137
138    let mut cmd = Command::new("cargo");
139    cmd.arg("binstall")
140        .arg("-y")
141        .arg("--locked")
142        .arg(spec.crate_name)
143        .arg("--bin")
144        .arg(spec.bin_name);
145    if force_latest {
146        cmd.arg("--force");
147    }
148
149    let status = cmd
150        .stdin(Stdio::inherit())
151        .stdout(Stdio::inherit())
152        .stderr(Stdio::inherit())
153        .status()
154        .with_context(|| crate::i18n::t(locale, "runtime.tools.install.error.execute_binstall"))?;
155
156    if status.success() {
157        Ok(())
158    } else {
159        bail!(
160            "{}",
161            crate::i18n::tf(
162                locale,
163                "runtime.tools.install.error.binstall_failed",
164                &[
165                    ("bin_name", spec.bin_name.to_string()),
166                    ("crate_name", spec.crate_name.to_string()),
167                    ("exit_code", format!("{:?}", status.code())),
168                ],
169            )
170        );
171    }
172}
173
174fn ensure_cargo_binstall() -> Result<()> {
175    let locale = crate::i18n::select_locale(None);
176    let installed_version = installed_cargo_binstall_version()?;
177    if installed_version.is_none() {
178        eprintln!(
179            "{}",
180            crate::i18n::t(&locale, "runtime.tools.install.installing_binstall")
181        );
182        return install_cargo_binstall();
183    }
184
185    let installed_version = installed_version.expect("checked is_some above");
186    match latest_cargo_binstall_version() {
187        Ok(latest_version) => {
188            if installed_version >= latest_version {
189                return Ok(());
190            }
191
192            eprintln!(
193                "{}",
194                crate::i18n::tf(
195                    &locale,
196                    "runtime.tools.install.updating_binstall",
197                    &[
198                        ("installed_version", installed_version.to_string()),
199                        ("latest_version", latest_version.to_string()),
200                    ],
201                )
202            );
203            install_cargo_binstall()
204        }
205        Err(err) => {
206            eprintln!(
207                "{}",
208                crate::i18n::tf(
209                    &locale,
210                    "runtime.tools.install.warn.latest_check_failed",
211                    &[
212                        ("error", err.to_string()),
213                        ("installed_version", installed_version.to_string()),
214                    ],
215                )
216            );
217            Ok(())
218        }
219    }
220}
221
222fn install_cargo_binstall() -> Result<()> {
223    let status = Command::new("cargo")
224        .arg("install")
225        .arg("cargo-binstall")
226        .arg("--locked")
227        .stdin(Stdio::inherit())
228        .stdout(Stdio::inherit())
229        .stderr(Stdio::inherit())
230        .status()
231        .with_context(|| {
232            crate::i18n::t(
233                &crate::i18n::select_locale(None),
234                "runtime.tools.install.error.execute_install_binstall",
235            )
236        })?;
237
238    if status.success() {
239        Ok(())
240    } else {
241        let locale = crate::i18n::select_locale(None);
242        bail!(
243            "{}",
244            crate::i18n::tf(
245                &locale,
246                "runtime.tools.install.error.install_binstall_failed",
247                &[("exit_code", format!("{:?}", status.code()))],
248            )
249        );
250    }
251}
252
253fn installed_cargo_binstall_version() -> Result<Option<Version>> {
254    let output = Command::new("cargo")
255        .arg("binstall")
256        .arg("--version")
257        .stdin(Stdio::null())
258        .stderr(Stdio::null())
259        .output();
260    let output = match output {
261        Ok(output) => output,
262        Err(_) => return Ok(None),
263    };
264    if !output.status.success() {
265        return Ok(None);
266    }
267
268    let stdout = String::from_utf8(output.stdout)
269        .context("`cargo binstall --version` returned non-UTF8 output")?;
270    parse_installed_cargo_binstall_version(&stdout)
271}
272
273fn latest_cargo_binstall_version() -> Result<Version> {
274    let output = Command::new("cargo")
275        .arg("search")
276        .arg("cargo-binstall")
277        .arg("--limit")
278        .arg("1")
279        .stdin(Stdio::null())
280        .stderr(Stdio::null())
281        .output()
282        .with_context(|| "failed to execute `cargo search cargo-binstall --limit 1`")?;
283    if !output.status.success() {
284        bail!(
285            "`cargo search cargo-binstall --limit 1` failed with exit code {:?}",
286            output.status.code()
287        );
288    }
289
290    let stdout = String::from_utf8(output.stdout)
291        .context("`cargo search cargo-binstall --limit 1` returned non-UTF8 output")?;
292    parse_latest_cargo_binstall_version(&stdout)
293}
294
295fn parse_installed_cargo_binstall_version(stdout: &str) -> Result<Option<Version>> {
296    let line = stdout.lines().next().unwrap_or_default();
297    let maybe_version = line
298        .split_whitespace()
299        .find_map(|token| Version::parse(token.trim_start_matches('v')).ok());
300    Ok(maybe_version)
301}
302
303fn parse_latest_cargo_binstall_version(stdout: &str) -> Result<Version> {
304    let first_line = stdout
305        .lines()
306        .find(|line| !line.trim().is_empty())
307        .ok_or_else(|| anyhow!("`cargo search cargo-binstall --limit 1` returned no results"))?;
308    let (_, rhs) = first_line
309        .split_once('=')
310        .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
311    let quoted = rhs
312        .split('#')
313        .next()
314        .map(str::trim)
315        .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
316    let version_text = quoted.trim_matches('"');
317    Version::parse(version_text)
318        .with_context(|| format!("failed to parse cargo-binstall version from `{first_line}`"))
319}
320
321#[cfg(test)]
322mod tests {
323    use super::{
324        DELEGATED_INSTALL_SPECS, parse_installed_cargo_binstall_version,
325        parse_latest_cargo_binstall_version,
326    };
327
328    #[test]
329    fn delegated_install_specs_include_runner_cli() {
330        let found = DELEGATED_INSTALL_SPECS.iter().any(|spec| {
331            spec.bin_name == "greentic-runner-cli" && spec.crate_name == "greentic-runner"
332        });
333        assert!(found);
334    }
335
336    #[test]
337    fn parse_installed_binstall_version_line() {
338        let parsed = parse_installed_cargo_binstall_version("cargo-binstall 1.15.7\n")
339            .expect("parse should succeed")
340            .expect("version should exist");
341        assert_eq!(parsed.to_string(), "1.15.7");
342    }
343
344    #[test]
345    fn parse_latest_binstall_version_line() {
346        let parsed = parse_latest_cargo_binstall_version(
347            "cargo-binstall = \"1.15.7\"    # Binary installation for rust projects\n",
348        )
349        .expect("parse should succeed");
350        assert_eq!(parsed.to_string(), "1.15.7");
351    }
352}