use std::path::{Path, PathBuf};
use std::sync::LazyLock;
pub static SYSTEM_PATHS: LazyLock<SystemPaths> = LazyLock::new(SystemPaths::detect);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemType {
NixOS,
Guix,
Fhs,
}
impl SystemType {
pub fn detect() -> Self {
if Path::new("/etc/NIXOS").exists() {
return SystemType::NixOS;
}
if Path::new("/nix/store").exists() {
if let Ok(target) = std::fs::read_link("/bin/sh") {
if target.to_string_lossy().contains("/nix/store") {
return SystemType::NixOS;
}
}
if !Path::new("/bin/sh").exists() {
return SystemType::NixOS;
}
}
if Path::new("/gnu/store").exists() {
if let Ok(target) = std::fs::read_link("/bin/sh") {
if target.to_string_lossy().contains("/gnu/store") {
return SystemType::Guix;
}
}
if !Path::new("/bin/sh").exists() {
return SystemType::Guix;
}
}
SystemType::Fhs
}
pub fn is_nix_like(self) -> bool {
matches!(self, SystemType::NixOS | SystemType::Guix)
}
}
#[derive(Debug, Clone)]
pub struct SystemPaths {
pub system_type: SystemType,
pub readonly_mounts: Vec<PathBuf>,
pub default_path: String,
}
impl SystemPaths {
pub fn detect() -> Self {
let system_type = SystemType::detect();
match system_type {
SystemType::NixOS => Self::nixos_paths(),
SystemType::Guix => Self::guix_paths(),
SystemType::Fhs => Self::fhs_paths(),
}
}
fn nixos_paths() -> Self {
let mut readonly_mounts = Vec::new();
if Path::new("/nix/store").exists() {
readonly_mounts.push(PathBuf::from("/nix/store"));
}
if Path::new("/run/current-system/sw").exists() {
readonly_mounts.push(PathBuf::from("/run/current-system/sw"));
}
let path_dirs = [
"/run/current-system/sw/bin",
"/nix/var/nix/profiles/default/bin",
];
let default_path = path_dirs
.iter()
.filter(|p| Path::new(p).exists())
.copied()
.collect::<Vec<_>>()
.join(":");
Self {
system_type: SystemType::NixOS,
readonly_mounts,
default_path: if default_path.is_empty() {
"/bin".to_string()
} else {
default_path
},
}
}
fn guix_paths() -> Self {
let mut readonly_mounts = Vec::new();
if Path::new("/gnu/store").exists() {
readonly_mounts.push(PathBuf::from("/gnu/store"));
}
Self {
system_type: SystemType::Guix,
readonly_mounts,
default_path: "/run/current-system/profile/bin".to_string(),
}
}
fn fhs_paths() -> Self {
const FHS_DIRS: &[&str] = &["/usr", "/bin", "/lib", "/lib64", "/sbin"];
let readonly_mounts = FHS_DIRS
.iter()
.map(Path::new)
.filter(|p| p.exists())
.map(Path::to_path_buf)
.collect();
Self {
system_type: SystemType::Fhs,
readonly_mounts,
default_path: "/usr/local/bin:/usr/bin:/bin".to_string(),
}
}
pub fn landlock_readonly_paths(&self) -> Vec<&Path> {
self.readonly_mounts.iter().map(|p| p.as_path()).collect()
}
}
pub fn is_nix_store_path(path: &Path) -> bool {
path.starts_with("/nix/store")
}
pub fn is_guix_store_path(path: &Path) -> bool {
path.starts_with("/gnu/store")
}
pub fn get_store_path(path: &Path) -> Option<PathBuf> {
let path_str = path.to_string_lossy();
for store in ["/nix/store", "/gnu/store"] {
if path_str.starts_with(store) {
return Some(PathBuf::from(store));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_system_type_detect() {
let system_type = SystemType::detect();
assert!(matches!(
system_type,
SystemType::NixOS | SystemType::Guix | SystemType::Fhs
));
}
#[test]
fn test_system_paths_detect() {
let paths = SystemPaths::detect();
assert!(!paths.default_path.is_empty());
}
#[test]
fn test_is_nix_store_path() {
assert!(is_nix_store_path(Path::new("/nix/store/abc123/bin/python")));
assert!(!is_nix_store_path(Path::new("/usr/bin/python")));
}
#[test]
fn test_get_store_path() {
let nix_path = Path::new("/nix/store/abc123/bin/python");
assert_eq!(get_store_path(nix_path), Some(PathBuf::from("/nix/store")));
let usr_path = Path::new("/usr/bin/python");
assert_eq!(get_store_path(usr_path), None);
}
}