From adf9dc2592aad3e66b09cb412e60337477bafb0d Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Thu, 18 Sep 2025 13:54:44 +0200 Subject: tools/docs: python_version: move version check from sphinx-pre-install The sphinx-pre-install code has some logic to deal with Python version, which ensures that a minimal version will be enforced for documentation build logic. Move it to a separate library to allow re-using its code. No functional changes. Signed-off-by: Mauro Carvalho Chehab Message-ID: Signed-off-by: Jonathan Corbet --- tools/docs/lib/python_version.py | 158 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tools/docs/lib/python_version.py (limited to 'tools/docs/lib/python_version.py') diff --git a/tools/docs/lib/python_version.py b/tools/docs/lib/python_version.py new file mode 100644 index 000000000000..a9fda2470a26 --- /dev/null +++ b/tools/docs/lib/python_version.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (c) 2017-2025 Mauro Carvalho Chehab + +""" +Handle Python version check logic. + +Not all Python versions are supported by scripts. Yet, on some cases, +like during documentation build, a newer version of python could be +available. + +This class allows checking if the minimal requirements are followed. + +Better than that, PythonVersion.check_python() not only checks the minimal +requirements, but it automatically switches to a the newest available +Python version if present. + +""" + +import os +import re +import subprocess +import sys + +from glob import glob + +class PythonVersion: + """ + Ancillary methods that checks for missing dependencies for different + types of types, like binaries, python modules, rpm deps, etc. + """ + + def __init__(self, version): + """Ïnitialize self.version tuple from a version string""" + self.version = self.parse_version(version) + + @staticmethod + def parse_version(version): + """Convert a major.minor.patch version into a tuple""" + return tuple(int(x) for x in version.split(".")) + + @staticmethod + def ver_str(version): + """Returns a version tuple as major.minor.patch""" + return ".".join([str(x) for x in version]) + + def __str__(self): + """Returns a version tuple as major.minor.patch from self.version""" + return self.ver_str(self.version) + + @staticmethod + def get_python_version(cmd): + """ + Get python version from a Python binary. As we need to detect if + are out there newer python binaries, we can't rely on sys.release here. + """ + + kwargs = {} + if sys.version_info < (3, 7): + kwargs['universal_newlines'] = True + else: + kwargs['text'] = True + + result = subprocess.run([cmd, "--version"], + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + **kwargs, check=False) + + version = result.stdout.strip() + + match = re.search(r"(\d+\.\d+\.\d+)", version) + if match: + return PythonVersion.parse_version(match.group(1)) + + print(f"Can't parse version {version}") + return (0, 0, 0) + + @staticmethod + def find_python(min_version): + """ + Detect if are out there any python 3.xy version newer than the + current one. + + Note: this routine is limited to up to 2 digits for python3. We + may need to update it one day, hopefully on a distant future. + """ + patterns = [ + "python3.[0-9][0-9]", + "python3.[0-9]", + ] + + python_cmd = [] + + # Seek for a python binary newer than min_version + for path in os.getenv("PATH", "").split(":"): + for pattern in patterns: + for cmd in glob(os.path.join(path, pattern)): + if os.path.isfile(cmd) and os.access(cmd, os.X_OK): + version = PythonVersion.get_python_version(cmd) + if version >= min_version: + python_cmd.append((version, cmd)) + + return sorted(python_cmd, reverse=True) + + @staticmethod + def check_python(min_version, show_alternatives=False, bail_out=False, + success_on_error=False): + """ + Check if the current python binary satisfies our minimal requirement + for Sphinx build. If not, re-run with a newer version if found. + """ + cur_ver = sys.version_info[:3] + if cur_ver >= min_version: + ver = PythonVersion.ver_str(cur_ver) + return + + python_ver = PythonVersion.ver_str(cur_ver) + + available_versions = PythonVersion.find_python(min_version) + if not available_versions: + print(f"ERROR: Python version {python_ver} is not spported anymore\n") + print(" Can't find a new version. This script may fail") + return + + script_path = os.path.abspath(sys.argv[0]) + + # Check possible alternatives + if available_versions: + new_python_cmd = available_versions[0][1] + else: + new_python_cmd = None + + if show_alternatives: + print("You could run, instead:") + for _, cmd in available_versions: + args = [cmd, script_path] + sys.argv[1:] + + cmd_str = " ".join(args) + print(f" {cmd_str}") + print() + + if bail_out: + msg = f"Python {python_ver} not supported. Bailing out" + if success_on_error: + print(msg, file=sys.stderr) + sys.exit(0) + else: + sys.exit(msg) + + print(f"Python {python_ver} not supported. Changing to {new_python_cmd}") + + # Restart script using the newer version + args = [new_python_cmd, script_path] + sys.argv[1:] + + try: + os.execv(new_python_cmd, args) + except OSError as e: + sys.exit(f"Failed to restart with {new_python_cmd}: {e}") -- cgit v1.2.3 From 62ea383b449f3b51c3e0602c70ffa5fda29d3649 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Thu, 18 Sep 2025 13:54:57 +0200 Subject: tools/docs: sphinx-* break documentation bulds on openSUSE Before this patch, building htmldocs on opensuseLEAP works fine: # make htmldocs Available Python versions: /usr/bin/python3.11 Python 3.6.15 not supported. Changing to /usr/bin/python3.11 Python 3.6.15 not supported. Changing to /usr/bin/python3.11 Using alabaster theme Using Python kernel-doc ... As the logic detects that Python 3.6 is too old and recommends intalling python311-Sphinx. If installed, documentation builds work like a charm. Yet, some develpers complained that running python3.11 instead of python3 should not happen. So, let's break the build to make them happier: $ make htmldocs Python 3.6.15 not supported. Bailing out You could run, instead: /usr/bin/python3.11 tools/docs/sphinx-build-wrapper htmldocs \ --sphinxdirs=. --conf=conf.py --builddir=Documentation/output --theme= --css= \ --paper= Python 3.6.15 not supported. Bailing out make[2]: *** [Documentation/Makefile:76: htmldocs] Error 1 make[1]: *** [Makefile:1806: htmldocs] Error 2 make: *** [Makefile:248: __sub-make] Error 2 It should be noticed that: 1. after this change, sphinx-pre-install needs to be called by hand: $ /usr/bin/python3.11 tools/docs/sphinx-pre-install Detected OS: openSUSE Leap 15.6. Sphinx version: 7.2.6 All optional dependencies are met. Needed package dependencies are met. 2. sphinx-build-wrapper will auto-detect python3.11 and suggest a way to build the docs using the parameters passed via make variables. In this specific example: /usr/bin/python3.11 tools/docs/sphinx-build-wrapper htmldocs --sphinxdirs=. --conf=conf.py --theme= --css= --paper= 3. As this needs to be executed outside docs Makefile, it won't run the validation check scripts nor build Rust documentation if enabled, as the extra scripts are part of the docs Makefile. Signed-off-by: Mauro Carvalho Chehab Message-ID: <0635c311295300e9fb48c0ea607e2408910036e3.1758196090.git.mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet --- tools/docs/lib/python_version.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'tools/docs/lib/python_version.py') diff --git a/tools/docs/lib/python_version.py b/tools/docs/lib/python_version.py index a9fda2470a26..4fde1b882164 100644 --- a/tools/docs/lib/python_version.py +++ b/tools/docs/lib/python_version.py @@ -20,9 +20,11 @@ Python version if present. import os import re import subprocess +import shlex import sys from glob import glob +from textwrap import indent class PythonVersion: """ @@ -44,6 +46,25 @@ class PythonVersion: """Returns a version tuple as major.minor.patch""" return ".".join([str(x) for x in version]) + @staticmethod + def cmd_print(cmd, max_len=80): + cmd_line = [] + + for w in cmd: + w = shlex.quote(w) + + if cmd_line: + if not max_len or len(cmd_line[-1]) + len(w) < max_len: + cmd_line[-1] += " " + w + continue + else: + cmd_line[-1] += " \\" + cmd_line.append(w) + else: + cmd_line.append(w) + + return "\n ".join(cmd_line) + def __str__(self): """Returns a version tuple as major.minor.patch from self.version""" return self.ver_str(self.version) @@ -130,14 +151,13 @@ class PythonVersion: else: new_python_cmd = None - if show_alternatives: + if show_alternatives and available_versions: print("You could run, instead:") for _, cmd in available_versions: args = [cmd, script_path] + sys.argv[1:] - cmd_str = " ".join(args) - print(f" {cmd_str}") - print() + cmd_str = indent(PythonVersion.cmd_print(args), " ") + print(f"{cmd_str}\n") if bail_out: msg = f"Python {python_ver} not supported. Bailing out" -- cgit v1.2.3