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