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)]
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" => {
77                            match detect_hardware_floating_point_support() {
78                                Ok(true) => target_lexicon::Environment::Gnueabihf,
79                                Ok(false) => target_lexicon::Environment::Gnueabi,
80                                Err(_) => target_lexicon::Environment::Gnu,
81                            }
82                        }
83                        _ => target_lexicon::Environment::Gnu,
84                    },
85                    LibcVersion::Musllinux { .. } => match env::consts::ARCH {
86                        "arm" | "armv5te" | "armv7" => {
87                            match detect_hardware_floating_point_support() {
88                                Ok(true) => target_lexicon::Environment::Musleabihf,
89                                Ok(false) => target_lexicon::Environment::Musleabi,
90                                Err(_) => target_lexicon::Environment::Musl,
91                            }
92                        }
93                        _ => target_lexicon::Environment::Musl,
94                    },
95                }))
96            }
97            "windows" | "macos" => Ok(Self::None),
98            // Use `None` on platforms without explicit support.
99            _ => Ok(Self::None),
100        }
101    }
102
103    pub fn is_musl(&self) -> bool {
104        matches!(
105            self,
106            Self::Some(
107                target_lexicon::Environment::Musl
108                    | target_lexicon::Environment::Musleabi
109                    | target_lexicon::Environment::Musleabihf
110            )
111        )
112    }
113}
114
115impl FromStr for Libc {
116    type Err = crate::Error;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        match s {
120            "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
121            "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
122            "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
123            "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
124            "musleabi" => Ok(Self::Some(target_lexicon::Environment::Musleabi)),
125            "musleabihf" => Ok(Self::Some(target_lexicon::Environment::Musleabihf)),
126            "none" => Ok(Self::None),
127            _ => Err(crate::Error::UnknownLibc(s.to_string())),
128        }
129    }
130}
131
132impl Display for Libc {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            Self::Some(env) => write!(f, "{env}"),
136            Self::None => write!(f, "none"),
137        }
138    }
139}
140
141impl From<&uv_platform_tags::Os> for Libc {
142    fn from(value: &uv_platform_tags::Os) -> Self {
143        match value {
144            uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
145            uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
146            uv_platform_tags::Os::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
147            _ => Self::None,
148        }
149    }
150}
151
152/// Determine whether we're running glibc or musl and in which version, given we are on linux.
153///
154/// Normally, we determine this from the python interpreter, which is more accurate, but when
155/// deciding which python interpreter to download, we need to figure this out from the environment.
156///
157/// A platform can have both musl and glibc installed. We determine the preferred platform by
158/// inspecting core binaries.
159pub(crate) fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
160    let ld_path = find_ld_path()?;
161    trace!("Found `ld` path: {}", ld_path.user_display());
162
163    match detect_musl_version(&ld_path) {
164        Ok(os) => return Ok(os),
165        Err(err) => {
166            trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
167        }
168    }
169    match detect_linux_libc_from_ld_symlink(&ld_path) {
170        Ok(os) => return Ok(os),
171        Err(err) => {
172            trace!(
173                "Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
174            );
175        }
176    }
177    match detect_glibc_version_from_ld(&ld_path) {
178        Ok(os_version) => return Ok(os_version),
179        Err(err) => {
180            trace!(
181                "Tried to find glibc version from `{} --version`, but failed: {}",
182                ld_path.simplified_display(),
183                err
184            );
185        }
186    }
187    Err(LibcDetectionError::NoLibcFound)
188}
189
190// glibc version is taken from `std/sys/unix/os.rs`.
191fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
192    let output = Command::new(ld_so)
193        .args(["--version"])
194        .output()
195        .map_err(|err| LibcDetectionError::FailedToRun {
196            libc: "glibc",
197            program: format!("{} --version", ld_so.user_display()),
198            err,
199        })?;
200    if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
201        return Ok(os);
202    }
203    if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
204        return Ok(os);
205    }
206    Err(LibcDetectionError::InvalidLdSoOutputGnu(
207        ld_so.to_path_buf(),
208    ))
209}
210
211/// Parse output `/lib64/ld-linux-x86-64.so.2 --version` and equivalent ld.so files.
212///
213/// Example: `ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.`.
214fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
215    static RE: LazyLock<Regex> =
216        LazyLock::new(|| Regex::new(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)").unwrap());
217
218    let output = String::from_utf8_lossy(output);
219    trace!("{kind} output from `ld.so --version`: {output:?}");
220    let (_, [version]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
221    // Parse the input as "x.y" glibc version.
222    let mut parsed_ints = version.split('.').map(str::parse).fuse();
223    let major = parsed_ints.next()?.ok()?;
224    let minor = parsed_ints.next()?.ok()?;
225    trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
226    Some(LibcVersion::Manylinux { major, minor })
227}
228
229fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
230    static RE: LazyLock<Regex> =
231        LazyLock::new(|| Regex::new(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$").unwrap());
232
233    let ld_path = fs::read_link(path)?;
234    let filename = ld_path
235        .file_name()
236        .ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
237        .to_string_lossy();
238    let (_, [major, minor]) = RE
239        .captures(&filename)
240        .map(|c| c.extract())
241        .ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
242    // OK since we are guaranteed to have between 1 and 3 ASCII digits and the
243    // maximum possible value, 999, fits into a u16.
244    let major = major.parse().expect("valid major version");
245    let minor = minor.parse().expect("valid minor version");
246    Ok(LibcVersion::Manylinux { major, minor })
247}
248
249/// Read the musl version from libc library's output. Taken from maturin.
250///
251/// The libc library should output something like this to `stderr`:
252///
253/// ```text
254/// musl libc (`x86_64`)
255/// Version 1.2.2
256/// Dynamic Program Loader
257/// ```
258fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
259    let ld_path = ld_path.as_ref();
260    let output = Command::new(ld_path)
261        .stdout(Stdio::null())
262        .stderr(Stdio::piped())
263        .output()
264        .map_err(|err| LibcDetectionError::FailedToRun {
265            libc: "musl",
266            program: ld_path.to_string_lossy().to_string(),
267            err,
268        })?;
269
270    if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
271        return Ok(os);
272    }
273    if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
274        return Ok(os);
275    }
276    Err(LibcDetectionError::InvalidLdSoOutputMusl(
277        ld_path.to_path_buf(),
278    ))
279}
280
281/// Parse the musl version from ld output.
282///
283/// Example: `Version 1.2.5`.
284fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
285    static RE: LazyLock<Regex> =
286        LazyLock::new(|| Regex::new(r"Version ([0-9]{1,4})\.([0-9]{1,4})").unwrap());
287
288    let output = String::from_utf8_lossy(output);
289    trace!("{kind} output from `ld`: {output:?}");
290    let (_, [major, minor]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
291    // unwrap-safety: Since we are guaranteed to have between 1 and 4 ASCII digits and the
292    // maximum possible value, 9999, fits into a u16.
293    let major = major.parse().expect("valid major version");
294    let minor = minor.parse().expect("valid minor version");
295    trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
296    Some(LibcVersion::Musllinux { major, minor })
297}
298
299/// Find musl ld path from executable's ELF header.
300fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
301    // At first, we just looked for /bin/ls. But on some Linux distros, /bin/ls
302    // is a shell script that just calls /usr/bin/ls. So we switched to looking
303    // at /bin/sh. But apparently in some environments, /bin/sh is itself just
304    // a shell script that calls /bin/dash. So... We just try a few different
305    // paths. In most cases, /bin/sh should work.
306    //
307    // See: https://github.com/astral-sh/uv/pull/1493
308    // See: https://github.com/astral-sh/uv/issues/1810
309    // See: https://github.com/astral-sh/uv/issues/4242#issuecomment-2306164449
310    let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
311    let mut found_anything = false;
312    for path in attempts {
313        if std::fs::exists(path).ok() == Some(true) {
314            found_anything = true;
315            if let Some(ld_path) = find_ld_path_at(path) {
316                return Ok(ld_path);
317            }
318        }
319    }
320
321    // If none of the common binaries exist or are parseable, try to find the
322    // dynamic linker directly on the filesystem. This handles minimal container
323    // images (e.g., Chainguard, distroless) that lack standard shell utilities
324    // but still have a dynamic linker installed.
325    //
326    // See: https://github.com/astral-sh/uv/issues/8635
327    if let Some(ld_path) = find_ld_path_from_filesystem() {
328        return Ok(ld_path);
329    }
330
331    let attempts_string = attempts.join(", ");
332    if !found_anything {
333        // Known failure cases here include running the distroless Docker images directly
334        // (depending on what subcommand you use) and certain Nix setups. See:
335        // https://github.com/astral-sh/uv/issues/8635
336        Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
337    } else {
338        Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
339    }
340}
341
342/// Search for a glibc or musl dynamic linker on the filesystem.
343///
344/// We prefer glibc over musl. If none of the expected paths exist, [`None`] is
345/// returned.
346fn find_ld_path_from_filesystem() -> Option<PathBuf> {
347    find_ld_path_from_root_and_arch(Path::new("/"), Arch::from_env())
348}
349
350fn find_ld_path_from_root_and_arch(root: &Path, architecture: Arch) -> Option<PathBuf> {
351    let Some(candidates) = dynamic_linker_candidates(architecture) else {
352        trace!("No known dynamic linker paths for architecture `{architecture}`");
353        return None;
354    };
355
356    let paths = candidates
357        .iter()
358        .map(|candidate| root.join(candidate.trim_start_matches('/')))
359        .collect::<Vec<_>>();
360
361    for path in &paths {
362        if std::fs::exists(path).ok() == Some(true) {
363            trace!(
364                "Found dynamic linker on filesystem: {}",
365                path.user_display()
366            );
367            return Some(path.clone());
368        }
369    }
370
371    trace!(
372        "Could not find dynamic linker in any expected filesystem path for architecture `{}`: {}",
373        architecture,
374        paths
375            .iter()
376            .map(|path| path.user_display().to_string())
377            .collect::<Vec<_>>()
378            .join(", ")
379    );
380    None
381}
382
383/// Return expected dynamic linker paths for the given architecture, in
384/// preference order.
385fn dynamic_linker_candidates(architecture: Arch) -> Option<&'static [&'static str]> {
386    let family = architecture.family();
387
388    match family {
389        target_lexicon::Architecture::X86_64 => {
390            Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1"])
391        }
392        target_lexicon::Architecture::X86_32(_) => {
393            Some(&["/lib/ld-linux.so.2", "/lib/ld-musl-i386.so.1"])
394        }
395        target_lexicon::Architecture::Aarch64(_) => match family.endianness().ok()? {
396            Endianness::Little => {
397                Some(&["/lib/ld-linux-aarch64.so.1", "/lib/ld-musl-aarch64.so.1"])
398            }
399            Endianness::Big => Some(&[
400                "/lib/ld-linux-aarch64_be.so.1",
401                "/lib/ld-musl-aarch64_be.so.1",
402            ]),
403        },
404        target_lexicon::Architecture::Arm(_) => match family.endianness().ok()? {
405            Endianness::Little => Some(&[
406                "/lib/ld-linux-armhf.so.3",
407                "/lib/ld-linux.so.3",
408                "/lib/ld-musl-armhf.so.1",
409                "/lib/ld-musl-arm.so.1",
410            ]),
411            Endianness::Big => Some(&[
412                "/lib/ld-linux-armhf.so.3",
413                "/lib/ld-linux.so.3",
414                "/lib/ld-musl-armebhf.so.1",
415                "/lib/ld-musl-armeb.so.1",
416            ]),
417        },
418        target_lexicon::Architecture::Powerpc64 => {
419            Some(&["/lib64/ld64.so.1", "/lib/ld-musl-powerpc64.so.1"])
420        }
421        target_lexicon::Architecture::Powerpc64le => {
422            Some(&["/lib64/ld64.so.2", "/lib/ld-musl-powerpc64le.so.1"])
423        }
424        target_lexicon::Architecture::S390x => Some(&["/lib/ld64.so.1", "/lib/ld-musl-s390x.so.1"]),
425        target_lexicon::Architecture::Riscv64(_) => Some(&[
426            "/lib/ld-linux-riscv64-lp64d.so.1",
427            "/lib/ld-linux-riscv64-lp64.so.1",
428            "/lib/ld-musl-riscv64.so.1",
429            "/lib/ld-musl-riscv64-sp.so.1",
430            "/lib/ld-musl-riscv64-sf.so.1",
431        ]),
432        target_lexicon::Architecture::LoongArch64 => Some(&[
433            "/lib64/ld-linux-loongarch-lp64d.so.1",
434            "/lib64/ld-linux-loongarch-lp64s.so.1",
435            "/lib/ld-musl-loongarch64.so.1",
436            "/lib/ld-musl-loongarch64-sp.so.1",
437            "/lib/ld-musl-loongarch64-sf.so.1",
438        ]),
439        _ => None,
440    }
441}
442
443/// Attempt to find the path to the `ld` executable by
444/// ELF parsing the given path. If this fails for any
445/// reason, then an error is returned.
446fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
447    let path = path.as_ref();
448    // Not all linux distributions have all of these paths.
449    let buffer = fs::read(path).ok()?;
450    let elf = match Elf::parse(&buffer) {
451        Ok(elf) => elf,
452        Err(err) => {
453            trace!(
454                "Could not parse ELF file at `{}`: `{}`",
455                path.user_display(),
456                err
457            );
458            return None;
459        }
460    };
461    let Some(elf_interpreter) = elf.interpreter else {
462        trace!(
463            "Couldn't find ELF interpreter path from {}",
464            path.user_display()
465        );
466        return None;
467    };
468
469    Some(PathBuf::from(elf_interpreter))
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use indoc::indoc;
476    use tempfile::tempdir;
477
478    #[test]
479    fn parse_ld_so_output() {
480        let ver_str = glibc_ld_output_to_version(
481            "stdout",
482            indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
483            Copyright (C) 2024 Free Software Foundation, Inc.
484            This is free software; see the source for copying conditions.
485            There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
486            PARTICULAR PURPOSE.
487        "},
488        )
489        .unwrap();
490        assert_eq!(
491            ver_str,
492            LibcVersion::Manylinux {
493                major: 2,
494                minor: 39
495            }
496        );
497    }
498
499    #[test]
500    fn parse_musl_ld_output() {
501        // This output was generated by running `/lib/ld-musl-x86_64.so.1`
502        // in an Alpine Docker image. The Alpine version:
503        //
504        // # cat /etc/alpine-release
505        // 3.19.1
506        let output = b"\
507musl libc (x86_64)
508Version 1.2.4_git20230717
509Dynamic Program Loader
510Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
511    ";
512        let got = musl_ld_output_to_version("stderr", output).unwrap();
513        assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
514    }
515
516    #[test]
517    fn dynamic_linker_candidates_prefer_glibc_before_musl() {
518        assert_eq!(
519            dynamic_linker_candidates(Arch::from_str("x86_64").unwrap()),
520            Some(&["/lib64/ld-linux-x86-64.so.2", "/lib/ld-musl-x86_64.so.1",][..])
521        );
522    }
523
524    #[test]
525    fn find_ld_path_from_root_and_arch_returns_glibc_when_only_glibc_is_present() {
526        let root = tempdir().unwrap();
527        let ld_path = root.path().join("lib64/ld-linux-x86-64.so.2");
528        fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
529        fs::write(&ld_path, "").unwrap();
530
531        let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
532
533        assert_eq!(got, Some(ld_path));
534    }
535
536    #[test]
537    fn find_ld_path_from_root_and_arch_returns_musl_when_only_musl_is_present() {
538        let root = tempdir().unwrap();
539        let ld_path = root.path().join("lib/ld-musl-x86_64.so.1");
540        fs::create_dir_all(ld_path.parent().unwrap()).unwrap();
541        fs::write(&ld_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(ld_path));
546    }
547
548    #[test]
549    fn find_ld_path_from_root_and_arch_returns_glibc_when_both_linkers_are_present() {
550        let root = tempdir().unwrap();
551        let glibc_path = root.path().join("lib64/ld-linux-x86-64.so.2");
552        let musl_path = root.path().join("lib/ld-musl-x86_64.so.1");
553        fs::create_dir_all(glibc_path.parent().unwrap()).unwrap();
554        fs::create_dir_all(musl_path.parent().unwrap()).unwrap();
555        fs::write(&glibc_path, "").unwrap();
556        fs::write(&musl_path, "").unwrap();
557
558        let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
559
560        assert_eq!(got, Some(glibc_path));
561    }
562
563    #[test]
564    fn find_ld_path_from_root_and_arch_returns_none_when_neither_linker_is_present() {
565        let root = tempdir().unwrap();
566
567        let got = find_ld_path_from_root_and_arch(root.path(), Arch::from_str("x86_64").unwrap());
568
569        assert_eq!(got, None);
570    }
571}