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