Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
165 lines
7.3 KiB
Python
165 lines
7.3 KiB
Python
"""Normalize worker validation commands before the supervisor executes them."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import re
|
|
|
|
|
|
SUPERVISOR_ENV = frozenset({
|
|
"WBV2_RECEIPT_DIR", "WBV2_DEPLOYED_ORIGIN", "WEB_BUILD_JOBS",
|
|
"CMAKE_BUILD_PARALLEL_LEVEL",
|
|
})
|
|
UNSET_ENV = frozenset({"SERVER_OPERATOR_NATIVE_REPORT"})
|
|
PATH_ENV = {
|
|
"EM_CONFIG": (".emscripten-web", "file"),
|
|
"EM_LLVM_ROOT": (".emsdk/upstream/bin", "directory"),
|
|
"EM_BINARYEN_ROOT": (".emsdk/upstream", "directory"),
|
|
"EM_NODE_JS": (".emsdk/node", "node"),
|
|
"BLENDER_BIN": (None, "blender"),
|
|
}
|
|
EMPTY_ENV = frozenset({"EM_LLVM_ADD_VERSION", "EM_CLANG_ADD_VERSION"})
|
|
EXECUTABLES = frozenset({"node", "python3", "npm", "cmake", "ninja", "ctest", "git"})
|
|
|
|
|
|
def _contained(root, value, directory):
|
|
path = Path(value)
|
|
if ".." in path.parts:
|
|
raise ValueError("validation path must not traverse parent directories")
|
|
path = (directory / path).resolve()
|
|
if not path.is_relative_to(root):
|
|
raise ValueError("validation path escapes repository")
|
|
return path
|
|
|
|
|
|
def _environment_value(root, directory, name, value):
|
|
if name in SUPERVISOR_ENV:
|
|
# Older worker results repeat the attempt path. Never forward it: a
|
|
# recovered result must use the supervisor's current receipt directory.
|
|
return None
|
|
if name in EMPTY_ENV:
|
|
if value:
|
|
raise ValueError(f"{name} must be empty")
|
|
return value
|
|
if name == "WEB_TEST_PORT":
|
|
if not re.fullmatch(r"[0-9]+", value) or not 1024 <= int(value) <= 65535:
|
|
raise ValueError("WEB_TEST_PORT must be an unprivileged TCP port")
|
|
return str(int(value))
|
|
if name not in PATH_ENV:
|
|
raise ValueError(f"unsupported validation environment variable: {name}")
|
|
if not value:
|
|
raise ValueError(f"{name} requires a repository path")
|
|
path = _contained(root, value, directory)
|
|
anchor, kind = PATH_ENV[name]
|
|
expected = (root / anchor).resolve() if anchor else None
|
|
if expected and (not expected.is_relative_to(root) or (
|
|
not path.is_relative_to(expected) if kind == "node" else path != expected
|
|
)):
|
|
raise ValueError(f"{name} must use the repository toolchain")
|
|
if kind == "directory":
|
|
valid = path.is_dir()
|
|
else:
|
|
valid = path.is_file() and (kind == "file" or path.name == kind)
|
|
if not valid:
|
|
raise ValueError(f"{name} does not name the required {kind}")
|
|
return str(path)
|
|
|
|
|
|
def _bound_parallelism(argv, max_build_jobs):
|
|
if argv[0] not in {"cmake", "ninja", "ctest"}:
|
|
return argv
|
|
result, index, has_parallelism = [argv[0]], 1, False
|
|
while index < len(argv):
|
|
arg = argv[index]
|
|
match = re.fullmatch(r"(?:-j=?|--parallel=)([0-9]+)", arg)
|
|
if arg in {"-j", "--parallel"} or match:
|
|
has_parallelism = True
|
|
value = match.group(1) if match else None
|
|
if value is None and index + 1 < len(argv) and re.fullmatch(r"[0-9]+", argv[index + 1]):
|
|
index += 1
|
|
value = argv[index]
|
|
jobs = int(value) if value is not None else max_build_jobs
|
|
# Zero and a missing number can request unlimited native jobs.
|
|
jobs = min(jobs, max_build_jobs) if jobs > 0 else max_build_jobs
|
|
result.extend(["-j", str(jobs)])
|
|
else:
|
|
result.append(arg)
|
|
index += 1
|
|
if not has_parallelism and (argv[0] in {"ninja", "ctest"} or "--build" in argv):
|
|
insertion = result.index("--") if "--" in result else len(result)
|
|
result[insertion:insertion] = ["-j", str(max_build_jobs)]
|
|
return result
|
|
|
|
|
|
def prepare_validation_command(root, argv, cwd, *, max_build_jobs=8):
|
|
"""Return (argv, cwd, env_overrides, unset_names), with no shell execution.
|
|
|
|
Merge env_overrides before supervisor-owned environment values and remove
|
|
unset_names from the inherited process environment. Include both in cache
|
|
keys. max_build_jobs must match the supervisor's configured build limit.
|
|
"""
|
|
root = Path(root).resolve()
|
|
if not isinstance(argv, (list, tuple)) or not argv or not all(
|
|
isinstance(value, str) and value and "\0" not in value for value in argv
|
|
):
|
|
raise ValueError("command must be an argv array")
|
|
if not isinstance(cwd, str) or not cwd or Path(cwd).is_absolute():
|
|
raise ValueError("command cwd must be a repository relative path")
|
|
directory = _contained(root, cwd, root)
|
|
if not directory.is_dir():
|
|
raise ValueError("command cwd does not exist")
|
|
if isinstance(max_build_jobs, bool) or not isinstance(max_build_jobs, int) or max_build_jobs <= 0:
|
|
raise ValueError("max_build_jobs must be a positive integer")
|
|
max_build_jobs = min(max_build_jobs, 8)
|
|
command, env, unset, seen = list(argv), {}, set(), set()
|
|
if command[0] == "env":
|
|
index = 1
|
|
while index < len(command):
|
|
arg = command[index]
|
|
if arg == "--":
|
|
index += 1
|
|
break
|
|
if arg == "-u":
|
|
index += 1
|
|
if index == len(command) or command[index] not in UNSET_ENV:
|
|
raise ValueError("unsupported validation environment unset")
|
|
name = command[index]
|
|
if name in seen:
|
|
raise ValueError("duplicate validation environment variable")
|
|
seen.add(name)
|
|
unset.add(name)
|
|
elif "=" in arg and not arg.startswith("-"):
|
|
name, value = arg.split("=", 1)
|
|
if name in seen:
|
|
raise ValueError("duplicate validation environment variable")
|
|
seen.add(name)
|
|
normalized = _environment_value(root, directory, name, value)
|
|
if normalized is not None:
|
|
env[name] = normalized
|
|
elif arg.startswith("-"):
|
|
raise ValueError("unsupported validation env option")
|
|
else:
|
|
break
|
|
index += 1
|
|
command = command[index:]
|
|
if not command or command[0] not in EXECUTABLES:
|
|
raise ValueError("unsupported validation executable")
|
|
if any(arg in {"-e", "--eval", "-c", "--command", "--input-type=module"}
|
|
or arg.startswith(("--eval=", "--command=")) for arg in command[1:]):
|
|
raise ValueError("inline code is not a validation entrypoint")
|
|
if command[0] in {"node", "python3"} and any(
|
|
arg.startswith("-e" if command[0] == "node" else "-c") and not arg.startswith("--")
|
|
for arg in command[1:]
|
|
):
|
|
raise ValueError("inline code is not a validation entrypoint")
|
|
if any(re.search(r"firefox|webkit", arg, re.I) for arg in command):
|
|
raise ValueError("only Chromium is permitted")
|
|
if command[0] == "git" and command != ["git", "diff", "--check"]:
|
|
raise ValueError("validation cannot change Git state")
|
|
if command[0] in {"node", "python3"}:
|
|
scripts = [arg for arg in command[1:] if arg.endswith((".mjs", ".js", ".py"))]
|
|
if not scripts or any(not _contained(root, path, directory).is_file() for path in scripts):
|
|
raise ValueError("a concrete repository validation script is required")
|
|
if command[0] == "npm" and "run" not in command:
|
|
raise ValueError("only repository npm run scripts are permitted")
|
|
return _bound_parallelism(command, max_build_jobs), directory, env, sorted(unset)
|