1use crate::cpuinfo::detect_hardware_floating_point_support;
7use fs_err as fs;
8use goblin::elf::Elf;
9use regex::Regex;
10use std::fmt::Display;
11use std::io;
12use std::path::{Path, PathBuf};
13use std::process::{Command, Stdio};
14use std::str::FromStr;
15use std::sync::LazyLock;
16use std::{env, fmt};
17use tracing::trace;
18use uv_fs::Simplified;
19use uv_static::EnvVars;
20
21#[derive(Debug, thiserror::Error)]
22pub enum LibcDetectionError {
23 #[error(
24 "Could not detect either glibc version nor musl libc version, at least one of which is required"
25 )]
26 NoLibcFound,
27 #[error("Failed to get base name of symbolic link path {0}")]
28 MissingBasePath(PathBuf),
29 #[error("Failed to find glibc version in the filename of linker: `{0}`")]
30 GlibcExtractionMismatch(PathBuf),
31 #[error("Failed to determine {libc} version by running: `{program}`")]
32 FailedToRun {
33 libc: &'static str,
34 program: String,
35 #[source]
36 err: io::Error,
37 },
38 #[error("Could not find glibc version in output of: `{0} --version`")]
39 InvalidLdSoOutputGnu(PathBuf),
40 #[error("Could not find musl version in output of: `{0}`")]
41 InvalidLdSoOutputMusl(PathBuf),
42 #[error("Could not read ELF interpreter from any of the following paths: {0}")]
43 CoreBinaryParsing(String),
44 #[error("Failed to find any common binaries to determine libc from: {0}")]
45 NoCommonBinariesFound(String),
46 #[error("Failed to determine libc")]
47 Io(#[from] io::Error),
48}
49
50#[derive(Debug, PartialEq, Eq)]
52pub enum LibcVersion {
53 Manylinux { major: u32, minor: u32 },
54 Musllinux { major: u32, minor: u32 },
55}
56
57#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
58pub enum Libc {
59 Some(target_lexicon::Environment),
60 None,
61}
62
63impl Libc {
64 pub fn from_env() -> Result<Self, crate::Error> {
65 match env::consts::OS {
66 "linux" => {
67 if let Ok(libc) = env::var(EnvVars::UV_LIBC) {
68 if !libc.is_empty() {
69 return Self::from_str(&libc);
70 }
71 }
72
73 Ok(Self::Some(match detect_linux_libc()? {
74 LibcVersion::Manylinux { .. } => match env::consts::ARCH {
75 "arm" | "armv5te" | "armv7" => {
79 match detect_hardware_floating_point_support() {
80 Ok(true) => target_lexicon::Environment::Gnueabihf,
81 Ok(false) => target_lexicon::Environment::Gnueabi,
82 Err(_) => target_lexicon::Environment::Gnu,
83 }
84 }
85 _ => target_lexicon::Environment::Gnu,
86 },
87 LibcVersion::Musllinux { .. } => target_lexicon::Environment::Musl,
88 }))
89 }
90 "windows" | "macos" => Ok(Self::None),
91 _ => Ok(Self::None),
93 }
94 }
95
96 pub fn is_musl(&self) -> bool {
97 matches!(self, Self::Some(target_lexicon::Environment::Musl))
98 }
99}
100
101impl FromStr for Libc {
102 type Err = crate::Error;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 match s {
106 "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
107 "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
108 "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
109 "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
110 "none" => Ok(Self::None),
111 _ => Err(crate::Error::UnknownLibc(s.to_string())),
112 }
113 }
114}
115
116impl Display for Libc {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 Self::Some(env) => write!(f, "{env}"),
120 Self::None => write!(f, "none"),
121 }
122 }
123}
124
125impl From<&uv_platform_tags::Os> for Libc {
126 fn from(value: &uv_platform_tags::Os) -> Self {
127 match value {
128 uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
129 uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
130 uv_platform_tags::Os::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
131 _ => Self::None,
132 }
133 }
134}
135
136pub(crate) fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
144 let ld_path = find_ld_path()?;
145 trace!("Found `ld` path: {}", ld_path.user_display());
146
147 match detect_musl_version(&ld_path) {
148 Ok(os) => return Ok(os),
149 Err(err) => {
150 trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
151 }
152 }
153 match detect_linux_libc_from_ld_symlink(&ld_path) {
154 Ok(os) => return Ok(os),
155 Err(err) => {
156 trace!(
157 "Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
158 );
159 }
160 }
161 match detect_glibc_version_from_ld(&ld_path) {
162 Ok(os_version) => return Ok(os_version),
163 Err(err) => {
164 trace!(
165 "Tried to find glibc version from `{} --version`, but failed: {}",
166 ld_path.simplified_display(),
167 err
168 );
169 }
170 }
171 Err(LibcDetectionError::NoLibcFound)
172}
173
174fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
176 let output = Command::new(ld_so)
177 .args(["--version"])
178 .output()
179 .map_err(|err| LibcDetectionError::FailedToRun {
180 libc: "glibc",
181 program: format!("{} --version", ld_so.user_display()),
182 err,
183 })?;
184 if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
185 return Ok(os);
186 }
187 if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
188 return Ok(os);
189 }
190 Err(LibcDetectionError::InvalidLdSoOutputGnu(
191 ld_so.to_path_buf(),
192 ))
193}
194
195fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
199 static RE: LazyLock<Regex> =
200 LazyLock::new(|| Regex::new(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)").unwrap());
201
202 let output = String::from_utf8_lossy(output);
203 trace!("{kind} output from `ld.so --version`: {output:?}");
204 let (_, [version]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
205 let mut parsed_ints = version.split('.').map(str::parse).fuse();
207 let major = parsed_ints.next()?.ok()?;
208 let minor = parsed_ints.next()?.ok()?;
209 trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
210 Some(LibcVersion::Manylinux { major, minor })
211}
212
213fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
214 static RE: LazyLock<Regex> =
215 LazyLock::new(|| Regex::new(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$").unwrap());
216
217 let ld_path = fs::read_link(path)?;
218 let filename = ld_path
219 .file_name()
220 .ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
221 .to_string_lossy();
222 let (_, [major, minor]) = RE
223 .captures(&filename)
224 .map(|c| c.extract())
225 .ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
226 let major = major.parse().expect("valid major version");
229 let minor = minor.parse().expect("valid minor version");
230 Ok(LibcVersion::Manylinux { major, minor })
231}
232
233fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
243 let ld_path = ld_path.as_ref();
244 let output = Command::new(ld_path)
245 .stdout(Stdio::null())
246 .stderr(Stdio::piped())
247 .output()
248 .map_err(|err| LibcDetectionError::FailedToRun {
249 libc: "musl",
250 program: ld_path.to_string_lossy().to_string(),
251 err,
252 })?;
253
254 if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
255 return Ok(os);
256 }
257 if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
258 return Ok(os);
259 }
260 Err(LibcDetectionError::InvalidLdSoOutputMusl(
261 ld_path.to_path_buf(),
262 ))
263}
264
265fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
269 static RE: LazyLock<Regex> =
270 LazyLock::new(|| Regex::new(r"Version ([0-9]{1,4})\.([0-9]{1,4})").unwrap());
271
272 let output = String::from_utf8_lossy(output);
273 trace!("{kind} output from `ld`: {output:?}");
274 let (_, [major, minor]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
275 let major = major.parse().expect("valid major version");
278 let minor = minor.parse().expect("valid minor version");
279 trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
280 Some(LibcVersion::Musllinux { major, minor })
281}
282
283fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
285 let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
295 let mut found_anything = false;
296 for path in attempts {
297 if std::fs::exists(path).ok() == Some(true) {
298 found_anything = true;
299 if let Some(ld_path) = find_ld_path_at(path) {
300 return Ok(ld_path);
301 }
302 }
303 }
304 let attempts_string = attempts.join(", ");
305 if !found_anything {
306 Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
310 } else {
311 Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
312 }
313}
314
315fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
319 let path = path.as_ref();
320 let buffer = fs::read(path).ok()?;
322 let elf = match Elf::parse(&buffer) {
323 Ok(elf) => elf,
324 Err(err) => {
325 trace!(
326 "Could not parse ELF file at `{}`: `{}`",
327 path.user_display(),
328 err
329 );
330 return None;
331 }
332 };
333 let Some(elf_interpreter) = elf.interpreter else {
334 trace!(
335 "Couldn't find ELF interpreter path from {}",
336 path.user_display()
337 );
338 return None;
339 };
340
341 Some(PathBuf::from(elf_interpreter))
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use indoc::indoc;
348
349 #[test]
350 fn parse_ld_so_output() {
351 let ver_str = glibc_ld_output_to_version(
352 "stdout",
353 indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
354 Copyright (C) 2024 Free Software Foundation, Inc.
355 This is free software; see the source for copying conditions.
356 There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
357 PARTICULAR PURPOSE.
358 "},
359 )
360 .unwrap();
361 assert_eq!(
362 ver_str,
363 LibcVersion::Manylinux {
364 major: 2,
365 minor: 39
366 }
367 );
368 }
369
370 #[test]
371 fn parse_musl_ld_output() {
372 let output = b"\
378musl libc (x86_64)
379Version 1.2.4_git20230717
380Dynamic Program Loader
381Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
382 ";
383 let got = musl_ld_output_to_version("stderr", output).unwrap();
384 assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
385 }
386}