1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3# Copyright (C) 2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org> 4# 5# pylint: disable=R0902, R0912, R0913, R0914, R0915, R0917, C0103 6# 7# Converted from docs Makefile and parallel-wrapper.sh, both under 8# GPLv2, copyrighted since 2008 by the following authors: 9# 10# Akira Yokosawa <akiyks@gmail.com> 11# Arnd Bergmann <arnd@arndb.de> 12# Breno Leitao <leitao@debian.org> 13# Carlos Bilbao <carlos.bilbao@amd.com> 14# Dave Young <dyoung@redhat.com> 15# Donald Hunter <donald.hunter@gmail.com> 16# Geert Uytterhoeven <geert+renesas@glider.be> 17# Jani Nikula <jani.nikula@intel.com> 18# Jan Stancek <jstancek@redhat.com> 19# Jonathan Corbet <corbet@lwn.net> 20# Joshua Clayton <stillcompiling@gmail.com> 21# Kees Cook <keescook@chromium.org> 22# Linus Torvalds <torvalds@linux-foundation.org> 23# Magnus Damm <damm+renesas@opensource.se> 24# Masahiro Yamada <masahiroy@kernel.org> 25# Mauro Carvalho Chehab <mchehab+huawei@kernel.org> 26# Maxim Cournoyer <maxim.cournoyer@gmail.com> 27# Peter Foley <pefoley2@pefoley.com> 28# Randy Dunlap <rdunlap@infradead.org> 29# Rob Herring <robh@kernel.org> 30# Shuah Khan <shuahkh@osg.samsung.com> 31# Thorsten Blum <thorsten.blum@toblux.com> 32# Tomas Winkler <tomas.winkler@intel.com> 33 34 35""" 36Sphinx build wrapper that handles Kernel-specific business rules: 37 38- it gets the Kernel build environment vars; 39- it determines what's the best parallelism; 40- it handles SPHINXDIRS 41 42This tool ensures that MIN_PYTHON_VERSION is satisfied. If version is 43below that, it seeks for a new Python version. If found, it re-runs using 44the newer version. 45""" 46 47import argparse 48import locale 49import os 50import re 51import shlex 52import shutil 53import subprocess 54import sys 55 56from concurrent import futures 57from glob import glob 58 59 60LIB_DIR = "../lib/python" 61SRC_DIR = os.path.dirname(os.path.realpath(__file__)) 62 63sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR)) 64 65from kdoc.python_version import PythonVersion 66from kdoc.latex_fonts import LatexFontChecker 67from jobserver import JobserverExec # pylint: disable=C0413,C0411,E0401 68 69# 70# Some constants 71# 72VENV_DEFAULT = "sphinx_latest" 73MIN_PYTHON_VERSION = PythonVersion("3.7").version 74PAPER = ["", "a4", "letter"] 75 76TARGETS = { 77 "cleandocs": { "builder": "clean" }, 78 "linkcheckdocs": { "builder": "linkcheck" }, 79 "htmldocs": { "builder": "html" }, 80 "epubdocs": { "builder": "epub", "out_dir": "epub" }, 81 "texinfodocs": { "builder": "texinfo", "out_dir": "texinfo" }, 82 "infodocs": { "builder": "texinfo", "out_dir": "texinfo" }, 83 "mandocs": { "builder": "man", "out_dir": "man" }, 84 "latexdocs": { "builder": "latex", "out_dir": "latex" }, 85 "pdfdocs": { "builder": "latex", "out_dir": "latex" }, 86 "xmldocs": { "builder": "xml", "out_dir": "xml" }, 87} 88 89 90# 91# SphinxBuilder class 92# 93 94class SphinxBuilder: 95 """ 96 Handles a sphinx-build target, adding needed arguments to build 97 with the Kernel. 98 """ 99 100 def get_path(self, path, use_cwd=False, abs_path=False): 101 """ 102 Ancillary routine to handle patches the right way, as shell does. 103 104 It first expands "~" and "~user". Then, if patch is not absolute, 105 join self.srctree. Finally, if requested, convert to abspath. 106 """ 107 108 path = os.path.expanduser(path) 109 if not path.startswith("/"): 110 if use_cwd: 111 base = os.getcwd() 112 else: 113 base = self.srctree 114 115 path = os.path.join(base, path) 116 117 if abs_path: 118 return os.path.abspath(path) 119 120 return path 121 122 def check_rust(self, sphinxdirs): 123 """ 124 Checks if Rust is enabled 125 """ 126 config = os.path.join(self.srctree, ".config") 127 128 if not {'.', 'rust'}.intersection(sphinxdirs): 129 return False 130 131 if not os.path.isfile(config): 132 return False 133 134 re_rust = re.compile(r"CONFIG_RUST=(m|y)") 135 136 try: 137 with open(config, "r", encoding="utf-8") as fp: 138 for line in fp: 139 if re_rust.match(line): 140 return True 141 142 except OSError as e: 143 print(f"Failed to open {config}", file=sys.stderr) 144 return False 145 146 return False 147 148 def get_sphinx_extra_opts(self, n_jobs): 149 """ 150 Get the number of jobs to be used for docs build passed via command 151 line and desired sphinx verbosity. 152 153 The number of jobs can be on different places: 154 155 1) It can be passed via "-j" argument; 156 2) The SPHINXOPTS="-j8" env var may have "-j"; 157 3) if called via GNU make, -j specifies the desired number of jobs. 158 with GNU makefile, this number is available via POSIX jobserver; 159 4) if none of the above is available, it should default to "-jauto", 160 and let sphinx decide the best value. 161 """ 162 163 # 164 # SPHINXOPTS env var, if used, contains extra arguments to be used 165 # by sphinx-build time. Among them, it may contain sphinx verbosity 166 # and desired number of parallel jobs. 167 # 168 parser = argparse.ArgumentParser() 169 parser.add_argument('-j', '--jobs', type=int) 170 parser.add_argument('-q', '--quiet', action='store_true') 171 parser.add_argument('-v', '--verbose', default=0, action='count') 172 173 # 174 # Other sphinx-build arguments go as-is, so place them 175 # at self.sphinxopts, using shell parser 176 # 177 sphinxopts = shlex.split(os.environ.get("SPHINXOPTS", "")) 178 179 # 180 # Build a list of sphinx args, honoring verbosity here if specified 181 # 182 183 sphinx_args, self.sphinxopts = parser.parse_known_args(sphinxopts) 184 185 verbose = sphinx_args.verbose 186 if self.verbose: 187 verbose += 1 188 189 if sphinx_args.quiet is True: 190 verbose = 0 191 192 # 193 # If the user explicitly sets "-j" at command line, use it. 194 # Otherwise, pick it from SPHINXOPTS args 195 # 196 if n_jobs: 197 self.n_jobs = n_jobs 198 elif sphinx_args.jobs: 199 self.n_jobs = sphinx_args.jobs 200 else: 201 self.n_jobs = None 202 203 if verbose < 1: 204 self.sphinxopts += ["-q"] 205 else: 206 for i in range(1, sphinx_args.verbose): 207 self.sphinxopts += ["-v"] 208 209 def __init__(self, builddir, venv=None, verbose=False, n_jobs=None, 210 interactive=None): 211 """Initialize internal variables""" 212 self.venv = venv 213 self.verbose = None 214 215 # 216 # Normal variables passed from Kernel's makefile 217 # 218 self.kernelversion = os.environ.get("KERNELVERSION", "unknown") 219 self.kernelrelease = os.environ.get("KERNELRELEASE", "unknown") 220 self.pdflatex = os.environ.get("PDFLATEX", "xelatex") 221 222 # 223 # Kernel main Makefile defines a PYTHON3 variable whose default is 224 # "python3". When set to a different value, it allows running a 225 # diferent version than the default official python3 package. 226 # Several distros package python3xx-sphinx packages with newer 227 # versions of Python and sphinx-build. 228 # 229 # Honor such variable different than default 230 # 231 self.python = os.environ.get("PYTHON3") 232 if self.python == "python3": 233 self.python = None 234 235 if not interactive: 236 self.latexopts = os.environ.get("LATEXOPTS", "-interaction=batchmode -no-shell-escape") 237 else: 238 self.latexopts = os.environ.get("LATEXOPTS", "") 239 240 if not verbose: 241 try: 242 verbose = bool(int(os.environ.get("KBUILD_VERBOSE", 0))) 243 except ValueError: 244 # Handles an eventual case where verbosity is not a number 245 # like KBUILD_VERBOSE="" 246 verbose = False 247 248 if verbose is not None: 249 self.verbose = verbose 250 251 # 252 # Source tree directory. This needs to be at os.environ, as 253 # Sphinx extensions use it 254 # 255 self.srctree = os.environ.get("srctree") 256 if not self.srctree: 257 self.srctree = "." 258 os.environ["srctree"] = self.srctree 259 260 # 261 # Now that we can expand srctree, get other directories as well 262 # 263 self.sphinxbuild = os.environ.get("SPHINXBUILD", "sphinx-build") 264 self.kerneldoc = self.get_path(os.environ.get("KERNELDOC", 265 "tools/docs/kernel-doc")) 266 self.builddir = self.get_path(builddir, use_cwd=True, abs_path=True) 267 268 # 269 # Get directory locations for LaTeX build toolchain 270 # 271 self.pdflatex_cmd = shutil.which(self.pdflatex) 272 self.latexmk_cmd = shutil.which("latexmk") 273 274 self.env = os.environ.copy() 275 276 self.get_sphinx_extra_opts(n_jobs) 277 278 # 279 # If venv command line argument is specified, run Sphinx from venv 280 # 281 if venv: 282 bin_dir = os.path.join(venv, "bin") 283 if not os.path.isfile(os.path.join(bin_dir, "activate")): 284 sys.exit(f"Venv {venv} not found.") 285 286 # "activate" virtual env 287 self.env["PATH"] = bin_dir + ":" + self.env["PATH"] 288 self.env["VIRTUAL_ENV"] = venv 289 if "PYTHONHOME" in self.env: 290 del self.env["PYTHONHOME"] 291 print(f"Setting venv to {venv}") 292 293 def run_sphinx(self, sphinx_build, build_args, *args, **pwargs): 294 """ 295 Executes sphinx-build using current python3 command. 296 297 When calling via GNU make, POSIX jobserver is used to tell how 298 many jobs are still available from a job pool. claim all remaining 299 jobs, as we don't want sphinx-build to run in parallel with other 300 jobs. 301 302 Despite that, the user may actually force a different value than 303 the number of available jobs via command line. 304 305 The "with" logic here is used to ensure that the claimed jobs will 306 be freed once subprocess finishes 307 """ 308 309 with JobserverExec() as jobserver: 310 if jobserver.claim: 311 # 312 # when GNU make is used, claim available jobs from jobserver 313 # 314 n_jobs = str(jobserver.claim) 315 else: 316 # 317 # Otherwise, let sphinx decide by default 318 # 319 n_jobs = "auto" 320 321 # 322 # If explicitly requested via command line, override default 323 # 324 if self.n_jobs: 325 n_jobs = str(self.n_jobs) 326 327 # 328 # We can't simply call python3 sphinx-build, as OpenSUSE 329 # Tumbleweed uses an ELF binary file (/usr/bin/alts) to switch 330 # between different versions of sphinx-build. So, only call it 331 # prepending "python3.xx" when PYTHON3 variable is not default. 332 # 333 if self.python: 334 cmd = [self.python] 335 else: 336 cmd = [] 337 338 cmd += [sphinx_build] 339 cmd += [f"-j{n_jobs}"] 340 cmd += build_args 341 cmd += self.sphinxopts 342 343 if self.verbose: 344 print(" ".join(cmd)) 345 346 return subprocess.call(cmd, *args, **pwargs) 347 348 def handle_html(self, css, output_dir): 349 """ 350 Extra steps for HTML and epub output. 351 352 For such targets, we need to ensure that CSS will be properly 353 copied to the output _static directory 354 """ 355 356 if css: 357 css = os.path.expanduser(css) 358 if not css.startswith("/"): 359 css = os.path.join(self.srctree, css) 360 361 static_dir = os.path.join(output_dir, "_static") 362 os.makedirs(static_dir, exist_ok=True) 363 364 try: 365 shutil.copy2(css, static_dir) 366 except (OSError, IOError) as e: 367 print(f"Warning: Failed to copy CSS: {e}", file=sys.stderr) 368 369 def build_pdf_file(self, latex_cmd, from_dir, path): 370 """Builds a single pdf file using latex_cmd""" 371 try: 372 subprocess.run(latex_cmd + [path], 373 cwd=from_dir, check=True, env=self.env) 374 375 return True 376 except subprocess.CalledProcessError: 377 return False 378 379 def pdf_parallel_build(self, tex_suffix, latex_cmd, tex_files, n_jobs): 380 """Build PDF files in parallel if possible""" 381 builds = {} 382 build_failed = False 383 max_len = 0 384 has_tex = False 385 386 # 387 # LaTeX PDF error code is almost useless for us: 388 # any warning makes it non-zero. For kernel doc builds it always return 389 # non-zero even when build succeeds. So, let's do the best next thing: 390 # Ignore build errors. At the end, check if all PDF files were built, 391 # printing a summary with the built ones and returning 0 if all of 392 # them were actually built. 393 # 394 with futures.ThreadPoolExecutor(max_workers=n_jobs) as executor: 395 jobs = {} 396 397 for from_dir, pdf_dir, entry in tex_files: 398 name = entry.name 399 400 if not name.endswith(tex_suffix): 401 continue 402 403 name = name[:-len(tex_suffix)] 404 has_tex = True 405 406 future = executor.submit(self.build_pdf_file, latex_cmd, 407 from_dir, entry.path) 408 jobs[future] = (from_dir, pdf_dir, name) 409 410 for future in futures.as_completed(jobs): 411 from_dir, pdf_dir, name = jobs[future] 412 413 pdf_name = name + ".pdf" 414 pdf_from = os.path.join(from_dir, pdf_name) 415 pdf_to = os.path.join(pdf_dir, pdf_name) 416 out_name = os.path.relpath(pdf_to, self.builddir) 417 max_len = max(max_len, len(out_name)) 418 419 try: 420 success = future.result() 421 422 if success and os.path.exists(pdf_from): 423 os.rename(pdf_from, pdf_to) 424 425 # 426 # if verbose, get the name of built PDF file 427 # 428 if self.verbose: 429 builds[out_name] = "SUCCESS" 430 else: 431 builds[out_name] = "FAILED" 432 build_failed = True 433 except futures.Error as e: 434 builds[out_name] = f"FAILED ({repr(e)})" 435 build_failed = True 436 437 # 438 # Handle case where no .tex files were found 439 # 440 if not has_tex: 441 out_name = "LaTeX files" 442 max_len = max(max_len, len(out_name)) 443 builds[out_name] = "FAILED: no .tex files were generated" 444 build_failed = True 445 446 return builds, build_failed, max_len 447 448 def handle_pdf(self, output_dirs, deny_vf): 449 """ 450 Extra steps for PDF output. 451 452 As PDF is handled via a LaTeX output, after building the .tex file, 453 a new build is needed to create the PDF output from the latex 454 directory. 455 """ 456 builds = {} 457 max_len = 0 458 tex_suffix = ".tex" 459 tex_files = [] 460 461 # 462 # Since early 2024, Fedora and openSUSE tumbleweed have started 463 # deploying variable-font format of "Noto CJK", causing LaTeX 464 # to break with CJK. Work around it, by denying the variable font 465 # usage during xelatex build by passing the location of a config 466 # file with a deny list. 467 # 468 # See tools/docs/lib/latex_fonts.py for more details. 469 # 470 if deny_vf: 471 deny_vf = os.path.expanduser(deny_vf) 472 if os.path.isdir(deny_vf): 473 self.env["XDG_CONFIG_HOME"] = deny_vf 474 475 for from_dir in output_dirs: 476 pdf_dir = os.path.join(from_dir, "../pdf") 477 os.makedirs(pdf_dir, exist_ok=True) 478 479 if self.latexmk_cmd: 480 latex_cmd = [self.latexmk_cmd, f"-{self.pdflatex}"] 481 else: 482 latex_cmd = [self.pdflatex] 483 484 latex_cmd.extend(shlex.split(self.latexopts)) 485 486 # Get a list of tex files to process 487 with os.scandir(from_dir) as it: 488 for entry in it: 489 if entry.name.endswith(tex_suffix): 490 tex_files.append((from_dir, pdf_dir, entry)) 491 492 # 493 # When using make, this won't be used, as the number of jobs comes 494 # from POSIX jobserver. So, this covers the case where build comes 495 # from command line. On such case, serialize by default, except if 496 # the user explicitly sets the number of jobs. 497 # 498 n_jobs = 1 499 500 # n_jobs is either an integer or "auto". Only use it if it is a number 501 if self.n_jobs: 502 try: 503 n_jobs = int(self.n_jobs) 504 except ValueError: 505 pass 506 507 # 508 # When using make, jobserver.claim is the number of jobs that were 509 # used with "-j" and that aren't used by other make targets 510 # 511 with JobserverExec() as jobserver: 512 n_jobs = 1 513 514 # 515 # Handle the case when a parameter is passed via command line, 516 # using it as default, if jobserver doesn't claim anything 517 # 518 if self.n_jobs: 519 try: 520 n_jobs = int(self.n_jobs) 521 except ValueError: 522 pass 523 524 if jobserver.claim: 525 n_jobs = jobserver.claim 526 527 builds, build_failed, max_len = self.pdf_parallel_build(tex_suffix, 528 latex_cmd, 529 tex_files, 530 n_jobs) 531 532 # 533 # In verbose mode, print a summary with the build results per file. 534 # Otherwise, print a single line with all failures, if any. 535 # On both cases, return code 1 indicates build failures, 536 # 537 if self.verbose: 538 msg = "Summary" 539 msg += "\n" + "=" * len(msg) 540 print() 541 print(msg) 542 543 for pdf_name, pdf_file in builds.items(): 544 print(f"{pdf_name:<{max_len}}: {pdf_file}") 545 546 print() 547 if build_failed: 548 msg = LatexFontChecker().check() 549 if msg: 550 print(msg) 551 552 sys.exit("Error: not all PDF files were created.") 553 554 elif build_failed: 555 n_failures = len(builds) 556 failures = ", ".join(builds.keys()) 557 558 msg = LatexFontChecker().check() 559 if msg: 560 print(msg) 561 562 sys.exit(f"Error: Can't build {n_failures} PDF file(s): {failures}") 563 564 def handle_info(self, output_dirs): 565 """ 566 Extra steps for Info output. 567 568 For texinfo generation, an additional make is needed from the 569 texinfo directory. 570 """ 571 572 for output_dir in output_dirs: 573 try: 574 subprocess.run(["make", "info"], cwd=output_dir, check=True) 575 except subprocess.CalledProcessError as e: 576 sys.exit(f"Error generating info docs: {e}") 577 578 def handle_man(self, kerneldoc, docs_dir, src_dir, output_dir): 579 """ 580 Create man pages from kernel-doc output 581 """ 582 583 re_kernel_doc = re.compile(r"^\.\.\s+kernel-doc::\s*(\S+)") 584 585 if docs_dir == src_dir: 586 # 587 # Pick the entire set of kernel-doc markups from the entire tree 588 # 589 kdoc_files = set([self.srctree]) 590 else: 591 kdoc_files = set() 592 593 for fname in glob(os.path.join(src_dir, "**"), recursive=True): 594 if os.path.isfile(fname) and fname.endswith(".rst"): 595 with open(fname, "r", encoding="utf-8") as in_fp: 596 data = in_fp.read() 597 598 for line in data.split("\n"): 599 match = re_kernel_doc.match(line) 600 if match: 601 if os.path.isfile(match.group(1)): 602 kdoc_files.add(match.group(1)) 603 604 if not kdoc_files: 605 sys.exit(f"Directory {src_dir} doesn't contain kernel-doc tags") 606 607 cmd = [ kerneldoc, "-m" ] + sorted(kdoc_files) 608 try: 609 if self.verbose: 610 print(" ".join(cmd)) 611 612 result = subprocess.run(cmd, stdout=subprocess.PIPE, text= True) 613 614 if result.returncode: 615 print(f"Warning: kernel-doc returned {result.returncode} warnings") 616 617 except (OSError, ValueError, subprocess.SubprocessError) as e: 618 sys.exit(f"Failed to create man pages for {src_dir}: {repr(e)}") 619 620 fp = None 621 try: 622 for line in result.stdout.split("\n"): 623 if not line.startswith(".TH"): 624 if fp: 625 fp.write(line + '\n') 626 continue 627 628 if fp: 629 fp.close() 630 631 # Use shlex here, as it handles well parameters with commas 632 args = shlex.split(line) 633 fname = f"{args[1]}.{args[2]}" 634 fname = fname.replace("/", " ") 635 fname = f"{output_dir}/{fname}" 636 637 if self.verbose: 638 print(f"Creating {fname}") 639 fp = open(fname, "w", encoding="utf-8") 640 fp.write(line + '\n') 641 finally: 642 if fp: 643 fp.close() 644 645 def cleandocs(self, builder): # pylint: disable=W0613 646 """Remove documentation output directory""" 647 shutil.rmtree(self.builddir, ignore_errors=True) 648 649 def build(self, target, sphinxdirs=None, 650 theme=None, css=None, paper=None, deny_vf=None, 651 skip_sphinx=False): 652 """ 653 Build documentation using Sphinx. This is the core function of this 654 module. It prepares all arguments required by sphinx-build. 655 """ 656 657 builder = TARGETS[target]["builder"] 658 out_dir = TARGETS[target].get("out_dir", "") 659 660 # 661 # Cleandocs doesn't require sphinx-build 662 # 663 if target == "cleandocs": 664 self.cleandocs(builder) 665 return 666 667 if theme: 668 os.environ["DOCS_THEME"] = theme 669 670 # 671 # Other targets require sphinx-build, so check if it exists 672 # 673 if not skip_sphinx: 674 sphinxbuild = shutil.which(self.sphinxbuild, path=self.env["PATH"]) 675 if not sphinxbuild and target != "mandocs": 676 sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n") 677 678 if target == "pdfdocs": 679 if not self.pdflatex_cmd and not self.latexmk_cmd: 680 sys.exit("Error: pdflatex or latexmk required for PDF generation") 681 682 docs_dir = os.path.abspath(os.path.join(self.srctree, "Documentation")) 683 684 # 685 # Fill in base arguments for Sphinx build 686 # 687 kerneldoc = self.kerneldoc 688 if kerneldoc.startswith(self.srctree): 689 kerneldoc = os.path.relpath(kerneldoc, self.srctree) 690 691 if not sphinxdirs: 692 sphinxdirs = os.environ.get("SPHINXDIRS", ".") 693 694 # 695 # sphinxdirs can be a list or a whitespace-separated string 696 # 697 sphinxdirs_list = [] 698 for sphinxdir in sphinxdirs: 699 if isinstance(sphinxdir, list): 700 sphinxdirs_list += sphinxdir 701 else: 702 sphinxdirs_list += sphinxdir.split() 703 704 args = [ "-b", builder, "-c", docs_dir ] 705 706 if builder == "latex": 707 if not paper: 708 paper = PAPER[1] 709 710 args.extend(["-D", f"latex_elements.papersize={paper}paper"]) 711 712 rustdoc = self.check_rust(sphinxdirs_list) 713 if rustdoc: 714 args.extend(["-t", "rustdoc"]) 715 716 # 717 # The sphinx-build tool has a bug: internally, it tries to set 718 # locale with locale.setlocale(locale.LC_ALL, ''). This causes a 719 # crash if language is not set. Detect and fix it. 720 # 721 try: 722 locale.setlocale(locale.LC_ALL, '') 723 except locale.Error: 724 self.env["LC_ALL"] = "C" 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"version={self.kernelversion}", 754 "-D", f"release={self.kernelrelease}", 755 "-D", f"kerneldoc_srctree={self.srctree}", 756 src_dir, 757 output_dir, 758 ] 759 760 if target == "mandocs": 761 self.handle_man(kerneldoc, docs_dir, src_dir, output_dir) 762 elif not skip_sphinx: 763 try: 764 result = self.run_sphinx(sphinxbuild, build_args, 765 env=self.env) 766 767 if result: 768 sys.exit(f"Build failed: return code: {result}") 769 770 except (OSError, ValueError, subprocess.SubprocessError) as e: 771 sys.exit(f"Build failed: {repr(e)}") 772 773 # 774 # Ensure that each html/epub output will have needed static files 775 # 776 if target in ["htmldocs", "epubdocs"]: 777 self.handle_html(css, output_dir) 778 779 # 780 # Step 2: Some targets (PDF and info) require an extra step once 781 # sphinx-build finishes 782 # 783 if target == "pdfdocs": 784 self.handle_pdf(output_dirs, deny_vf) 785 elif target == "infodocs": 786 self.handle_info(output_dirs) 787 788 if rustdoc and target in ["htmldocs", "epubdocs"]: 789 print("Building rust docs") 790 if "MAKE" in self.env: 791 cmd = [self.env["MAKE"]] 792 else: 793 cmd = ["make", "LLVM=1"] 794 795 cmd += [ "rustdoc"] 796 if self.verbose: 797 print(" ".join(cmd)) 798 799 try: 800 subprocess.run(cmd, check=True) 801 except subprocess.CalledProcessError as e: 802 print(f"Ignored errors when building rustdoc: {e}. Is RUST enabled?", 803 file=sys.stderr) 804 805def jobs_type(value): 806 """ 807 Handle valid values for -j. Accepts Sphinx "-jauto", plus a number 808 equal or bigger than one. 809 """ 810 if value is None: 811 return None 812 813 if value.lower() == 'auto': 814 return value.lower() 815 816 try: 817 if int(value) >= 1: 818 return value 819 820 raise argparse.ArgumentTypeError(f"Minimum jobs is 1, got {value}") 821 except ValueError: 822 raise argparse.ArgumentTypeError(f"Must be 'auto' or positive integer, got {value}") # pylint: disable=W0707 823 824EPILOG=""" 825Besides the command line arguments, several environment variables affect its 826default behavior, meant to be used when called via Kernel Makefile: 827 828- KERNELVERSION: Kernel major version 829- KERNELRELEASE: Kernel release 830- KBUILD_VERBOSE: Contains the value of "make V=[0|1] variable. 831 When V=0 (KBUILD_VERBOSE=0), sets verbose level to "-q". 832- SPHINXBUILD: Documentation build tool (default: "sphinx-build"). 833- SPHINXOPTS: Extra options pased to SPHINXBUILD 834 (default: "-j auto" and "-q" if KBUILD_VERBOSE=0). 835 The "-v" flag can be used to increase verbosity. 836 If V=0, the first "-v" will drop "-q". 837- PYTHON3: Python command to run SPHINXBUILD 838- PDFLATEX: LaTeX PDF engine. (default: "xelatex") 839- LATEXOPTS: Optional set of command line arguments to the LaTeX engine 840- srctree: Location of the Kernel root directory (default: "."). 841 842""" 843 844def main(): 845 """ 846 Main function. The only mandatory argument is the target. If not 847 specified, the other arguments will use default values if not 848 specified at os.environ. 849 """ 850 parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, 851 description=__doc__, 852 epilog=EPILOG) 853 854 parser.add_argument("target", choices=list(TARGETS.keys()), 855 help="Documentation target to build") 856 parser.add_argument("--sphinxdirs", nargs="+", 857 help="Specific directories to build") 858 parser.add_argument("--builddir", default="output", 859 help="Sphinx configuration file (default: %(default)s)") 860 861 parser.add_argument("--theme", help="Sphinx theme to use") 862 863 parser.add_argument("--css", help="Custom CSS file for HTML/EPUB") 864 865 parser.add_argument("--paper", choices=PAPER, default=PAPER[0], 866 help="Paper size for LaTeX/PDF output") 867 868 parser.add_argument('--deny-vf', 869 help="Configuration to deny variable fonts on pdf builds") 870 871 parser.add_argument("-v", "--verbose", action='store_true', 872 help="place build in verbose mode") 873 874 parser.add_argument('-j', '--jobs', type=jobs_type, 875 help="Sets number of jobs to use with sphinx-build(default: auto)") 876 877 parser.add_argument('-i', '--interactive', action='store_true', 878 help="Change latex default to run in interactive mode") 879 880 parser.add_argument('-s', '--skip-sphinx-build', action='store_true', 881 help="Skip sphinx-build step") 882 883 parser.add_argument("-V", "--venv", nargs='?', const=f'{VENV_DEFAULT}', 884 default=None, 885 help=f'If used, run Sphinx from a venv dir (default dir: {VENV_DEFAULT})') 886 887 args = parser.parse_args() 888 889 PythonVersion.check_python(MIN_PYTHON_VERSION, show_alternatives=True, 890 bail_out=True) 891 892 builder = SphinxBuilder(builddir=args.builddir, venv=args.venv, 893 verbose=args.verbose, n_jobs=args.jobs, 894 interactive=args.interactive) 895 896 builder.build(args.target, sphinxdirs=args.sphinxdirs, 897 theme=args.theme, css=args.css, paper=args.paper, 898 deny_vf=args.deny_vf, 899 skip_sphinx=args.skip_sphinx_build) 900 901if __name__ == "__main__": 902 main() 903