scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
152 lines
5.0 KiB
Python
Executable File
152 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Check generated outputs in an isolated worktree without mutating the caller."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
EXPLICIT_GENERATED = {
|
|
"lib/data/ducet_order.dart",
|
|
"lib/data/hid_key_labels.dart",
|
|
"lib/data/iso_639_data.dart",
|
|
"server/relay_protocol_gen.go",
|
|
}
|
|
DEPENDENCY_STATE = ("package_config.json", "package_graph.json")
|
|
|
|
|
|
def _run(root: Path, *args: str) -> subprocess.CompletedProcess[bytes]:
|
|
return subprocess.run(args, cwd=root, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
|
|
def _is_generated(relative: str) -> bool:
|
|
return relative in EXPLICIT_GENERATED or (
|
|
relative.startswith("lib/") and (relative.endswith(".g.dart") or relative.endswith(".freezed.dart"))
|
|
)
|
|
|
|
|
|
def _generated_state(root: Path) -> dict[str, bytes]:
|
|
state = {}
|
|
lib = root / "lib"
|
|
if lib.exists():
|
|
for path in lib.rglob("*.dart"):
|
|
relative = path.relative_to(root).as_posix()
|
|
if _is_generated(relative) and path.is_file():
|
|
state[relative] = path.read_bytes()
|
|
for relative in EXPLICIT_GENERATED:
|
|
path = root / relative
|
|
if path.is_file():
|
|
state[relative] = path.read_bytes()
|
|
return state
|
|
|
|
|
|
def _nul_paths(result: bytes) -> list[str]:
|
|
return [os.fsdecode(value) for value in result.split(b"\0") if value]
|
|
|
|
|
|
def _copy_overlay_path(source_root: Path, target_root: Path, relative: str) -> None:
|
|
source = source_root / relative
|
|
target = target_root / relative
|
|
if not source.exists() and not source.is_symlink():
|
|
if target.is_dir() and not target.is_symlink():
|
|
shutil.rmtree(target)
|
|
else:
|
|
target.unlink(missing_ok=True)
|
|
return
|
|
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
if target.is_dir() and not target.is_symlink():
|
|
shutil.rmtree(target)
|
|
else:
|
|
target.unlink(missing_ok=True)
|
|
if source.is_symlink():
|
|
target.symlink_to(os.readlink(source))
|
|
else:
|
|
shutil.copy2(source, target)
|
|
|
|
|
|
def _copy_dependency_state(source_root: Path, target_root: Path) -> None:
|
|
source = source_root / ".dart_tool"
|
|
target = target_root / ".dart_tool"
|
|
for relative in DEPENDENCY_STATE:
|
|
source_path = source / relative
|
|
if source_path.is_file():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source_path, target / relative)
|
|
|
|
|
|
def _create_isolated_checkout(root: Path, destination: Path) -> None:
|
|
_run(root, "git", "worktree", "add", "--detach", "--quiet", str(destination), "HEAD")
|
|
changed = _nul_paths(
|
|
_run(root, "git", "diff", "--no-renames", "--name-only", "-z", "HEAD", "--").stdout
|
|
)
|
|
untracked = _nul_paths(_run(root, "git", "ls-files", "--others", "--exclude-standard", "-z").stdout)
|
|
for relative in sorted(set(changed + untracked)):
|
|
_copy_overlay_path(root, destination, relative)
|
|
_copy_dependency_state(root, destination)
|
|
|
|
|
|
def _dirty_generated_paths(root: Path) -> set[str]:
|
|
changed = _nul_paths(_run(root, "git", "diff", "--name-only", "-z", "--").stdout)
|
|
untracked = _nul_paths(
|
|
_run(
|
|
root,
|
|
"git",
|
|
"ls-files",
|
|
"--others",
|
|
"--exclude-standard",
|
|
"-z",
|
|
"--",
|
|
"lib",
|
|
*EXPLICIT_GENERATED,
|
|
).stdout
|
|
)
|
|
return {relative for relative in changed + untracked if _is_generated(relative)}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
root = Path.cwd().resolve()
|
|
caller_state = _generated_state(root)
|
|
temporary = Path(tempfile.mkdtemp(prefix="plezy-codegen-check-"))
|
|
checkout = temporary / "checkout"
|
|
registered = True
|
|
try:
|
|
_create_isolated_checkout(root, checkout)
|
|
result = subprocess.run(["bash", "scripts/codegen.sh", *arguments], cwd=checkout, check=False)
|
|
if result.returncode != 0:
|
|
return result.returncode
|
|
|
|
expected_state = _generated_state(checkout)
|
|
stale = {
|
|
relative
|
|
for relative in caller_state.keys() | expected_state.keys()
|
|
if caller_state.get(relative) != expected_state.get(relative)
|
|
}
|
|
stale.update(_dirty_generated_paths(root))
|
|
if stale:
|
|
print("Generated files are out of date:", file=sys.stderr)
|
|
for relative in sorted(stale):
|
|
print(f" {relative}", file=sys.stderr)
|
|
print("Run 'scripts/codegen.sh' and commit the result.", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
finally:
|
|
if registered:
|
|
subprocess.run(
|
|
["git", "worktree", "remove", "--force", str(checkout)],
|
|
cwd=root,
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
shutil.rmtree(temporary, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|