summaryrefslogtreecommitdiff
path: root/scripts/lib/jobserver.py
diff options
context:
space:
mode:
authorJonathan Corbet <corbet@lwn.net>2025-10-17 14:11:30 -0600
committerJonathan Corbet <corbet@lwn.net>2025-10-17 14:11:30 -0600
commit3df5affb4be217b161d21a76c5763417d1cf743b (patch)
tree4b9999ece087239bdff2eedf1739f2011748e707 /scripts/lib/jobserver.py
parentd0841b8761da8c8d8681b452c3899658fdfb9ca6 (diff)
parente123e00a5872756644154f5ad8db2efbd1abdfca (diff)
Merge branch 'build-script' into docs-mw
Quoth Mauro: This series should probably be called: "Move the trick-or-treat build hacks accumulated over time into a single place and document them." as this reflects its main goal. As such: - it places the jobserver logic on a library; - it removes sphinx/parallel-wrapper.sh; - the code now properly implements a jobserver-aware logic to do the parallelism when called via GNU make, failing back to "-j" when there's no jobserver; - converts check-variable-fonts.sh to Python and uses it via function call; - drops an extra script to generate man pages, adding a makefile target for it; - ensures that return code is 0 when PDF successfully builds; - about half of the script is comments and documentation. I tried to do my best to document all tricks that are inside the script. This way, the docs build steps is now documented. It should be noticed that it is out of the scope of this series to change the implementation. Surely the process can be improved, but first let's consolidate and document everything on a single place. Such script was written in a way that it can be called either directly or via a Makefile. Running outside Makefile is interesting specially when debug is needed. The command line interface replaces the need of having lots of env vars before calling sphinx-build: $ ./tools/docs/sphinx-build-wrapper --help usage: sphinx-build-wrapper [-h] [--sphinxdirs SPHINXDIRS [SPHINXDIRS ...]] [--conf CONF] [--builddir BUILDDIR] [--theme THEME] [--css CSS] [--paper {,a4,letter}] [-v] [-j JOBS] [-i] [-V [VENV]] {cleandocs,linkcheckdocs,htmldocs,epubdocs,texinfodocs,infodocs,mandocs,latexdocs,pdfdocs,xmldocs} Kernel documentation builder positional arguments: {cleandocs,linkcheckdocs,htmldocs,epubdocs,texinfodocs,infodocs,mandocs,latexdocs,pdfdocs,xmldocs} Documentation target to build options: -h, --help show this help message and exit --sphinxdirs SPHINXDIRS [SPHINXDIRS ...] Specific directories to build --conf CONF Sphinx configuration file --builddir BUILDDIR Sphinx configuration file --theme THEME Sphinx theme to use --css CSS Custom CSS file for HTML/EPUB --paper {,a4,letter} Paper size for LaTeX/PDF output -v, --verbose place build in verbose mode -j, --jobs JOBS Sets number of jobs to use with sphinx-build -i, --interactive Change latex default to run in interactive mode -V, --venv [VENV] If used, run Sphinx from a venv dir (default dir: sphinx_latest) the only mandatory argument is the target, which is identical with "make" targets. The call inside Makefile doesn't use the last four arguments. They're there to help identifying problems at the build: -v makes the output verbose; -j helps to test parallelism; -i runs latexmk in interactive mode, allowing to debug PDF build issues; -V is useful when testing it with different venvs. When used with GNU make (or some other make which implements jobserver), a call like: make -j <targets> htmldocs will make the wrapper to automatically use POSIX jobserver to claim the number of available job slots, calling sphinx-build with a "-j" parameter reflecting it. ON such case, the default can be overriden via SPHINXDIRS argument. Visiable changes when compared with the old behavior: When V=0, the only visible difference is that: - pdfdocs target now returns 0 on success, 1 on failures. This addresses an issue over the current process where we it always return success even on failures; - it will now print the name of PDF files that failed to build, if any. In verbose mode, sphinx-build-wrapper and sphinx-build command lines are now displayed.
Diffstat (limited to 'scripts/lib/jobserver.py')
-rwxr-xr-xscripts/lib/jobserver.py149
1 files changed, 149 insertions, 0 deletions
diff --git a/scripts/lib/jobserver.py b/scripts/lib/jobserver.py
new file mode 100755
index 000000000000..a24f30ef4fa8
--- /dev/null
+++ b/scripts/lib/jobserver.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0+
+#
+# pylint: disable=C0103,C0209
+#
+#
+
+"""
+Interacts with the POSIX jobserver during the Kernel build time.
+
+A "normal" jobserver task, like the one initiated by a make subrocess would do:
+
+ - open read/write file descriptors to communicate with the job server;
+ - ask for one slot by calling:
+ claim = os.read(reader, 1)
+ - when the job finshes, call:
+ os.write(writer, b"+") # os.write(writer, claim)
+
+Here, the goal is different: This script aims to get the remaining number
+of slots available, using all of them to run a command which handle tasks in
+parallel. To to that, it has a loop that ends only after there are no
+slots left. It then increments the number by one, in order to allow a
+call equivalent to make -j$((claim+1)), e.g. having a parent make creating
+$claim child to do the actual work.
+
+The end goal here is to keep the total number of build tasks under the
+limit established by the initial make -j$n_proc call.
+
+See:
+ https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver
+"""
+
+import errno
+import os
+import subprocess
+import sys
+
+class JobserverExec:
+ """
+ Claim all slots from make using POSIX Jobserver.
+
+ The main methods here are:
+ - open(): reserves all slots;
+ - close(): method returns all used slots back to make;
+ - run(): executes a command setting PARALLELISM=<available slots jobs + 1>
+ """
+
+ def __init__(self):
+ """Initialize internal vars"""
+ self.claim = 0
+ self.jobs = b""
+ self.reader = None
+ self.writer = None
+ self.is_open = False
+
+ def open(self):
+ """Reserve all available slots to be claimed later on"""
+
+ if self.is_open:
+ return
+
+ try:
+ # Fetch the make environment options.
+ flags = os.environ["MAKEFLAGS"]
+ # Look for "--jobserver=R,W"
+ # Note that GNU Make has used --jobserver-fds and --jobserver-auth
+ # so this handles all of them.
+ opts = [x for x in flags.split(" ") if x.startswith("--jobserver")]
+
+ # Parse out R,W file descriptor numbers and set them nonblocking.
+ # If the MAKEFLAGS variable contains multiple instances of the
+ # --jobserver-auth= option, the last one is relevant.
+ fds = opts[-1].split("=", 1)[1]
+
+ # Starting with GNU Make 4.4, named pipes are used for reader
+ # and writer.
+ # Example argument: --jobserver-auth=fifo:/tmp/GMfifo8134
+ _, _, path = fds.partition("fifo:")
+
+ if path:
+ self.reader = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
+ self.writer = os.open(path, os.O_WRONLY)
+ else:
+ self.reader, self.writer = [int(x) for x in fds.split(",", 1)]
+ # Open a private copy of reader to avoid setting nonblocking
+ # on an unexpecting process with the same reader fd.
+ self.reader = os.open("/proc/self/fd/%d" % (self.reader),
+ os.O_RDONLY | os.O_NONBLOCK)
+
+ # Read out as many jobserver slots as possible
+ while True:
+ try:
+ slot = os.read(self.reader, 8)
+ self.jobs += slot
+ except (OSError, IOError) as e:
+ if e.errno == errno.EWOULDBLOCK:
+ # Stop at the end of the jobserver queue.
+ break
+ # If something went wrong, give back the jobs.
+ if self.jobs:
+ os.write(self.writer, self.jobs)
+ raise e
+
+ # Add a bump for our caller's reserveration, since we're just going
+ # to sit here blocked on our child.
+ self.claim = len(self.jobs) + 1
+
+ except (KeyError, IndexError, ValueError, OSError, IOError):
+ # Any missing environment strings or bad fds should result in just
+ # not being parallel.
+ self.claim = None
+
+ self.is_open = True
+
+ def close(self):
+ """Return all reserved slots to Jobserver"""
+
+ if not self.is_open:
+ return
+
+ # Return all the reserved slots.
+ if len(self.jobs):
+ os.write(self.writer, self.jobs)
+
+ self.is_open = False
+
+ def __enter__(self):
+ self.open()
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_traceback):
+ self.close()
+
+ def run(self, cmd, *args, **pwargs):
+ """
+ Run a command setting PARALLELISM env variable to the number of
+ available job slots (claim) + 1, e.g. it will reserve claim slots
+ to do the actual build work, plus one to monitor its children.
+ """
+ self.open() # Ensure that self.claim is set
+
+ # We can only claim parallelism if there was a jobserver (i.e. a
+ # top-level "-jN" argument) and there were no other failures. Otherwise
+ # leave out the environment variable and let the child figure out what
+ # is best.
+ if self.claim:
+ os.environ["PARALLELISM"] = str(self.claim)
+
+ return subprocess.call(cmd, *args, **pwargs)