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::{env, fmt};
16use target_lexicon::Endianness;
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)]
52enum 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(crate) 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 && !libc.is_empty() =>
69 {
70 Self::from_str(&libc)
71 }
72 "linux" => Ok(Self::Some(match detect_linux_libc()? {
73 LibcVersion::Manylinux { .. } => match env::consts::ARCH {
74 "arm" | "armv5te" | "armv7" => match detect_hardware_floating_point_support() {
75 Ok(true) => target_lexicon::Environment::Gnueabihf,
76 Ok(false) => target_lexicon::Environment::Gnueabi,
77 Err(_) => target_lexicon::Environment::Gnu,
78 },
79 _ => target_lexicon::Environment::Gnu,
80 },
81 LibcVersion::Musllinux { .. } => match env::consts::ARCH {
82 "arm" | "armv5te" | "armv7" => match detect_hardware_floating_point_support() {
83 Ok(true) => target_lexicon::Environment::Musleabihf,
84 Ok(false) => target_lexicon::Environment::Musleabi,
85 Err(_) => target_lexicon::Environment::Musl,
86 },
87 _ => 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!(
98 self,
99 Self::Some(
100 target_lexicon::Environment::Musl
101 | target_lexicon::Environment::Musleabi
102 | target_lexicon::Environment::Musleabihf
103 )
104 )
105 }
106}
107
108impl FromStr for Libc {
109 type Err = crate::Error;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 match s {
113 "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
114 "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
115 "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
116 "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
117 "musleabi" => Ok(Self::Some(target_lexicon::Environment::Musleabi)),
118 "musleabihf" => Ok(Self::Some(target_lexicon::Environment::Musleabihf)),
119 "none" => Ok(Self::None),
120 _ => Err(crate::Error::UnknownLibc(s.to_string())),
121 }
122 }
123}
124
125impl Display for Libc {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 match self {
128 Self::Some(env) => write!(f, "{env}"),
129 Self::None => write!(f, "none"),
130 }
131 }
132}
133
134impl From<&uv_platform_tags::Os> for Libc {
135 fn from(value: &uv_platform_tags::Os) -> Self {
136 match value {
137 uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
138 uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
139 uv_platform_tags::Os::Pyodide { .. } | uv_platform_tags::Os::PyEmscripten { .. } => {
140 Self::Some(target_lexicon::Environment::Musl)
141 }
142 _ => Self::None,
143 }
144 }
145}
146
147fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
155 let ld_path = find_ld_path()?;
156 trace!("Found `ld` path: {}", ld_path.user_display());
157
158 match detect_musl_version(&ld_path) {
159 Ok(os) => return Ok(os),
160 Err(err) => {
161 trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
162 }
163 }
164 match detect_linux_libc_from_ld_symlink(&ld_path) {
165 Ok(os) => return Ok(os),
166 Err(err) => {
167 trace!(
168 "Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
169 );
170 }
171 }
172 match detect_glibc_version_from_ld(&ld_path) {
173 Ok(os_version) => return Ok(os_version),
174 Err(err) => {
175 trace!(
176 "Tried to find glibc version from `{} --version`, but failed: {}",
177 ld_path.simplified_display(),
178 err
179 );
180 }
181 }
182 Err(LibcDetectionError::NoLibcFound)
183}
184
185fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
187 let output = Command::new(ld_so)
188 .args(["--version"])
189 .output()
190 .map_err(|err| LibcDetectionError::FailedToRun {
191 libc: "glibc",
192 program: format!("{} --version", ld_so.user_display()),
193 err,
194 })?;
195 if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
196 return Ok(os);
197 }
198 if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
199 return Ok(os);
200 }
201 Err(LibcDetectionError::InvalidLdSoOutputGnu(
202 ld_so.to_path_buf(),
203 ))
204}
205
206fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
210 let output = String::from_utf8_lossy(output);
211 trace!("{kind} output from `ld.so --version`: {output:?}");
212 let (_, [version]) = regex!(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)")
213 .captures(output.as_ref())
214 .map(|c| c.extract())?;
215 let mut parsed_ints = version.split('.').map(str::parse).fuse();
217 let major = parsed_ints.next()?.ok()?;
218 let minor = parsed_ints.next()?.ok()?;
219 trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
220 Some(LibcVersion::Manylinux { major, minor })
221}
222
223fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
224 let ld_path = fs::read_link(path)?;
225 let filename = ld_path
226 .file_name()
227 .ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
228 .to_string_lossy();
229 let (_, [major, minor]) = regex!(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$")
230 .captures(&filename)
231 .map(|c| c.extract())
232 .ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
233 let major = major.parse().expect("valid major version");
236 let minor = minor.parse().expect("valid minor version");
237 Ok(LibcVersion::Manylinux { major, minor })
238}
239
240fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
250 let ld_path = ld_path.as_ref();
251 let output = Command::new(ld_path)
252 .stdout(Stdio::null())
253 .stderr(Stdio::piped())
254 .output()
255 .map_err(|err| LibcDetectionError::FailedToRun {
256 libc: "musl",
257 program: ld_path.to_string_lossy().to_string(),
258 err,
259 })?;
260
261 if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
262 return Ok(os);
263 }
264 if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
265 return Ok(os);
266 }
267 Err(LibcDetectionError::InvalidLdSoOutputMusl(
268 ld_path.to_path_buf(),
269 ))
270}
271
272fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
276 let output = String::from_utf8_lossy(output);
277 trace!("{kind} output from `ld`: {output:?}");
278 let (_, [major, minor]) = regex!(r"Version ([0-9]{1,4})\.([0-9]{1,4})")
279 .captures(output.as_ref())
280 .map(|c| c.extract())?;
281 let major = major.parse().expect("valid major version");
284 let minor = minor.parse().expect("valid minor version");
285 trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
286 Some(LibcVersion::Musllinux { major, minor })
287}
288
289fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
291 let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
301 let mut found_anything = false;
302 for path in attempts {
303 if std::fs::exists(path).ok() == Some(true) {
304 found_anything = true;
305 if let Some(ld_path) = find_ld_path_at(path) {
306 return Ok(ld_path);
307 }
308 }
309 }
310
311 if let Some(ld_path) = find_ld_path_from_filesystem() {
318 return Ok(ld_path);
319 }
320
321 let attempts_string = attempts.join(", ");
322 if !found_anything {
323 Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
327 } else {
328 Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
329 }
330}
331
332fn find_ld_path_from_filesystem() -> Option<PathBuf> {
337 find_ld_path_from_root_and_arch(Path::new("/"), Arch::from_env())
338}
339
340fn find_ld_path_from_root_and_arch(root: &Path, architecture: Arch) -> Option<PathBuf> {
341 let Some(candidates) = dynamic_linker_candidates(architecture) else {
342 trace!("No known dynamic linker paths for architecture `{architecture}`");
343 return None;
344 };
345
346 let paths = candidates
347 .iter()
348 .map(|candidate| root.join(candidate.trim_start_matches('/')))
349 .collect::<Vec<_>>();
350
351 for path in &paths {
352 if std::fs::exists(path).ok() == Some(true) {
353 trace!(
354 "Found dynamic linker on filesystem: {}",
355 path.user_display()
356 );
357 return Some(path.clone());
358 }
359 }
360
361 trace!(
362 "Could not find dynamic linker in any expected filesystem path for architecture `{}`: {}",
363 architecture,
364 paths
365 .iter()
366 .map(|path| path.user_display().to_string())
367 .collect::<Vec<_>>()
368 .join(", ")
369 );
370 None
371}
372
373fn dynamic_linker_candidates(architecture: Arch) -> Option<&'static [&'static str]> {
376 let family = architecture.family();
377
378 match family {
379 target_lexicon::Architecture::X86_64 => {
380 Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1"])
381 }
382 target_lexicon::Architecture::X86_32(_) => {
383 Some(&["/lib/ld-linux.so.2", "/lib/ld-musl-i386.so.1"])
384 }
385 target_lexicon::Architecture::Aarch64(_) => match family.endianness().ok()? {
386 Endianness::Little => {
387 Some(&["/lib/ld-linux-aarch64.so.1", "/lib/ld-musl-aarch64.so.1"])
388 }
389 Endianness::Big => Some(&[
390 "/lib/ld-linux-aarch64_be.so.1",
391 "/lib/ld-musl-aarch64_be.so.1",
392 ]),
393 },
394 target_lexicon::Architecture::Arm(_) => match family.endianness().ok()? {
395 Endianness::Little => Some(&[
396 "/lib/ld-linux-armhf.so.3",
397 "/lib/ld-linux.so.3",
398 "/lib/ld-musl-armhf.so.1",
399 "/lib/ld-musl-arm.so.1",
400 ]),
401 Endianness::Big => Some(&[
402 "/lib/ld-linux-armhf.so.3",
403 "/lib/ld-linux.so.3",
404 "/lib/ld-musl-armebhf.so.1",
405 "/lib/ld-musl-armeb.so.1",
406 ]),
407 },
408 target_lexicon::Architecture::Powerpc64 => {
409 Some(&["/lib64/ld64.so.1", "/lib/ld-musl-powerpc64.so.1"])
410 }
411 target_lexicon::Architecture::Powerpc64le => {
412 Some(&["/lib64/ld64.so.2", "/lib/ld-musl-powerpc64le.so.1"])
413 }
414 target_lexicon::Architecture::S390x => Some(&["/lib/ld64.so.1", "/lib/ld-musl-s390x.so.1"]),
415 target_lexicon::Architecture::Riscv64(_) => Some(&[
416 "/lib/ld-linux-riscv64-lp64d.so.1",
417 "/lib/ld-linux-riscv64-lp64.so.1",
418 "/lib/ld-musl-riscv64.so.1",
419 "/lib/ld-musl-riscv64-sp.so.1",
420 "/lib/ld-musl-riscv64-sf.so.1",
421 ]),
422 target_lexicon::Architecture::LoongArch64 => Some(&[
423 "/lib64/ld-linux-loongarch-lp64d.so.1",
424 "/lib64/ld-linux-loongarch-lp64s.so.1",
425 "/lib/ld-musl-loongarch64.so.1",
426 "/lib/ld-musl-loongarch64-sp.so.1",
427 "/lib/ld-musl-loongarch64-sf.so.1",
428 ]),
429 _ => None,
430 }
431}
432
433fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
437 let path = path.as_ref();
438 let buffer = fs::read(path).ok()?;
440 let elf = match Elf::parse(&buffer) {
441 Ok(elf) => elf,
442 Err(err) => {
443 trace!(
444 "Could not parse ELF file at `{}`: `{}`",
445 path.user_display(),
446 err
447 );
448 return None;
449 }
450 };
451 let Some(elf_interpreter) = elf.interpreter else {
452 trace!(
453 "Couldn't find ELF interpreter path from {}",
454 path.user_display()
455 );
456 return None;
457 };
458
459 Some(PathBuf::from(elf_interpreter))
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use indoc::indoc;
466 use tempfile::tempdir;
467
468 #[test]
469 fn parse_ld_so_output() {
470 let ver_str = glibc_ld_output_to_version(
471 "stdout",
472 indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
473 Copyright (C) 2024 Free Software Foundation, Inc.
474 This is free software; see the source for copying conditions.
475 There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
476 PARTICULAR PURPOSE.
477 "},
478 )
479 .unwrap();
480 assert_eq!(
481 ver_str,
482 LibcVersion::Manylinux {
483 major: 2,
484 minor: 39
485 }
486 );
487 }
488
489 #[test]
490 fn parse_musl_ld_output() {
491 let output = b"\
497musl libc (x86_64)
498Version 1.2.4_git20230717
499Dynamic Program Loader
500Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
501 ";
502 let got = musl_ld_output_to_version("stderr", output).unwrap();
503 assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
504 }
505
506 #[test]
507 fn dynamic_linker_candidates_prefer_glibc_before_musl() {
508 assert_eq!(
509 dynamic_linker_candidates(Arch::from_str("x86_64").unwrap()),
510 Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1",][..])
511 );
512 }
513
514 #[test]
515 fn find_ld_path_from_root_and_arch_returns_glibc_when_only_glibc_is_present() {
516 let root = tempdir().unwrap();
517 let ld_path = root.path().join("lib64/ld-linux-x86-64.so.2");
518 fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
519 fs::write(&ld_path, "").unwrap();
520
521 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
522
523 assert_eq!(got, Some(ld_path));
524 }
525
526 #[test]
527 fn find_ld_path_from_root_and_arch_returns_musl_when_only_musl_is_present() {
528 let root = tempdir().unwrap();
529 let ld_path = root.path().join("lib/ld-musl-x86_64.so.1");
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_glibc_when_both_linkers_are_present() {
540 let root = tempdir().unwrap();
541 let glibc_path = root.path().join("lib64/ld-linux-x86-64.so.2");
542 let musl_path = root.path().join("lib/ld-musl-x86_64.so.1");
543 fs::create_dir_all(glibc_path.parent().unwrap()).unwrap();
544 fs::create_dir_all(musl_path.parent().unwrap()).unwrap();
545 fs::write(&glibc_path, "").unwrap();
546 fs::write(&musl_path, "").unwrap();
547
548 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
549
550 assert_eq!(got, Some(glibc_path));
551 }
552
553 #[test]
554 fn find_ld_path_from_root_and_arch_returns_none_when_neither_linker_is_present() {
555 let root = tempdir().unwrap();
556
557 let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
558
559 assert_eq!(got, None);
560 }
561}