//! Environment discovery: finding binaries on PATH and detecting available //! toolchains / package managers on the host system. use std::path::PathBuf; use std::process::Command; /// Rust toolchain availability on the host PATH. #[derive(Debug, Clone)] pub struct RustToolchain { pub has_rustup: bool, pub has_cargo: bool, } /// Web / scripting language toolchain availability. #[derive(Debug, Clone)] pub struct WebToolchain { pub has_npm: bool, pub has_go: bool, pub has_java: bool, } /// General-purpose platform utilities. #[derive(Debug, Clone)] pub struct PlatformUtils { pub has_curl: bool, pub has_tar: bool, } /// Pacman and Brew package managers (Arch / macOS). #[derive(Debug, Clone)] pub struct PacmanBrew { pub has_pacman: bool, pub has_brew: bool, } /// Apt and DNF package managers (Debian / Fedora). #[derive(Debug, Clone)] pub struct AptDnf { pub has_apt: bool, pub has_dnf: bool, } /// Snapshot of the host environment used to decide which install tiers are viable. /// /// Populated by `detect_env()` once per `provision_all_with_progress()` call so we /// don't re-shell out for every server. `is_linux` / `is_macos` are /// computed at startup (compile time would also work, but keeping the /// shape uniform with the rest of the struct makes the call sites tidy). #[derive(Debug, Clone)] pub struct EnvInfo { pub rust: RustToolchain, pub web: WebToolchain, pub platform: PlatformUtils, pub pacman_brew: PacmanBrew, pub apt_dnf: AptDnf, pub is_linux: bool, pub is_macos: bool, } /// Check whether `binary` exists on PATH by shelling out to `which`. /// /// Flow: `Command::new("which").arg(binary).output()` → on Unix /// `which` returns exit 0 + stdout path when found, non-zero /// otherwise. We return the first stdout line as the `PathBuf`. /// /// Returns None if `which` itself is missing, fails to spawn, or the /// binary is not on PATH. We deliberately don't cache this — it's only /// called during provisioning and the results feed into install-tier /// gating, which is already cheap. pub fn which(binary: &str) -> Option { let output = Command::new("which").arg(binary).output().ok()?; if !output.status.success() { return None; } let stdout = String::from_utf8_lossy(&output.stdout); let first = stdout.lines().next()?.trim(); if first.is_empty() { None } else { Some(PathBuf::from(first)) } } /// Snapshot the host environment: which toolchains and package managers /// are available, and what OS we're on. /// /// Flow: shell out to `which` for each tool in parallel (sequentially, /// actually — the calls are fast and the ordering doesn't matter) /// → set `EnvInfo` flags. Linux/macOS are detected via cfg at /// compile time since `which` won't tell us. /// /// Edge case: `which` may not exist on Windows; we guard with cfg so /// this only ever runs on Unix-like targets. pub fn detect_env() -> EnvInfo { EnvInfo { rust: RustToolchain { has_rustup: which("rustup").is_some(), has_cargo: which("cargo").is_some(), }, web: WebToolchain { has_npm: which("npm").is_some(), has_go: which("go").is_some(), has_java: which("java").is_some(), }, platform: PlatformUtils { has_curl: which("curl").is_some(), has_tar: which("tar").is_some(), }, pacman_brew: PacmanBrew { has_pacman: which("pacman").is_some(), has_brew: which("brew").is_some(), }, apt_dnf: AptDnf { has_apt: which("apt").is_some() || which("apt-get").is_some(), has_dnf: which("dnf").is_some(), }, is_linux: cfg!(target_os = "linux"), is_macos: cfg!(target_os = "macos"), } }