_musllinux.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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. from __future__ import annotations
  6. import functools
  7. import re
  8. import subprocess
  9. import sys
  10. from typing import TYPE_CHECKING, NamedTuple
  11. from ._elffile import ELFFile
  12. if TYPE_CHECKING:
  13. from collections.abc import Iterator, Sequence
  14. class _MuslVersion(NamedTuple):
  15. major: int
  16. minor: int
  17. def _parse_musl_version(output: str) -> _MuslVersion | None:
  18. lines = [n for n in (n.strip() for n in output.splitlines()) if n]
  19. if len(lines) < 2 or lines[0][:4] != "musl":
  20. return None
  21. m = re.match(r"Version (\d+)\.(\d+)", lines[1])
  22. if not m:
  23. return None
  24. return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
  25. @functools.lru_cache
  26. def _get_musl_version(executable: str) -> _MuslVersion | None:
  27. """Detect currently-running musl runtime version.
  28. This is done by checking the specified executable's dynamic linking
  29. information, and invoking the loader to parse its output for a version
  30. string. If the loader is musl, the output would be something like::
  31. musl libc (x86_64)
  32. Version 1.2.2
  33. Dynamic Program Loader
  34. """
  35. try:
  36. with open(executable, "rb") as f:
  37. ld = ELFFile(f).interpreter
  38. except (OSError, TypeError, ValueError):
  39. return None
  40. if ld is None or "musl" not in ld:
  41. return None
  42. proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
  43. return _parse_musl_version(proc.stderr)
  44. def platform_tags(archs: Sequence[str]) -> Iterator[str]:
  45. """Generate musllinux tags compatible to the current platform.
  46. :param archs: Sequence of compatible architectures.
  47. The first one shall be the closest to the actual architecture and be the part of
  48. platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
  49. The ``linux_`` prefix is assumed as a prerequisite for the current platform to
  50. be musllinux-compatible.
  51. :returns: An iterator of compatible musllinux tags.
  52. """
  53. sys_musl = _get_musl_version(sys.executable)
  54. if sys_musl is None: # Python not dynamically linked against musl.
  55. return
  56. for arch in archs:
  57. for minor in range(sys_musl.minor, -1, -1):
  58. yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
  59. if __name__ == "__main__": # pragma: no cover
  60. import sysconfig
  61. plat = sysconfig.get_platform()
  62. assert plat.startswith("linux-"), "not linux"
  63. print("plat:", plat)
  64. print("musl:", _get_musl_version(sys.executable))
  65. print("tags:", end=" ")
  66. for t in platform_tags([re.sub(r"[.-]", "_", plat.split("-", 1)[-1])]):
  67. print(t, end="\n ")