Files
workinf_Blender_Wasm/my-tools/wbv2_validation.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

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)