Skip to main content

uv_platform/
libc.rs

1//! Determine the libc (glibc or musl) on linux.
2//!
3//! Taken from `glibc_version` (<https://github.com/delta-incubator/glibc-version-rs>),
4//! which used the Apache 2.0 license (but not the MIT license)
5
6use 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/// We support glibc (manylinux) and musl (musllinux) on linux.
52#[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            // Use `None` on platforms without explicit support.
93            _ => 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
148/// Determine whether we're running glibc or musl and in which version, given we are on linux.
149///
150/// Normally, we determine this from the python interpreter, which is more accurate, but when
151/// deciding which python interpreter to download, we need to figure this out from the environment.
152///
153/// A platform can have both musl and glibc installed. We determine the preferred platform by
154/// inspecting core binaries.
155fn 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
186// glibc version is taken from `std/sys/unix/os.rs`.
187fn 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
207/// Parse output `/lib64/ld-linux-x86-64.so.2 --version` and equivalent ld.so files.
208///
209/// Example: `ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.`.
210fn 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    // Parse the input as "x.y" glibc version.
218    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    // OK since we are guaranteed to have between 1 and 3 ASCII digits and the
239    // maximum possible value, 999, fits into a u16.
240    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
245/// Read the musl version from libc library's output. Taken from maturin.
246///
247/// The libc library should output something like this to `stderr`:
248///
249/// ```text
250/// musl libc (`x86_64`)
251/// Version 1.2.2
252/// Dynamic Program Loader
253/// ```
254fn 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
277/// Parse the musl version from ld output.
278///
279/// Example: `Version 1.2.5`.
280fn 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    // unwrap-safety: Since we are guaranteed to have between 1 and 4 ASCII digits and the
288    // maximum possible value, 9999, fits into a u16.
289    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
295/// Find musl ld path from executable's ELF header.
296fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
297    // At first, we just looked for /bin/ls. But on some Linux distros, /bin/ls
298    // is a shell script that just calls /usr/bin/ls. So we switched to looking
299    // at /bin/sh. But apparently in some environments, /bin/sh is itself just
300    // a shell script that calls /bin/dash. So... We just try a few different
301    // paths. In most cases, /bin/sh should work.
302    //
303    // See: https://github.com/astral-sh/uv/pull/1493
304    // See: https://github.com/astral-sh/uv/issues/1810
305    // See: https://github.com/astral-sh/uv/issues/4242#issuecomment-2306164449
306    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 none of the common binaries exist or are parseable, try to find the
318    // dynamic linker directly on the filesystem. This handles minimal container
319    // images (e.g., Chainguard, distroless) that lack standard shell utilities
320    // but still have a dynamic linker installed.
321    //
322    // See: https://github.com/astral-sh/uv/issues/8635
323    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        // Known failure cases here include running the distroless Docker images directly
330        // (depending on what subcommand you use) and certain Nix setups. See:
331        // https://github.com/astral-sh/uv/issues/8635
332        Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
333    } else {
334        Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
335    }
336}
337
338/// Search for a glibc or musl dynamic linker on the filesystem.
339///
340/// We prefer glibc over musl. If none of the expected paths exist, [`None`] is
341/// returned.
342fn 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
379/// Return expected dynamic linker paths for the given architecture, in
380/// preference order.
381fn 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
439/// Attempt to find the path to the `ld` executable by
440/// ELF parsing the given path. If this fails for any
441/// reason, then an error is returned.
442fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
443    let path = path.as_ref();
444    // Not all linux distributions have all of these paths.
445    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        // This output was generated by running `/lib/ld-musl-x86_64.so.1`
498        // in an Alpine Docker image. The Alpine version:
499        //
500        // # cat /etc/alpine-release
501        // 3.19.1
502        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}