mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 11:22:21 +02:00
refactor(frontend): establish source ownership and architecture gates (#6701)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish explicitly owned native ES modules; never run at application startup."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def module_path(base: Path, relative: str) -> Path:
|
||||
"""Accept canonical JS paths only, contained within the declared tree."""
|
||||
if not isinstance(relative, str):
|
||||
raise ValueError("module paths must be strings")
|
||||
parts = PurePosixPath(relative)
|
||||
if (
|
||||
parts.is_absolute()
|
||||
or parts.as_posix() != relative
|
||||
or ".." in parts.parts
|
||||
or "\\" in relative
|
||||
or "?" in relative
|
||||
or "#" in relative
|
||||
or parts.suffix != ".js"
|
||||
):
|
||||
raise ValueError(f"invalid module path: {relative!r}")
|
||||
path = base / relative
|
||||
if not path.resolve().is_relative_to(base.resolve()):
|
||||
raise ValueError(f"module path escapes its tree: {relative!r}")
|
||||
if any(p.is_symlink() for p in (path, *path.parents)):
|
||||
raise ValueError(f"symlink in module path: {relative!r}")
|
||||
return path
|
||||
|
||||
|
||||
def check_lf_attributes(root: Path, paths: set[Path]) -> None:
|
||||
"""Require portable, repository-owned LF attributes for every mapped path.
|
||||
|
||||
Ask Git in an isolated temporary repository so global attributes, local
|
||||
info/attributes and alternate Git directories cannot mask missing rules.
|
||||
Copy only attribute files at ancestors of the manifest's source/output paths.
|
||||
"""
|
||||
git = shutil.which("git")
|
||||
if git is None:
|
||||
raise ValueError("Git is required for frontend publication attribute checks")
|
||||
attributes = {root / ".gitattributes"}
|
||||
for path in paths:
|
||||
for parent in path.parents:
|
||||
if parent == root:
|
||||
break
|
||||
attributes.add(parent / ".gitattributes")
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
|
||||
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull)
|
||||
relative = sorted(path.relative_to(root).as_posix() for path in paths)
|
||||
with tempfile.TemporaryDirectory(prefix="ody-frontend-attributes-") as directory:
|
||||
target = Path(directory)
|
||||
for attribute in attributes:
|
||||
if attribute.is_symlink():
|
||||
raise ValueError(f"symlink attribute file: {attribute.relative_to(root)}")
|
||||
if attribute.is_file():
|
||||
copy = target / attribute.relative_to(root)
|
||||
copy.parent.mkdir(parents=True, exist_ok=True)
|
||||
copy.write_bytes(attribute.read_bytes())
|
||||
command = [git, "-c", f"core.attributesFile={os.devnull}", "-C", str(target)]
|
||||
try:
|
||||
subprocess.run(command + ["init", "--quiet", "--template="], env=env, check=True, capture_output=True, timeout=15)
|
||||
result = subprocess.run(
|
||||
command + ["check-attr", "-z", "--stdin", "text", "eol"],
|
||||
input=("\0".join(relative) + "\0").encode("utf-8"),
|
||||
env=env, check=True, capture_output=True, timeout=15,
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
|
||||
raise ValueError("Git frontend attribute check failed") from error
|
||||
fields = result.stdout.decode("utf-8").split("\0")[:-1]
|
||||
effective = {name: {} for name in relative}
|
||||
for index in range(0, len(fields), 3):
|
||||
name, attribute, value = fields[index:index + 3]
|
||||
effective[name][attribute] = value
|
||||
invalid = [
|
||||
f"{name} (text={values.get('text')}, eol={values.get('eol')})"
|
||||
for name, values in effective.items()
|
||||
if values != {"text": "set", "eol": "lf"}
|
||||
]
|
||||
if invalid:
|
||||
raise ValueError("manifest paths require repository-owned 'text eol=lf' Git attributes: " + ", ".join(invalid))
|
||||
|
||||
|
||||
def synchronize(root: Path = ROOT, *, check: bool = False) -> list[str]:
|
||||
"""Validate the entire plan before writing. Return stale deployment paths."""
|
||||
root = root.resolve()
|
||||
source_root = root / "frontend" / "src"
|
||||
static_root = root / "static"
|
||||
manifest = json.loads((root / "frontend" / "modules.json").read_text(encoding="utf-8"))
|
||||
if not isinstance(manifest, dict) or manifest.get("version") != 1 or not isinstance(manifest.get("modules"), list):
|
||||
raise ValueError("expected frontend module manifest version 1")
|
||||
if not manifest["modules"]:
|
||||
raise ValueError("module manifest must not be empty")
|
||||
sources, outputs, plan = set(), set(), []
|
||||
for entry in manifest["modules"]:
|
||||
source = module_path(source_root, entry["source"])
|
||||
output = module_path(static_root, entry["deployment"])
|
||||
if not entry["deployment"].startswith("js/"):
|
||||
raise ValueError("deployment modules must be under static/js/")
|
||||
if source in sources or output in outputs:
|
||||
raise ValueError("each source and deployment path must be unique")
|
||||
sources.add(source)
|
||||
outputs.add(output)
|
||||
content = source.read_bytes()
|
||||
if output.exists() and not output.is_file():
|
||||
raise ValueError(f"deployment path is not a file: {output}")
|
||||
plan.append((output, content))
|
||||
unlisted = set(source_root.rglob("*.js")) - sources
|
||||
if unlisted:
|
||||
raise ValueError(f"source modules missing from manifest: {sorted(map(str, unlisted))}")
|
||||
check_lf_attributes(root, sources | outputs)
|
||||
stale = []
|
||||
for output, content in plan:
|
||||
if not output.is_file() or output.read_bytes() != content:
|
||||
stale.append(output.relative_to(root).as_posix())
|
||||
if not check:
|
||||
for output, content in plan:
|
||||
if output.relative_to(root).as_posix() in stale:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_bytes(content)
|
||||
return stale
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true", help="fail on invalid attributes or stale output; write no deployment files")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
stale = synchronize(check=args.check)
|
||||
except (OSError, ValueError, KeyError, TypeError) as error:
|
||||
parser.exit(2, f"frontend: {error}\n")
|
||||
if args.check and stale:
|
||||
print("Stale frontend deployment modules: " + ", ".join(stale))
|
||||
print("Run python scripts/build_frontend.py and include the source and output changes together.")
|
||||
return 1
|
||||
print(f"Frontend {'check passed' if args.check else 'published'} ({len(stale)} changed modules).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user