Skip to content

gh-87414: add musl support to platform.libc_ver #103784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
120 changes: 120 additions & 0 deletions Lib/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
import sys
import functools
import itertools
import struct

### Globals & Constants

Expand Down Expand Up @@ -199,6 +200,10 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
binary = f.read(chunksize)
pos = 0
while pos < len(binary):
if b'musl' in binary:
mv = _get_musl_version(executable)
return "musl", mv

if b'libc' in binary or b'GLIBC' in binary:
m = _libc_search.search(binary, pos)
else:
Expand Down Expand Up @@ -1344,6 +1349,121 @@ def freedesktop_os_release():
return _os_release_cache.copy()


### musl libc version support
# These functions were copied and adapted from the packaging module:
# https://github.com/pypa/packaging/blob/main/src/packaging/_musllinux.py
# https://github.com/pypa/packaging/blob/main/src/packaging/_elffile.py

class ELFInvalid(ValueError):
pass


class ELFFile:
"""
Representation of an ELF executable.
"""

def __init__(self, f):
self._f = f

try:
ident = self._read("16B")
except struct.error:
raise ELFInvalid("unable to parse identification")
magic = bytes(ident[:4])
if magic != b"\x7fELF":
raise ELFInvalid(f"invalid magic: {magic!r}")

self.capacity = ident[4] # Format for program header (bitness).
self.encoding = ident[5] # Data structure encoding (endianness).

try:
# e_fmt: Format for program header.
# p_fmt: Format for section header.
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
e_fmt, self._p_fmt, self._p_idx = {
(1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB.
(1, 2): (">HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB.
(2, 1): ("<HHIQQQIHHH", "<IIQQQQQQ", (0, 2, 5)), # 64-bit LSB.
(2, 2): (">HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB.
}[(self.capacity, self.encoding)]
except KeyError:
raise ELFInvalid(
f"unrecognized capacity ({self.capacity}) or "
f"encoding ({self.encoding})"
)

try:
(
_,
self.machine, # Architecture type.
_,
_,
self._e_phoff, # Offset of program header.
_,
self.flags, # Processor-specific flags.
_,
self._e_phentsize, # Size of section.
self._e_phnum, # Number of sections.
) = self._read(e_fmt)
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e

def _read(self, fmt):
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))

@property
def interpreter(self):
"""
The path recorded in the ``PT_INTERP`` section header.
"""
for index in range(self._e_phnum):
self._f.seek(self._e_phoff + self._e_phentsize * index)
try:
data = self._read(self._p_fmt)
except struct.error:
continue
if data[self._p_idx[0]] != 3: # Not PT_INTERP.
continue
self._f.seek(data[self._p_idx[1]])
return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0")
return None

def _parse_musl_version(output):
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
if len(lines) < 2 or lines[0][:4] != "musl":
return None
m = re.match(r"Version (\d+)\.(\d+)", lines[1])
if not m:
return None
return f"{m.group(1)}.{m.group(2)}"


@functools.lru_cache()
def _get_musl_version(executable):
"""Detect currently-running musl runtime version.

This is done by checking the specified executable's dynamic linking
information, and invoking the loader to parse its output for a version
string. If the loader is musl, the output would be something like::

musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
"""
import subprocess

try:
with open(executable, "rb") as f:
ld = ELFFile(f).interpreter
except (OSError, TypeError, ValueError):
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
return _parse_musl_version(proc.stderr)


### Command line interface

if __name__ == '__main__':
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,19 @@ def wrapper(*args, **kw):
return wrapper
return decorator

def requires_musl():
"""Decorator raising SkipTest if the musl is not available."""
import subprocess
proc = subprocess.run(["ldd"], stderr=subprocess.PIPE, universal_newlines=True)
if "musl" in proc.stderr:
skip = False
else:
skip = True

return unittest.skipIf(
skip,
f"musl is not available in this platform",
)

def skip_if_buildbot(reason=None):
"""Decorator raising SkipTest if running on a buildbot."""
Expand Down
47 changes: 47 additions & 0 deletions Lib/test/test_platform.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
import os
import copy
import pickle
Expand Down Expand Up @@ -68,6 +69,9 @@
"""


ELFFILE_HEADER = b"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"


class PlatformTest(unittest.TestCase):
def clear_caches(self):
platform._platform_cache.clear()
Expand Down Expand Up @@ -538,6 +542,49 @@ def test_parse_os_release(self):
self.assertEqual(info, expected)
self.assertEqual(len(info["SPECIALS"]), 5)

def test_parse_musl_version(self):
output = """\
musl libc (x86_64)
Version 1.2.3
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]
"""
self.assertEqual(platform._parse_musl_version(output), "1.2")

@support.requires_subprocess()
@support.requires_musl()
def test_libc_ver_musl(self):
self.assertEqual(platform.libc_ver(), ("musl", "1.2"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this always be 1.2?



@support.requires_musl()
class ELFFileTest(unittest.TestCase):

def test_get_interpreter(self):
with open(sys.executable, "rb") as f:
elffile = platform.ELFFile(f)
self.assertEqual(elffile.interpreter, "/lib/ld-musl-x86_64.so.1")

def test_init_invalid_magic(self):
BAD_ELFFILE_HEADER = b"\x7fBAD\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
f = io.BytesIO(BAD_ELFFILE_HEADER)
self.assertRaisesRegex(
platform.ELFInvalid,
"invalid magic:",
platform.ELFFile,
f,
)

def test_init_parse_error(self):
EMPTY_ELF_HEADER = b"\x00"
f = io.BytesIO(EMPTY_ELF_HEADER)
self.assertRaisesRegex(
platform.ELFInvalid,
"unable to parse identification",
platform.ELFFile,
f,
)


if __name__ == '__main__':
unittest.main()