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