feat(admin-config-tool): wire xceadmin into meson + nix build

Reopens the 2026-07-21 design decision to keep xceadmin outside meson.
Standard onboarding consistency (README 3.3 `nix build .#xcecore` and
3.4 `meson compile` working for xceadmin like every other component)
outweighs the earlier concern about meson's single-artifact model not
fitting a multi-file pip install.

- src/xceadmin/meson.build: custom_target() wrapping install+test,
  stamp-file output, build_always_stale (matches this repo's existing
  clean-rebuild-every-invocation convention).
- src/xceadmin/meson-build.sh: out-of-tree pinned install + pytest;
  stages a repo-mirror/ symlink tree so tests' fixed-relative-path
  assumptions (src/configs/, src/schema/, ha/data/) still resolve from
  a copied location, without touching their deliberate fail-loud
  behavior.
- src/meson.build: subdir('xceadmin') under component != 'xcecli'.
- src/tools/xce-build.sh: xceadmin case now delegates to
  `meson compile -C "$BUILD_DIR" xceadmin` instead of its own
  pip/venv implementation, like every other component.

Verified both build paths: meson compile (165/165 passed) and
nix build --option sandbox false .#xcecore_debug (128 passed / 37
skipped — cert-dependent tests correctly skip under nix's
git-tracked-only hermetic source). AppImage bundling (README 3.5)
is explicitly deferred to future work, not part of this change.
This commit is contained in:
Rongsong Shen 2026-07-22 10:46:45 +00:00
parent b6bf7cb8e3
commit e4fd9e1d3f
8 changed files with 129 additions and 43 deletions

View file

@ -67,6 +67,7 @@ if component != 'xcecli'
subdir('autoscaler')
subdir('scheduler')
subdir('frontend/apiserver')
subdir('xceadmin')
configure_file(
input: 'tools/appimage/entry.sh',

View file

@ -113,41 +113,24 @@ case "$COMPONENT" in
fi
;;
xceadmin)
# Python — no meson, dispatched by this case statement alone (design
# review correction: xceadmin is deliberately NOT wired into
# src/meson.build/meson.options, see .kiro/specs/admin-config-tool/
# design.md, File Structure Plan). Ignores $BUILD_DIR/$MESON_OPTS;
# $PROFILE only selects editable vs. pinned pip install below.
XCEADMIN_DIR="$SRC_DIR/xceadmin"
SCHEMA_DST="$XCEADMIN_DIR/src/xcecommon/schema_files"
echo "=== Syncing xceadmin bundled schemas ==="
mkdir -p "$SCHEMA_DST/json"
cp "$SRC_DIR/schema/controller.capnp" "$SCHEMA_DST/controller.capnp"
cp "$SRC_DIR/schema/c.capnp" "$SCHEMA_DST/c.capnp"
cp "$SRC_DIR/configs/schema/"*.schema.json "$SCHEMA_DST/json/"
cd "$XCEADMIN_DIR"
# nix's python3 site-packages lives under the read-only /nix/store,
# so `pip install` (editable or pinned) can't target it directly —
# it refuses with "externally-managed-environment" (PEP 668). Use a
# throwaway venv that inherits the nix-provided pycapnp/pyyaml/
# jsonschema/click/pytest (--system-site-packages) so pip only ever
# has to install xceadmin itself, never rebuild those from PyPI.
# Recreated on every invocation for a clean, reproducible install —
# same philosophy as setup_build's `rm -rf "$BUILD_DIR"` above.
VENV_DIR="$XCEADMIN_DIR/.venv"
rm -rf "$VENV_DIR"
python3 -m venv --system-site-packages "$VENV_DIR"
if [ "$PROFILE" = "release" ]; then
echo "=== Building xceadmin Python package (pinned, non-editable) ==="
"$VENV_DIR/bin/pip" install . --quiet
else
echo "=== Building xceadmin Python package (editable) ==="
"$VENV_DIR/bin/pip" install -e . --quiet
fi
echo "=== Running xceadmin tests ==="
"$VENV_DIR/bin/python" -m pytest tests/ -v
echo "=== Done: xceadmin (profile=$PROFILE) ==="
exit 0
# Meson-driven like every other component (src/xceadmin/meson.build,
# custom_target wrapping meson-build.sh) -- installs pinned/non-
# editable out-of-tree in the meson build dir and runs pytest,
# every invocation regardless of $PROFILE (xceadmin's suite is pure
# Python, unrelated to the ASAN/release C compiler flags $PROFILE
# otherwise controls, and fast enough — ~2s — that always running
# it costs nothing). This is a strictly stronger guarantee than the
# old editable-install-by-default behavior: every run now exercises
# real package-data resolution (bug-1460's class of bug), not just
# the `release` profile.
#
# For fast iterative dev without a full meson reconfigure, xceadmin
# can still be built standalone: cd xceadmin && pip install -e .
# (or see src/xceadmin/meson-build.sh for the exact out-of-tree
# steps this target runs).
setup_build
echo "=== Building + testing xceadmin (via meson) ==="
meson compile -C "$BUILD_DIR" xceadmin
;;
all)
setup_build

