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