History

scripts: sphinx-build-wrapper: get rid of uapi/media Makefile

Now that kernel-include directive supports parsing data structs directly, we can finally get rid of the horrible hack we added to support parsing media uAPI symbols. As a side effect, Documentation/output doesn't have anymore media auto-generated .rst files on it.

Mauro Carvalho Chehab committed 1 year ago

Commit 8a298571 parent a49adfa16 files · +745 −76Browse files

Unified · Split

16 files
  1. Documentation/Makefile +1 −2
  2. Documentation/userspace-api/media/Makefile +0 −64
  3. Documentation/userspace-api/media/cec/cec-header.rst +3 −2
  4. Documentation/userspace-api/media/cec/cec.h.rst.exceptions +0 −0
  5. Documentation/userspace-api/media/dvb/ca.h.rst.exceptions +0 −0
  6. Documentation/userspace-api/media/dvb/dmx.h.rst.exceptions +0 −0
  7. Documentation/userspace-api/media/dvb/frontend.h.rst.exceptions +0 −0
  8. Documentation/userspace-api/media/dvb/headers.rst +13 −4
  9. Documentation/userspace-api/media/dvb/net.h.rst.exceptions +0 −0
  10. Documentation/userspace-api/media/mediactl/media-header.rst +3 −2
  11. Documentation/userspace-api/media/mediactl/media.h.rst.exceptions +0 −0
  12. Documentation/userspace-api/media/rc/lirc-header.rst +3 −1
  13. Documentation/userspace-api/media/rc/lirc.h.rst.exceptions +0 −0
  14. Documentation/userspace-api/media/v4l/videodev.rst +3 −1
  15. Documentation/userspace-api/media/v4l/videodev2.h.rst.exceptions +0 −0
  16. scripts/sphinx-build-wrapper +719 −0
