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=R0903,R0912,R0913,R0914,R0917,C0301
6
7"""
8Install minimal supported requirements for different Sphinx versions
9and optionally test the build.
10"""
11
12import argparse
13import asyncio
14import os.path
15import shutil
16import sys
17import time
18import subprocess
19
20# Minimal python version supported by the building system.
21
22PYTHON = os.path.basename(sys.executable)
23
24min_python_bin = None
25
26for i in range(9, 13):
27 p = f"python3.{i}"
28 if shutil.which(p):
29 min_python_bin = p
30 break
31
32if not min_python_bin:
33 min_python_bin = PYTHON
34
35# Starting from 8.0, Python 3.9 is not supported anymore.
36PYTHON_VER_CHANGES = {(8, 0, 0): PYTHON}
37
38DEFAULT_VERSIONS_TO_TEST = [
39 (3, 4, 3), # Minimal supported version
40 (5, 3, 0), # CentOS Stream 9 / AlmaLinux 9
41 (6, 1, 1), # Debian 12
42 (7, 2, 1), # openSUSE Leap 15.6
43 (7, 2, 6), # Ubuntu 24.04 LTS
44 (7, 4, 7), # Ubuntu 24.10
45 (7, 3, 0), # openSUSE Tumbleweed
46 (8, 1, 3), # Fedora 42
47 (8, 2, 3) # Latest version - covers rolling distros
48]
49
50# Sphinx versions to be installed and their incremental requirements
51SPHINX_REQUIREMENTS = {
52 # Oldest versions we support for each package required by Sphinx 3.4.3
53 (3, 4, 3): {
54 "docutils": "0.16",
55 "alabaster": "0.7.12",
56 "babel": "2.8.0",
57 "certifi": "2020.6.20",
58 "docutils": "0.16",
59 "idna": "2.10",
60 "imagesize": "1.2.0",
61 "Jinja2": "2.11.2",
62 "MarkupSafe": "1.1.1",
63 "packaging": "20.4",
64 "Pygments": "2.6.1",
65 "PyYAML": "5.1",
66 "requests": "2.24.0",
67 "snowballstemmer": "2.0.0",
68 "sphinxcontrib-applehelp": "1.0.2",
69 "sphinxcontrib-devhelp": "1.0.2",
70 "sphinxcontrib-htmlhelp": "1.0.3",
71 "sphinxcontrib-jsmath": "1.0.1",
72 "sphinxcontrib-qthelp": "1.0.3",
73 "sphinxcontrib-serializinghtml": "1.1.4",
74 "urllib3": "1.25.9",
75 },
76
77 # Update package dependencies to a more modern base. The goal here
78 # is to avoid to many incremental changes for the next entries
79 (3, 5, 0): {
80 "alabaster": "0.7.13",
81 "babel": "2.17.0",
82 "certifi": "2025.6.15",
83 "idna": "3.10",
84 "imagesize": "1.4.1",
85 "packaging": "25.0",
86 "Pygments": "2.8.1",
87 "requests": "2.32.4",
88 "snowballstemmer": "3.0.1",
89 "sphinxcontrib-applehelp": "1.0.4",
90 "sphinxcontrib-htmlhelp": "2.0.1",
91 "sphinxcontrib-serializinghtml": "1.1.5",
92 "urllib3": "2.0.0",
93 },
94
95 # Starting from here, ensure all docutils versions are covered with
96 # supported Sphinx versions. Other packages are upgraded only when
97 # required by pip
98 (4, 0, 0): {
99 "PyYAML": "5.1",
100 },
101 (4, 1, 0): {
102 "docutils": "0.17",
103 "Pygments": "2.19.1",
104 "Jinja2": "3.0.3",
105 "MarkupSafe": "2.0",
106 },
107 (4, 3, 0): {},
108 (4, 4, 0): {},
109 (4, 5, 0): {
110 "docutils": "0.17.1",
111 },
112 (5, 0, 0): {},
113 (5, 1, 0): {},
114 (5, 2, 0): {
115 "docutils": "0.18",
116 "Jinja2": "3.1.2",
117 "MarkupSafe": "2.0",
118 "PyYAML": "5.3.1",
119 },
120 (5, 3, 0): {
121 "docutils": "0.18.1",
122 },
123 (6, 0, 0): {},
124 (6, 1, 0): {},
125 (6, 2, 0): {
126 "PyYAML": "5.4.1",
127 },
128 (7, 0, 0): {},
129 (7, 1, 0): {},
130 (7, 2, 0): {
131 "docutils": "0.19",
132 "PyYAML": "6.0.1",
133 "sphinxcontrib-serializinghtml": "1.1.9",
134 },
135 (7, 2, 6): {
136 "docutils": "0.20",
137 },
138 (7, 3, 0): {
139 "alabaster": "0.7.14",
140 "PyYAML": "6.0.1",
141 "tomli": "2.0.1",
142 },
143 (7, 4, 0): {
144 "docutils": "0.20.1",
145 "PyYAML": "6.0.1",
146 },
147 (8, 0, 0): {
148 "docutils": "0.21",
149 },
150 (8, 1, 0): {
151 "docutils": "0.21.1",
152 "PyYAML": "6.0.1",
153 "sphinxcontrib-applehelp": "1.0.7",
154 "sphinxcontrib-devhelp": "1.0.6",
155 "sphinxcontrib-htmlhelp": "2.0.6",
156 "sphinxcontrib-qthelp": "1.0.6",
157 },
158 (8, 2, 0): {
159 "docutils": "0.21.2",
160 "PyYAML": "6.0.1",
161 "sphinxcontrib-serializinghtml": "1.1.9",
162 },
163}
164
165
166class AsyncCommands:
167 """Excecute command synchronously"""
168
169 def __init__(self, fp=None):
170
171 self.stdout = None
172 self.stderr = None
173 self.output = None
174 self.fp = fp
175
176 def log(self, out, verbose, is_info=True):
177 out = out.removesuffix('\n')
178
179 if verbose:
180 if is_info:
181 print(out)
182 else:
183 print(out, file=sys.stderr)
184
185 if self.fp:
186 self.fp.write(out + "\n")
187
188 async def _read(self, stream, verbose, is_info):
189 """Ancillary routine to capture while displaying"""
190
191 while stream is not None:
192 line = await stream.readline()
193 if line:
194 out = line.decode("utf-8", errors="backslashreplace")
195 self.log(out, verbose, is_info)
196 if is_info:
197 self.stdout += out
198 else:
199 self.stderr += out
200 else:
201 break
202
203 async def run(self, cmd, capture_output=False, check=False,
204 env=None, verbose=True):
205
206 """
207 Execute an arbitrary command, handling errors.
208
209 Please notice that this class is not thread safe
210 """
211
212 self.stdout = ""
213 self.stderr = ""
214
215 self.log("$ " + " ".join(cmd), verbose)
216
217 proc = await asyncio.create_subprocess_exec(cmd[0],
218 *cmd[1:],
219 env=env,
220 stdout=asyncio.subprocess.PIPE,
221 stderr=asyncio.subprocess.PIPE)
222
223 # Handle input and output in realtime
224 await asyncio.gather(
225 self._read(proc.stdout, verbose, True),
226 self._read(proc.stderr, verbose, False),
227 )
228
229 await proc.wait()
230
231 if check and proc.returncode > 0:
232 raise subprocess.CalledProcessError(returncode=proc.returncode,
233 cmd=" ".join(cmd),
234 output=self.stdout,
235 stderr=self.stderr)
236
237 if capture_output:
238 if proc.returncode > 0:
239 self.log(f"Error {proc.returncode}", verbose=True, is_info=False)
240 return ""
241
242 return self.output
243
244 ret = subprocess.CompletedProcess(args=cmd,
245 returncode=proc.returncode,
246 stdout=self.stdout,
247 stderr=self.stderr)
248
249 return ret
250
251
252class SphinxVenv:
253 """
254 Installs Sphinx on one virtual env per Sphinx version with a minimal
255 set of dependencies, adjusting them to each specific version.
256 """
257
258 def __init__(self):
259 """Initialize instance variables"""
260
261 self.built_time = {}
262 self.first_run = True
263
264 async def _handle_version(self, args, fp,
265 cur_ver, cur_requirements, python_bin):
266 """Handle a single Sphinx version"""
267
268 cmd = AsyncCommands(fp)
269
270 ver = ".".join(map(str, cur_ver))
271
272 if not self.first_run and args.wait_input and args.build:
273 ret = input("Press Enter to continue or 'a' to abort: ").strip().lower()
274 if ret == "a":
275 print("Aborted.")
276 sys.exit()
277 else:
278 self.first_run = False
279
280 venv_dir = f"Sphinx_{ver}"
281 req_file = f"requirements_{ver}.txt"
282
283 cmd.log(f"\nSphinx {ver} with {python_bin}", verbose=True)
284
285 # Create venv
286 await cmd.run([python_bin, "-m", "venv", venv_dir],
287 verbose=args.verbose, check=True)
288 pip = os.path.join(venv_dir, "bin/pip")
289
290 # Create install list
291 reqs = []
292 for pkg, verstr in cur_requirements.items():
293 reqs.append(f"{pkg}=={verstr}")
294
295 reqs.append(f"Sphinx=={ver}")
296
297 await cmd.run([pip, "install"] + reqs, check=True, verbose=args.verbose)
298
299 # Freeze environment
300 result = await cmd.run([pip, "freeze"], verbose=False, check=True)
301
302 # Pip install succeeded. Write requirements file
303 if args.req_file:
304 with open(req_file, "w", encoding="utf-8") as fp:
305 fp.write(result.stdout)
306
307 if args.build:
308 start_time = time.time()
309
310 # Prepare a venv environment
311 env = os.environ.copy()
312 bin_dir = os.path.join(venv_dir, "bin")
313 env["PATH"] = bin_dir + ":" + env["PATH"]
314 env["VIRTUAL_ENV"] = venv_dir
315 if "PYTHONHOME" in env:
316 del env["PYTHONHOME"]
317
318 # Test doc build
319 await cmd.run(["make", "cleandocs"], env=env, check=True)
320 make = ["make"]
321
322 if args.output:
323 sphinx_build = os.path.realpath(f"{bin_dir}/sphinx-build")
324 make += [f"O={args.output}", f"SPHINXBUILD={sphinx_build}"]
325
326 if args.make_args:
327 make += args.make_args
328
329 make += args.targets
330
331 if args.verbose:
332 cmd.log(f". {bin_dir}/activate", verbose=True)
333 await cmd.run(make, env=env, check=True, verbose=True)
334 if args.verbose:
335 cmd.log("deactivate", verbose=True)
336
337 end_time = time.time()
338 elapsed_time = end_time - start_time
339 hours, minutes = divmod(elapsed_time, 3600)
340 minutes, seconds = divmod(minutes, 60)
341
342 hours = int(hours)
343 minutes = int(minutes)
344 seconds = int(seconds)
345
346 self.built_time[ver] = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
347
348 cmd.log(f"Finished doc build for Sphinx {ver}. Elapsed time: {self.built_time[ver]}", verbose=True)
349
350 async def run(self, args):
351 """
352 Navigate though multiple Sphinx versions, handling each of them
353 on a loop.
354 """
355
356 if args.log:
357 fp = open(args.log, "w", encoding="utf-8")
358 if not args.verbose:
359 args.verbose = False
360 else:
361 fp = None
362 if not args.verbose:
363 args.verbose = True
364
365 cur_requirements = {}
366 python_bin = min_python_bin
367
368 vers = set(SPHINX_REQUIREMENTS.keys()) | set(args.versions)
369
370 for cur_ver in sorted(vers):
371 if cur_ver in SPHINX_REQUIREMENTS:
372 new_reqs = SPHINX_REQUIREMENTS[cur_ver]
373 cur_requirements.update(new_reqs)
374
375 if cur_ver in PYTHON_VER_CHANGES: # pylint: disable=R1715
376 python_bin = PYTHON_VER_CHANGES[cur_ver]
377
378 if cur_ver not in args.versions:
379 continue
380
381 if args.min_version:
382 if cur_ver < args.min_version:
383 continue
384
385 if args.max_version:
386 if cur_ver > args.max_version:
387 break
388
389 await self._handle_version(args, fp, cur_ver, cur_requirements,
390 python_bin)
391
392 if args.build:
393 cmd = AsyncCommands(fp)
394 cmd.log("\nSummary:", verbose=True)
395 for ver, elapsed_time in sorted(self.built_time.items()):
396 cmd.log(f"\tSphinx {ver} elapsed time: {elapsed_time}",
397 verbose=True)
398
399 if fp:
400 fp.close()
401
402def parse_version(ver_str):
403 """Convert a version string into a tuple."""
404
405 return tuple(map(int, ver_str.split(".")))
406
407
408DEFAULT_VERS = " - "
409DEFAULT_VERS += "\n - ".join(map(lambda v: f"{v[0]}.{v[1]}.{v[2]}",
410 DEFAULT_VERSIONS_TO_TEST))
411
412SCRIPT = os.path.relpath(__file__)
413
414DESCRIPTION = f"""
415This tool allows creating Python virtual environments for different
416Sphinx versions that are supported by the Linux Kernel build system.
417
418Besides creating the virtual environment, it can also test building
419the documentation using "make htmldocs" (and/or other doc targets).
420
421If called without "--versions" argument, it covers the versions shipped
422on major distros, plus the lowest supported version:
423
424{DEFAULT_VERS}
425
426A typical usage is to run:
427
428 {SCRIPT} -m -l sphinx_builds.log
429
430This will create one virtual env for the default version set and run
431"make htmldocs" for each version, creating a log file with the
432excecuted commands on it.
433
434NOTE: The build time can be very long, specially on old versions. Also, there
435is a known bug with Sphinx version 6.0.x: each subprocess uses a lot of
436memory. That, together with "-jauto" may cause OOM killer to cause
437failures at the doc generation. To minimize the risk, you may use the
438"-a" command line parameter to constrain the built directories and/or
439reduce the number of threads from "-jauto" to, for instance, "-j4":
440
441 {SCRIPT} -m -V 6.0.1 -a "SPHINXDIRS=process" "SPHINXOPTS='-j4'"
442
443"""
444
445MAKE_TARGETS = [
446 "htmldocs",
447 "texinfodocs",
448 "infodocs",
449 "latexdocs",
450 "pdfdocs",
451 "epubdocs",
452 "xmldocs",
453]
454
455async def main():
456 """Main program"""
457
458 parser = argparse.ArgumentParser(description=DESCRIPTION,
459 formatter_class=argparse.RawDescriptionHelpFormatter)
460
461 ver_group = parser.add_argument_group("Version range options")
462
463 ver_group.add_argument('-V', '--versions', nargs="*",
464 default=DEFAULT_VERSIONS_TO_TEST,type=parse_version,
465 help='Sphinx versions to test')
466 ver_group.add_argument('--min-version', "--min", type=parse_version,
467 help='Sphinx minimal version')
468 ver_group.add_argument('--max-version', "--max", type=parse_version,
469 help='Sphinx maximum version')
470 ver_group.add_argument('-f', '--full', action='store_true',
471 help='Add all Sphinx (major,minor) supported versions to the version range')
472
473 build_group = parser.add_argument_group("Build options")
474
475 build_group.add_argument('-b', '--build', action='store_true',
476 help='Build documentation')
477 build_group.add_argument('-a', '--make-args', nargs="*",
478 help='extra arguments for make, like SPHINXDIRS=netlink/specs',
479 )
480 build_group.add_argument('-t', '--targets', nargs="+", choices=MAKE_TARGETS,
481 default=[MAKE_TARGETS[0]],
482 help="make build targets. Default: htmldocs.")
483 build_group.add_argument("-o", '--output',
484 help="output directory for the make O=OUTPUT")
485
486 other_group = parser.add_argument_group("Other options")
487
488 other_group.add_argument('-r', '--req-file', action='store_true',
489 help='write a requirements.txt file')
490 other_group.add_argument('-l', '--log',
491 help='Log command output on a file')
492 other_group.add_argument('-v', '--verbose', action='store_true',
493 help='Verbose all commands')
494 other_group.add_argument('-i', '--wait-input', action='store_true',
495 help='Wait for an enter before going to the next version')
496
497 args = parser.parse_args()
498
499 if not args.make_args:
500 args.make_args = []
501
502 sphinx_versions = sorted(list(SPHINX_REQUIREMENTS.keys()))
503
504 if args.full:
505 args.versions += list(SPHINX_REQUIREMENTS.keys())
506
507 venv = SphinxVenv()
508 await venv.run(args)
509
510
511# Call main method
512if __name__ == "__main__":
513 asyncio.run(main())