Adds src/tools/test-xceadmin-build-install-version.sh (same house style as tasks 6.1/6.2), which runs xceadmin's full real meson-driven build cycle (schema sync, venv install, pytest, wheel build), installs the resulting wheel into a fresh scratch venv, and separately does an editable install into a second fresh scratch venv. Asserts the wheel install carries real, non-"unknown" commit id and build time while the editable install carries the committed "unknown"/"unknown" fallback stub, and explicitly asserts the two differ -- proving the generated build-info module genuinely replaces the fallback rather than both installs coincidentally agreeing. Verified independently by review with a full from-scratch re-run: 261 passed/3 skipped (matching tasks 5.1/5.2's established baseline), both install paths correct, and the demonstration block's crux case (identical build-info values must be rejected by the differ-check) confirmed correct. This is the final task of the build-time-versioning spec; all 13 sub-tasks are now complete. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
17 KiB
Implementation Plan
-
1. Foundation: meson build-time option and C project-wide define
- Add a
build_timemeson option (string, default'') alongside the existingcommit_idoption insrc/meson.options - Extend
src/meson.build's existingadd_project_arguments()call to also defineVERSION_BUILD_TIMEfor every C component, mirroring howVERSION_COMMIT_IDis already threaded through - Observable: a plain
meson setupwith neither option set still configures successfully (both defines resolve to empty string, no build failure) - Requirements: 1.1, 1.2, 1.4, 4.3
- Add a
-
2. Foundation: compute real values at both build entry points
-
2.1 (P) xce-build.sh computes and passes commit id and build time
- Compute
commit_idviagit rev-parse --short HEAD, appended with-dirtywhen the tree has uncommitted changes (git status --porcelainorgit diff --quiet HEAD --) - Compute
build_timeviadate -u +%Y-%m-%dT%H:%M:%SZ - Fall back to the literal string
unknownforcommit_idwhengititself fails (e.g. no.gitpresent) - Append
-Dcommit_id="$commit_id" -Dbuild_time="$build_time"toMESON_OPTSfor every build profile, before the existingmeson setup/meson setup --reconfigurecalls - After computing both values, verify each is non-empty (
build_timealways is;commit_idfalls back tounknownonly on a genuinegitfailure) 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 debugfollowed by the resulting binary's--versionshows a commit id matching the currentgit rev-parse --short HEADat 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
- Compute
-
2.2 (P) Nix packages compute and pass build time
Add a— corrected during review (round 1 REJECTED):builtins.currentTime-derived build-time value insrc/flake.nixbuiltins.currentTimebreaks pure-evalnix build(requires--impure) and yields non-UTC epoch seconds. Implemented instead as apreConfigurebuild-phasedate -u +%Y-%m-%dT%H:%M:%SZhook inxce.nix/xcecli.nixappending tomesonFlagsArray, alongside the unchanged eval-time-Dcommit_id=wiring- Fail loudly (non-empty guard) if
date -uever produces empty output, before the value reachesmesonFlagsArray - Observable:
nix build .#xcecore_debugproduces a binary whose--versionshows a non-empty, ISO-8601 UTC build time, without anygitinvocation 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
-
3. Core: C daemon version display
-
3.1 Extend the shared version macro with build time and unknown fallback
Extend—MK_VERSION/STRMK_VERSION/STRcannot express the empty-string-to-unknownsubstitution at the preprocessor level, so implemented as new runtime functionsxce_version_field()/xce_compose_version()(utils.h/utils.c) instead;MK_VERSION/STRmarked SUPERSEDED with an explicit doc comment and handoff notes for 3.2 (round 1 REJECTED for silently orphaningMK_VERSION+ undocumentedargp_program_versionfile-scope-const-initializer trap; round 2 APPROVED after adding the SUPERSEDED comment + NOTE FOR CALLERS guidance)- Both fields render as
unknownwhen their underlying define is empty (verified via"", not justNULL), never a blank/garbled string - Observable: a build with both meson options unset produces a
--versionstring containingunknownfor both fields, not empty gaps -- 5 new unit tests intest_utils.cconfirm this - Requirements: 1.4, 2.2
- Boundary: utils.h
-
3.2 (P) Controller and scheduler: version in --version and startup log
argp_program_versionmigrated toxce_compose_version()via astatic char version_buf[128]computed and assigned beforeargp_parse()in all 3 files (controller/main.c, scheduler/main.c, test.c); zero remainingMK_VERSIONcallers 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 (--peersand 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, andxce_scheduler --versionall print commit id + build time and exit; all startup log branches for both daemons are byte-identical to their own--versionoutput - 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
-
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 computesxce_compose_version()into astatic char version_buf[128]and returns before any startup work (check_user()/logdir/xcecd_init/xcecd_start) - Both startup INFO log branches (
--peersand no---peers) extended to log the same buffer - Observable confirmed live (independently re-verified by review):
xcecd --versionprints and exits; both startup log branches (captured via a realsudodaemon run) are byte-identical to--versionoutput - Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3
- Boundary: worker/main.c
- Depends: 3.1
- Added
-
4. Core: Go binary version display
-
4.1 go_build script injects ldflags from environment
src/tools/go_buildreadsXCE_GIT_COMMIT/XCE_BUILD_TIMEvia[ -z ... ](correctly empty-safe, not just unset-safe) and appends-ldflags "-X main.GitCommit=... -X main.BuildTime=..."to itsgo buildinvocation, falling back tounknown- Confirmed (research finding, independently reproduced by review):
-Xon amainpackage 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_buildwith neither env var set still produces a working binary whose injected vars readunknown - Requirements: 1.1, 1.2, 1.4, 4.1, 4.2
- Boundary: go_build script
-
4.2 apiserver and xcecli meson builds pass resolved values through
- Both meson.build files pass
XCE_GIT_COMMIT/XCE_BUILD_TIME(fromget_option('commit_id')/get_option('build_time')) intogo_build's env, UNQUOTED (go_build'sfix_input_envs()only quote-stripsCURRENT_SOURCE_DIR/BUILD_ROOT, not these two -- quoting them would embed literal"chars and corrupt the-ldflagsargument) - 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.BuildTimesymbols exist in either binary yet (go tool nmconfirms) since apiserver/xcecli'smainpackages 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
- Both meson.build files pass
-
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) mirrorxce-build.sh's dirty-check semantics andgo_build's exact-ldflagsshape;build-cli/build-serverrecipes reference$(VERSION_LDFLAGS)unknownfallback verified via empty-output detection (robust forgit rev-parse --short HEADspecifically: never succeeds with empty stdout)--versionCLI output not yet observable (tasks 4.4/4.5 not landed) -- verified instead viago version -m's embedded build-info metadata, matching livegit rev-parse --short HEAD+ dirty suffixmake 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
-
4.4 (P) apiserver: version flag, vars, and startup log
GitCommit/BuildTimepackage vars (defaultunknown);-versionflag checked beforeutils.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):
-versionand the startup log's first line are byte-identical (45c9b026-dirty, matchinggit 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
-
4.5 (P) xcecli: version field and vars
GitCommit/BuildTimevars inmain.go;router.go'sExecute()extended with aversion stringparameter, settingrootCmd.VersionbeforerootCmd.Execute()runs (the sole call site, confirmed by both implementer and review)- Cobra's native
--version/-vauto-registers oncerootCmd.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 --versionprints commit id (matchinggit 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
-
5. Core: xceadmin version display
-
5.1 (P) meson-build.sh generates a build-info module before packaging
commit_id/build_timepassed as 2 new positional args ($6/$7, matching this custom_target's existing all-positional style); write block sits between schema-sync andpip install, with an independent-per-fieldunknownfallback 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.pyand the packaged wheel both contain real, matching, non-unknownvalues; 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
-
5.2 (P) Committed fallback stub and CLI version wiring
- Committed
src/xceadmin/src/xceadmin/_build_info.pywithGIT_COMMIT/BUILD_TIMEboth"unknown"-- exact variable names verified identical to 5.1'smeson-build.sh-generated module;cli.py's import is a genuinetry/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 viaimportlib.metadata.version("xceadmin")plus both_build_infofields, no hand-parsedpyproject.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 --versionprints0.1.0 (unknown, unknown), exit 0, no import error; the meson-built wheel installed into a separate scratch venv ->--versionshows real, non-unknowncommit 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
- Committed
-
6. Integration: cross-toolchain consistency and regression verification
-
6.1 Motivating-incident regression test
- New standalone script
src/tools/test-build-version-regression.sh(follows the existingtest-commit-durability.sh/test-heartbeat-tolerance.shconvention: real-build regression check, not a mesontest()) runs realxce-build.sh schedulerbuilds in fresh dirs for bothasananddebug, extracts the commit id fromxce_scheduler --version, and compares it against a livegit rev-parse --short HEAD(+-dirty, using the identical dirty-detection logic asxce-build.shitself) - Deliberately tolerates only
xce-build.sh's own known-unrelated flaky-test exit code (bug-1560) via captured exit status, not a blanketset -ebypass -- a real compile failure still fails the check, since fresh build dirs mean no stale binary can survive to fool the binary-existence +--versiongate - 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-
unknowncommit id; a built-in demonstration block proves the comparison logic correctly fails loudly on a wrong hash, an empty commit id, and the literalunknownfallback - Requirements: 4.1, 4.2, 4.3, 5.1, 5.2
- Depends: 2.1, 3.2, 3.3
- New standalone script
-
6.2 Version/startup-log consistency test
- New standalone script
src/tools/test-version-startup-consistency.sh(same house style as task 6.1'stest-build-version-regression.sh) buildsxce_schedulerandxce_apiserverfor 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/-versionoutput 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) andxce-build.sh apiserver'sgo_build-hardcodedGOPROXY=goproxy.cnhanging in this sandbox (confirmed live by review via orphaned processes) -- via a directgo build -mod=readonly -ldflagsinvocation computingcommit_id/build_timewith the identical logic and injecting them via the identical-Xshapego_builditself uses; only the orchestration layer differs, not what's injected or how task 4.4's unmodifiedmain.goconsumes 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
- New standalone script
-
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 realxce-build.sh xceadmincycle (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-
unknowncommit id + build time; editable install shows the committedunknown/unknownfallback; a demonstration block proves the check rejects a wheel install showingunknown, 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
- New standalone script
Implementation Notes
- Task 2.2:
builtins.currentTimecannot be used for anything feeding anix buildderivation without--impure— Nix's flake pure-eval mode strips it entirely, breaking the plain, documentednix build .#xcecore/.#xcecore_debugcommands. 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'spreConfigure(or similar) build-phase shell hook using ordinary coreutils (date -u), appended tomesonFlagsArray— never in eval-time Nix expressions.self.shortRev/self.dirtyShortRevremain 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.