myxce/.kiro/specs/build-time-versioning/tasks.md
Rongsong Shen fb09afe6be
feat(build-time-versioning): xcecli --version via Cobra rootCmd.Version
Adds GitCommit/BuildTime vars to xcecli's main.go. Cobra's rootCmd is
declared unexported in router.go (a different file than the task's
original main.go-only boundary), so main.go can't reach it directly --
resolved by extending router.go's sole exported entry point,
Execute(cm, version), to accept the composed version string and set
rootCmd.Version before rootCmd.Execute() runs. Cobra then auto-registers
--version/-v natively once rootCmd.Version is non-empty (confirmed via
reading Cobra v1.9.1 source directly) -- zero additional flag-parsing
code, as designed.

Live-verified via both build paths (meson xce-build.sh and make
build-cli): real commit id (matching git rev-parse --short HEAD plus
dirty suffix) and build time, exit 0.

xcecli's startup log deliberately not extended -- Requirement 3 scopes
that to the four daemons only; xcecli is a one-shot CLI. Corrects a
stale design.md line that had assumed otherwise.

Task 4.5/13 -- closes out the Go binary version display phase (tasks
4.1-4.5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 14:07:38 +00:00

14 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 — 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
  • 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
  • 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
  • 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 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
  • 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
  • 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
  • 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
  • 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
  • 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.