use std::path::{Path, PathBuf};
/// Where the contract comes from: **the vendored `proto/holger.proto`, always.**
///
/// holger's `proto/holger.proto` is the source of truth for what this console may
/// say, and the copy in `proto/` is that contract, committed — byte-identical to
/// holger's `origin/main` today, and `tests/proto_is_holgers.rs` is the alarm that
/// fires when it stops being.
///
/// # ⚠ Why the build does NOT read `../holger/proto/holger.proto`
///
/// It did, between 2026-09-15 and 2026-09-17: "compile against the server's REAL
/// contract, not a copy of it", preferring the sibling checkout whenever the nordisk
/// layout put one beside us. That spelling makes this repository's build depend on
/// the state of a WORKING TREE it does not own, and the failure it produces is
/// unreadable. Measured 2026-09-17: `~/git/holger` was parked on an old
/// main (170 lines — no `WhoAmI`, no SearchService, SbomService, PropertyService or
/// ReplicationService) while `proto/holger.proto` here matched holger's `origin/main`
/// exactly (414 lines, sha256 4ff9be95…). `cargo build --target
/// wasm32-unknown-unknown` compiled the stale file and failed with NINETEEN
/// `no method named …` errors inside `src/grpc.rs`, naming neither the proto nor the
/// sibling. A `git switch` in an unrelated repository must not be able to do that.
///
/// The drift check belongs in a test, where a stale sibling is a printed note and not
/// a broken build, and `tests/proto_is_holgers.rs` — which the same change introduced
/// — is already exactly that test. This function is what makes the console's build
/// reproducible from `git clone` alone, which is also the only build GitHub CI can do.
fn proto_file(manifest_dir: &Path) -> PathBuf {
manifest_dir.join("proto/holger.proto")
}
/// Every message `AdminService.AdminSnapshot` answers with (and `LoadedModule`,
/// which it reuses) — the same list `crates/holger-ui-server/build.rs` carries.
const ADMIN_VIEW_MESSAGES: &[&str] = &[
"AdminSnapshotReply", "AdminDisks", "AdminDisk", "AdminPartition", "AdminBlockDevice", "AdminUnresolved",
"AdminTwin", "AdminTwinCredential", "AdminTwinRow", "AdminGrowMember", "AdminGrowth", "AdminClockSource",
"AdminClockSync", "AdminClock", "AdminSelfRegistration", "AdminEntitlement", "AdminPlugins", "AdminAudit",
"AdminLogs", "AdminPayments", "LoadedModule",
// AdminSnapshotReply fields 14-16 (holger 9847632: measured disk speed, every
// repository's usage / cache / quota, the job queue's depth).
"AdminDiskSpeed", "AdminTenant", "AdminQueue",
// AdminSnapshotReply field 17 (the Sync pane): every paired downstream per
// repository, holger's GrowService.Downstreams rows.
"AdminDownstreamRow", "AdminDownstreamRepo",
// The Queue and Admission panes (holger admit-queue): `JobService.ListJobs` and
// `AdmissionService.ListUsage` / `AdmitPlan`, relayed by the same server.
"ListJobsResponse", "JobRow", "ListUsageResponse", "UsageRow", "WorkUsageRow", "CostRow", "AdmitPlanResponse",
"EstimateRow",
];
fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
let proto = proto_file(&manifest_dir);
let include = proto.parent().ok_or("proto file has no parent dir")?;
println!("cargo:rerun-if-changed=proto/holger.proto");
println!("cargo:rerun-if-changed=proto/holger-room.proto");
println!("cargo:rerun-if-changed=proto/holger-account.proto");
println!("cargo:rerun-if-changed=proto/holger-token.proto");
// Parse with protox — pure Rust, no `protoc` binary, no subprocess — the same
// pipeline holger's own `cargo xtask gen-proto` uses.
// holger's own room contract (`holger.room.v1`): where more room is bought.
let room = include.join("holger-room.proto");
// holger's account contract (`holger.account.v1`, lane P4): the repositories a
// person has, the private ones an admin makes, each one's client setup and plan.
let account = include.join("holger-account.proto");
// holger's token contract (`holger.token.v1`, lane P3): repository tokens for
// package clients — mint (the secret shown once), list, revoke.
let token = include.join("holger-token.proto");
let fds = protox::compile([proto.clone(), room, account, token], [include])?;
// Client stubs only — the server lives in the holger main repo.
// `build_transport(false)`: tonic's hyper transport does not compile on
// wasm32; the browser transport is tonic-web-wasm-client instead.
// ★ The admin view reaches the browser as JSON from this console's own
// server (`/api/admin/snapshot`), in holger's message shapes: the SAME list
// the server derives `Serialize` over (`crates/holger-ui-server/build.rs`),
// read with `Deserialize` and every field defaulted, so an older or newer
// holger that omits a field is read as its default rather than refused.
let mut builder = tonic_prost_build::configure();
for m in ADMIN_VIEW_MESSAGES {
builder = builder.type_attribute(format!(".holger.v1.{m}"), "#[derive(serde::Deserialize)] #[serde(default)]");
}
builder
.build_server(false)
.build_transport(false)
.skip_protoc_run()
.compile_fds(fds)?;
Ok(())
}