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