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
59LIB_DIR = "lib"
60SRC_DIR = os.path.dirname(os.path.realpath(__file__))
61
62sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR))
63
64from jobserver import JobserverExec # pylint: disable=C0413
65
66
67def parse_version(version):
68 """Convert a major.minor.patch version into a tuple"""
69 return tuple(int(x) for x in version.split("."))
70
71def ver_str(version):
72 """Returns a version tuple as major.minor.patch"""
73
74 return ".".join([str(x) for x in version])
75
76# Minimal supported Python version needed by Sphinx and its extensions
77MIN_PYTHON_VERSION = parse_version("3.7")
78
79# Default value for --venv parameter
80VENV_DEFAULT = "sphinx_latest"
81
82# List of make targets and its corresponding builder and output directory
83TARGETS = {
84 "cleandocs": {
85 "builder": "clean",
86 },
87 "htmldocs": {
88 "builder": "html",
89 },
90 "epubdocs": {
91 "builder": "epub",
92 "out_dir": "epub",
93 },
94 "texinfodocs": {
95 "builder": "texinfo",
96 "out_dir": "texinfo",
97 },
98 "infodocs": {
99 "builder": "texinfo",
100 "out_dir": "texinfo",
101 },
102 "latexdocs": {
103 "builder": "latex",
104 "out_dir": "latex",
105 },
106 "pdfdocs": {
107 "builder": "latex",
108 "out_dir": "latex",
109 },
110 "xmldocs": {
111 "builder": "xml",
112 "out_dir": "xml",
113 },
114 "linkcheckdocs": {
115 "builder": "linkcheck"
116 },
117}
118
119# Paper sizes. An empty value will pick the default
120PAPER = ["", "a4", "letter"]
121
122class SphinxBuilder:
123 """
124 Handles a sphinx-build target, adding needed arguments to build
125 with the Kernel.
126 """
127
128 def is_rust_enabled(self):
129 """Check if rust is enabled at .config"""
130 config_path = os.path.join(self.srctree, ".config")
131 if os.path.isfile(config_path):
132 with open(config_path, "r", encoding="utf-8") as f:
133 return "CONFIG_RUST=y" in f.read()
134 return False
135
136 def get_path(self, path, abs_path=False):
137 """
138 Ancillary routine to handle patches the right way, as shell does.
139
140 It first expands "~" and "~user". Then, if patch is not absolute,
141 join self.srctree. Finally, if requested, convert to abspath.
142 """
143
144 path = os.path.expanduser(path)
145 if not path.startswith("/"):
146 path = os.path.join(self.srctree, path)
147
148 if abs_path:
149 return os.path.abspath(path)
150
151 return path
152
153 def __init__(self, venv=None, verbose=False, n_jobs=None, interactive=None):
154 """Initialize internal variables"""
155 self.venv = venv
156 self.verbose = None
157
158 # Normal variables passed from Kernel's makefile
159 self.kernelversion = os.environ.get("KERNELVERSION", "unknown")
160 self.kernelrelease = os.environ.get("KERNELRELEASE", "unknown")
161 self.pdflatex = os.environ.get("PDFLATEX", "xelatex")
162
163 if not interactive:
164 self.latexopts = os.environ.get("LATEXOPTS", "-interaction=batchmode -no-shell-escape")
165 else:
166 self.latexopts = os.environ.get("LATEXOPTS", "")
167
168 if not verbose:
169 verbose = bool(os.environ.get("KBUILD_VERBOSE", "") != "")
170
171 # Handle SPHINXOPTS evironment
172 sphinxopts = shlex.split(os.environ.get("SPHINXOPTS", ""))
173
174 # As we handle number of jobs and quiet in separate, we need to pick
175 # it the same way as sphinx-build would pick, so let's use argparse
176 # do to the right argument expansion
177 parser = argparse.ArgumentParser()
178 parser.add_argument('-j', '--jobs', type=int)
179 parser.add_argument('-q', '--quiet', type=int)
180
181 # Other sphinx-build arguments go as-is, so place them
182 # at self.sphinxopts
183 sphinx_args, self.sphinxopts = parser.parse_known_args(sphinxopts)
184 if sphinx_args.quiet == True:
185 self.verbose = False
186
187 if sphinx_args.jobs:
188 self.n_jobs = sphinx_args.jobs
189
190 # Command line arguments was passed, override SPHINXOPTS
191 if verbose is not None:
192 self.verbose = verbose
193
194 self.n_jobs = n_jobs
195
196 # Source tree directory. This needs to be at os.environ, as
197 # Sphinx extensions and media uAPI makefile needs it
198 self.srctree = os.environ.get("srctree")
199 if not self.srctree:
200 self.srctree = "."
201 os.environ["srctree"] = self.srctree
202
203 # Now that we can expand srctree, get other directories as well
204 self.sphinxbuild = os.environ.get("SPHINXBUILD", "sphinx-build")
205 self.kerneldoc = self.get_path(os.environ.get("KERNELDOC",
206 "scripts/kernel-doc.py"))
207 self.obj = os.environ.get("obj", "Documentation")
208 self.builddir = self.get_path(os.path.join(self.obj, "output"),
209 abs_path=True)
210
211 # Media uAPI needs it
212 os.environ["BUILDDIR"] = self.builddir
213
214 # Detect if rust is enabled
215 self.config_rust = self.is_rust_enabled()
216
217 # Get directory locations for LaTeX build toolchain
218 self.pdflatex_cmd = shutil.which(self.pdflatex)
219 self.latexmk_cmd = shutil.which("latexmk")
220
221 self.env = os.environ.copy()
222
223 # If venv parameter is specified, run Sphinx from venv
224 if venv:
225 bin_dir = os.path.join(venv, "bin")
226 if os.path.isfile(os.path.join(bin_dir, "activate")):
227 # "activate" virtual env
228 self.env["PATH"] = bin_dir + ":" + self.env["PATH"]
229 self.env["VIRTUAL_ENV"] = venv
230 if "PYTHONHOME" in self.env:
231 del self.env["PYTHONHOME"]
232 print(f"Setting venv to {venv}")
233 else:
234 sys.exit(f"Venv {venv} not found.")
235
236 def run_sphinx(self, sphinx_build, build_args, *args, **pwargs):
237 """
238 Executes sphinx-build using current python3 command and setting
239 -j parameter if possible to run the build in parallel.
240 """
241
242 with JobserverExec() as jobserver:
243 if jobserver.claim:
244 n_jobs = str(jobserver.claim)
245 else:
246 n_jobs = "auto" # Supported since Sphinx 1.7
247
248 cmd = []
249
250 if self.venv:
251 cmd.append("python")
252 else:
253 cmd.append(sys.executable)
254
255 cmd.append(sphinx_build)
256
257 # if present, SPHINXOPTS or command line --jobs overrides default
258 if self.n_jobs:
259 n_jobs = str(self.n_jobs)
260
261 if n_jobs:
262 cmd += [f"-j{n_jobs}"]
263
264 if not self.verbose:
265 cmd.append("-q")
266
267 cmd += self.sphinxopts
268
269 cmd += build_args
270
271 if self.verbose:
272 print(" ".join(cmd))
273
274 rc = subprocess.call(cmd, *args, **pwargs)
275
276 def handle_html(self, css, output_dir):
277 """
278 Extra steps for HTML and epub output.
279
280 For such targets, we need to ensure that CSS will be properly
281 copied to the output _static directory
282 """
283
284 if not css:
285 return
286
287 css = os.path.expanduser(css)
288 if not css.startswith("/"):
289 css = os.path.join(self.srctree, css)
290
291 static_dir = os.path.join(output_dir, "_static")
292 os.makedirs(static_dir, exist_ok=True)
293
294 try:
295 shutil.copy2(css, static_dir)
296 except (OSError, IOError) as e:
297 print(f"Warning: Failed to copy CSS: {e}", file=sys.stderr)
298
299 def build_pdf_file(self, latex_cmd, from_dir, path):
300 """Builds a single pdf file using latex_cmd"""
301 try:
302 subprocess.run(latex_cmd + [path],
303 cwd=from_dir, check=True)
304
305 return True
306 except subprocess.CalledProcessError:
307 # LaTeX PDF error code is almost useless: it returns
308 # error codes even when build succeeds but has warnings.
309 # So, we'll ignore the results
310 return False
311
312 def pdf_parallel_build(self, tex_suffix, latex_cmd, tex_files, n_jobs):
313 """Build PDF files in parallel if possible"""
314 builds = {}
315 build_failed = False
316 max_len = 0
317 has_tex = False
318
319 # Process files in parallel
320 with futures.ThreadPoolExecutor(max_workers=n_jobs) as executor:
321 jobs = {}
322
323 for from_dir, pdf_dir, entry in tex_files:
324 name = entry.name
325
326 if not name.endswith(tex_suffix):
327 continue
328
329 name = name[:-len(tex_suffix)]
330
331 max_len = max(max_len, len(name))
332
333 has_tex = True
334
335 future = executor.submit(self.build_pdf_file, latex_cmd,
336 from_dir, entry.path)
337 jobs[future] = (from_dir, name, entry.path)
338
339 for future in futures.as_completed(jobs):
340 from_dir, name, path = jobs[future]
341
342 pdf_name = name + ".pdf"
343 pdf_from = os.path.join(from_dir, pdf_name)
344
345 try:
346 success = future.result()
347
348 if success and os.path.exists(pdf_from):
349 pdf_to = os.path.join(pdf_dir, pdf_name)
350
351 os.rename(pdf_from, pdf_to)
352 builds[name] = os.path.relpath(pdf_to, self.builddir)
353 else:
354 builds[name] = "FAILED"
355 build_failed = True
356 except Exception as e:
357 builds[name] = f"FAILED ({str(e)})"
358 build_failed = True
359
360 # Handle case where no .tex files were found
361 if not has_tex:
362 name = "Sphinx LaTeX builder"
363 max_len = max(max_len, len(name))
364 builds[name] = "FAILED (no .tex file was generated)"
365 build_failed = True
366
367 return builds, build_failed, max_len
368
369 def handle_pdf(self, output_dirs):
370 """
371 Extra steps for PDF output.
372
373 As PDF is handled via a LaTeX output, after building the .tex file,
374 a new build is needed to create the PDF output from the latex
375 directory.
376 """
377 builds = {}
378 max_len = 0
379 tex_suffix = ".tex"
380
381 # Get all tex files that will be used for PDF build
382 tex_files = []
383 for from_dir in output_dirs:
384 pdf_dir = os.path.join(from_dir, "../pdf")
385 os.makedirs(pdf_dir, exist_ok=True)
386
387 if self.latexmk_cmd:
388 latex_cmd = [self.latexmk_cmd, f"-{self.pdflatex}"]
389 else:
390 latex_cmd = [self.pdflatex]
391
392 latex_cmd.extend(shlex.split(self.latexopts))
393
394 # Get a list of tex files to process
395 with os.scandir(from_dir) as it:
396 for entry in it:
397 if entry.name.endswith(tex_suffix):
398 tex_files.append((from_dir, pdf_dir, entry))
399
400 # When using make, this won't be used, as the number of jobs comes
401 # from POSIX jobserver. So, this covers the case where build comes
402 # from command line. On such case, serialize by default, except if
403 # the user explicitly sets the number of jobs.
404 n_jobs = 1
405
406 # n_jobs is either an integer or "auto". Only use it if it is a number
407 if self.n_jobs:
408 try:
409 n_jobs = int(self.n_jobs)
410 except ValueError:
411 pass
412
413 # When using make, jobserver.claim is the number of jobs that were
414 # used with "-j" and that aren't used by other make targets
415 with JobserverExec() as jobserver:
416 n_jobs = 1
417
418 # Handle the case when a parameter is passed via command line,
419 # using it as default, if jobserver doesn't claim anything
420 if self.n_jobs:
421 try:
422 n_jobs = int(self.n_jobs)
423 except ValueError:
424 pass
425
426 if jobserver.claim:
427 n_jobs = jobserver.claim
428
429 # Build files in parallel
430 builds, build_failed, max_len = self.pdf_parallel_build(tex_suffix,
431 latex_cmd,
432 tex_files,
433 n_jobs)
434
435 msg = "Summary"
436 msg += "\n" + "=" * len(msg)
437 print()
438 print(msg)
439
440 for pdf_name, pdf_file in builds.items():
441 print(f"{pdf_name:<{max_len}}: {pdf_file}")
442
443 print()
444
445 # return an error if a PDF file is missing
446
447 if build_failed:
448 sys.exit(f"PDF build failed: not all PDF files were created.")
449 else:
450 print("All PDF files were built.")
451
452 def handle_info(self, output_dirs):
453 """
454 Extra steps for Info output.
455
456 For texinfo generation, an additional make is needed from the
457 texinfo directory.
458 """
459
460 for output_dir in output_dirs:
461 try:
462 subprocess.run(["make", "info"], cwd=output_dir, check=True)
463 except subprocess.CalledProcessError as e:
464 sys.exit(f"Error generating info docs: {e}")
465
466 def cleandocs(self, builder):
467
468 shutil.rmtree(self.builddir, ignore_errors=True)
469
470 def build(self, target, sphinxdirs=None, conf="conf.py",
471 theme=None, css=None, paper=None):
472 """
473 Build documentation using Sphinx. This is the core function of this
474 module. It prepares all arguments required by sphinx-build.
475 """
476
477 builder = TARGETS[target]["builder"]
478 out_dir = TARGETS[target].get("out_dir", "")
479
480 # Cleandocs doesn't require sphinx-build
481 if target == "cleandocs":
482 self.cleandocs(builder)
483 return
484
485 # Other targets require sphinx-build
486 sphinxbuild = shutil.which(self.sphinxbuild, path=self.env["PATH"])
487 if not sphinxbuild:
488 sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n")
489
490 if builder == "latex":
491 if not self.pdflatex_cmd and not self.latexmk_cmd:
492 sys.exit("Error: pdflatex or latexmk required for PDF generation")
493
494 docs_dir = os.path.abspath(os.path.join(self.srctree, "Documentation"))
495
496 # Prepare base arguments for Sphinx build
497 kerneldoc = self.kerneldoc
498 if kerneldoc.startswith(self.srctree):
499 kerneldoc = os.path.relpath(kerneldoc, self.srctree)
500
501 # Prepare common Sphinx options
502 args = [
503 "-b", builder,
504 "-c", docs_dir,
505 ]
506
507 if builder == "latex":
508 if not paper:
509 paper = PAPER[1]
510
511 args.extend(["-D", f"latex_elements.papersize={paper}paper"])
512
513 if self.config_rust:
514 args.extend(["-t", "rustdoc"])
515
516 if conf:
517 self.env["SPHINX_CONF"] = self.get_path(conf, abs_path=True)
518
519 if not sphinxdirs:
520 sphinxdirs = os.environ.get("SPHINXDIRS", ".")
521
522 # The sphinx-build tool has a bug: internally, it tries to set
523 # locale with locale.setlocale(locale.LC_ALL, ''). This causes a
524 # crash if language is not set. Detect and fix it.
525 try:
526 locale.setlocale(locale.LC_ALL, '')
527 except Exception:
528 self.env["LC_ALL"] = "C"
529 self.env["LANG"] = "C"
530
531 # sphinxdirs can be a list or a whitespace-separated string
532 sphinxdirs_list = []
533 for sphinxdir in sphinxdirs:
534 if isinstance(sphinxdir, list):
535 sphinxdirs_list += sphinxdir
536 else:
537 for name in sphinxdir.split(" "):
538 sphinxdirs_list.append(name)
539
540 # Build each directory
541 output_dirs = []
542 for sphinxdir in sphinxdirs_list:
543 src_dir = os.path.join(docs_dir, sphinxdir)
544 doctree_dir = os.path.join(self.builddir, ".doctrees")
545 output_dir = os.path.join(self.builddir, sphinxdir, out_dir)
546
547 # Make directory names canonical
548 src_dir = os.path.normpath(src_dir)
549 doctree_dir = os.path.normpath(doctree_dir)
550 output_dir = os.path.normpath(output_dir)
551
552 os.makedirs(doctree_dir, exist_ok=True)
553 os.makedirs(output_dir, exist_ok=True)
554
555 output_dirs.append(output_dir)
556
557 build_args = args + [
558 "-d", doctree_dir,
559 "-D", f"kerneldoc_bin={kerneldoc}",
560 "-D", f"version={self.kernelversion}",
561 "-D", f"release={self.kernelrelease}",
562 "-D", f"kerneldoc_srctree={self.srctree}",
563 src_dir,
564 output_dir,
565 ]
566
567 # Execute sphinx-build
568 try:
569 self.run_sphinx(sphinxbuild, build_args, env=self.env)
570 except Exception as e:
571 sys.exit(f"Build failed: {e}")
572
573 # Ensure that html/epub will have needed static files
574 if target in ["htmldocs", "epubdocs"]:
575 self.handle_html(css, output_dir)
576
577 # PDF and Info require a second build step
578 if target == "pdfdocs":
579 self.handle_pdf(output_dirs)
580 elif target == "infodocs":
581 self.handle_info(output_dirs)
582
583 @staticmethod
584 def get_python_version(cmd):
585 """
586 Get python version from a Python binary. As we need to detect if
587 are out there newer python binaries, we can't rely on sys.release here.
588 """
589
590 result = subprocess.run([cmd, "--version"], check=True,
591 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
592 universal_newlines=True)
593 version = result.stdout.strip()
594
595 match = re.search(r"(\d+\.\d+\.\d+)", version)
596 if match:
597 return parse_version(match.group(1))
598
599 print(f"Can't parse version {version}")
600 return (0, 0, 0)
601
602 @staticmethod
603 def find_python():
604 """
605 Detect if are out there any python 3.xy version newer than the
606 current one.
607
608 Note: this routine is limited to up to 2 digits for python3. We
609 may need to update it one day, hopefully on a distant future.
610 """
611 patterns = [
612 "python3.[0-9]",
613 "python3.[0-9][0-9]",
614 ]
615
616 # Seek for a python binary newer than MIN_PYTHON_VERSION
617 for path in os.getenv("PATH", "").split(":"):
618 for pattern in patterns:
619 for cmd in glob(os.path.join(path, pattern)):
620 if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
621 version = SphinxBuilder.get_python_version(cmd)
622 if version >= MIN_PYTHON_VERSION:
623 return cmd
624
625 return None
626
627 @staticmethod
628 def check_python():
629 """
630 Check if the current python binary satisfies our minimal requirement
631 for Sphinx build. If not, re-run with a newer version if found.
632 """
633 cur_ver = sys.version_info[:3]
634 if cur_ver >= MIN_PYTHON_VERSION:
635 return
636
637 python_ver = ver_str(cur_ver)
638
639 new_python_cmd = SphinxBuilder.find_python()
640 if not new_python_cmd:
641 sys.exit(f"Python version {python_ver} is not supported anymore.")
642
643 # Restart script using the newer version
644 script_path = os.path.abspath(sys.argv[0])
645 args = [new_python_cmd, script_path] + sys.argv[1:]
646
647 print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
648
649 try:
650 os.execv(new_python_cmd, args)
651 except OSError as e:
652 sys.exit(f"Failed to restart with {new_python_cmd}: {e}")
653
654def jobs_type(value):
655 """
656 Handle valid values for -j. Accepts Sphinx "-jauto", plus a number
657 equal or bigger than one.
658 """
659 if value is None:
660 return None
661
662 if value.lower() == 'auto':
663 return value.lower()
664
665 try:
666 if int(value) >= 1:
667 return value
668
669 raise argparse.ArgumentTypeError(f"Minimum jobs is 1, got {value}")
670 except ValueError:
671 raise argparse.ArgumentTypeError(f"Must be 'auto' or positive integer, got {value}")
672
673def main():
674 """
675 Main function. The only mandatory argument is the target. If not
676 specified, the other arguments will use default values if not
677 specified at os.environ.
678 """
679 parser = argparse.ArgumentParser(description="Kernel documentation builder")
680
681 parser.add_argument("target", choices=list(TARGETS.keys()),
682 help="Documentation target to build")
683 parser.add_argument("--sphinxdirs", nargs="+",
684 help="Specific directories to build")
685 parser.add_argument("--conf", default="conf.py",
686 help="Sphinx configuration file")
687
688 parser.add_argument("--theme", help="Sphinx theme to use")
689
690 parser.add_argument("--css", help="Custom CSS file for HTML/EPUB")
691
692 parser.add_argument("--paper", choices=PAPER, default=PAPER[0],
693 help="Paper size for LaTeX/PDF output")
694
695 parser.add_argument("-v", "--verbose", action='store_true',
696 help="place build in verbose mode")
697
698 parser.add_argument('-j', '--jobs', type=jobs_type,
699 help="Sets number of jobs to use with sphinx-build")
700
701 parser.add_argument('-i', '--interactive', action='store_true',
702 help="Change latex default to run in interactive mode")
703
704 parser.add_argument("-V", "--venv", nargs='?', const=f'{VENV_DEFAULT}',
705 default=None,
706 help=f'If used, run Sphinx from a venv dir (default dir: {VENV_DEFAULT})')
707
708 args = parser.parse_args()
709
710 SphinxBuilder.check_python()
711
712 builder = SphinxBuilder(venv=args.venv, verbose=args.verbose,
713 n_jobs=args.jobs, interactive=args.interactive)
714
715 builder.build(args.target, sphinxdirs=args.sphinxdirs, conf=args.conf,
716 theme=args.theme, css=args.css, paper=args.paper)
717
718if __name__ == "__main__":
719 main()