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(crate) 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 { .. } | uv_platform_tags::Os::PyEmscripten { .. } => {
147 Self::Some(target_lexicon::Environment::Musl)
148 }
149 _ => Self::None,
150 }
151 }
152}
153
154fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
162 let ld_path = find_ld_path()?;
163 trace!("Found `ld` path: {}", ld_path.user_display());
164
165 match detect_musl_version(&ld_path) {
166 Ok(os) => return Ok(os),
167 Err(err) => {
168 trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
169 }
170 }
171 match detect_linux_libc_from_ld_symlink(&ld_path) {
172 Ok(os) => return Ok(os),
173 Err(err) => {
174 trace!(
175 "Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
176 );
177 }
178 }
179 match detect_glibc_version_from_ld(&ld_path) {
180 Ok(os_version) => return Ok(os_version),
181 Err(err) => {
182 trace!(
183 "Tried to find glibc version from `{} --version`, but failed: {}",
184 ld_path.simplified_display(),
185 err
186 );
187 }
188 }
189 Err(LibcDetectionError::NoLibcFound)
190}
191
192fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
194 let output = Command::new(ld_so)
195 .args(["--version"])
196 .output()
197 .map_err(|err| LibcDetectionError::FailedToRun {
198 libc: "glibc",
199 program: format!("{} --version", ld_so.user_display()),
200 err,
201 })?;
202 if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
203 return Ok(os);
204 }
205 if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
206 return Ok(os);
207 }
208 Err(LibcDetectionError::InvalidLdSoOutputGnu(
209 ld_so.to_path_buf(),
210 ))
211}
212
213fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
217 static RE: LazyLock<Regex> =
218 LazyLock::new(|| Regex::new(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)").unwrap());
219
220 let output = String::from_utf8_lossy(output);
221 trace!("{kind} output from `ld.so --version`: {output:?}");
222 let (_, [version]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
223 let mut parsed_ints = version.split('.').map(str::parse).fuse();
225 let major = parsed_ints.next()?.ok()?;
226 let minor = parsed_ints.next()?.ok()?;
227 trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
228 Some(LibcVersion::Manylinux { major, minor })
229}
230
231fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
232 static RE: LazyLock<Regex> =
233 LazyLock::new(|| Regex::new(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$").unwrap());
234
235 let ld_path = fs::read_link(path)?;
236 let filename = ld_path
237 .file_name()
238 .ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
239 .to_string_lossy();
240 let (_, [major, minor]) = RE
241 .captures(&filename)
242 .map(|c| c.extract())
243 .ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
244 let major = major.parse().expect("valid major version");
247 let minor = minor.parse().expect("valid minor version");
248 Ok(LibcVersion::Manylinux { major, minor })
249}
250
251fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
261 let ld_path = ld_path.as_ref();
262 let output = Command::new(ld_path)
263 .stdout(Stdio::null())
264 .stderr(Stdio::piped())
265 .output()
266 .map_err(|err| LibcDetectionError::FailedToRun {
267 libc: "musl",
268 program: ld_path.to_string_lossy().to_string(),
269 err,
270 })?;
271
272 if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
273 return Ok(os);
274 }
275 if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
276 return Ok(os);
277 }
278 Err(LibcDetectionError::InvalidLdSoOutputMusl(
279 ld_path.to_path_buf(),
280 ))
281}
282
283fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
287 static RE: LazyLock<Regex> =
288 LazyLock::new(|| Regex::new(r"Version ([0-9]{1,4})\.([0-9]{1,4})").unwrap());
289
290 let output = String::from_utf8_lossy(output);
291 trace!("{kind} output from `ld`: {output:?}");
292 let (_, [major, minor]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
293 let major = major.parse().expect("valid major version");
296 let minor = minor.parse().expect("valid minor version");
297 trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
298 Some(LibcVersion::Musllinux { major, minor })
299}
300
301fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
303 let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
313 let mut found_anything = false;
314 for path in attempts {
315 if std::fs::exists(path).ok() == Some(true) {
316 found_anything = true;
317 if let Some(ld_path) = find_ld_path_at(path) {
318 return Ok(ld_path);
319 }
320 }
321 }
322
323 if let Some(ld_path) = find_ld_path_from_filesystem() {
330 return Ok(ld_path);
331 }
332
333 let attempts_string = attempts.join(", ");
334 if !found_anything {
335 Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
339 } else {
340 Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
341 }
342}
343
344fn find_ld_path_from_filesystem() -> Option<PathBuf> {
349 find_ld_path_from_root_and_arch(Path::new("/"), Arch::from_env())
350}
351
352fn find_ld_path_from_root_and_arch(root: &Path, architecture: Arch) -> Option<PathBuf> {
353 let Some(candidates) = dynamic_linker_candidates(architecture) else {
354 trace!("No known dynamic linker paths for architecture `{architecture}`");
355 return None;
356 };
357
358 let paths = candidates
359 .iter()
360 .map(|candidate| root.join(candidate.trim_start_matches('/')))
361 .collect::<Vec<_>>();
362
363 for path in &paths {
364 if std::fs::exists(path).ok() == Some(true) {
365 trace!(
366 "Found dynamic linker on filesystem: {}",
367 path.user_display()
368 );
369 return Some(path.clone());
370 }
371 }
372
373 trace!(
374 "Could not find dynamic linker in any expected filesystem path for architecture `{}`: {}",
375 architecture,
376 paths
377 .iter()
378 .map(|path| path.user_display().to_string())
379 .collect::<Vec<_>>()
380 .join(", ")
381 );
382 None
383}
384
385fn dynamic_linker_candidates(architecture: Arch) -> Option<&'static [&'static str]> {
388 let family = architecture.family();
389
390 match family {
391 target_lexicon::Architecture::X86_64 => {
392 Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1"])
393 }
394 target_lexicon::Architecture::X86_32(_) => {
395 Some(&["/lib/ld-linux.so.2", "/lib/ld-musl-i386.so.1"])
396 }
397 target_lexicon::Architecture::Aarch64(_) => match family.endianness().ok()? {
398 Endianness::Little => {
399 Some(&["/lib/ld-linux-aarch64.so.1", "/lib/ld-musl-aarch64.so.1"])
400 }
401 Endianness::Big => Some(&[
402 "/lib/ld-linux-aarch64_be.so.1",
403 "/lib/ld-musl-aarch64_be.so.1",
404 ]),
405 },
406 target_lexicon::Architecture::Arm(_) => match family.endianness().ok()? {
407 Endianness::Little => Some(&[
408 "/lib/ld-linux-armhf.so.3",
409 "/lib/ld-linux.so.3",
410 "/lib/ld-musl-armhf.so.1",
411 "/lib/ld-musl-arm.so.1",
412 ]),
413 Endianness::Big => Some(&[
414 "/lib/ld-linux-armhf.so.3",
415 "/lib/ld-linux.so.3",
416 "/lib/ld-musl-armebhf.so.1",
417 "/lib/ld-musl-armeb.so.1",
418 ]),
419 },
420 target_lexicon::Architecture::Powerpc64 => {
421 Some(&["/lib64/ld64.so.1", "/lib/ld-musl-powerpc64.so.1"])
422 }
423 target_lexicon::Architecture::Powerpc64le => {
424 Some(&["/lib64/ld64.so.2", "/lib/ld-musl-powerpc64le.so.1"])
425 }
426 target_lexicon::Architecture::S390x => Some(&["/lib/ld64.so.1", "/lib/ld-musl-s390x.so.1"]),
427 target_lexicon::Architecture::Riscv64(_) => Some(&[
428 "/lib/ld-linux-riscv64-lp64d.so.1",
429 "/lib/ld-linux-riscv64-lp64.so.1",
430 "/lib/ld-musl-riscv64.so.1",
431 "/lib/ld-musl-riscv64-sp.so.1",
432 "/lib/ld-musl-riscv64-sf.so.1",
433 ]),
434 target_lexicon::Architecture::LoongArch64 => Some(&[
435 "/lib64/ld-linux-loongarch-lp64d.so.1",
436 "/lib64/ld-linux-loongarch-lp64s.so.1",
437 "/lib/ld-musl-loongarch64.so.1",
438 "/lib/ld-musl-loongarch64-sp.so.1",
439 "/lib/ld-musl-loongarch64-sf.so.1",
440 ]),
441 _ => None,
442 }
443}
444
445fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
449 let path = path.as_ref();
450 let buffer = fs::read(path).ok()?;
452 let elf = match Elf::parse(&buffer) {
453 Ok(elf) => elf,
454 Err(err) => {
455 trace!(
456 "Could not parse ELF file at `{}`: `{}`",
457 path.user_display(),
458 err
459 );
460 return None;
461 }
462 };
463 let Some(elf_interpreter) = elf.interpreter else {
464 trace!(
465 "Couldn't find ELF interpreter path from {}",
466 path.user_display()
467 );
468 return None;
469 };
470
471 Some(PathBuf::from(elf_interpreter))
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use indoc::indoc;
478 use tempfile::tempdir;
479
480 #[test]
481 fn parse_ld_so_output() {
482 let ver_str = glibc_ld_output_to_version(
483 "stdout",
484 indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
485 Copyright (C) 2024 Free Software Foundation, Inc.
486 This is free software; see the source for copying conditions.
487 There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
488 PARTICULAR PURPOSE.
489 "},
490 )
491 .unwrap();
492 assert_eq!(
493 ver_str,
494 LibcVersion::Manylinux {
495 major: 2,
496 minor: 39
497 }
498 );
499 }
500
501 #[test]
502 fn parse_musl_ld_output() {
503 let output = b"\
509musl libc (x86_64)
510Version 1.2.4_git20230717
511Dynamic Program Loader
512Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
513 ";
514 let got = musl_ld_output_to_version("stderr", output).unwrap();
515 assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
516 }
517
518 #[test]
519 fn dynamic_linker_candidates_prefer_glibc_before_musl() {
520 assert_eq!(
521 dynamic_linker_candidates(Arch::from_str("x86_64").unwrap()),
522 Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1",][..])
523 );
524 }
525
526 #[test]
527 fn find_ld_path_from_root_and_arch_returns_glibc_when_only_glibc_is_present() {
528 let root = tempdir().unwrap();
529 let ld_path = root.path().join("lib64/ld-linux-x86-64.so.2");
530 fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
531 fs::write(&ld_path, "").unwrap();
532
533 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
534
535 assert_eq!(got, Some(ld_path));
536 }
537
538 #[test]
539 fn find_ld_path_from_root_and_arch_returns_musl_when_only_musl_is_present() {
540 let root = tempdir().unwrap();
541 let ld_path = root.path().join("lib/ld-musl-x86_64.so.1");
542 fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
543 fs::write(&ld_path, "").unwrap();
544
545 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
546
547 assert_eq!(got, Some(ld_path));
548 }
549
550 #[test]
551 fn find_ld_path_from_root_and_arch_returns_glibc_when_both_linkers_are_present() {
552 let root = tempdir().unwrap();
553 let glibc_path = root.path().join("lib64/ld-linux-x86-64.so.2");
554 let musl_path = root.path().join("lib/ld-musl-x86_64.so.1");
555 fs::create_dir_all(glibc_path.parent().unwrap()).unwrap();
556 fs::create_dir_all(musl_path.parent().unwrap()).unwrap();
557 fs::write(&glibc_path, "").unwrap();
558 fs::write(&musl_path, "").unwrap();
559
560 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
561
562 assert_eq!(got, Some(glibc_path));
563 }
564
565 #[test]
566 fn find_ld_path_from_root_and_arch_returns_none_when_neither_linker_is_present() {
567 let root = tempdir().unwrap();
568
569 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
570
571 assert_eq!(got, None);
572 }
573}