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