Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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()