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