Skip to main content

nex_pkg/
discover.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use anyhow::{Context, Result};
5
6/// The operating system nex is running on.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Platform {
9    Darwin,
10    Linux,
11}
12
13/// Detected Linux desktop environment (if any).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[allow(dead_code)]
16pub enum DesktopEnvironment {
17    Gnome,
18    Kde,
19    Cosmic,
20    Other,
21    None,
22}
23
24impl std::fmt::Display for Platform {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Platform::Darwin => write!(f, "macOS"),
28            Platform::Linux => write!(f, "Linux"),
29        }
30    }
31}
32
33impl std::fmt::Display for DesktopEnvironment {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            DesktopEnvironment::Gnome => write!(f, "GNOME"),
37            DesktopEnvironment::Kde => write!(f, "KDE Plasma"),
38            DesktopEnvironment::Cosmic => write!(f, "COSMIC"),
39            DesktopEnvironment::Other => write!(f, "other"),
40            DesktopEnvironment::None => write!(f, "none"),
41        }
42    }
43}
44
45/// Detect the current platform at runtime (not compile-time).
46pub fn detect_platform() -> Platform {
47    if runtime_os() == "darwin" {
48        Platform::Darwin
49    } else {
50        Platform::Linux
51    }
52}
53
54/// Detect the Linux desktop environment from standard env vars.
55#[allow(dead_code)]
56pub fn detect_desktop_environment() -> DesktopEnvironment {
57    if detect_platform() == Platform::Darwin {
58        return DesktopEnvironment::None;
59    }
60
61    // XDG_CURRENT_DESKTOP is the standard signal (set by display managers / session)
62    if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
63        let lower = desktop.to_lowercase();
64        if lower.contains("gnome") {
65            return DesktopEnvironment::Gnome;
66        }
67        if lower.contains("kde") || lower.contains("plasma") {
68            return DesktopEnvironment::Kde;
69        }
70        if lower.contains("cosmic") {
71            return DesktopEnvironment::Cosmic;
72        }
73        return DesktopEnvironment::Other;
74    }
75
76    // Fallback: DESKTOP_SESSION
77    if let Ok(session) = std::env::var("DESKTOP_SESSION") {
78        let lower = session.to_lowercase();
79        if lower.contains("gnome") {
80            return DesktopEnvironment::Gnome;
81        }
82        if lower.contains("plasma") || lower.contains("kde") {
83            return DesktopEnvironment::Kde;
84        }
85        if lower.contains("cosmic") {
86            return DesktopEnvironment::Cosmic;
87        }
88        return DesktopEnvironment::Other;
89    }
90
91    // Fallback: check for running processes
92    if Command::new("pgrep")
93        .arg("gnome-shell")
94        .output()
95        .map(|o| o.status.success())
96        .unwrap_or(false)
97    {
98        return DesktopEnvironment::Gnome;
99    }
100    if Command::new("pgrep")
101        .arg("plasmashell")
102        .output()
103        .map(|o| o.status.success())
104        .unwrap_or(false)
105    {
106        return DesktopEnvironment::Kde;
107    }
108    if Command::new("pgrep")
109        .arg("cosmic-comp")
110        .output()
111        .map(|o| o.status.success())
112        .unwrap_or(false)
113    {
114        return DesktopEnvironment::Cosmic;
115    }
116
117    DesktopEnvironment::None
118}
119
120/// Check if the system is running NixOS (has /etc/NIXOS marker).
121#[allow(dead_code)]
122pub fn is_nixos() -> bool {
123    Path::new("/etc/NIXOS").exists()
124}
125
126/// Auto-detect the nix config repo by walking up from CWD, then checking well-known paths.
127/// Finds both nix-darwin (macOS) and NixOS repos.
128pub fn find_repo() -> Result<PathBuf> {
129    // 1. Walk up from CWD looking for flake.nix with nix system configurations
130    if let Ok(cwd) = std::env::current_dir() {
131        let mut dir = cwd.as_path();
132        loop {
133            let flake = dir.join("flake.nix");
134            if flake.exists() && is_nex_flake(&flake) {
135                return Ok(dir.to_path_buf());
136            }
137            match dir.parent() {
138                Some(parent) => dir = parent,
139                None => break,
140            }
141        }
142    }
143
144    // 2. Check well-known paths
145    if let Some(home) = dirs::home_dir() {
146        let candidates = [
147            home.join("workspace/black-meridian/styrene-lab/macos-nix"),
148            home.join("macos-nix"),
149            home.join("nix-config"),
150            home.join(".config/nix-darwin"),
151            home.join(".config/nixos"),
152            // Legacy: NixOS configs were dropped in /etc/nixos by older polymerize
153            // runs and the upstream nixos-install installer. nex relocate moves
154            // these into ~/nix-config; the path stays here for back-compat.
155            PathBuf::from("/etc/nixos"),
156        ];
157        for path in &candidates {
158            let flake = path.join("flake.nix");
159            if flake.exists() && is_nex_flake(&flake) {
160                return Ok(path.clone());
161            }
162        }
163    }
164
165    anyhow::bail!("could not find nix config repo (nix-darwin or NixOS)")
166}
167
168/// Check if a flake.nix contains darwinConfigurations or nixosConfigurations.
169fn is_nex_flake(path: &Path) -> bool {
170    std::fs::read_to_string(path)
171        .map(|content| {
172            content.contains("darwinConfigurations") || content.contains("nixosConfigurations")
173        })
174        .unwrap_or(false)
175}
176
177/// Auto-detect the hostname. Uses scutil on macOS, /etc/hostname or hostname(1) on Linux.
178pub fn hostname() -> Result<String> {
179    match detect_platform() {
180        Platform::Darwin => {
181            let output = Command::new("scutil")
182                .args(["--get", "LocalHostName"])
183                .output()
184                .context("failed to run scutil --get LocalHostName")?;
185
186            if !output.status.success() {
187                anyhow::bail!("scutil --get LocalHostName failed");
188            }
189
190            let name = String::from_utf8(output.stdout)
191                .context("hostname is not valid UTF-8")?
192                .trim()
193                .to_string();
194
195            Ok(name)
196        }
197        Platform::Linux => {
198            // Try /etc/hostname first (most reliable on NixOS)
199            if let Ok(name) = std::fs::read_to_string("/etc/hostname") {
200                let trimmed = name.trim().to_string();
201                if !trimmed.is_empty() {
202                    return Ok(trimmed);
203                }
204            }
205
206            // Fallback to hostname command
207            let output = Command::new("hostname")
208                .output()
209                .context("failed to run hostname")?;
210
211            if !output.status.success() {
212                anyhow::bail!("hostname command failed");
213            }
214
215            let name = String::from_utf8(output.stdout)
216                .context("hostname is not valid UTF-8")?
217                .trim()
218                .to_string();
219
220            Ok(name)
221        }
222    }
223}
224
225/// Detect the nix system string for this machine.
226/// Uses runtime detection so cross-compiled binaries report the correct target.
227pub fn detect_system() -> &'static str {
228    let os = runtime_os();
229    let arch = runtime_arch();
230    match (arch, os) {
231        ("x86_64", "darwin") => "x86_64-darwin",
232        ("aarch64", "darwin") => "aarch64-darwin",
233        ("x86_64", "linux") => "x86_64-linux",
234        ("aarch64", "linux") => "aarch64-linux",
235        _ => {
236            // Fallback to compile-time detection
237            if cfg!(target_os = "macos") {
238                if cfg!(target_arch = "x86_64") {
239                    "x86_64-darwin"
240                } else {
241                    "aarch64-darwin"
242                }
243            } else {
244                if cfg!(target_arch = "x86_64") {
245                    "x86_64-linux"
246                } else {
247                    "aarch64-linux"
248                }
249            }
250        }
251    }
252}
253
254/// Runtime OS detection via uname.
255fn runtime_os() -> &'static str {
256    // Check /proc/version first (Linux-only, fast, no subprocess)
257    if Path::new("/proc/version").exists() {
258        return "linux";
259    }
260    // macOS has no /proc
261    if Path::new("/System/Library").exists() {
262        return "darwin";
263    }
264    // Fallback to compile-time
265    if cfg!(target_os = "macos") {
266        "darwin"
267    } else {
268        "linux"
269    }
270}
271
272/// Runtime architecture detection via uname -m.
273fn runtime_arch() -> &'static str {
274    if let Ok(output) = Command::new("uname").arg("-m").output() {
275        if output.status.success() {
276            let arch = String::from_utf8_lossy(&output.stdout);
277            let arch = arch.trim();
278            return match arch {
279                "x86_64" => "x86_64",
280                "aarch64" | "arm64" => "aarch64",
281                _ => {
282                    if cfg!(target_arch = "x86_64") {
283                        "x86_64"
284                    } else {
285                        "aarch64"
286                    }
287                }
288            };
289        }
290    }
291    if cfg!(target_arch = "x86_64") {
292        "x86_64"
293    } else {
294        "aarch64"
295    }
296}
297
298/// Return the conventional repo directory name for this platform.
299pub fn default_repo_name() -> &'static str {
300    match detect_platform() {
301        Platform::Darwin => "macos-nix",
302        Platform::Linux => "nix-config",
303    }
304}
305
306#[cfg(test)]
307#[allow(clippy::unwrap_used)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_detect_system_returns_valid_string() {
313        let sys = detect_system();
314        assert!(
315            [
316                "x86_64-darwin",
317                "aarch64-darwin",
318                "x86_64-linux",
319                "aarch64-linux"
320            ]
321            .contains(&sys),
322            "detect_system returned unexpected: {sys}"
323        );
324    }
325
326    #[test]
327    fn test_detect_platform_consistent_with_system() {
328        let platform = detect_platform();
329        let system = detect_system();
330        match platform {
331            Platform::Darwin => assert!(system.ends_with("-darwin")),
332            Platform::Linux => assert!(system.ends_with("-linux")),
333        }
334    }
335
336    #[test]
337    fn test_runtime_arch_returns_known() {
338        let arch = runtime_arch();
339        assert!(
340            ["x86_64", "aarch64"].contains(&arch),
341            "runtime_arch returned unexpected: {arch}"
342        );
343    }
344
345    #[test]
346    fn test_platform_display() {
347        assert_eq!(format!("{}", Platform::Darwin), "macOS");
348        assert_eq!(format!("{}", Platform::Linux), "Linux");
349    }
350
351    #[test]
352    fn test_default_repo_name() {
353        let name = default_repo_name();
354        assert!(name == "macos-nix" || name == "nix-config");
355    }
356}