"""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)