python_version.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: GPL-2.0-or-later
  3. # Copyright (c) 2017-2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
  4. """
  5. Handle Python version check logic.
  6. Not all Python versions are supported by scripts. Yet, on some cases,
  7. like during documentation build, a newer version of python could be
  8. available.
  9. This class allows checking if the minimal requirements are followed.
  10. Better than that, PythonVersion.check_python() not only checks the minimal
  11. requirements, but it automatically switches to a the newest available
  12. Python version if present.
  13. """
  14. import os
  15. import re
  16. import subprocess
  17. import shlex
  18. import sys
  19. from glob import glob
  20. from textwrap import indent
  21. class PythonVersion:
  22. """
  23. Ancillary methods that checks for missing dependencies for different
  24. types of types, like binaries, python modules, rpm deps, etc.
  25. """
  26. def __init__(self, version):
  27. """
  28. Ïnitialize self.version tuple from a version string.
  29. """
  30. self.version = self.parse_version(version)
  31. @staticmethod
  32. def parse_version(version):
  33. """
  34. Convert a major.minor.patch version into a tuple.
  35. """
  36. return tuple(int(x) for x in version.split("."))
  37. @staticmethod
  38. def ver_str(version):
  39. """
  40. Returns a version tuple as major.minor.patch.
  41. """
  42. return ".".join([str(x) for x in version])
  43. @staticmethod
  44. def cmd_print(cmd, max_len=80):
  45. """
  46. Outputs a command line, repecting maximum width.
  47. """
  48. cmd_line = []
  49. for w in cmd:
  50. w = shlex.quote(w)
  51. if cmd_line:
  52. if not max_len or len(cmd_line[-1]) + len(w) < max_len:
  53. cmd_line[-1] += " " + w
  54. continue
  55. else:
  56. cmd_line[-1] += " \\"
  57. cmd_line.append(w)
  58. else:
  59. cmd_line.append(w)
  60. return "\n ".join(cmd_line)
  61. def __str__(self):
  62. """
  63. Return a version tuple as major.minor.patch from self.version.
  64. """
  65. return self.ver_str(self.version)
  66. @staticmethod
  67. def get_python_version(cmd):
  68. """
  69. Get python version from a Python binary. As we need to detect if
  70. are out there newer python binaries, we can't rely on sys.release here.
  71. """
  72. kwargs = {}
  73. if sys.version_info < (3, 7):
  74. kwargs['universal_newlines'] = True
  75. else:
  76. kwargs['text'] = True
  77. result = subprocess.run([cmd, "--version"],
  78. stdout = subprocess.PIPE,
  79. stderr = subprocess.PIPE,
  80. **kwargs, check=False)
  81. version = result.stdout.strip()
  82. match = re.search(r"(\d+\.\d+\.\d+)", version)
  83. if match:
  84. return PythonVersion.parse_version(match.group(1))
  85. print(f"Can't parse version {version}")
  86. return (0, 0, 0)
  87. @staticmethod
  88. def find_python(min_version):
  89. """
  90. Detect if are out there any python 3.xy version newer than the
  91. current one.
  92. Note: this routine is limited to up to 2 digits for python3. We
  93. may need to update it one day, hopefully on a distant future.
  94. """
  95. patterns = [
  96. "python3.[0-9][0-9]",
  97. "python3.[0-9]",
  98. ]
  99. python_cmd = []
  100. # Seek for a python binary newer than min_version
  101. for path in os.getenv("PATH", "").split(":"):
  102. for pattern in patterns:
  103. for cmd in glob(os.path.join(path, pattern)):
  104. if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
  105. version = PythonVersion.get_python_version(cmd)
  106. if version >= min_version:
  107. python_cmd.append((version, cmd))
  108. return sorted(python_cmd, reverse=True)
  109. @staticmethod
  110. def check_python(min_version, show_alternatives=False, bail_out=False,
  111. success_on_error=False):
  112. """
  113. Check if the current python binary satisfies our minimal requirement
  114. for Sphinx build. If not, re-run with a newer version if found.
  115. """
  116. cur_ver = sys.version_info[:3]
  117. if cur_ver >= min_version:
  118. ver = PythonVersion.ver_str(cur_ver)
  119. return
  120. python_ver = PythonVersion.ver_str(cur_ver)
  121. available_versions = PythonVersion.find_python(min_version)
  122. if not available_versions:
  123. print(f"ERROR: Python version {python_ver} is not supported anymore\n")
  124. print(" Can't find a new version. This script may fail")
  125. return
  126. script_path = os.path.abspath(sys.argv[0])
  127. # Check possible alternatives
  128. if available_versions:
  129. new_python_cmd = available_versions[0][1]
  130. else:
  131. new_python_cmd = None
  132. if show_alternatives and available_versions:
  133. print("You could run, instead:")
  134. for _, cmd in available_versions:
  135. args = [cmd, script_path] + sys.argv[1:]
  136. cmd_str = indent(PythonVersion.cmd_print(args), " ")
  137. print(f"{cmd_str}\n")
  138. if bail_out:
  139. msg = f"Python {python_ver} not supported. Bailing out"
  140. if success_on_error:
  141. print(msg, file=sys.stderr)
  142. sys.exit(0)
  143. else:
  144. sys.exit(msg)
  145. print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
  146. # Restart script using the newer version
  147. args = [new_python_cmd, script_path] + sys.argv[1:]
  148. try:
  149. os.execv(new_python_cmd, args)
  150. except OSError as e:
  151. sys.exit(f"Failed to restart with {new_python_cmd}: {e}")