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