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-core", 1052 "texlive-latexextra", 1053 "ttf-dejavu", 1054 ] 1055 1056 if self.pdf: 1057 self.check_pacman_missing(archlinux_tex_pkgs, 1058 DepManager.PDF_MANDATORY) 1059 1060 self.check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"], 1061 "noto-fonts-cjk", 1062 DepManager.PDF_MANDATORY) 1063 1064 1065 return self.get_install_progs(progs, "pacman -S") 1066 1067 def give_gentoo_hints(self): 1068 """ 1069 Provide package installation hints for Gentoo. 1070 """ 1071 texlive_deps = [ 1072 "dev-texlive/texlive-latexextra", 1073 "dev-texlive/texlive-xetex", 1074 "media-fonts/dejavu", 1075 "media-fonts/lm", 1076 ] 1077 1078 progs = { 1079 "convert": "media-gfx/imagemagick", 1080 "dot": "media-gfx/graphviz", 1081 "rsvg-convert": "gnome-base/librsvg", 1082 "virtualenv": "dev-python/virtualenv", 1083 "xelatex": " ".join(texlive_deps), 1084 "yaml": "dev-python/pyyaml", 1085 "python-sphinx": "dev-python/sphinx", 1086 } 1087 1088 if self.pdf: 1089 pdf_pkgs = { 1090 "media-fonts/dejavu": [ 1091 "/usr/share/fonts/dejavu/DejaVuSans.ttf", 1092 ], 1093 "media-fonts/noto-cjk": [ 1094 "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf", 1095 "/usr/share/fonts/noto-cjk/NotoSerifCJK-Regular.ttc", 1096 ], 1097 } 1098 for package, files in pdf_pkgs.items(): 1099 self.check_missing_file(files, package, DepManager.PDF_MANDATORY) 1100 1101 # Handling dependencies is a nightmare, as Gentoo refuses to emerge 1102 # some packages if there's no package.use file describing them. 1103 # To make it worse, compilation flags shall also be present there 1104 # for some packages. If USE is not perfect, error/warning messages 1105 # like those are shown: 1106 # 1107 # !!! The following binary packages have been ignored due to non matching USE: 1108 # 1109 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_13 qt6 svg 1110 # =media-gfx/graphviz-12.2.1-r1 X pdf python_single_target_python3_12 -python_single_target_python3_13 qt6 svg 1111 # =media-gfx/graphviz-12.2.1-r1 X pdf qt6 svg 1112 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_10 qt6 svg 1113 # =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 1114 # =media-fonts/noto-cjk-20190416 X 1115 # =app-text/texlive-core-2024-r1 X cjk -xetex 1116 # =app-text/texlive-core-2024-r1 X -xetex 1117 # =app-text/texlive-core-2024-r1 -xetex 1118 # =dev-libs/zziplib-0.13.79-r1 sdl 1119 # 1120 # And will ignore such packages, installing the remaining ones. That 1121 # affects mostly the image extension and PDF generation. 1122 1123 # Package dependencies and the minimal needed args: 1124 portages = { 1125 "graphviz": "media-gfx/graphviz", 1126 "imagemagick": "media-gfx/imagemagick", 1127 "media-libs": "media-libs/harfbuzz icu", 1128 "media-fonts": "media-fonts/noto-cjk", 1129 "texlive": "app-text/texlive-core xetex", 1130 "zziblib": "dev-libs/zziplib sdl", 1131 } 1132 1133 extra_cmds = "" 1134 if not self.distro_msg: 1135 self.distro_msg = "Note: Gentoo requires package.use to be adjusted before emerging packages" 1136 1137 use_base = "/etc/portage/package.use" 1138 files = glob(f"{use_base}/*") 1139 1140 for fname, portage in portages.items(): 1141 install = False 1142 1143 while install is False: 1144 if not files: 1145 # No files under package.usage. Install all 1146 install = True 1147 break 1148 1149 args = portage.split(" ") 1150 1151 name = args.pop(0) 1152 1153 cmd = ["grep", "-l", "-E", rf"^{name}\b" ] + files 1154 result = self.run(cmd, stdout=subprocess.PIPE, text=True) 1155 if result.returncode or not result.stdout.strip(): 1156 # File containing portage name not found 1157 install = True 1158 break 1159 1160 # Ensure that needed USE flags are present 1161 if args: 1162 match_fname = result.stdout.strip() 1163 with open(match_fname, 'r', encoding='utf8', 1164 errors='backslashreplace') as fp: 1165 for line in fp: 1166 for arg in args: 1167 if arg.startswith("-"): 1168 continue 1169 1170 if not re.search(rf"\s*{arg}\b", line): 1171 # Needed file argument not found 1172 install = True 1173 break 1174 1175 # Everything looks ok, don't install 1176 break 1177 1178 # emit a code to setup missing USE 1179 if install: 1180 extra_cmds += (f"sudo su -c 'echo \"{portage}\" > {use_base}/{fname}'\n") 1181 1182 # Now, we can use emerge and let it respect USE 1183 return self.get_install_progs(progs, 1184 "emerge --ask --changed-use --binpkg-respect-use=y", 1185 extra_cmds) 1186 1187 def get_install(self): 1188 """ 1189 OS-specific hints logic. Seeks for a hinter. If found, use it to 1190 provide package-manager specific install commands. 1191 1192 Otherwise, outputs install instructions for the meta-packages. 1193 1194 Returns a string with the command to be executed to install the 1195 the needed packages, if distro found. Otherwise, return just a 1196 list of packages that require installation. 1197 """ 1198 os_hints = { 1199 re.compile("Red Hat Enterprise Linux"): self.give_redhat_hints, 1200 re.compile("Fedora"): self.give_redhat_hints, 1201 re.compile("AlmaLinux"): self.give_redhat_hints, 1202 re.compile("Amazon Linux"): self.give_redhat_hints, 1203 re.compile("CentOS"): self.give_redhat_hints, 1204 re.compile("openEuler"): self.give_redhat_hints, 1205 re.compile("Oracle Linux Server"): self.give_redhat_hints, 1206 re.compile("Rocky Linux"): self.give_redhat_hints, 1207 re.compile("Springdale Open Enterprise"): self.give_redhat_hints, 1208 1209 re.compile("Ubuntu"): self.give_debian_hints, 1210 re.compile("Debian"): self.give_debian_hints, 1211 re.compile("Devuan"): self.give_debian_hints, 1212 re.compile("Kali"): self.give_debian_hints, 1213 re.compile("Mint"): self.give_debian_hints, 1214 1215 re.compile("openSUSE"): self.give_opensuse_hints, 1216 1217 re.compile("Mageia"): self.give_mageia_hints, 1218 re.compile("OpenMandriva"): self.give_mageia_hints, 1219 1220 re.compile("Arch Linux"): self.give_arch_linux_hints, 1221 re.compile("Gentoo"): self.give_gentoo_hints, 1222 } 1223 1224 # If the OS is detected, use per-OS hint logic 1225 for regex, os_hint in os_hints.items(): 1226 if regex.search(self.system_release): 1227 return os_hint() 1228 1229 # 1230 # Fall-back to generic hint code for other distros 1231 # That's far from ideal, specially for LaTeX dependencies. 1232 # 1233 progs = {"sphinx-build": "sphinx"} 1234 if self.pdf: 1235 self.check_missing_tex() 1236 1237 self.distro_msg = \ 1238 f"I don't know distro {self.system_release}.\n" \ 1239 "So, I can't provide you a hint with the install procedure.\n" \ 1240 "There are likely missing dependencies." 1241 1242 return self.get_install_progs(progs, None) 1243 1244 # 1245 # Common dependencies 1246 # 1247 def deactivate_help(self): 1248 """ 1249 Print a helper message to disable a virtual environment. 1250 """ 1251 1252 print("\n If you want to exit the virtualenv, you can use:") 1253 print("\tdeactivate") 1254 1255 def get_virtenv(self): 1256 """ 1257 Give a hint about how to activate an already-existing virtual 1258 environment containing sphinx-build. 1259 1260 Returns a tuble with (activate_cmd_path, sphinx_version) with 1261 the newest available virtual env. 1262 """ 1263 1264 cwd = os.getcwd() 1265 1266 activates = [] 1267 1268 # Add all sphinx prefixes with possible version numbers 1269 for p in self.virtenv_prefix: 1270 activates += glob(f"{cwd}/{p}[0-9]*/bin/activate") 1271 1272 activates.sort(reverse=True, key=str.lower) 1273 1274 # Place sphinx_latest first, if it exists 1275 for p in self.virtenv_prefix: 1276 activates = glob(f"{cwd}/{p}*latest/bin/activate") + activates 1277 1278 ver = (0, 0, 0) 1279 for f in activates: 1280 # Discard too old Sphinx virtual environments 1281 match = re.search(r"(\d+)\.(\d+)\.(\d+)", f) 1282 if match: 1283 ver = (int(match.group(1)), int(match.group(2)), int(match.group(3))) 1284 1285 if ver < self.min_version: 1286 continue 1287 1288 sphinx_cmd = f.replace("activate", "sphinx-build") 1289 if not os.path.isfile(sphinx_cmd): 1290 continue 1291 1292 ver = self.get_sphinx_version(sphinx_cmd) 1293 1294 if not ver: 1295 venv_dir = f.replace("/bin/activate", "") 1296 print(f"Warning: virtual environment {venv_dir} is not working.\n" \ 1297 "Python version upgrade? Remove it with:\n\n" \ 1298 "\trm -rf {venv_dir}\n\n") 1299 else: 1300 if self.need_sphinx and ver >= self.min_version: 1301 return (f, ver) 1302 elif parse_version(ver) > self.cur_version: 1303 return (f, ver) 1304 1305 return ("", ver) 1306 1307 def recommend_sphinx_upgrade(self): 1308 """ 1309 Check if Sphinx needs to be upgraded. 1310 1311 Returns a tuple with the higest available Sphinx version if found. 1312 Otherwise, returns None to indicate either that no upgrade is needed 1313 or no venv was found. 1314 """ 1315 1316 # Avoid running sphinx-builds from venv if cur_version is good 1317 if self.cur_version and self.cur_version >= RECOMMENDED_VERSION: 1318 self.latest_avail_ver = self.cur_version 1319 return None 1320 1321 # Get the highest version from sphinx_*/bin/sphinx-build and the 1322 # corresponding command to activate the venv/virtenv 1323 self.activate_cmd, self.venv_ver = self.get_virtenv() 1324 1325 # Store the highest version from Sphinx existing virtualenvs 1326 if self.activate_cmd and self.venv_ver > self.cur_version: 1327 self.latest_avail_ver = self.venv_ver 1328 else: 1329 if self.cur_version: 1330 self.latest_avail_ver = self.cur_version 1331 else: 1332 self.latest_avail_ver = (0, 0, 0) 1333 1334 # As we don't know package version of Sphinx, and there's no 1335 # virtual environments, don't check if upgrades are needed 1336 if not self.virtualenv: 1337 if not self.latest_avail_ver: 1338 return None 1339 1340 return self.latest_avail_ver 1341 1342 # Either there are already a virtual env or a new one should be created 1343 self.need_pip = True 1344 1345 if not self.latest_avail_ver: 1346 return None 1347 1348 # Return if the reason is due to an upgrade or not 1349 if self.latest_avail_ver != (0, 0, 0): 1350 if self.latest_avail_ver < RECOMMENDED_VERSION: 1351 self.rec_sphinx_upgrade = 1 1352 1353 return self.latest_avail_ver 1354 1355 def recommend_package(self): 1356 """ 1357 Recommend installing Sphinx as a distro-specific package. 1358 """ 1359 1360 print("\n2) As a package with:") 1361 1362 old_need = self.deps.need 1363 old_optional = self.deps.optional 1364 1365 self.pdf = False 1366 self.deps.optional = 0 1367 old_verbose = self.verbose_warn_install 1368 self.verbose_warn_install = 0 1369 1370 self.deps.clear_deps() 1371 1372 self.deps.add_package("python-sphinx", DepManager.PYTHON_MANDATORY) 1373 1374 cmd = self.get_install() 1375 if cmd: 1376 print(cmd) 1377 1378 self.deps.need = old_need 1379 self.deps.optional = old_optional 1380 self.verbose_warn_install = old_verbose 1381 1382 def recommend_sphinx_version(self, virtualenv_cmd): 1383 """ 1384 Provide recommendations for installing or upgrading Sphinx based 1385 on current version. 1386 1387 The logic here is complex, as it have to deal with different versions: 1388 1389 - minimal supported version; 1390 - minimal PDF version; 1391 - recommended version. 1392 1393 It also needs to work fine with both distro's package and 1394 venv/virtualenv 1395 """ 1396 1397 if self.recommend_python: 1398 cur_ver = sys.version_info[:3] 1399 if cur_ver < MIN_PYTHON_VERSION: 1400 print(f"\nPython version {cur_ver} is incompatible with doc build.\n" \ 1401 "Please upgrade it and re-run.\n") 1402 return 1403 1404 # Version is OK. Nothing to do. 1405 if self.cur_version != (0, 0, 0) and self.cur_version >= RECOMMENDED_VERSION: 1406 return 1407 1408 if self.latest_avail_ver: 1409 latest_avail_ver = ver_str(self.latest_avail_ver) 1410 1411 if not self.need_sphinx: 1412 # sphinx-build is present and its version is >= $min_version 1413 1414 # only recommend enabling a newer virtenv version if makes sense. 1415 if self.latest_avail_ver and self.latest_avail_ver > self.cur_version: 1416 print(f"\nYou may also use the newer Sphinx version {latest_avail_ver} with:") 1417 if f"{self.virtenv_prefix}" in os.getcwd(): 1418 print("\tdeactivate") 1419 print(f"\t. {self.activate_cmd}") 1420 self.deactivate_help() 1421 return 1422 1423 if self.latest_avail_ver and self.latest_avail_ver >= RECOMMENDED_VERSION: 1424 return 1425 1426 if not self.virtualenv: 1427 # No sphinx either via package or via virtenv. As we can't 1428 # Compare the versions here, just return, recommending the 1429 # user to install it from the package distro. 1430 if not self.latest_avail_ver or self.latest_avail_ver == (0, 0, 0): 1431 return 1432 1433 # User doesn't want a virtenv recommendation, but he already 1434 # installed one via virtenv with a newer version. 1435 # So, print commands to enable it 1436 if self.latest_avail_ver > self.cur_version: 1437 print(f"\nYou may also use the Sphinx virtualenv version {latest_avail_ver} with:") 1438 if f"{self.virtenv_prefix}" in os.getcwd(): 1439 print("\tdeactivate") 1440 print(f"\t. {self.activate_cmd}") 1441 self.deactivate_help() 1442 return 1443 print("\n") 1444 else: 1445 if self.need_sphinx: 1446 self.deps.need += 1 1447 1448 # Suggest newer versions if current ones are too old 1449 if self.latest_avail_ver and self.latest_avail_ver >= self.min_version: 1450 if self.latest_avail_ver >= RECOMMENDED_VERSION: 1451 print(f"\nNeed to activate Sphinx (version {latest_avail_ver}) on virtualenv with:") 1452 print(f"\t. {self.activate_cmd}") 1453 self.deactivate_help() 1454 return 1455 1456 # Version is above the minimal required one, but may be 1457 # below the recommended one. So, print warnings/notes 1458 if self.latest_avail_ver < RECOMMENDED_VERSION: 1459 print(f"Warning: It is recommended at least Sphinx version {RECOMMENDED_VERSION}.") 1460 1461 # At this point, either it needs Sphinx or upgrade is recommended, 1462 # both via pip 1463 1464 if self.rec_sphinx_upgrade: 1465 if not self.virtualenv: 1466 print("Instead of install/upgrade Python Sphinx pkg, you could use pip/pypi with:\n\n") 1467 else: 1468 print("To upgrade Sphinx, use:\n\n") 1469 else: 1470 print("\nSphinx needs to be installed either:\n1) via pip/pypi with:\n") 1471 1472 if not virtualenv_cmd: 1473 print(" Currently not possible.\n") 1474 print(" Please upgrade Python to a newer version and run this script again") 1475 else: 1476 print(f"\t{virtualenv_cmd} {self.virtenv_dir}") 1477 print(f"\t. {self.virtenv_dir}/bin/activate") 1478 print(f"\tpip install -r {self.requirement_file}") 1479 self.deactivate_help() 1480 1481 if self.package_supported: 1482 self.recommend_package() 1483 1484 print("\n" \ 1485 " Please note that Sphinx currentlys produce false-positive\n" \ 1486 " warnings when the same name is used for more than one type (functions,\n" \ 1487 " structs, enums,...). This is known Sphinx bug. For more details, see:\n" \ 1488 "\thttps://github.com/sphinx-doc/sphinx/pull/8313") 1489 1490 def check_needs(self): 1491 """ 1492 Main method that checks needed dependencies and provides 1493 recommendations. 1494 """ 1495 self.python_cmd = sys.executable 1496 1497 # Check if Sphinx is already accessible from current environment 1498 self.check_sphinx(self.conf) 1499 1500 if self.system_release: 1501 print(f"Detected OS: {self.system_release}.") 1502 else: 1503 print("Unknown OS") 1504 if self.cur_version != (0, 0, 0): 1505 ver = ver_str(self.cur_version) 1506 print(f"Sphinx version: {ver}\n") 1507 1508 # Check the type of virtual env, depending on Python version 1509 virtualenv_cmd = None 1510 1511 if sys.version_info < MIN_PYTHON_VERSION: 1512 min_ver = ver_str(MIN_PYTHON_VERSION) 1513 print(f"ERROR: at least python {min_ver} is required to build the kernel docs") 1514 self.need_sphinx = 1 1515 1516 self.venv_ver = self.recommend_sphinx_upgrade() 1517 1518 if self.need_pip: 1519 if sys.version_info < MIN_PYTHON_VERSION: 1520 self.need_pip = False 1521 print("Warning: python version is not supported.") 1522 else: 1523 virtualenv_cmd = f"{self.python_cmd} -m venv" 1524 self.check_python_module("ensurepip") 1525 1526 # Check for needed programs/tools 1527 self.check_perl_module("Pod::Usage", DepManager.SYSTEM_MANDATORY) 1528 1529 self.check_program("make", DepManager.SYSTEM_MANDATORY) 1530 self.check_program("which", DepManager.SYSTEM_MANDATORY) 1531 1532 self.check_program("dot", DepManager.SYSTEM_OPTIONAL) 1533 self.check_program("convert", DepManager.SYSTEM_OPTIONAL) 1534 1535 self.check_python_module("yaml") 1536 1537 if self.pdf: 1538 self.check_program("xelatex", DepManager.PDF_MANDATORY) 1539 self.check_program("rsvg-convert", DepManager.PDF_MANDATORY) 1540 self.check_program("latexmk", DepManager.PDF_MANDATORY) 1541 1542 # Do distro-specific checks and output distro-install commands 1543 cmd = self.get_install() 1544 if cmd: 1545 print(cmd) 1546 1547 # If distro requires some special instructions, print here. 1548 # Please notice that get_install() needs to be called first. 1549 if self.distro_msg: 1550 print("\n" + self.distro_msg) 1551 1552 if not self.python_cmd: 1553 if self.need == 1: 1554 sys.exit("Can't build as 1 mandatory dependency is missing") 1555 elif self.need: 1556 sys.exit(f"Can't build as {self.need} mandatory dependencies are missing") 1557 1558 # Check if sphinx-build is called sphinx-build-3 1559 if self.need_symlink: 1560 sphinx_path = self.which("sphinx-build-3") 1561 if sphinx_path: 1562 print(f"\tsudo ln -sf {sphinx_path} /usr/bin/sphinx-build\n") 1563 1564 self.recommend_sphinx_version(virtualenv_cmd) 1565 print("") 1566 1567 if not self.deps.optional: 1568 print("All optional dependencies are met.") 1569 1570 if self.deps.need == 1: 1571 sys.exit("Can't build as 1 mandatory dependency is missing") 1572 elif self.deps.need: 1573 sys.exit(f"Can't build as {self.deps.need} mandatory dependencies are missing") 1574 1575 print("Needed package dependencies are met.") 1576 1577DESCRIPTION = """ 1578Process some flags related to Sphinx installation and documentation build. 1579""" 1580 1581 1582def main(): 1583 """Main function""" 1584 parser = argparse.ArgumentParser(description=DESCRIPTION) 1585 1586 parser.add_argument( 1587 "--no-virtualenv", 1588 action="store_false", 1589 dest="virtualenv", 1590 help="Recommend installing Sphinx instead of using a virtualenv", 1591 ) 1592 1593 parser.add_argument( 1594 "--no-pdf", 1595 action="store_false", 1596 dest="pdf", 1597 help="Don't check for dependencies required to build PDF docs", 1598 ) 1599 1600 parser.add_argument( 1601 "--version-check", 1602 action="store_true", 1603 dest="version_check", 1604 help="If version is compatible, don't check for missing dependencies", 1605 ) 1606 1607 args = parser.parse_args() 1608 1609 checker = SphinxDependencyChecker(args) 1610 1611 checker.check_python() 1612 checker.check_needs() 1613 1614# Call main if not used as module 1615if __name__ == "__main__": 1616 main() 1617