Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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}")