greentic_dev/
passthrough.rs1use 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 env_key = format!("GREENTIC_DEV_BIN_{}", name.replace('-', "_").to_uppercase());
11 if let Ok(path) = env::var(&env_key) {
12 let pb = PathBuf::from(path);
13 if pb.exists() {
14 return Ok(pb);
15 }
16 bail!("{env_key} points to non-existent binary: {}", pb.display());
17 }
18
19 if let Ok(path) = which::which(name) {
20 return Ok(path);
21 }
22
23 bail!(
24 "failed to find `{name}` in PATH; set {env_key}, install `{name}` with cargo binstall, or run `greentic-dev install tools` (`--latest` to force-refresh)"
25 )
26}
27
28pub fn run_passthrough(bin: &Path, args: &[OsString], verbose: bool) -> Result<ExitStatus> {
29 if verbose {
30 eprintln!("greentic-dev passthrough -> {} {:?}", bin.display(), args);
31 let _ = Command::new(bin)
32 .arg("--version")
33 .stdout(Stdio::inherit())
34 .stderr(Stdio::inherit())
35 .status();
36 }
37
38 Command::new(bin)
39 .args(args)
40 .stdin(Stdio::inherit())
41 .stdout(Stdio::inherit())
42 .stderr(Stdio::inherit())
43 .status()
44 .map_err(|e| anyhow!("failed to execute {}: {e}", bin.display()))
45}
46
47#[derive(Clone, Copy)]
48struct InstallSpec {
49 crate_name: &'static str,
50 bin_name: &'static str,
51}
52
53const DELEGATED_INSTALL_SPECS: [InstallSpec; 7] = [
54 InstallSpec {
55 crate_name: "greentic-component",
56 bin_name: "greentic-component",
57 },
58 InstallSpec {
59 crate_name: "greentic-flow",
60 bin_name: "greentic-flow",
61 },
62 InstallSpec {
63 crate_name: "greentic-pack",
64 bin_name: "greentic-pack",
65 },
66 InstallSpec {
67 crate_name: "greentic-runner",
68 bin_name: "greentic-runner",
69 },
70 InstallSpec {
71 crate_name: "greentic-runner",
72 bin_name: "greentic-runner-cli",
73 },
74 InstallSpec {
75 crate_name: "greentic-gui",
76 bin_name: "greentic-gui",
77 },
78 InstallSpec {
79 crate_name: "greentic-secrets",
80 bin_name: "greentic-secrets",
81 },
82];
83
84pub fn install_all_delegated_tools(latest: bool) -> Result<()> {
85 ensure_cargo_binstall()?;
86 for spec in DELEGATED_INSTALL_SPECS {
87 install_with_binstall(spec, latest)?;
88 }
89 Ok(())
90}
91
92fn install_with_binstall(spec: InstallSpec, force_latest: bool) -> Result<()> {
93 eprintln!(
94 "greentic-dev: installing `{}` from crate `{}` via cargo binstall...",
95 spec.bin_name, spec.crate_name
96 );
97
98 let mut cmd = Command::new("cargo");
99 cmd.arg("binstall")
100 .arg("-y")
101 .arg("--locked")
102 .arg(spec.crate_name)
103 .arg("--bin")
104 .arg(spec.bin_name);
105 if force_latest {
106 cmd.arg("--force");
107 }
108
109 let status = cmd
110 .stdin(Stdio::inherit())
111 .stdout(Stdio::inherit())
112 .stderr(Stdio::inherit())
113 .status()
114 .with_context(|| "failed to execute `cargo binstall`")?;
115
116 if status.success() {
117 Ok(())
118 } else {
119 bail!(
120 "`cargo binstall` failed while installing `{}` (crate `{}`), exit code {:?}",
121 spec.bin_name,
122 spec.crate_name,
123 status.code()
124 );
125 }
126}
127
128fn ensure_cargo_binstall() -> Result<()> {
129 let installed_version = installed_cargo_binstall_version()?;
130 if installed_version.is_none() {
131 eprintln!("greentic-dev: installing `cargo-binstall` via cargo...");
132 return install_cargo_binstall();
133 }
134
135 let installed_version = installed_version.expect("checked is_some above");
136 match latest_cargo_binstall_version() {
137 Ok(latest_version) => {
138 if installed_version >= latest_version {
139 return Ok(());
140 }
141
142 eprintln!(
143 "greentic-dev: updating `cargo-binstall` from {} to {} via cargo...",
144 installed_version, latest_version
145 );
146 install_cargo_binstall()
147 }
148 Err(err) => {
149 eprintln!(
150 "greentic-dev: failed to check latest `cargo-binstall` version ({err}); continuing with installed version {installed_version}."
151 );
152 Ok(())
153 }
154 }
155}
156
157fn install_cargo_binstall() -> Result<()> {
158 let status = Command::new("cargo")
159 .arg("install")
160 .arg("cargo-binstall")
161 .arg("--locked")
162 .stdin(Stdio::inherit())
163 .stdout(Stdio::inherit())
164 .stderr(Stdio::inherit())
165 .status()
166 .with_context(|| "failed to execute `cargo install cargo-binstall --locked`")?;
167
168 if status.success() {
169 Ok(())
170 } else {
171 bail!(
172 "failed to install cargo-binstall; `cargo install cargo-binstall --locked` exit code {:?}",
173 status.code()
174 );
175 }
176}
177
178fn installed_cargo_binstall_version() -> Result<Option<Version>> {
179 let output = Command::new("cargo")
180 .arg("binstall")
181 .arg("--version")
182 .stdin(Stdio::null())
183 .stderr(Stdio::null())
184 .output();
185 let output = match output {
186 Ok(output) => output,
187 Err(_) => return Ok(None),
188 };
189 if !output.status.success() {
190 return Ok(None);
191 }
192
193 let stdout = String::from_utf8(output.stdout)
194 .context("`cargo binstall --version` returned non-UTF8 output")?;
195 parse_installed_cargo_binstall_version(&stdout)
196}
197
198fn latest_cargo_binstall_version() -> Result<Version> {
199 let output = Command::new("cargo")
200 .arg("search")
201 .arg("cargo-binstall")
202 .arg("--limit")
203 .arg("1")
204 .stdin(Stdio::null())
205 .stderr(Stdio::null())
206 .output()
207 .with_context(|| "failed to execute `cargo search cargo-binstall --limit 1`")?;
208 if !output.status.success() {
209 bail!(
210 "`cargo search cargo-binstall --limit 1` failed with exit code {:?}",
211 output.status.code()
212 );
213 }
214
215 let stdout = String::from_utf8(output.stdout)
216 .context("`cargo search cargo-binstall --limit 1` returned non-UTF8 output")?;
217 parse_latest_cargo_binstall_version(&stdout)
218}
219
220fn parse_installed_cargo_binstall_version(stdout: &str) -> Result<Option<Version>> {
221 let line = stdout.lines().next().unwrap_or_default();
222 let maybe_version = line
223 .split_whitespace()
224 .find_map(|token| Version::parse(token.trim_start_matches('v')).ok());
225 Ok(maybe_version)
226}
227
228fn parse_latest_cargo_binstall_version(stdout: &str) -> Result<Version> {
229 let first_line = stdout
230 .lines()
231 .find(|line| !line.trim().is_empty())
232 .ok_or_else(|| anyhow!("`cargo search cargo-binstall --limit 1` returned no results"))?;
233 let (_, rhs) = first_line
234 .split_once('=')
235 .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
236 let quoted = rhs
237 .split('#')
238 .next()
239 .map(str::trim)
240 .ok_or_else(|| anyhow!("unexpected cargo search output: {first_line}"))?;
241 let version_text = quoted.trim_matches('"');
242 Version::parse(version_text)
243 .with_context(|| format!("failed to parse cargo-binstall version from `{first_line}`"))
244}
245
246#[cfg(test)]
247mod tests {
248 use super::{
249 DELEGATED_INSTALL_SPECS, parse_installed_cargo_binstall_version,
250 parse_latest_cargo_binstall_version,
251 };
252
253 #[test]
254 fn delegated_install_specs_include_runner_cli() {
255 let found = DELEGATED_INSTALL_SPECS.iter().any(|spec| {
256 spec.bin_name == "greentic-runner-cli" && spec.crate_name == "greentic-runner"
257 });
258 assert!(found);
259 }
260
261 #[test]
262 fn parse_installed_binstall_version_line() {
263 let parsed = parse_installed_cargo_binstall_version("cargo-binstall 1.15.7\n")
264 .expect("parse should succeed")
265 .expect("version should exist");
266 assert_eq!(parsed.to_string(), "1.15.7");
267 }
268
269 #[test]
270 fn parse_latest_binstall_version_line() {
271 let parsed = parse_latest_cargo_binstall_version(
272 "cargo-binstall = \"1.15.7\" # Binary installation for rust projects\n",
273 )
274 .expect("parse should succeed");
275 assert_eq!(parsed.to_string(), "1.15.7");
276 }
277}