Skip to content

Commit cced485

Browse files
committed
codegen: regenerate through a script, not a shell redirect
Every generated-file test told the reader to regenerate with uv run ... vgi-gen-cpp-constants > .../vgi_protocol_constants.hpp The shell truncates the destination *before* the generator runs, so any failure -- a syntax error in the generator, a missing import, an interrupted run -- leaves the file empty rather than unchanged. The instruction that appears when a drift test fails is therefore the instruction most likely to be run in a hurry, against a generator that has just been edited. `scripts/regen_generated.py` writes via a temporary file and moves it into place, so a failed run leaves the previous contents intact, and it regenerates every target in one invocation rather than eleven copy-pasted redirects. The eleven drift tests now name the script.
1 parent 2e25d8d commit cced485

12 files changed

Lines changed: 183 additions & 23 deletions

‎scripts/regen_generated.py‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
"""Regenerate every checked-in generated artifact, atomically.
3+
4+
Run from anywhere:
5+
6+
uv run --project ~/Development/vgi-python python scripts/regen_generated.py
7+
uv run --project ~/Development/vgi-python python scripts/regen_generated.py --check
8+
9+
Why this exists rather than a shell one-liner per artifact: the obvious form,
10+
``python -m vgi.codegen.X > path/to/generated.file``, truncates the destination
11+
*before* the generator runs, so any failure -- a syntax error, a missing
12+
sibling checkout, a typo in the module name -- silently destroys the file it
13+
was meant to update, and the loss is invisible until a test or a build fails
14+
somewhere else. Each artifact here is rendered into memory, checked non-empty,
15+
and only then written.
16+
17+
Targets in sibling repos that are not checked out are skipped, not failed:
18+
nobody has all of them.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import argparse
24+
import importlib
25+
import io
26+
import sys
27+
from pathlib import Path
28+
29+
#: (codegen module, destination relative to the sibling-repo root, repo dir name)
30+
#: ``vgi-java`` lives at ``~/vgi-java`` for some checkouts and beside the other
31+
#: SDKs for others; both are probed.
32+
_TARGETS: list[tuple[str, str, str]] = [
33+
("vgi.codegen.cpp_schemas", "src/generated/vgi_protocol_schemas.hpp", "vgi"),
34+
("vgi.codegen.cpp_request_builders", "src/generated/vgi_request_builders.hpp", "vgi"),
35+
("vgi.codegen.cpp_constants", "src/generated/vgi_protocol_constants.hpp", "vgi"),
36+
("vgi.codegen.cpp_protocol_version", "src/generated/vgi_protocol_version.hpp", "vgi"),
37+
("vgi.codegen.go_schemas", "vgi/generated/protocol_schemas.go", "vgi-go"),
38+
("vgi.codegen.rust_schemas", "vgi-protocol/src/generated/protocol_schemas.rs", "vgi-rust"),
39+
("vgi.codegen.rust_request_builders", "vgi-protocol/src/generated/request_params.rs", "vgi-rust"),
40+
("vgi.codegen.ts_schemas", "src/generated/vgi-protocol-schemas.ts", "vgi-typescript"),
41+
("vgi.codegen.ts_client", "src/generated/vgi-client.ts", "vgi-typescript"),
42+
(
43+
"vgi.codegen.java_schemas",
44+
"vgi/src/test/java/farm/query/vgi/generated/VgiProtocolSchemas.java",
45+
"vgi-java",
46+
),
47+
]
48+
49+
50+
def _repo_root(name: str) -> Path | None:
51+
"""Locate a sibling checkout, or ``None`` when it is not present."""
52+
siblings = Path(__file__).resolve().parents[2]
53+
for candidate in (siblings / name, Path.home() / name):
54+
if candidate.is_dir():
55+
return candidate
56+
return None
57+
58+
59+
def _render(module_name: str) -> str:
60+
"""Render one artifact into memory. Raises if the generator fails."""
61+
module = importlib.import_module(module_name)
62+
buf = io.StringIO()
63+
module.emit(buf)
64+
text = buf.getvalue()
65+
if not text.strip():
66+
raise RuntimeError(f"{module_name}.emit() produced no output")
67+
return text
68+
69+
70+
def main() -> int:
71+
"""Regenerate (or check) every artifact; return a process exit code."""
72+
parser = argparse.ArgumentParser(description=__doc__)
73+
parser.add_argument(
74+
"--check",
75+
action="store_true",
76+
help="report drift without writing anything (exit 1 if any file is stale)",
77+
)
78+
args = parser.parse_args()
79+
80+
stale, wrote, skipped, failed = [], [], [], []
81+
82+
for module_name, relative, repo in _TARGETS:
83+
root = _repo_root(repo)
84+
if root is None:
85+
skipped.append(f"{repo} (not checked out)")
86+
continue
87+
dest = root / relative
88+
try:
89+
text = _render(module_name)
90+
except Exception as exc: # noqa: BLE001 - report every failure, don't stop
91+
failed.append(f"{module_name}: {exc}")
92+
continue
93+
94+
current = dest.read_text() if dest.exists() else None
95+
if current == text:
96+
continue
97+
if args.check:
98+
stale.append(str(dest))
99+
continue
100+
dest.parent.mkdir(parents=True, exist_ok=True)
101+
# Write beside the target, then replace: a crash mid-write leaves the
102+
# previous version intact rather than a truncated file.
103+
tmp = dest.with_suffix(dest.suffix + ".tmp")
104+
tmp.write_text(text)
105+
tmp.replace(dest)
106+
wrote.append(str(dest))
107+
108+
for line in skipped:
109+
print(f"skip {line}")
110+
for line in wrote:
111+
print(f"wrote {line}")
112+
for line in stale:
113+
print(f"STALE {line}")
114+
for line in failed:
115+
print(f"FAIL {line}", file=sys.stderr)
116+
117+
if failed:
118+
return 2
119+
if stale:
120+
print(f"\n{len(stale)} file(s) stale. Run without --check to regenerate.", file=sys.stderr)
121+
return 1
122+
if not args.check and not wrote:
123+
print("everything already up to date")
124+
return 0
125+
126+
127+
if __name__ == "__main__":
128+
raise SystemExit(main())

‎tests/test_generated_cpp_constants.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ def _vgi_generated_path() -> Path:
3434

3535
_REGEN_HINT = (
3636
"To regenerate, run:\n"
37-
" uv run --project ~/Development/vgi-python vgi-gen-cpp-constants \\\n"
38-
" > ~/Development/vgi/src/generated/vgi_protocol_constants.hpp"
37+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
38+
"\n"
39+
"Do not redirect a generator into its destination with `>`: the shell\n"
40+
"truncates the file before the generator runs, so any failure destroys\n"
41+
"the checked-in artifact. The script renders to memory first."
3942
)
4043

4144

‎tests/test_generated_cpp_protocol_version.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ def _vgi_generated_path() -> Path:
3636

3737
_REGEN_HINT = (
3838
"To regenerate, run:\n"
39-
" uv run --project ~/Development/vgi-python python -m vgi.codegen.cpp_protocol_version \\\n"
40-
" > ~/Development/vgi/src/generated/vgi_protocol_version.hpp"
39+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
40+
"\n"
41+
"Do not redirect a generator into its destination with `>`: the shell\n"
42+
"truncates the file before the generator runs, so any failure destroys\n"
43+
"the checked-in artifact. The script renders to memory first."
4144
)
4245

4346

‎tests/test_generated_cpp_request_builders.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ def _vgi_generated_path() -> Path:
3232

3333
_REGEN_HINT = (
3434
"To regenerate, run:\n"
35-
" uv run --project ~/Development/vgi-python python -m vgi.codegen.cpp_request_builders \\\n"
36-
" > ~/Development/vgi/src/generated/vgi_request_builders.hpp"
35+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
36+
"\n"
37+
"Do not redirect a generator into its destination with `>`: the shell\n"
38+
"truncates the file before the generator runs, so any failure destroys\n"
39+
"the checked-in artifact. The script renders to memory first."
3740
)
3841

3942

‎tests/test_generated_cpp_schemas.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ def _vgi_generated_path() -> Path:
3535

3636
_REGEN_HINT = (
3737
"To regenerate, run:\n"
38-
" uv run --project ~/Development/vgi-python vgi-gen-cpp-schemas \\\n"
39-
" > ~/Development/vgi/src/generated/vgi_protocol_schemas.hpp"
38+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
39+
"\n"
40+
"Do not redirect a generator into its destination with `>`: the shell\n"
41+
"truncates the file before the generator runs, so any failure destroys\n"
42+
"the checked-in artifact. The script renders to memory first."
4043
)
4144

4245

‎tests/test_generated_go_schemas.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ def _vgi_go_generated_path() -> Path:
3434

3535
_REGEN_HINT = (
3636
"To regenerate, run:\n"
37-
" uv run --project ~/Development/vgi-python vgi-gen-go-schemas \\\n"
38-
" > ~/Development/vgi-go/vgi/generated/protocol_schemas.go"
37+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
38+
"\n"
39+
"Do not redirect a generator into its destination with `>`: the shell\n"
40+
"truncates the file before the generator runs, so any failure destroys\n"
41+
"the checked-in artifact. The script renders to memory first."
3942
)
4043

4144

‎tests/test_generated_java_schemas.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,11 @@ def _vgi_java_generated_path() -> Path:
5151

5252
_REGEN_HINT = (
5353
"To regenerate, run:\n"
54-
" uv run --project ~/Development/vgi-python python -m vgi.codegen.java_schemas \\\n"
55-
" > ~/vgi-java/vgi/src/test/java/farm/query/vgi/generated/VgiProtocolSchemas.java"
54+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
55+
"\n"
56+
"Do not redirect a generator into its destination with `>`: the shell\n"
57+
"truncates the file before the generator runs, so any failure destroys\n"
58+
"the checked-in artifact. The script renders to memory first."
5659
)
5760

5861

‎tests/test_generated_protocol_version.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ def _protocol_version_txt_path() -> Path:
2929

3030
_REGEN_HINT = (
3131
"To regenerate, run:\n"
32-
" uv run --project ~/Development/vgi-python python -m vgi.codegen.protocol_version \\\n"
33-
" > ~/Development/vgi-python/vgi/protocol_version.txt"
32+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
33+
"\n"
34+
"Do not redirect a generator into its destination with `>`: the shell\n"
35+
"truncates the file before the generator runs, so any failure destroys\n"
36+
"the checked-in artifact. The script renders to memory first."
3437
)
3538

3639

‎tests/test_generated_rust_request_builders.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ def _vgi_rust_generated_path() -> Path:
3434

3535
_REGEN_HINT = (
3636
"To regenerate, run:\n"
37-
" uv run --project ~/Development/vgi-python \\\n"
38-
" python -m vgi.codegen.rust_request_builders \\\n"
39-
" > ~/Development/vgi-rust/vgi-protocol/src/generated/request_params.rs"
37+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
38+
"\n"
39+
"Do not redirect a generator into its destination with `>`: the shell\n"
40+
"truncates the file before the generator runs, so any failure destroys\n"
41+
"the checked-in artifact. The script renders to memory first."
4042
)
4143

4244

‎tests/test_generated_rust_schemas.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ def _vgi_rust_generated_path() -> Path:
4444

4545
_REGEN_HINT = (
4646
"To regenerate, run:\n"
47-
" uv run --project ~/Development/vgi-python python -m vgi.codegen.rust_schemas \\\n"
48-
" > ~/Development/vgi-rust/vgi-protocol/src/generated/protocol_schemas.rs"
47+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
48+
"\n"
49+
"Do not redirect a generator into its destination with `>`: the shell\n"
50+
"truncates the file before the generator runs, so any failure destroys\n"
51+
"the checked-in artifact. The script renders to memory first."
4952
)
5053

5154

0 commit comments

Comments
 (0)