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
10pub 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}