Adds xce_version_field()/xce_compose_version() (utils.h/utils.c) to compose a version string carrying commit id + build time, rendering "unknown" for either field when its underlying meson-supplied define is empty -- ready for controller/scheduler/worker to consume. MK_VERSION/STR (the pre-existing, compile-time-only macro pair) can't express this empty-to-"unknown" substitution at the preprocessor level, so this is a runtime mechanism instead. Round 1 review REJECTED an earlier version of this task for silently orphaning MK_VERSION/STR (the task explicitly asked to "extend" them) and for leaving an undocumented architectural trap: argp_program_version is a file-scope const char* initialized at compile time, which cannot call a runtime function -- consuming code must instead compute into a static buffer inside main() and assign the pointer before argp_parse() runs. Round 2 adds an explicit SUPERSEDED doc comment on MK_VERSION/STR plus NOTE FOR CALLERS guidance covering both points, and was APPROVED. Also expands task 3.2's boundary to include controller/testsuite/test.c (a third MK_VERSION/STR call site discovered during review) so no caller of the superseded macro survives implementation. 5 new unit tests in test_utils.c (RED->GREEN, TDD). Task 3.1/13 of build-time-versioning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
13 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
- Update each daemon's
argp_program_versionconstruction to usexce_compose_version()/xce_version_field()from 3.1 (NOT the now-supersededMK_VERSION/STR) -- sinceargp_program_versionis a file-scopeconst char*global (mutable pointer, notconst char* const) currently initialized at compile time, this requires computing the composed string into a static buffer insidemain()and assigning the pointer there, beforeargp_parse()runs, rather than a one-line macro swap - Add the version string to each daemon's existing first startup INFO log line (controller's
"My name is %s"line, scheduler's"HA cluster config..."line) rather than a second, independently-formatted line src/controller/testsuite/test.c:662is a THIRDMK_VERSION/STRcall site (thexce_testREPL driver's own version output) discovered during 3.1's review, outside this sub-task's original scope -- migrate it too in this same task (same file family, same daemon, same minimal fix shape), so no caller of the superseded macro remains after this task lands- Observable:
xce_controller --versionandxce_scheduler --versioneach print commit id + build time and exit without starting the daemon; each daemon's startup log's first lines include the identical two values;xce_test --version(or equivalent) also reflects the new composed string, not the old commit-id-only one - 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
- Update each daemon's
-
3.3 (P) Worker: add --version and startup log line
- Add a long-only
--versionoption to worker'sgetopt_long()option table and a handling case that prints the same macro output and exits before any other startup work - Add the version string to worker's existing first startup INFO log line (its own
"HA cluster config..."line) - Observable:
xcecd --versionprints commit id + build time and exits without starting the daemon; the startup log's first lines include the identical two values - Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3
- Boundary: worker/main.c
- Depends: 3.1
- Add a long-only
-
4. Core: Go binary version display
-
4.1 go_build script injects ldflags from environment
src/tools/go_buildreadsXCE_GIT_COMMIT/XCE_BUILD_TIMEfrom its environment (both optional, empty-safe) and appends-ldflags "-X main.GitCommit=... -X main.BuildTime=..."to itsgo buildinvocation, falling back tounknownfor any unset value- 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
src/frontend/apiserver/meson.buildandsrc/frontend/xcecli/meson.buildread the project'scommit_id/build_timemeson options and pass them asXCE_GIT_COMMIT/XCE_BUILD_TIMEenv vars into their existingcustom_target()calls- Observable: a meson-driven
/xce-build apiserver(with 2.1's values set) produces anxce_apiserverbinary whose linked-in vars are non-unknown - Requirements: 1.1, 1.2, 4.1, 4.2
- Boundary: apiserver/meson.build, xcecli/meson.build
- Depends: 4.1
-
4.3 (P) Makefile dev-loop path computes and passes the same values
src/frontend/Makefile'sbuild-server/build-clitargets compute commit id (git, dirty-suffixed) and build time (date) directly and pass the identical-ldflagsshape 4.1 establishes, independent of meson- Fall back to
unknownfor commit id whengititself fails (e.g. no.gitpresent), matching 2.1's/4.1's fallback discipline - Observable:
make build-serverandmake build-clieach produce a binary whose--version/-versionoutput matches the same commit idgit rev-parse --short HEADreports at build time - 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
- Declare
GitCommit/BuildTimepackage vars (defaultunknown) insrc/frontend/apiserver/cmd/main.go - Add a
-versionflag, checked beforeutils.InitLogger(), that prints both values and exits with no logging side effects - Extend the existing
logger.Info("********** Initialize Logger done!")call site to also log both values - Observable:
xce_apiserver -versionprints both values and exits without starting the server; the startup log's first lines include the identical two values - Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2
- Boundary: apiserver/cmd/main.go
- Depends: 4.1
- Declare
-
4.5 (P) xcecli: version field and vars
- Declare the same two package vars in
src/frontend/xcecli/cmd/main.go; set Cobra's built-inrootCmd.Versionfield from them once at init - Observable:
xcecli --version(Cobra's native handling) prints both values and exits - Requirements: 1.1, 1.2, 2.1
- Boundary: xcecli/cmd/main.go
- Depends: 4.1
- Declare the same two package vars in
-
5. Core: xceadmin version display
-
5.1 (P) meson-build.sh generates a build-info module before packaging
src/xceadmin/meson.build'scustom_target()passes the resolved commit id/build time tomeson-build.sh(new positional args or env vars)meson-build.shwrites a small_build_info.py(containingGIT_COMMIT/BUILD_TIMEstring constants,unknownfallback) into the mirrored source tree's xceadmin package directory, before the existingpip installstep, matching the schema-sync step's own before-install ordering- After writing the module, verify it actually landed in the mirrored tree before proceeding to
pip install(e.g. a file-exists check) and fail the build loudly if it did not, rather than silently packaging the fallback-shaped-but-wrong wheel - Observable: a meson-driven
/xce-build xceadminproduces a wheel whose installed_build_info.pycontains non-unknownvalues when 2.1's values are set - 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
- Commit a fallback
src/xceadmin/src/xceadmin/_build_info.pywith both fields set tounknown(used whenever 5.1's generated version isn't present, e.g.pip install -e .) - Wire
click.version_option()oncli.py's top-level group, importing_build_infodefensively and composing its version string frompyproject.toml's static version plus the two build-info fields - Observable: both
pip install -e .(no meson) and the meson-built wheel produce a workingxceadmin --versionthat prints and exits; the editable-install case showsunknownfor both build fields, never an import error - Requirements: 1.1, 1.2, 2.1
- Boundary: xceadmin/src/xceadmin
- Commit a fallback
-
6. Integration: cross-toolchain consistency and regression verification
-
6.1 Motivating-incident regression test
- Build a C daemon via
/xce-build(any profile) and confirm its--versioncommit id is non-unknownand matchesgit rev-parse --short HEADat build time -- the exact scenario that was previously silent/empty - Repeat against at least the
asananddebugprofiles specifically (the motivating incident was adebug-profile binary), confirming both carry accurate, matching values - Observable: an automated check fails loudly if
xce-build.sh's computed commit id ever regresses to empty/missing, in either profile - Requirements: 4.1, 4.2, 4.3, 5.1, 5.2
- Depends: 2.1, 3.2, 3.3
- Build a C daemon via
-
6.2 Version/startup-log consistency test
- For one C daemon (controller or scheduler) and one Go binary (apiserver), capture the startup log's version line and separately run
--version/-version, asserting the two report byte-identical commit id and build time - Observable: a passing test proves
--versionand the startup log can never disagree for the same artifact - Requirements: 2.2
- Depends: 3.2, 4.4
- For one C daemon (controller or scheduler) and one Go binary (apiserver), capture the startup log's version line and separately run
-
6.3 xceadmin end-to-end build/install/version cycle
- Run xceadmin's full meson-driven build (schema sync, venv install, pytest, wheel build), install the resulting wheel, and confirm
xceadmin --versionreports the generated (non-unknown) build-info fields - Observable: the installed wheel's
--versionoutput differs from an editable (pip install -e .) install'sunknown-only output, proving the generated module actually replaces the fallback - Requirements: 1.1, 1.2, 2.1, 4.1, 4.2
- Depends: 5.1, 5.2
- Run xceadmin's full meson-driven build (schema sync, venv install, pytest, wheel build), install the resulting wheel, and confirm
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.