myxce/.kiro/specs/build-time-versioning/tasks.md
Rongsong Shen d08fda76a5
feat(build-time-versioning): nix packages compute build time at build phase
First attempt used builtins.currentTime at flake-evaluation time -- this
broke the plain, documented `nix build .#xcecore_debug` command entirely
(pure-eval strips builtins.currentTime, requiring --impure) and produced
a raw epoch-seconds string instead of UTC ISO-8601. Caught by task-local
review (REJECTED, round 1) before merge.

Fixed by moving the computation into each derivation's own build phase:
a preConfigure hook in xce.nix/xcecli.nix runs `date -u` (stdenv's
bundled coreutils, unaffected by pure-eval) and appends -Dbuild_time= to
mesonFlagsArray, with a fail-loud guard against empty output. commit_id's
existing self.shortRev/self.dirtyShortRev eval-time mechanism is
untouched. Verified via nix eval (no --impure) and nix build reaching
configure with real values on both xcecore_debug and xcecli (round 2,
APPROVED).

Task 2.2/13 of build-time-versioning. Also corrects design.md/research.md,
which described the now-superseded builtins.currentTime approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:32:34 +00:00

12 KiB

Implementation Plan

  • 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
  • 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_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
  • 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
  • 3. Core: C daemon version display

  • 3.1 Extend the shared version macro with build time and unknown fallback

    • Extend MK_VERSION/STR in src/include/utils.h so the composed string carries both commit id and build time
    • Both fields render as unknown when their underlying define is empty, 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
    • 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_version construction to use the extended macro from 3.1
    • 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
    • Observable: xce_controller --version and xce_scheduler --version each print commit id + build time and exit without starting the daemon; each daemon's 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: controller/main.c, scheduler/main.c
    • Depends: 3.1
  • 3.3 (P) Worker: add --version and startup log line

    • Add a long-only --version option to worker's getopt_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 --version prints 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
  • 4. Core: Go binary version display

  • 4.1 go_build script injects ldflags from environment

    • src/tools/go_build reads XCE_GIT_COMMIT/XCE_BUILD_TIME from its environment (both optional, empty-safe) and appends -ldflags "-X main.GitCommit=... -X main.BuildTime=..." to its go build invocation, falling back to unknown for any unset value
    • 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
  • 4.2 apiserver and xcecli meson builds pass resolved values through

    • src/frontend/apiserver/meson.build and src/frontend/xcecli/meson.build read the project's commit_id/build_time meson options and pass them as XCE_GIT_COMMIT/XCE_BUILD_TIME env vars into their existing custom_target() calls
    • Observable: a meson-driven /xce-build apiserver (with 2.1's values set) produces an xce_apiserver binary 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's build-server/build-cli targets compute commit id (git, dirty-suffixed) and build time (date) directly and pass the identical -ldflags shape 4.1 establishes, independent of meson
    • Fall back to unknown for commit id when git itself fails (e.g. no .git present), matching 2.1's/4.1's fallback discipline
    • Observable: make build-server and make build-cli each produce a binary whose --version/-version output matches the same commit id git rev-parse --short HEAD reports 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/BuildTime package vars (default unknown) in src/frontend/apiserver/cmd/main.go
    • Add a -version flag, checked before utils.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 -version prints 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
  • 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-in rootCmd.Version field 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
  • 5. Core: xceadmin version display

  • 5.1 (P) meson-build.sh generates a build-info module before packaging

    • src/xceadmin/meson.build's custom_target() passes the resolved commit id/build time to meson-build.sh (new positional args or env vars)
    • meson-build.sh writes a small _build_info.py (containing GIT_COMMIT/BUILD_TIME string constants, unknown fallback) into the mirrored source tree's xceadmin package directory, before the existing pip install step, 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 xceadmin produces a wheel whose installed _build_info.py contains non-unknown values 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.py with both fields set to unknown (used whenever 5.1's generated version isn't present, e.g. pip install -e .)
    • Wire click.version_option() on cli.py's top-level group, importing _build_info defensively and composing its version string from pyproject.toml's static version plus the two build-info fields
    • Observable: both pip install -e . (no meson) and the meson-built wheel produce a working xceadmin --version that prints and exits; the editable-install case shows unknown for both build fields, never an import error
    • Requirements: 1.1, 1.2, 2.1
    • Boundary: xceadmin/src/xceadmin
  • 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 --version commit id is non-unknown and matches git rev-parse --short HEAD at build time -- the exact scenario that was previously silent/empty
    • Repeat against at least the asan and debug profiles specifically (the motivating incident was a debug-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
  • 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 --version and the startup log can never disagree for the same artifact
    • Requirements: 2.2
    • Depends: 3.2, 4.4
  • 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 --version reports the generated (non-unknown) build-info fields
    • Observable: the installed wheel's --version output differs from an editable (pip install -e .) install's unknown-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

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.