fix: detect musl libc when downloading python-build-standalone (#2889)
* fix: detect musl libc when downloading python-build-standalone
`platform.libc_ver()` only ever reports glibc and returns `("", "")` on
musl systems, so `"musl" in platform.libc_ver()` was never true and musl
hosts (e.g. Alpine) always selected the `gnu` python-build-standalone
asset, which won't run.
Detect musl by shelling out to `ldd --version`, which prints "musl libc"
on musl-based systems. This probes the actual system loader rather than
the host Python's linkage, so it stays correct even when cibuildwheel
runs under a relocatable/statically-linked interpreter. Falls back to
glibc when `ldd` is unavailable.
Assisted-by: ClaudeCode:claude-opus-4.8
* test: add type annotations to satisfy mypy and ruff
Assisted-by: ClaudeCode:claude-opus-4.8
This commit is contained in:
@@ -5,6 +5,7 @@ import functools
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import platform
|
import platform
|
||||||
|
import subprocess
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
@@ -42,6 +43,26 @@ class PythonBuildStandaloneError(Exception):
|
|||||||
"""Errors related to python-build-standalone."""
|
"""Errors related to python-build-standalone."""
|
||||||
|
|
||||||
|
|
||||||
|
def _is_musl_libc() -> bool:
|
||||||
|
"""
|
||||||
|
Detect whether the host uses musl libc (e.g. Alpine Linux).
|
||||||
|
|
||||||
|
``platform.libc_ver()`` only ever reports glibc, so we shell out to ``ldd``,
|
||||||
|
which prints "musl libc" on musl-based systems (and "GNU libc"/"GLIBC" on
|
||||||
|
glibc systems). If ``ldd`` is unavailable, we fall back to assuming glibc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ldd = subprocess.run(
|
||||||
|
["ldd", "--version"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return "musl" in (ldd.stdout + ldd.stderr).lower()
|
||||||
|
|
||||||
|
|
||||||
def _get_platform_identifiers() -> tuple[str, str, str | None]:
|
def _get_platform_identifiers() -> tuple[str, str, str | None]:
|
||||||
"""
|
"""
|
||||||
Detects the current platform and returns architecture, platform, and libc
|
Detects the current platform and returns architecture, platform, and libc
|
||||||
@@ -67,7 +88,7 @@ def _get_platform_identifiers() -> tuple[str, str, str | None]:
|
|||||||
# Map OS + Libc
|
# Map OS + Libc
|
||||||
if system == "Linux":
|
if system == "Linux":
|
||||||
platform_identifier = "unknown-linux"
|
platform_identifier = "unknown-linux"
|
||||||
libc_identifier = "musl" if "musl" in (platform.libc_ver() or ("", "")) else "gnu"
|
libc_identifier = "musl" if _is_musl_libc() else "gnu"
|
||||||
elif system == "Darwin":
|
elif system == "Darwin":
|
||||||
platform_identifier = "apple-darwin"
|
platform_identifier = "apple-darwin"
|
||||||
elif system == "Windows":
|
elif system == "Windows":
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cibuildwheel.util import python_build_standalone as pbs
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
# Real-world `ldd --version` outputs.
|
||||||
|
GLIBC_STDOUT = "ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35\n"
|
||||||
|
MUSL_STDERR = "musl libc (x86_64)\nVersion 1.2.4\nDynamic Program Loader\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_ldd(
|
||||||
|
stdout: str = "", stderr: str = ""
|
||||||
|
) -> Callable[..., subprocess.CompletedProcess[str]]:
|
||||||
|
def run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.CompletedProcess(args=args, returncode=0, stdout=stdout, stderr=stderr)
|
||||||
|
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_musl_libc_detects_musl(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
# musl's ldd prints to stderr and exits non-zero, but capture both streams
|
||||||
|
monkeypatch.setattr(subprocess, "run", _fake_ldd(stderr=MUSL_STDERR))
|
||||||
|
assert pbs._is_musl_libc() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_musl_libc_detects_glibc(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(subprocess, "run", _fake_ldd(stdout=GLIBC_STDOUT))
|
||||||
|
assert pbs._is_musl_libc() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_musl_libc_missing_ldd(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
def run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
||||||
|
msg = "ldd"
|
||||||
|
raise FileNotFoundError(msg)
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", run)
|
||||||
|
assert pbs._is_musl_libc() is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("musl", "expected_libc"),
|
||||||
|
[(True, "musl"), (False, "gnu")],
|
||||||
|
)
|
||||||
|
def test_linux_platform_identifiers_libc(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, musl: bool, expected_libc: str
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||||
|
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setattr(pbs, "_is_musl_libc", lambda: musl)
|
||||||
|
|
||||||
|
arch, platform_id, libc = pbs._get_platform_identifiers()
|
||||||
|
assert arch == "x86_64"
|
||||||
|
assert platform_id == "unknown-linux"
|
||||||
|
assert libc == expected_libc
|
||||||
Reference in New Issue
Block a user