返回 CodeWhale
build-lock.py
根目录 / scripts / build-lock.py
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
75 lines PYTHON