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" && is_musl_system() {
Some("musl".to_string())
} else {
None
};
Platform {
os,
arch: settings.arch().to_string(),
qualifier,
}
}
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" => {}
_ => bail!(
"Unsupported architecture '{}'. Supported: x64, arm64, x86",
self.arch
),
}
if let Some(qualifier) = &self.qualifier {
match qualifier.as_str() {
"gnu" | "musl" | "msvc" | "baseline" | "musl-baseline" => {}
_ => bail!(
"Unsupported qualifier '{}'. Supported: gnu, 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")]
fn is_musl_system() -> bool {
use std::sync::LazyLock;
static IS_MUSL: LazyLock<bool> = LazyLock::new(|| {
if let Ok(val) = std::env::var("MISE_LIBC") {
match val.to_lowercase().as_str() {
"musl" => return true,
"gnu" => return false,
_ => {} }
}
for dir in ["/lib", "/lib64"] {
if has_file_prefix(dir, "ld-linux-") {
return false;
}
}
for dir in ["/lib", "/lib64"] {
if has_file_prefix(dir, "ld-musl-") {
return true;
}
}
cfg!(target_env = "musl")
});
*IS_MUSL
}
#[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)
}
#[cfg(not(target_os = "linux"))]
fn is_musl_system() -> bool {
false
}
#[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("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()
);
}
}