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