1*778b8ebeSJonathan Corbet#!/usr/bin/env python3 2*778b8ebeSJonathan Corbet# SPDX-License-Identifier: GPL-2.0+ 3*778b8ebeSJonathan Corbet# 4*778b8ebeSJonathan Corbet# pylint: disable=C0103,C0209 5*778b8ebeSJonathan Corbet# 6*778b8ebeSJonathan Corbet# 7*778b8ebeSJonathan Corbet 8*778b8ebeSJonathan Corbet""" 9*778b8ebeSJonathan CorbetInteracts with the POSIX jobserver during the Kernel build time. 10*778b8ebeSJonathan Corbet 11*778b8ebeSJonathan CorbetA "normal" jobserver task, like the one initiated by a make subrocess would do: 12*778b8ebeSJonathan Corbet 13*778b8ebeSJonathan Corbet - open read/write file descriptors to communicate with the job server; 14*778b8ebeSJonathan Corbet - ask for one slot by calling: 15*778b8ebeSJonathan Corbet claim = os.read(reader, 1) 16*778b8ebeSJonathan Corbet - when the job finshes, call: 17*778b8ebeSJonathan Corbet os.write(writer, b"+") # os.write(writer, claim) 18*778b8ebeSJonathan Corbet 19*778b8ebeSJonathan CorbetHere, the goal is different: This script aims to get the remaining number 20*778b8ebeSJonathan Corbetof slots available, using all of them to run a command which handle tasks in 21*778b8ebeSJonathan Corbetparallel. To to that, it has a loop that ends only after there are no 22*778b8ebeSJonathan Corbetslots left. It then increments the number by one, in order to allow a 23*778b8ebeSJonathan Corbetcall equivalent to make -j$((claim+1)), e.g. having a parent make creating 24*778b8ebeSJonathan Corbet$claim child to do the actual work. 25*778b8ebeSJonathan Corbet 26*778b8ebeSJonathan CorbetThe end goal here is to keep the total number of build tasks under the 27*778b8ebeSJonathan Corbetlimit established by the initial make -j$n_proc call. 28*778b8ebeSJonathan Corbet 29*778b8ebeSJonathan CorbetSee: 30*778b8ebeSJonathan Corbet https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver 31*778b8ebeSJonathan Corbet""" 32*778b8ebeSJonathan Corbet 33*778b8ebeSJonathan Corbetimport errno 34*778b8ebeSJonathan Corbetimport os 35*778b8ebeSJonathan Corbetimport subprocess 36*778b8ebeSJonathan Corbetimport sys 37*778b8ebeSJonathan Corbet 38*778b8ebeSJonathan Corbetclass JobserverExec: 39*778b8ebeSJonathan Corbet """ 40*778b8ebeSJonathan Corbet Claim all slots from make using POSIX Jobserver. 41*778b8ebeSJonathan Corbet 42*778b8ebeSJonathan Corbet The main methods here are: 43*778b8ebeSJonathan Corbet - open(): reserves all slots; 44*778b8ebeSJonathan Corbet - close(): method returns all used slots back to make; 45*778b8ebeSJonathan Corbet - run(): executes a command setting PARALLELISM=<available slots jobs + 1> 46*778b8ebeSJonathan Corbet """ 47*778b8ebeSJonathan Corbet 48*778b8ebeSJonathan Corbet def __init__(self): 49*778b8ebeSJonathan Corbet """Initialize internal vars""" 50*778b8ebeSJonathan Corbet self.claim = 0 51*778b8ebeSJonathan Corbet self.jobs = b"" 52*778b8ebeSJonathan Corbet self.reader = None 53*778b8ebeSJonathan Corbet self.writer = None 54*778b8ebeSJonathan Corbet self.is_open = False 55*778b8ebeSJonathan Corbet 56*778b8ebeSJonathan Corbet def open(self): 57*778b8ebeSJonathan Corbet """Reserve all available slots to be claimed later on""" 58*778b8ebeSJonathan Corbet 59*778b8ebeSJonathan Corbet if self.is_open: 60*778b8ebeSJonathan Corbet return 61*778b8ebeSJonathan Corbet 62*778b8ebeSJonathan Corbet try: 63*778b8ebeSJonathan Corbet # Fetch the make environment options. 64*778b8ebeSJonathan Corbet flags = os.environ["MAKEFLAGS"] 65*778b8ebeSJonathan Corbet # Look for "--jobserver=R,W" 66*778b8ebeSJonathan Corbet # Note that GNU Make has used --jobserver-fds and --jobserver-auth 67*778b8ebeSJonathan Corbet # so this handles all of them. 68*778b8ebeSJonathan Corbet opts = [x for x in flags.split(" ") if x.startswith("--jobserver")] 69*778b8ebeSJonathan Corbet 70*778b8ebeSJonathan Corbet # Parse out R,W file descriptor numbers and set them nonblocking. 71*778b8ebeSJonathan Corbet # If the MAKEFLAGS variable contains multiple instances of the 72*778b8ebeSJonathan Corbet # --jobserver-auth= option, the last one is relevant. 73*778b8ebeSJonathan Corbet fds = opts[-1].split("=", 1)[1] 74*778b8ebeSJonathan Corbet 75*778b8ebeSJonathan Corbet # Starting with GNU Make 4.4, named pipes are used for reader 76*778b8ebeSJonathan Corbet # and writer. 77*778b8ebeSJonathan Corbet # Example argument: --jobserver-auth=fifo:/tmp/GMfifo8134 78*778b8ebeSJonathan Corbet _, _, path = fds.partition("fifo:") 79*778b8ebeSJonathan Corbet 80*778b8ebeSJonathan Corbet if path: 81*778b8ebeSJonathan Corbet self.reader = os.open(path, os.O_RDONLY | os.O_NONBLOCK) 82*778b8ebeSJonathan Corbet self.writer = os.open(path, os.O_WRONLY) 83*778b8ebeSJonathan Corbet else: 84*778b8ebeSJonathan Corbet self.reader, self.writer = [int(x) for x in fds.split(",", 1)] 85*778b8ebeSJonathan Corbet # Open a private copy of reader to avoid setting nonblocking 86*778b8ebeSJonathan Corbet # on an unexpecting process with the same reader fd. 87*778b8ebeSJonathan Corbet self.reader = os.open("/proc/self/fd/%d" % (self.reader), 88*778b8ebeSJonathan Corbet os.O_RDONLY | os.O_NONBLOCK) 89*778b8ebeSJonathan Corbet 90*778b8ebeSJonathan Corbet # Read out as many jobserver slots as possible 91*778b8ebeSJonathan Corbet while True: 92*778b8ebeSJonathan Corbet try: 93*778b8ebeSJonathan Corbet slot = os.read(self.reader, 8) 94*778b8ebeSJonathan Corbet self.jobs += slot 95*778b8ebeSJonathan Corbet except (OSError, IOError) as e: 96*778b8ebeSJonathan Corbet if e.errno == errno.EWOULDBLOCK: 97*778b8ebeSJonathan Corbet # Stop at the end of the jobserver queue. 98*778b8ebeSJonathan Corbet break 99*778b8ebeSJonathan Corbet # If something went wrong, give back the jobs. 100*778b8ebeSJonathan Corbet if self.jobs: 101*778b8ebeSJonathan Corbet os.write(self.writer, self.jobs) 102*778b8ebeSJonathan Corbet raise e 103*778b8ebeSJonathan Corbet 104*778b8ebeSJonathan Corbet # Add a bump for our caller's reserveration, since we're just going 105*778b8ebeSJonathan Corbet # to sit here blocked on our child. 106*778b8ebeSJonathan Corbet self.claim = len(self.jobs) + 1 107*778b8ebeSJonathan Corbet 108*778b8ebeSJonathan Corbet except (KeyError, IndexError, ValueError, OSError, IOError): 109*778b8ebeSJonathan Corbet # Any missing environment strings or bad fds should result in just 110*778b8ebeSJonathan Corbet # not being parallel. 111*778b8ebeSJonathan Corbet self.claim = None 112*778b8ebeSJonathan Corbet 113*778b8ebeSJonathan Corbet self.is_open = True 114*778b8ebeSJonathan Corbet 115*778b8ebeSJonathan Corbet def close(self): 116*778b8ebeSJonathan Corbet """Return all reserved slots to Jobserver""" 117*778b8ebeSJonathan Corbet 118*778b8ebeSJonathan Corbet if not self.is_open: 119*778b8ebeSJonathan Corbet return 120*778b8ebeSJonathan Corbet 121*778b8ebeSJonathan Corbet # Return all the reserved slots. 122*778b8ebeSJonathan Corbet if len(self.jobs): 123*778b8ebeSJonathan Corbet os.write(self.writer, self.jobs) 124*778b8ebeSJonathan Corbet 125*778b8ebeSJonathan Corbet self.is_open = False 126*778b8ebeSJonathan Corbet 127*778b8ebeSJonathan Corbet def __enter__(self): 128*778b8ebeSJonathan Corbet self.open() 129*778b8ebeSJonathan Corbet return self 130*778b8ebeSJonathan Corbet 131*778b8ebeSJonathan Corbet def __exit__(self, exc_type, exc_value, exc_traceback): 132*778b8ebeSJonathan Corbet self.close() 133*778b8ebeSJonathan Corbet 134*778b8ebeSJonathan Corbet def run(self, cmd, *args, **pwargs): 135*778b8ebeSJonathan Corbet """ 136*778b8ebeSJonathan Corbet Run a command setting PARALLELISM env variable to the number of 137*778b8ebeSJonathan Corbet available job slots (claim) + 1, e.g. it will reserve claim slots 138*778b8ebeSJonathan Corbet to do the actual build work, plus one to monitor its children. 139*778b8ebeSJonathan Corbet """ 140*778b8ebeSJonathan Corbet self.open() # Ensure that self.claim is set 141*778b8ebeSJonathan Corbet 142*778b8ebeSJonathan Corbet # We can only claim parallelism if there was a jobserver (i.e. a 143*778b8ebeSJonathan Corbet # top-level "-jN" argument) and there were no other failures. Otherwise 144*778b8ebeSJonathan Corbet # leave out the environment variable and let the child figure out what 145*778b8ebeSJonathan Corbet # is best. 146*778b8ebeSJonathan Corbet if self.claim: 147*778b8ebeSJonathan Corbet os.environ["PARALLELISM"] = str(self.claim) 148*778b8ebeSJonathan Corbet 149*778b8ebeSJonathan Corbet return subprocess.call(cmd, *args, **pwargs) 150