You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
4.3KB

  1. """PEP 656 support.
  2. This module implements logic to detect if the currently running Python is
  3. linked against musl, and what musl version is used.
  4. """
  5. import contextlib
  6. import functools
  7. import operator
  8. import os
  9. import re
  10. import struct
  11. import subprocess
  12. import sys
  13. from typing import IO, Iterator, NamedTuple, Optional, Tuple
  14. def _read_unpacked(f: IO[bytes], fmt: str) -> Tuple[int, ...]:
  15. return struct.unpack(fmt, f.read(struct.calcsize(fmt)))
  16. def _parse_ld_musl_from_elf(f: IO[bytes]) -> Optional[str]:
  17. """Detect musl libc location by parsing the Python executable.
  18. Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
  19. ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
  20. """
  21. f.seek(0)
  22. try:
  23. ident = _read_unpacked(f, "16B")
  24. except struct.error:
  25. return None
  26. if ident[:4] != tuple(b"\x7fELF"): # Invalid magic, not ELF.
  27. return None
  28. f.seek(struct.calcsize("HHI"), 1) # Skip file type, machine, and version.
  29. try:
  30. # e_fmt: Format for program header.
  31. # p_fmt: Format for section header.
  32. # p_idx: Indexes to find p_type, p_offset, and p_filesz.
  33. e_fmt, p_fmt, p_idx = {
  34. 1: ("IIIIHHH", "IIIIIIII", (0, 1, 4)), # 32-bit.
  35. 2: ("QQQIHHH", "IIQQQQQQ", (0, 2, 5)), # 64-bit.
  36. }[ident[4]]
  37. except KeyError:
  38. return None
  39. else:
  40. p_get = operator.itemgetter(*p_idx)
  41. # Find the interpreter section and return its content.
  42. try:
  43. _, e_phoff, _, _, _, e_phentsize, e_phnum = _read_unpacked(f, e_fmt)
  44. except struct.error:
  45. return None
  46. for i in range(e_phnum + 1):
  47. f.seek(e_phoff + e_phentsize * i)
  48. try:
  49. p_type, p_offset, p_filesz = p_get(_read_unpacked(f, p_fmt))
  50. except struct.error:
  51. return None
  52. if p_type != 3: # Not PT_INTERP.
  53. continue
  54. f.seek(p_offset)
  55. interpreter = os.fsdecode(f.read(p_filesz)).strip("\0")
  56. if "musl" not in interpreter:
  57. return None
  58. return interpreter
  59. return None
  60. class _MuslVersion(NamedTuple):
  61. major: int
  62. minor: int
  63. def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
  64. lines = [n for n in (n.strip() for n in output.splitlines()) if n]
  65. if len(lines) < 2 or lines[0][:4] != "musl":
  66. return None
  67. m = re.match(r"Version (\d+)\.(\d+)", lines[1])
  68. if not m:
  69. return None
  70. return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
  71. @functools.lru_cache()
  72. def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
  73. """Detect currently-running musl runtime version.
  74. This is done by checking the specified executable's dynamic linking
  75. information, and invoking the loader to parse its output for a version
  76. string. If the loader is musl, the output would be something like::
  77. musl libc (x86_64)
  78. Version 1.2.2
  79. Dynamic Program Loader
  80. """
  81. with contextlib.ExitStack() as stack:
  82. try:
  83. f = stack.enter_context(open(executable, "rb"))
  84. except IOError:
  85. return None
  86. ld = _parse_ld_musl_from_elf(f)
  87. if not ld:
  88. return None
  89. proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
  90. return _parse_musl_version(proc.stderr)
  91. def platform_tags(arch: str) -> Iterator[str]:
  92. """Generate musllinux tags compatible to the current platform.
  93. :param arch: Should be the part of platform tag after the ``linux_``
  94. prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a
  95. prerequisite for the current platform to be musllinux-compatible.
  96. :returns: An iterator of compatible musllinux tags.
  97. """
  98. sys_musl = _get_musl_version(sys.executable)
  99. if sys_musl is None: # Python not dynamically linked against musl.
  100. return
  101. for minor in range(sys_musl.minor, -1, -1):
  102. yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
  103. if __name__ == "__main__": # pragma: no cover
  104. import sysconfig
  105. plat = sysconfig.get_platform()
  106. assert plat.startswith("linux-"), "not linux"
  107. print("plat:", plat)
  108. print("musl:", _get_musl_version(sys.executable))
  109. print("tags:", end=" ")
  110. for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])):
  111. print(t, end="\n ")