xref: /linux/tools/lib/python/kdoc/python_version.py (revision 72c395024dac5e215136cbff793455f065603b06)
1adf9dc25SMauro Carvalho Chehab#!/usr/bin/env python3
2adf9dc25SMauro Carvalho Chehab# SPDX-License-Identifier: GPL-2.0-or-later
3adf9dc25SMauro Carvalho Chehab# Copyright (c) 2017-2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
4adf9dc25SMauro Carvalho Chehab
5adf9dc25SMauro Carvalho Chehab"""
6adf9dc25SMauro Carvalho ChehabHandle Python version check logic.
7adf9dc25SMauro Carvalho Chehab
8adf9dc25SMauro Carvalho ChehabNot all Python versions are supported by scripts. Yet, on some cases,
9adf9dc25SMauro Carvalho Chehablike during documentation build, a newer version of python could be
10adf9dc25SMauro Carvalho Chehabavailable.
11adf9dc25SMauro Carvalho Chehab
12adf9dc25SMauro Carvalho ChehabThis class allows checking if the minimal requirements are followed.
13adf9dc25SMauro Carvalho Chehab
14adf9dc25SMauro Carvalho ChehabBetter than that, PythonVersion.check_python() not only checks the minimal
15adf9dc25SMauro Carvalho Chehabrequirements, but it automatically switches to a the newest available
16adf9dc25SMauro Carvalho ChehabPython version if present.
17adf9dc25SMauro Carvalho Chehab
18adf9dc25SMauro Carvalho Chehab"""
19adf9dc25SMauro Carvalho Chehab
20adf9dc25SMauro Carvalho Chehabimport os
21adf9dc25SMauro Carvalho Chehabimport re
22adf9dc25SMauro Carvalho Chehabimport subprocess
2362ea383bSMauro Carvalho Chehabimport shlex
24adf9dc25SMauro Carvalho Chehabimport sys
25adf9dc25SMauro Carvalho Chehab
26adf9dc25SMauro Carvalho Chehabfrom glob import glob
2762ea383bSMauro Carvalho Chehabfrom textwrap import indent
28adf9dc25SMauro Carvalho Chehab
29adf9dc25SMauro Carvalho Chehabclass PythonVersion:
30adf9dc25SMauro Carvalho Chehab    """
31adf9dc25SMauro Carvalho Chehab    Ancillary methods that checks for missing dependencies for different
32adf9dc25SMauro Carvalho Chehab    types of types, like binaries, python modules, rpm deps, etc.
33adf9dc25SMauro Carvalho Chehab    """
34adf9dc25SMauro Carvalho Chehab
35adf9dc25SMauro Carvalho Chehab    def __init__(self, version):
36*33220c1fSMauro Carvalho Chehab        """
37*33220c1fSMauro Carvalho Chehab        Ïnitialize self.version tuple from a version string.
38*33220c1fSMauro Carvalho Chehab        """
39adf9dc25SMauro Carvalho Chehab        self.version = self.parse_version(version)
40adf9dc25SMauro Carvalho Chehab
41adf9dc25SMauro Carvalho Chehab    @staticmethod
42adf9dc25SMauro Carvalho Chehab    def parse_version(version):
43*33220c1fSMauro Carvalho Chehab        """
44*33220c1fSMauro Carvalho Chehab        Convert a major.minor.patch version into a tuple.
45*33220c1fSMauro Carvalho Chehab        """
46adf9dc25SMauro Carvalho Chehab        return tuple(int(x) for x in version.split("."))
47adf9dc25SMauro Carvalho Chehab
48adf9dc25SMauro Carvalho Chehab    @staticmethod
49adf9dc25SMauro Carvalho Chehab    def ver_str(version):
50*33220c1fSMauro Carvalho Chehab        """
51*33220c1fSMauro Carvalho Chehab        Returns a version tuple as major.minor.patch.
52*33220c1fSMauro Carvalho Chehab        """
53adf9dc25SMauro Carvalho Chehab        return ".".join([str(x) for x in version])
54adf9dc25SMauro Carvalho Chehab
5562ea383bSMauro Carvalho Chehab    @staticmethod
5662ea383bSMauro Carvalho Chehab    def cmd_print(cmd, max_len=80):
57*33220c1fSMauro Carvalho Chehab        """
58*33220c1fSMauro Carvalho Chehab        Outputs a command line, repecting maximum width.
59*33220c1fSMauro Carvalho Chehab        """
60*33220c1fSMauro Carvalho Chehab
6162ea383bSMauro Carvalho Chehab        cmd_line = []
6262ea383bSMauro Carvalho Chehab
6362ea383bSMauro Carvalho Chehab        for w in cmd:
6462ea383bSMauro Carvalho Chehab            w = shlex.quote(w)
6562ea383bSMauro Carvalho Chehab
6662ea383bSMauro Carvalho Chehab            if cmd_line:
6762ea383bSMauro Carvalho Chehab                if not max_len or len(cmd_line[-1]) + len(w) < max_len:
6862ea383bSMauro Carvalho Chehab                    cmd_line[-1] += " " + w
6962ea383bSMauro Carvalho Chehab                    continue
7062ea383bSMauro Carvalho Chehab                else:
7162ea383bSMauro Carvalho Chehab                    cmd_line[-1] += " \\"
7262ea383bSMauro Carvalho Chehab                    cmd_line.append(w)
7362ea383bSMauro Carvalho Chehab            else:
7462ea383bSMauro Carvalho Chehab                cmd_line.append(w)
7562ea383bSMauro Carvalho Chehab
7662ea383bSMauro Carvalho Chehab        return "\n  ".join(cmd_line)
7762ea383bSMauro Carvalho Chehab
78adf9dc25SMauro Carvalho Chehab    def __str__(self):
79*33220c1fSMauro Carvalho Chehab        """
80*33220c1fSMauro Carvalho Chehab        Return a version tuple as major.minor.patch from self.version.
81*33220c1fSMauro Carvalho Chehab        """
82adf9dc25SMauro Carvalho Chehab        return self.ver_str(self.version)
83adf9dc25SMauro Carvalho Chehab
84adf9dc25SMauro Carvalho Chehab    @staticmethod
85adf9dc25SMauro Carvalho Chehab    def get_python_version(cmd):
86adf9dc25SMauro Carvalho Chehab        """
87adf9dc25SMauro Carvalho Chehab        Get python version from a Python binary. As we need to detect if
88adf9dc25SMauro Carvalho Chehab        are out there newer python binaries, we can't rely on sys.release here.
89adf9dc25SMauro Carvalho Chehab        """
90adf9dc25SMauro Carvalho Chehab
91adf9dc25SMauro Carvalho Chehab        kwargs = {}
92adf9dc25SMauro Carvalho Chehab        if sys.version_info < (3, 7):
93adf9dc25SMauro Carvalho Chehab            kwargs['universal_newlines'] = True
94adf9dc25SMauro Carvalho Chehab        else:
95adf9dc25SMauro Carvalho Chehab            kwargs['text'] = True
96adf9dc25SMauro Carvalho Chehab
97adf9dc25SMauro Carvalho Chehab        result = subprocess.run([cmd, "--version"],
98adf9dc25SMauro Carvalho Chehab                                stdout = subprocess.PIPE,
99adf9dc25SMauro Carvalho Chehab                                stderr = subprocess.PIPE,
100adf9dc25SMauro Carvalho Chehab                                **kwargs, check=False)
101adf9dc25SMauro Carvalho Chehab
102adf9dc25SMauro Carvalho Chehab        version = result.stdout.strip()
103adf9dc25SMauro Carvalho Chehab
104adf9dc25SMauro Carvalho Chehab        match = re.search(r"(\d+\.\d+\.\d+)", version)
105adf9dc25SMauro Carvalho Chehab        if match:
106adf9dc25SMauro Carvalho Chehab            return PythonVersion.parse_version(match.group(1))
107adf9dc25SMauro Carvalho Chehab
108adf9dc25SMauro Carvalho Chehab        print(f"Can't parse version {version}")
109adf9dc25SMauro Carvalho Chehab        return (0, 0, 0)
110adf9dc25SMauro Carvalho Chehab
111adf9dc25SMauro Carvalho Chehab    @staticmethod
112adf9dc25SMauro Carvalho Chehab    def find_python(min_version):
113adf9dc25SMauro Carvalho Chehab        """
114adf9dc25SMauro Carvalho Chehab        Detect if are out there any python 3.xy version newer than the
115adf9dc25SMauro Carvalho Chehab        current one.
116adf9dc25SMauro Carvalho Chehab
117adf9dc25SMauro Carvalho Chehab        Note: this routine is limited to up to 2 digits for python3. We
118adf9dc25SMauro Carvalho Chehab        may need to update it one day, hopefully on a distant future.
119adf9dc25SMauro Carvalho Chehab        """
120adf9dc25SMauro Carvalho Chehab        patterns = [
121adf9dc25SMauro Carvalho Chehab            "python3.[0-9][0-9]",
122adf9dc25SMauro Carvalho Chehab            "python3.[0-9]",
123adf9dc25SMauro Carvalho Chehab        ]
124adf9dc25SMauro Carvalho Chehab
125adf9dc25SMauro Carvalho Chehab        python_cmd = []
126adf9dc25SMauro Carvalho Chehab
127adf9dc25SMauro Carvalho Chehab        # Seek for a python binary newer than min_version
128adf9dc25SMauro Carvalho Chehab        for path in os.getenv("PATH", "").split(":"):
129adf9dc25SMauro Carvalho Chehab            for pattern in patterns:
130adf9dc25SMauro Carvalho Chehab                for cmd in glob(os.path.join(path, pattern)):
131adf9dc25SMauro Carvalho Chehab                    if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
132adf9dc25SMauro Carvalho Chehab                        version = PythonVersion.get_python_version(cmd)
133adf9dc25SMauro Carvalho Chehab                        if version >= min_version:
134adf9dc25SMauro Carvalho Chehab                            python_cmd.append((version, cmd))
135adf9dc25SMauro Carvalho Chehab
136adf9dc25SMauro Carvalho Chehab        return sorted(python_cmd, reverse=True)
137adf9dc25SMauro Carvalho Chehab
138adf9dc25SMauro Carvalho Chehab    @staticmethod
139adf9dc25SMauro Carvalho Chehab    def check_python(min_version, show_alternatives=False, bail_out=False,
140adf9dc25SMauro Carvalho Chehab                     success_on_error=False):
141adf9dc25SMauro Carvalho Chehab        """
142adf9dc25SMauro Carvalho Chehab        Check if the current python binary satisfies our minimal requirement
143adf9dc25SMauro Carvalho Chehab        for Sphinx build. If not, re-run with a newer version if found.
144adf9dc25SMauro Carvalho Chehab        """
145adf9dc25SMauro Carvalho Chehab        cur_ver = sys.version_info[:3]
146adf9dc25SMauro Carvalho Chehab        if cur_ver >= min_version:
147adf9dc25SMauro Carvalho Chehab            ver = PythonVersion.ver_str(cur_ver)
148adf9dc25SMauro Carvalho Chehab            return
149adf9dc25SMauro Carvalho Chehab
150adf9dc25SMauro Carvalho Chehab        python_ver = PythonVersion.ver_str(cur_ver)
151adf9dc25SMauro Carvalho Chehab
152adf9dc25SMauro Carvalho Chehab        available_versions = PythonVersion.find_python(min_version)
153adf9dc25SMauro Carvalho Chehab        if not available_versions:
1545f88f44dSRandy Dunlap            print(f"ERROR: Python version {python_ver} is not supported anymore\n")
155adf9dc25SMauro Carvalho Chehab            print("       Can't find a new version. This script may fail")
156adf9dc25SMauro Carvalho Chehab            return
157adf9dc25SMauro Carvalho Chehab
158adf9dc25SMauro Carvalho Chehab        script_path = os.path.abspath(sys.argv[0])
159adf9dc25SMauro Carvalho Chehab
160adf9dc25SMauro Carvalho Chehab        # Check possible alternatives
161adf9dc25SMauro Carvalho Chehab        if available_versions:
162adf9dc25SMauro Carvalho Chehab            new_python_cmd = available_versions[0][1]
163adf9dc25SMauro Carvalho Chehab        else:
164adf9dc25SMauro Carvalho Chehab            new_python_cmd = None
165adf9dc25SMauro Carvalho Chehab
16662ea383bSMauro Carvalho Chehab        if show_alternatives and available_versions:
167adf9dc25SMauro Carvalho Chehab            print("You could run, instead:")
168adf9dc25SMauro Carvalho Chehab            for _, cmd in available_versions:
169adf9dc25SMauro Carvalho Chehab                args = [cmd, script_path] + sys.argv[1:]
170adf9dc25SMauro Carvalho Chehab
17162ea383bSMauro Carvalho Chehab                cmd_str = indent(PythonVersion.cmd_print(args), "  ")
17262ea383bSMauro Carvalho Chehab                print(f"{cmd_str}\n")
173adf9dc25SMauro Carvalho Chehab
174adf9dc25SMauro Carvalho Chehab        if bail_out:
175adf9dc25SMauro Carvalho Chehab            msg = f"Python {python_ver} not supported. Bailing out"
176adf9dc25SMauro Carvalho Chehab            if success_on_error:
177adf9dc25SMauro Carvalho Chehab                print(msg, file=sys.stderr)
178adf9dc25SMauro Carvalho Chehab                sys.exit(0)
179adf9dc25SMauro Carvalho Chehab            else:
180adf9dc25SMauro Carvalho Chehab                sys.exit(msg)
181adf9dc25SMauro Carvalho Chehab
182adf9dc25SMauro Carvalho Chehab        print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
183adf9dc25SMauro Carvalho Chehab
184adf9dc25SMauro Carvalho Chehab        # Restart script using the newer version
185adf9dc25SMauro Carvalho Chehab        args = [new_python_cmd, script_path] + sys.argv[1:]
186adf9dc25SMauro Carvalho Chehab
187adf9dc25SMauro Carvalho Chehab        try:
188adf9dc25SMauro Carvalho Chehab            os.execv(new_python_cmd, args)
189adf9dc25SMauro Carvalho Chehab        except OSError as e:
190adf9dc25SMauro Carvalho Chehab            sys.exit(f"Failed to restart with {new_python_cmd}: {e}")
191