Add renderer and package readiness validation gates
This commit is contained in:
188
scripts/dev/check_renderer_api_contract.py
Normal file
188
scripts/dev/check_renderer_api_contract.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate renderer API contract purity for key rendering components."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""")
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
CHECKS = {
|
||||
"renderer_api": {
|
||||
"roots": [REPO_ROOT / "src" / "renderer_api"],
|
||||
"allowed_include_prefixes": ("foundation/", "renderer_api/"),
|
||||
"forbidden_include_tokens": (
|
||||
"renderer_gl/",
|
||||
"platform/",
|
||||
"platform_api/",
|
||||
"platform_",
|
||||
"opengl",
|
||||
"glad",
|
||||
"vulkan",
|
||||
"d3d",
|
||||
"directx",
|
||||
"metal",
|
||||
"appkit",
|
||||
"cocoa",
|
||||
"objc/",
|
||||
"windows.h",
|
||||
"x11/",
|
||||
"wayland-",
|
||||
"android/",
|
||||
),
|
||||
"forbidden_body_tokens": (
|
||||
"OpenGl",
|
||||
"OpenGL",
|
||||
"opengl_",
|
||||
"GL_",
|
||||
"Vulkan",
|
||||
"Vk",
|
||||
"MTL",
|
||||
"D3D",
|
||||
"vulkan",
|
||||
"metal",
|
||||
"renderer_gl",
|
||||
"pp_platform_",
|
||||
),
|
||||
},
|
||||
"paint_renderer": {
|
||||
"roots": [REPO_ROOT / "src" / "paint_renderer"],
|
||||
"allowed_include_prefixes": (
|
||||
"assets/",
|
||||
"document/",
|
||||
"foundation/",
|
||||
"paint/",
|
||||
"paint_renderer/",
|
||||
"renderer_api/",
|
||||
),
|
||||
"forbidden_include_tokens": (
|
||||
"renderer_gl/",
|
||||
"platform/",
|
||||
"platform_api/",
|
||||
"platform_",
|
||||
"opengl",
|
||||
"glad",
|
||||
"vulkan",
|
||||
"d3d",
|
||||
"directx",
|
||||
"metal",
|
||||
"appkit",
|
||||
"cocoa",
|
||||
"objc/",
|
||||
"windows.h",
|
||||
"x11/",
|
||||
"wayland-",
|
||||
"android/",
|
||||
),
|
||||
"forbidden_body_tokens": (
|
||||
"OpenGl",
|
||||
"OpenGL",
|
||||
"opengl_",
|
||||
"GL_",
|
||||
"Vulkan",
|
||||
"Vk",
|
||||
"MTL",
|
||||
"D3D",
|
||||
"vulkan",
|
||||
"metal",
|
||||
"renderer_gl",
|
||||
"pp_platform_",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
ALLOWED_EXTERNAL_PREFIXES = ("stb/",)
|
||||
|
||||
|
||||
def is_forbidden_include(allowlist: tuple[str, ...], forbidden_tokens: tuple[str, ...], include: str) -> tuple[bool, str | None]:
|
||||
include_lower = include.lower()
|
||||
if any(token in include_lower for token in forbidden_tokens):
|
||||
token = next(token for token in forbidden_tokens if token in include_lower)
|
||||
return True, token
|
||||
|
||||
if "/" in include and include_lower.startswith(ALLOWED_EXTERNAL_PREFIXES):
|
||||
return False, None
|
||||
|
||||
if "/" in include and not any(include_lower.startswith(prefix) for prefix in allowlist):
|
||||
return True, "cross-component-include"
|
||||
return False, None
|
||||
|
||||
|
||||
def scan_component(name: str, config: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
violations: list[dict[str, Any]] = []
|
||||
for root in config["roots"]:
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}:
|
||||
continue
|
||||
|
||||
text = path.read_text(encoding="utf-8").splitlines()
|
||||
for line_no, line in enumerate(text, start=1):
|
||||
include_match = INCLUDE_RE.match(line)
|
||||
if include_match:
|
||||
include = (include_match.group(2) or include_match.group(3) or "").strip()
|
||||
forbidden, reason = is_forbidden_include(
|
||||
config["allowed_include_prefixes"],
|
||||
config["forbidden_include_tokens"],
|
||||
include,
|
||||
)
|
||||
if forbidden:
|
||||
violations.append(
|
||||
{
|
||||
"component": name,
|
||||
"file": str(path.relative_to(REPO_ROOT)),
|
||||
"line": line_no,
|
||||
"kind": "forbidden-include",
|
||||
"include": include,
|
||||
"detail": reason,
|
||||
"text": line.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
joined = "\n".join(text)
|
||||
for token in config["forbidden_body_tokens"]:
|
||||
if token in joined:
|
||||
violations.append(
|
||||
{
|
||||
"component": name,
|
||||
"file": str(path.relative_to(REPO_ROOT)),
|
||||
"line": 0,
|
||||
"kind": "forbidden-body-token",
|
||||
"include": token,
|
||||
"detail": "backend- or platform-specific symbol in renderer contract path",
|
||||
"text": "",
|
||||
}
|
||||
)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
violations: list[dict[str, Any]] = []
|
||||
for component, config in CHECKS.items():
|
||||
violations.extend(scan_component(component, config))
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": len(violations) == 0,
|
||||
"summary": {
|
||||
"componentCount": len(CHECKS),
|
||||
"violationCount": len(violations),
|
||||
},
|
||||
"violations": violations,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
)
|
||||
return 0 if not violations else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user