1use crate::{Arch, 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 target_lexicon::Endianness;
18use tracing::trace;
19use uv_fs::Simplified;
20use uv_static::EnvVars;
21
22#[derive(Debug, thiserror::Error)]
23pub enum LibcDetectionError {
24 #[error(
25 "Could not detect either glibc version nor musl libc version, at least one of which is required"
26 )]
27 NoLibcFound,
28 #[error("Failed to get base name of symbolic link path {0}")]
29 MissingBasePath(PathBuf),
30 #[error("Failed to find glibc version in the filename of linker: `{0}`")]
31 GlibcExtractionMismatch(PathBuf),
32 #[error("Failed to determine {libc} version by running: `{program}`")]
33 FailedToRun {
34 libc: &'static str,
35 program: String,
36 #[source]
37 err: io::Error,
38 },
39 #[error("Could not find glibc version in output of: `{0} --version`")]
40 InvalidLdSoOutputGnu(PathBuf),
41 #[error("Could not find musl version in output of: `{0}`")]
42 InvalidLdSoOutputMusl(PathBuf),
43 #[error("Could not read ELF interpreter from any of the following paths: {0}")]
44 CoreBinaryParsing(String),
45 #[error("Failed to find any common binaries to determine libc from: {0}")]
46 NoCommonBinariesFound(String),
47 #[error("Failed to determine libc")]
48 Io(#[from] io::Error),
49}
50
51#[derive(Debug, PartialEq, Eq)]
53pub enum LibcVersion {
54 Manylinux { major: u32, minor: u32 },
55 Musllinux { major: u32, minor: u32 },
56}
57
58#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
59pub enum Libc {
60 Some(target_lexicon::Environment),
61 None,
62}
63
64impl Libc {
65 pub fn from_env() -> Result<Self, crate::Error> {
66 match env::consts::OS {
67 "linux" => {
68 if let Ok(libc) = env::var(EnvVars::UV_LIBC) {
69 if !libc.is_empty() {
70 return Self::from_str(&libc);
71 }
72 }
73
74 Ok(Self::Some(match detect_linux_libc()? {
75 LibcVersion::Manylinux { .. } => match env::consts::ARCH {
76 "arm" | "armv5te" | "armv7" => {
77 match detect_hardware_floating_point_support() {
78 Ok(true) => target_lexicon::Environment::Gnueabihf,
79 Ok(false) => target_lexicon::Environment::Gnueabi,
80 Err(_) => target_lexicon::Environment::Gnu,
81 }
82 }
83 _ => target_lexicon::Environment::Gnu,
84 },
85 LibcVersion::Musllinux { .. } => match env::consts::ARCH {
86 "arm" | "armv5te" | "armv7" => {
87 match detect_hardware_floating_point_support() {
88 Ok(true) => target_lexicon::Environment::Musleabihf,
89 Ok(false) => target_lexicon::Environment::Musleabi,
90 Err(_) => target_lexicon::Environment::Musl,
91 }
92 }
93 _ => target_lexicon::Environment::Musl,
94 },
95 }))
96 }
97 "windows" | "macos" => Ok(Self::None),
98 _ => Ok(Self::None),
100 }
101 }
102
103 pub fn is_musl(&self) -> bool {
104 matches!(
105 self,
106 Self::Some(
107 target_lexicon::Environment::Musl
108 | target_lexicon::Environment::Musleabi
109 | target_lexicon::Environment::Musleabihf
110 )
111 )
112 }
113}
114
115impl FromStr for Libc {
116 type Err = crate::Error;
117
118 fn from_str(s: &str) -> Result<Self, Self::Err> {
119 match s {
120 "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
121 "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
122 "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
123 "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
124 "musleabi" => Ok(Self::Some(target_lexicon::Environment::Musleabi)),
125 "musleabihf" => Ok(Self::Some(target_lexicon::Environment::Musleabihf)),
126 "none" => Ok(Self::None),
127 _ => Err(crate::Error::UnknownLibc(s.to_string())),
128 }
129 }
130}
131
132impl Display for Libc {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 Self::Some(env) => write!(f, "{env}"),
136 Self::None => write!(f, "none"),
137 }
138 }
139}
140
141impl From<&uv_platform_tags::Os> for Libc {
142 fn from(value: &uv_platform_tags::Os) -> Self {
143 match value {
144 uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
145 uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
146 uv_platform_tags::Os::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
147 _ => Self::None,
148 }
149 }
150}
151
152pub(crate) fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
160 let ld_path = find_ld_path()?;
161 trace!("Found `ld` path: {}", ld_path.user_display());
162
163 match detect_musl_version(&ld_path) {
164 Ok(os) => return Ok(os),
165 Err(err) => {
166 trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
167 }
168 }
169 match detect_linux_libc_from_ld_symlink(&ld_path) {
170 Ok(os) => return Ok(os),
171 Err(err) => {
172 trace!(
173 "Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
174 );
175 }
176 }
177 match detect_glibc_version_from_ld(&ld_path) {
178 Ok(os_version) => return Ok(os_version),
179 Err(err) => {
180 trace!(
181 "Tried to find glibc version from `{} --version`, but failed: {}",
182 ld_path.simplified_display(),
183 err
184 );
185 }
186 }
187 Err(LibcDetectionError::NoLibcFound)
188}
189
190fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
192 let output = Command::new(ld_so)
193 .args(["--version"])
194 .output()
195 .map_err(|err| LibcDetectionError::FailedToRun {
196 libc: "glibc",
197 program: format!("{} --version", ld_so.user_display()),
198 err,
199 })?;
200 if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
201 return Ok(os);
202 }
203 if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
204 return Ok(os);
205 }
206 Err(LibcDetectionError::InvalidLdSoOutputGnu(
207 ld_so.to_path_buf(),
208 ))
209}
210
211fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
215 static RE: LazyLock<Regex> =
216 LazyLock::new(|| Regex::new(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)").unwrap());
217
218 let output = String::from_utf8_lossy(output);
219 trace!("{kind} output from `ld.so --version`: {output:?}");
220 let (_, [version]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
221 let mut parsed_ints = version.split('.').map(str::parse).fuse();
223 let major = parsed_ints.next()?.ok()?;
224 let minor = parsed_ints.next()?.ok()?;
225 trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
226 Some(LibcVersion::Manylinux { major, minor })
227}
228
229fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
230 static RE: LazyLock<Regex> =
231 LazyLock::new(|| Regex::new(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$").unwrap());
232
233 let ld_path = fs::read_link(path)?;
234 let filename = ld_path
235 .file_name()
236 .ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
237 .to_string_lossy();
238 let (_, [major, minor]) = RE
239 .captures(&filename)
240 .map(|c| c.extract())
241 .ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
242 let major = major.parse().expect("valid major version");
245 let minor = minor.parse().expect("valid minor version");
246 Ok(LibcVersion::Manylinux { major, minor })
247}
248
249fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
259 let ld_path = ld_path.as_ref();
260 let output = Command::new(ld_path)
261 .stdout(Stdio::null())
262 .stderr(Stdio::piped())
263 .output()
264 .map_err(|err| LibcDetectionError::FailedToRun {
265 libc: "musl",
266 program: ld_path.to_string_lossy().to_string(),
267 err,
268 })?;
269
270 if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
271 return Ok(os);
272 }
273 if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
274 return Ok(os);
275 }
276 Err(LibcDetectionError::InvalidLdSoOutputMusl(
277 ld_path.to_path_buf(),
278 ))
279}
280
281fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
285 static RE: LazyLock<Regex> =
286 LazyLock::new(|| Regex::new(r"Version ([0-9]{1,4})\.([0-9]{1,4})").unwrap());
287
288 let output = String::from_utf8_lossy(output);
289 trace!("{kind} output from `ld`: {output:?}");
290 let (_, [major, minor]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
291 let major = major.parse().expect("valid major version");
294 let minor = minor.parse().expect("valid minor version");
295 trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
296 Some(LibcVersion::Musllinux { major, minor })
297}
298
299fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
301 let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
311 let mut found_anything = false;
312 for path in attempts {
313 if std::fs::exists(path).ok() == Some(true) {
314 found_anything = true;
315 if let Some(ld_path) = find_ld_path_at(path) {
316 return Ok(ld_path);
317 }
318 }
319 }
320
321 if let Some(ld_path) = find_ld_path_from_filesystem() {
328 return Ok(ld_path);
329 }
330
331 let attempts_string = attempts.join(", ");
332 if !found_anything {
333 Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
337 } else {
338 Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
339 }
340}
341
342fn find_ld_path_from_filesystem() -> Option<PathBuf> {
347 find_ld_path_from_root_and_arch(Path::new("/"), Arch::from_env())
348}
349
350fn find_ld_path_from_root_and_arch(root: &Path, architecture: Arch) -> Option<PathBuf> {
351 let Some(candidates) = dynamic_linker_candidates(architecture) else {
352 trace!("No known dynamic linker paths for architecture `{architecture}`");
353 return None;
354 };
355
356 let paths = candidates
357 .iter()
358 .map(|candidate| root.join(candidate.trim_start_matches('/')))
359 .collect::<Vec<_>>();
360
361 for path in &paths {
362 if std::fs::exists(path).ok() == Some(true) {
363 trace!(
364 "Found dynamic linker on filesystem: {}",
365 path.user_display()
366 );
367 return Some(path.clone());
368 }
369 }
370
371 trace!(
372 "Could not find dynamic linker in any expected filesystem path for architecture `{}`: {}",
373 architecture,
374 paths
375 .iter()
376 .map(|path| path.user_display().to_string())
377 .collect::<Vec<_>>()
378 .join(", ")
379 );
380 None
381}
382
383fn dynamic_linker_candidates(architecture: Arch) -> Option<&'static [&'static str]> {
386 let family = architecture.family();
387
388 match family {
389 target_lexicon::Architecture::X86_64 => {
390 Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1"])
391 }
392 target_lexicon::Architecture::X86_32(_) => {
393 Some(&["/lib/ld-linux.so.2", "/lib/ld-musl-i386.so.1"])
394 }
395 target_lexicon::Architecture::Aarch64(_) => match family.endianness().ok()? {
396 Endianness::Little => {
397 Some(&["/lib/ld-linux-aarch64.so.1", "/lib/ld-musl-aarch64.so.1"])
398 }
399 Endianness::Big => Some(&[
400 "/lib/ld-linux-aarch64_be.so.1",
401 "/lib/ld-musl-aarch64_be.so.1",
402 ]),
403 },
404 target_lexicon::Architecture::Arm(_) => match family.endianness().ok()? {
405 Endianness::Little => Some(&[
406 "/lib/ld-linux-armhf.so.3",
407 "/lib/ld-linux.so.3",
408 "/lib/ld-musl-armhf.so.1",
409 "/lib/ld-musl-arm.so.1",
410 ]),
411 Endianness::Big => Some(&[
412 "/lib/ld-linux-armhf.so.3",
413 "/lib/ld-linux.so.3",
414 "/lib/ld-musl-armebhf.so.1",
415 "/lib/ld-musl-armeb.so.1",
416 ]),
417 },
418 target_lexicon::Architecture::Powerpc64 => {
419 Some(&["/lib64/ld64.so.1", "/lib/ld-musl-powerpc64.so.1"])
420 }
421 target_lexicon::Architecture::Powerpc64le => {
422 Some(&["/lib64/ld64.so.2", "/lib/ld-musl-powerpc64le.so.1"])
423 }
424 target_lexicon::Architecture::S390x => Some(&["/lib/ld64.so.1", "/lib/ld-musl-s390x.so.1"]),
425 target_lexicon::Architecture::Riscv64(_) => Some(&[
426 "/lib/ld-linux-riscv64-lp64d.so.1",
427 "/lib/ld-linux-riscv64-lp64.so.1",
428 "/lib/ld-musl-riscv64.so.1",
429 "/lib/ld-musl-riscv64-sp.so.1",
430 "/lib/ld-musl-riscv64-sf.so.1",
431 ]),
432 target_lexicon::Architecture::LoongArch64 => Some(&[
433 "/lib64/ld-linux-loongarch-lp64d.so.1",
434 "/lib64/ld-linux-loongarch-lp64s.so.1",
435 "/lib/ld-musl-loongarch64.so.1",
436 "/lib/ld-musl-loongarch64-sp.so.1",
437 "/lib/ld-musl-loongarch64-sf.so.1",
438 ]),
439 _ => None,
440 }
441}
442
443fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
447 let path = path.as_ref();
448 let buffer = fs::read(path).ok()?;
450 let elf = match Elf::parse(&buffer) {
451 Ok(elf) => elf,
452 Err(err) => {
453 trace!(
454 "Could not parse ELF file at `{}`: `{}`",
455 path.user_display(),
456 err
457 );
458 return None;
459 }
460 };
461 let Some(elf_interpreter) = elf.interpreter else {
462 trace!(
463 "Couldn't find ELF interpreter path from {}",
464 path.user_display()
465 );
466 return None;
467 };
468
469 Some(PathBuf::from(elf_interpreter))
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use indoc::indoc;
476 use tempfile::tempdir;
477
478 #[test]
479 fn parse_ld_so_output() {
480 let ver_str = glibc_ld_output_to_version(
481 "stdout",
482 indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
483 Copyright (C) 2024 Free Software Foundation, Inc.
484 This is free software; see the source for copying conditions.
485 There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
486 PARTICULAR PURPOSE.
487 "},
488 )
489 .unwrap();
490 assert_eq!(
491 ver_str,
492 LibcVersion::Manylinux {
493 major: 2,
494 minor: 39
495 }
496 );
497 }
498
499 #[test]
500 fn parse_musl_ld_output() {
501 let output = b"\
507musl libc (x86_64)
508Version 1.2.4_git20230717
509Dynamic Program Loader
510Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
511 ";
512 let got = musl_ld_output_to_version("stderr", output).unwrap();
513 assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
514 }
515
516 #[test]
517 fn dynamic_linker_candidates_prefer_glibc_before_musl() {
518 assert_eq!(
519 dynamic_linker_candidates(Arch::from_str("x86_64").unwrap()),
520 Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1",][..])
521 );
522 }
523
524 #[test]
525 fn find_ld_path_from_root_and_arch_returns_glibc_when_only_glibc_is_present() {
526 let root = tempdir().unwrap();
527 let ld_path = root.path().join("lib64/ld-linux-x86-64.so.2");
528 fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
529 fs::write(&ld_path, "").unwrap();
530
531 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
532
533 assert_eq!(got, Some(ld_path));
534 }
535
536 #[test]
537 fn find_ld_path_from_root_and_arch_returns_musl_when_only_musl_is_present() {
538 let root = tempdir().unwrap();
539 let ld_path = root.path().join("lib/ld-musl-x86_64.so.1");
540 fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
541 fs::write(&ld_path, "").unwrap();
542
543 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
544
545 assert_eq!(got, Some(ld_path));
546 }
547
548 #[test]
549 fn find_ld_path_from_root_and_arch_returns_glibc_when_both_linkers_are_present() {
550 let root = tempdir().unwrap();
551 let glibc_path = root.path().join("lib64/ld-linux-x86-64.so.2");
552 let musl_path = root.path().join("lib/ld-musl-x86_64.so.1");
553 fs::create_dir_all(glibc_path.parent().unwrap()).unwrap();
554 fs::create_dir_all(musl_path.parent().unwrap()).unwrap();
555 fs::write(&glibc_path, "").unwrap();
556 fs::write(&musl_path, "").unwrap();
557
558 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
559
560 assert_eq!(got, Some(glibc_path));
561 }
562
563 #[test]
564 fn find_ld_path_from_root_and_arch_returns_none_when_neither_linker_is_present() {
565 let root = tempdir().unwrap();
566
567 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
568
569 assert_eq!(got, None);
570 }
571}