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 59 60LIB_DIR = "../lib/python" 61SRC_DIR = os.path.dirname(os.path.realpath(__file__)) 62 63sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR)) 64 65from kdoc.python_version import PythonVersion 66from kdoc.latex_fonts import LatexFontChecker 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', action='store_true') 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 += build_args 302 cmd += self.sphinxopts 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 skip_sphinx=False): 627 """ 628 Build documentation using Sphinx. This is the core function of this 629 module. It prepares all arguments required by sphinx-build. 630 """ 631 632 builder = TARGETS[target]["builder"] 633 out_dir = TARGETS[target].get("out_dir", "") 634 635 # 636 # Cleandocs doesn't require sphinx-build 637 # 638 if target == "cleandocs": 639 self.cleandocs(builder) 640 return 641 642 if theme: 643 os.environ["DOCS_THEME"] = theme 644 645 # 646 # Other targets require sphinx-build, so check if it exists 647 # 648 if not skip_sphinx: 649 sphinxbuild = shutil.which(self.sphinxbuild, path=self.env["PATH"]) 650 if not sphinxbuild and target != "mandocs": 651 sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n") 652 653 if target == "pdfdocs": 654 if not self.pdflatex_cmd and not self.latexmk_cmd: 655 sys.exit("Error: pdflatex or latexmk required for PDF generation") 656 657 docs_dir = os.path.abspath(os.path.join(self.srctree, "Documentation")) 658 659 # 660 # Fill in base arguments for Sphinx build 661 # 662 kerneldoc = self.kerneldoc 663 if kerneldoc.startswith(self.srctree): 664 kerneldoc = os.path.relpath(kerneldoc, self.srctree) 665 666 args = [ "-b", builder, "-c", docs_dir ] 667 668 if builder == "latex": 669 if not paper: 670 paper = PAPER[1] 671 672 args.extend(["-D", f"latex_elements.papersize={paper}paper"]) 673 674 if rustdoc: 675 args.extend(["-t", "rustdoc"]) 676 677 if not sphinxdirs: 678 sphinxdirs = os.environ.get("SPHINXDIRS", ".") 679 680 # 681 # The sphinx-build tool has a bug: internally, it tries to set 682 # locale with locale.setlocale(locale.LC_ALL, ''). This causes a 683 # crash if language is not set. Detect and fix it. 684 # 685 try: 686 locale.setlocale(locale.LC_ALL, '') 687 except locale.Error: 688 self.env["LC_ALL"] = "C" 689 690 # 691 # sphinxdirs can be a list or a whitespace-separated string 692 # 693 sphinxdirs_list = [] 694 for sphinxdir in sphinxdirs: 695 if isinstance(sphinxdir, list): 696 sphinxdirs_list += sphinxdir 697 else: 698 sphinxdirs_list += sphinxdir.split() 699 700 # 701 # Step 1: Build each directory in separate. 702 # 703 # This is not the best way of handling it, as cross-references between 704 # them will be broken, but this is what we've been doing since 705 # the beginning. 706 # 707 output_dirs = [] 708 for sphinxdir in sphinxdirs_list: 709 src_dir = os.path.join(docs_dir, sphinxdir) 710 doctree_dir = os.path.join(self.builddir, ".doctrees") 711 output_dir = os.path.join(self.builddir, sphinxdir, out_dir) 712 713 # 714 # Make directory names canonical 715 # 716 src_dir = os.path.normpath(src_dir) 717 doctree_dir = os.path.normpath(doctree_dir) 718 output_dir = os.path.normpath(output_dir) 719 720 os.makedirs(doctree_dir, exist_ok=True) 721 os.makedirs(output_dir, exist_ok=True) 722 723 output_dirs.append(output_dir) 724 725 build_args = args + [ 726 "-d", doctree_dir, 727 "-D", f"kerneldoc_bin={kerneldoc}", 728 "-D", f"version={self.kernelversion}", 729 "-D", f"release={self.kernelrelease}", 730 "-D", f"kerneldoc_srctree={self.srctree}", 731 src_dir, 732 output_dir, 733 ] 734 735 if target == "mandocs": 736 self.handle_man(kerneldoc, docs_dir, src_dir, output_dir) 737 elif not skip_sphinx: 738 try: 739 result = self.run_sphinx(sphinxbuild, build_args, 740 env=self.env) 741 742 if result: 743 sys.exit(f"Build failed: return code: {result}") 744 745 except (OSError, ValueError, subprocess.SubprocessError) as e: 746 sys.exit(f"Build failed: {repr(e)}") 747 748 # 749 # Ensure that each html/epub output will have needed static files 750 # 751 if target in ["htmldocs", "epubdocs"]: 752 self.handle_html(css, output_dir, rustdoc) 753 754 # 755 # Step 2: Some targets (PDF and info) require an extra step once 756 # sphinx-build finishes 757 # 758 if target == "pdfdocs": 759 self.handle_pdf(output_dirs, deny_vf) 760 elif target == "infodocs": 761 self.handle_info(output_dirs) 762 763def jobs_type(value): 764 """ 765 Handle valid values for -j. Accepts Sphinx "-jauto", plus a number 766 equal or bigger than one. 767 """ 768 if value is None: 769 return None 770 771 if value.lower() == 'auto': 772 return value.lower() 773 774 try: 775 if int(value) >= 1: 776 return value 777 778 raise argparse.ArgumentTypeError(f"Minimum jobs is 1, got {value}") 779 except ValueError: 780 raise argparse.ArgumentTypeError(f"Must be 'auto' or positive integer, got {value}") # pylint: disable=W0707 781 782def main(): 783 """ 784 Main function. The only mandatory argument is the target. If not 785 specified, the other arguments will use default values if not 786 specified at os.environ. 787 """ 788 parser = argparse.ArgumentParser(description="Kernel documentation builder") 789 790 parser.add_argument("target", choices=list(TARGETS.keys()), 791 help="Documentation target to build") 792 parser.add_argument("--sphinxdirs", nargs="+", 793 help="Specific directories to build") 794 parser.add_argument("--builddir", default="output", 795 help="Sphinx configuration file") 796 797 parser.add_argument("--theme", help="Sphinx theme to use") 798 799 parser.add_argument("--css", help="Custom CSS file for HTML/EPUB") 800 801 parser.add_argument("--paper", choices=PAPER, default=PAPER[0], 802 help="Paper size for LaTeX/PDF output") 803 804 parser.add_argument('--deny-vf', 805 help="Configuration to deny variable fonts on pdf builds") 806 807 parser.add_argument('--rustdoc', action="store_true", 808 help="Enable rustdoc build. Requires CONFIG_RUST") 809 810 parser.add_argument("-v", "--verbose", action='store_true', 811 help="place build in verbose mode") 812 813 parser.add_argument('-j', '--jobs', type=jobs_type, 814 help="Sets number of jobs to use with sphinx-build") 815 816 parser.add_argument('-i', '--interactive', action='store_true', 817 help="Change latex default to run in interactive mode") 818 819 parser.add_argument('-s', '--skip-sphinx-build', action='store_true', 820 help="Skip sphinx-build step") 821 822 parser.add_argument("-V", "--venv", nargs='?', const=f'{VENV_DEFAULT}', 823 default=None, 824 help=f'If used, run Sphinx from a venv dir (default dir: {VENV_DEFAULT})') 825 826 args = parser.parse_args() 827 828 PythonVersion.check_python(MIN_PYTHON_VERSION, show_alternatives=True, 829 bail_out=True) 830 831 builder = SphinxBuilder(builddir=args.builddir, venv=args.venv, 832 verbose=args.verbose, n_jobs=args.jobs, 833 interactive=args.interactive) 834 835 builder.build(args.target, sphinxdirs=args.sphinxdirs, 836 theme=args.theme, css=args.css, paper=args.paper, 837 rustdoc=args.rustdoc, deny_vf=args.deny_vf, 838 skip_sphinx=args.skip_sphinx_build) 839 840if __name__ == "__main__": 841 main() 842