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