From 968cfda089a50132c20165a21e0595433605ab30 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 7 Mar 2026 01:42:54 +0000 Subject: [PATCH 1/6] feat: runtime musl/glibc detection for correct libc variant selection - Detect musl at runtime by checking for /lib/ld-musl-* instead of using compile-time cfg!(target_env = "musl"), so a musl-built mise on a glibc system (or vice versa) picks the correct artifact variant - Set qualifier on Platform::current() for musl systems so lockfile keys and asset selection use the right libc variant - Add linux-x64-musl and linux-arm64-musl to common_platforms() so mise lock resolves checksums/URLs for both glibc and musl variants - Update get_platform_key() to include the qualifier from Platform::current() - Add github:snyk/cli as a registry backend for snyk Closes #8456 Co-Authored-By: Claude Opus 4.6 --- registry/snyk.toml | 2 +- src/backend/asset_matcher.rs | 12 ++++++---- src/backend/mod.rs | 8 +++---- src/platform.rs | 45 ++++++++++++++++++++++++++++++++---- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/registry/snyk.toml b/registry/snyk.toml index 1df1806c97..7f623fe5cb 100644 --- a/registry/snyk.toml +++ b/registry/snyk.toml @@ -1,2 +1,2 @@ -backends = ["aqua:snyk/cli", "asdf:nirfuchs/asdf-snyk"] +backends = ["aqua:snyk/cli", "github:snyk/cli", "asdf:nirfuchs/asdf-snyk"] description = "Snyk CLI scans and monitors your projects for security vulnerabilities" diff --git a/src/backend/asset_matcher.rs b/src/backend/asset_matcher.rs index 38f72c70dd..69ae414605 100644 --- a/src/backend/asset_matcher.rs +++ b/src/backend/asset_matcher.rs @@ -176,15 +176,19 @@ pub struct AssetPicker { } impl AssetPicker { - /// Create an AssetPicker with an explicit libc setting + /// Create an AssetPicker with an explicit libc setting. + /// When no explicit libc is provided, uses runtime detection via Platform::current() + /// to determine the system's libc (musl vs glibc), rather than compile-time checks. pub fn with_libc(target_os: String, target_arch: String, libc: Option) -> Self { let target_libc = libc.unwrap_or_else(|| { if target_os == "windows" { "msvc".to_string() - } else if cfg!(target_env = "musl") { - "musl".to_string() } else { - "gnu".to_string() + let current = crate::platform::Platform::current(); + match current.qualifier.as_deref() { + Some("musl") | Some("musl-baseline") => "musl".to_string(), + _ => "gnu".to_string(), + } } }); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 378ae8d9fc..9f868b67d8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -373,12 +373,10 @@ pub trait Backend: Debug + Send + Sync { fn ba(&self) -> &Arc; /// Generates a platform key for lockfile storage. - /// Default implementation uses os-arch format, but backends can override for more specific keys. + /// Default implementation uses the current platform key (os-arch or os-arch-qualifier), + /// which includes the libc qualifier on musl systems. fn get_platform_key(&self) -> String { - let settings = Settings::get(); - let os = settings.os(); - let arch = settings.arch(); - format!("{os}-{arch}") + Platform::current().to_key() } /// Resolves the lockfile options for a tool request on a target platform. diff --git a/src/platform.rs b/src/platform.rs index 5f089709ee..397791eaff 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -38,13 +38,20 @@ impl Platform { } } - /// Get the current platform from system information + /// Get the current platform from system information. + /// On Linux, detects musl vs glibc at runtime and sets the qualifier accordingly. 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: settings.os().to_string(), + os, arch: settings.arch().to_string(), - qualifier: None, + qualifier, } } @@ -117,7 +124,9 @@ impl Platform { pub fn common_platforms() -> Vec { 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(), @@ -174,6 +183,32 @@ impl From<&str> for Platform { } } +/// Detect whether the system uses musl libc at runtime. +/// Checks for the musl dynamic linker in /lib, which is present on musl-based +/// systems like Alpine Linux. This is a runtime check (not compile-time) so a +/// statically-linked or musl-built mise binary running on a glibc system will +/// correctly detect glibc, and vice versa. +#[cfg(target_os = "linux")] +fn is_musl_system() -> bool { + use std::sync::LazyLock; + static IS_MUSL: LazyLock = LazyLock::new(|| { + if let Ok(entries) = std::fs::read_dir("/lib") { + for entry in entries.flatten() { + if entry.file_name().to_string_lossy().starts_with("ld-musl-") { + return true; + } + } + } + false + }); + *IS_MUSL +} + +#[cfg(not(target_os = "linux"))] +fn is_musl_system() -> bool { + false +} + #[cfg(test)] mod tests { use super::*; @@ -283,11 +318,13 @@ mod tests { #[test] fn test_common_platforms() { let platforms = Platform::common_platforms(); - assert_eq!(platforms.len(), 5); + assert_eq!(platforms.len(), 7); let keys: Vec = 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())); From a68a16467cd71686df7dfb642e761fad1254c76d Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 7 Mar 2026 01:57:06 +0000 Subject: [PATCH 2/6] fix: address review feedback for musl detection - Fix cross-platform lockfile bug: AssetPicker::with_libc now defaults to "gnu" when no qualifier is passed, instead of checking Platform::current(). This prevents `mise lock` on musl systems from incorrectly populating unqualified `linux-x64` entries with musl assets. - Add ToolVersion::get_lock_platform() fallback: when looking up a qualified key like `linux-x64-musl`, falls back to the unqualified `linux-x64` key. This maintains backward compatibility with existing lockfiles in --locked mode. - Update all lockfile read sites (github, java, locked-mode check) to use the new fallback method. Co-Authored-By: Claude Opus 4.6 --- src/backend/asset_matcher.rs | 12 +++++------- src/backend/github.rs | 2 +- src/backend/mod.rs | 3 +-- src/plugins/core/java.rs | 2 +- src/toolset/tool_version.rs | 19 +++++++++++++++++++ 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/backend/asset_matcher.rs b/src/backend/asset_matcher.rs index 69ae414605..abf7888a36 100644 --- a/src/backend/asset_matcher.rs +++ b/src/backend/asset_matcher.rs @@ -177,18 +177,16 @@ pub struct AssetPicker { impl AssetPicker { /// Create an AssetPicker with an explicit libc setting. - /// When no explicit libc is provided, uses runtime detection via Platform::current() - /// to determine the system's libc (musl vs glibc), rather than compile-time checks. + /// When no explicit libc is provided, defaults to the platform's standard libc + /// (msvc for Windows, gnu for Linux/other). The caller is responsible for passing + /// the correct libc qualifier from PlatformTarget — this avoids polluting + /// cross-platform lockfile entries with the current system's libc. pub fn with_libc(target_os: String, target_arch: String, libc: Option) -> Self { let target_libc = libc.unwrap_or_else(|| { if target_os == "windows" { "msvc".to_string() } else { - let current = crate::platform::Platform::current(); - match current.qualifier.as_deref() { - Some("musl") | Some("musl-baseline") => "musl".to_string(), - _ => "gnu".to_string(), - } + "gnu".to_string() } }); diff --git a/src/backend/github.rs b/src/backend/github.rs index 6674f3874f..c349227d76 100644 --- a/src/backend/github.rs +++ b/src/backend/github.rs @@ -260,7 +260,7 @@ impl Backend for UnifiedGitBackend { // Check if URL already exists in lockfile platforms first let platform_key = self.get_platform_key(); - let asset = if let Some(existing_platform) = tv.lock_platforms.get(&platform_key) + let asset = if let Some(existing_platform) = tv.get_lock_platform(&platform_key) && existing_platform.url.is_some() { debug!( diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 9f868b67d8..d27bf97d0c 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -987,8 +987,7 @@ pub trait Backend: Debug + Send + Sync { if ctx.locked && !tv.request.source().is_tool_stub() && self.supports_lockfile_url() { let platform_key = self.get_platform_key(); let has_lockfile_url = tv - .lock_platforms - .get(&platform_key) + .get_lock_platform(&platform_key) .and_then(|p| p.url.as_ref()) .is_some(); if !has_lockfile_url { diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 6d6a3b7ee8..7fb4f4b180 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -439,7 +439,7 @@ impl Backend for JavaPlugin { // Check if URL already exists in lockfile platforms first let platform_key = self.get_platform_key(); let (metadata, tarball_path) = - if let Some(platform_info) = tv.lock_platforms.get(&platform_key) { + if let Some(platform_info) = tv.get_lock_platform(&platform_key) { if let Some(ref url) = platform_info.url { // Use the filename from the URL, not the platform key let filename = url.split('/').next_back().unwrap(); diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index c7c6f13edb..2ca75cf46a 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -86,6 +86,25 @@ impl ToolVersion { tv } + /// Look up platform info with fallback to the base (unqualified) key. + /// For example, if platform_key is "linux-x64-musl" and not found, + /// falls back to "linux-x64". This provides backward compatibility + /// with lockfiles generated before musl qualifier support was added. + pub fn get_lock_platform(&self, platform_key: &str) -> Option<&PlatformInfo> { + self.lock_platforms.get(platform_key).or_else(|| { + if let Ok(platform) = crate::platform::Platform::parse(platform_key) { + if platform.qualifier.is_some() { + let base_key = format!("{}-{}", platform.os, platform.arch); + self.lock_platforms.get(&base_key) + } else { + None + } + } else { + None + } + }) + } + pub fn ba(&self) -> &BackendArg { self.request.ba() } From 049a44d2594695941e747dfd0bbdba173d62fc58 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 7 Mar 2026 02:03:59 +0000 Subject: [PATCH 3/6] fix(backend): use lockfile fallback for aqua platform lookup The aqua backend was still using tv.lock_platforms.get() directly instead of tv.get_lock_platform(), missing the fallback from qualified keys (linux-x64-musl) to unqualified keys (linux-x64) for pre-existing lockfiles. This caused --locked mode to pass validation but then fail to find the URL during aqua's actual install. Co-Authored-By: Claude Opus 4.6 --- src/backend/aqua.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 17cdd2c16c..d0af32f672 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -284,8 +284,7 @@ impl Backend for AquaBackend { // This allows us to skip API calls when lockfile has the URL let platform_key = self.get_platform_key(); let existing_platform = tv - .lock_platforms - .get(&platform_key) + .get_lock_platform(&platform_key) .and_then(|asset| asset.url.clone()); // Skip get_version_tags() API call if we have lockfile URL From 142f0178533a3007571309f8ef62639413f165fe Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 7 Mar 2026 02:11:44 +0000 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20remove=20musl=E2=86=92glibc=20lockfi?= =?UTF-8?q?le=20fallback=20that=20installs=20wrong=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove get_lock_platform() fallback from linux-x64-musl to linux-x64. On musl systems with old lockfiles, this fallback would silently install glibc binaries (which won't run) instead of either resolving fresh (normal mode) or erroring with a helpful "run mise lock" message (--locked mode). Both of those behaviors are correct and preferable to silently installing the wrong binary. Co-Authored-By: Claude Opus 4.6 --- src/backend/aqua.rs | 3 ++- src/backend/github.rs | 2 +- src/backend/mod.rs | 3 ++- src/plugins/core/java.rs | 2 +- src/toolset/tool_version.rs | 19 ------------------- 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index d0af32f672..17cdd2c16c 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -284,7 +284,8 @@ impl Backend for AquaBackend { // This allows us to skip API calls when lockfile has the URL let platform_key = self.get_platform_key(); let existing_platform = tv - .get_lock_platform(&platform_key) + .lock_platforms + .get(&platform_key) .and_then(|asset| asset.url.clone()); // Skip get_version_tags() API call if we have lockfile URL diff --git a/src/backend/github.rs b/src/backend/github.rs index c349227d76..6674f3874f 100644 --- a/src/backend/github.rs +++ b/src/backend/github.rs @@ -260,7 +260,7 @@ impl Backend for UnifiedGitBackend { // Check if URL already exists in lockfile platforms first let platform_key = self.get_platform_key(); - let asset = if let Some(existing_platform) = tv.get_lock_platform(&platform_key) + let asset = if let Some(existing_platform) = tv.lock_platforms.get(&platform_key) && existing_platform.url.is_some() { debug!( diff --git a/src/backend/mod.rs b/src/backend/mod.rs index d27bf97d0c..9f868b67d8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -987,7 +987,8 @@ pub trait Backend: Debug + Send + Sync { if ctx.locked && !tv.request.source().is_tool_stub() && self.supports_lockfile_url() { let platform_key = self.get_platform_key(); let has_lockfile_url = tv - .get_lock_platform(&platform_key) + .lock_platforms + .get(&platform_key) .and_then(|p| p.url.as_ref()) .is_some(); if !has_lockfile_url { diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 7fb4f4b180..6d6a3b7ee8 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -439,7 +439,7 @@ impl Backend for JavaPlugin { // Check if URL already exists in lockfile platforms first let platform_key = self.get_platform_key(); let (metadata, tarball_path) = - if let Some(platform_info) = tv.get_lock_platform(&platform_key) { + if let Some(platform_info) = tv.lock_platforms.get(&platform_key) { if let Some(ref url) = platform_info.url { // Use the filename from the URL, not the platform key let filename = url.split('/').next_back().unwrap(); diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index 2ca75cf46a..c7c6f13edb 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -86,25 +86,6 @@ impl ToolVersion { tv } - /// Look up platform info with fallback to the base (unqualified) key. - /// For example, if platform_key is "linux-x64-musl" and not found, - /// falls back to "linux-x64". This provides backward compatibility - /// with lockfiles generated before musl qualifier support was added. - pub fn get_lock_platform(&self, platform_key: &str) -> Option<&PlatformInfo> { - self.lock_platforms.get(platform_key).or_else(|| { - if let Ok(platform) = crate::platform::Platform::parse(platform_key) { - if platform.qualifier.is_some() { - let base_key = format!("{}-{}", platform.os, platform.arch); - self.lock_platforms.get(&base_key) - } else { - None - } - } else { - None - } - }) - } - pub fn ba(&self) -> &BackendArg { self.request.ba() } From 7874b45ee2d5d310a23476b54b0161e9ca817aed Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 7 Mar 2026 02:22:31 +0000 Subject: [PATCH 5/6] fix: use ldd-based musl detection and update e2e test for 7 platforms - Replace /lib/ld-musl-* file check with ldd --version output parsing. The ld-musl linker file can exist on glibc systems with musl-tools installed for cross-compilation, causing false positive detection. ldd --version reliably distinguishes: musl's ldd prints "musl libc", glibc's prints GNU version info. - Update e2e test_lock to expect 7 platforms (added musl variants). Co-Authored-By: Claude Opus 4.6 --- e2e/cli/test_lock | 4 ++-- src/platform.rs | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/e2e/cli/test_lock b/e2e/cli/test_lock index 7ff524d2bb..bc3d86ab02 100644 --- a/e2e/cli/test_lock +++ b/e2e/cli/test_lock @@ -16,9 +16,9 @@ EOF rm -f mise.lock -# Test that lock command generates lockfile for all 5 default platforms +# Test that lock command generates lockfile for all 7 default platforms (including musl variants) output=$(mise lock 2>&1) -assert_contains "echo '$output'" "Targeting 5 platform(s)" +assert_contains "echo '$output'" "Targeting 7 platform(s)" assert_contains "echo '$output'" "Processing 1 tool(s)" assert_contains "echo '$output'" "Updated" assert_contains "echo '$output'" "Lockfile written to" diff --git a/src/platform.rs b/src/platform.rs index 397791eaff..322307cc1a 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -184,22 +184,22 @@ impl From<&str> for Platform { } /// Detect whether the system uses musl libc at runtime. -/// Checks for the musl dynamic linker in /lib, which is present on musl-based -/// systems like Alpine Linux. This is a runtime check (not compile-time) so a -/// statically-linked or musl-built mise binary running on a glibc system will -/// correctly detect glibc, and vice versa. +/// Uses `ldd --version` output which reliably distinguishes musl from glibc: +/// - glibc's ldd prints version info (e.g., "ldd (GNU libc) 2.31") +/// - musl's ldd prints "musl libc" in its output +/// This avoids false positives from `/lib/ld-musl-*` which can exist on glibc +/// systems with musl-tools installed for cross-compilation. #[cfg(target_os = "linux")] fn is_musl_system() -> bool { use std::sync::LazyLock; static IS_MUSL: LazyLock = LazyLock::new(|| { - if let Ok(entries) = std::fs::read_dir("/lib") { - for entry in entries.flatten() { - if entry.file_name().to_string_lossy().starts_with("ld-musl-") { - return true; - } - } - } - false + let Ok(output) = std::process::Command::new("ldd").arg("--version").output() else { + return false; + }; + // musl's ldd prints to stderr, glibc's to stdout + let text = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + stderr.contains("musl") || text.contains("musl") }); *IS_MUSL } From bd5f8d2f772ba22350d587f8c9b2423252dfd897 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 7 Mar 2026 02:25:12 +0000 Subject: [PATCH 6/6] fix: avoid shelling out for musl detection, use filesystem checks Replace ldd --version subprocess with filesystem checks for the dynamic linker. glibc systems always have /lib[64]/ld-linux-*, even when musl-tools is installed for cross-compilation. Musl systems (Alpine, Void musl, etc.) only have /lib/ld-musl-* without ld-linux-*. Checking for the absence of glibc's linker first avoids false positives and is faster than spawning a subprocess. Co-Authored-By: Claude Opus 4.6 --- src/platform.rs | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/platform.rs b/src/platform.rs index 322307cc1a..d2d447c139 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -184,26 +184,37 @@ impl From<&str> for Platform { } /// Detect whether the system uses musl libc at runtime. -/// Uses `ldd --version` output which reliably distinguishes musl from glibc: -/// - glibc's ldd prints version info (e.g., "ldd (GNU libc) 2.31") -/// - musl's ldd prints "musl libc" in its output -/// This avoids false positives from `/lib/ld-musl-*` which can exist on glibc -/// systems with musl-tools installed for cross-compilation. +/// 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-*`. #[cfg(target_os = "linux")] fn is_musl_system() -> bool { use std::sync::LazyLock; static IS_MUSL: LazyLock = LazyLock::new(|| { - let Ok(output) = std::process::Command::new("ldd").arg("--version").output() else { - return false; - }; - // musl's ldd prints to stderr, glibc's to stdout - let text = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - stderr.contains("musl") || text.contains("musl") + // If glibc's dynamic linker exists, this is a glibc system + for dir in ["/lib", "/lib64"] { + if has_file_prefix(dir, "ld-linux-") { + return false; + } + } + // No glibc linker found — check for musl's + has_file_prefix("/lib", "ld-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