1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3# Copyright (C) 2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org> 4# 5# pylint: disable=R0902, R0912, R0913, R0914, R0915, R0917, C0103 6# 7# Converted from docs Makefile and parallel-wrapper.sh, both under 8# GPLv2, copyrighted since 2008 by the following authors: 9# 10# Akira Yokosawa <akiyks@gmail.com> 11# Arnd Bergmann <arnd@arndb.de> 12# Breno Leitao <leitao@debian.org> 13# Carlos Bilbao <carlos.bilbao@amd.com> 14# Dave Young <dyoung@redhat.com> 15# Donald Hunter <donald.hunter@gmail.com> 16# Geert Uytterhoeven <geert+renesas@glider.be> 17# Jani Nikula <jani.nikula@intel.com> 18# Jan Stancek <jstancek@redhat.com> 19# Jonathan Corbet <corbet@lwn.net> 20# Joshua Clayton <stillcompiling@gmail.com> 21# Kees Cook <keescook@chromium.org> 22# Linus Torvalds <torvalds@linux-foundation.org> 23# Magnus Damm <damm+renesas@opensource.se> 24# Masahiro Yamada <masahiroy@kernel.org> 25# Mauro Carvalho Chehab <mchehab+huawei@kernel.org> 26# Maxim Cournoyer <maxim.cournoyer@gmail.com> 27# Peter Foley <pefoley2@pefoley.com> 28# Randy Dunlap <rdunlap@infradead.org> 29# Rob Herring <robh@kernel.org> 30# Shuah Khan <shuahkh@osg.samsung.com> 31# Thorsten Blum <thorsten.blum@toblux.com> 32# Tomas Winkler <tomas.winkler@intel.com> 33 34 35""" 36Sphinx build wrapper that handles Kernel-specific business rules: 37 38- it gets the Kernel build environment vars; 39- it determines what's the best parallelism; 40- it handles SPHINXDIRS 41 42This tool ensures that MIN_PYTHON_VERSION is satisfied. If version is 43below that, it seeks for a new Python version. If found, it re-runs using 44the newer version. 45""" 46 47import argparse 48import locale 49import os 50import re 51import shlex 52import shutil 53import subprocess 54import sys 55 56from concurrent import futures 57from glob import glob 58 59from lib.python_version import PythonVersion 60from lib.latex_fonts import LatexFontChecker 61 62LIB_DIR = "../../scripts/lib" 63SRC_DIR = os.path.dirname(os.path.realpath(__file__)) 64 65sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR)) 66 67from jobserver import JobserverExec # pylint: disable=C0413,C0411,E0401 68 69# 70# Some constants 71# 72VENV_DEFAULT = "sphinx_latest" 73MIN_PYTHON_VERSION = PythonVersion("3.7").version 74PAPER = ["", "a4", "letter"] 75 76TARGETS = { 77 "cleandocs": { "builder": "clean" }, 78 "linkcheckdocs": { "builder": "linkcheck" }, 79 "htmldocs": { "builder": "html" }, 80 "epubdocs": { "builder": "epub", "out_dir": "epub" }, 81 "texinfodocs": { "builder": "texinfo", "out_dir": "texinfo" }, 82 "infodocs": { "builder": "texinfo", "out_dir": "texinfo" }, 83 "mandocs": { "builder": "man", "out_dir": "man" }, 84 "latexdocs": { "builder": "latex", "out_dir": "latex" }, 85 "pdfdocs": { "builder": "latex", "out_dir": "latex" }, 86 "xmldocs": { "builder": "xml", "out_dir": "xml" }, 87} 88 89 90# 91# SphinxBuilder class 92# 93 94class SphinxBuilder: 95 """ 96 Handles a sphinx-build target, adding needed arguments to build 97 with the Kernel. 98 """ 99 100 def get_path(self, path, use_cwd=False, abs_path=False): 101 """ 102 Ancillary routine to handle patches the right way, as shell does. 103 104 It first expands "~" and "~user". Then, if patch is not absolute, 105 join self.srctree. Finally, if requested, convert to abspath. 106 """ 107 108 path = os.path.expanduser(path) 109 if not path.startswith("/"): 110 if use_cwd: 111 base = os.getcwd() 112 else: 113 base = self.srctree 114 115 path = os.path.join(base, path) 116 117 if abs_path: 118 return os.path.abspath(path) 119 120 return path 121 122 def get_sphinx_extra_opts(self, n_jobs): 123 """ 124 Get the number of jobs to be used for docs build passed via command 125 line and desired sphinx verbosity. 126 127 The number of jobs can be on different places: 128 129 1) It can be passed via "-j" argument; 130 2) The SPHINXOPTS="-j8" env var may have "-j"; 131 3) if called via GNU make, -j specifies the desired number of jobs. 132 with GNU makefile, this number is available via POSIX jobserver; 133 4) if none of the above is available, it should default to "-jauto", 134 and let sphinx decide the best value. 135 """ 136 137 # 138 # SPHINXOPTS env var, if used, contains extra arguments to be used 139 # by sphinx-build time. Among them, it may contain sphinx verbosity 140 # and desired number of parallel jobs. 141 # 142 parser = argparse.ArgumentParser() 143 parser.add_argument('-j', '--jobs', type=int) 144 parser.add_argument('-q', '--quiet', type=int) 145 146 # 147 # Other sphinx-build arguments go as-is, so place them 148 # at self.sphinxopts, using shell parser 149 # 150 sphinxopts = shlex.split(os.environ.get("SPHINXOPTS", "")) 151 152 # 153 # Build a list of sphinx args, honoring verbosity here if specified 154 # 155 156 verbose = self.verbose 157 sphinx_args, self.sphinxopts = parser.parse_known_args(sphinxopts) 158 if sphinx_args.quiet is True: 159 verbose = False 160 161 # 162 # If the user explicitly sets "-j" at command line, use it. 163 # Otherwise, pick it from SPHINXOPTS args 164 # 165 if n_jobs: 166 self.n_jobs = n_jobs 167 elif sphinx_args.jobs: 168 self.n_jobs = sphinx_args.jobs 169 else: 170 self.n_jobs = None 171 172 if not verbose: 173 self.sphinxopts += ["-q"] 174 175 def __init__(self, builddir, venv=None, verbose=False, n_jobs=None, 176 interactive=None): 177 """Initialize internal variables""" 178 self.venv = venv 179 self.verbose = None 180 181 # 182 # Normal variables passed from Kernel's makefile 183 # 184 self.kernelversion = os.environ.get("KERNELVERSION", "unknown") 185 self.kernelrelease = os.environ.get("KERNELRELEASE", "unknown") 186 self.pdflatex = os.environ.get("PDFLATEX", "xelatex") 187 188 # 189 # Kernel main Makefile defines a PYTHON3 variable whose default is 190 # "python3". When set to a different value, it allows running a 191 # diferent version than the default official python3 package. 192 # Several distros package python3xx-sphinx packages with newer 193 # versions of Python and sphinx-build. 194 # 195 # Honor such variable different than default 196 # 197 self.python = os.environ.get("PYTHON3") 198 if self.python == "python3": 199 self.python = None 200 201 if not interactive: 202 self.latexopts = os.environ.get("LATEXOPTS", "-interaction=batchmode -no-shell-escape") 203 else: 204 self.latexopts = os.environ.get("LATEXOPTS", "") 205 206 if not verbose: 207 verbose = bool(os.environ.get("KBUILD_VERBOSE", "") != "") 208 209 if verbose is not None: 210 self.verbose = verbose 211 212 # 213 # Source tree directory. This needs to be at os.environ, as 214 # Sphinx extensions use it 215 # 216 self.srctree = os.environ.get("srctree") 217 if not self.srctree: 218 self.srctree = "." 219 os.environ["srctree"] = self.srctree 220 221 # 222 # Now that we can expand srctree, get other directories as well 223 # 224 self.sphinxbuild = os.environ.get("SPHINXBUILD", "sphinx-build") 225 self.kerneldoc = self.get_path(os.environ.get("KERNELDOC", 226 "scripts/kernel-doc.py")) 227 self.builddir = self.get_path(builddir, use_cwd=True, abs_path=True) 228 229 # 230 # Get directory locations for LaTeX build toolchain 231 # 232 self.pdflatex_cmd = shutil.which(self.pdflatex) 233 self.latexmk_cmd = shutil.which("latexmk") 234 235 self.env = os.environ.copy() 236 237 self.get_sphinx_extra_opts(n_jobs) 238 239 # 240 # If venv command line argument is specified, run Sphinx from venv 241 # 242 if venv: 243 bin_dir = os.path.join(venv, "bin") 244 if not os.path.isfile(os.path.join(bin_dir, "activate")): 245 sys.exit(f"Venv {venv} not found.") 246 247 # "activate" virtual env 248 self.env["PATH"] = bin_dir + ":" + self.env["PATH"] 249 self.env["VIRTUAL_ENV"] = venv 250 if "PYTHONHOME" in self.env: 251 del self.env["PYTHONHOME"] 252 print(f"Setting venv to {venv}") 253 254 def run_sphinx(self, sphinx_build, build_args, *args, **pwargs): 255 """ 256 Executes sphinx-build using current python3 command. 257 258 When calling via GNU make, POSIX jobserver is used to tell how 259 many jobs are still available from a job pool. claim all remaining 260 jobs, as we don't want sphinx-build to run in parallel with other 261 jobs. 262 263 Despite that, the user may actually force a different value than 264 the number of available jobs via command line. 265 266 The "with" logic here is used to ensure that the claimed jobs will 267 be freed once subprocess finishes 268 """ 269 270 with JobserverExec() as jobserver: 271 if jobserver.claim: 272 # 273 # when GNU make is used, claim available jobs from jobserver 274 # 275 n_jobs = str(jobserver.claim) 276 else: 277 # 278 # Otherwise, let sphinx decide by default 279 # 280 n_jobs = "auto" 281 282 # 283 # If explicitly requested via command line, override default 284 # 285 if self.n_jobs: 286 n_jobs = str(self.n_jobs) 287 288 # 289 # We can't simply call python3 sphinx-build, as OpenSUSE 290 # Tumbleweed uses an ELF binary file (/usr/bin/alts) to switch 291 # between different versions of sphinx-build. So, only call it 292 # prepending "python3.xx" when PYTHON3 variable is not default. 293 # 294 if self.python: 295 cmd = [self.python] 296 else: 297 cmd = [] 298 299 cmd += [sphinx_build] 300 cmd += [f"-j{n_jobs}"] 301 cmd += self.sphinxopts 302 cmd += build_args 303 304 if self.verbose: 305 print(" ".join(cmd)) 306 307 return subprocess.call(cmd, *args, **pwargs) 308 309 def handle_html(self, css, output_dir, rustdoc): 310 """ 311 Extra steps for HTML and epub output. 312 313 For such targets, we need to ensure that CSS will be properly 314 copied to the output _static directory 315 """ 316 317 if css: 318 css = os.path.expanduser(css) 319 if not css.startswith("/"): 320 css = os.path.join(self.srctree, css) 321 322 static_dir = os.path.join(output_dir, "_static") 323 os.makedirs(static_dir, exist_ok=True) 324 325 try: 326 shutil.copy2(css, static_dir) 327 except (OSError, IOError) as e: 328 print(f"Warning: Failed to copy CSS: {e}", file=sys.stderr) 329 330 if rustdoc: 331 if "MAKE" in self.env: 332 cmd = [self.env["MAKE"]] 333 else: 334 cmd = ["make", "LLVM=1"] 335 336 cmd += [ "rustdoc"] 337 if self.verbose: 338 print(" ".join(cmd)) 339 340 try: 341 subprocess.run(cmd, check=True) 342 except subprocess.CalledProcessError as e: 343 print(f"Ignored errors when building rustdoc: {e}. Is RUST enabled?", 344 file=sys.stderr) 345 346 def build_pdf_file(self, latex_cmd, from_dir, path): 347 """Builds a single pdf file using latex_cmd""" 348 try: 349 subprocess.run(latex_cmd + [path], 350 cwd=from_dir, check=True, env=self.env) 351 352 return True 353 except subprocess.CalledProcessError: 354 return False 355 356 def pdf_parallel_build(self, tex_suffix, latex_cmd, tex_files, n_jobs): 357 """Build PDF files in parallel if possible""" 358 builds = {} 359 build_failed = False 360 max_len = 0 361 has_tex = False 362 363 # 364 # LaTeX PDF error code is almost useless for us: 365 # any warning makes it non-zero. For kernel doc builds it always return 366 # non-zero even when build succeeds. So, let's do the best next thing: 367 # Ignore build errors. At the end, check if all PDF files were built, 368 # printing a summary with the built ones and returning 0 if all of 369 # them were actually built. 370 # 371 with futures.ThreadPoolExecutor(max_workers=n_jobs) as executor: 372 jobs = {} 373 374 for from_dir, pdf_dir, entry in tex_files: 375 name = entry.name 376 377 if not name.endswith(tex_suffix): 378 continue 379 380 name = name[:-len(tex_suffix)] 381 has_tex = True 382 383 future = executor.submit(self.build_pdf_file, latex_cmd, 384 from_dir, entry.path) 385 jobs[future] = (from_dir, pdf_dir, name) 386 387 for future in futures.as_completed(jobs): 388 from_dir, pdf_dir, name = jobs[future] 389 390 pdf_name = name + ".pdf" 391 pdf_from = os.path.join(from_dir, pdf_name) 392 pdf_to = os.path.join(pdf_dir, pdf_name) 393 out_name = os.path.relpath(pdf_to, self.builddir) 394 max_len = max(max_len, len(out_name)) 395 396 try: 397 success = future.result() 398 399 if success and os.path.exists(pdf_from): 400 os.rename(pdf_from, pdf_to) 401 402 # 403 # if verbose, get the name of built PDF file 404 # 405 if self.verbose: 406 builds[out_name] = "SUCCESS" 407 else: 408 builds[out_name] = "FAILED" 409 build_failed = True 410 except futures.Error as e: 411 builds[out_name] = f"FAILED ({repr(e)})" 412 build_failed = True 413 414 # 415 # Handle case where no .tex files were found 416 # 417 if not has_tex: 418 out_name = "LaTeX files" 419 max_len = max(max_len, len(out_name)) 420 builds[out_name] = "FAILED: no .tex files were generated" 421 build_failed = True 422 423 return builds, build_failed, max_len 424 425 def handle_pdf(self, output_dirs, deny_vf): 426 """ 427 Extra steps for PDF output. 428 429 As PDF is handled via a LaTeX output, after building the .tex file, 430 a new build is needed to create the PDF output from the latex 431 directory. 432 """ 433 builds = {} 434 max_len = 0 435 tex_suffix = ".tex" 436 tex_files = [] 437 438 # 439 # Since early 2024, Fedora and openSUSE tumbleweed have started 440 # deploying variable-font format of "Noto CJK", causing LaTeX 441 # to break with CJK. Work around it, by denying the variable font 442 # usage during xelatex build by passing the location of a config 443 # file with a deny list. 444 # 445 # See tools/docs/lib/latex_fonts.py for more details. 446 # 447 if deny_vf: 448 deny_vf = os.path.expanduser(deny_vf) 449 if os.path.isdir(deny_vf): 450 self.env["XDG_CONFIG_HOME"] = deny_vf 451 452 for from_dir in output_dirs: 453 pdf_dir = os.path.join(from_dir, "../pdf") 454 os.makedirs(pdf_dir, exist_ok=True) 455 456 if self.latexmk_cmd: 457 latex_cmd = [self.latexmk_cmd, f"-{self.pdflatex}"] 458 else: 459 latex_cmd = [self.pdflatex] 460 461 latex_cmd.extend(shlex.split(self.latexopts)) 462 463 # Get a list of tex files to process 464 with os.scandir(from_dir) as it: 465 for entry in it: 466 if entry.name.endswith(tex_suffix): 467 tex_files.append((from_dir, pdf_dir, entry)) 468 469 # 470 # When using make, this won't be used, as the number of jobs comes 471 # from POSIX jobserver. So, this covers the case where build comes 472 # from command line. On such case, serialize by default, except if 473 # the user explicitly sets the number of jobs. 474 # 475 n_jobs = 1 476 477 # n_jobs is either an integer or "auto". Only use it if it is a number 478 if self.n_jobs: 479 try: 480 n_jobs = int(self.n_jobs) 481 except ValueError: 482 pass 483 484 # 485 # When using make, jobserver.claim is the number of jobs that were 486 # used with "-j" and that aren't used by other make targets 487 # 488 with JobserverExec() as jobserver: 489 n_jobs = 1 490 491 # 492 # Handle the case when a parameter is passed via command line, 493 # using it as default, if jobserver doesn't claim anything 494 # 495 if self.n_jobs: 496 try: 497 n_jobs = int(self.n_jobs) 498 except ValueError: 499 pass 500 501 if jobserver.claim: 502 n_jobs = jobserver.claim 503 504 builds, build_failed, max_len = self.pdf_parallel_build(tex_suffix, 505 latex_cmd, 506 tex_files, 507 n_jobs) 508 509 # 510 # In verbose mode, print a summary with the build results per file. 511 # Otherwise, print a single line with all failures, if any. 512 # On both cases, return code 1 indicates build failures, 513 # 514 if self.verbose: 515 msg = "Summary" 516 msg += "\n" + "=" * len(msg) 517 print() 518 print(msg) 519 520 for pdf_name, pdf_file in builds.items(): 521 print(f"{pdf_name:<{max_len}}: {pdf_file}") 522 523 print() 524 if build_failed: 525 msg = LatexFontChecker().check() 526 if msg: 527 print(msg) 528 529 sys.exit("Error: not all PDF files were created.") 530 531 elif build_failed: 532 n_failures = len(builds) 533 failures = ", ".join(builds.keys()) 534 535 msg = LatexFontChecker().check() 536 if msg: 537 print(msg) 538 539 sys.exit(f"Error: Can't build {n_failures} PDF file(s): {failures}") 540 541 def handle_info(self, output_dirs): 542 """ 543 Extra steps for Info output. 544 545 For texinfo generation, an additional make is needed from the 546 texinfo directory. 547 """ 548 549 for output_dir in output_dirs: 550 try: 551 subprocess.run(["make", "info"], cwd=output_dir, check=True) 552 except subprocess.CalledProcessError as e: 553 sys.exit(f"Error generating info docs: {e}") 554 555 def handle_man(self, kerneldoc, docs_dir, src_dir, output_dir): 556 """ 557 Create man pages from kernel-doc output 558 """ 559 560 re_kernel_doc = re.compile(r"^\.\.\s+kernel-doc::\s*(\S+)") 561 re_man = re.compile(r'^\.TH "[^"]*" (\d+) "([^"]*)"') 562 563 if docs_dir == src_dir: 564 # 565 # Pick the entire set of kernel-doc markups from the entire tree 566 # 567 kdoc_files = set([self.srctree]) 568 else: 569 kdoc_files = set() 570 571 for fname in glob(os.path.join(src_dir, "**"), recursive=True): 572 if os.path.isfile(fname) and fname.endswith(".rst"): 573 with open(fname, "r", encoding="utf-8") as in_fp: 574 data = in_fp.read() 575 576 for line in data.split("\n"): 577 match = re_kernel_doc.match(line) 578 if match: 579 if os.path.isfile(match.group(1)): 580 kdoc_files.add(match.group(1)) 581 582 if not kdoc_files: 583 sys.exit(f"Directory {src_dir} doesn't contain kernel-doc tags") 584 585 cmd = [ kerneldoc, "-m" ] + sorted(kdoc_files) 586 try: 587 if self.verbose: 588 print(" ".join(cmd)) 589 590 result = subprocess.run(cmd, stdout=subprocess.PIPE, text= True) 591 592 if result.returncode: 593 print(f"Warning: kernel-doc returned {result.returncode} warnings") 594 595 except (OSError, ValueError, subprocess.SubprocessError) as e: 596 sys.exit(f"Failed to create man pages for {src_dir}: {repr(e)}") 597 598 fp = None 599 try: 600 for line in result.stdout.split("\n"): 601 match = re_man.match(line) 602 if not match: 603 if fp: 604 fp.write(line + '\n') 605 continue 606 607 if fp: 608 fp.close() 609 610 fname = f"{output_dir}/{match.group(2)}.{match.group(1)}" 611 612 if self.verbose: 613 print(f"Creating {fname}") 614 fp = open(fname, "w", encoding="utf-8") 615 fp.write(line + '\n') 616 finally: 617 if fp: 618 fp.close() 619 620 def cleandocs(self, builder): # pylint: disable=W0613 621 """Remove documentation output directory""" 622 shutil.rmtree(self.builddir, ignore_errors=True) 623 624 def build(self, target, sphinxdirs=None, 625 theme=None, css=None, paper=None, deny_vf=None, rustdoc=False): 626 """ 627 Build documentation using Sphinx. This is the core function of this 628 module. It prepares all arguments required by sphinx-build. 629 """ 630 631 builder = TARGETS[target]["builder"] 632 out_dir = TARGETS[target].get("out_dir", "") 633 634 # 635 # Cleandocs doesn't require sphinx-build 636 # 637 if target == "cleandocs": 638 self.cleandocs(builder) 639 return 640 641 if theme: 642 os.environ["DOCS_THEME"] = theme 643 644 # 645 # Other targets require sphinx-build, so check if it exists 646 # 647 sphinxbuild = shutil.which(self.sphinxbuild, path=self.env["PATH"]) 648 if not sphinxbuild and target != "mandocs": 649 sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n") 650 651 if builder == "latex": 652 if not self.pdflatex_cmd and not self.latexmk_cmd: 653 sys.exit("Error: pdflatex or latexmk required for PDF generation") 654 655 docs_dir = os.path.abspath(os.path.join(self.srctree, "Documentation")) 656 657 # 658 # Fill in base arguments for Sphinx build 659 # 660 kerneldoc = self.kerneldoc 661 if kerneldoc.startswith(self.srctree): 662 kerneldoc = os.path.relpath(kerneldoc, self.srctree) 663 664 args = [ "-b", builder, "-c", docs_dir ] 665 666 if builder == "latex": 667 if not paper: 668 paper = PAPER[1] 669 670 args.extend(["-D", f"latex_elements.papersize={paper}paper"]) 671 672 if rustdoc: 673 args.extend(["-t", "rustdoc"]) 674 675 if not sphinxdirs: 676 sphinxdirs = os.environ.get("SPHINXDIRS", ".") 677 678 # 679 # The sphinx-build tool has a bug: internally, it tries to set 680 # locale with locale.setlocale(locale.LC_ALL, ''). This causes a 681 # crash if language is not set. Detect and fix it. 682 # 683 try: 684 locale.setlocale(locale.LC_ALL, '') 685 except locale.Error: 686 self.env["LC_ALL"] = "C" 687 688 # 689 # sphinxdirs can be a list or a whitespace-separated string 690 # 691 sphinxdirs_list = [] 692 for sphinxdir in sphinxdirs: 693 if isinstance(sphinxdir, list): 694 sphinxdirs_list += sphinxdir 695 else: 696 sphinxdirs_list += sphinxdir.split() 697 698 # 699 # Step 1: Build each directory in separate. 700 # 701 # This is not the best way of handling it, as cross-references between 702 # them will be broken, but this is what we've been doing since 703 # the beginning. 704 # 705 output_dirs = [] 706 for sphinxdir in sphinxdirs_list: 707 src_dir = os.path.join(docs_dir, sphinxdir) 708 doctree_dir = os.path.join(self.builddir, ".doctrees") 709 output_dir = os.path.join(self.builddir, sphinxdir, out_dir) 710 711 # 712 # Make directory names canonical 713 # 714 src_dir = os.path.normpath(src_dir) 715 doctree_dir = os.path.normpath(doctree_dir) 716 output_dir = os.path.normpath(output_dir) 717 718 os.makedirs(doctree_dir, exist_ok=True) 719 os.makedirs(output_dir, exist_ok=True) 720 721 output_dirs.append(output_dir) 722 723 build_args = args + [ 724 "-d", doctree_dir, 725 "-D", f"kerneldoc_bin={kerneldoc}", 726 "-D", f"version={self.kernelversion}", 727 "-D", f"release={self.kernelrelease}", 728 "-D", f"kerneldoc_srctree={self.srctree}", 729 src_dir, 730 output_dir, 731 ] 732 733 if target == "mandocs": 734 self.handle_man(kerneldoc, docs_dir, src_dir, output_dir) 735 else: 736 try: 737 result = self.run_sphinx(sphinxbuild, build_args, 738 env=self.env) 739 740 if result: 741 sys.exit(f"Build failed: return code: {result}") 742 743 except (OSError, ValueError, subprocess.SubprocessError) as e: 744 sys.exit(f"Build failed: {repr(e)}") 745 746 # 747 # Ensure that each html/epub output will have needed static files 748 # 749 if target in ["htmldocs", "epubdocs"]: 750 self.handle_html(css, output_dir, rustdoc) 751 752 # 753 # Step 2: Some targets (PDF and info) require an extra step once 754 # sphinx-build finishes 755 # 756 if target == "pdfdocs": 757 self.handle_pdf(output_dirs, deny_vf) 758 elif target == "infodocs": 759 self.handle_info(output_dirs) 760 761def jobs_type(value): 762 """ 763 Handle valid values for -j. Accepts Sphinx "-jauto", plus a number 764 equal or bigger than one. 765 """ 766 if value is None: 767 return None 768 769 if value.lower() == 'auto': 770 return value.lower() 771 772 try: 773 if int(value) >= 1: 774 return value 775 776 raise argparse.ArgumentTypeError(f"Minimum jobs is 1, got {value}") 777 except ValueError: 778 raise argparse.ArgumentTypeError(f"Must be 'auto' or positive integer, got {value}") # pylint: disable=W0707 779 780def main(): 781 """ 782 Main function. The only mandatory argument is the target. If not 783 specified, the other arguments will use default values if not 784 specified at os.environ. 785 """ 786 parser = argparse.ArgumentParser(description="Kernel documentation builder") 787 788 parser.add_argument("target", choices=list(TARGETS.keys()), 789 help="Documentation target to build") 790 parser.add_argument("--sphinxdirs", nargs="+", 791 help="Specific directories to build") 792 parser.add_argument("--builddir", default="output", 793 help="Sphinx configuration file") 794 795 parser.add_argument("--theme", help="Sphinx theme to use") 796 797 parser.add_argument("--css", help="Custom CSS file for HTML/EPUB") 798 799 parser.add_argument("--paper", choices=PAPER, default=PAPER[0], 800 help="Paper size for LaTeX/PDF output") 801 802 parser.add_argument('--deny-vf', 803 help="Configuration to deny variable fonts on pdf builds") 804 805 parser.add_argument('--rustdoc', action="store_true", 806 help="Enable rustdoc build. Requires CONFIG_RUST") 807 808 parser.add_argument("-v", "--verbose", action='store_true', 809 help="place build in verbose mode") 810 811 parser.add_argument('-j', '--jobs', type=jobs_type, 812 help="Sets number of jobs to use with sphinx-build") 813 814 parser.add_argument('-i', '--interactive', action='store_true', 815 help="Change latex default to run in interactive mode") 816 817 parser.add_argument("-V", "--venv", nargs='?', const=f'{VENV_DEFAULT}', 818 default=None, 819 help=f'If used, run Sphinx from a venv dir (default dir: {VENV_DEFAULT})') 820 821 args = parser.parse_args() 822 823 PythonVersion.check_python(MIN_PYTHON_VERSION, show_alternatives=True, 824 bail_out=True) 825 826 builder = SphinxBuilder(builddir=args.builddir, venv=args.venv, 827 verbose=args.verbose, n_jobs=args.jobs, 828 interactive=args.interactive) 829 830 builder.build(args.target, sphinxdirs=args.sphinxdirs, 831 theme=args.theme, css=args.css, paper=args.paper, 832 rustdoc=args.rustdoc, deny_vf=args.deny_vf) 833 834if __name__ == "__main__": 835 main() 836