use crate::{Arch, cpuinfo::detect_hardware_floating_point_support};
use fs_err as fs;
use goblin::elf::Elf;
use regex::Regex;
use std::fmt::Display;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::LazyLock;
use std::{env, fmt};
use target_lexicon::Endianness;
use tracing::trace;
use uv_fs::Simplified;
use uv_static::EnvVars;
#[derive(Debug, thiserror::Error)]
pub enum LibcDetectionError {
#[error(
"Could not detect either glibc version nor musl libc version, at least one of which is required"
)]
NoLibcFound,
#[error("Failed to get base name of symbolic link path {0}")]
MissingBasePath(PathBuf),
#[error("Failed to find glibc version in the filename of linker: `{0}`")]
GlibcExtractionMismatch(PathBuf),
#[error("Failed to determine {libc} version by running: `{program}`")]
FailedToRun {
libc: &'static str,
program: String,
#[source]
err: io::Error,
},
#[error("Could not find glibc version in output of: `{0} --version`")]
InvalidLdSoOutputGnu(PathBuf),
#[error("Could not find musl version in output of: `{0}`")]
InvalidLdSoOutputMusl(PathBuf),
#[error("Could not read ELF interpreter from any of the following paths: {0}")]
CoreBinaryParsing(String),
#[error("Failed to find any common binaries to determine libc from: {0}")]
NoCommonBinariesFound(String),
#[error("Failed to determine libc")]
Io(#[from] io::Error),
}
#[derive(Debug, PartialEq, Eq)]
pub enum LibcVersion {
Manylinux { major: u32, minor: u32 },
Musllinux { major: u32, minor: u32 },
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
pub enum Libc {
Some(target_lexicon::Environment),
None,
}
impl Libc {
pub fn from_env() -> Result<Self, crate::Error> {
match env::consts::OS {
"linux" => {
if let Ok(libc) = env::var(EnvVars::UV_LIBC) {
if !libc.is_empty() {
return Self::from_str(&libc);
}
}
Ok(Self::Some(match detect_linux_libc()? {
LibcVersion::Manylinux { .. } => match env::consts::ARCH {
"arm" | "armv5te" | "armv7" => {
match detect_hardware_floating_point_support() {
Ok(true) => target_lexicon::Environment::Gnueabihf,
Ok(false) => target_lexicon::Environment::Gnueabi,
Err(_) => target_lexicon::Environment::Gnu,
}
}
_ => target_lexicon::Environment::Gnu,
},
LibcVersion::Musllinux { .. } => match env::consts::ARCH {
"arm" | "armv5te" | "armv7" => {
match detect_hardware_floating_point_support() {
Ok(true) => target_lexicon::Environment::Musleabihf,
Ok(false) => target_lexicon::Environment::Musleabi,
Err(_) => target_lexicon::Environment::Musl,
}
}
_ => target_lexicon::Environment::Musl,
},
}))
}
"windows" | "macos" => Ok(Self::None),
_ => Ok(Self::None),
}
}
pub fn is_musl(&self) -> bool {
matches!(
self,
Self::Some(
target_lexicon::Environment::Musl
| target_lexicon::Environment::Musleabi
| target_lexicon::Environment::Musleabihf
)
)
}
}
impl FromStr for Libc {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
"gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
"gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
"musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
"musleabi" => Ok(Self::Some(target_lexicon::Environment::Musleabi)),
"musleabihf" => Ok(Self::Some(target_lexicon::Environment::Musleabihf)),
"none" => Ok(Self::None),
_ => Err(crate::Error::UnknownLibc(s.to_string())),
}
}
}
impl Display for Libc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Some(env) => write!(f, "{env}"),
Self::None => write!(f, "none"),
}
}
}
impl From<&uv_platform_tags::Os> for Libc {
fn from(value: &uv_platform_tags::Os) -> Self {
match value {
uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
uv_platform_tags::Os::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
_ => Self::None,
}
}
}
pub(crate) fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
let ld_path = find_ld_path()?;
trace!("Found `ld` path: {}", ld_path.user_display());
match detect_musl_version(&ld_path) {
Ok(os) => return Ok(os),
Err(err) => {
trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
}
}
match detect_linux_libc_from_ld_symlink(&ld_path) {
Ok(os) => return Ok(os),
Err(err) => {
trace!(
"Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
);
}
}
match detect_glibc_version_from_ld(&ld_path) {
Ok(os_version) => return Ok(os_version),
Err(err) => {
trace!(
"Tried to find glibc version from `{} --version`, but failed: {}",
ld_path.simplified_display(),
err
);
}
}
Err(LibcDetectionError::NoLibcFound)
}
fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
let output = Command::new(ld_so)
.args(["--version"])
.output()
.map_err(|err| LibcDetectionError::FailedToRun {
libc: "glibc",
program: format!("{} --version", ld_so.user_display()),
err,
})?;
if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
return Ok(os);
}
if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
return Ok(os);
}
Err(LibcDetectionError::InvalidLdSoOutputGnu(
ld_so.to_path_buf(),
))
}
fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)").unwrap());
let output = String::from_utf8_lossy(output);
trace!("{kind} output from `ld.so --version`: {output:?}");
let (_, [version]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
let mut parsed_ints = version.split('.').map(str::parse).fuse();
let major = parsed_ints.next()?.ok()?;
let minor = parsed_ints.next()?.ok()?;
trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
Some(LibcVersion::Manylinux { major, minor })
}
fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$").unwrap());
let ld_path = fs::read_link(path)?;
let filename = ld_path
.file_name()
.ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
.to_string_lossy();
let (_, [major, minor]) = RE
.captures(&filename)
.map(|c| c.extract())
.ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
let major = major.parse().expect("valid major version");
let minor = minor.parse().expect("valid minor version");
Ok(LibcVersion::Manylinux { major, minor })
}
fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
let ld_path = ld_path.as_ref();
let output = Command::new(ld_path)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|err| LibcDetectionError::FailedToRun {
libc: "musl",
program: ld_path.to_string_lossy().to_string(),
err,
})?;
if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
return Ok(os);
}
if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
return Ok(os);
}
Err(LibcDetectionError::InvalidLdSoOutputMusl(
ld_path.to_path_buf(),
))
}
fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Version ([0-9]{1,4})\.([0-9]{1,4})").unwrap());
let output = String::from_utf8_lossy(output);
trace!("{kind} output from `ld`: {output:?}");
let (_, [major, minor]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
let major = major.parse().expect("valid major version");
let minor = minor.parse().expect("valid minor version");
trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
Some(LibcVersion::Musllinux { major, minor })
}
fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
let mut found_anything = false;
for path in attempts {
if std::fs::exists(path).ok() == Some(true) {
found_anything = true;
if let Some(ld_path) = find_ld_path_at(path) {
return Ok(ld_path);
}
}
}
if let Some(ld_path) = find_ld_path_from_filesystem() {
return Ok(ld_path);
}
let attempts_string = attempts.join(", ");
if !found_anything {
Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
} else {
Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
}
}
fn find_ld_path_from_filesystem() -> Option<PathBuf> {
find_ld_path_from_root_and_arch(Path::new("/"), Arch::from_env())
}
fn find_ld_path_from_root_and_arch(root: &Path, architecture: Arch) -> Option<PathBuf> {
let Some(candidates) = dynamic_linker_candidates(architecture) else {
trace!("No known dynamic linker paths for architecture `{architecture}`");
return None;
};
let paths = candidates
.iter()
.map(|candidate| root.join(candidate.trim_start_matches('/')))
.collect::<Vec<_>>();
for path in &paths {
if std::fs::exists(path).ok() == Some(true) {
trace!(
"Found dynamic linker on filesystem: {}",
path.user_display()
);
return Some(path.clone());
}
}
trace!(
"Could not find dynamic linker in any expected filesystem path for architecture `{}`: {}",
architecture,
paths
.iter()
.map(|path| path.user_display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
None
}
fn dynamic_linker_candidates(architecture: Arch) -> Option<&'static [&'static str]> {
let family = architecture.family();
match family {
target_lexicon::Architecture::X86_64 => {
Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1"])
}
target_lexicon::Architecture::X86_32(_) => {
Some(&["/lib/ld-linux.so.2", "/lib/ld-musl-i386.so.1"])
}
target_lexicon::Architecture::Aarch64(_) => match family.endianness().ok()? {
Endianness::Little => {
Some(&["/lib/ld-linux-aarch64.so.1", "/lib/ld-musl-aarch64.so.1"])
}
Endianness::Big => Some(&[
"/lib/ld-linux-aarch64_be.so.1",
"/lib/ld-musl-aarch64_be.so.1",
]),
},
target_lexicon::Architecture::Arm(_) => match family.endianness().ok()? {
Endianness::Little => Some(&[
"/lib/ld-linux-armhf.so.3",
"/lib/ld-linux.so.3",
"/lib/ld-musl-armhf.so.1",
"/lib/ld-musl-arm.so.1",
]),
Endianness::Big => Some(&[
"/lib/ld-linux-armhf.so.3",
"/lib/ld-linux.so.3",
"/lib/ld-musl-armebhf.so.1",
"/lib/ld-musl-armeb.so.1",
]),
},
target_lexicon::Architecture::Powerpc64 => {
Some(&["/lib64/ld64.so.1", "/lib/ld-musl-powerpc64.so.1"])
}
target_lexicon::Architecture::Powerpc64le => {
Some(&["/lib64/ld64.so.2", "/lib/ld-musl-powerpc64le.so.1"])
}
target_lexicon::Architecture::S390x => Some(&["/lib/ld64.so.1", "/lib/ld-musl-s390x.so.1"]),
target_lexicon::Architecture::Riscv64(_) => Some(&[
"/lib/ld-linux-riscv64-lp64d.so.1",
"/lib/ld-linux-riscv64-lp64.so.1",
"/lib/ld-musl-riscv64.so.1",
"/lib/ld-musl-riscv64-sp.so.1",
"/lib/ld-musl-riscv64-sf.so.1",
]),
target_lexicon::Architecture::LoongArch64 => Some(&[
"/lib64/ld-linux-loongarch-lp64d.so.1",
"/lib64/ld-linux-loongarch-lp64s.so.1",
"/lib/ld-musl-loongarch64.so.1",
"/lib/ld-musl-loongarch64-sp.so.1",
"/lib/ld-musl-loongarch64-sf.so.1",
]),
_ => None,
}
}
fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
let path = path.as_ref();
let buffer = fs::read(path).ok()?;
let elf = match Elf::parse(&buffer) {
Ok(elf) => elf,
Err(err) => {
trace!(
"Could not parse ELF file at `{}`: `{}`",
path.user_display(),
err
);
return None;
}
};
let Some(elf_interpreter) = elf.interpreter else {
trace!(
"Couldn't find ELF interpreter path from {}",
path.user_display()
);
return None;
};
Some(PathBuf::from(elf_interpreter))
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use tempfile::tempdir;
#[test]
fn parse_ld_so_output() {
let ver_str = glibc_ld_output_to_version(
"stdout",
indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
"},
)
.unwrap();
assert_eq!(
ver_str,
LibcVersion::Manylinux {
major: 2,
minor: 39
}
);
}
#[test]
fn parse_musl_ld_output() {
let output = b"\
musl libc (x86_64)
Version 1.2.4_git20230717
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
";
let got = musl_ld_output_to_version("stderr", output).unwrap();
assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
}
#[test]
fn dynamic_linker_candidates_prefer_glibc_before_musl() {
assert_eq!(
dynamic_linker_candidates(Arch::from_str("x86_64").unwrap()),
Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1",][..])
);
}
#[test]
fn find_ld_path_from_root_and_arch_returns_glibc_when_only_glibc_is_present() {
let root = tempdir().unwrap();
let ld_path = root.path().join("lib64/ld-linux-x86-64.so.2");
fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
fs::write(&ld_path, "").unwrap();
let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
assert_eq!(got, Some(ld_path));
}
#[test]
fn find_ld_path_from_root_and_arch_returns_musl_when_only_musl_is_present() {
let root = tempdir().unwrap();
let ld_path = root.path().join("lib/ld-musl-x86_64.so.1");
fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
fs::write(&ld_path, "").unwrap();
let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
assert_eq!(got, Some(ld_path));
}
#[test]
fn find_ld_path_from_root_and_arch_returns_glibc_when_both_linkers_are_present() {
let root = tempdir().unwrap();
let glibc_path = root.path().join("lib64/ld-linux-x86-64.so.2");
let musl_path = root.path().join("lib/ld-musl-x86_64.so.1");
fs::create_dir_all(glibc_path.parent().unwrap()).unwrap();
fs::create_dir_all(musl_path.parent().unwrap()).unwrap();
fs::write(&glibc_path, "").unwrap();
fs::write(&musl_path, "").unwrap();
let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
assert_eq!(got, Some(glibc_path));
}
#[test]
fn find_ld_path_from_root_and_arch_returns_none_when_neither_linker_is_present() {
let root = tempdir().unwrap();
let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
assert_eq!(got, None);
}
}