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