Переглянути джерело

Add-on image: ship-the-binary model (trivial pinned rust:alpine build in CI)

The Pi is not the build host — a first-boot release build OOMs a 4 GB
Pi. The add-on image now ships the sm121 binary: CI (sm121-ha-integration)
compiles it per arch with a TRIVIAL 'cargo build --release' on pinned
rust:1.98.1-alpine3.22 (no --target, no RUSTFLAGS, no CC — a plain
dynamic-musl binary runs on the alpine:3.22 final image; the earlier
+crt-static attempt is what made the musl target refuse proc-macros)
and publishes ghcr.io/kilork/sm121-addon (~19 MB).

- Makefile addon-image: rsync this repo into the sibling's launcher-src/
  + docker build (no TARGET build-arg anymore).
- scripts/smoke-addon.sh: same local build; no launcher-compile env.
- AGENTS.md: add-on section updated to the ship-the-binary model.
Alexander Korolev 1 тиждень тому
батько
коміт
905c5fa952
3 змінених файлів з 57 додано та 51 видалено
  1. 27 22
      AGENTS.md
  2. 14 12
      Makefile
  3. 16 17
      scripts/smoke-addon.sh

+ 27 - 22
AGENTS.md

@@ -782,28 +782,33 @@ exclusive with `--build-node`.
 
 **The add-on image** — the HA add-on store lives in the sibling
 **sm121-ha-integration** repo (`repository.json` + `sm121/` add-on dir +
-`.github/workflows/`, same pattern as `kilork/cups-addon`): a THIN image
-(debian-slim + git/jq/curl/gcc, no sm121 binary, no toolchain — small
-enough for GHCR's free tier). The **Pi is the build host**: the entrypoint
-installs rustup on first boot (cached in the add-on's data folder) and
-compiles sm121 from the launcher repo (`sm121_launcher_url` @
-`sm121_launcher_rev`, default main — set a commit to pin; `sm121_build_jobs`
-defaults to 2 so a Pi 4 doesn't OOM under HA). The arch always matches and
-the binary never goes stale; first boot = toolchain download + a release
-build (minutes on a Pi 5). **GitHub Actions builds + pushes**
-`ghcr.io/kilork/sm121-addon` (aarch64 + amd64 + manifest) on push to main
-via OIDC — no `docker login` anywhere (the GHCR package is created
-private: set it public once so HA pulls anonymously). Local dev:
-`make addon-image` (builds from the sibling repo) +
-`scripts/smoke-addon.sh` (entrypoint + `file://` recipe repo + `file://`
-launcher checkout + curl /api/state). The entrypoint writes the effective
-config from the add-on options (env), drops the SSH key (base64 option),
-`sm121 repos sync`, then `sm121 web --port 3100 --bind 0.0.0.0` (a bearer
-token is required) with a background re-sync loop
-(`SM121_REPO_RESYNC_MINUTES`). Its `custom_components/` custom-integration
-prototype is superseded (entities are a phase-2 nicety over the add-on's
-REST API). The cluster definitions (private IPs — never in the public
-recipe repo) are supplied as the **`sm121_config_extra` add-on option**
+`.github/workflows/`, same pattern as `kilork/cups-addon`). Multi-stage
+build (`sm121/Dockerfile`): a **rust:alpine** stage compiles sm121 from
+the launcher repo (CI checks it out as `launcher-src/` in the build
+context) with a trivial `cargo build --release` (pinned
+`rust:1.98.1-alpine3.22`, native arch per runner, dynamic musl); the
+**alpine:3.22** final
+image ships it + git/jq/curl + the entrypoint (~25 MB, no toolchain,
+no compile-on-boot — a 4 GB Pi must not OOM on a release build, so the
+Pi is *not* the build host). The CI matrix builds each arch natively
+(aarch64 on an arm runner, amd64 on x86) and composes the multi-arch
+manifest. **GitHub Actions builds + pushes**
+`ghcr.io/kilork/sm121-addon` (aarch64 + amd64 + `:latest`/`:1.1.0`
+manifest) on push to main via OIDC — no `docker login` anywhere (the
+GHCR package is created private: set it public once so HA pulls
+anonymously). A launcher change does not trigger the add-on CI
+(cross-repo) — re-run it (workflow_dispatch) to ship a new binary.
+Local dev: `make addon-image` (rsyncs this repo into the sibling's
+`launcher-src/` + docker build) + `scripts/smoke-addon.sh` (entrypoint +
+`file://` recipe repo + curl /api/state). The entrypoint writes the
+effective config from the add-on options (env), drops the SSH key
+(base64 option), `sm121 repos sync`, then `sm121 web --port 3100 --bind
+0.0.0.0` (a bearer token is required) with a background re-sync loop
+(`SM121_REPO_RESYNC_MINUTES`). Its `custom_components/`
+custom-integration prototype is superseded (entities are a phase-2
+nicety over the add-on's REST API). The cluster definitions (private IPs
+— never in the public recipe repo) are supplied as the
+**`sm121_config_extra` add-on option**
 (raw TOML, appended to the generated config): `[[clusters]]`/nccl +
 `[home_assistant]` + `[mikrotik]` live in HA's own option storage —
 private, on the Pi, no second repo, no PAT. If that TOML carries

+ 14 - 12
Makefile

@@ -36,20 +36,22 @@ check-lines:
 fmt:
 	cargo fmt --all
 
-# The sm121 HA add-on image (docker-images/sm121-addon/): multi-stage
-# docker build from the repo root (src/ compiled in-container — the binary
-# always matches the container's arch; .dockerignore trims the context).
+# The sm121 HA add-on image (the sibling sm121-ha-integration repo is the
+# HA add-on store: repository.json + sm121/ + the GitHub Actions
+# workflows). Multi-stage: rust:alpine compiles sm121 from launcher-src/
+# (this repo, rsynced into the build context) with a trivial
+# `cargo build --release` (pinned rust:alpine, native arch, dynamic musl);
+# the alpine final image ships it (~25 MB — no toolchain, no
+# compile-on-boot: a 4 GB Pi must not OOM on a release build).
 # Smoke test: scripts/smoke-addon.sh.
-# The add-on image lives in the sibling sm121-ha-integration repo (the HA
-# add-on store: repository.json + sm121/ + the GitHub Actions workflows)
-# and is built + pushed THERE by CI (ghcr.io/kilork/sm121-addon, OIDC —
-# no docker login). Local build for dev / smoke:
+# CI (sm121-ha-integration) builds + publishes ghcr.io/kilork/sm121-addon
+# (aarch64 + amd64 + multi-arch manifest, OIDC — no docker login) on
+# push to main. The GHCR package must be set public (first time only).
+# Local build for dev / smoke:
 addon-image:
-	docker build -t sm121-addon:latest -f ../sm121-ha-integration/sm121/Dockerfile ../sm121-ha-integration/sm121/
-
-# No push target: GitHub Actions (sm121-ha-integration) publishes
-# ghcr.io/kilork/sm121-addon on push to main (aarch64 + amd64 + manifest).
-# The GHCR package must be set public (first time only) so HA can pull it.
+	rm -rf ../sm121-ha-integration/launcher-src
+	rsync -a --exclude .git --exclude target ./ ../sm121-ha-integration/launcher-src/
+	docker build -t sm121-addon:latest -f ../sm121-ha-integration/sm121/Dockerfile ../sm121-ha-integration/
 
 # Auto-fix lints (clippy --fix); review the diff before committing.
 clippy-fix:

+ 16 - 17
scripts/smoke-addon.sh

@@ -1,12 +1,9 @@
 #!/usr/bin/env bash
 # Smoke test for the sm121 add-on image (specs/tech-architecture/
-# ha-addon-recipe-repos.md): run the entrypoint with a `file://` recipe
-# repo (mounted) + a token, and assert the dashboard answers on 3100
-# (curl /api/state).
-#
-# The thin image compiles sm121 from the launcher repo on first boot —
-# hermetically, from THIS checkout via file:// (SM121_LAUNCHER_URL), with
-# the cargo target dir persisted in a volume (re-runs are incremental).
+# ha-addon-recipe-repos.md): build the image from the sibling add-on repo
+# (rust:alpine compiles sm121 from THIS checkout → static musl binary →
+# alpine final), run the entrypoint with a `file://` recipe repo
+# (mounted), and assert the dashboard answers on 3100 (curl /api/state).
 #
 #   scripts/smoke-addon.sh [image]     (default sm121-addon:latest)
 set -euo pipefail
@@ -17,9 +14,14 @@ TOKEN=smoke-test
 
 # The image source lives in the sibling add-on repo (the HA add-on store).
 # Build it locally if it isn't there yet (CI builds the registry one).
+# The Dockerfile's rust:alpine stage compiles the launcher from
+# launcher-src/ in the build context — rsync this checkout there.
+HA_REPO="../sm121-ha-integration"
 if ! docker image inspect "$IMG" >/dev/null 2>&1; then
-  echo "building $IMG from ../sm121-ha-integration/sm121/"
-  docker build -q -t "$IMG" -f ../sm121-ha-integration/sm121/Dockerfile ../sm121-ha-integration/sm121/
+  echo "building $IMG (launcher source → $HA_REPO/launcher-src/)"
+  rm -rf "$HA_REPO/launcher-src"
+  rsync -a --exclude .git --exclude target ./ "$HA_REPO/launcher-src/"
+  docker build -q -t "$IMG" -f "$HA_REPO/sm121/Dockerfile" "$HA_REPO/"
 fi
 
 # A minimal `file://` recipe repo, mounted read-only at /repos. It lives
@@ -33,8 +35,8 @@ echo 'engine = "vllm"' > "$REPO/recipes/demo/recipe.toml"
 git -C "$REPO" add -A
 git -C "$REPO" -c user.email=t@sm121.test -c user.name=sm121-test commit -qm init
 
-# Persistent HOME (the add-on data folder in HA; here: the sm121 binary +
-# the cargo target cache, so re-runs don't rebuild the world).
+# Persistent HOME (the add-on data folder in HA; here: the sm121 cache
+# dir — ~/.cache/sm121 — so re-runs reuse the recipe-repo cache).
 HOMEV="$(pwd)/target/smoke-addon-home"
 mkdir -p "$HOMEV"
 
@@ -42,19 +44,16 @@ CID="$(docker run -d \
   -p 3100:3100 \
   -v "$REPO":/repos:ro \
   -v "$HOMEV":/root \
-  -v "$(pwd)":/src-launcher:ro \
   -e SM121_RECIPE_REPOS='[{"url":"file:///repos"}]' \
-  -e SM121_LAUNCHER_URL="file:///src-launcher" \
-  -e SM121_LAUNCHER_REV=HEAD \
   -e SM121_SSH_USER=u \
   -e SM121_WEB_TOKEN="$TOKEN" \
   "$IMG")"
 trap 'docker rm -f "$CID" >/dev/null 2>&1 || true; rm -rf "$REPO"' EXIT
 
-# Wait for the dashboard (up to 8 min — the first run compiles sm121 from
-# source; a warm cache is seconds).
+# First boot is fast now (the binary ships in the image — just the
+# recipe-repo sync); give it 60 s to be safe.
 ok=""
-for _ in $(seq 1 480); do
+for _ in $(seq 1 60); do
   if curl -sf -H "Authorization: Bearer $TOKEN" \
       http://127.0.0.1:3100/api/state >/dev/null 2>&1; then
     ok=1