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 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 = False
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 "fonts-dejavu": [
768 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
769 ],
770 "fonts-noto-cjk": [
771 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
772 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
773 "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
774 ],
775 "tex-gyre": [
776 "/usr/share/texmf/tex/latex/tex-gyre/tgtermes.sty"
777 ],
778 "texlive-fonts-recommended": [
779 "/usr/share/texlive/texmf-dist/fonts/tfm/adobe/zapfding/pzdr.tfm",
780 ],
781 "texlive-lang-chinese": [
782 "/usr/share/texlive/texmf-dist/tex/latex/ctex/ctexhook.sty",
783 ],
784 }
785
786 for package, files in pdf_pkgs.items():
787 self.check_missing_file(files, package, DepManager.PDF_MANDATORY)
788
789 self.check_program("dvipng", DepManager.PDF_MANDATORY)
790
791 if not self.distro_msg:
792 self.distro_msg = \
793 "Note: ImageMagick is broken on some distros, affecting PDF output. For more details:\n" \
794 "\thttps://askubuntu.com/questions/1158894/imagemagick-still-broken-using-with-usr-bin-convert"
795
796 return self.get_install_progs(progs, "apt-get install")
797
798 def give_redhat_hints(self):
799 """
800 Provide package installation hints for RedHat-based distros
801 (Fedora, RHEL and RHEL-based variants).
802 """
803 progs = {
804 "Pod::Usage": "perl-Pod-Usage",
805 "convert": "ImageMagick",
806 "dot": "graphviz",
807 "python-sphinx": "python3-sphinx",
808 "rsvg-convert": "librsvg2-tools",
809 "virtualenv": "python3-virtualenv",
810 "xelatex": "texlive-xetex-bin",
811 "yaml": "python3-pyyaml",
812 }
813
814 fedora_tex_pkgs = [
815 "dejavu-sans-fonts",
816 "dejavu-sans-mono-fonts",
817 "dejavu-serif-fonts",
818 "texlive-collection-fontsrecommended",
819 "texlive-collection-latex",
820 "texlive-xecjk",
821 ]
822
823 fedora = False
824 rel = None
825
826 match = re.search(r"(release|Linux)\s+(\d+)", self.system_release)
827 if match:
828 rel = int(match.group(2))
829
830 if not rel:
831 print("Couldn't identify release number")
832 noto_sans_redhat = None
833 self.pdf = False
834 elif re.search("Fedora", self.system_release):
835 # Fedora 38 and upper use this CJK font
836
837 noto_sans_redhat = "google-noto-sans-cjk-fonts"
838 fedora = True
839 else:
840 # Almalinux, CentOS, RHEL, ...
841
842 # at least up to version 9 (and Fedora < 38), that's the CJK font
843 noto_sans_redhat = "google-noto-sans-cjk-ttc-fonts"
844
845 progs["virtualenv"] = "python-virtualenv"
846
847 if not rel or rel < 8:
848 print("ERROR: Distro not supported. Too old?")
849 return
850
851 # RHEL 8 uses Python 3.6, which is not compatible with
852 # the build system anymore. Suggest Python 3.11
853 if rel == 8:
854 self.check_program("python3.9", DepManager.SYSTEM_MANDATORY)
855 progs["python3.9"] = "python39"
856 progs["yaml"] = "python39-pyyaml"
857
858 self.recommend_python = True
859
860 # There's no python39-sphinx package. Only pip is supported
861 self.package_supported = False
862
863 if not self.distro_msg:
864 self.distro_msg = \
865 "Note: RHEL-based distros typically require extra repositories.\n" \
866 "For most, enabling epel and crb are enough:\n" \
867 "\tsudo dnf install -y epel-release\n" \
868 "\tsudo dnf config-manager --set-enabled crb\n" \
869 "Yet, some may have other required repositories. Those commands could be useful:\n" \
870 "\tsudo dnf repolist all\n" \
871 "\tsudo dnf repoquery --available --info <pkgs>\n" \
872 "\tsudo dnf config-manager --set-enabled '*' # enable all - probably not what you want"
873
874 if self.pdf:
875 pdf_pkgs = [
876 "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
877 "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc",
878 ]
879
880 self.check_missing_file(pdf_pkgs, noto_sans_redhat, DepManager.PDF_MANDATORY)
881
882 self.check_rpm_missing(fedora_tex_pkgs, DepManager.PDF_MANDATORY)
883
884 self.check_missing_tex(DepManager.PDF_MANDATORY)
885
886 # There's no texlive-ctex on RHEL 8 repositories. This will
887 # likely affect CJK pdf build only.
888 if not fedora and rel == 8:
889 self.deps.del_package("texlive-ctex")
890
891 return self.get_install_progs(progs, "dnf install")
892
893 def give_opensuse_hints(self):
894 """
895 Provide package installation hints for openSUSE-based distros
896 (Leap and Tumbleweed).
897 """
898 progs = {
899 "Pod::Usage": "perl-Pod-Usage",
900 "convert": "ImageMagick",
901 "dot": "graphviz",
902 "python-sphinx": "python3-sphinx",
903 "virtualenv": "python3-virtualenv",
904 "xelatex": "texlive-xetex-bin texlive-dejavu",
905 "yaml": "python3-pyyaml",
906 }
907
908 suse_tex_pkgs = [
909 "texlive-babel-english",
910 "texlive-caption",
911 "texlive-colortbl",
912 "texlive-courier",
913 "texlive-dvips",
914 "texlive-helvetic",
915 "texlive-makeindex",
916 "texlive-metafont",
917 "texlive-metapost",
918 "texlive-palatino",
919 "texlive-preview",
920 "texlive-times",
921 "texlive-zapfchan",
922 "texlive-zapfding",
923 ]
924
925 progs["latexmk"] = "texlive-latexmk-bin"
926
927 match = re.search(r"(Leap)\s+(\d+).(\d)", self.system_release)
928 if match:
929 rel = int(match.group(2))
930
931 # Leap 15.x uses Python 3.6, which is not compatible with
932 # the build system anymore. Suggest Python 3.11
933 if rel == 15:
934 if not self.which(self.python_cmd):
935 self.check_program("python3.11", DepManager.SYSTEM_MANDATORY)
936 progs["python3.11"] = "python311"
937 self.recommend_python = True
938
939 progs.update({
940 "python-sphinx": "python311-Sphinx python311-Sphinx-latex",
941 "virtualenv": "python311-virtualenv",
942 "yaml": "python311-PyYAML",
943 })
944 else:
945 # Tumbleweed defaults to Python 3.11
946
947 progs.update({
948 "python-sphinx": "python313-Sphinx python313-Sphinx-latex",
949 "virtualenv": "python313-virtualenv",
950 "yaml": "python313-PyYAML",
951 })
952
953 # FIXME: add support for installing CJK fonts
954 #
955 # I tried hard, but was unable to find a way to install
956 # "Noto Sans CJK SC" on openSUSE
957
958 if self.pdf:
959 self.check_rpm_missing(suse_tex_pkgs, DepManager.PDF_MANDATORY)
960 if self.pdf:
961 self.check_missing_tex()
962
963 return self.get_install_progs(progs, "zypper install --no-recommends")
964
965 def give_mageia_hints(self):
966 """
967 Provide package installation hints for Mageia and OpenMandriva.
968 """
969 progs = {
970 "Pod::Usage": "perl-Pod-Usage",
971 "convert": "ImageMagick",
972 "dot": "graphviz",
973 "python-sphinx": "python3-sphinx",
974 "rsvg-convert": "librsvg2",
975 "virtualenv": "python3-virtualenv",
976 "xelatex": "texlive",
977 "yaml": "python3-yaml",
978 }
979
980 tex_pkgs = [
981 "texlive-fontsextra",
982 "texlive-fonts-asian",
983 "fonts-ttf-dejavu",
984 ]
985
986 if re.search(r"OpenMandriva", self.system_release):
987 packager_cmd = "dnf install"
988 noto_sans = "noto-sans-cjk-fonts"
989 tex_pkgs = [
990 "texlive-collection-basic",
991 "texlive-collection-langcjk",
992 "texlive-collection-fontsextra",
993 "texlive-collection-fontsrecommended"
994 ]
995
996 # Tested on OpenMandriva Lx 4.3
997 progs["convert"] = "imagemagick"
998 progs["yaml"] = "python-pyyaml"
999 progs["python-virtualenv"] = "python-virtualenv"
1000 progs["python-sphinx"] = "python-sphinx"
1001 progs["xelatex"] = "texlive"
1002
1003 self.check_program("python-virtualenv", DepManager.PYTHON_MANDATORY)
1004
1005 # On my tests with openMandriva LX 4.0 docker image, upgraded
1006 # to 4.3, python-virtualenv package is broken: it is missing
1007 # ensurepip. Without it, the alternative would be to run:
1008 # python3 -m venv --without-pip ~/sphinx_latest, but running
1009 # pip there won't install sphinx at venv.
1010 #
1011 # Add a note about that.
1012
1013 if not self.distro_msg:
1014 self.distro_msg = \
1015 "Notes:\n"\
1016 "1. for venv, ensurepip could be broken, preventing its install method.\n" \
1017 "2. at least on OpenMandriva LX 4.3, texlive packages seem broken"
1018
1019 else:
1020 packager_cmd = "urpmi"
1021 noto_sans = "google-noto-sans-cjk-ttc-fonts"
1022
1023 progs["latexmk"] = "texlive-collection-basic"
1024
1025 if self.pdf:
1026 pdf_pkgs = [
1027 "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
1028 "/usr/share/fonts/TTF/NotoSans-Regular.ttf",
1029 ]
1030
1031 self.check_missing_file(pdf_pkgs, noto_sans, DepManager.PDF_MANDATORY)
1032 self.check_rpm_missing(tex_pkgs, DepManager.PDF_MANDATORY)
1033
1034 return self.get_install_progs(progs, packager_cmd)
1035
1036 def give_arch_linux_hints(self):
1037 """
1038 Provide package installation hints for ArchLinux.
1039 """
1040 progs = {
1041 "convert": "imagemagick",
1042 "dot": "graphviz",
1043 "latexmk": "texlive-core",
1044 "rsvg-convert": "extra/librsvg",
1045 "virtualenv": "python-virtualenv",
1046 "xelatex": "texlive-xetex",
1047 "yaml": "python-yaml",
1048 }
1049
1050 archlinux_tex_pkgs = [
1051 "texlive-basic",
1052 "texlive-binextra",
1053 "texlive-core",
1054 "texlive-fontsrecommended",
1055 "texlive-langchinese",
1056 "texlive-langcjk",
1057 "texlive-latexextra",
1058 "ttf-dejavu",
1059 ]
1060
1061 if self.pdf:
1062 self.check_pacman_missing(archlinux_tex_pkgs,
1063 DepManager.PDF_MANDATORY)
1064
1065 self.check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"],
1066 "noto-fonts-cjk",
1067 DepManager.PDF_MANDATORY)
1068
1069
1070 return self.get_install_progs(progs, "pacman -S")
1071
1072 def give_gentoo_hints(self):
1073 """
1074 Provide package installation hints for Gentoo.
1075 """
1076 texlive_deps = [
1077 "dev-texlive/texlive-fontsrecommended",
1078 "dev-texlive/texlive-latexextra",
1079 "dev-texlive/texlive-xetex",
1080 "media-fonts/dejavu",
1081 ]
1082
1083 progs = {
1084 "convert": "media-gfx/imagemagick",
1085 "dot": "media-gfx/graphviz",
1086 "rsvg-convert": "gnome-base/librsvg",
1087 "virtualenv": "dev-python/virtualenv",
1088 "xelatex": " ".join(texlive_deps),
1089 "yaml": "dev-python/pyyaml",
1090 "python-sphinx": "dev-python/sphinx",
1091 }
1092
1093 if self.pdf:
1094 pdf_pkgs = {
1095 "media-fonts/dejavu": [
1096 "/usr/share/fonts/dejavu/DejaVuSans.ttf",
1097 ],
1098 "media-fonts/noto-cjk": [
1099 "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
1100 "/usr/share/fonts/noto-cjk/NotoSerifCJK-Regular.ttc",
1101 ],
1102 }
1103 for package, files in pdf_pkgs.items():
1104 self.check_missing_file(files, package, DepManager.PDF_MANDATORY)
1105
1106 # Handling dependencies is a nightmare, as Gentoo refuses to emerge
1107 # some packages if there's no package.use file describing them.
1108 # To make it worse, compilation flags shall also be present there
1109 # for some packages. If USE is not perfect, error/warning messages
1110 # like those are shown:
1111 #
1112 # !!! The following binary packages have been ignored due to non matching USE:
1113 #
1114 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_13 qt6 svg
1115 # =media-gfx/graphviz-12.2.1-r1 X pdf python_single_target_python3_12 -python_single_target_python3_13 qt6 svg
1116 # =media-gfx/graphviz-12.2.1-r1 X pdf qt6 svg
1117 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_10 qt6 svg
1118 # =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
1119 # =media-fonts/noto-cjk-20190416 X
1120 # =app-text/texlive-core-2024-r1 X cjk -xetex
1121 # =app-text/texlive-core-2024-r1 X -xetex
1122 # =app-text/texlive-core-2024-r1 -xetex
1123 # =dev-libs/zziplib-0.13.79-r1 sdl
1124 #
1125 # And will ignore such packages, installing the remaining ones. That
1126 # affects mostly the image extension and PDF generation.
1127
1128 # Package dependencies and the minimal needed args:
1129 portages = {
1130 "graphviz": "media-gfx/graphviz",
1131 "imagemagick": "media-gfx/imagemagick",
1132 "media-libs": "media-libs/harfbuzz icu",
1133 "media-fonts": "media-fonts/noto-cjk",
1134 "texlive": "app-text/texlive-core xetex",
1135 "zziblib": "dev-libs/zziplib sdl",
1136 }
1137
1138 extra_cmds = ""
1139 if not self.distro_msg:
1140 self.distro_msg = "Note: Gentoo requires package.use to be adjusted before emerging packages"
1141
1142 use_base = "/etc/portage/package.use"
1143 files = glob(f"{use_base}/*")
1144
1145 for fname, portage in portages.items():
1146 install = False
1147
1148 while install is False:
1149 if not files:
1150 # No files under package.usage. Install all
1151 install = True
1152 break
1153
1154 args = portage.split(" ")
1155
1156 name = args.pop(0)
1157
1158 cmd = ["grep", "-l", "-E", rf"^{name}\b" ] + files
1159 result = self.run(cmd, stdout=subprocess.PIPE, text=True)
1160 if result.returncode or not result.stdout.strip():
1161 # File containing portage name not found
1162 install = True
1163 break
1164
1165 # Ensure that needed USE flags are present
1166 if args:
1167 match_fname = result.stdout.strip()
1168 with open(match_fname, 'r', encoding='utf8',
1169 errors='backslashreplace') as fp:
1170 for line in fp:
1171 for arg in args:
1172 if arg.startswith("-"):
1173 continue
1174
1175 if not re.search(rf"\s*{arg}\b", line):
1176 # Needed file argument not found
1177 install = True
1178 break
1179
1180 # Everything looks ok, don't install
1181 break
1182
1183 # emit a code to setup missing USE
1184 if install:
1185 extra_cmds += (f"sudo su -c 'echo \"{portage}\" > {use_base}/{fname}'\n")
1186
1187 # Now, we can use emerge and let it respect USE
1188 return self.get_install_progs(progs,
1189 "emerge --ask --changed-use --binpkg-respect-use=y",
1190 extra_cmds)
1191
1192 def get_install(self):
1193 """
1194 OS-specific hints logic. Seeks for a hinter. If found, use it to
1195 provide package-manager specific install commands.
1196
1197 Otherwise, outputs install instructions for the meta-packages.
1198
1199 Returns a string with the command to be executed to install the
1200 the needed packages, if distro found. Otherwise, return just a
1201 list of packages that require installation.
1202 """
1203 os_hints = {
1204 re.compile("Red Hat Enterprise Linux"): self.give_redhat_hints,
1205 re.compile("Fedora"): self.give_redhat_hints,
1206 re.compile("AlmaLinux"): self.give_redhat_hints,
1207 re.compile("Amazon Linux"): self.give_redhat_hints,
1208 re.compile("CentOS"): self.give_redhat_hints,
1209 re.compile("openEuler"): self.give_redhat_hints,
1210 re.compile("Oracle Linux Server"): self.give_redhat_hints,
1211 re.compile("Rocky Linux"): self.give_redhat_hints,
1212 re.compile("Springdale Open Enterprise"): self.give_redhat_hints,
1213
1214 re.compile("Ubuntu"): self.give_debian_hints,
1215 re.compile("Debian"): self.give_debian_hints,
1216 re.compile("Devuan"): self.give_debian_hints,
1217 re.compile("Kali"): self.give_debian_hints,
1218 re.compile("Mint"): self.give_debian_hints,
1219
1220 re.compile("openSUSE"): self.give_opensuse_hints,
1221
1222 re.compile("Mageia"): self.give_mageia_hints,
1223 re.compile("OpenMandriva"): self.give_mageia_hints,
1224
1225 re.compile("Arch Linux"): self.give_arch_linux_hints,
1226 re.compile("Gentoo"): self.give_gentoo_hints,
1227 }
1228
1229 # If the OS is detected, use per-OS hint logic
1230 for regex, os_hint in os_hints.items():
1231 if regex.search(self.system_release):
1232 return os_hint()
1233
1234 #
1235 # Fall-back to generic hint code for other distros
1236 # That's far from ideal, specially for LaTeX dependencies.
1237 #
1238 progs = {"sphinx-build": "sphinx"}
1239 if self.pdf:
1240 self.check_missing_tex()
1241
1242 self.distro_msg = \
1243 f"I don't know distro {self.system_release}.\n" \
1244 "So, I can't provide you a hint with the install procedure.\n" \
1245 "There are likely missing dependencies."
1246
1247 return self.get_install_progs(progs, None)
1248
1249 #
1250 # Common dependencies
1251 #
1252 def deactivate_help(self):
1253 """
1254 Print a helper message to disable a virtual environment.
1255 """
1256
1257 print("\n If you want to exit the virtualenv, you can use:")
1258 print("\tdeactivate")
1259
1260 def get_virtenv(self):
1261 """
1262 Give a hint about how to activate an already-existing virtual
1263 environment containing sphinx-build.
1264
1265 Returns a tuble with (activate_cmd_path, sphinx_version) with
1266 the newest available virtual env.
1267 """
1268
1269 cwd = os.getcwd()
1270
1271 activates = []
1272
1273 # Add all sphinx prefixes with possible version numbers
1274 for p in self.virtenv_prefix:
1275 activates += glob(f"{cwd}/{p}[0-9]*/bin/activate")
1276
1277 activates.sort(reverse=True, key=str.lower)
1278
1279 # Place sphinx_latest first, if it exists
1280 for p in self.virtenv_prefix:
1281 activates = glob(f"{cwd}/{p}*latest/bin/activate") + activates
1282
1283 ver = (0, 0, 0)
1284 for f in activates:
1285 # Discard too old Sphinx virtual environments
1286 match = re.search(r"(\d+)\.(\d+)\.(\d+)", f)
1287 if match:
1288 ver = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
1289
1290 if ver < self.min_version:
1291 continue
1292
1293 sphinx_cmd = f.replace("activate", "sphinx-build")
1294 if not os.path.isfile(sphinx_cmd):
1295 continue
1296
1297 ver = self.get_sphinx_version(sphinx_cmd)
1298
1299 if not ver:
1300 venv_dir = f.replace("/bin/activate", "")
1301 print(f"Warning: virtual environment {venv_dir} is not working.\n" \
1302 "Python version upgrade? Remove it with:\n\n" \
1303 "\trm -rf {venv_dir}\n\n")
1304 else:
1305 if self.need_sphinx and ver >= self.min_version:
1306 return (f, ver)
1307 elif parse_version(ver) > self.cur_version:
1308 return (f, ver)
1309
1310 return ("", ver)
1311
1312 def recommend_sphinx_upgrade(self):
1313 """
1314 Check if Sphinx needs to be upgraded.
1315
1316 Returns a tuple with the higest available Sphinx version if found.
1317 Otherwise, returns None to indicate either that no upgrade is needed
1318 or no venv was found.
1319 """
1320
1321 # Avoid running sphinx-builds from venv if cur_version is good
1322 if self.cur_version and self.cur_version >= RECOMMENDED_VERSION:
1323 self.latest_avail_ver = self.cur_version
1324 return None
1325
1326 # Get the highest version from sphinx_*/bin/sphinx-build and the
1327 # corresponding command to activate the venv/virtenv
1328 self.activate_cmd, self.venv_ver = self.get_virtenv()
1329
1330 # Store the highest version from Sphinx existing virtualenvs
1331 if self.activate_cmd and self.venv_ver > self.cur_version:
1332 self.latest_avail_ver = self.venv_ver
1333 else:
1334 if self.cur_version:
1335 self.latest_avail_ver = self.cur_version
1336 else:
1337 self.latest_avail_ver = (0, 0, 0)
1338
1339 # As we don't know package version of Sphinx, and there's no
1340 # virtual environments, don't check if upgrades are needed
1341 if not self.virtualenv:
1342 if not self.latest_avail_ver:
1343 return None
1344
1345 return self.latest_avail_ver
1346
1347 # Either there are already a virtual env or a new one should be created
1348 self.need_pip = True
1349
1350 if not self.latest_avail_ver:
1351 return None
1352
1353 # Return if the reason is due to an upgrade or not
1354 if self.latest_avail_ver != (0, 0, 0):
1355 if self.latest_avail_ver < RECOMMENDED_VERSION:
1356 self.rec_sphinx_upgrade = 1
1357
1358 return self.latest_avail_ver
1359
1360 def recommend_package(self):
1361 """
1362 Recommend installing Sphinx as a distro-specific package.
1363 """
1364
1365 print("\n2) As a package with:")
1366
1367 old_need = self.deps.need
1368 old_optional = self.deps.optional
1369
1370 self.pdf = False
1371 self.deps.optional = 0
1372 old_verbose = self.verbose_warn_install
1373 self.verbose_warn_install = 0
1374
1375 self.deps.clear_deps()
1376
1377 self.deps.add_package("python-sphinx", DepManager.PYTHON_MANDATORY)
1378
1379 cmd = self.get_install()
1380 if cmd:
1381 print(cmd)
1382
1383 self.deps.need = old_need
1384 self.deps.optional = old_optional
1385 self.verbose_warn_install = old_verbose
1386
1387 def recommend_sphinx_version(self, virtualenv_cmd):
1388 """
1389 Provide recommendations for installing or upgrading Sphinx based
1390 on current version.
1391
1392 The logic here is complex, as it have to deal with different versions:
1393
1394 - minimal supported version;
1395 - minimal PDF version;
1396 - recommended version.
1397
1398 It also needs to work fine with both distro's package and
1399 venv/virtualenv
1400 """
1401
1402 if self.recommend_python:
1403 cur_ver = sys.version_info[:3]
1404 if cur_ver < MIN_PYTHON_VERSION:
1405 print(f"\nPython version {cur_ver} is incompatible with doc build.\n" \
1406 "Please upgrade it and re-run.\n")
1407 return
1408
1409 # Version is OK. Nothing to do.
1410 if self.cur_version != (0, 0, 0) and self.cur_version >= RECOMMENDED_VERSION:
1411 return
1412
1413 if self.latest_avail_ver:
1414 latest_avail_ver = ver_str(self.latest_avail_ver)
1415
1416 if not self.need_sphinx:
1417 # sphinx-build is present and its version is >= $min_version
1418
1419 # only recommend enabling a newer virtenv version if makes sense.
1420 if self.latest_avail_ver and self.latest_avail_ver > self.cur_version:
1421 print(f"\nYou may also use the newer Sphinx version {latest_avail_ver} with:")
1422 if f"{self.virtenv_prefix}" in os.getcwd():
1423 print("\tdeactivate")
1424 print(f"\t. {self.activate_cmd}")
1425 self.deactivate_help()
1426 return
1427
1428 if self.latest_avail_ver and self.latest_avail_ver >= RECOMMENDED_VERSION:
1429 return
1430
1431 if not self.virtualenv:
1432 # No sphinx either via package or via virtenv. As we can't
1433 # Compare the versions here, just return, recommending the
1434 # user to install it from the package distro.
1435 if not self.latest_avail_ver or self.latest_avail_ver == (0, 0, 0):
1436 return
1437
1438 # User doesn't want a virtenv recommendation, but he already
1439 # installed one via virtenv with a newer version.
1440 # So, print commands to enable it
1441 if self.latest_avail_ver > self.cur_version:
1442 print(f"\nYou may also use the Sphinx virtualenv version {latest_avail_ver} with:")
1443 if f"{self.virtenv_prefix}" in os.getcwd():
1444 print("\tdeactivate")
1445 print(f"\t. {self.activate_cmd}")
1446 self.deactivate_help()
1447 return
1448 print("\n")
1449 else:
1450 if self.need_sphinx:
1451 self.deps.need += 1
1452
1453 # Suggest newer versions if current ones are too old
1454 if self.latest_avail_ver and self.latest_avail_ver >= self.min_version:
1455 if self.latest_avail_ver >= RECOMMENDED_VERSION:
1456 print(f"\nNeed to activate Sphinx (version {latest_avail_ver}) on virtualenv with:")
1457 print(f"\t. {self.activate_cmd}")
1458 self.deactivate_help()
1459 return
1460
1461 # Version is above the minimal required one, but may be
1462 # below the recommended one. So, print warnings/notes
1463 if self.latest_avail_ver < RECOMMENDED_VERSION:
1464 print(f"Warning: It is recommended at least Sphinx version {RECOMMENDED_VERSION}.")
1465
1466 # At this point, either it needs Sphinx or upgrade is recommended,
1467 # both via pip
1468
1469 if self.rec_sphinx_upgrade:
1470 if not self.virtualenv:
1471 print("Instead of install/upgrade Python Sphinx pkg, you could use pip/pypi with:\n\n")
1472 else:
1473 print("To upgrade Sphinx, use:\n\n")
1474 else:
1475 print("\nSphinx needs to be installed either:\n1) via pip/pypi with:\n")
1476
1477 if not virtualenv_cmd:
1478 print(" Currently not possible.\n")
1479 print(" Please upgrade Python to a newer version and run this script again")
1480 else:
1481 print(f"\t{virtualenv_cmd} {self.virtenv_dir}")
1482 print(f"\t. {self.virtenv_dir}/bin/activate")
1483 print(f"\tpip install -r {self.requirement_file}")
1484 self.deactivate_help()
1485
1486 if self.package_supported:
1487 self.recommend_package()
1488
1489 print("\n" \
1490 " Please note that Sphinx currentlys produce false-positive\n" \
1491 " warnings when the same name is used for more than one type (functions,\n" \
1492 " structs, enums,...). This is known Sphinx bug. For more details, see:\n" \
1493 "\thttps://github.com/sphinx-doc/sphinx/pull/8313")
1494
1495 def check_needs(self):
1496 """
1497 Main method that checks needed dependencies and provides
1498 recommendations.
1499 """
1500 self.python_cmd = sys.executable
1501
1502 # Check if Sphinx is already accessible from current environment
1503 self.check_sphinx(self.conf)
1504
1505 if self.system_release:
1506 print(f"Detected OS: {self.system_release}.")
1507 else:
1508 print("Unknown OS")
1509 if self.cur_version != (0, 0, 0):
1510 ver = ver_str(self.cur_version)
1511 print(f"Sphinx version: {ver}\n")
1512
1513 # Check the type of virtual env, depending on Python version
1514 virtualenv_cmd = None
1515
1516 if sys.version_info < MIN_PYTHON_VERSION:
1517 min_ver = ver_str(MIN_PYTHON_VERSION)
1518 print(f"ERROR: at least python {min_ver} is required to build the kernel docs")
1519 self.need_sphinx = 1
1520
1521 self.venv_ver = self.recommend_sphinx_upgrade()
1522
1523 if self.need_pip:
1524 if sys.version_info < MIN_PYTHON_VERSION:
1525 self.need_pip = False
1526 print("Warning: python version is not supported.")
1527 else:
1528 virtualenv_cmd = f"{self.python_cmd} -m venv"
1529 self.check_python_module("ensurepip")
1530
1531 # Check for needed programs/tools
1532 self.check_perl_module("Pod::Usage", DepManager.SYSTEM_MANDATORY)
1533
1534 self.check_program("make", DepManager.SYSTEM_MANDATORY)
1535 self.check_program("which", DepManager.SYSTEM_MANDATORY)
1536
1537 self.check_program("dot", DepManager.SYSTEM_OPTIONAL)
1538 self.check_program("convert", DepManager.SYSTEM_OPTIONAL)
1539
1540 self.check_python_module("yaml")
1541
1542 if self.pdf:
1543 self.check_program("xelatex", DepManager.PDF_MANDATORY)
1544 self.check_program("rsvg-convert", DepManager.PDF_MANDATORY)
1545 self.check_program("latexmk", DepManager.PDF_MANDATORY)
1546
1547 # Do distro-specific checks and output distro-install commands
1548 cmd = self.get_install()
1549 if cmd:
1550 print(cmd)
1551
1552 # If distro requires some special instructions, print here.
1553 # Please notice that get_install() needs to be called first.
1554 if self.distro_msg:
1555 print("\n" + self.distro_msg)
1556
1557 if not self.python_cmd:
1558 if self.need == 1:
1559 sys.exit("Can't build as 1 mandatory dependency is missing")
1560 elif self.need:
1561 sys.exit(f"Can't build as {self.need} mandatory dependencies are missing")
1562
1563 # Check if sphinx-build is called sphinx-build-3
1564 if self.need_symlink:
1565 sphinx_path = self.which("sphinx-build-3")
1566 if sphinx_path:
1567 print(f"\tsudo ln -sf {sphinx_path} /usr/bin/sphinx-build\n")
1568
1569 self.recommend_sphinx_version(virtualenv_cmd)
1570 print("")
1571
1572 if not self.deps.optional:
1573 print("All optional dependencies are met.")
1574
1575 if self.deps.need == 1:
1576 sys.exit("Can't build as 1 mandatory dependency is missing")
1577 elif self.deps.need:
1578 sys.exit(f"Can't build as {self.deps.need} mandatory dependencies are missing")
1579
1580 print("Needed package dependencies are met.")
1581
1582DESCRIPTION = """
1583Process some flags related to Sphinx installation and documentation build.
1584"""
1585
1586
1587def main():
1588 """Main function"""
1589 parser = argparse.ArgumentParser(description=DESCRIPTION)
1590
1591 parser.add_argument(
1592 "--no-virtualenv",
1593 action="store_false",
1594 dest="virtualenv",
1595 help="Recommend installing Sphinx instead of using a virtualenv",
1596 )
1597
1598 parser.add_argument(
1599 "--no-pdf",
1600 action="store_false",
1601 dest="pdf",
1602 help="Don't check for dependencies required to build PDF docs",
1603 )
1604
1605 parser.add_argument(
1606 "--version-check",
1607 action="store_true",
1608 dest="version_check",
1609 help="If version is compatible, don't check for missing dependencies",
1610 )
1611
1612 args = parser.parse_args()
1613
1614 checker = SphinxDependencyChecker(args)
1615
1616 checker.check_python()
1617 checker.check_needs()
1618
1619# Call main if not used as module
1620if __name__ == "__main__":
1621 main()