xref: /linux/scripts/jobserver-exec (revision 2a14f021210fcbc271591d4c592eb4adca6bf127)
1d8d2d382SMasahiro Yamada#!/usr/bin/env python3
251e46c7aSKees Cook# SPDX-License-Identifier: GPL-2.0+
351e46c7aSKees Cook#
4*2a14f021SMauro Carvalho Chehab# pylint: disable=C0103,C0209
5*2a14f021SMauro Carvalho Chehab#
651e46c7aSKees Cook# This determines how many parallel tasks "make" is expecting, as it is
751e46c7aSKees Cook# not exposed via an special variables, reserves them all, runs a subprocess
851e46c7aSKees Cook# with PARALLELISM environment variable set, and releases the jobs back again.
951e46c7aSKees Cook#
1051e46c7aSKees Cook# https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver
1151e46c7aSKees Cook
12*2a14f021SMauro Carvalho Chehab"""
13*2a14f021SMauro Carvalho ChehabInteracts with the POSIX jobserver during the Kernel build time.
14*2a14f021SMauro Carvalho Chehab
15*2a14f021SMauro Carvalho ChehabA "normal" jobserver task, like the one initiated by a make subrocess would do:
16*2a14f021SMauro Carvalho Chehab
17*2a14f021SMauro Carvalho Chehab    - open read/write file descriptors to communicate with the job server;
18*2a14f021SMauro Carvalho Chehab    - ask for one slot by calling:
19*2a14f021SMauro Carvalho Chehab        claim = os.read(reader, 1)
20*2a14f021SMauro Carvalho Chehab    - when the job finshes, call:
21*2a14f021SMauro Carvalho Chehab        os.write(writer, b"+")  # os.write(writer, claim)
22*2a14f021SMauro Carvalho Chehab
23*2a14f021SMauro Carvalho ChehabHere, the goal is different: This script aims to get the remaining number
24*2a14f021SMauro Carvalho Chehabof slots available, using all of them to run a command which handle tasks in
25*2a14f021SMauro Carvalho Chehabparallel. To to that, it has a loop that ends only after there are no
26*2a14f021SMauro Carvalho Chehabslots left. It then increments the number by one, in order to allow a
27*2a14f021SMauro Carvalho Chehabcall equivalent to make -j$((claim+1)), e.g. having a parent make creating
28*2a14f021SMauro Carvalho Chehab$claim child to do the actual work.
29*2a14f021SMauro Carvalho Chehab
30*2a14f021SMauro Carvalho ChehabThe end goal here is to keep the total number of build tasks under the
31*2a14f021SMauro Carvalho Chehablimit established by the initial make -j$n_proc call.
32*2a14f021SMauro Carvalho Chehab"""
33*2a14f021SMauro Carvalho Chehab
34*2a14f021SMauro Carvalho Chehabimport errno
35*2a14f021SMauro Carvalho Chehabimport os
36*2a14f021SMauro Carvalho Chehabimport subprocess
37*2a14f021SMauro Carvalho Chehabimport sys
38*2a14f021SMauro Carvalho Chehab
39*2a14f021SMauro Carvalho Chehab
40*2a14f021SMauro Carvalho Chehabclass JobserverExec:
41*2a14f021SMauro Carvalho Chehab    """
42*2a14f021SMauro Carvalho Chehab    Claim all slots from make using POSIX Jobserver.
43*2a14f021SMauro Carvalho Chehab
44*2a14f021SMauro Carvalho Chehab    The main methods here are:
45*2a14f021SMauro Carvalho Chehab    - open(): reserves all slots;
46*2a14f021SMauro Carvalho Chehab    - close(): method returns all used slots back to make;
47*2a14f021SMauro Carvalho Chehab    - run(): executes a command setting PARALLELISM=<available slots jobs + 1>
48*2a14f021SMauro Carvalho Chehab    """
49*2a14f021SMauro Carvalho Chehab
50*2a14f021SMauro Carvalho Chehab    def __init__(self):
51*2a14f021SMauro Carvalho Chehab        """Initialize internal vars"""
52*2a14f021SMauro Carvalho Chehab        self.claim = 0
53*2a14f021SMauro Carvalho Chehab        self.jobs = b""
54*2a14f021SMauro Carvalho Chehab        self.reader = None
55*2a14f021SMauro Carvalho Chehab        self.writer = None
56*2a14f021SMauro Carvalho Chehab        self.is_open = False
57*2a14f021SMauro Carvalho Chehab
58*2a14f021SMauro Carvalho Chehab    def open(self):
59*2a14f021SMauro Carvalho Chehab        """Reserve all available slots to be claimed later on"""
60*2a14f021SMauro Carvalho Chehab
61*2a14f021SMauro Carvalho Chehab        if self.is_open:
62*2a14f021SMauro Carvalho Chehab            return
63*2a14f021SMauro Carvalho Chehab
6451e46c7aSKees Cook        try:
6551e46c7aSKees Cook            # Fetch the make environment options.
66*2a14f021SMauro Carvalho Chehab            flags = os.environ["MAKEFLAGS"]
6751e46c7aSKees Cook            # Look for "--jobserver=R,W"
6851e46c7aSKees Cook            # Note that GNU Make has used --jobserver-fds and --jobserver-auth
6951e46c7aSKees Cook            # so this handles all of them.
7051e46c7aSKees Cook            opts = [x for x in flags.split(" ") if x.startswith("--jobserver")]
7151e46c7aSKees Cook
7251e46c7aSKees Cook            # Parse out R,W file descriptor numbers and set them nonblocking.
73f8f4dc76SMasahiro Yamada            # If the MAKEFLAGS variable contains multiple instances of the
74f8f4dc76SMasahiro Yamada            # --jobserver-auth= option, the last one is relevant.
75f8f4dc76SMasahiro Yamada            fds = opts[-1].split("=", 1)[1]
768c008988SMartin Liska
77*2a14f021SMauro Carvalho Chehab            # Starting with GNU Make 4.4, named pipes are used for reader
78*2a14f021SMauro Carvalho Chehab            # and writer.
798c008988SMartin Liska            # Example argument: --jobserver-auth=fifo:/tmp/GMfifo8134
80*2a14f021SMauro Carvalho Chehab            _, _, path = fds.partition("fifo:")
818c008988SMartin Liska
828c008988SMartin Liska            if path:
83*2a14f021SMauro Carvalho Chehab                self.reader = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
84*2a14f021SMauro Carvalho Chehab                self.writer = os.open(path, os.O_WRONLY)
858c008988SMartin Liska            else:
86*2a14f021SMauro Carvalho Chehab                self.reader, self.writer = [int(x) for x in fds.split(",", 1)]
8751e46c7aSKees Cook                # Open a private copy of reader to avoid setting nonblocking
8851e46c7aSKees Cook                # on an unexpecting process with the same reader fd.
89*2a14f021SMauro Carvalho Chehab                self.reader = os.open("/proc/self/fd/%d" % (self.reader),
9051e46c7aSKees Cook                                      os.O_RDONLY | os.O_NONBLOCK)
9151e46c7aSKees Cook
92*2a14f021SMauro Carvalho Chehab            # Read out as many jobserver slots as possible
9351e46c7aSKees Cook            while True:
9451e46c7aSKees Cook                try:
95*2a14f021SMauro Carvalho Chehab                    slot = os.read(self.reader, 8)
96*2a14f021SMauro Carvalho Chehab                    self.jobs += slot
9751e46c7aSKees Cook                except (OSError, IOError) as e:
9851e46c7aSKees Cook                    if e.errno == errno.EWOULDBLOCK:
9951e46c7aSKees Cook                        # Stop at the end of the jobserver queue.
10051e46c7aSKees Cook                        break
10151e46c7aSKees Cook                    # If something went wrong, give back the jobs.
102*2a14f021SMauro Carvalho Chehab                    if self.jobs:
103*2a14f021SMauro Carvalho Chehab                        os.write(self.writer, self.jobs)
10451e46c7aSKees Cook                    raise e
105*2a14f021SMauro Carvalho Chehab
10651e46c7aSKees Cook            # Add a bump for our caller's reserveration, since we're just going
10751e46c7aSKees Cook            # to sit here blocked on our child.
108*2a14f021SMauro Carvalho Chehab            self.claim = len(self.jobs) + 1
109*2a14f021SMauro Carvalho Chehab
110*2a14f021SMauro Carvalho Chehab        except (KeyError, IndexError, ValueError, OSError, IOError):
11151e46c7aSKees Cook            # Any missing environment strings or bad fds should result in just
11251e46c7aSKees Cook            # not being parallel.
113*2a14f021SMauro Carvalho Chehab            self.claim = None
11451e46c7aSKees Cook
115*2a14f021SMauro Carvalho Chehab        self.is_open = True
11651e46c7aSKees Cook
117*2a14f021SMauro Carvalho Chehab    def close(self):
118*2a14f021SMauro Carvalho Chehab        """Return all reserved slots to Jobserver"""
119*2a14f021SMauro Carvalho Chehab
120*2a14f021SMauro Carvalho Chehab        if not self.is_open:
121*2a14f021SMauro Carvalho Chehab            return
12251e46c7aSKees Cook
12351e46c7aSKees Cook        # Return all the reserved slots.
124*2a14f021SMauro Carvalho Chehab        if len(self.jobs):
125*2a14f021SMauro Carvalho Chehab            os.write(self.writer, self.jobs)
12651e46c7aSKees Cook
127*2a14f021SMauro Carvalho Chehab        self.is_open = False
128*2a14f021SMauro Carvalho Chehab
129*2a14f021SMauro Carvalho Chehab    def __enter__(self):
130*2a14f021SMauro Carvalho Chehab        self.open()
131*2a14f021SMauro Carvalho Chehab        return self
132*2a14f021SMauro Carvalho Chehab
133*2a14f021SMauro Carvalho Chehab    def __exit__(self, exc_type, exc_value, exc_traceback):
134*2a14f021SMauro Carvalho Chehab        self.close()
135*2a14f021SMauro Carvalho Chehab
136*2a14f021SMauro Carvalho Chehab    def run(self, cmd):
137*2a14f021SMauro Carvalho Chehab        """
138*2a14f021SMauro Carvalho Chehab        Run a command setting PARALLELISM env variable to the number of
139*2a14f021SMauro Carvalho Chehab        available job slots (claim) + 1, e.g. it will reserve claim slots
140*2a14f021SMauro Carvalho Chehab        to do the actual build work, plus one to monitor its children.
141*2a14f021SMauro Carvalho Chehab        """
142*2a14f021SMauro Carvalho Chehab        self.open()             # Ensure that self.claim is set
143*2a14f021SMauro Carvalho Chehab
144*2a14f021SMauro Carvalho Chehab        # We can only claim parallelism if there was a jobserver (i.e. a
145*2a14f021SMauro Carvalho Chehab        # top-level "-jN" argument) and there were no other failures. Otherwise
146*2a14f021SMauro Carvalho Chehab        # leave out the environment variable and let the child figure out what
147*2a14f021SMauro Carvalho Chehab        # is best.
148*2a14f021SMauro Carvalho Chehab        if self.claim:
149*2a14f021SMauro Carvalho Chehab            os.environ["PARALLELISM"] = str(self.claim)
150*2a14f021SMauro Carvalho Chehab
151*2a14f021SMauro Carvalho Chehab        return subprocess.call(cmd)
152*2a14f021SMauro Carvalho Chehab
153*2a14f021SMauro Carvalho Chehab
154*2a14f021SMauro Carvalho Chehabdef main():
155*2a14f021SMauro Carvalho Chehab    """Main program"""
156*2a14f021SMauro Carvalho Chehab    with JobserverExec() as jobserver:
157*2a14f021SMauro Carvalho Chehab        jobserver.run(sys.argv[1:])
158*2a14f021SMauro Carvalho Chehab
159*2a14f021SMauro Carvalho Chehab
160*2a14f021SMauro Carvalho Chehabif __name__ == "__main__":
161*2a14f021SMauro Carvalho Chehab    main()
162