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