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>
8.8 KiB
8.8 KiB
Research & Design Decisions
Summary
- Feature:
build-time-versioning - Discovery Scope: Extension (existing, partial C-side infrastructure; zero Go/Python infrastructure)
- Key Findings:
- Controller and scheduler already have a working
--version/-Vflag (GNUargpauto-generates it fromargp_program_version = MK_VERSION("xce v0.0.1-", STR(VERSION_COMMIT_ID)), bothmain.cs) and a project-wideVERSION_COMMIT_IDpreprocessor define wired through meson (src/meson.build:30,src/meson.options'scommit_idoption) — but it carries no build timestamp, andxce-build.shnever sets-Dcommit_id=, so every locally-built binary (the entire/xce-builddev loop, including the one that produced the stale binary in the motivating incident) gets an empty commit id today. - Worker uses plain
getopt_long(), notargp— it has no--versionat all. gitis deliberately not a nix devShell buildInput (src/nixSupport/xce.nix) — the existingcommit_idmechanism avoids shelling out to git entirely; the two production nix packages (xce.nix:109,xcecli.nix:30) derive it from the flake's ownself.shortRev/self.dirtyShortRevintrospection instead. Any new mechanism must respect this: norun_command/custom_targetinmeson.buildmay shell out togit.- Go (apiserver, xcecli) and Python (xceadmin) have zero version infrastructure — no
-ldflagsinjection anywhere insrc/tools/go_buildorsrc/frontend/Makefile, noclick.version_option, no dynamicpyproject.tomlversion.
- Controller and scheduler already have a working
Research Log
Where does xce-build.sh (the actual dev-loop path) get its build metadata from?
- Context: the motivating incident happened via
/xce-build scheduler debug, notnix build— need to know which computation path is actually load-bearing for fixing it. - Findings:
xce-build.sh'sMESON_OPTSarray (set per-profile atxce-build.sh:70-72) never includes-Dcommit_id=or anything build-time related;meson setupis called with only--buildtype/-Db_sanitize.gitIS available here despite not being a declared nix devShell buildInput, becausenix develop(unlike a sandboxednix build) layers onto the ambient host PATH rather than replacing it — this project's own scripts already run from inside a real git checkout. - Implications:
xce-build.shmust be the one to compute commit id (git rev-parse --short HEAD,-dirtysuffixed viagit diff --quiet HEAD --/git status --porcelain) and build time (date -u), independently of the nix-package path, and pass both via meson options. This is the fix that actually closes the incident's gap — the nix-package path already has a working (commit-id-only) mechanism.
Message/value plumbing per toolchain
- Context: three different build tool invocations (meson C custom target-free preprocessor define, Go's
go_buildshell script +Makefile, Python'smeson-build.shcustom_target) need the same two values. - Findings:
src/meson.build:30already threadscommit_idproject-wide viaadd_project_arguments()for C.src/tools/go_build(invoked by bothapiserver/meson.build's andxcecli/meson.build'scustom_target()) already threads other env vars (e.g. CGO flags) into itsgo buildinvocation — same mechanism can carry newXCE_GIT_COMMIT/XCE_BUILD_TIMEenv vars for-ldflags -X.src/xceadmin/meson.build'scustom_targetalready passes several positional args tomeson-build.sh; two more (or two new env vars) can carry the same values for a small generated Python module written before thepip installstep. - Implications: meson itself is the single source of truth for one build invocation — it resolves the two new options (
commit_id, now actually SET byxce-build.sh; a newbuild_time) once, and every language-specific build step (C define, Go ldflags, Python generated module) reads from that same resolved value instead of independently recomputing it. Avoids the 3 toolchains ever disagreeing within one build.
Dirty-tree representation: separate flag vs. suffix convention
- Context: Requirement 1 AC3 requires distinguishing an uncommitted-changes build from an exact commit.
- Findings: both git's own
--dirtyflag and this project's existing Nixself.dirtyShortRevalready use a suffix convention (<hash>-dirty) rather than a separate boolean field. - Implications: reuse that convention —
commit_id's own string value carries-dirtywhen applicable, no third meson option/env var needed. Matches the Build-vs-Adopt lens (reuse an established convention already present twice in this codebase) and the Simplification lens (one string carries both facts).
Design Decisions
Decision: One resolved value pair, fed from two independent computation sites
- Context: production (
nix build) and dev-loop (xce-build.sh) are architecturally different environments (fully sandboxed vs. ambient-PATH devShell) that already computecommit_iddifferently for exactly that reason. - Alternatives Considered:
- Make
xce-build.shalso avoid shelling out to git, deriving everything from Nix like the packages do — rejected,xce-build.shdoesn't invoke Nix package builds at all, it drivesmesondirectly insidenix develop. - Add a git-invoking
run_command()insidemeson.builditself, used by both paths — rejected, breaks the sandboxednix buildpath where git is unavailable by design.
- Make
- Selected Approach: extend the existing split —
xce-build.shcomputes viagit+dateand passes-Dcommit_id=/-Dbuild_time=; the nix packages (xce.nix,xcecli.nix) computecommit_idviaself.shortRev/self.dirtyShortRev(unchanged, eval-time) andbuild_timevia apreConfigurebuild-phasedate -uhook appending tomesonFlagsArray(build-time, not eval-time), passed the same-Dbuild_time=way. - Rationale: matches the codebase's own existing precedent exactly; zero new dependencies; keeps the sandboxed build's reproducibility model intact.
- Trade-offs: two computation sites to keep in sync (same risk the existing
commit_id-only mechanism already carries and has lived with). - Follow-up: verify during implementation that a plain
meson setupwith neitherxce-build.shnor a nix package (e.g.meson setup buildrun by hand) degrades to the "unknown" fallback (Requirement 1 AC4), not a build failure. - Correction (task 2.2, round 1 → round 2): the nix-side value was originally attempted via
builtins.currentTime(eval-time) as noted above — this broke Nix's flake pure-eval mode entirely (nix build .#xcecore_debug, the documented no-flag command, failed witherror: attribute 'currentTime' missing, requiring--impure) and produced a raw epoch-seconds string rather than UTC ISO-8601. Caught by task-local review (REJECTED, round 1) before merge; corrected to thepreConfigure/date -ubuild-phase approach described in "Selected Approach" above, verified to restore pure-eval and produce a correctly-formatted value (round 2, APPROVED). Recorded here so this decision's rationale stays accurate for anyone reading onlyresearch.md.
Decision: Worker gets a hand-rolled --version case, not a switch to argp
- Context: worker uses
getopt_long(), notargp; controller/scheduler's free--versioncomes from argp's own auto-generated handling. - Alternatives Considered: migrate worker's arg parsing to argp for consistency — rejected as disproportionate scope (a full parser migration to fix a missing flag).
- Selected Approach: add a
{"version", no_argument, ...}long option and acase, printing the sameMK_VERSION-style string then exiting 0 — mirrors argp's own output shape without adopting argp. - Rationale: minimal, consistent output, zero risk to worker's existing option handling.
Risks & Mitigations
- Two independent computation sites (dev-loop vs. nix-package) could drift in format — mitigate by having both resolve to the exact same two meson option names and the exact same downstream consumption code, so only the value-sourcing differs, never the shape.
go_build/meson-build.share shared shell scripts already carrying other env-var-driven behavior (CGO flags, schema sync) — new env vars must be additive/optional so a caller that doesn't set them still builds (falls back to "unknown"), not required.
References
src/meson.build:30,src/meson.options— existingcommit_idmechanism.src/flake.nix:15-18,src/nixSupport/xce.nix:109,xcecli.nix:30— existing Nix-side commit id derivation.src/tools/xce-build.sh:70-98— dev-loop meson invocation, current gap.src/tools/go_build,src/frontend/apiserver/meson.build:53-62,src/frontend/xcecli/meson.build:9-17— Go build plumbing.src/xceadmin/meson.build:27-47,src/xceadmin/meson-build.sh— Python build plumbing.