Files
GameNetworkingSockets/.github/run-single-config.py
T
2026-05-03 22:34:19 -07:00

218 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Build and test a single GameNetworkingSockets configuration."""
from __future__ import annotations
import argparse
import os
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Configure/build/test one explicit configuration.")
parser.add_argument("--compiler", choices=["gcc", "clang"], required=True)
parser.add_argument("--build-dir", required=True)
parser.add_argument("--build-type", default="RelWithDebInfo")
parser.add_argument("--sanitizer", choices=["none", "asan", "ubsan", "tsan"], default="none")
parser.add_argument("--generator", default="Ninja")
parser.add_argument("--use-webrtc", action="store_true")
parser.add_argument("--lto", action="store_true")
parser.add_argument("--crypto", choices=["default", "libsodium"], default="default")
parser.add_argument("--crypto25519", choices=["default", "Reference", "libsodium"], default="default")
parser.add_argument("--run-tests", action="store_true")
parser.add_argument(
"--tests",
nargs="*",
default=None,
help="Test command specs like test_crypto or test_connection:suite-quick",
)
parser.add_argument("--targets", nargs="*", default=[])
parser.add_argument("--phase", choices=["configure", "build", "test", "all"], default="all")
parser.add_argument("--no-clean", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--cmake-arg", action="append", default=[])
return parser.parse_args()
def shell_join(cmd: list[str]) -> str:
return shlex.join(cmd)
def run_cmd(cmd: list[str], env: dict[str, str], cwd: Path, dry_run: bool) -> None:
print(f"[cmd] {shell_join(cmd)}", flush=True)
if dry_run:
return
subprocess.run(cmd, env=env, cwd=str(cwd), check=True)
def compiler_env(compiler: str, base_env: dict[str, str]) -> dict[str, str]:
env = dict(base_env)
if compiler == "gcc":
env["CC"] = "gcc"
env["CXX"] = "g++"
else:
env["CC"] = "clang"
env["CXX"] = "clang++"
return env
def parse_test_specs(specs: list[str] | None) -> list[list[str]]:
if specs is None:
specs = ["test_crypto", "test_connection:suite-quick"]
commands: list[list[str]] = []
for spec in specs:
parts = spec.split(":")
exe = parts[0]
args = parts[1:]
commands.append([exe, *args])
return commands
def main() -> int:
# In CI, stdout is usually not a TTY, so force line-buffering for timely logs.
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(line_buffering=True)
args = parse_args()
repo_root = Path(__file__).resolve().parents[1]
env = compiler_env(args.compiler, os.environ)
if args.sanitizer == "tsan":
supp = repo_root / "tests" / "tsan.supp"
env["TSAN_OPTIONS"] = f"suppressions={supp}"
cmake_args = [
"cmake",
"-S",
".",
"-B",
args.build_dir,
"-G",
args.generator,
"-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",
"-DCMAKE_C_COMPILER_LAUNCHER=ccache",
"-DBUILD_TESTS=ON",
"-DBUILD_EXAMPLES=ON",
"-DENABLE_ICE=ON",
"-DWERROR=ON",
f"-DCMAKE_BUILD_TYPE={args.build_type}",
]
if args.sanitizer == "asan":
cmake_args.append("-DSANITIZE_ADDRESS:BOOL=ON")
elif args.sanitizer == "ubsan":
cmake_args.append("-DSANITIZE_UNDEFINED:BOOL=ON")
elif args.sanitizer == "tsan":
cmake_args.append("-DSANITIZE_THREAD:BOOL=ON")
if args.use_webrtc:
cmake_args.append("-DUSE_STEAMWEBRTC=ON")
if args.lto:
cmake_args.append("-DLTO=ON")
if args.crypto != "default":
cmake_args.append(f"-DUSE_CRYPTO={args.crypto}")
if args.crypto25519 != "default":
cmake_args.append(f"-DUSE_CRYPTO25519={args.crypto25519}")
cmake_args.extend(args.cmake_arg)
build_cmd = ["cmake", "--build", args.build_dir, "--", "-v"]
if args.targets:
build_cmd.extend(args.targets)
test_cmds: list[list[str]] = []
test_cwd = repo_root / args.build_dir / "bin"
if args.run_tests:
for test_parts in parse_test_specs(args.tests):
test_prog = test_parts[0]
test_args = test_parts[1:]
test_path = str((test_cwd / test_prog).resolve())
if test_prog.endswith(".py"):
test_cmds.append(["python3", test_path, *test_args])
else:
test_cmds.append([test_path, *test_args])
# If ccache isn't available in a local environment, remove launcher flags.
if shutil.which("ccache") is None:
cmake_args = [
arg
for arg in cmake_args
if not arg.startswith("-DCMAKE_CXX_COMPILER_LAUNCHER=")
and not arg.startswith("-DCMAKE_C_COMPILER_LAUNCHER=")
]
repro_cmd = ["python3", ".github/run-single-config.py", *sys.argv[1:]]
print("\n=== Single Config Build ===")
print(f"repo_root = {repo_root}")
print(f"compiler = {args.compiler} (CC={env['CC']} CXX={env['CXX']})")
print(f"build_dir = {args.build_dir}")
print(f"build_type = {args.build_type}")
print(f"sanitizer = {args.sanitizer}")
print(f"use_webrtc = {int(args.use_webrtc)}")
print(f"lto = {int(args.lto)}")
print(f"crypto = {args.crypto}")
print(f"crypto25519 = {args.crypto25519}")
print(f"phase = {args.phase}")
print(f"test_cwd = {test_cwd}")
print(f"targets = {' '.join(args.targets) if args.targets else '(all default targets)'}")
print(f"run_tests = {int(args.run_tests)}")
if args.run_tests:
print("tests = " + ", ".join(shell_join(x) for x in test_cmds))
print("\nRepro command:")
print(shell_join(repro_cmd))
print("\nConfigure command:")
print(shell_join(cmake_args))
print("\nBuild command:")
print(shell_join(build_cmd))
if test_cmds:
print("\nTest commands:")
for cmd in test_cmds:
print(shell_join(cmd))
if args.no_clean and args.phase in ("configure", "all"):
pass
elif args.phase in ("configure", "all"):
build_dir = repo_root / args.build_dir
if build_dir.exists() and not args.dry_run:
shutil.rmtree(build_dir)
try:
if args.phase in ("configure", "all"):
run_cmd(cmake_args, env=env, cwd=repo_root, dry_run=args.dry_run)
if args.phase in ("build", "all"):
run_cmd(build_cmd, env=env, cwd=repo_root, dry_run=args.dry_run)
if args.phase in ("test", "all"):
for cmd in test_cmds:
run_cmd(cmd, env=env, cwd=test_cwd, dry_run=args.dry_run)
except subprocess.CalledProcessError as exc:
print("\nBuild/test failed.")
print("Re-run exactly:")
print(shell_join(repro_cmd))
return exc.returncode
print("\nSingle-config build completed successfully.")
return 0
if __name__ == "__main__":
raise SystemExit(main())