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