The GNU make jobserver Implementation (2015)
make.mad-scientist.net
make.mad-scientist.net
https://www.gnu.org/software/make/manual/html_node/POSIX-Job...
It's a bit more robust. For example when you have a Makefile like this:
target:
+python3 script.py $@
where `script.py` itself spawns a sub-process that is jobserver aware (like make, gcc, cargo, ...), you don't have to worry about whether the fds of the python process, needed for communicating with the jobserver, are inherited in the subprocess.Previously you had to run `subprocess.Popen(..., close_fds=False)` and make sure MAKEFLAGS was inherited.
Now you only have to ensure MAKEFLAGS is propagated, and that the sub-process can access the fifo by path (which is sometimes problematic, for example when running docker).
I'm at the point where if you're going to require users to install something, rather than a newer version of make, I'd rather require a more modern build tool and disregard make entirely.
If "close all FDs" is a workaround for other bugs, fix those bugs too.
Please don't let your children trash something you gave them just because they don't appreciate it yet.
The correct design would be to explicitly whitelist the FDs that the child process will inherit. This is exactly what Python does - its standard API to spawn subprocesses not only has close_fds=True set by default, it also has pass_fds where you list the ones that should not be closed.
FWIW I also don't see why the parent should care if the child closes its inherited descriptors, since it retains its own copy which remains open.
Really, the older I get and the more I see of the cmake/ninja/Bazel/whatever world... GNU Make was a better tool even 20 years ago.
The one thing I wish were possible (trivially) in make, and I understand why it's not, is the ability to define a dependency not on whether a file exists or not but on the result of executing a process.
For example, do I need to rebuild this docker container? The current method to determine that is to create a stamp file; create the docker container then `touch .docker-image-created.stamp`. Unfortunately, those stamps can become out of date if the docker container is removed (or old, etc.) so it leads to confusing situations where make's interpretation of the current state is separate from the reality, and in large complex projects where there are lots of interconnected parts, sometimes your only feasible route is to remove everything and completely rebuild from scratch.
There's also an inexplicable lack of debugging output insofar as printing what target make is currently running. It has a ton of debug output, but no option I can find that says "I am running this target right now", "okay I'm done this target now" without manually adding $(info ...) statements to your makefile.
Of course the project consists of multiple module which you should be able to build seperately, thus recursive calls are quite common amongst the makefiles.
First test with only a hand full of jobs (-j) already failed in the beginning, i could fix these quite fast (missing makefile targets).
Now i have the situation that on some build systems (with faster CPU) i still see races, where the same makfile target for a subproject runs at the same time and overwrites each others target files. On other build systems it works without any issue. However, ive still failed to reproduce the failure manually, it usually happens during automatic build invoked by jenkins or gitlab.
Is there a way to make "make" simulate those builds so one could tell where the cause for the races is in detail?
Is there a way to make "make" simulate those builds so one could tell where the cause for the races is in detail?
Have you tried? make —-dry-run:)
import asyncio
import os
import signal
import sys
async def get_stdin_reader():
loop = asyncio.get_running_loop()
reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(reader)
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
return reader
async def amain():
print(f'pid: {os.getpid()}')
sig_queue: asyncio.Queue[None] = asyncio.Queue()
def sig_handler() -> None:
sig_queue.put_nowait(None)
loop= asyncio.get_running_loop()
loop.add_signal_handler(signal.SIGUSR1, sig_handler)
reader = await get_stdin_reader()
task1 = loop.create_task(reader.read(1))
task2 = loop.create_task(sig_queue.get())
pending = {task1, task2}
while True:
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
if task1 in done:
task1 = loop.create_task(reader.read(1))
pending.add(task1)
print('got char on stdin')
if task2 in done:
task2 = loop.create_task(sig_queue.get())
pending.add(task2)
print('got signal')
asyncio.run(amain())Opting for a harder algorithm just to avoid a few lines of compatibility shims seems like very much the wrong tradeoff.
Very likely gmake works on non-posix platforms. Whether there is a requirement that the jobserver also works there I don't know.
It also needs to work on non-linux platforms. So a portable solution (or multiple non-portable ones) is needed.
edit: Oh, it uses `signal.set_wakeup_fd`, interesting.
https://docs.python.org/3/library/signal.html#signal.set_wak...
edit2: it looks like the fd comes from a self-socket, but yeah, it's the same approach. The function is even called `_make_self_pipe`.
https://github.com/python/cpython/blob/bf21e2160d1dc6869fb23...
These OS specific APIs are sometimes required and may make certain usecases a lot easier to implement correctly (or even at all), but a job server for a build system isn't one of those. The make jobs can be required to behave and stay in the process group the job server creates for them just like shell job control. Which can be done with just POSIX API. You just have to blow the dust from the relevant tombs the ancients left us (e.g. Advanced Programming in the UNIX Environment).
Refuse the temptation and don't make infrastructure tools like gmake depend OS specific APIs. Doing so would make the world a worse place for everyone else.
Even if you're going to implement OS-specific APIs you still need a general case for OSes that don't support those APIs or don't support them correctly. That means you still need to solve the original problem regardless, which then means that it doesn't actually save you time and energy to use those APIs but rather creates more development and maintenance work and not less.
If those APIs don't provide you a significant benefit (reliability, performance, etc.) then it's likely not worth implementing them at all if the general case works, and if it doesn't work you have to fix it anyway.
Make is supposed to be the interface, implementations should be able to leverage all the platform capabilities they need, because make is part of that platform.
Forcing lowest common denominator also makes world worse place, as does relying on single specific implementation.
AFAIK, make's jobserver protocol is much older than signalfd.
> June 16th, 2003 (updated September 26th, 2015)
so when it was originally written signalfd(2) would not be available on Linux until 2.6.22 was released in July 2007, 4 years later.
I continue to believe that make is and remains the superior technology. Everyone else does like one or two things better (usually just limited to the developer-facing configuration language) and everything else worse.
And if you're using xargs somewhere in the middle of a parallel build, having xargs -P respect the jobserver's limits would be a nice touch.