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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 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
try:
import _wmi
except ImportError:
Expand Down Expand Up @@ -204,6 +205,13 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
binary = f.read(chunksize)
pos = 0
while pos < len(binary):
# We first check 'musl' in the binary, because the next
# condition of looking for 'libc' in binary will be
# true for the case of musl.
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 @@ -1355,6 +1363,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/4d8534061364e3cbfee582192ab81a095ec2db51/src/packaging/_musllinux.py
# https://github.com/pypa/packaging/blob/4d8534061364e3cbfee582192ab81a095ec2db51/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
21 changes: 21 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,27 @@ def wrapper(*args, **kw):
return wrapper
return decorator

def requires_musl():
"""Decorator raising SkipTest if the musl is not available."""
import subprocess
if sys.platform == "darwin":
_cmd = "otool -L"
elif sys.platform == "linux":
_cmd = "ldd"
else:
return False

proc = subprocess.run(_cmd.split(), 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()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add musl support to ``platform.libc_ver`` function.