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