# SPDX-License-Identifier: GPL-3.0-or-later """Run a build with a temporary apt-cacher-ng process and persistent cache.""" import errno import os import random import signal import socket import subprocess import sys import tempfile import time from pathlib import Path WORK = Path("/work") BINARY = Path("/usr/sbin/apt-cacher-ng") CA_FILE = Path("/etc/ssl/certs/ca-certificates.crt") def stop(process): if process is None or process.poll() is not None: return process.terminate() try: process.wait(timeout=10) except subprocess.TimeoutExpired: process.kill() process.wait() def wait_ready(process, port, log): deadline = time.monotonic() + 10 while time.monotonic() < deadline: if process.poll() is not None: raise RuntimeError( "apt-cacher-ng exited during startup. Inspect " + str(log) ) try: with socket.create_connection(("127.0.0.1", port), timeout=0.2): return except OSError: time.sleep(0.05) raise RuntimeError("apt-cacher-ng did not start. Inspect " + str(log)) def cache_port(): # apt-cacher-ng 3.7.5 requires listener ports below 32768. for port in random.SystemRandom().sample(range(16000, 32000), 64): with socket.socket() as reservation: try: reservation.bind(("127.0.0.1", port)) except OSError as error: if error.errno == errno.EADDRINUSE: continue raise return port raise RuntimeError("No free loopback port for apt-cacher-ng.") def run_cached(command): if not BINARY.is_file(): raise FileNotFoundError("Run tools/plasma-next setup to install apt-cacher-ng.") cache = WORK / "apt-cache" logs = WORK / "records/apt-cache" for path in (cache, logs): if path.is_symlink(): raise ValueError("Cache directories must not be symlinks: " + str(path)) path.mkdir(parents=True, exist_ok=True) port = cache_port() proxy = None child = None log = logs / "daemon.log" with tempfile.TemporaryDirectory(prefix="apt-cache-", dir=WORK / "tmp") as temp: config = Path(temp) / "acng.conf" config.write_text( f"CacheDir: {cache}\n" f"LogDir: {logs}\n" f"Port: {port}\n" "BindAddress: 127.0.0.1\n" "ForeGround: 1\n" "AllowUserPorts: 80 443\n" f"SocketPath: {Path(temp) / 'cache.sock'}\n" f"CAfile: {CA_FILE}\n" ) with log.open("ab") as output: try: proxy = subprocess.Popen( [str(BINARY), "-c", temp], stdout=output, stderr=subprocess.STDOUT, ) wait_ready(proxy, port, log) env = { **os.environ, "PLASMA_NEXT_APT_CACHE_URL": f"http://127.0.0.1:{port}", } child = subprocess.Popen(command, env=env) status = child.wait() return status if status >= 0 else 128 - status finally: stop(child) stop(proxy) def interrupted(signum, frame): raise SystemExit(128 + signum) if __name__ == "__main__": if len(sys.argv) < 2: raise SystemExit("Supply the build command to run.") signal.signal(signal.SIGTERM, interrupted) signal.signal(signal.SIGINT, interrupted) raise SystemExit(run_cached(sys.argv[1:]))