Documentation/Makefile+1−2
@@ -87,7 +87,7 @@ loop_cmd = $(echo-cmd) $(cmd_$(1)) || exit; · 86 unmodified lines
8787 PYTHONPYCACHEPREFIX ?= $(abspath $(BUILDDIR)/__pycache__)
8888
8989 quiet_cmd_sphinx = SPHINX $@ --> file://$(abspath $(BUILDDIR)/$3/$4)
90− cmd_sphinx = $(MAKE) BUILDDIR=$(abspath $(BUILDDIR)) $(build)=Documentation/userspace-api/media $2 && \
90+ cmd_sphinx = \
9191 PYTHONPYCACHEPREFIX="$(PYTHONPYCACHEPREFIX)" \
9292 BUILDDIR=$(abspath $(BUILDDIR)) SPHINX_CONF=$(abspath $(src)/$5/$(SPHINX_CONF)) \
9393 $(PYTHON3) $(srctree)/scripts/jobserver-exec \
@@ -171,7 +171,6 @@ refcheckdocs: · 77 unmodified lines
171171
172172 cleandocs:
173173 $(Q)rm -rf $(BUILDDIR)
174− $(Q)$(MAKE) BUILDDIR=$(abspath $(BUILDDIR)) $(build)=Documentation/userspace-api/media clean
175174
176175 dochelp:
177176 @echo ' Linux kernel internal documentation in different formats from ReST:'
Documentation/userspace-api/media/Makefile+0−64
@@ -1,64 +0,0 @@
1−# SPDX-License-Identifier: GPL-2.0
2−
3−# Rules to convert a .h file to inline RST documentation
4−
5−SRC_DIR=$(srctree)/Documentation/userspace-api/media
6−PARSER = $(srctree)/tools/docs/parse-headers.py
7−UAPI = $(srctree)/include/uapi/linux
8−KAPI = $(srctree)/include/linux
9−
10−FILES = ca.h.rst dmx.h.rst frontend.h.rst net.h.rst \
11− videodev2.h.rst media.h.rst cec.h.rst lirc.h.rst
12−
13−TARGETS := $(addprefix $(BUILDDIR)/, $(FILES))
14−
15−gen_rst = \
16− echo ${PARSER} $< $@ $(SRC_DIR)/$(notdir $@).exceptions; \
17− ${PARSER} $< $@ $(SRC_DIR)/$(notdir $@).exceptions
18−
19−quiet_gen_rst = echo ' PARSE $(patsubst $(srctree)/%,%,$<)'; \
20− ${PARSER} $< $@ $(SRC_DIR)/$(notdir $@).exceptions
21−
22−silent_gen_rst = ${gen_rst}
23−
24−$(BUILDDIR)/ca.h.rst: ${UAPI}/dvb/ca.h ${PARSER} $(SRC_DIR)/ca.h.rst.exceptions
25− @$($(quiet)gen_rst)
26−
27−$(BUILDDIR)/dmx.h.rst: ${UAPI}/dvb/dmx.h ${PARSER} $(SRC_DIR)/dmx.h.rst.exceptions
28− @$($(quiet)gen_rst)
29−
30−$(BUILDDIR)/frontend.h.rst: ${UAPI}/dvb/frontend.h ${PARSER} $(SRC_DIR)/frontend.h.rst.exceptions
31− @$($(quiet)gen_rst)
32−
33−$(BUILDDIR)/net.h.rst: ${UAPI}/dvb/net.h ${PARSER} $(SRC_DIR)/net.h.rst.exceptions
34− @$($(quiet)gen_rst)
35−
36−$(BUILDDIR)/videodev2.h.rst: ${UAPI}/videodev2.h ${PARSER} $(SRC_DIR)/videodev2.h.rst.exceptions
37− @$($(quiet)gen_rst)
38−
39−$(BUILDDIR)/media.h.rst: ${UAPI}/media.h ${PARSER} $(SRC_DIR)/media.h.rst.exceptions
40− @$($(quiet)gen_rst)
41−
42−$(BUILDDIR)/cec.h.rst: ${UAPI}/cec.h ${PARSER} $(SRC_DIR)/cec.h.rst.exceptions
43− @$($(quiet)gen_rst)
44−
45−$(BUILDDIR)/lirc.h.rst: ${UAPI}/lirc.h ${PARSER} $(SRC_DIR)/lirc.h.rst.exceptions
46− @$($(quiet)gen_rst)
47−
48−# Media build rules
49−
50−.PHONY: all html texinfo epub xml latex
51−
52−all: $(IMGDOT) $(BUILDDIR) ${TARGETS}
53−html: all
54−texinfo: all
55−epub: all
56−xml: all
57−latex: $(IMGPDF) all
58−linkcheck:
59−
60−clean:
61− -rm -f $(DOTTGT) $(IMGTGT) ${TARGETS} 2>/dev/null
62−
63−$(BUILDDIR):
64− $(Q)mkdir -p $@
Documentation/userspace-api/media/cec/cec-header.rst+3−2
@@ -6,5 +6,6 @@ · 5 unmodified lines
66 CEC Header File
77 ***************
88
9−.. kernel-include:: $BUILDDIR/cec.h.rst
10−
9+.. kernel-include:: include/uapi/linux/cec.h
10+ :generate-cross-refs:
11+ :exception-file: cec.h.rst.exceptions
Documentation/userspace-api/media/cec/cec.h.rst.exceptionsrenamed from Documentation/userspace-api/media/cec.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/dvb/ca.h.rst.exceptionsrenamed from Documentation/userspace-api/media/ca.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/dvb/dmx.h.rst.exceptionsrenamed from Documentation/userspace-api/media/dmx.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/dvb/frontend.h.rst.exceptionsrenamed from Documentation/userspace-api/media/frontend.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/dvb/headers.rst+13−4
@@ -7,10 +7,19 @@ Digital TV uAPI header files · 6 unmodified lines
77 Digital TV uAPI headers
88 ***********************
99
10−.. kernel-include:: $BUILDDIR/frontend.h.rst
10+.. kernel-include:: include/uapi/linux/dvb/frontend.h
11+ :generate-cross-refs:
12+ :exception-file: frontend.h.rst.exceptions
1113
12−.. kernel-include:: $BUILDDIR/dmx.h.rst
14+.. kernel-include:: include/uapi/linux/dvb/dmx.h
15+ :generate-cross-refs:
16+ :exception-file: dmx.h.rst.exceptions
1317
14−.. kernel-include:: $BUILDDIR/ca.h.rst
18+.. kernel-include:: include/uapi/linux/dvb/ca.h
19+ :generate-cross-refs:
20+ :exception-file: ca.h.rst.exceptions
21+
22+.. kernel-include:: include/uapi/linux/dvb/net.h
23+ :generate-cross-refs:
24+ :exception-file: net.h.rst.exceptions
1525
16−.. kernel-include:: $BUILDDIR/net.h.rst
Documentation/userspace-api/media/dvb/net.h.rst.exceptionsrenamed from Documentation/userspace-api/media/net.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/mediactl/media-header.rst+3−2
@@ -6,5 +6,6 @@ · 5 unmodified lines
66 Media Controller Header File
77 ****************************
88
9−.. kernel-include:: $BUILDDIR/media.h.rst
10−
9+.. kernel-include:: include/uapi/linux/media.h
10+ :generate-cross-refs:
11+ :exception-file: media.h.rst.exceptions
Documentation/userspace-api/media/mediactl/media.h.rst.exceptionsrenamed from Documentation/userspace-api/media/media.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/rc/lirc-header.rst+3−1
@@ -6,5 +6,7 @@ · 5 unmodified lines
66 LIRC Header File
77 ****************
88
9−.. kernel-include:: $BUILDDIR/lirc.h.rst
9+.. kernel-include:: include/uapi/linux/lirc.h
10+ :generate-cross-refs:
11+ :exception-file: lirc.h.rst.exceptions
1012
Documentation/userspace-api/media/rc/lirc.h.rst.exceptionsrenamed from Documentation/userspace-api/media/lirc.h.rst.exceptions+0−0

