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 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()