# Implementation Plan - [x] 1. Foundation: meson build-time option and C project-wide define - Add a `build_time` meson option (string, default `''`) alongside the existing `commit_id` option in `src/meson.options` - Extend `src/meson.build`'s existing `add_project_arguments()` call to also define `VERSION_BUILD_TIME` for every C component, mirroring how `VERSION_COMMIT_ID` is already threaded through - Observable: a plain `meson setup` with neither option set still configures successfully (both defines resolve to empty string, no build failure) - _Requirements: 1.1, 1.2, 1.4, 4.3_ - [x] 2. Foundation: compute real values at both build entry points - [x] 2.1 (P) xce-build.sh computes and passes commit id and build time - Compute `commit_id` via `git rev-parse --short HEAD`, appended with `-dirty` when the tree has uncommitted changes (`git status --porcelain` or `git diff --quiet HEAD --`) - Compute `build_time` via `date -u +%Y-%m-%dT%H:%M:%SZ` - Fall back to the literal string `unknown` for `commit_id` when `git` itself fails (e.g. no `.git` present) - Append `-Dcommit_id="$commit_id" -Dbuild_time="$build_time"` to `MESON_OPTS` for every build profile, before the existing `meson setup`/`meson setup --reconfigure` calls - After computing both values, verify each is non-empty (`build_time` always is; `commit_id` falls back to `unknown` only on a genuine `git` failure) and print a loud warning to stderr if either resolves empty anyway -- the build tooling must not silently proceed with a value it cannot confirm - Observable: running `/xce-build scheduler debug` followed by the resulting binary's `--version` shows a commit id matching the current `git rev-parse --short HEAD` at build time, not empty; deliberately breaking the computation (e.g. temporarily renaming `.git`) prints the warning instead of silently succeeding - _Requirements: 1.1, 1.2, 1.3, 1.4, 4.1, 4.2, 5.1, 5.2_ - _Boundary: xce-build.sh_ - [x] 2.2 (P) Nix packages compute and pass build time - ~~Add a `builtins.currentTime`-derived build-time value in `src/flake.nix`~~ — corrected during review (round 1 REJECTED): `builtins.currentTime` breaks pure-eval `nix build` (requires `--impure`) and yields non-UTC epoch seconds. Implemented instead as a `preConfigure` build-phase `date -u +%Y-%m-%dT%H:%M:%SZ` hook in `xce.nix`/`xcecli.nix` appending to `mesonFlagsArray`, alongside the unchanged eval-time `-Dcommit_id=` wiring - Fail loudly (non-empty guard) if `date -u` ever produces empty output, before the value reaches `mesonFlagsArray` - Observable: `nix build .#xcecore_debug` produces a binary whose `--version` shows a non-empty, ISO-8601 UTC build time, without any `git` invocation inside the sandboxed derivation, and without requiring `--impure` - _Requirements: 1.1, 1.2, 1.4, 4.1, 4.2, 5.1, 5.2_ - _Boundary: Nix packaging_ - [x] 3. Core: C daemon version display - [x] 3.1 Extend the shared version macro with build time and unknown fallback - ~~Extend `MK_VERSION`/`STR`~~ — `MK_VERSION`/`STR` cannot express the empty-string-to-`unknown` substitution at the preprocessor level, so implemented as new runtime functions `xce_version_field()`/`xce_compose_version()` (`utils.h`/`utils.c`) instead; `MK_VERSION`/`STR` marked SUPERSEDED with an explicit doc comment and handoff notes for 3.2 (round 1 REJECTED for silently orphaning `MK_VERSION` + undocumented `argp_program_version` file-scope-const-initializer trap; round 2 APPROVED after adding the SUPERSEDED comment + NOTE FOR CALLERS guidance) - Both fields render as `unknown` when their underlying define is empty (verified via `""`, not just `NULL`), never a blank/garbled string - Observable: a build with both meson options unset produces a `--version` string containing `unknown` for both fields, not empty gaps -- 5 new unit tests in `test_utils.c` confirm this - _Requirements: 1.4, 2.2_ - _Boundary: utils.h_ - [x] 3.2 (P) Controller and scheduler: version in --version and startup log - `argp_program_version` migrated to `xce_compose_version()` via a `static char version_buf[128]` computed and assigned before `argp_parse()` in all 3 files (controller/main.c, scheduler/main.c, test.c); zero remaining `MK_VERSION` callers repo-wide outside its own definition - Startup log lines read the identical `version_buf`, never recomputed -- extended to BOTH of scheduler's mutually-exclusive first-INFO branches (`--peers` and no-`--peers`), not just the one literally quoted in this task's text, since Requirement 3.1's "when the daemon starts" is unconditional - Observable confirmed live (independently re-verified by review, not just self-reported): `xce_controller --version`, `xce_test --version`, and `xce_scheduler --version` all print commit id + build time and exit; all startup log branches for both daemons are byte-identical to their own `--version` output - _Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3_ - _Boundary: controller/main.c, scheduler/main.c, controller/testsuite/test.c_ - _Depends: 3.1_ - [x] 3.3 (P) Worker: add --version and startup log line - Added `OPT_VERSION=257` (long-only, beyond ASCII range, no collision with `"n:l:vh"`/`OPT_PEERS=256`); handling case computes `xce_compose_version()` into a `static char version_buf[128]` and returns before any startup work (`check_user()`/logdir/`xcecd_init`/`xcecd_start`) - Both startup INFO log branches (`--peers` and no-`--peers`) extended to log the same buffer - Observable confirmed live (independently re-verified by review): `xcecd --version` prints and exits; both startup log branches (captured via a real `sudo` daemon run) are byte-identical to `--version` output - _Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3_ - _Boundary: worker/main.c_ - _Depends: 3.1_ - [x] 4. Core: Go binary version display - [x] 4.1 go_build script injects ldflags from environment - `src/tools/go_build` reads `XCE_GIT_COMMIT`/`XCE_BUILD_TIME` via `[ -z ... ]` (correctly empty-safe, not just unset-safe) and appends `-ldflags "-X main.GitCommit=... -X main.BuildTime=..."` to its `go build` invocation, falling back to `unknown` - Confirmed (research finding, independently reproduced by review): `-X` on a `main` package var that doesn't exist yet is silently ignored by the Go linker, not a build error -- this task has no real ordering dependency on 4.4/4.5 - Observable: invoking `go_build` with neither env var set still produces a working binary whose injected vars read `unknown` - _Requirements: 1.1, 1.2, 1.4, 4.1, 4.2_ - _Boundary: go_build script_ - [x] 4.2 apiserver and xcecli meson builds pass resolved values through - Both meson.build files pass `XCE_GIT_COMMIT`/`XCE_BUILD_TIME` (from `get_option('commit_id')`/`get_option('build_time')`) into `go_build`'s env, UNQUOTED (go_build's `fix_input_envs()` only quote-strips `CURRENT_SOURCE_DIR`/`BUILD_ROOT`, not these two -- quoting them would embed literal `"` chars and corrupt the `-ldflags` argument) - Verified live (independently, by review): real commit id + build time reach `go build`'s actual invocation and are embedded in both binaries' Go build-info metadata (`go version -m`) - Observable partially deferred by design: no `main.GitCommit`/`main.BuildTime` symbols exist in either binary yet (`go tool nm` confirms) since apiserver/xcecli's `main` packages don't declare those vars until tasks 4.4/4.5 -- this task's own scope (`_Depends: 4.1_` only) is the env-var plumbing itself, not the full round trip; task 6.2 owns re-verifying that once 4.4 lands - _Requirements: 1.1, 1.2, 4.1, 4.2_ - _Boundary: apiserver/meson.build, xcecli/meson.build_ - _Depends: 4.1_ - [x] 4.3 (P) Makefile dev-loop path computes and passes the same values - `GIT_COMMIT_HASH`/`GIT_STATUS`/`GIT_COMMIT`/`BUILD_TIME`/`VERSION_LDFLAGS` (`:=`, computed once per invocation) mirror `xce-build.sh`'s dirty-check semantics and `go_build`'s exact `-ldflags` shape; `build-cli`/`build-server` recipes reference `$(VERSION_LDFLAGS)` - `unknown` fallback verified via empty-output detection (robust for `git rev-parse --short HEAD` specifically: never succeeds with empty stdout) - `--version` CLI output not yet observable (tasks 4.4/4.5 not landed) -- verified instead via `go version -m`'s embedded build-info metadata, matching live `git rev-parse --short HEAD` + dirty suffix - `make build-server`'s pre-existing CGO failure (bug-1597) independently confirmed unrelated via a stash A/B (identical failure on the unmodified Makefile) -- not a regression - _Requirements: 1.1, 1.2, 1.3, 1.4, 4.1, 4.2, 5.1, 5.2_ - _Boundary: Makefile_ - _Depends: 4.1_ - [x] 4.4 (P) apiserver: version flag, vars, and startup log - `GitCommit`/`BuildTime` package vars (default `unknown`); `-version` flag checked before `utils.PromptForTopDir`/`utils.InitLogger()`, zero filesystem side effects (live-verified: no logs dir created) - Startup log extended via `zap.String("gitCommit", ...)`/`zap.String("buildTime", ...)` structured fields on the existing `"Initialize Logger done!"` line, per design.md's explicit mandate - Observable confirmed live (independently re-verified by review, full round trip): `-version` and the startup log's first line are byte-identical (`45c9b026-dirty`, matching `git rev-parse --short HEAD` + dirty suffix, and a real UTC timestamp) - _Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2_ - _Boundary: apiserver/cmd/main.go_ - _Depends: 4.1_ - [x] 4.5 (P) xcecli: version field and vars - `GitCommit`/`BuildTime` vars in `main.go`; `router.go`'s `Execute()` extended with a `version string` parameter, setting `rootCmd.Version` before `rootCmd.Execute()` runs (the sole call site, confirmed by both implementer and review) - Cobra's native `--version`/`-v` auto-registers once `rootCmd.Version != ""` (confirmed via reading Cobra v1.9.1 source directly) -- zero additional flag-parsing code, as designed - Observable confirmed live (independently re-verified by review, both build paths): `xcecli --version` prints commit id (matching `git rev-parse --short HEAD` + dirty suffix) and build time, exits 0 - xcecli's startup log deliberately NOT extended -- Requirement 3 scopes that obligation to the four daemons only; xcecli is a one-shot CLI (design.md corrected to match) - _Requirements: 1.1, 1.2, 2.1_ - _Boundary: xcecli/cmd/main.go, xcecli/router/router.go_ - _Depends: 4.1_ - [x] 5. Core: xceadmin version display - [x] 5.1 (P) meson-build.sh generates a build-info module before packaging - `commit_id`/`build_time` passed as 2 new positional args (`$6`/`$7`, matching this custom_target's existing all-positional style); write block sits between schema-sync and `pip install`, with an independent-per-field `unknown` fallback and a file-existence guard checking the exact path it just wrote (the mirrored tree, not the real source tree) - Observable confirmed live (full real build+test+wheel cycle, independently re-run by review): mirrored tree's `_build_info.py` and the packaged wheel both contain real, matching, non-`unknown` values; 258 tests pass, 3 skips confirmed as documented live-HA e2e tests - _Requirements: 1.1, 1.2, 1.4, 4.1, 4.2_ - _Boundary: xceadmin/meson.build, xceadmin/meson-build.sh_ - _Depends: 1_ - [x] 5.2 (P) Committed fallback stub and CLI version wiring - Committed `src/xceadmin/src/xceadmin/_build_info.py` with `GIT_COMMIT`/`BUILD_TIME` both `"unknown"` -- exact variable names verified identical to 5.1's `meson-build.sh`-generated module; `cli.py`'s import is a genuine `try/except ImportError` (inline fallback class), not a bare import - `click.version_option()` wired directly on the top-level `@click.group() def cli()`; version string composed via `importlib.metadata.version("xceadmin")` plus both `_build_info` fields, no hand-parsed `pyproject.toml` - Observable confirmed live (independently re-verified by review, both install paths): fresh scratch-venv editable install (`pip install --no-deps -e .`, no meson) -> `xceadmin --version` prints `0.1.0 (unknown, unknown)`, exit 0, no import error; the meson-built wheel installed into a separate scratch venv -> `--version` shows real, non-`unknown` commit id + build time. Full meson build/test: 261 passed, 3 skipped (258 baseline + 3 new tests) - _Requirements: 1.1, 1.2, 2.1_ - _Boundary: xceadmin/src/xceadmin_ - _Depends: 5.1_ - [x] 6. Integration: cross-toolchain consistency and regression verification - [x] 6.1 Motivating-incident regression test - New standalone script `src/tools/test-build-version-regression.sh` (follows the existing `test-commit-durability.sh`/`test-heartbeat-tolerance.sh` convention: real-build regression check, not a meson `test()`) runs real `xce-build.sh scheduler` builds in fresh dirs for both `asan` and `debug`, extracts the commit id from `xce_scheduler --version`, and compares it against a live `git rev-parse --short HEAD` (+`-dirty`, using the identical dirty-detection logic as `xce-build.sh` itself) - Deliberately tolerates only `xce-build.sh`'s own known-unrelated flaky-test exit code (bug-1560) via captured exit status, not a blanket `set -e` bypass -- a real compile failure still fails the check, since fresh build dirs mean no stale binary can survive to fool the binary-existence + `--version` gate - Observable confirmed live (independently re-run twice, by both implementer and review, from-scratch builds each time): both asan and debug profiles PASS with a real, matching, non-`unknown` commit id; a built-in demonstration block proves the comparison logic correctly fails loudly on a wrong hash, an empty commit id, and the literal `unknown` fallback - _Requirements: 4.1, 4.2, 4.3, 5.1, 5.2_ - _Depends: 2.1, 3.2, 3.3_ - [x] 6.2 Version/startup-log consistency test - New standalone script `src/tools/test-version-startup-consistency.sh` (same house style as task 6.1's `test-build-version-regression.sh`) builds `xce_scheduler` and `xce_apiserver` for real, starts each daemon, greps its startup log, and asserts the commit-id/build-time VALUES (not whole-line text, since the C daemon's plain-text log line and apiserver's structured zap-JSON fields are differently shaped by design) are identical between `--version`/`-version` output and the startup log - apiserver's build routes around two pre-existing, unrelated, independently-reproduced sandbox blockers -- `make build-server`'s missing CGO env (bug-1597) and `xce-build.sh apiserver`'s `go_build`-hardcoded `GOPROXY=goproxy.cn` hanging in this sandbox (confirmed live by review via orphaned processes) -- via a direct `go build -mod=readonly -ldflags` invocation computing `commit_id`/`build_time` with the identical logic and injecting them via the identical `-X` shape `go_build` itself uses; only the orchestration layer differs, not what's injected or how task 4.4's unmodified `main.go` consumes it - Observable confirmed live (independently re-run in full by both implementer and review, real builds + real daemon startups each time): scheduler and apiserver both PASS with matching, real commit id + build time; a demonstration block proves the check correctly rejects a commit-id mismatch, a build-time mismatch, and an empty field on either side - _Requirements: 2.2_ - _Depends: 3.2, 4.4_ - [x] 6.3 xceadmin end-to-end build/install/version cycle - New standalone script `src/tools/test-xceadmin-build-install-version.sh` (same house style as tasks 6.1/6.2) runs the full real `xce-build.sh xceadmin` cycle (schema sync, venv install, pytest, wheel build), installs the resulting wheel into a fresh scratch venv, and separately does an editable install (`pip install -e src/xceadmin`) into a second fresh scratch venv - Explicit differ-assertion (not just two independent pass/fail checks) that the wheel install's and editable install's build-info fields are NOT equal -- proving the generated module genuinely replaces the fallback rather than both installs coincidentally agreeing - Observable confirmed live (independently re-run in full by both implementer and review, real build+two installs each time): wheel install shows real, non-`unknown` commit id + build time; editable install shows the committed `unknown`/`unknown` fallback; a demonstration block proves the check rejects a wheel install showing `unknown`, an editable install showing a real-looking hash, and -- the crux case -- two identical build-info outputs fed to the differ-check - _Requirements: 1.1, 1.2, 2.1, 4.1, 4.2_ - _Depends: 5.1, 5.2_ ## Implementation Notes - **Task 2.2**: `builtins.currentTime` cannot be used for anything feeding a `nix build` derivation without `--impure` — Nix's flake pure-eval mode strips it entirely, breaking the plain, documented `nix build .#xcecore`/`.#xcecore_debug` commands. It also yields a raw epoch-seconds integer, not UTC ISO-8601. Any nix-side build metadata that must vary per-build (timestamps, etc.) belongs in a derivation's `preConfigure` (or similar) build-phase shell hook using ordinary coreutils (`date -u`), appended to `mesonFlagsArray` — never in eval-time Nix expressions. `self.shortRev`/`self.dirtyShortRev` remain the correct, eval-time-safe mechanism specifically for the commit id (Nix computes those specially even under pure-eval), so this distinction is per-value, not a blanket "avoid nix expressions" rule.