File renamed without changes.

Documentation/userspace-api/media/v4l/videodev.rst+3−1
@@ -6,4 +6,6 @@ · 5 unmodified lines
66 Video For Linux Two Header File
77 *******************************
88
9−.. kernel-include:: $BUILDDIR/videodev2.h.rst
9+.. kernel-include:: include/uapi/linux/videodev2.h
10+ :generate-cross-refs:
11+ :exception-file: videodev2.h.rst.exceptions
Documentation/userspace-api/media/v4l/videodev2.h.rst.exceptionsrenamed from Documentation/userspace-api/media/videodev2.h.rst.exceptions+0−0

File renamed without changes.

scripts/sphinx-build-wrapper+719−0
@@ -0,0 +1,719 @@
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+"""
36+Sphinx 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+
42+This tool ensures that MIN_PYTHON_VERSION is satisfied. If version is
43+below that, it seeks for a new Python version. If found, it re-runs using
44+the newer version.
45+"""
46+
47+import argparse
48+import locale
49+import os
50+import re
51+import shlex
52+import shutil
53+import subprocess
54+import sys
55+
56+from concurrent import futures
57+from glob import glob
58+
59+LIB_DIR = "lib"
60+SRC_DIR = os.path.dirname(os.path.realpath(__file__))
61+
62+sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR))
63+
64+from jobserver import JobserverExec # pylint: disable=C0413
65+
66+
67+def parse_version(version):
68+ """Convert a major.minor.patch version into a tuple"""
69+ return tuple(int(x) for x in version.split("."))
70+
71+def 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
77+MIN_PYTHON_VERSION = parse_version("3.7")
78+
79+# Default value for --venv parameter
80+VENV_DEFAULT = "sphinx_latest"
81+
82+# List of make targets and its corresponding builder and output directory
83+TARGETS = {
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
120+PAPER = ["", "a4", "letter"]
121+
122+class 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+
654+def 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+
673+def 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+
718+if __name__ == "__main__":
719+ main()