| 1 | #!/usr/bin/env python3 |
| 2 | """Run one command while holding this machine's Codewhale build lock. |
| 3 | |
| 4 | scripts/build-lock.py <lock-file> -- <command> [args...] |
| 5 | |
| 6 | Several agents share one checkout and one memory-constrained machine. Cargo's |
| 7 | own lock is per target directory, so builds into different target dirs still |
| 8 | run concurrently and exhaust memory. This holds an exclusive flock on a lock |
| 9 | file under the persistent cache root for the lifetime of the command, and |
| 10 | says who holds it while waiting. |
| 11 | |
| 12 | The command runs as a child, never via exec: a daemon it spawns (an sccache |
| 13 | server, say) must not inherit the descriptor and pin the lock after the build |
| 14 | exits. Nested invocations see CODEWHALE_BUILD_LOCK_HELD and run unlocked. |
| 15 | |
| 16 | Known limitation: advisory only. Cargo started outside scripts/dev-cargo.sh |
| 17 | does not take the lock, and on a platform without fcntl (Windows) the |
| 18 | command runs unlocked after a warning. |
| 19 | """ |
| 20 | |
| 21 | import os |
| 22 | import signal |
| 23 | import subprocess |
| 24 | import sys |
| 25 | import time |
| 26 | |
| 27 | |
| 28 | def main() -> int: |
| 29 | if len(sys.argv) < 4 or sys.argv[2] != "--": |
| 30 | print("usage: build-lock.py <lock-file> -- <command> [args...]", file=sys.stderr) |
| 31 | return 2 |
| 32 | lock_path, command = sys.argv[1], sys.argv[3:] |
| 33 | env = dict(os.environ, CODEWHALE_BUILD_LOCK_HELD="1") |
| 34 | try: |
| 35 | import fcntl |
| 36 | except ImportError: |
| 37 | print("build-lock: fcntl unavailable; running without the machine build lock", file=sys.stderr) |
| 38 | return subprocess.call(command, env=env) |
| 39 | |
| 40 | os.makedirs(os.path.dirname(lock_path) or ".", exist_ok=True) |
| 41 | fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600) |
| 42 | try: |
| 43 | fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| 44 | except BlockingIOError: |
| 45 | holder = os.pread(fd, 512, 0).decode(errors="replace").strip() or "holder unknown" |
| 46 | print( |
| 47 | f"build-lock: waiting for another Cargo build on this machine ({holder}); " |
| 48 | "set CODEWHALE_BUILD_LOCK=0 to skip", |
| 49 | file=sys.stderr, |
| 50 | flush=True, |
| 51 | ) |
| 52 | started = time.monotonic() |
| 53 | fcntl.flock(fd, fcntl.LOCK_EX) |
| 54 | print( |
| 55 | f"build-lock: acquired after {time.monotonic() - started:.0f}s", |
| 56 | file=sys.stderr, |
| 57 | flush=True, |
| 58 | ) |
| 59 | os.ftruncate(fd, 0) |
| 60 | summary = " ".join(command[:5]) |
| 61 | os.pwrite(fd, f"pid {os.getpid()} in {os.getcwd()}: {summary}\n".encode(), 0) |
| 62 | |
| 63 | child = subprocess.Popen(command, env=env) |
| 64 | for forwarded in (signal.SIGTERM, signal.SIGHUP): |
| 65 | signal.signal(forwarded, lambda signum, _frame: child.send_signal(signum)) |
| 66 | # SIGINT reaches the whole foreground process group already; only keep |
| 67 | # waiting so the lock outlives the child's own shutdown. |
| 68 | signal.signal(signal.SIGINT, signal.SIG_IGN) |
| 69 | status = child.wait() |
| 70 | return 128 - status if status < 0 else status |
| 71 | |
| 72 | |
| 73 | if __name__ == "__main__": |
| 74 | sys.exit(main()) |
| 75 |