fix(tooling): cargo_binary rebuilds a stale binary, not only a missing one

core.process.cargo_binary built the Rust binary only when it was absent, a
behaviour inherited from the three bash wrappers it replaced (tooling/atlas,
generate-brands, generate-corporations). After an edit to the Rust source it
went on running the old binary.

For generate_brands this breaks the one promise the systems.db stamp makes.
`make regen-db` stamps the SHA of the NEW brand sources onto
generated_brands.toml produced by the OLD code, so the gate reports fresh and
the output is stale. It had already happened: the committed binary wrote
"Re-run: reach generate brands" while main.rs still said
tooling/generate-brands, so what was on disk had not been built from the
committed source.

It now always runs `cargo build --bin <name>` and lets cargo decide. An
up-to-date build is incremental and fast; a real rebuild is announced as an
event, since a silent ninety-second compile reads as a hang.

Proven on generate_brands (T-1253): with the old header restored, the rebuilt
binary reproduces generated_brands.toml byte-for-byte (sha256 e748531…). With
the new header it differs by exactly that one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 20:13:44 +02:00
co-authored by Claude Opus 5.5
parent 5a01450585
commit e3daf561d1
+24 -19
View File
@@ -147,34 +147,39 @@ def run(
def cargo_binary(name: str, *args: str, capture: bool = True) -> str: def cargo_binary(name: str, *args: str, capture: bool = True) -> str:
"""Run a Rust binary from `server/`, building it first if it is absent. """Run a Rust binary from `server/`, building it first if it is stale.
Three scripts wrote this by hand — `tooling/atlas`, `generate-brands`, Three scripts wrote this by hand — `tooling/atlas`, `generate-brands`,
`generate-corporations` — each ~24 lines of identical bash: check for the `generate-corporations` — each ~24 lines of identical bash: check for the
debug binary, `cargo build --bin` it if missing, `exec` it with every debug binary, `cargo build --bin` it if missing, `exec` it with every
argument. Three copies of one idea is a substrate, so it lives here. argument. Three copies of one idea is a substrate, so it lives here.
Build-if-missing is kept because it is genuinely useful on a cold checkout, It ALWAYS asks cargo to build, and lets cargo decide whether anything is
but it is announced rather than silent: a first run that takes ninety stale. The copies all built only when the binary was MISSING, so an edit
seconds with no explanation reads as a hang. to the Rust source ran the old binary — and for generate_brands that meant
`make regen-db` stamped the new source SHA onto output the old code
produced, the one thing the stamp exists to prevent (found T-1253: a header
change to main.rs did not reach generated_brands.toml). An up-to-date build
is incremental and fast; a real rebuild is announced, since ninety silent
seconds reads as a hang.
""" """
binary = config.path("server", "target", "debug", name) from tooling.core import console
if not binary.is_file():
from tooling.core import console
console.event(f"building the {name} binary (first run)", level="warn") binary = config.path("server", "target", "debug", name)
build = run( build = run(
["cargo", "build", "--bin", name], ["cargo", "build", "--bin", name],
cwd=config.path("server"), cwd=config.path("server"),
check=False, check=False,
missing_fix="install Rust — make setup-rust", missing_fix="install Rust — make setup-rust",
)
if build.returncode != 0:
raise ReachError(
f"could not build {name}\n{(build.stderr or '').strip()}",
fix=f"cd server && cargo build --bin {name} — for the full error",
exit_code=build.returncode,
) )
if build.returncode != 0: if "Compiling" in (build.stderr or ""):
raise ReachError( console.event(f"rebuilt the {name} binary — its source changed since the last build", level="warn")
f"could not build {name}\n{(build.stderr or '').strip()}",
fix=f"cd server && cargo build --bin {name} — for the full error",
exit_code=build.returncode,
)
result = run([str(binary), *args], check=False, capture=capture) result = run([str(binary), *args], check=False, capture=capture)
if result.returncode != 0: if result.returncode != 0: