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::{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/// We support glibc (manylinux) and musl (musllinux) on linux.
51#[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            // Use `None` on platforms without explicit support.
92            _ => 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
147/// Determine whether we're running glibc or musl and in which version, given we are on linux.
148///
149/// Normally, we determine this from the python interpreter, which is more accurate, but when
150/// deciding which python interpreter to download, we need to figure this out from the environment.
151///
152/// A platform can have both musl and glibc installed. We determine the preferred platform by
153/// inspecting core binaries.
154fn 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
185// glibc version is taken from `std/sys/unix/os.rs`.
186fn 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
206/// Parse output `/lib64/ld-linux-x86-64.so.2 --version` and equivalent ld.so files.
207///
208/// Example: `ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.`.
209fn 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    // Parse the input as "x.y" glibc version.
216    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    // OK since we are guaranteed to have between 1 and 3 ASCII digits and the
234    // maximum possible value, 999, fits into a u16.
235    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
240/// Read the musl version from libc library's output. Taken from maturin.
241///
242/// The libc library should output something like this to `stderr`:
243///
244/// ```text
245/// musl libc (`x86_64`)
246/// Version 1.2.2
247/// Dynamic Program Loader
248/// ```
249fn 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
272/// Parse the musl version from ld output.
273///
274/// Example: `Version 1.2.5`.
275fn 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    // unwrap-safety: Since we are guaranteed to have between 1 and 4 ASCII digits and the
282    // maximum possible value, 9999, fits into a u16.
283    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
289/// Find musl ld path from executable's ELF header.
290fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
291    // At first, we just looked for /bin/ls. But on some Linux distros, /bin/ls
292    // is a shell script that just calls /usr/bin/ls. So we switched to looking
293    // at /bin/sh. But apparently in some environments, /bin/sh is itself just
294    // a shell script that calls /bin/dash. So... We just try a few different
295    // paths. In most cases, /bin/sh should work.
296    //
297    // See: https://github.com/astral-sh/uv/pull/1493
298    // See: https://github.com/astral-sh/uv/issues/1810
299    // See: https://github.com/astral-sh/uv/issues/4242#issuecomment-2306164449
300    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 none of the common binaries exist or are parseable, try to find the
312    // dynamic linker directly on the filesystem. This handles minimal container
313    // images (e.g., Chainguard, distroless) that lack standard shell utilities
314    // but still have a dynamic linker installed.
315    //
316    // See: https://github.com/astral-sh/uv/issues/8635
317    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        // Known failure cases here include running the distroless Docker images directly
324        // (depending on what subcommand you use) and certain Nix setups. See:
325        // https://github.com/astral-sh/uv/issues/8635
326        Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
327    } else {
328        Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
329    }
330}
331
332/// Search for a glibc or musl dynamic linker on the filesystem.
333///
334/// We prefer glibc over musl. If none of the expected paths exist, [`None`] is
335/// returned.
336fn 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
373/// Return expected dynamic linker paths for the given architecture, in
374/// preference order.
375fn 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
433/// Attempt to find the path to the `ld` executable by
434/// ELF parsing the given path. If this fails for any
435/// reason, then an error is returned.
436fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
437    let path = path.as_ref();
438    // Not all linux distributions have all of these paths.
439    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        // This output was generated by running `/lib/ld-musl-x86_64.so.1`
492        // in an Alpine Docker image. The Alpine version:
493        //
494        // # cat /etc/alpine-release
495        // 3.19.1
496        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}