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