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