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