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