use crate::config::Settings;
use eyre::{Result, bail};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Platform {
pub os: String,
pub arch: String,
pub qualifier: Option<String>,
}
impl Platform {
pub fn parse(platform_str: &str) -> Result<Self> {
let parts: Vec<&str> = platform_str.split('-').collect();
match parts.len() {
0 | 1 => bail!(
"Invalid platform format '{}'. Expected 'os-arch' or 'os-arch-qualifier'",
platform_str
),
2 => Ok(Platform {
os: parts[0].to_string(),
arch: parts[1].to_string(),
qualifier: None,
}),
_ => {
let qualifier = parts[2..].join("-");
Ok(Platform {
os: parts[0].to_string(),
arch: parts[1].to_string(),
qualifier: Some(qualifier),
})
}
}
}
pub fn current() -> Self {
let settings = Settings::get();
let os = settings.os().to_string();
let qualifier = if os == "linux" {
match settings.libc() {
Some("musl") => Some("musl".to_string()),
Some("gnu") => None,
_ if is_musl_system() => Some("musl".to_string()),
_ => None,
}
} else {
None
};
Platform {
os,
arch: settings.arch().to_string(),
qualifier,
}
}
pub fn libc(&self) -> Option<&str> {
self.qualifier
.as_deref()?
.split('-')
.find_map(|part| match part {
"gnu" | "glibc" => Some("gnu"),
"musl" => Some("musl"),
_ => None,
})
}
pub fn validate(&self) -> Result<()> {
match self.os.as_str() {
"linux" | "macos" | "windows" => {}
_ => bail!(
"Unsupported OS '{}'. Supported: linux, macos, windows",
self.os
),
}
match self.arch.as_str() {
"x64" | "arm64" | "x86" | "loongarch64" | "riscv64" => {}
_ => bail!(
"Unsupported architecture '{}'. Supported: x64, arm64, x86, loongarch64, riscv64",
self.arch
),
}
if let Some(qualifier) = &self.qualifier {
match qualifier.as_str() {
"gnu" | "glibc" | "musl" | "msvc" | "baseline" | "musl-baseline" => {}
_ => bail!(
"Unsupported qualifier '{}'. Supported: gnu, glibc, musl, msvc, baseline, musl-baseline",
qualifier
),
}
}
Ok(())
}
pub fn is_compatible_with_current(&self) -> bool {
let current = Self::current();
self.os == current.os && self.arch == current.arch
}
pub fn to_key(&self) -> String {
match &self.qualifier {
Some(qualifier) => format!("{}-{}-{}", self.os, self.arch, qualifier),
None => format!("{}-{}", self.os, self.arch),
}
}
pub fn parse_multiple(platform_strings: &[String]) -> Result<Vec<Self>> {
let mut platforms = Vec::new();
for platform_str in platform_strings {
let platform = Self::parse(platform_str)?;
platform.validate()?;
platforms.push(platform);
}
platforms.sort();
platforms.dedup();
Ok(platforms)
}
pub fn common_platforms() -> Vec<Self> {
vec![
Platform::parse("linux-x64").unwrap(),
Platform::parse("linux-x64-musl").unwrap(),
Platform::parse("linux-arm64").unwrap(),
Platform::parse("linux-arm64-musl").unwrap(),
Platform::parse("macos-x64").unwrap(),
Platform::parse("macos-arm64").unwrap(),
Platform::parse("windows-x64").unwrap(),
]
}
pub fn is_windows(&self) -> bool {
self.os == "windows"
}
pub fn is_macos(&self) -> bool {
self.os == "macos"
}
pub fn is_linux(&self) -> bool {
self.os == "linux"
}
pub fn is_arm64(&self) -> bool {
self.arch == "arm64"
}
pub fn is_x64(&self) -> bool {
self.arch == "x64"
}
}
impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_key())
}
}
impl From<String> for Platform {
fn from(s: String) -> Self {
Self::parse(&s).unwrap_or_else(|_| {
Self::current()
})
}
}
impl From<&str> for Platform {
fn from(s: &str) -> Self {
Self::parse(s).unwrap_or_else(|_| {
Self::current()
})
}
}
#[cfg(target_os = "linux")]
pub fn detect_libc() -> Option<&'static str> {
use std::sync::LazyLock;
static DETECTED: LazyLock<Option<&'static str>> = 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 Some("gnu");
}
}
for dir in ["/lib", "/lib64"] {
if has_file_prefix(dir, "ld-musl-") {
return Some("musl");
}
}
if cfg!(target_env = "musl") {
return Some("musl");
}
if cfg!(target_env = "gnu") {
return Some("gnu");
}
None
});
*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<bool> {
let content = std::fs::read_to_string(path).ok()?;
let mut ids: Vec<String> = 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));
}
}
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")]
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)
}
fn is_musl_system() -> bool {
detect_libc() == Some("musl")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_parse_basic() {
let platform = Platform::parse("linux-x64").unwrap();
assert_eq!(platform.os, "linux");
assert_eq!(platform.arch, "x64");
assert_eq!(platform.qualifier, None);
}
#[test]
fn test_platform_parse_with_qualifier() {
let platform = Platform::parse("linux-x64-gnu").unwrap();
assert_eq!(platform.os, "linux");
assert_eq!(platform.arch, "x64");
assert_eq!(platform.qualifier, Some("gnu".to_string()));
}
#[test]
fn test_platform_parse_with_compound_qualifier() {
let platform = Platform::parse("linux-x64-musl-baseline").unwrap();
assert_eq!(platform.os, "linux");
assert_eq!(platform.arch, "x64");
assert_eq!(platform.qualifier, Some("musl-baseline".to_string()));
assert_eq!(platform.to_key(), "linux-x64-musl-baseline");
let reparsed = Platform::parse(&platform.to_key()).unwrap();
assert_eq!(reparsed.qualifier, Some("musl-baseline".to_string()));
}
#[test]
fn test_platform_parse_invalid() {
assert!(Platform::parse("linux").is_err());
assert!(Platform::parse("").is_err());
}
#[test]
fn test_platform_validation() {
assert!(Platform::parse("linux-x64").unwrap().validate().is_ok());
assert!(Platform::parse("macos-arm64").unwrap().validate().is_ok());
assert!(Platform::parse("windows-x64").unwrap().validate().is_ok());
assert!(Platform::parse("linux-x64-gnu").unwrap().validate().is_ok());
assert!(
Platform::parse("linux-x64-glibc")
.unwrap()
.validate()
.is_ok()
);
assert!(Platform::parse("invalid-x64").unwrap().validate().is_err());
assert!(
Platform::parse("linux-invalid")
.unwrap()
.validate()
.is_err()
);
assert!(
Platform::parse("linux-x64-invalid")
.unwrap()
.validate()
.is_err()
);
}
#[test]
fn test_platform_to_key() {
let platform1 = Platform::parse("linux-x64").unwrap();
assert_eq!(platform1.to_key(), "linux-x64");
let platform2 = Platform::parse("linux-x64-gnu").unwrap();
assert_eq!(platform2.to_key(), "linux-x64-gnu");
}
#[test]
fn test_platform_multiple_parsing() {
let platform_strings = vec![
"linux-x64".to_string(),
"macos-arm64".to_string(),
"linux-x64".to_string(), ];
let platforms = Platform::parse_multiple(&platform_strings).unwrap();
assert_eq!(platforms.len(), 2);
assert_eq!(platforms[0].to_key(), "linux-x64");
assert_eq!(platforms[1].to_key(), "macos-arm64");
}
#[test]
fn test_platform_helpers() {
let linux_platform = Platform::parse("linux-arm64").unwrap();
assert!(linux_platform.is_linux());
assert!(linux_platform.is_arm64());
assert!(!linux_platform.is_windows());
assert!(!linux_platform.is_x64());
let windows_platform = Platform::parse("windows-x64").unwrap();
assert!(windows_platform.is_windows());
assert!(windows_platform.is_x64());
assert!(!windows_platform.is_linux());
assert!(!windows_platform.is_arm64());
}
#[test]
fn test_common_platforms() {
let platforms = Platform::common_platforms();
assert_eq!(platforms.len(), 7);
let keys: Vec<String> = platforms.iter().map(|p| p.to_key()).collect();
assert!(keys.contains(&"linux-x64".to_string()));
assert!(keys.contains(&"linux-x64-musl".to_string()));
assert!(keys.contains(&"linux-arm64".to_string()));
assert!(keys.contains(&"linux-arm64-musl".to_string()));
assert!(keys.contains(&"macos-x64".to_string()));
assert!(keys.contains(&"macos-arm64".to_string()));
assert!(keys.contains(&"windows-x64".to_string()));
}
#[cfg(all(target_os = "linux", target_env = "musl"))]
#[test]
fn test_musl_binary_detects_musl() {
assert!(
is_musl_system(),
"musl-compiled binary should detect musl system"
);
}
#[cfg(all(target_os = "linux", target_env = "musl"))]
#[test]
fn test_current_platform_has_musl_qualifier() {
let platform = Platform::current();
assert_eq!(
platform.qualifier.as_deref(),
Some("musl"),
"musl-compiled binary should have musl qualifier, got: {}",
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() {
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);
}
}