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::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 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)]
52pub enum 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 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                    if !libc.is_empty() {
69                        return Self::from_str(&libc);
70                    }
71                }
72
73                Ok(Self::Some(match detect_linux_libc()? {
74                    LibcVersion::Manylinux { .. } => match env::consts::ARCH {
75                        // Checks if the CPU supports hardware floating-point operations.
76                        // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment.
77                        // download-metadata.json only includes armv7.
78                        "arm" | "armv5te" | "armv7" => {
79                            match detect_hardware_floating_point_support() {
80                                Ok(true) => target_lexicon::Environment::Gnueabihf,
81                                Ok(false) => target_lexicon::Environment::Gnueabi,
82                                Err(_) => target_lexicon::Environment::Gnu,
83                            }
84                        }
85                        _ => target_lexicon::Environment::Gnu,
86                    },
87                    LibcVersion::Musllinux { .. } => 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!(self, Self::Some(target_lexicon::Environment::Musl))
98    }
99}
100
101impl FromStr for Libc {
102    type Err = crate::Error;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        match s {
106            "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
107            "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)),
108            "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)),
109            "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
110            "none" => Ok(Self::None),
111            _ => Err(crate::Error::UnknownLibc(s.to_string())),
112        }
113    }
114}
115
116impl Display for Libc {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::Some(env) => write!(f, "{env}"),
120            Self::None => write!(f, "none"),
121        }
122    }
123}
124
125impl From<&uv_platform_tags::Os> for Libc {
126    fn from(value: &uv_platform_tags::Os) -> Self {
127        match value {
128            uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
129            uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
130            uv_platform_tags::Os::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
131            _ => Self::None,
132        }
133    }
134}
135
136/// Determine whether we're running glibc or musl and in which version, given we are on linux.
137///
138/// Normally, we determine this from the python interpreter, which is more accurate, but when
139/// deciding which python interpreter to download, we need to figure this out from the environment.
140///
141/// A platform can have both musl and glibc installed. We determine the preferred platform by
142/// inspecting core binaries.
143pub(crate) fn detect_linux_libc() -> Result<LibcVersion, LibcDetectionError> {
144    let ld_path = find_ld_path()?;
145    trace!("Found `ld` path: {}", ld_path.user_display());
146
147    match detect_musl_version(&ld_path) {
148        Ok(os) => return Ok(os),
149        Err(err) => {
150            trace!("Tried to find musl version by running `{ld_path:?}`, but failed: {err}");
151        }
152    }
153    match detect_linux_libc_from_ld_symlink(&ld_path) {
154        Ok(os) => return Ok(os),
155        Err(err) => {
156            trace!(
157                "Tried to find libc version from possible symlink at {ld_path:?}, but failed: {err}"
158            );
159        }
160    }
161    match detect_glibc_version_from_ld(&ld_path) {
162        Ok(os_version) => return Ok(os_version),
163        Err(err) => {
164            trace!(
165                "Tried to find glibc version from `{} --version`, but failed: {}",
166                ld_path.simplified_display(),
167                err
168            );
169        }
170    }
171    Err(LibcDetectionError::NoLibcFound)
172}
173
174// glibc version is taken from `std/sys/unix/os.rs`.
175fn detect_glibc_version_from_ld(ld_so: &Path) -> Result<LibcVersion, LibcDetectionError> {
176    let output = Command::new(ld_so)
177        .args(["--version"])
178        .output()
179        .map_err(|err| LibcDetectionError::FailedToRun {
180            libc: "glibc",
181            program: format!("{} --version", ld_so.user_display()),
182            err,
183        })?;
184    if let Some(os) = glibc_ld_output_to_version("stdout", &output.stdout) {
185        return Ok(os);
186    }
187    if let Some(os) = glibc_ld_output_to_version("stderr", &output.stderr) {
188        return Ok(os);
189    }
190    Err(LibcDetectionError::InvalidLdSoOutputGnu(
191        ld_so.to_path_buf(),
192    ))
193}
194
195/// Parse output `/lib64/ld-linux-x86-64.so.2 --version` and equivalent ld.so files.
196///
197/// Example: `ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.`.
198fn glibc_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
199    static RE: LazyLock<Regex> =
200        LazyLock::new(|| Regex::new(r"ld.so \(.+\) .* ([0-9]+\.[0-9]+)").unwrap());
201
202    let output = String::from_utf8_lossy(output);
203    trace!("{kind} output from `ld.so --version`: {output:?}");
204    let (_, [version]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
205    // Parse the input as "x.y" glibc version.
206    let mut parsed_ints = version.split('.').map(str::parse).fuse();
207    let major = parsed_ints.next()?.ok()?;
208    let minor = parsed_ints.next()?.ok()?;
209    trace!("Found manylinux {major}.{minor} in {kind} of ld.so version");
210    Some(LibcVersion::Manylinux { major, minor })
211}
212
213fn detect_linux_libc_from_ld_symlink(path: &Path) -> Result<LibcVersion, LibcDetectionError> {
214    static RE: LazyLock<Regex> =
215        LazyLock::new(|| Regex::new(r"^ld-([0-9]{1,3})\.([0-9]{1,3})\.so$").unwrap());
216
217    let ld_path = fs::read_link(path)?;
218    let filename = ld_path
219        .file_name()
220        .ok_or_else(|| LibcDetectionError::MissingBasePath(ld_path.clone()))?
221        .to_string_lossy();
222    let (_, [major, minor]) = RE
223        .captures(&filename)
224        .map(|c| c.extract())
225        .ok_or_else(|| LibcDetectionError::GlibcExtractionMismatch(ld_path.clone()))?;
226    // OK since we are guaranteed to have between 1 and 3 ASCII digits and the
227    // maximum possible value, 999, fits into a u16.
228    let major = major.parse().expect("valid major version");
229    let minor = minor.parse().expect("valid minor version");
230    Ok(LibcVersion::Manylinux { major, minor })
231}
232
233/// Read the musl version from libc library's output. Taken from maturin.
234///
235/// The libc library should output something like this to `stderr`:
236///
237/// ```text
238/// musl libc (`x86_64`)
239/// Version 1.2.2
240/// Dynamic Program Loader
241/// ```
242fn detect_musl_version(ld_path: impl AsRef<Path>) -> Result<LibcVersion, LibcDetectionError> {
243    let ld_path = ld_path.as_ref();
244    let output = Command::new(ld_path)
245        .stdout(Stdio::null())
246        .stderr(Stdio::piped())
247        .output()
248        .map_err(|err| LibcDetectionError::FailedToRun {
249            libc: "musl",
250            program: ld_path.to_string_lossy().to_string(),
251            err,
252        })?;
253
254    if let Some(os) = musl_ld_output_to_version("stdout", &output.stdout) {
255        return Ok(os);
256    }
257    if let Some(os) = musl_ld_output_to_version("stderr", &output.stderr) {
258        return Ok(os);
259    }
260    Err(LibcDetectionError::InvalidLdSoOutputMusl(
261        ld_path.to_path_buf(),
262    ))
263}
264
265/// Parse the musl version from ld output.
266///
267/// Example: `Version 1.2.5`.
268fn musl_ld_output_to_version(kind: &str, output: &[u8]) -> Option<LibcVersion> {
269    static RE: LazyLock<Regex> =
270        LazyLock::new(|| Regex::new(r"Version ([0-9]{1,4})\.([0-9]{1,4})").unwrap());
271
272    let output = String::from_utf8_lossy(output);
273    trace!("{kind} output from `ld`: {output:?}");
274    let (_, [major, minor]) = RE.captures(output.as_ref()).map(|c| c.extract())?;
275    // unwrap-safety: Since we are guaranteed to have between 1 and 4 ASCII digits and the
276    // maximum possible value, 9999, fits into a u16.
277    let major = major.parse().expect("valid major version");
278    let minor = minor.parse().expect("valid minor version");
279    trace!("Found musllinux {major}.{minor} in {kind} of `ld`");
280    Some(LibcVersion::Musllinux { major, minor })
281}
282
283/// Find musl ld path from executable's ELF header.
284fn find_ld_path() -> Result<PathBuf, LibcDetectionError> {
285    // At first, we just looked for /bin/ls. But on some Linux distros, /bin/ls
286    // is a shell script that just calls /usr/bin/ls. So we switched to looking
287    // at /bin/sh. But apparently in some environments, /bin/sh is itself just
288    // a shell script that calls /bin/dash. So... We just try a few different
289    // paths. In most cases, /bin/sh should work.
290    //
291    // See: https://github.com/astral-sh/uv/pull/1493
292    // See: https://github.com/astral-sh/uv/issues/1810
293    // See: https://github.com/astral-sh/uv/issues/4242#issuecomment-2306164449
294    let attempts = ["/bin/sh", "/usr/bin/env", "/bin/dash", "/bin/ls"];
295    let mut found_anything = false;
296    for path in attempts {
297        if std::fs::exists(path).ok() == Some(true) {
298            found_anything = true;
299            if let Some(ld_path) = find_ld_path_at(path) {
300                return Ok(ld_path);
301            }
302        }
303    }
304    let attempts_string = attempts.join(", ");
305    if !found_anything {
306        // Known failure cases here include running the distroless Docker images directly
307        // (depending on what subcommand you use) and certain Nix setups. See:
308        // https://github.com/astral-sh/uv/issues/8635
309        Err(LibcDetectionError::NoCommonBinariesFound(attempts_string))
310    } else {
311        Err(LibcDetectionError::CoreBinaryParsing(attempts_string))
312    }
313}
314
315/// Attempt to find the path to the `ld` executable by
316/// ELF parsing the given path. If this fails for any
317/// reason, then an error is returned.
318fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
319    let path = path.as_ref();
320    // Not all linux distributions have all of these paths.
321    let buffer = fs::read(path).ok()?;
322    let elf = match Elf::parse(&buffer) {
323        Ok(elf) => elf,
324        Err(err) => {
325            trace!(
326                "Could not parse ELF file at `{}`: `{}`",
327                path.user_display(),
328                err
329            );
330            return None;
331        }
332    };
333    let Some(elf_interpreter) = elf.interpreter else {
334        trace!(
335            "Couldn't find ELF interpreter path from {}",
336            path.user_display()
337        );
338        return None;
339    };
340
341    Some(PathBuf::from(elf_interpreter))
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use indoc::indoc;
348
349    #[test]
350    fn parse_ld_so_output() {
351        let ver_str = glibc_ld_output_to_version(
352            "stdout",
353            indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39.
354            Copyright (C) 2024 Free Software Foundation, Inc.
355            This is free software; see the source for copying conditions.
356            There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
357            PARTICULAR PURPOSE.
358        "},
359        )
360        .unwrap();
361        assert_eq!(
362            ver_str,
363            LibcVersion::Manylinux {
364                major: 2,
365                minor: 39
366            }
367        );
368    }
369
370    #[test]
371    fn parse_musl_ld_output() {
372        // This output was generated by running `/lib/ld-musl-x86_64.so.1`
373        // in an Alpine Docker image. The Alpine version:
374        //
375        // # cat /etc/alpine-release
376        // 3.19.1
377        let output = b"\
378musl libc (x86_64)
379Version 1.2.4_git20230717
380Dynamic Program Loader
381Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\
382    ";
383        let got = musl_ld_output_to_version("stderr", output).unwrap();
384        assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 });
385    }
386}