"""Serialize writers to an existing CMake build tree, including inherited children.""" from __future__ import annotations import argparse import fcntl import json import os from pathlib import Path import subprocess import sys import time def cmake_build_tree(command: list[str], cwd: Path) -> Path | None: if len(command) < 3 or Path(command[0]).name != "cmake" or command[1] != "--build": return None return (cwd / command[2]).resolve() def run_build(command: list[str], *, cwd: Path, env: dict | None = None, timeout: float = 1800, **kwargs) -> subprocess.CompletedProcess: tree = cmake_build_tree(command, cwd) if tree is None: return subprocess.run(command, cwd=cwd, env=env, timeout=timeout, **kwargs) started = time.monotonic() # Do not create a misspelled build directory just to acquire its lock. with (tree / ".freecad-web-build.lock").open("a") as lock: waited = False while True: try: fcntl.flock(lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) break except BlockingIOError: if not waited: print(json.dumps({"stage": "build-lock-wait", "tree": str(tree)}), file=sys.stderr, flush=True) waited = True if time.monotonic() - started >= timeout: raise subprocess.TimeoutExpired(command, timeout, stderr="timed out waiting for the build-tree writer lock") time.sleep(min(0.05, max(0.001, timeout - (time.monotonic() - started)))) delay = time.monotonic() - started if waited: print(json.dumps({"stage": "build-lock-acquired", "tree": str(tree), "waitSeconds": round(delay, 3)}), file=sys.stderr, flush=True) descriptors = (*kwargs.pop("pass_fds", ()), lock.fileno()) # Children retain the lock if an outer runner exits before its linker. return subprocess.run(command, cwd=cwd, env=env, timeout=max(0.001, timeout - delay), pass_fds=descriptors, **kwargs) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--timeout", type=float, default=1800) parser.add_argument("command", nargs=argparse.REMAINDER) args = parser.parse_args() command = args.command[1:] if args.command[:1] == ["--"] else args.command if cmake_build_tree(command, Path.cwd()) is None: parser.error("expected: -- cmake --build [build options]") try: return run_build(command, cwd=Path.cwd(), env=os.environ.copy(), timeout=args.timeout).returncode except subprocess.TimeoutExpired as error: print(str(error), file=sys.stderr) return 124 if __name__ == "__main__": raise SystemExit(main())