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 """ 37 Ïnitialize self.version tuple from a version string. 38 """ 39 self.version = self.parse_version(version) 40 41 @staticmethod 42 def parse_version(version): 43 """ 44 Convert a major.minor.patch version into a tuple. 45 """ 46 return tuple(int(x) for x in version.split(".")) 47 48 @staticmethod 49 def ver_str(version): 50 """ 51 Returns a version tuple as major.minor.patch. 52 """ 53 return ".".join([str(x) for x in version]) 54 55 @staticmethod 56 def cmd_print(cmd, max_len=80): 57 """ 58 Outputs a command line, repecting maximum width. 59 """ 60 61 cmd_line = [] 62 63 for w in cmd: 64 w = shlex.quote(w) 65 66 if cmd_line: 67 if not max_len or len(cmd_line[-1]) + len(w) < max_len: 68 cmd_line[-1] += " " + w 69 continue 70 else: 71 cmd_line[-1] += " \\" 72 cmd_line.append(w) 73 else: 74 cmd_line.append(w) 75 76 return "\n ".join(cmd_line) 77 78 def __str__(self): 79 """ 80 Return a version tuple as major.minor.patch from self.version. 81 """ 82 return self.ver_str(self.version) 83 84 @staticmethod 85 def get_python_version(cmd): 86 """ 87 Get python version from a Python binary. As we need to detect if 88 are out there newer python binaries, we can't rely on sys.release here. 89 """ 90 91 kwargs = {} 92 if sys.version_info < (3, 7): 93 kwargs['universal_newlines'] = True 94 else: 95 kwargs['text'] = True 96 97 result = subprocess.run([cmd, "--version"], 98 stdout = subprocess.PIPE, 99 stderr = subprocess.PIPE, 100 **kwargs, check=False) 101 102 version = result.stdout.strip() 103 104 match = re.search(r"(\d+\.\d+\.\d+)", version) 105 if match: 106 return PythonVersion.parse_version(match.group(1)) 107 108 print(f"Can't parse version {version}") 109 return (0, 0, 0) 110 111 @staticmethod 112 def find_python(min_version): 113 """ 114 Detect if are out there any python 3.xy version newer than the 115 current one. 116 117 Note: this routine is limited to up to 2 digits for python3. We 118 may need to update it one day, hopefully on a distant future. 119 """ 120 patterns = [ 121 "python3.[0-9][0-9]", 122 "python3.[0-9]", 123 ] 124 125 python_cmd = [] 126 127 # Seek for a python binary newer than min_version 128 for path in os.getenv("PATH", "").split(":"): 129 for pattern in patterns: 130 for cmd in glob(os.path.join(path, pattern)): 131 if os.path.isfile(cmd) and os.access(cmd, os.X_OK): 132 version = PythonVersion.get_python_version(cmd) 133 if version >= min_version: 134 python_cmd.append((version, cmd)) 135 136 return sorted(python_cmd, reverse=True) 137 138 @staticmethod 139 def check_python(min_version, show_alternatives=False, bail_out=False, 140 success_on_error=False): 141 """ 142 Check if the current python binary satisfies our minimal requirement 143 for Sphinx build. If not, re-run with a newer version if found. 144 """ 145 cur_ver = sys.version_info[:3] 146 if cur_ver >= min_version: 147 ver = PythonVersion.ver_str(cur_ver) 148 return 149 150 python_ver = PythonVersion.ver_str(cur_ver) 151 152 available_versions = PythonVersion.find_python(min_version) 153 if not available_versions: 154 print(f"ERROR: Python version {python_ver} is not supported anymore\n") 155 print(" Can't find a new version. This script may fail") 156 return 157 158 script_path = os.path.abspath(sys.argv[0]) 159 160 # Check possible alternatives 161 if available_versions: 162 new_python_cmd = available_versions[0][1] 163 else: 164 new_python_cmd = None 165 166 if show_alternatives and available_versions: 167 print("You could run, instead:") 168 for _, cmd in available_versions: 169 args = [cmd, script_path] + sys.argv[1:] 170 171 cmd_str = indent(PythonVersion.cmd_print(args), " ") 172 print(f"{cmd_str}\n") 173 174 if bail_out: 175 msg = f"Python {python_ver} not supported. Bailing out" 176 if success_on_error: 177 print(msg, file=sys.stderr) 178 sys.exit(0) 179 else: 180 sys.exit(msg) 181 182 print(f"Python {python_ver} not supported. Changing to {new_python_cmd}") 183 184 # Restart script using the newer version 185 args = [new_python_cmd, script_path] + sys.argv[1:] 186 187 try: 188 os.execv(new_python_cmd, args) 189 except OSError as e: 190 sys.exit(f"Failed to restart with {new_python_cmd}: {e}") 191