lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <54c314960cc51518c9f50edf77af993d66153fe3.1750924501.git.mchehab+huawei@kernel.org>
Date: Thu, 26 Jun 2025 10:06:30 +0200
From: Mauro Carvalho Chehab <mchehab+huawei@...nel.org>
To: Linux Doc Mailing List <linux-doc@...r.kernel.org>,
	Jonathan Corbet <corbet@....net>
Cc: Mauro Carvalho Chehab <mchehab+huawei@...nel.org>,
	"Akira Yokosawa" <akiyks@...il.com>,
	"Mauro Carvalho Chehab" <mchehab+huawei@...nel.org>,
	"Sai Vishnu M" <saivishnu725@...il.com>,
	linux-kernel@...r.kernel.org
Subject: [PATCH 2/3] scripts: sphinx-pre-install: Convert script to Python

Port scripts/sphinx-pre-install to Python. That allows
a better maintainance of the file.

Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@...nel.org>
---
 scripts/sphinx-pre-install.py | 1102 +++++++++++++++++++++++++++++++++
 1 file changed, 1102 insertions(+)
 create mode 100755 scripts/sphinx-pre-install.py

diff --git a/scripts/sphinx-pre-install.py b/scripts/sphinx-pre-install.py
new file mode 100755
index 000000000000..ad6384166916
--- /dev/null
+++ b/scripts/sphinx-pre-install.py
@@ -0,0 +1,1102 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2017-2025 Mauro Carvalho Chehab <mchehab+huawei@...nel.org>
+#
+# pylint: disable=C0103,C0114,C0115,C0116,C0301
+# pylint: disable=R0902,R0904,R0912,R0915,R1705,R1710,E1121
+
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from glob import glob
+
+
+def parse_version(version):
+    """Convert a major.minor.patch version into a tuple"""
+#
+    return tuple(int(x) for x in version.split("."))
+
+
+def ver_str(version):
+    """Returns a version tuple as major.minor.patch"""
+
+    return ".".join([str(x) for x in version])
+
+
+RECOMMENDED_VERSION = parse_version("3.4.3")
+
+
+class SphinxDependencyChecker:
+    # List of required texlive packages on Fedora and OpenSuse
+    texlive = {
+        "amsfonts.sty":       "texlive-amsfonts",
+        "amsmath.sty":        "texlive-amsmath",
+        "amssymb.sty":        "texlive-amsfonts",
+        "amsthm.sty":         "texlive-amscls",
+        "anyfontsize.sty":    "texlive-anyfontsize",
+        "atbegshi.sty":       "texlive-oberdiek",
+        "bm.sty":             "texlive-tools",
+        "capt-of.sty":        "texlive-capt-of",
+        "cmap.sty":           "texlive-cmap",
+        "ctexhook.sty":       "texlive-ctex",
+        "ecrm1000.tfm":       "texlive-ec",
+        "eqparbox.sty":       "texlive-eqparbox",
+        "eu1enc.def":         "texlive-euenc",
+        "fancybox.sty":       "texlive-fancybox",
+        "fancyvrb.sty":       "texlive-fancyvrb",
+        "float.sty":          "texlive-float",
+        "fncychap.sty":       "texlive-fncychap",
+        "footnote.sty":       "texlive-mdwtools",
+        "framed.sty":         "texlive-framed",
+        "luatex85.sty":       "texlive-luatex85",
+        "multirow.sty":       "texlive-multirow",
+        "needspace.sty":      "texlive-needspace",
+        "palatino.sty":       "texlive-psnfss",
+        "parskip.sty":        "texlive-parskip",
+        "polyglossia.sty":    "texlive-polyglossia",
+        "tabulary.sty":       "texlive-tabulary",
+        "threeparttable.sty": "texlive-threeparttable",
+        "titlesec.sty":       "texlive-titlesec",
+        "ucs.sty":            "texlive-ucs",
+        "upquote.sty":        "texlive-upquote",
+        "wrapfig.sty":        "texlive-wrapfig",
+    }
+
+    def __init__(self, args):
+        self.pdf = args.pdf
+        self.virtualenv = args.virtualenv
+        self.version_check = args.version_check
+
+        self.missing = {}
+
+        self.need = 0
+        self.optional = 0
+        self.need_symlink = 0
+        self.need_sphinx = 0
+        self.need_pip = 0
+        self.need_virtualenv = 0
+        self.rec_sphinx_upgrade = 0
+        self.verbose_warn_install = 1
+
+        self.system_release = ""
+        self.install = ""
+        self.virtenv_dir = ""
+        self.python_cmd = ""
+        self.activate_cmd = ""
+
+        self.min_version = (0, 0, 0)
+        self.cur_version = (0, 0, 0)
+        self.latest_avail_ver = (0, 0, 0)
+        self.venv_ver = (0, 0, 0)
+
+        prefix = os.environ.get("srctree", ".") + "/"
+
+        self.conf = prefix + "Documentation/conf.py"
+        self.requirement_file = prefix + "Documentation/sphinx/requirements.txt"
+        self.virtenv_prefix = ["sphinx_", "Sphinx_" ]
+
+    #
+    # Ancillary methods that don't depend on self
+    #
+
+    @staticmethod
+    def which(prog):
+        for path in os.environ.get("PATH", "").split(":"):
+            full_path = os.path.join(path, prog)
+            if os.access(full_path, os.X_OK):
+                return full_path
+
+        return None
+
+    @staticmethod
+    def find_python_no_venv():
+        # FIXME: does it makes sense now that this script is in Python?
+
+        result = subprocess.run(["pwd"], capture_output=True, text=True)
+        cur_dir = result.stdout.strip()
+
+        python_names = ["python3", "python"]
+
+        for d in os.environ.get("PATH", "").split(":"):
+            if f"{cur_dir}/sphinx" in d:
+                continue
+
+            for p in python_names:
+                if os.access(os.path.join(d, p), os.X_OK):
+                    return os.path.join(d, p)
+
+        # Python not found at the PATH
+        return python_names[-1]
+
+    @staticmethod
+    def run(*args, **kwargs):
+        """Excecute a command, hiding its output by default"""
+
+        if not kwargs.get('capture_output', False):
+            if 'stdout' not in kwargs:
+                kwargs['stdout'] = subprocess.DEVNULL
+            if 'stderr' not in kwargs:
+                kwargs['stderr'] = subprocess.DEVNULL
+
+        return subprocess.run(*args, **kwargs)
+
+    #
+    # Methods to check if a feature exists
+    #
+
+    # Note: is_optional has 3 states:
+    #   - 0: mandatory
+    #   - 1: optional, but nice to have
+    #   - 2: LaTeX optional - pdf builds without it, but may have visual impact
+
+    def check_missing(self, progs):
+        for prog, is_optional in sorted(self.missing.items()):
+            # At least on some LTS distros like CentOS 7, texlive doesn't
+            # provide all packages we need. When such distros are
+            # detected, we have to disable PDF output.
+            #
+            # So, we need to ignore the packages that distros would
+            # need for LaTeX to work
+            if is_optional == 2 and not self.pdf:
+                self.optional -= 1
+                continue
+
+            if self.verbose_warn_install:
+                if is_optional:
+                    print(f'Warning: better to also install "{prog}".')
+                else:
+                    print(f'ERROR: please install "{prog}", otherwise, build won\'t work.')
+
+            self.install += " " + progs.get(prog, prog)
+
+        self.install = self.install.lstrip()
+
+    def add_package(self, package, is_optional):
+        self.missing[package] = is_optional
+        if is_optional:
+            self.optional += 1
+        else:
+            self.need += 1
+
+    def check_missing_file(self, files, package, is_optional):
+        for f in files:
+            if os.path.exists(f):
+                return
+        self.add_package(package, is_optional)
+
+    def check_program(self, prog, is_optional):
+        found = self.which(prog)
+        if found:
+            return found
+
+        self.add_package(prog, is_optional)
+
+        return None
+
+    def check_perl_module(self, prog, is_optional):
+        # While testing with lxc download template, one of the
+        # distros (Oracle) didn't have perl - nor even an option to install
+        # before installing oraclelinux-release-el9 package.
+        #
+        # Check it before running an error. If perl is not there,
+        # add it as a mandatory package, as some parts of the doc builder
+        # needs it.
+        if not self.which("perl"):
+            self.add_package("perl", 0)
+            self.add_package(prog, is_optional)
+            return
+
+        try:
+            self.run(["perl", f"-M{prog}", "-e", "1"], check=True)
+        except subprocess.CalledProcessError:
+            self.add_package(prog, is_optional)
+
+    def check_python_module(self, module, is_optional):
+        # FIXME: is it needed at the Python version? Maybe due to venv?
+        if not self.python_cmd:
+            return
+
+        try:
+            self.run([self.python_cmd, "-c", f"import {module}"], check=True)
+        except subprocess.CalledProcessError:
+            self.add_package(module, is_optional)
+
+    def check_rpm_missing(self, pkgs, is_optional):
+        for prog in pkgs:
+            try:
+                self.run(["rpm", "-q", prog], check=True)
+            except subprocess.CalledProcessError:
+                self.add_package(prog, is_optional)
+
+    def check_pacman_missing(self, pkgs, is_optional):
+        for prog in pkgs:
+            try:
+                self.run(["pacman", "-Q", prog], check=True)
+            except subprocess.CalledProcessError:
+                self.add_package(prog, is_optional)
+
+    def check_missing_tex(self, is_optional):
+        kpsewhich = self.which("kpsewhich")
+        for prog, package in self.texlive.items():
+
+            # If kpsewhich is not there, just add it to deps
+            if not kpsewhich:
+                self.add_package(package, is_optional)
+                continue
+
+            # Check if the package is needed
+            try:
+                result = self.run(
+                    [kpsewhich, prog], stdout=subprocess.PIPE, text=True, check=True
+                )
+
+                # Didn't find. Add it
+                if not result.stdout.strip():
+                    self.add_package(package, is_optional)
+
+            except subprocess.CalledProcessError:
+                # kpsewhich returned an error. Add it, just in case
+                self.add_package(package, is_optional)
+
+    def get_sphinx_fname(self):
+        if "SPHINXBUILD" in os.environ:
+            return os.environ["SPHINXBUILD"]
+
+        fname = "sphinx-build"
+        if self.which(fname):
+            return fname
+
+        fname = "sphinx-build-3"
+        if self.which(fname):
+            self.need_symlink = 1
+            return fname
+
+        return ""
+
+    def get_sphinx_version(self, cmd):
+        try:
+            result = self.run([cmd, "--version"],
+                              stdout=subprocess.PIPE,
+                              stderr=subprocess.STDOUT,
+                              text=True, check=True)
+        except (subprocess.CalledProcessError, FileNotFoundError):
+            return None
+
+        for line in result.stdout.split("\n"):
+            match = re.match(r"^sphinx-build\s+([\d\.]+)(?:\+(?:/[\da-f]+)|b\d+)?\s*$", line)
+            if match:
+                return parse_version(m1.group(1))
+
+            match = re.match(r"^Sphinx.*\s+([\d\.]+)\s*$", line)
+            if match:
+                return parse_version(m2.group(1))
+
+    def check_sphinx(self):
+        try:
+            with open(self.conf, "r", encoding="utf-8") as f:
+                for line in f:
+                    match = re.match(r"^\s*needs_sphinx\s*=\s*[\'\"]([\d\.]+)[\'\"]", line)
+                    if match:
+                        self.min_version = parse_version(match.group(1))
+                        break
+        except IOError:
+            sys.exit(f"Can't open {self.conf}")
+
+        if not self.min_version:
+            sys.exit(f"Can't get needs_sphinx version from {self.conf}")
+
+        self.virtenv_dir = self.virtenv_prefix[0] + "latest"
+
+        sphinx = self.get_sphinx_fname()
+        if not sphinx:
+            self.need_sphinx = 1
+            return
+
+        self.cur_version = self.get_sphinx_version(sphinx)
+        if not self.cur_version:
+            sys.exit(f"{sphinx} didn't return its version")
+
+        if self.cur_version < self.min_version:
+            curver = ver_str(self.cur_version)
+            minver = ver_str(self.min_version)
+
+            print(f"ERROR: Sphinx version is {curver}. It should be >= {minver}")
+            self.need_sphinx = 1
+            return
+
+        # On version check mode, just assume Sphinx has all mandatory deps
+        if self.version_check and self.cur_version >= RECOMMENDED_VERSION:
+            sys.exit(0)
+
+    def catcheck(self, filename):
+        if os.path.exists(filename):
+            with open(filename, "r", encoding="utf-8") as f:
+                return f.read().strip()
+        return ""
+
+    #
+    # Distro-specific hints methods
+    #
+
+    def give_debian_hints(self):
+        progs = {
+            "Pod::Usage":    "perl-modules",
+            "convert":       "imagemagick",
+            "dot":           "graphviz",
+            "ensurepip":     "python3-venv",
+            "python-sphinx": "python3-sphinx",
+            "rsvg-convert":  "librsvg2-bin",
+            "virtualenv":    "virtualenv",
+            "xelatex":       "texlive-xetex",
+            "yaml":          "python3-yaml",
+        }
+
+        if self.pdf:
+            pdf_pkgs = {
+                "texlive-lang-chinese": [
+                    "/usr/share/texlive/texmf-dist/tex/latex/ctex/ctexhook.sty",
+                ],
+                "fonts-dejavu": [
+                    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
+                ],
+                "fonts-noto-cjk": [
+                    "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
+                    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
+                    "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
+                ],
+            }
+
+            for package, files in pdf_pkgs.items():
+                self.check_missing_file(files, package, 2)
+
+            self.check_program("dvipng", 2)
+
+        self.check_missing(progs)
+
+        if not self.need and not self.optional:
+            return
+
+        if self.verbose_warn_install:
+            print("You should run:")
+        print(f"\n\tsudo apt-get install {self.install}")
+
+    def give_redhat_hints(self):
+        progs = {
+            "Pod::Usage":       "perl-Pod-Usage",
+            "convert":          "ImageMagick",
+            "dot":              "graphviz",
+            "python-sphinx":    "python3-sphinx",
+            "rsvg-convert":     "librsvg2-tools",
+            "virtualenv":       "python3-virtualenv",
+            "xelatex":          "texlive-xetex-bin",
+            "yaml":             "python3-pyyaml",
+        }
+
+        fedora26_opt_pkgs = [
+            "graphviz-gd",  # Fedora 26: needed for PDF support
+        ]
+
+        fedora_tex_pkgs = [
+            "dejavu-sans-fonts",
+            "dejavu-sans-mono-fonts",
+            "dejavu-serif-fonts",
+            "texlive-collection-fontsrecommended",
+            "texlive-collection-latex",
+            "texlive-xecjk",
+        ]
+
+        old = 0
+        rel = None
+        pkg_manager = "dnf"
+
+        match = re.search(r"(release|Linux)\s+(\d+)", self.system_release)
+        if match:
+            rel = int(match.group(2))
+
+        if not rel:
+            print("Couldn't identify release number")
+            old = 1
+            self.pdf = False
+        elif re.search("Fedora", self.system_release):
+            # Fedora 38 and upper use this CJK font
+
+            noto_sans_redhat = "google-noto-sans-cjk-fonts"
+        else:
+            # Almalinux, CentOS, RHEL, ...
+
+            # at least up to version 9 (and Fedora < 38), that's the CJK font
+            noto_sans_redhat = "google-noto-sans-cjk-ttc-fonts"
+
+            progs["virtualenv"] = "python-virtualenv"
+
+            if rel and rel < 8:
+                old = 1
+                self.pdf = False
+
+                # RHEL 7 is in ELS, currently up to Jun, 2026
+
+                print("Note: texlive packages on RHEL/CENTOS <= 7 are incomplete. Can't support PDF output")
+                print("If you want to build PDF, please read:")
+                print("\thttps://www.systutorials.com/241660/how-to-install-tex-live-on-centos-7-linux/")
+
+        if self.pdf:
+            pdf_pkgs = [
+                "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
+                "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc",
+            ]
+
+            self.check_missing_file(pdf_pkgs, noto_sans_redhat, 2)
+
+            if not old:
+                self.check_rpm_missing(fedora26_opt_pkgs, 2)
+                self.check_rpm_missing(fedora_tex_pkgs, 2)
+
+            self.check_missing_tex(2)
+
+        self.check_missing(progs)
+
+        if not self.need and not self.optional:
+            return
+
+        if self.verbose_warn_install:
+            print("You should run:")
+
+        if old:
+            # dnf is there since Fedora 18+ and RHEL 8
+            pkg_manager = "yum"
+
+        print(f"\n\tsudo {pkg_manager} install -y {self.install}")
+
+    def give_opensuse_hints(self):
+        progs = {
+            "Pod::Usage":    "perl-Pod-Usage",
+            "convert":       "ImageMagick",
+            "dot":           "graphviz",
+            "python-sphinx": "python3-sphinx",
+            "virtualenv":    "python3-virtualenv",
+            "xelatex":       "texlive-xetex-bin",
+            "yaml":          "python3-pyyaml",
+        }
+
+        # On Tumbleweed, this package is also named rsvg-convert
+        if not re.search(r"Tumbleweed", self.system_release):
+            progs["rsvg-convert"] = "rsvg-view"
+
+        suse_tex_pkgs = [
+            "texlive-babel-english",
+            "texlive-caption",
+            "texlive-colortbl",
+            "texlive-courier",
+            "texlive-dvips",
+            "texlive-helvetic",
+            "texlive-makeindex",
+            "texlive-metafont",
+            "texlive-metapost",
+            "texlive-palatino",
+            "texlive-preview",
+            "texlive-times",
+            "texlive-zapfchan",
+            "texlive-zapfding",
+        ]
+
+        progs["latexmk"] = "texlive-latexmk-bin"
+
+        # FIXME: add support for installing CJK fonts
+        #
+        # I tried hard, but was unable to find a way to install
+        # "Noto Sans CJK SC" on openSUSE
+
+        if self.pdf:
+            self.check_rpm_missing(suse_tex_pkgs, 2)
+        if self.pdf:
+            self.check_missing_tex(2)
+        self.check_missing(progs)
+
+        if not self.need and not self.optional:
+            return
+
+        if self.verbose_warn_install:
+            print("You should run:")
+        print(f"\n\tsudo zypper install --no-recommends {self.install}")
+
+    def give_mageia_hints(self):
+        progs = {
+            "Pod::Usage":    "perl-Pod-Usage",
+            "convert":       "ImageMagick",
+            "dot":           "graphviz",
+            "python-sphinx": "python3-sphinx",
+            "rsvg-convert":  "librsvg2",
+            "virtualenv":    "python3-virtualenv",
+            "xelatex":       "texlive",
+            "yaml":          "python3-yaml",
+        }
+
+        tex_pkgs = [
+            "texlive-fontsextra",
+        ]
+
+        if re.search(r"OpenMandriva", self.system_release):
+            packager_cmd = "dnf install"
+            noto_sans = "noto-sans-cjk-fonts"
+            tex_pkgs = ["texlive-collection-fontsextra"]
+        else:
+            packager_cmd = "urpmi"
+            noto_sans = "google-noto-sans-cjk-ttc-fonts"
+
+        progs["latexmk"] = "texlive-collection-basic"
+
+        if self.pdf:
+            pdf_pkgs = [
+                "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
+                "/usr/share/fonts/TTF/NotoSans-Regular.ttf",
+            ]
+
+            self.check_missing_file(pdf_pkgs, noto_sans, 2)
+            self.check_rpm_missing(tex_pkgs, 2)
+
+        self.check_missing(progs)
+
+        if not self.need and not self.optional:
+            return
+        if self.verbose_warn_install:
+            print("You should run:")
+        print(f"\n\tsudo {packager_cmd} {self.install}")
+
+    def give_arch_linux_hints(self):
+        progs = {
+            "convert":      "imagemagick",
+            "dot":          "graphviz",
+            "latexmk":      "texlive-core",
+            "rsvg-convert": "extra/librsvg",
+            "virtualenv":   "python-virtualenv",
+            "xelatex":      "texlive-xetex",
+            "yaml":         "python-yaml",
+        }
+
+        archlinux_tex_pkgs = [
+            "texlive-core",
+            "texlive-latexextra",
+            "ttf-dejavu",
+        ]
+
+        if self.pdf:
+            self.check_pacman_missing(archlinux_tex_pkgs, 2)
+
+            self.check_missing_file(
+                ["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"],
+                "noto-fonts-cjk",
+                2,
+            )
+
+        self.check_missing(progs)
+
+        if not self.need and not self.optional:
+            return
+        if self.verbose_warn_install:
+            print("You should run:")
+        print(f"\n\tsudo pacman -S {self.install}")
+
+    def give_gentoo_hints(self):
+        progs = {
+            "convert":      "media-gfx/imagemagick",
+            "dot":          "media-gfx/graphviz",
+            "rsvg-convert": "gnome-base/librsvg",
+            "virtualenv":   "dev-python/virtualenv",
+            "xelatex":      "dev-texlive/texlive-xetex media-fonts/dejavu",
+            "yaml":         "dev-python/pyyaml",
+        }
+
+        if self.pdf:
+            pdf_pkgs = {
+                "media-fonts/dejavu": [
+                    "/usr/share/fonts/dejavu/DejaVuSans.ttf",
+                ],
+                "media-fonts/noto-cjk": [
+                    "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
+                    "/usr/share/fonts/noto-cjk/NotoSerifCJK-Regular.ttc",
+                ],
+            }
+            for package, files in pdf_pkgs.items():
+                self.check_missing_file(files, package, 2)
+
+        self.check_missing(progs)
+
+        if not self.need and not self.optional:
+            return
+
+        if self.verbose_warn_install:
+            print("You should run:")
+        print("\n")
+
+        imagemagick = "media-gfx/imagemagick svg png"
+        cairo = "media-gfx/graphviz cairo pdf"
+        portage_imagemagick = "/etc/portage/package.use/imagemagick"
+        portage_cairo = "/etc/portage/package.use/graphviz"
+
+        result = self.run(["grep", "imagemagick", portage_imagemagick],
+                          stdout=subprocess.PIPE, text=True)
+        if not result.stdout.strip():
+            print(f"\tsudo su -c 'echo \"{imagemagick}\" > {portage_imagemagick}'")
+
+        result = self.run(["grep", "graphviz", portage_cairo],
+                          stdout=subprocess.PIPE, text=True)
+
+        if not result.stdout.strip():
+            print(f"\tsudo su -c 'echo \"{cairo}\" > {portage_cairo}'")
+
+        print(f"\tsudo emerge --ask {self.install}")
+
+    #
+    # Dispatch the check to an os_specific hinter
+    #
+
+    def check_distros(self):
+        # OS-specific hints logic
+        os_hints = {
+            re.compile("Red Hat Enterprise Linux"):   self.give_redhat_hints,
+            re.compile("Fedora"):                     self.give_redhat_hints,
+            re.compile("AlmaLinux"):                  self.give_redhat_hints,
+            re.compile("Amazon Linux"):               self.give_redhat_hints,
+            re.compile("CentOS"):                     self.give_redhat_hints,
+            re.compile("openEuler"):                  self.give_redhat_hints,
+            re.compile("Oracle Linux Server"):        self.give_redhat_hints,
+            re.compile("Rocky Linux"):                self.give_redhat_hints,
+            re.compile("Scientific Linux"):           self.give_redhat_hints,
+            re.compile("Springdale Open Enterprise"): self.give_redhat_hints,
+
+            re.compile("Ubuntu"):                     self.give_debian_hints,
+            re.compile("Debian"):                     self.give_debian_hints,
+            re.compile("Devuan"):                     self.give_debian_hints,
+            re.compile("Kali"):                       self.give_debian_hints,
+            re.compile("Mint"):                       self.give_debian_hints,
+
+            re.compile("openSUSE"):                   self.give_opensuse_hints,
+
+            re.compile("Mageia"):                     self.give_mageia_hints,
+            re.compile("OpenMandriva"):               self.give_mageia_hints,
+
+            re.compile("Arch Linux"):                 self.give_arch_linux_hints,
+            re.compile("Gentoo"):                     self.give_gentoo_hints,
+        }
+
+        # If the OS is detected, use per-OS hint logic
+        for regex, os_hint in os_hints.items():
+            if regex.search(self.system_release):
+                os_hint()
+
+                return
+
+        #
+        # Fall-back to generic hint code for other distros
+        # That's far from ideal, specially for LaTeX dependencies.
+        #
+        progs = {"sphinx-build": "sphinx"}
+        if self.pdf:
+            self.check_missing_tex(2)
+
+        self.check_missing(progs)
+
+        print(f"I don't know distro {self.system_release}.")
+        print("So, I can't provide you a hint with the install procedure.")
+        print("There are likely missing dependencies.")
+
+    #
+    # Common dependencies
+    #
+    def deactivate_help(self):
+        print("\n    If you want to exit the virtualenv, you can use:")
+        print("\tdeactivate")
+
+    def get_virtenv(self):
+        cwd = os.getcwd()
+
+        activates = []
+
+        # Add all sphinx prefixes with possible version numbers
+        for p in self.virtenv_prefix:
+            activates += glob(f"{cwd}/{p}[0-9]*/bin/activate")
+
+        activates.sort(reverse=True, key=str.lower)
+
+        # Place sphinx_latest first, if it exists
+        for p in self.virtenv_prefix:
+            activates = glob(f"{cwd}/{p}*latest/bin/activate") + activates
+
+        ver = (0, 0, 0)
+        for f in activates:
+            # Discard too old Sphinx virtual environments
+            match = re.search(r"(\d+)\.(\d+)\.(\d+)", f)
+            if match:
+                ver = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
+
+                if ver < self.min_version:
+                    continue
+
+            sphinx_cmd = f.replace("activate", "sphinx-build")
+            if not os.path.isfile(sphinx_cmd):
+                continue
+
+            ver = self.get_sphinx_version(sphinx_cmd)
+
+            if not ver:
+                venv_dir = f.replace("/bin/activate", "")
+                print(f"Warning: virtual environment {venv_dir} is not working.\n" \
+                      "Python version upgrade? Remove it with:\n\n" \
+                      "\trm -rf {venv_dir}\n\n")
+            else:
+                if self.need_sphinx and ver >= self.min_version:
+                    return (f, ver)
+                elif parse_version(ver) > self.cur_version:
+                    return (f, ver)
+
+        return ("", ver)
+
+    def recommend_sphinx_upgrade(self):
+        # Avoid running sphinx-builds from venv if cur_version is good
+        if self.cur_version and self.cur_version >= RECOMMENDED_VERSION:
+            self.latest_avail_ver = self.cur_version
+            return None
+
+        # Get the highest version from sphinx_*/bin/sphinx-build and the
+        # corresponding command to activate the venv/virtenv
+        self.activate_cmd, self.venv_ver = self.get_virtenv()
+
+        # Store the highest version from Sphinx existing virtualenvs
+        if self.activate_cmd and self.venv_ver > self.cur_version:
+            self.latest_avail_ver = self.venv_ver
+        else:
+            if self.cur_version:
+                self.latest_avail_ver = self.cur_version
+            else:
+                self.latest_avail_ver = (0, 0, 0)
+
+        # As we don't know package version of Sphinx, and there's no
+        # virtual environments, don't check if upgrades are needed
+        if not self.virtualenv:
+            if not self.latest_avail_ver:
+                return None
+
+            return self.latest_avail_ver
+
+        # Either there are already a virtual env or a new one should be created
+        self.need_pip = 1
+
+        if not self.latest_avail_ver:
+            return None
+
+        # Return if the reason is due to an upgrade or not
+        if self.latest_avail_ver != (0, 0, 0):
+            if self.latest_avail_ver < RECOMMENDED_VERSION:
+                self.rec_sphinx_upgrade = 1
+
+        return self.latest_avail_ver
+
+    def recommend_sphinx_version(self, virtualenv_cmd):
+        # The logic here is complex, as it have to deal with different versions:
+        #	- minimal supported version;
+        #	- minimal PDF version;
+        #	- recommended version.
+        # It also needs to work fine with both distro's package and venv/virtualenv
+
+        # Version is OK. Nothing to do.
+        if self.cur_version != (0, 0, 0) and self.cur_version >= RECOMMENDED_VERSION:
+            return
+
+        if not self.need_sphinx:
+            # sphinx-build is present and its version is >= $min_version
+
+            # only recommend enabling a newer virtenv version if makes sense.
+            if self.latest_avail_ver and self.latest_avail_ver > self.cur_version:
+                print("\nYou may also use the newer Sphinx version {self.latest_avail_ver} with:")
+                if f"{self.virtenv_prefix}" in os.getcwd():
+                    print("\tdeactivate")
+                print(f"\t. {self.activate_cmd}")
+                self.deactivate_help()
+                return
+
+            if self.latest_avail_ver and self.latest_avail_ver >= RECOMMENDED_VERSION:
+                return
+
+        if not self.virtualenv:
+            # No sphinx either via package or via virtenv. As we can't
+            # Compare the versions here, just return, recommending the
+            # user to install it from the package distro.
+            if not self.latest_avail_ver or self.latest_avail_ver == (0, 0, 0):
+                return
+
+            # User doesn't want a virtenv recommendation, but he already
+            # installed one via virtenv with a newer version.
+            # So, print commands to enable it
+            if self.latest_avail_ver > self.cur_version:
+                print("\nYou may also use the Sphinx virtualenv version {self.latest_avail_ver} with:")
+                if f"{self.virtenv_prefix}" in os.getcwd():
+                    print("\tdeactivate")
+                print(f"\t. {self.activate_cmd}")
+                self.deactivate_help()
+                return
+            print("\n")
+        else:
+            if self.need_sphinx:
+                self.need += 1
+
+        # Suggest newer versions if current ones are too old
+        if self.latest_avail_ver and self.latest_avail_ver >= self.min_version:
+            if self.latest_avail_ver >= RECOMMENDED_VERSION:
+                print("\nNeed to activate Sphinx (version {self.latest_avail_ver}) on virtualenv with:")
+                print(f"\t. {self.activate_cmd}")
+                self.deactivate_help()
+                return
+
+            # Version is above the minimal required one, but may be
+            # below the recommended one. So, print warnings/notes
+            if self.latest_avail_ver < RECOMMENDED_VERSION:
+                print(f"Warning: It is recommended at least Sphinx version {RECOMMENDED_VERSION}.")
+
+        # At this point, either it needs Sphinx or upgrade is recommended,
+        # both via pip
+
+        if self.rec_sphinx_upgrade:
+            if not self.virtualenv:
+                print("Instead of install/upgrade Python Sphinx pkg, you could use pip/pypi with:\n\n")
+            else:
+                print("To upgrade Sphinx, use:\n\n")
+        else:
+            print("\nSphinx needs to be installed either:\n1) via pip/pypi with:\n")
+
+        self.python_cmd = self.find_python_no_venv()
+
+        print(f"\t{virtualenv_cmd} {self.virtenv_dir}")
+        print(f"\t. {self.virtenv_dir}/bin/activate")
+        print(f"\tpip install -r {self.requirement_file}")
+        self.deactivate_help()
+
+        print("\n2) As a package with:")
+
+        old_need = self.need
+        old_optional = self.optional
+        self.missing = {}
+        self.pdf = False
+        self.optional = 0
+        self.install = ""
+        old_verbose = self.verbose_warn_install
+        self.verbose_warn_install = 0
+
+        self.add_package("python-sphinx", 0)
+
+        self.check_distros()
+
+        self.need = old_need
+        self.optional = old_optional
+        self.verbose_warn_install = old_verbose
+
+        print("\n" \
+              "    Please note that Sphinx >= 3.0 will currently produce false-positive\n" \
+              "   warning when the same name is used for more than one type (functions,\n" \
+              "   structs, enums,...). This is known Sphinx bug. For more details, see:\n" \
+              "\thttps://github.com/sphinx-doc/sphinx/pull/8313")
+
+    def check_needs(self):
+        self.get_system_release()
+
+        # Check if Sphinx is already accessible from current environment
+        self.check_sphinx()
+
+        if self.system_release:
+            print(f"Detected OS: {self.system_release}.")
+        else:
+            print("Unknown OS")
+        if self.cur_version != (0, 0, 0):
+            ver = ver_str(self.cur_version)
+            print(f"Sphinx version: {ver}\n")
+
+        # FIXME: Check python command line, trying first python3
+        self.python_cmd = self.which("python3")
+        if not self.python_cmd:
+            self.python_cmd = self.check_program("python", 0)
+
+        # Check the type of virtual env, depending on Python version
+        if self.python_cmd:
+            if self.virtualenv:
+                try:
+                    result = self.run(
+                        [self.python_cmd, "--version"],
+                        capture_output=True,
+                        text=True,
+                        check=True,
+                    )
+
+                    output = result.stdout + result.stderr
+
+                    match = re.search(r"(\d+)\.(\d+)\.", output)
+                    if match:
+                        major = int(match.group(1))
+                        minor = int(match.group(2))
+
+                        if major < 3:
+                            sys.exit("Python 3 is required to build the kernel docs")
+                        if major == 3 and minor < 3:
+                            self.need_virtualenv = True
+                    else:
+                        sys.exit(f"Warning: couldn't identify {self.python_cmd} version!")
+
+                except subprocess.CalledProcessError as e:
+                    sys.exit(f"Error checking Python version: {e}")
+            else:
+                self.add_package("python-sphinx", 0)
+
+        self.venv_ver = self.recommend_sphinx_upgrade()
+
+        virtualenv_cmd = ""
+
+        if self.need_pip:
+            # Set virtualenv command line, if python < 3.3
+            # FIXME: can be removed as we're now with an upper min requirement
+            #        but then we need to check python version
+            if self.need_virtualenv:
+                virtualenv_cmd = self.which("virtualenv-3")
+                if not virtualenv_cmd:
+                    virtualenv_cmd = self.which("virtualenv-3.5")
+                if not virtualenv_cmd:
+                    self.check_program("virtualenv", 0)
+                    virtualenv_cmd = "virtualenv"
+            else:
+                virtualenv_cmd = f"{self.python_cmd} -m venv"
+                self.check_python_module("ensurepip", 0)
+
+        # Check for needed programs/tools
+        self.check_perl_module("Pod::Usage", 0)
+        self.check_python_module("yaml", 0)
+        self.check_program("make", 0)
+        self.check_program("gcc", 0)
+        self.check_program("dot", 1)
+        self.check_program("convert", 1)
+
+        if self.pdf:
+            # Extra PDF files - should use 2 for LaTeX is_optional
+            self.check_program("xelatex", 2)
+            self.check_program("rsvg-convert", 2)
+            self.check_program("latexmk", 2)
+
+        # Do distro-specific checks and output distro-install commands
+        self.check_distros()
+
+        if not self.python_cmd:
+            if self.need == 1:
+                sys.exit("Can't build as 1 mandatory dependency is missing")
+            elif self.need:
+                sys.exit(f"Can't build as {self.need} mandatory dependencies are missing")
+
+        # Check if sphinx-build is called sphinx-build-3
+        if self.need_symlink:
+            sphinx_path = self.which("sphinx-build-3")
+            if sphinx_path:
+                print(f"\tsudo ln -sf {sphinx_path} /usr/bin/sphinx-build\n")
+
+        self.recommend_sphinx_version(virtualenv_cmd)
+        print("")
+
+        if not self.optional:
+            print("All optional dependencies are met.")
+
+        if self.need == 1:
+            sys.exit("Can't build as 1 mandatory dependency is missing")
+        elif self.need:
+            sys.exit(f"Can't build as {self.need} mandatory dependencies are missing")
+
+        print("Needed package dependencies are met.")
+
+    def get_system_release(self):
+        """
+        Determine the system type. There's no unique way that would work
+        with all distros with a minimal package install. So, several
+        methods are used here.
+
+        By default, it will use lsb_release function. If not available, it will
+        fail back to reading the known different places where the distro name
+        is stored.
+
+        Several modern distros now have /etc/os-release, which usually have
+        a decent coverage.
+        """
+
+        if self.which("lsb_release"):
+            result = self.run(["lsb_release", "-d"], capture_output=True, text=True)
+            self.system_release = result.stdout.replace("Description:", "").strip()
+
+        release_files = [
+            "/etc/system-release",
+            "/etc/redhat-release",
+            "/etc/lsb-release",
+            "/etc/gentoo-release",
+        ]
+
+        if not self.system_release:
+            for f in release_files:
+                self.system_release = self.catcheck(f)
+                if self.system_release:
+                    break
+
+        # This seems more common than LSB these days
+        if not self.system_release:
+            os_var = {}
+            try:
+                with open("/etc/os-release", "r", encoding="utf-8") as f:
+                    for line in f:
+                        match = re.match(r"^([\w\d\_]+)=\"?([^\"]*)\"?\n", line)
+                        if match:
+                            os_var[match.group(1)] = match.group(2)
+
+                self.system_release = os_var.get("NAME", "")
+                if "VERSION_ID" in os_var:
+                    self.system_release += " " + os_var["VERSION_ID"]
+                elif "VERSION" in os_var:
+                    self.system_release += " " + os_var["VERSION"]
+            except IOError:
+                pass
+
+        if not self.system_release:
+            self.system_release = self.catcheck("/etc/issue")
+
+        self.system_release = self.system_release.strip()
+
+DESCRIPTION = """
+Process some flags related to Sphinx installation and documentation build.
+"""
+
+
+def main():
+    parser = argparse.ArgumentParser(description=DESCRIPTION)
+
+    parser.add_argument(
+        "--no-virtualenv",
+        action="store_false",
+        dest="virtualenv",
+        help="Recommend installing Sphinx instead of using a virtualenv",
+    )
+
+    parser.add_argument(
+        "--no-pdf",
+        action="store_false",
+        dest="pdf",
+        help="Don't check for dependencies required to build PDF docs",
+    )
+
+    parser.add_argument(
+        "--version-check",
+        action="store_true",
+        dest="version_check",
+        help="If version is compatible, don't check for missing dependencies",
+    )
+
+    args = parser.parse_args()
+
+    checker = SphinxDependencyChecker(args)
+
+    checker.check_needs()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.49.0


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