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