Skip to main content

cli/
proc.rs

1//! Small subprocess-related helpers with no domain-specific logic.
2
3use anyhow::{Result, bail};
4
5/// Checks whether `name` is a real executable on `PATH` (including the
6/// `.exe` suffix on Windows), without spawning it.
7///
8/// Used before shelling out to an external CLI (`gpg`, `age`, `age-keygen`,
9/// `age-plugin-se`, `age-plugin-phone`, `base64`) so a missing dependency fails with a clear
10/// "not installed" message instead of a raw spawn error.
11pub(crate) fn ensure_command(name: &str) -> Result<()> {
12    let found = std::env::var_os("PATH")
13        .map(|paths| {
14            std::env::split_paths(&paths).any(|dir| {
15                if dir.join(name).is_file() {
16                    return true;
17                }
18                #[cfg(windows)]
19                if dir.join(format!("{name}.exe")).is_file() {
20                    return true;
21                }
22                false
23            })
24        })
25        .unwrap_or(false);
26    if !found {
27        bail!("{name} is not installed or not on PATH");
28    }
29    Ok(())
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use crate::test_support::env_lock;
36
37    #[test]
38    fn ensure_command_fails_for_nonexistent_binary() {
39        let result = ensure_command("shine-definitely-not-a-real-binary-xyz123");
40        assert!(result.is_err());
41    }
42
43    #[tokio::test]
44    async fn ensure_command_succeeds_when_binary_on_path() {
45        let dir = crate::test_support::make_temp_dir("shine-proc").await;
46        let bin_path = dir.join("myfakebin");
47        tokio::fs::write(&bin_path, b"").await.unwrap();
48
49        let result = {
50            let _guard = env_lock();
51            let old_path = std::env::var_os("PATH");
52            // SAFETY: env_lock() serialises all env-mutation tests in this
53            // crate, preventing concurrent writes to the process environment.
54            // No `.await` occurs while the guard is held.
55            unsafe { std::env::set_var("PATH", &dir) };
56
57            let result = ensure_command("myfakebin");
58
59            // SAFETY: same env_lock() guard as above.
60            unsafe {
61                match old_path {
62                    Some(value) => std::env::set_var("PATH", value),
63                    None => std::env::remove_var("PATH"),
64                }
65            }
66            result
67        };
68
69        tokio::fs::remove_dir_all(&dir).await.unwrap();
70
71        assert!(result.is_ok());
72    }
73}