Skip to content
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()