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