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