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