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