diff --git a/.idea/vcs.xml b/.idea/vcs.xml index f92c7b4b7b..35b8d31c0f 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,8 @@ + + + diff --git a/crates/vfox/src/config.rs b/crates/vfox/src/config.rs index 654625b692..a0da134dcd 100644 --- a/crates/vfox/src/config.rs +++ b/crates/vfox/src/config.rs @@ -44,63 +44,6 @@ pub fn arch() -> String { } } -/// Detect the libc environment type at runtime. -/// Returns `Some("gnu")` on glibc Linux, `Some("musl")` on musl Linux, `None` elsewhere. -// NOTE: This logic mirrors is_musl_system() in src/platform.rs. Keep in sync. -#[cfg(target_os = "linux")] -pub(crate) fn env_type() -> Option { - use std::sync::LazyLock as Lazy; - static ENV_TYPE: Lazy> = Lazy::new(|| { - // Allow explicit override via environment variable (only gnu/musl accepted) - if let Ok(val) = std::env::var("MISE_LIBC") { - match val.to_lowercase().as_str() { - "musl" => return Some("musl".to_string()), - "glibc" | "gnu" => return Some("gnu".to_string()), - _ => {} // invalid value ignored, fall through to runtime detection - } - } - // If glibc's dynamic linker exists, this is a glibc system - for dir in ["/lib", "/lib64"] { - if has_file_prefix(dir, "ld-linux-") { - return Some("gnu".to_string()); - } - } - // No glibc linker found — check for musl's - for dir in ["/lib", "/lib64"] { - if has_file_prefix(dir, "ld-musl-") { - return Some("musl".to_string()); - } - } - // No linker found at all (e.g., scratch/busybox container) — - // fall back to the binary's compile-time target - if cfg!(target_env = "musl") { - return Some("musl".to_string()); - } - if cfg!(target_env = "gnu") { - return Some("gnu".to_string()); - } - None - }); - ENV_TYPE.clone() -} - -#[cfg(target_os = "linux")] -fn has_file_prefix(dir: &str, prefix: &str) -> bool { - std::fs::read_dir(dir) - .map(|entries| { - entries - .flatten() - .any(|e| e.file_name().to_string_lossy().starts_with(prefix)) - }) - .unwrap_or(false) -} - -/// On non-Linux platforms, libc variant is not applicable. -#[cfg(not(target_os = "linux"))] -pub(crate) fn env_type() -> Option { - None -} - #[cfg(test)] mod tests { use super::*; @@ -116,45 +59,4 @@ mod tests { let arch = arch(); assert!(!arch.is_empty()); } - - #[test] - fn test_env_type() { - let et = env_type(); - match et.as_deref() { - Some("gnu") | Some("musl") | None => {} - other => panic!("unexpected env_type: {other:?}"), - } - } - - #[cfg(not(target_os = "linux"))] - #[test] - fn test_env_type_non_linux_returns_none() { - assert_eq!(env_type(), None); - } - - #[cfg(target_os = "linux")] - #[test] - fn test_env_type_returns_some_on_linux() { - // On any Linux system (glibc or musl), env_type() should return - // Some("gnu") or Some("musl") — never None. Even in minimal - // containers with no linker, the compile-time fallback applies. - let et = env_type(); - assert!( - et.is_some(), - "env_type() returned None on Linux — expected Some(\"gnu\") or Some(\"musl\")" - ); - } - - #[cfg(all(target_os = "linux", target_env = "musl"))] - #[test] - fn test_env_type_musl_binary_returns_musl() { - // A musl-compiled binary should always report musl, regardless of - // the host system's linker files (covers scratch containers). - let et = env_type(); - assert_eq!( - et.as_deref(), - Some("musl"), - "musl-compiled binary should return Some(\"musl\")" - ); - } } diff --git a/crates/vfox/src/runtime.rs b/crates/vfox/src/runtime.rs index 268c21a732..d6a209417c 100644 --- a/crates/vfox/src/runtime.rs +++ b/crates/vfox/src/runtime.rs @@ -1,4 +1,4 @@ -use crate::config::{arch, env_type, os}; +use crate::config::{arch, os}; use mlua::{UserData, UserDataFields}; use std::path::PathBuf; use std::sync::LazyLock as Lazy; @@ -17,7 +17,7 @@ static RUNTIME: Lazy> = Lazy::new(|| { Mutex::new(Runtime { os: os(), arch: arch(), - env_type: env_type(), + env_type: None, version: "0.6.0".to_string(), // https://github.com/version-fox/vfox/releases plugin_dir_path: PathBuf::new(), }) @@ -64,7 +64,7 @@ impl Runtime { let mut runtime = RUNTIME.lock().unwrap(); runtime.os = os(); runtime.arch = arch(); - runtime.env_type = env_type(); + runtime.env_type = None; } } diff --git a/src/platform.rs b/src/platform.rs index 26280f2ea4..19f9cc3326 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -199,33 +199,78 @@ impl From<&str> for Platform { } } -/// Detect whether the system uses musl libc at runtime. -/// Checks for the absence of glibc's dynamic linker (`ld-linux-*`) in /lib and /lib64. -/// On glibc systems, `ld-linux-*` is always present (even if musl-tools is installed -/// for cross-compilation, which also places `ld-musl-*` in /lib). On musl-only systems -/// (Alpine, Void musl, etc.), only `ld-musl-*` exists without `ld-linux-*`. -// NOTE: This logic is mirrored in crates/vfox/src/config.rs env_type(). Keep in sync. +/// Detect the current libc variant on Linux. +/// +/// Returns `Some("gnu")` on glibc Linux, `Some("musl")` on musl Linux, +/// `None` on non-Linux or when the variant can't be determined (e.g. minimal +/// containers compiled against an unusual target_env). +/// +/// Detection order on Linux: +/// 1. `/etc/os-release` ID/ID_LIKE — strong signal for known musl distros. +/// Necessary because compat shims like `gcompat` on Alpine install +/// `/lib/ld-linux-*` alongside `/lib/ld-musl-*`, which would otherwise +/// cause the linker-based fallback to misclassify the system as glibc. +/// 2. Linker file presence in `/lib` and `/lib64`. +/// 3. Compile-time target (`target_env`) — for scratch/busybox containers +/// with no linker files. #[cfg(target_os = "linux")] -fn is_musl_system() -> bool { +pub fn detect_libc() -> Option<&'static str> { use std::sync::LazyLock; - static IS_MUSL: LazyLock = LazyLock::new(|| { - // If glibc's dynamic linker exists, this is a glibc system + static DETECTED: LazyLock> = LazyLock::new(|| { + if let Some(true) = musl_from_os_release("/etc/os-release") { + return Some("musl"); + } for dir in ["/lib", "/lib64"] { if has_file_prefix(dir, "ld-linux-") { - return false; + return Some("gnu"); } } - // No glibc linker found — check for musl's for dir in ["/lib", "/lib64"] { if has_file_prefix(dir, "ld-musl-") { - return true; + return Some("musl"); } } - // No linker found at all (e.g., scratch/busybox container) — - // fall back to the binary's compile-time target - cfg!(target_env = "musl") + if cfg!(target_env = "musl") { + return Some("musl"); + } + if cfg!(target_env = "gnu") { + return Some("gnu"); + } + None }); - *IS_MUSL + *DETECTED +} + +#[cfg(not(target_os = "linux"))] +pub fn detect_libc() -> Option<&'static str> { + None +} + +#[cfg(target_os = "linux")] +fn musl_from_os_release(path: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let mut ids: Vec = Vec::new(); + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + if key == "ID" || key == "ID_LIKE" { + let value = value.trim().trim_matches('"').trim_matches('\''); + ids.extend(value.split_whitespace().map(str::to_string)); + } + } + // Known musl-libc distros. Compat shims (gcompat) don't change this — the + // underlying libc is still musl. + const MUSL_DISTROS: &[&str] = &["alpine", "postmarketos", "chimera"]; + if ids.iter().any(|id| MUSL_DISTROS.contains(&id.as_str())) { + return Some(true); + } + None } #[cfg(target_os = "linux")] @@ -239,9 +284,8 @@ fn has_file_prefix(dir: &str, prefix: &str) -> bool { .unwrap_or(false) } -#[cfg(not(target_os = "linux"))] fn is_musl_system() -> bool { - false + detect_libc() == Some("musl") } #[cfg(test)] @@ -395,4 +439,66 @@ mod tests { platform.to_key() ); } + + #[cfg(target_os = "linux")] + #[test] + fn test_os_release_alpine_id_is_musl() { + let tmp = std::env::temp_dir().join("mise-libc-alpine"); + std::fs::write( + &tmp, + "NAME=\"Alpine Linux\"\nID=alpine\nVERSION_ID=3.22.4\n", + ) + .unwrap(); + assert_eq!(musl_from_os_release(tmp.to_str().unwrap()), Some(true)); + let _ = std::fs::remove_file(&tmp); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_os_release_id_like_alpine_is_musl() { + let tmp = std::env::temp_dir().join("mise-libc-id-like"); + std::fs::write(&tmp, "ID=postmarketos\nID_LIKE=\"alpine\"\n").unwrap(); + assert_eq!(musl_from_os_release(tmp.to_str().unwrap()), Some(true)); + let _ = std::fs::remove_file(&tmp); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_os_release_debian_returns_none() { + let tmp = std::env::temp_dir().join("mise-libc-debian"); + std::fs::write(&tmp, "ID=debian\nID_LIKE=\"\"\n").unwrap(); + assert_eq!(musl_from_os_release(tmp.to_str().unwrap()), None); + let _ = std::fs::remove_file(&tmp); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_os_release_missing_returns_none() { + assert_eq!(musl_from_os_release("/nonexistent/os-release"), None); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_os_release_comments_and_blank_lines_do_not_short_circuit() { + // Regression: previously `split_once('=')?` returned None on the first + // comment or blank line, causing the function to ignore the `ID=` line + // that came after and silently fall back to linker-based detection. + let tmp = std::env::temp_dir().join("mise-libc-comments"); + std::fs::write( + &tmp, + "# this is a comment\n\nNAME=\"Alpine Linux\"\nID=alpine\n", + ) + .unwrap(); + assert_eq!(musl_from_os_release(tmp.to_str().unwrap()), Some(true)); + let _ = std::fs::remove_file(&tmp); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_os_release_whitespace_around_key_tolerated() { + let tmp = std::env::temp_dir().join("mise-libc-whitespace"); + std::fs::write(&tmp, " ID = alpine \n").unwrap(); + assert_eq!(musl_from_os_release(tmp.to_str().unwrap()), Some(true)); + let _ = std::fs::remove_file(&tmp); + } } diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 1133492088..4b368cf58e 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -126,7 +126,10 @@ impl VfoxPlugin { pub fn vfox(&self) -> Result<(Vfox, mpsc::Receiver)> { let settings = Settings::get(); let env_type = if settings.os() == "linux" { - settings.libc().map(str::to_string) + settings + .libc() + .map(str::to_string) + .or_else(|| crate::platform::detect_libc().map(str::to_string)) } else { None };