xref: /linux/scripts/sphinx-pre-install (revision 9f51a1d6966760dadf62ff1f62c614a342fb7e62)
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# pylint: disable=C0103,C0114,C0115,C0116,C0301,C0302
6# pylint: disable=R0902,R0904,R0911,R0912,R0914,R0915,R1705,R1710,E1121
7
8# Note: this script requires at least Python 3.6 to run.
9# Don't add changes not compatible with it, it is meant to report
10# incompatible python versions.
11
12"""
13Dependency checker for Sphinx documentation Kernel build.
14
15This module provides tools to check for all required dependencies needed to
16build documentation using Sphinx, including system packages, Python modules
17and LaTeX packages for PDF generation.
18
19It detect packages for a subset of Linux distributions used by Kernel
20maintainers, showing hints and missing dependencies.
21
22The main class SphinxDependencyChecker handles the dependency checking logic
23and provides recommendations for installing missing packages. It supports both
24system package installations and  Python virtual environments. By default,
25system pacage install is recommended.
26"""
27
28import argparse
29import os
30import re
31import subprocess
32import sys
33from glob import glob
34
35
36def parse_version(version):
37    """Convert a major.minor.patch version into a tuple"""
38    return tuple(int(x) for x in version.split("."))
39
40
41def ver_str(version):
42    """Returns a version tuple as major.minor.patch"""
43
44    return ".".join([str(x) for x in version])
45
46
47RECOMMENDED_VERSION = parse_version("3.4.3")
48MIN_PYTHON_VERSION = parse_version("3.7")
49
50
51class DepManager:
52    """
53    Manage package dependencies. There are three types of dependencies:
54
55    - System: dependencies required for docs build;
56    - Python: python dependencies for a native distro Sphinx install;
57    - PDF: dependencies needed by PDF builds.
58
59    Each dependency can be mandatory or optional. Not installing an optional
60    dependency won't break the build, but will cause degradation at the
61    docs output.
62    """
63
64    # Internal types of dependencies. Don't use them outside DepManager class.
65    _SYS_TYPE = 0
66    _PHY_TYPE = 1
67    _PDF_TYPE = 2
68
69    # Dependencies visible outside the class.
70    # The keys are tuple with: (type, is_mandatory flag).
71    #
72    # Currently we're not using all optional dep types. Yet, we'll keep all
73    # possible combinations here. They're not many, and that makes easier
74    # if later needed and for the name() method below
75
76    SYSTEM_MANDATORY = (_SYS_TYPE, True)
77    PYTHON_MANDATORY = (_PHY_TYPE, True)
78    PDF_MANDATORY = (_PDF_TYPE, True)
79
80    SYSTEM_OPTIONAL = (_SYS_TYPE, False)
81    PYTHON_OPTIONAL = (_PHY_TYPE, False)
82    PDF_OPTIONAL = (_PDF_TYPE, True)
83
84    def __init__(self, pdf):
85        """
86        Initialize internal vars:
87
88        - missing: missing dependencies list, containing a distro-independent
89                   name for a missing dependency and its type.
90        - missing_pkg: ancillary dict containing missing dependencies in
91                       distro namespace, organized by type.
92        - need: total number of needed dependencies. Never cleaned.
93        - optional: total number of optional dependencies. Never cleaned.
94        - pdf: Is PDF support enabled?
95        """
96        self.missing = {}
97        self.missing_pkg = {}
98        self.need = 0
99        self.optional = 0
100        self.pdf = pdf
101
102    @staticmethod
103    def name(dtype):
104        """
105        Ancillary routine to output a warn/error message reporting
106        missing dependencies.
107        """
108        if dtype[0] == DepManager._SYS_TYPE:
109            msg = "build"
110        elif dtype[0] == DepManager._PHY_TYPE:
111            msg = "Python"
112        else:
113            msg = "PDF"
114
115        if dtype[1]:
116            return f"ERROR: {msg} mandatory deps missing"
117        else:
118            return f"Warning: {msg} optional deps missing"
119
120    @staticmethod
121    def is_optional(dtype):
122        """Ancillary routine to report if a dependency is optional"""
123        return not dtype[1]
124
125    @staticmethod
126    def is_pdf(dtype):
127        """Ancillary routine to report if a dependency is for PDF generation"""
128        if dtype[0] == DepManager._PDF_TYPE:
129            return True
130
131        return False
132
133    def add_package(self, package, dtype):
134        """
135        Add a package at the self.missing() dictionary.
136        Doesn't update missing_pkg.
137        """
138        is_optional = DepManager.is_optional(dtype)
139        self.missing[package] = dtype
140        if is_optional:
141            self.optional += 1
142        else:
143            self.need += 1
144
145    def del_package(self, package):
146        """
147        Remove a package at the self.missing() dictionary.
148        Doesn't update missing_pkg.
149        """
150        if package in self.missing:
151            del self.missing[package]
152
153    def clear_deps(self):
154        """
155        Clear dependencies without changing needed/optional.
156
157        This is an ackward way to have a separate section to recommend
158        a package after system main dependencies.
159
160        TODO: rework the logic to prevent needing it.
161        """
162
163        self.missing = {}
164        self.missing_pkg = {}
165
166    def check_missing(self, progs):
167        """
168        Update self.missing_pkg, using progs dict to convert from the
169        agnostic package name to distro-specific one.
170
171        Returns an string with the packages to be installed, sorted and
172        with eventual duplicates removed.
173        """
174
175        self.missing_pkg = {}
176
177        for prog, dtype in sorted(self.missing.items()):
178            # At least on some LTS distros like CentOS 7, texlive doesn't
179            # provide all packages we need. When such distros are
180            # detected, we have to disable PDF output.
181            #
182            # So, we need to ignore the packages that distros would
183            # need for LaTeX to work
184            if DepManager.is_pdf(dtype) and not self.pdf:
185                self.optional -= 1
186                continue
187
188            if not dtype in self.missing_pkg:
189                self.missing_pkg[dtype] = []
190
191            self.missing_pkg[dtype].append(progs.get(prog, prog))
192
193        install = []
194        for dtype, pkgs in self.missing_pkg.items():
195            install += pkgs
196
197        return " ".join(sorted(set(install)))
198
199    def warn_install(self):
200        """
201        Emit warnings/errors related to missing packages.
202        """
203
204        output_msg = ""
205
206        for dtype in sorted(self.missing_pkg.keys()):
207            progs = " ".join(sorted(set(self.missing_pkg[dtype])))
208
209            try:
210                name = DepManager.name(dtype)
211                output_msg += f'{name}:\t{progs}\n'
212            except KeyError:
213                raise KeyError(f"ERROR!!!: invalid dtype for {progs}: {dtype}")
214
215        if output_msg:
216            print(f"\n{output_msg}")
217
218class AncillaryMethods:
219    """
220    Ancillary methods that checks for missing dependencies for different
221    types of types, like binaries, python modules, rpm deps, etc.
222    """
223
224    @staticmethod
225    def which(prog):
226        """
227        Our own implementation of which(). We could instead use
228        shutil.which(), but this function is simple enough.
229        Probably faster to use this implementation than to import shutil.
230        """
231        for path in os.environ.get("PATH", "").split(":"):
232            full_path = os.path.join(path, prog)
233            if os.access(full_path, os.X_OK):
234                return full_path
235
236        return None
237
238    @staticmethod
239    def get_python_version(cmd):
240        """
241        Get python version from a Python binary. As we need to detect if
242        are out there newer python binaries, we can't rely on sys.release here.
243        """
244
245        result = SphinxDependencyChecker.run([cmd, "--version"],
246                                            capture_output=True, text=True)
247        version = result.stdout.strip()
248
249        match = re.search(r"(\d+\.\d+\.\d+)", version)
250        if match:
251            return parse_version(match.group(1))
252
253        print(f"Can't parse version {version}")
254        return (0, 0, 0)
255
256    @staticmethod
257    def find_python():
258        """
259        Detect if are out there any python 3.xy version newer than the
260        current one.
261
262        Note: this routine is limited to up to 2 digits for python3. We
263        may need to update it one day, hopefully on a distant future.
264        """
265        patterns = [
266            "python3.[0-9]",
267            "python3.[0-9][0-9]",
268        ]
269
270        # Seek for a python binary newer than MIN_PYTHON_VERSION
271        for path in os.getenv("PATH", "").split(":"):
272            for pattern in patterns:
273                for cmd in glob(os.path.join(path, pattern)):
274                    if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
275                        version = SphinxDependencyChecker.get_python_version(cmd)
276                        if version >= MIN_PYTHON_VERSION:
277                            return cmd
278
279    @staticmethod
280    def check_python():
281        """
282        Check if the current python binary satisfies our minimal requirement
283        for Sphinx build. If not, re-run with a newer version if found.
284        """
285        cur_ver = sys.version_info[:3]
286        if cur_ver >= MIN_PYTHON_VERSION:
287            ver = ver_str(cur_ver)
288            print(f"Python version: {ver}")
289
290            # This could be useful for debugging purposes
291            if SphinxDependencyChecker.which("docutils"):
292                result = SphinxDependencyChecker.run(["docutils", "--version"],
293                                                    capture_output=True, text=True)
294                ver = result.stdout.strip()
295                match = re.search(r"(\d+\.\d+\.\d+)", ver)
296                if match:
297                    ver = match.group(1)
298
299                print(f"Docutils version: {ver}")
300
301            return
302
303        python_ver = ver_str(cur_ver)
304
305        new_python_cmd = SphinxDependencyChecker.find_python()
306        if not new_python_cmd:
307            print(f"ERROR: Python version {python_ver} is not spported anymore\n")
308            print("       Can't find a new version. This script may fail")
309            return
310
311        # Restart script using the newer version
312        script_path = os.path.abspath(sys.argv[0])
313        args = [new_python_cmd, script_path] + sys.argv[1:]
314
315        print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
316
317        try:
318            os.execv(new_python_cmd, args)
319        except OSError as e:
320            sys.exit(f"Failed to restart with {new_python_cmd}: {e}")
321
322    @staticmethod
323    def run(*args, **kwargs):
324        """
325        Excecute a command, hiding its output by default.
326        Preserve comatibility with older Python versions.
327        """
328
329        capture_output = kwargs.pop('capture_output', False)
330
331        if capture_output:
332            if 'stdout' not in kwargs:
333                kwargs['stdout'] = subprocess.PIPE
334            if 'stderr' not in kwargs:
335                kwargs['stderr'] = subprocess.PIPE
336        else:
337            if 'stdout' not in kwargs:
338                kwargs['stdout'] = subprocess.DEVNULL
339            if 'stderr' not in kwargs:
340                kwargs['stderr'] = subprocess.DEVNULL
341
342        # Don't break with older Python versions
343        if 'text' in kwargs and sys.version_info < (3, 7):
344            kwargs['universal_newlines'] = kwargs.pop('text')
345
346        return subprocess.run(*args, **kwargs)
347
348class MissingCheckers(AncillaryMethods):
349    """
350    Contains some ancillary checkers for different types of binaries and
351    package managers.
352    """
353
354    def __init__(self, args, texlive):
355        """
356        Initialize its internal variables
357        """
358        self.pdf = args.pdf
359        self.virtualenv = args.virtualenv
360        self.version_check = args.version_check
361        self.texlive = texlive
362
363        self.min_version = (0, 0, 0)
364        self.cur_version = (0, 0, 0)
365
366        self.deps = DepManager(self.pdf)
367
368        self.need_symlink = 0
369        self.need_sphinx = 0
370
371        self.verbose_warn_install = 1
372
373        self.virtenv_dir = ""
374        self.install = ""
375        self.python_cmd = ""
376
377        self.virtenv_prefix = ["sphinx_", "Sphinx_" ]
378
379    def check_missing_file(self, files, package, dtype):
380        """
381        Does the file exists? If not, add it to missing dependencies.
382        """
383        for f in files:
384            if os.path.exists(f):
385                return
386        self.deps.add_package(package, dtype)
387
388    def check_program(self, prog, dtype):
389        """
390        Does the program exists and it is at the PATH?
391        If not, add it to missing dependencies.
392        """
393        found = self.which(prog)
394        if found:
395            return found
396
397        self.deps.add_package(prog, dtype)
398
399        return None
400
401    def check_perl_module(self, prog, dtype):
402        """
403        Does perl have a dependency? Is it available?
404        If not, add it to missing dependencies.
405
406        Right now, we still need Perl for doc build, as it is required
407        by some tools called at docs or kernel build time, like:
408
409            scripts/documentation-file-ref-check
410
411        Also, checkpatch is on Perl.
412        """
413
414        # While testing with lxc download template, one of the
415        # distros (Oracle) didn't have perl - nor even an option to install
416        # before installing oraclelinux-release-el9 package.
417        #
418        # Check it before running an error. If perl is not there,
419        # add it as a mandatory package, as some parts of the doc builder
420        # needs it.
421        if not self.which("perl"):
422            self.deps.add_package("perl", DepManager.SYSTEM_MANDATORY)
423            self.deps.add_package(prog, dtype)
424            return
425
426        try:
427            self.run(["perl", f"-M{prog}", "-e", "1"], check=True)
428        except subprocess.CalledProcessError:
429            self.deps.add_package(prog, dtype)
430
431    def check_python_module(self, module, is_optional=False):
432        """
433        Does a python module exists outside venv? If not, add it to missing
434        dependencies.
435        """
436        if is_optional:
437            dtype = DepManager.PYTHON_OPTIONAL
438        else:
439            dtype = DepManager.PYTHON_MANDATORY
440
441        try:
442            self.run([self.python_cmd, "-c", f"import {module}"], check=True)
443        except subprocess.CalledProcessError:
444            self.deps.add_package(module, dtype)
445
446    def check_rpm_missing(self, pkgs, dtype):
447        """
448        Does a rpm package exists? If not, add it to missing dependencies.
449        """
450        for prog in pkgs:
451            try:
452                self.run(["rpm", "-q", prog], check=True)
453            except subprocess.CalledProcessError:
454                self.deps.add_package(prog, dtype)
455
456    def check_pacman_missing(self, pkgs, dtype):
457        """
458        Does a pacman package exists? If not, add it to missing dependencies.
459        """
460        for prog in pkgs:
461            try:
462                self.run(["pacman", "-Q", prog], check=True)
463            except subprocess.CalledProcessError:
464                self.deps.add_package(prog, dtype)
465
466    def check_missing_tex(self, is_optional=False):
467        """
468        Does a LaTeX package exists? If not, add it to missing dependencies.
469        """
470        if is_optional:
471            dtype = DepManager.PDF_OPTIONAL
472        else:
473            dtype = DepManager.PDF_MANDATORY
474
475        kpsewhich = self.which("kpsewhich")
476        for prog, package in self.texlive.items():
477
478            # If kpsewhich is not there, just add it to deps
479            if not kpsewhich:
480                self.deps.add_package(package, dtype)
481                continue
482
483            # Check if the package is needed
484            try:
485                result = self.run(
486                    [kpsewhich, prog], stdout=subprocess.PIPE, text=True, check=True
487                )
488
489                # Didn't find. Add it
490                if not result.stdout.strip():
491                    self.deps.add_package(package, dtype)
492
493            except subprocess.CalledProcessError:
494                # kpsewhich returned an error. Add it, just in case
495                self.deps.add_package(package, dtype)
496
497    def get_sphinx_fname(self):
498        """
499        Gets the binary filename for sphinx-build.
500        """
501        if "SPHINXBUILD" in os.environ:
502            return os.environ["SPHINXBUILD"]
503
504        fname = "sphinx-build"
505        if self.which(fname):
506            return fname
507
508        fname = "sphinx-build-3"
509        if self.which(fname):
510            self.need_symlink = 1
511            return fname
512
513        return ""
514
515    def get_sphinx_version(self, cmd):
516        """
517        Gets sphinx-build version.
518        """
519        try:
520            result = self.run([cmd, "--version"],
521                              stdout=subprocess.PIPE,
522                              stderr=subprocess.STDOUT,
523                              text=True, check=True)
524        except (subprocess.CalledProcessError, FileNotFoundError):
525            return None
526
527        for line in result.stdout.split("\n"):
528            match = re.match(r"^sphinx-build\s+([\d\.]+)(?:\+(?:/[\da-f]+)|b\d+)?\s*$", line)
529            if match:
530                return parse_version(match.group(1))
531
532            match = re.match(r"^Sphinx.*\s+([\d\.]+)\s*$", line)
533            if match:
534                return parse_version(match.group(1))
535
536    def check_sphinx(self, conf):
537        """
538        Checks Sphinx minimal requirements
539        """
540        try:
541            with open(conf, "r", encoding="utf-8") as f:
542                for line in f:
543                    match = re.match(r"^\s*needs_sphinx\s*=\s*[\'\"]([\d\.]+)[\'\"]", line)
544                    if match:
545                        self.min_version = parse_version(match.group(1))
546                        break
547        except IOError:
548            sys.exit(f"Can't open {conf}")
549
550        if not self.min_version:
551            sys.exit(f"Can't get needs_sphinx version from {conf}")
552
553        self.virtenv_dir = self.virtenv_prefix[0] + "latest"
554
555        sphinx = self.get_sphinx_fname()
556        if not sphinx:
557            self.need_sphinx = 1
558            return
559
560        self.cur_version = self.get_sphinx_version(sphinx)
561        if not self.cur_version:
562            sys.exit(f"{sphinx} didn't return its version")
563
564        if self.cur_version < self.min_version:
565            curver = ver_str(self.cur_version)
566            minver = ver_str(self.min_version)
567
568            print(f"ERROR: Sphinx version is {curver}. It should be >= {minver}")
569            self.need_sphinx = 1
570            return
571
572        # On version check mode, just assume Sphinx has all mandatory deps
573        if self.version_check and self.cur_version >= RECOMMENDED_VERSION:
574            sys.exit(0)
575
576    def catcheck(self, filename):
577        """
578        Reads a file if it exists, returning as string.
579        If not found, returns an empty string.
580        """
581        if os.path.exists(filename):
582            with open(filename, "r", encoding="utf-8") as f:
583                return f.read().strip()
584        return ""
585
586    def get_system_release(self):
587        """
588        Determine the system type. There's no unique way that would work
589        with all distros with a minimal package install. So, several
590        methods are used here.
591
592        By default, it will use lsb_release function. If not available, it will
593        fail back to reading the known different places where the distro name
594        is stored.
595
596        Several modern distros now have /etc/os-release, which usually have
597        a decent coverage.
598        """
599
600        system_release = ""
601
602        if self.which("lsb_release"):
603            result = self.run(["lsb_release", "-d"], capture_output=True, text=True)
604            system_release = result.stdout.replace("Description:", "").strip()
605
606        release_files = [
607            "/etc/system-release",
608            "/etc/redhat-release",
609            "/etc/lsb-release",
610            "/etc/gentoo-release",
611        ]
612
613        if not system_release:
614            for f in release_files:
615                system_release = self.catcheck(f)
616                if system_release:
617                    break
618
619        # This seems more common than LSB these days
620        if not system_release:
621            os_var = {}
622            try:
623                with open("/etc/os-release", "r", encoding="utf-8") as f:
624                    for line in f:
625                        match = re.match(r"^([\w\d\_]+)=\"?([^\"]*)\"?\n", line)
626                        if match:
627                            os_var[match.group(1)] = match.group(2)
628
629                system_release = os_var.get("NAME", "")
630                if "VERSION_ID" in os_var:
631                    system_release += " " + os_var["VERSION_ID"]
632                elif "VERSION" in os_var:
633                    system_release += " " + os_var["VERSION"]
634            except IOError:
635                pass
636
637        if not system_release:
638            system_release = self.catcheck("/etc/issue")
639
640        system_release = system_release.strip()
641
642        return system_release
643
644class SphinxDependencyChecker(MissingCheckers):
645    """
646    Main class for checking Sphinx documentation build dependencies.
647
648    - Check for missing system packages;
649    - Check for missing Python modules;
650    - Check for missing LaTeX packages needed by PDF generation;
651    - Propose Sphinx install via Python Virtual environment;
652    - Propose Sphinx install via distro-specific package install.
653    """
654    def __init__(self, args):
655        """Initialize checker variables"""
656
657        # List of required texlive packages on Fedora and OpenSuse
658        texlive = {
659            "amsfonts.sty":       "texlive-amsfonts",
660            "amsmath.sty":        "texlive-amsmath",
661            "amssymb.sty":        "texlive-amsfonts",
662            "amsthm.sty":         "texlive-amscls",
663            "anyfontsize.sty":    "texlive-anyfontsize",
664            "atbegshi.sty":       "texlive-oberdiek",
665            "bm.sty":             "texlive-tools",
666            "capt-of.sty":        "texlive-capt-of",
667            "cmap.sty":           "texlive-cmap",
668            "ctexhook.sty":       "texlive-ctex",
669            "ecrm1000.tfm":       "texlive-ec",
670            "eqparbox.sty":       "texlive-eqparbox",
671            "eu1enc.def":         "texlive-euenc",
672            "fancybox.sty":       "texlive-fancybox",
673            "fancyvrb.sty":       "texlive-fancyvrb",
674            "float.sty":          "texlive-float",
675            "fncychap.sty":       "texlive-fncychap",
676            "footnote.sty":       "texlive-mdwtools",
677            "framed.sty":         "texlive-framed",
678            "luatex85.sty":       "texlive-luatex85",
679            "multirow.sty":       "texlive-multirow",
680            "needspace.sty":      "texlive-needspace",
681            "palatino.sty":       "texlive-psnfss",
682            "parskip.sty":        "texlive-parskip",
683            "polyglossia.sty":    "texlive-polyglossia",
684            "tabulary.sty":       "texlive-tabulary",
685            "threeparttable.sty": "texlive-threeparttable",
686            "titlesec.sty":       "texlive-titlesec",
687            "ucs.sty":            "texlive-ucs",
688            "upquote.sty":        "texlive-upquote",
689            "wrapfig.sty":        "texlive-wrapfig",
690        }
691
692        super().__init__(args, texlive)
693
694        self.need_pip = False
695        self.rec_sphinx_upgrade = 0
696
697        self.system_release = self.get_system_release()
698        self.activate_cmd = ""
699
700        # Some distros may not have a Sphinx shipped package compatible with
701        # our minimal requirements
702        self.package_supported = True
703
704        # Recommend a new python version
705        self.recommend_python = None
706
707        # Certain hints are meant to be shown only once
708        self.distro_msg = None
709
710        self.latest_avail_ver = (0, 0, 0)
711        self.venv_ver = (0, 0, 0)
712
713        prefix = os.environ.get("srctree", ".") + "/"
714
715        self.conf = prefix + "Documentation/conf.py"
716        self.requirement_file = prefix + "Documentation/sphinx/requirements.txt"
717
718    def get_install_progs(self, progs, cmd, extra=None):
719        """
720        Check for missing dependencies using the provided program mapping.
721
722        The actual distro-specific programs are mapped via progs argument.
723        """
724        install = self.deps.check_missing(progs)
725
726        if self.verbose_warn_install:
727            self.deps.warn_install()
728
729        if not install:
730            return
731
732        if cmd:
733            if self.verbose_warn_install:
734                msg = "You should run:"
735            else:
736                msg = ""
737
738            if extra:
739                msg += "\n\t" + extra.replace("\n", "\n\t")
740
741            return(msg + "\n\tsudo " + cmd + " " + install)
742
743        return None
744
745    #
746    # Distro-specific hints methods
747    #
748
749    def give_debian_hints(self):
750        """
751        Provide package installation hints for Debian-based distros.
752        """
753        progs = {
754            "Pod::Usage":    "perl-modules",
755            "convert":       "imagemagick",
756            "dot":           "graphviz",
757            "ensurepip":     "python3-venv",
758            "python-sphinx": "python3-sphinx",
759            "rsvg-convert":  "librsvg2-bin",
760            "virtualenv":    "virtualenv",
761            "xelatex":       "texlive-xetex",
762            "yaml":          "python3-yaml",
763        }
764
765        if self.pdf:
766            pdf_pkgs = {
767                "texlive-lang-chinese": [
768                    "/usr/share/texlive/texmf-dist/tex/latex/ctex/ctexhook.sty",
769                ],
770                "fonts-dejavu": [
771                    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
772                ],
773                "fonts-noto-cjk": [
774                    "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
775                    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
776                    "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
777                ],
778            }
779
780            for package, files in pdf_pkgs.items():
781                self.check_missing_file(files, package, DepManager.PDF_MANDATORY)
782
783            self.check_program("dvipng", DepManager.PDF_MANDATORY)
784
785        return self.get_install_progs(progs, "apt-get install")
786
787    def give_redhat_hints(self):
788        """
789        Provide package installation hints for RedHat-based distros
790        (Fedora, RHEL and RHEL-based variants).
791        """
792        progs = {
793            "Pod::Usage":       "perl-Pod-Usage",
794            "convert":          "ImageMagick",
795            "dot":              "graphviz",
796            "python-sphinx":    "python3-sphinx",
797            "rsvg-convert":     "librsvg2-tools",
798            "virtualenv":       "python3-virtualenv",
799            "xelatex":          "texlive-xetex-bin",
800            "yaml":             "python3-pyyaml",
801        }
802
803        fedora_tex_pkgs = [
804            "dejavu-sans-fonts",
805            "dejavu-sans-mono-fonts",
806            "dejavu-serif-fonts",
807            "texlive-collection-fontsrecommended",
808            "texlive-collection-latex",
809            "texlive-xecjk",
810        ]
811
812        fedora = False
813        rel = None
814
815        match = re.search(r"(release|Linux)\s+(\d+)", self.system_release)
816        if match:
817            rel = int(match.group(2))
818
819        if not rel:
820            print("Couldn't identify release number")
821            noto_sans_redhat = None
822            self.pdf = False
823        elif re.search("Fedora", self.system_release):
824            # Fedora 38 and upper use this CJK font
825
826            noto_sans_redhat = "google-noto-sans-cjk-fonts"
827            fedora = True
828        else:
829            # Almalinux, CentOS, RHEL, ...
830
831            # at least up to version 9 (and Fedora < 38), that's the CJK font
832            noto_sans_redhat = "google-noto-sans-cjk-ttc-fonts"
833
834            progs["virtualenv"] = "python-virtualenv"
835
836            if not rel or rel < 8:
837                print("ERROR: Distro not supported. Too old?")
838                return
839
840            # RHEL 8 uses Python 3.6, which is not compatible with
841            # the build system anymore. Suggest Python 3.11
842            if rel == 8:
843                self.check_program("python3.9", DepManager.SYSTEM_MANDATORY)
844                progs["python3.9"] = "python39"
845                progs["yaml"] = "python39-pyyaml"
846
847                self.recommend_python = True
848
849                # There's no python39-sphinx package. Only pip is supported
850                self.package_supported = False
851
852            if not self.distro_msg:
853                self.distro_msg = \
854                    "Note: RHEL-based distros typically require extra repositories.\n" \
855                    "For most, enabling epel and crb are enough:\n" \
856                    "\tsudo dnf install -y epel-release\n" \
857                    "\tsudo dnf config-manager --set-enabled crb\n" \
858                    "Yet, some may have other required repositories. Those commands could be useful:\n" \
859                    "\tsudo dnf repolist all\n" \
860                    "\tsudo dnf repoquery --available --info <pkgs>\n" \
861                    "\tsudo dnf config-manager --set-enabled '*' # enable all - probably not what you want"
862
863        if self.pdf:
864            pdf_pkgs = [
865                "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
866                "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc",
867            ]
868
869            self.check_missing_file(pdf_pkgs, noto_sans_redhat, DepManager.PDF_MANDATORY)
870
871            self.check_rpm_missing(fedora_tex_pkgs, DepManager.PDF_MANDATORY)
872
873            self.check_missing_tex(DepManager.PDF_MANDATORY)
874
875            # There's no texlive-ctex on RHEL 8 repositories. This will
876            # likely affect CJK pdf build only.
877            if not fedora and rel == 8:
878                self.deps.del_package("texlive-ctex")
879
880        return self.get_install_progs(progs, "dnf install")
881
882    def give_opensuse_hints(self):
883        """
884        Provide package installation hints for openSUSE-based distros
885        (Leap and Tumbleweed).
886        """
887        progs = {
888            "Pod::Usage":    "perl-Pod-Usage",
889            "convert":       "ImageMagick",
890            "dot":           "graphviz",
891            "python-sphinx": "python3-sphinx",
892            "virtualenv":    "python3-virtualenv",
893            "xelatex":       "texlive-xetex-bin",
894            "yaml":          "python3-pyyaml",
895        }
896
897        suse_tex_pkgs = [
898            "texlive-babel-english",
899            "texlive-caption",
900            "texlive-colortbl",
901            "texlive-courier",
902            "texlive-dvips",
903            "texlive-helvetic",
904            "texlive-makeindex",
905            "texlive-metafont",
906            "texlive-metapost",
907            "texlive-palatino",
908            "texlive-preview",
909            "texlive-times",
910            "texlive-zapfchan",
911            "texlive-zapfding",
912        ]
913
914        progs["latexmk"] = "texlive-latexmk-bin"
915
916        match = re.search(r"(Leap)\s+(\d+).(\d)", self.system_release)
917        if match:
918            rel = int(match.group(2))
919
920            # Leap 15.x uses Python 3.6, which is not compatible with
921            # the build system anymore. Suggest Python 3.11
922            if rel == 15:
923                if not self.which(self.python_cmd):
924                    self.check_program("python3.11", DepManager.SYSTEM_MANDATORY)
925                    progs["python3.11"] = "python311"
926                    self.recommend_python = True
927
928                progs.update({
929                    "python-sphinx": "python311-Sphinx",
930                    "virtualenv":    "python311-virtualenv",
931                    "yaml":          "python311-PyYAML",
932                })
933        else:
934            # Tumbleweed defaults to Python 3.11
935
936            progs.update({
937                "python-sphinx": "python313-Sphinx",
938                "virtualenv":    "python313-virtualenv",
939                "yaml":          "python313-PyYAML",
940            })
941
942        # FIXME: add support for installing CJK fonts
943        #
944        # I tried hard, but was unable to find a way to install
945        # "Noto Sans CJK SC" on openSUSE
946
947        if self.pdf:
948            self.check_rpm_missing(suse_tex_pkgs, DepManager.PDF_MANDATORY)
949        if self.pdf:
950            self.check_missing_tex()
951
952        return self.get_install_progs(progs, "zypper install --no-recommends")
953
954    def give_mageia_hints(self):
955        """
956        Provide package installation hints for Mageia and OpenMandriva.
957        """
958        progs = {
959            "Pod::Usage":    "perl-Pod-Usage",
960            "convert":       "ImageMagick",
961            "dot":           "graphviz",
962            "python-sphinx": "python3-sphinx",
963            "rsvg-convert":  "librsvg2",
964            "virtualenv":    "python3-virtualenv",
965            "xelatex":       "texlive",
966            "yaml":          "python3-yaml",
967        }
968
969        tex_pkgs = [
970            "texlive-fontsextra",
971        ]
972
973        if re.search(r"OpenMandriva", self.system_release):
974            packager_cmd = "dnf install"
975            noto_sans = "noto-sans-cjk-fonts"
976            tex_pkgs = ["texlive-collection-fontsextra"]
977
978            # Tested on OpenMandriva Lx 4.3
979            progs["convert"] = "imagemagick"
980            progs["yaml"] = "python-pyyaml"
981
982        else:
983            packager_cmd = "urpmi"
984            noto_sans = "google-noto-sans-cjk-ttc-fonts"
985
986        progs["latexmk"] = "texlive-collection-basic"
987
988        if self.pdf:
989            pdf_pkgs = [
990                "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
991                "/usr/share/fonts/TTF/NotoSans-Regular.ttf",
992            ]
993
994            self.check_missing_file(pdf_pkgs, noto_sans, DepManager.PDF_MANDATORY)
995            self.check_rpm_missing(tex_pkgs, DepManager.PDF_MANDATORY)
996
997        return self.get_install_progs(progs, packager_cmd)
998
999    def give_arch_linux_hints(self):
1000        """
1001        Provide package installation hints for ArchLinux.
1002        """
1003        progs = {
1004            "convert":      "imagemagick",
1005            "dot":          "graphviz",
1006            "latexmk":      "texlive-core",
1007            "rsvg-convert": "extra/librsvg",
1008            "virtualenv":   "python-virtualenv",
1009            "xelatex":      "texlive-xetex",
1010            "yaml":         "python-yaml",
1011        }
1012
1013        archlinux_tex_pkgs = [
1014            "texlive-core",
1015            "texlive-latexextra",
1016            "ttf-dejavu",
1017        ]
1018
1019        if self.pdf:
1020            self.check_pacman_missing(archlinux_tex_pkgs,
1021                                      DepManager.PDF_MANDATORY)
1022
1023            self.check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"],
1024                                    "noto-fonts-cjk",
1025                                    DepManager.PDF_MANDATORY)
1026
1027
1028        return self.get_install_progs(progs, "pacman -S")
1029
1030    def give_gentoo_hints(self):
1031        """
1032        Provide package installation hints for Gentoo.
1033        """
1034        progs = {
1035            "convert":       "media-gfx/imagemagick",
1036            "dot":           "media-gfx/graphviz",
1037            "rsvg-convert":  "gnome-base/librsvg",
1038            "virtualenv":    "dev-python/virtualenv",
1039            "xelatex":       "dev-texlive/texlive-xetex media-fonts/dejavu",
1040            "yaml":          "dev-python/pyyaml",
1041            "python-sphinx": "dev-python/sphinx",
1042        }
1043
1044        if self.pdf:
1045            pdf_pkgs = {
1046                "media-fonts/dejavu": [
1047                    "/usr/share/fonts/dejavu/DejaVuSans.ttf",
1048                ],
1049                "media-fonts/noto-cjk": [
1050                    "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
1051                    "/usr/share/fonts/noto-cjk/NotoSerifCJK-Regular.ttc",
1052                ],
1053            }
1054            for package, files in pdf_pkgs.items():
1055                self.check_missing_file(files, package, DepManager.PDF_MANDATORY)
1056
1057        # Handling dependencies is a nightmare, as Gentoo refuses to emerge
1058        # some packages if there's no package.use file describing them.
1059        # To make it worse, compilation flags shall also be present there
1060        # for some packages. If USE is not perfect, error/warning messages
1061        #   like those are shown:
1062        #
1063        #   !!! The following binary packages have been ignored due to non matching USE:
1064        #
1065        #    =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_13 qt6 svg
1066        #    =media-gfx/graphviz-12.2.1-r1 X pdf python_single_target_python3_12 -python_single_target_python3_13 qt6 svg
1067        #    =media-gfx/graphviz-12.2.1-r1 X pdf qt6 svg
1068        #    =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_10 qt6 svg
1069        #    =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_10 python_single_target_python3_12 -python_single_target_python3_13 qt6 svg
1070        #    =media-fonts/noto-cjk-20190416 X
1071        #    =app-text/texlive-core-2024-r1 X cjk -xetex
1072        #    =app-text/texlive-core-2024-r1 X -xetex
1073        #    =app-text/texlive-core-2024-r1 -xetex
1074        #    =dev-libs/zziplib-0.13.79-r1 sdl
1075        #
1076        # And will ignore such packages, installing the remaining ones. That
1077        # affects mostly the image extension and PDF generation.
1078
1079        # Package dependencies and the minimal needed args:
1080        portages = {
1081            "graphviz": "media-gfx/graphviz",
1082            "imagemagick": "media-gfx/imagemagick",
1083            "media-libs": "media-libs/harfbuzz icu",
1084            "media-fonts": "media-fonts/noto-cjk",
1085            "texlive": "app-text/texlive-core xetex",
1086            "zziblib": "dev-libs/zziplib sdl",
1087        }
1088
1089        extra_cmds = ""
1090        if not self.distro_msg:
1091            self.distro_msg = "Note: Gentoo requires package.use to be adjusted before emerging packages"
1092
1093            use_base = "/etc/portage/package.use"
1094            files = glob(f"{use_base}/*")
1095
1096            for fname, portage in portages.items():
1097                install = False
1098
1099                while install is False:
1100                    if not files:
1101                        # No files under package.usage. Install all
1102                        install = True
1103                        break
1104
1105                    args = portage.split(" ")
1106
1107                    name = args.pop(0)
1108
1109                    cmd = ["grep", "-l", "-E", rf"^{name}\b" ] + files
1110                    result = self.run(cmd, stdout=subprocess.PIPE, text=True)
1111                    if result.returncode or not result.stdout.strip():
1112                        # File containing portage name not found
1113                        install = True
1114                        break
1115
1116                    # Ensure that needed USE flags are present
1117                    if args:
1118                        match_fname = result.stdout.strip()
1119                        with open(match_fname, 'r', encoding='utf8',
1120                                errors='backslashreplace') as fp:
1121                            for line in fp:
1122                                for arg in args:
1123                                    if arg.startswith("-"):
1124                                        continue
1125
1126                                if not re.search(rf"\s*{arg}\b", line):
1127                                    # Needed file argument not found
1128                                    install = True
1129                                    break
1130
1131                    # Everything looks ok, don't install
1132                    break
1133
1134                # emit a code to setup missing USE
1135                if install:
1136                    extra_cmds += (f"sudo su -c 'echo \"{portage}\" > {use_base}/{fname}'\n")
1137
1138        # Now, we can use emerge and let it respect USE
1139        return self.get_install_progs(progs,
1140                                      "emerge --ask --changed-use --binpkg-respect-use=y",
1141                                      extra_cmds)
1142
1143    def get_install(self):
1144        """
1145        OS-specific hints logic. Seeks for a hinter. If found, use it to
1146        provide package-manager specific install commands.
1147
1148        Otherwise, outputs install instructions for the meta-packages.
1149
1150        Returns a string with the command to be executed to install the
1151        the needed packages, if distro found. Otherwise, return just a
1152        list of packages that require installation.
1153        """
1154        os_hints = {
1155            re.compile("Red Hat Enterprise Linux"):   self.give_redhat_hints,
1156            re.compile("Fedora"):                     self.give_redhat_hints,
1157            re.compile("AlmaLinux"):                  self.give_redhat_hints,
1158            re.compile("Amazon Linux"):               self.give_redhat_hints,
1159            re.compile("CentOS"):                     self.give_redhat_hints,
1160            re.compile("openEuler"):                  self.give_redhat_hints,
1161            re.compile("Oracle Linux Server"):        self.give_redhat_hints,
1162            re.compile("Rocky Linux"):                self.give_redhat_hints,
1163            re.compile("Springdale Open Enterprise"): self.give_redhat_hints,
1164
1165            re.compile("Ubuntu"):                     self.give_debian_hints,
1166            re.compile("Debian"):                     self.give_debian_hints,
1167            re.compile("Devuan"):                     self.give_debian_hints,
1168            re.compile("Kali"):                       self.give_debian_hints,
1169            re.compile("Mint"):                       self.give_debian_hints,
1170
1171            re.compile("openSUSE"):                   self.give_opensuse_hints,
1172
1173            re.compile("Mageia"):                     self.give_mageia_hints,
1174            re.compile("OpenMandriva"):               self.give_mageia_hints,
1175
1176            re.compile("Arch Linux"):                 self.give_arch_linux_hints,
1177            re.compile("Gentoo"):                     self.give_gentoo_hints,
1178        }
1179
1180        # If the OS is detected, use per-OS hint logic
1181        for regex, os_hint in os_hints.items():
1182            if regex.search(self.system_release):
1183                return os_hint()
1184
1185        #
1186        # Fall-back to generic hint code for other distros
1187        # That's far from ideal, specially for LaTeX dependencies.
1188        #
1189        progs = {"sphinx-build": "sphinx"}
1190        if self.pdf:
1191            self.check_missing_tex()
1192
1193        self.distro_msg = \
1194            f"I don't know distro {self.system_release}.\n" \
1195            "So, I can't provide you a hint with the install procedure.\n" \
1196            "There are likely missing dependencies.\n"
1197
1198        return self.get_install_progs(progs, None)
1199
1200    #
1201    # Common dependencies
1202    #
1203    def deactivate_help(self):
1204        """
1205        Print a helper message to disable a virtual environment.
1206        """
1207
1208        print("\n    If you want to exit the virtualenv, you can use:")
1209        print("\tdeactivate")
1210
1211    def get_virtenv(self):
1212        """
1213        Give a hint about how to activate an already-existing virtual
1214        environment containing sphinx-build.
1215
1216        Returns a tuble with (activate_cmd_path, sphinx_version) with
1217        the newest available virtual env.
1218        """
1219
1220        cwd = os.getcwd()
1221
1222        activates = []
1223
1224        # Add all sphinx prefixes with possible version numbers
1225        for p in self.virtenv_prefix:
1226            activates += glob(f"{cwd}/{p}[0-9]*/bin/activate")
1227
1228        activates.sort(reverse=True, key=str.lower)
1229
1230        # Place sphinx_latest first, if it exists
1231        for p in self.virtenv_prefix:
1232            activates = glob(f"{cwd}/{p}*latest/bin/activate") + activates
1233
1234        ver = (0, 0, 0)
1235        for f in activates:
1236            # Discard too old Sphinx virtual environments
1237            match = re.search(r"(\d+)\.(\d+)\.(\d+)", f)
1238            if match:
1239                ver = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
1240
1241                if ver < self.min_version:
1242                    continue
1243
1244            sphinx_cmd = f.replace("activate", "sphinx-build")
1245            if not os.path.isfile(sphinx_cmd):
1246                continue
1247
1248            ver = self.get_sphinx_version(sphinx_cmd)
1249
1250            if not ver:
1251                venv_dir = f.replace("/bin/activate", "")
1252                print(f"Warning: virtual environment {venv_dir} is not working.\n" \
1253                      "Python version upgrade? Remove it with:\n\n" \
1254                      "\trm -rf {venv_dir}\n\n")
1255            else:
1256                if self.need_sphinx and ver >= self.min_version:
1257                    return (f, ver)
1258                elif parse_version(ver) > self.cur_version:
1259                    return (f, ver)
1260
1261        return ("", ver)
1262
1263    def recommend_sphinx_upgrade(self):
1264        """
1265        Check if Sphinx needs to be upgraded.
1266
1267        Returns a tuple with the higest available Sphinx version if found.
1268        Otherwise, returns None to indicate either that no upgrade is needed
1269        or no venv was found.
1270        """
1271
1272        # Avoid running sphinx-builds from venv if cur_version is good
1273        if self.cur_version and self.cur_version >= RECOMMENDED_VERSION:
1274            self.latest_avail_ver = self.cur_version
1275            return None
1276
1277        # Get the highest version from sphinx_*/bin/sphinx-build and the
1278        # corresponding command to activate the venv/virtenv
1279        self.activate_cmd, self.venv_ver = self.get_virtenv()
1280
1281        # Store the highest version from Sphinx existing virtualenvs
1282        if self.activate_cmd and self.venv_ver > self.cur_version:
1283            self.latest_avail_ver = self.venv_ver
1284        else:
1285            if self.cur_version:
1286                self.latest_avail_ver = self.cur_version
1287            else:
1288                self.latest_avail_ver = (0, 0, 0)
1289
1290        # As we don't know package version of Sphinx, and there's no
1291        # virtual environments, don't check if upgrades are needed
1292        if not self.virtualenv:
1293            if not self.latest_avail_ver:
1294                return None
1295
1296            return self.latest_avail_ver
1297
1298        # Either there are already a virtual env or a new one should be created
1299        self.need_pip = True
1300
1301        if not self.latest_avail_ver:
1302            return None
1303
1304        # Return if the reason is due to an upgrade or not
1305        if self.latest_avail_ver != (0, 0, 0):
1306            if self.latest_avail_ver < RECOMMENDED_VERSION:
1307                self.rec_sphinx_upgrade = 1
1308
1309        return self.latest_avail_ver
1310
1311    def recommend_package(self):
1312        """
1313        Recommend installing Sphinx as a distro-specific package.
1314        """
1315
1316        print("\n2) As a package with:")
1317
1318        old_need = self.deps.need
1319        old_optional = self.deps.optional
1320
1321        self.pdf = False
1322        self.deps.optional = 0
1323        old_verbose = self.verbose_warn_install
1324        self.verbose_warn_install = 0
1325
1326        self.deps.clear_deps()
1327
1328        self.deps.add_package("python-sphinx", DepManager.PYTHON_MANDATORY)
1329
1330        cmd = self.get_install()
1331        if cmd:
1332            print(cmd)
1333
1334        self.deps.need = old_need
1335        self.deps.optional = old_optional
1336        self.verbose_warn_install = old_verbose
1337
1338    def recommend_sphinx_version(self, virtualenv_cmd):
1339        """
1340        Provide recommendations for installing or upgrading Sphinx based
1341        on current version.
1342
1343        The logic here is complex, as it have to deal with different versions:
1344
1345        - minimal supported version;
1346        - minimal PDF version;
1347        - recommended version.
1348
1349        It also needs to work fine with both distro's package and
1350        venv/virtualenv
1351        """
1352
1353        if self.recommend_python:
1354            cur_ver = sys.version_info[:3]
1355            if cur_ver < MIN_PYTHON_VERSION:
1356                print(f"\nPython version {cur_ver} is incompatible with doc build.\n" \
1357                    "Please upgrade it and re-run.\n")
1358                return
1359
1360        # Version is OK. Nothing to do.
1361        if self.cur_version != (0, 0, 0) and self.cur_version >= RECOMMENDED_VERSION:
1362            return
1363
1364        if self.latest_avail_ver:
1365            latest_avail_ver = ver_str(self.latest_avail_ver)
1366
1367        if not self.need_sphinx:
1368            # sphinx-build is present and its version is >= $min_version
1369
1370            # only recommend enabling a newer virtenv version if makes sense.
1371            if self.latest_avail_ver and self.latest_avail_ver > self.cur_version:
1372                print(f"\nYou may also use the newer Sphinx version {latest_avail_ver} with:")
1373                if f"{self.virtenv_prefix}" in os.getcwd():
1374                    print("\tdeactivate")
1375                print(f"\t. {self.activate_cmd}")
1376                self.deactivate_help()
1377                return
1378
1379            if self.latest_avail_ver and self.latest_avail_ver >= RECOMMENDED_VERSION:
1380                return
1381
1382        if not self.virtualenv:
1383            # No sphinx either via package or via virtenv. As we can't
1384            # Compare the versions here, just return, recommending the
1385            # user to install it from the package distro.
1386            if not self.latest_avail_ver or self.latest_avail_ver == (0, 0, 0):
1387                return
1388
1389            # User doesn't want a virtenv recommendation, but he already
1390            # installed one via virtenv with a newer version.
1391            # So, print commands to enable it
1392            if self.latest_avail_ver > self.cur_version:
1393                print(f"\nYou may also use the Sphinx virtualenv version {latest_avail_ver} with:")
1394                if f"{self.virtenv_prefix}" in os.getcwd():
1395                    print("\tdeactivate")
1396                print(f"\t. {self.activate_cmd}")
1397                self.deactivate_help()
1398                return
1399            print("\n")
1400        else:
1401            if self.need_sphinx:
1402                self.deps.need += 1
1403
1404        # Suggest newer versions if current ones are too old
1405        if self.latest_avail_ver and self.latest_avail_ver >= self.min_version:
1406            if self.latest_avail_ver >= RECOMMENDED_VERSION:
1407                print(f"\nNeed to activate Sphinx (version {latest_avail_ver}) on virtualenv with:")
1408                print(f"\t. {self.activate_cmd}")
1409                self.deactivate_help()
1410                return
1411
1412            # Version is above the minimal required one, but may be
1413            # below the recommended one. So, print warnings/notes
1414            if self.latest_avail_ver < RECOMMENDED_VERSION:
1415                print(f"Warning: It is recommended at least Sphinx version {RECOMMENDED_VERSION}.")
1416
1417        # At this point, either it needs Sphinx or upgrade is recommended,
1418        # both via pip
1419
1420        if self.rec_sphinx_upgrade:
1421            if not self.virtualenv:
1422                print("Instead of install/upgrade Python Sphinx pkg, you could use pip/pypi with:\n\n")
1423            else:
1424                print("To upgrade Sphinx, use:\n\n")
1425        else:
1426            print("\nSphinx needs to be installed either:\n1) via pip/pypi with:\n")
1427
1428        if not virtualenv_cmd:
1429            print("   Currently not possible.\n")
1430            print("   Please upgrade Python to a newer version and run this script again")
1431        else:
1432            print(f"\t{virtualenv_cmd} {self.virtenv_dir}")
1433            print(f"\t. {self.virtenv_dir}/bin/activate")
1434            print(f"\tpip install -r {self.requirement_file}")
1435            self.deactivate_help()
1436
1437        if self.package_supported:
1438            self.recommend_package()
1439
1440        print("\n" \
1441              "   Please note that Sphinx currentlys produce false-positive\n" \
1442              "   warnings when the same name is used for more than one type (functions,\n" \
1443              "   structs, enums,...). This is known Sphinx bug. For more details, see:\n" \
1444              "\thttps://github.com/sphinx-doc/sphinx/pull/8313")
1445
1446    def check_needs(self):
1447        """
1448        Main method that checks needed dependencies and provides
1449        recommendations.
1450        """
1451        self.python_cmd = sys.executable
1452
1453        # Check if Sphinx is already accessible from current environment
1454        self.check_sphinx(self.conf)
1455
1456        if self.system_release:
1457            print(f"Detected OS: {self.system_release}.")
1458        else:
1459            print("Unknown OS")
1460        if self.cur_version != (0, 0, 0):
1461            ver = ver_str(self.cur_version)
1462            print(f"Sphinx version: {ver}\n")
1463
1464        # Check the type of virtual env, depending on Python version
1465        virtualenv_cmd = None
1466
1467        if sys.version_info < MIN_PYTHON_VERSION:
1468            min_ver = ver_str(MIN_PYTHON_VERSION)
1469            print(f"ERROR: at least python {min_ver} is required to build the kernel docs")
1470            self.need_sphinx = 1
1471
1472        self.venv_ver = self.recommend_sphinx_upgrade()
1473
1474        if self.need_pip:
1475            if sys.version_info < MIN_PYTHON_VERSION:
1476                self.need_pip = False
1477                print("Warning: python version is not supported.")
1478            else:
1479                virtualenv_cmd = f"{self.python_cmd} -m venv"
1480                self.check_python_module("ensurepip")
1481
1482        # Check for needed programs/tools
1483        self.check_perl_module("Pod::Usage", DepManager.SYSTEM_MANDATORY)
1484
1485        self.check_program("make", DepManager.SYSTEM_MANDATORY)
1486        self.check_program("which", DepManager.SYSTEM_MANDATORY)
1487
1488        self.check_program("dot", DepManager.SYSTEM_OPTIONAL)
1489        self.check_program("convert", DepManager.SYSTEM_OPTIONAL)
1490
1491        self.check_python_module("yaml")
1492
1493        if self.pdf:
1494            self.check_program("xelatex", DepManager.PDF_MANDATORY)
1495            self.check_program("rsvg-convert", DepManager.PDF_MANDATORY)
1496            self.check_program("latexmk", DepManager.PDF_MANDATORY)
1497
1498        # Do distro-specific checks and output distro-install commands
1499        cmd = self.get_install()
1500        if cmd:
1501            print(cmd)
1502
1503        # If distro requires some special instructions, print here.
1504        # Please notice that get_install() needs to be called first.
1505        if self.distro_msg:
1506            print("\n" + self.distro_msg)
1507
1508        if not self.python_cmd:
1509            if self.need == 1:
1510                sys.exit("Can't build as 1 mandatory dependency is missing")
1511            elif self.need:
1512                sys.exit(f"Can't build as {self.need} mandatory dependencies are missing")
1513
1514        # Check if sphinx-build is called sphinx-build-3
1515        if self.need_symlink:
1516            sphinx_path = self.which("sphinx-build-3")
1517            if sphinx_path:
1518                print(f"\tsudo ln -sf {sphinx_path} /usr/bin/sphinx-build\n")
1519
1520        self.recommend_sphinx_version(virtualenv_cmd)
1521        print("")
1522
1523        if not self.deps.optional:
1524            print("All optional dependencies are met.")
1525
1526        if self.deps.need == 1:
1527            sys.exit("Can't build as 1 mandatory dependency is missing")
1528        elif self.deps.need:
1529            sys.exit(f"Can't build as {self.deps.need} mandatory dependencies are missing")
1530
1531        print("Needed package dependencies are met.")
1532
1533DESCRIPTION = """
1534Process some flags related to Sphinx installation and documentation build.
1535"""
1536
1537
1538def main():
1539    """Main function"""
1540    parser = argparse.ArgumentParser(description=DESCRIPTION)
1541
1542    parser.add_argument(
1543        "--no-virtualenv",
1544        action="store_false",
1545        dest="virtualenv",
1546        help="Recommend installing Sphinx instead of using a virtualenv",
1547    )
1548
1549    parser.add_argument(
1550        "--no-pdf",
1551        action="store_false",
1552        dest="pdf",
1553        help="Don't check for dependencies required to build PDF docs",
1554    )
1555
1556    parser.add_argument(
1557        "--version-check",
1558        action="store_true",
1559        dest="version_check",
1560        help="If version is compatible, don't check for missing dependencies",
1561    )
1562
1563    args = parser.parse_args()
1564
1565    checker = SphinxDependencyChecker(args)
1566
1567    checker.check_python()
1568    checker.check_needs()
1569
1570# Call main if not used as module
1571if __name__ == "__main__":
1572    main()
1573