xref: /linux/tools/lib/python/jobserver.py (revision 72c395024dac5e215136cbff793455f065603b06)
1fce6df7eSMauro Carvalho Chehab#!/usr/bin/env python3
2fce6df7eSMauro Carvalho Chehab# SPDX-License-Identifier: GPL-2.0+
3fce6df7eSMauro Carvalho Chehab#
4fce6df7eSMauro Carvalho Chehab# pylint: disable=C0103,C0209
5fce6df7eSMauro Carvalho Chehab#
6fce6df7eSMauro Carvalho Chehab#
7fce6df7eSMauro Carvalho Chehab
8fce6df7eSMauro Carvalho Chehab"""
9fce6df7eSMauro Carvalho ChehabInteracts with the POSIX jobserver during the Kernel build time.
10fce6df7eSMauro Carvalho Chehab
11fce6df7eSMauro Carvalho ChehabA "normal" jobserver task, like the one initiated by a make subrocess would do:
12fce6df7eSMauro Carvalho Chehab
13fce6df7eSMauro Carvalho Chehab    - open read/write file descriptors to communicate with the job server;
14*8b85f614SMauro Carvalho Chehab    - ask for one slot by calling::
15*8b85f614SMauro Carvalho Chehab
16fce6df7eSMauro Carvalho Chehab        claim = os.read(reader, 1)
17*8b85f614SMauro Carvalho Chehab
18*8b85f614SMauro Carvalho Chehab    - when the job finshes, call::
19*8b85f614SMauro Carvalho Chehab
20fce6df7eSMauro Carvalho Chehab        os.write(writer, b"+")  # os.write(writer, claim)
21fce6df7eSMauro Carvalho Chehab
22fce6df7eSMauro Carvalho ChehabHere, the goal is different: This script aims to get the remaining number
23fce6df7eSMauro Carvalho Chehabof slots available, using all of them to run a command which handle tasks in
24fce6df7eSMauro Carvalho Chehabparallel. To to that, it has a loop that ends only after there are no
25fce6df7eSMauro Carvalho Chehabslots left. It then increments the number by one, in order to allow a
26*8b85f614SMauro Carvalho Chehabcall equivalent to ``make -j$((claim+1))``, e.g. having a parent make creating
27fce6df7eSMauro Carvalho Chehab$claim child to do the actual work.
28fce6df7eSMauro Carvalho Chehab
29fce6df7eSMauro Carvalho ChehabThe end goal here is to keep the total number of build tasks under the
30*8b85f614SMauro Carvalho Chehablimit established by the initial ``make -j$n_proc`` call.
31fce6df7eSMauro Carvalho Chehab
32fce6df7eSMauro Carvalho ChehabSee:
33fce6df7eSMauro Carvalho Chehab    https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver
34fce6df7eSMauro Carvalho Chehab"""
35fce6df7eSMauro Carvalho Chehab
36fce6df7eSMauro Carvalho Chehabimport errno
37fce6df7eSMauro Carvalho Chehabimport os
38fce6df7eSMauro Carvalho Chehabimport subprocess
39fce6df7eSMauro Carvalho Chehabimport sys
40fce6df7eSMauro Carvalho Chehab
41b2664a90SJonathan Corbetdef warn(text, *args):
42b2664a90SJonathan Corbet    print(f'WARNING: {text}', *args, file = sys.stderr)
43b2664a90SJonathan Corbet
44fce6df7eSMauro Carvalho Chehabclass JobserverExec:
45fce6df7eSMauro Carvalho Chehab    """
46fce6df7eSMauro Carvalho Chehab    Claim all slots from make using POSIX Jobserver.
47fce6df7eSMauro Carvalho Chehab
48fce6df7eSMauro Carvalho Chehab    The main methods here are:
49*8b85f614SMauro Carvalho Chehab
50fce6df7eSMauro Carvalho Chehab    - open(): reserves all slots;
51fce6df7eSMauro Carvalho Chehab    - close(): method returns all used slots back to make;
52*8b85f614SMauro Carvalho Chehab    - run(): executes a command setting PARALLELISM=<available slots jobs + 1>.
53fce6df7eSMauro Carvalho Chehab    """
54fce6df7eSMauro Carvalho Chehab
55fce6df7eSMauro Carvalho Chehab    def __init__(self):
56*8b85f614SMauro Carvalho Chehab        """Initialize internal vars."""
57fce6df7eSMauro Carvalho Chehab        self.claim = 0
58fce6df7eSMauro Carvalho Chehab        self.jobs = b""
59fce6df7eSMauro Carvalho Chehab        self.reader = None
60fce6df7eSMauro Carvalho Chehab        self.writer = None
61fce6df7eSMauro Carvalho Chehab        self.is_open = False
62fce6df7eSMauro Carvalho Chehab
63fce6df7eSMauro Carvalho Chehab    def open(self):
64*8b85f614SMauro Carvalho Chehab        """Reserve all available slots to be claimed later on."""
65fce6df7eSMauro Carvalho Chehab
66fce6df7eSMauro Carvalho Chehab        if self.is_open:
67fce6df7eSMauro Carvalho Chehab            return
68b2664a90SJonathan Corbet        self.is_open = True  # We only try once
69b2664a90SJonathan Corbet        self.claim = None
70b2664a90SJonathan Corbet        #
71b2664a90SJonathan Corbet        # Check the make flags for "--jobserver=R,W"
72fce6df7eSMauro Carvalho Chehab        # Note that GNU Make has used --jobserver-fds and --jobserver-auth
73fce6df7eSMauro Carvalho Chehab        # so this handles all of them.
74b2664a90SJonathan Corbet        #
75b2664a90SJonathan Corbet        flags = os.environ.get('MAKEFLAGS', '')
76fce6df7eSMauro Carvalho Chehab        opts = [x for x in flags.split(" ") if x.startswith("--jobserver")]
77b2664a90SJonathan Corbet        if not opts:
78b2664a90SJonathan Corbet            return
79b2664a90SJonathan Corbet        #
80b2664a90SJonathan Corbet        # Separate out the provided file descriptors
81b2664a90SJonathan Corbet        #
82b2664a90SJonathan Corbet        split_opt = opts[-1].split('=', 1)
83b2664a90SJonathan Corbet        if len(split_opt) != 2:
84b2664a90SJonathan Corbet            warn('unparseable option:', opts[-1])
85b2664a90SJonathan Corbet            return
86b2664a90SJonathan Corbet        fds = split_opt[1]
87b2664a90SJonathan Corbet        #
88b2664a90SJonathan Corbet        # As of GNU Make 4.4, we'll be looking for a named pipe
89b2664a90SJonathan Corbet        # identified as fifo:path
90b2664a90SJonathan Corbet        #
91b2664a90SJonathan Corbet        if fds.startswith('fifo:'):
92b2664a90SJonathan Corbet            path = fds[len('fifo:'):]
93b2664a90SJonathan Corbet            try:
94fce6df7eSMauro Carvalho Chehab                self.reader = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
95fce6df7eSMauro Carvalho Chehab                self.writer = os.open(path, os.O_WRONLY)
96b2664a90SJonathan Corbet            except (OSError, IOError):
97b2664a90SJonathan Corbet                warn('unable to open jobserver pipe', path)
98b2664a90SJonathan Corbet                return
99b2664a90SJonathan Corbet        #
100b2664a90SJonathan Corbet        # Otherwise look for integer file-descriptor numbers.
101b2664a90SJonathan Corbet        #
102fce6df7eSMauro Carvalho Chehab        else:
103b2664a90SJonathan Corbet            split_fds = fds.split(',')
104b2664a90SJonathan Corbet            if len(split_fds) != 2:
105b2664a90SJonathan Corbet                warn('malformed jobserver file descriptors:', fds)
106b2664a90SJonathan Corbet                return
107b2664a90SJonathan Corbet            try:
108b2664a90SJonathan Corbet                self.reader = int(split_fds[0])
109b2664a90SJonathan Corbet                self.writer = int(split_fds[1])
110b2664a90SJonathan Corbet            except ValueError:
111b2664a90SJonathan Corbet                warn('non-integer jobserver file-descriptors:', fds)
112b2664a90SJonathan Corbet                return
113b2664a90SJonathan Corbet            try:
114b2664a90SJonathan Corbet                #
115fce6df7eSMauro Carvalho Chehab                # Open a private copy of reader to avoid setting nonblocking
116fce6df7eSMauro Carvalho Chehab                # on an unexpecting process with the same reader fd.
117b2664a90SJonathan Corbet                #
118b2664a90SJonathan Corbet                self.reader = os.open(f"/proc/self/fd/{self.reader}",
119fce6df7eSMauro Carvalho Chehab                                      os.O_RDONLY | os.O_NONBLOCK)
120b2664a90SJonathan Corbet            except (IOError, OSError) as e:
121b2664a90SJonathan Corbet                warn('Unable to reopen jobserver read-side pipe:', repr(e))
122b2664a90SJonathan Corbet                return
123b2664a90SJonathan Corbet        #
124b2664a90SJonathan Corbet        # OK, we have the channel to the job server; read out as many jobserver
125b2664a90SJonathan Corbet        # slots as possible.
126b2664a90SJonathan Corbet        #
127fce6df7eSMauro Carvalho Chehab        while True:
128fce6df7eSMauro Carvalho Chehab            try:
129fce6df7eSMauro Carvalho Chehab                slot = os.read(self.reader, 8)
130bbf8c67aSChangbin Du                if not slot:
131b2664a90SJonathan Corbet                    #
132b2664a90SJonathan Corbet                    # Something went wrong.  Clear self.jobs to avoid writing
133b2664a90SJonathan Corbet                    # weirdness back to the jobserver and give up.
134bbf8c67aSChangbin Du                    self.jobs = b""
135b2664a90SJonathan Corbet                    warn("unexpected empty token from jobserver;"
136b2664a90SJonathan Corbet                         " possible invalid '--jobserver-auth=' setting")
137b2664a90SJonathan Corbet                    self.claim = None
138b2664a90SJonathan Corbet                    return
139fce6df7eSMauro Carvalho Chehab            except (OSError, IOError) as e:
140b2664a90SJonathan Corbet                #
141b2664a90SJonathan Corbet                # If there is nothing more to read then we are done.
142b2664a90SJonathan Corbet                #
143fce6df7eSMauro Carvalho Chehab                if e.errno == errno.EWOULDBLOCK:
144fce6df7eSMauro Carvalho Chehab                    break
145b2664a90SJonathan Corbet                #
146b2664a90SJonathan Corbet                # Anything else says that something went weird; give back
147b2664a90SJonathan Corbet                # the jobs and give up.
148b2664a90SJonathan Corbet                #
149fce6df7eSMauro Carvalho Chehab                if self.jobs:
150fce6df7eSMauro Carvalho Chehab                    os.write(self.writer, self.jobs)
151b2664a90SJonathan Corbet                    self.claim = None
152b2664a90SJonathan Corbet                    warn('error reading from jobserver pipe', repr(e))
153b2664a90SJonathan Corbet                    return
154b2664a90SJonathan Corbet            self.jobs += slot
155b2664a90SJonathan Corbet        #
156fce6df7eSMauro Carvalho Chehab        # Add a bump for our caller's reserveration, since we're just going
157fce6df7eSMauro Carvalho Chehab        # to sit here blocked on our child.
158b2664a90SJonathan Corbet        #
159fce6df7eSMauro Carvalho Chehab        self.claim = len(self.jobs) + 1
160fce6df7eSMauro Carvalho Chehab
161fce6df7eSMauro Carvalho Chehab    def close(self):
162*8b85f614SMauro Carvalho Chehab        """Return all reserved slots to Jobserver."""
163fce6df7eSMauro Carvalho Chehab
164fce6df7eSMauro Carvalho Chehab        if not self.is_open:
165fce6df7eSMauro Carvalho Chehab            return
166fce6df7eSMauro Carvalho Chehab
167fce6df7eSMauro Carvalho Chehab        # Return all the reserved slots.
168fce6df7eSMauro Carvalho Chehab        if len(self.jobs):
169fce6df7eSMauro Carvalho Chehab            os.write(self.writer, self.jobs)
170fce6df7eSMauro Carvalho Chehab
171fce6df7eSMauro Carvalho Chehab        self.is_open = False
172fce6df7eSMauro Carvalho Chehab
173fce6df7eSMauro Carvalho Chehab    def __enter__(self):
174fce6df7eSMauro Carvalho Chehab        self.open()
175fce6df7eSMauro Carvalho Chehab        return self
176fce6df7eSMauro Carvalho Chehab
177fce6df7eSMauro Carvalho Chehab    def __exit__(self, exc_type, exc_value, exc_traceback):
178fce6df7eSMauro Carvalho Chehab        self.close()
179fce6df7eSMauro Carvalho Chehab
180fce6df7eSMauro Carvalho Chehab    def run(self, cmd, *args, **pwargs):
181fce6df7eSMauro Carvalho Chehab        """
182fce6df7eSMauro Carvalho Chehab        Run a command setting PARALLELISM env variable to the number of
183fce6df7eSMauro Carvalho Chehab        available job slots (claim) + 1, e.g. it will reserve claim slots
184fce6df7eSMauro Carvalho Chehab        to do the actual build work, plus one to monitor its children.
185fce6df7eSMauro Carvalho Chehab        """
186fce6df7eSMauro Carvalho Chehab        self.open()             # Ensure that self.claim is set
187fce6df7eSMauro Carvalho Chehab
188fce6df7eSMauro Carvalho Chehab        # We can only claim parallelism if there was a jobserver (i.e. a
189fce6df7eSMauro Carvalho Chehab        # top-level "-jN" argument) and there were no other failures. Otherwise
190fce6df7eSMauro Carvalho Chehab        # leave out the environment variable and let the child figure out what
191fce6df7eSMauro Carvalho Chehab        # is best.
192fce6df7eSMauro Carvalho Chehab        if self.claim:
193fce6df7eSMauro Carvalho Chehab            os.environ["PARALLELISM"] = str(self.claim)
194fce6df7eSMauro Carvalho Chehab
195fce6df7eSMauro Carvalho Chehab        return subprocess.call(cmd, *args, **pwargs)
196