62
src/xceadmin/meson-build.sh Executable file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Meson-driven xceadmin build: install + test out-of-tree, in the meson
# build directory. Never writes into the live source checkout -- unlike
# xce-build.sh's separate xceadmin case (editable install, .venv inside
# src/xceadmin/, meant for fast dev-loop iteration), this path is what
# `meson compile`/`nix build .#xcecore` actually invoke, and both need
# the same purity guarantee every other meson subdir here already has.
#
# Several tests reach for sibling directories outside xcecommon/xceadmin's
# own tree via a fixed relative path (src/configs/, src/schema/, ha/data/
# -- e.g. test_validation.py's REPO_CONFIGS_DIR deliberately fails loud
# rather than skips if that layout assumption breaks, by design). Rather
# than touch test code, this script recreates the exact repo layout
# out-of-tree via symlinks (read-only references, never a write into the
# real tree) so every one of those relative-path assumptions still
# resolves correctly from the copied location.
set -euo pipefail
SOURCE_DIR="$1" # .../src/xceadmin (meson.current_source_dir())
BUILD_DIR="$2" # .../xceadmin (meson.current_build_dir())
REPO_ROOT="$3" # real repo root (contains ha/, src/)
STAMP_FILE="$4"
SRC_DIR="$REPO_ROOT/src"
MIRROR_DIR="$BUILD_DIR/repo-mirror"
WORK_DIR="$MIRROR_DIR/src/xceadmin"
VENV_DIR="$BUILD_DIR/.venv"
echo "=== xceadmin (meson): staging out-of-tree repo-layout mirror ==="
rm -rf "$MIRROR_DIR"
mkdir -p "$MIRROR_DIR/src"
ln -s "$REPO_ROOT/ha" "$MIRROR_DIR/ha"
ln -s "$SRC_DIR/configs" "$MIRROR_DIR/src/configs"
ln -s "$SRC_DIR/schema" "$MIRROR_DIR/src/schema"
cp -r "$SOURCE_DIR" "$WORK_DIR"
# Never trust a stray dev-loop artifact from the live checkout (xce-build.sh's
# own editable-install byproducts, if this happens to be a live checkout and
# not a nix-sandboxed source copy) -- always start from a clean tree.
rm -rf "$WORK_DIR/.venv" "$WORK_DIR/build" "$WORK_DIR"/src/*.egg-info \
"$WORK_DIR/.pytest_cache" "$WORK_DIR/.ruff_cache" \
"$WORK_DIR/src/xcecommon/schema_files"
find "$WORK_DIR" -name "__pycache__" -type d -prune -exec rm -rf {} +
SCHEMA_DST="$WORK_DIR/src/xcecommon/schema_files"
echo "=== xceadmin (meson): syncing bundled schemas ==="
mkdir -p "$SCHEMA_DST/json"
cp "$SRC_DIR/schema/controller.capnp" "$SCHEMA_DST/controller.capnp"
cp "$SRC_DIR/schema/c.capnp" "$SCHEMA_DST/c.capnp"
cp "$SRC_DIR/configs/schema/"*.schema.json "$SCHEMA_DST/json/"
echo "=== xceadmin (meson): pinned install into build-dir venv ==="
rm -rf "$VENV_DIR"
python3 -m venv --system-site-packages "$VENV_DIR"
"$VENV_DIR/bin/pip" install "$WORK_DIR" --quiet
echo "=== xceadmin (meson): running tests ==="
"$VENV_DIR/bin/python" -m pytest "$WORK_DIR/tests" -v
touch "$STAMP_FILE"
echo "=== xceadmin (meson): done ==="

34
src/xceadmin/meson.build Normal file
View file

@ -0,0 +1,34 @@
# xceadmin (admin-config-tool): Python package, no C compilation.
#
# Unlike every other subdir here, there is no single build artifact for
# meson/ninja to track -- `pip install` produces a multi-file venv tree.
# custom_target()'s output is a stamp file representing "install +
# test succeeded", following the same pattern used to wrap non-native
# build systems (cargo, npm, etc.) inside meson elsewhere in the
# ecosystem. The real work happens in meson-build.sh, out-of-tree in
# this subdir's own build directory -- never writing into the live
# source checkout, unlike xce-build.sh's separate editable-install
# fast dev-loop path (`xce-build.sh xceadmin`, still available
# standalone, but no longer xce-build.sh's OWN xceadmin implementation
# -- see tools/xce-build.sh, which now just calls this same target).
xceadmin_stamp = custom_target('xceadmin',
output: 'xceadmin-build.stamp',
# No meaningful per-file `input:` list: meson-build.sh copies the whole
# subdir tree regardless, so tracking a subset of .py files would only
# give false confidence about staleness detection. build_always_stale
# instead matches this project's existing convention (every
# xce-build.sh component does `rm -rf "$BUILD_DIR"` and rebuilds clean
# on every invocation) rather than relying on ninja's file-mtime cache.
command: [
find_program('bash'),
meson.current_source_dir() / 'meson-build.sh',
meson.current_source_dir(),
meson.current_build_dir(),
meson.current_source_dir() / '..' / '..', # repo root (contains ha/, src/)
'@OUTPUT@',
],
build_by_default: true,
build_always_stale: true,
console: true,
)

View file

@ -11,9 +11,11 @@ dependencies = [
"click>=8.0",
]
# NOT wired into meson (src/meson.build / src/meson.options) — xceadmin is
# dispatched by src/tools/xce-build.sh's case-statement pattern instead.
# See .kiro/specs/admin-config-tool/design.md, File Structure Plan.
# Wired into meson (src/xceadmin/meson.build custom_target, pulled in by
# src/meson.build's subdir('xceadmin')) so `nix build .#xcecore[_debug]`
# and `meson compile` install + test this package like every other
# component; src/tools/xce-build.sh's xceadmin case now just delegates to
# that same meson target.
[project.optional-dependencies]
test = ["pytest"]