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