diff options
Diffstat (limited to 'docs/plans/2026-07-13-image-builder.md')
| -rw-r--r-- | docs/plans/2026-07-13-image-builder.md | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/docs/plans/2026-07-13-image-builder.md b/docs/plans/2026-07-13-image-builder.md new file mode 100644 index 0000000..0c61051 --- /dev/null +++ b/docs/plans/2026-07-13-image-builder.md @@ -0,0 +1,790 @@ +# sbo-testbuild image builder — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build the three chained bash scripts that produce `sbo-testbuild:current` / `sbo-testbuild:15.0` docker images from NAS Slackware trees and push them to a LAN registry, consumed by `.extras/test-build`. + +**Architecture:** Adapt the forge `slackware/docker-images` scripts (`bootstrap.sh`, `build-full-image.sh`) into `.extras/image-builder/`. Add a third `build-sbo-testbuild.sh` layer. Share config + helpers via `config` and `lib.sh`. Pure-logic helpers are unit-tested with an assert-based self-check; the docker chain is verified manually on the VM. + +**Tech Stack:** bash, docker, slackpkg/installpkg (Slackware), `registry:2`. + +**Reference:** design spec at `.extras/docs/specs/2026-07-13-image-builder-design.md`. Forge originals cloned at (scratchpad) `docker-images/scripts/` — re-clone with `git clone slackware_forge:slackware/docker-images.git` if gone. + +--- + +## File Structure + +``` +.extras/image-builder/ +├── config REGISTRY, MIRROR, VARIANTS, PKGDIR (sourced by all) +├── lib.sh _log/_warn/_err, fetch(), txz_hash(), require_mount() +├── bootstrap.sh base image FROM scratch (root; installpkg from NAS) +├── build-full-image.sh full image FROM base (slackpkg all series) +├── build-sbo-testbuild.sh testbuild image FROM full (installpkg sbopkg + tools) +├── test-image-builder.sh assert-based self-check for lib.sh logic +└── README VM-side setup checklist +``` + +Responsibilities: +- **config** — the only place hostnames/paths/variants live. No script hardcodes them. +- **lib.sh** — all shared, testable logic. Sourced by the three build scripts and by the test. No side effects at source time (only function defs + nothing else). +- **bootstrap/full/testbuild** — one image each, thin: guard, gate, generate Dockerfile, build, push. +- **test-image-builder.sh** — sources lib.sh, exercises fetch/txz_hash/require_mount with asserts. No docker. + +--- + +## Task 1: config + lib.sh skeleton + +**Files:** +- Create: `.extras/image-builder/config` +- Create: `.extras/image-builder/lib.sh` + +- [ ] **Step 1: Write config** + +Create `.extras/image-builder/config`: + +```sh +# Shared configuration for the sbo-testbuild image builder. +# Sourced by bootstrap.sh, build-full-image.sh, build-sbo-testbuild.sh, tests. + +REGISTRY="docker.noland.dnx:5000" +MIRROR="file:///mnt/nas" +VARIANTS=(current 15.0) # x86_64 only for now +PKGDIR="/opt/sbo-testbuild/pkgs" # sbopkg + sbo-maintainer-tools .txz +HASH_DIR="/var/cache/sbo-testbuild" # ChangeLog hashes for rebuild gating +``` + +- [ ] **Step 2: Write lib.sh with logging + require_mount** + +Create `.extras/image-builder/lib.sh`: + +```sh +# Shared helpers for the image builder. Source, do not execute. +# Sourcing must have no side effects beyond defining functions. + +_log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${LOG_TAG:-image-builder}] $*"; } +_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${LOG_TAG:-image-builder}] WARNING: $*" >&2; } +_err() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${LOG_TAG:-image-builder}] ERROR: $*" >&2; exit 1; } + +# mirror_path MIRROR RELPATH +# Join a MIRROR (file:///... or http(s)://...) with a relative path. +mirror_path() { + local mirror="$1" rel="$2" + printf '%s/%s' "${mirror%/}" "${rel#/}" +} + +# is_local_mirror MIRROR -> 0 if file:// or bare absolute path, else 1 +is_local_mirror() { + case "$1" in + file://*) return 0 ;; + http://*|https://*) return 1 ;; + /*) return 0 ;; + *) return 1 ;; + esac +} + +# strip_scheme URL -> filesystem path for a file:// or bare-local URL +strip_scheme() { + local u="$1" + case "$u" in + file://*) printf '%s' "${u#file://}" ;; + *) printf '%s' "$u" ;; + esac +} + +# fetch SRC DEST +# Copy SRC (a full mirror URL) to DEST. Local mirror -> cp, http -> curl. +# Returns non-zero on failure; caller decides whether that is fatal. +fetch() { + local src="$1" dest="$2" + if is_local_mirror "$src"; then + local path; path="$(strip_scheme "$src")" + [[ -f "$path" ]] || return 1 + cp "$path" "$dest" + else + curl -sSfL --connect-timeout 60 "$src" -o "$dest" + fi +} + +# txz_hash DIR +# Stable hash of the set of .txz files in DIR (names + contents), order +# independent. Empty/absent dir -> empty string. +txz_hash() { + local dir="$1" + [[ -d "$dir" ]] || { printf ''; return 0; } + local files; files=$(find "$dir" -maxdepth 1 -name '*.txz' -type f | sort) + [[ -n "$files" ]] || { printf ''; return 0; } + # Hash sorted "name sha256" lines so reordering does not change output. + while IFS= read -r f; do + printf '%s %s\n' "$(basename "$f")" "$(sha256sum "$f" | cut -d' ' -f1)" + done <<< "$files" | sha256sum | cut -d' ' -f1 +} + +# require_mount VERSION +# Assert the NAS tree for VERSION is mounted (file:// mirror only). +# x86_64 tree dir is slackware64-${VERSION}. +require_mount() { + local version="$1" + is_local_mirror "$MIRROR" || return 0 # http mirror: nothing to check + local base; base="$(strip_scheme "$MIRROR")" + local dir="${base}/slackware64-${version}" + [[ -d "$dir" ]] || _err "NAS tree not mounted: ${dir}" +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add .extras/image-builder/config .extras/image-builder/lib.sh +git commit -m 'image-builder: config + lib.sh helpers' +``` + +--- + +## Task 2: lib.sh self-check + +**Files:** +- Create: `.extras/image-builder/test-image-builder.sh` + +- [ ] **Step 1: Write the test** + +Create `.extras/image-builder/test-image-builder.sh`: + +```sh +#!/bin/bash +# Assert-based self-check for lib.sh pure logic. No docker, no network. +# Run: bash .extras/image-builder/test-image-builder.sh +set -u +cd "$(dirname "$0")" +source ./lib.sh + +pass=0 fail=0 +check() { # DESC EXPECTED ACTUAL + if [[ "$2" == "$3" ]]; then pass=$((pass+1)); + else fail=$((fail+1)); echo "FAIL: $1"; echo " expected: [$2]"; echo " actual: [$3]"; fi +} +check_rc() { # DESC EXPECTED_RC ACTUAL_RC + if [[ "$2" == "$3" ]]; then pass=$((pass+1)); + else fail=$((fail+1)); echo "FAIL: $1 (rc expected $2 got $3)"; fi +} + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +# --- mirror_path --- +check "mirror_path trailing slash" \ + "file:///mnt/nas/slackware64-current/PACKAGES.TXT" \ + "$(mirror_path 'file:///mnt/nas/' 'slackware64-current/PACKAGES.TXT')" +check "mirror_path no trailing slash" \ + "file:///mnt/nas/x/y" "$(mirror_path 'file:///mnt/nas' '/x/y')" + +# --- is_local_mirror --- +is_local_mirror "file:///mnt/nas"; check_rc "is_local file://" 0 $? +is_local_mirror "/mnt/nas"; check_rc "is_local bare abs" 0 $? +is_local_mirror "https://x/y"; check_rc "is_local https" 1 $? + +# --- strip_scheme --- +check "strip_scheme file://" "/mnt/nas" "$(strip_scheme 'file:///mnt/nas')" +check "strip_scheme bare" "/mnt/nas" "$(strip_scheme '/mnt/nas')" + +# --- fetch: local file resolves --- +echo hello > "$tmp/src.txt" +fetch "file://$tmp/src.txt" "$tmp/dst.txt"; check_rc "fetch local ok" 0 $? +check "fetch local content" "hello" "$(cat "$tmp/dst.txt" 2>/dev/null)" + +# --- fetch: missing local file errors --- +fetch "file://$tmp/nope.txt" "$tmp/x.txt"; check_rc "fetch local missing" 1 $? + +# --- txz_hash: stable + content-sensitive --- +mkdir -p "$tmp/pkgs" +printf a > "$tmp/pkgs/sbopkg-1.txz" +printf b > "$tmp/pkgs/tools-1.txz" +h1="$(txz_hash "$tmp/pkgs")" +h2="$(txz_hash "$tmp/pkgs")" +check "txz_hash stable" "$h1" "$h2" +printf c > "$tmp/pkgs/tools-1.txz" # change content +h3="$(txz_hash "$tmp/pkgs")" +check_rc "txz_hash changes on content" 0 "$([[ "$h1" != "$h3" ]] && echo 0 || echo 1)" +check "txz_hash empty dir" "" "$(txz_hash "$tmp/empty-nope")" + +# --- require_mount: missing mount errors (subshell to catch _err exit) --- +MIRROR="file://$tmp/no-such-root" +( require_mount current ) 2>/dev/null; check_rc "require_mount missing" 1 $? +mkdir -p "$tmp/mnt/slackware64-current" +MIRROR="file://$tmp/mnt" +( require_mount current ) 2>/dev/null; check_rc "require_mount present" 0 $? +MIRROR="https://example/x" +( require_mount current ) 2>/dev/null; check_rc "require_mount http skips" 0 $? + +echo "----" +echo "PASS: $pass FAIL: $fail" +[[ "$fail" -eq 0 ]] +``` + +- [ ] **Step 2: Run — expect PASS** + +Run: `bash .extras/image-builder/test-image-builder.sh` +Expected: final line `PASS: N FAIL: 0`, exit 0. + +If any FAIL, fix `lib.sh` (the test encodes the intended behavior). + +- [ ] **Step 3: Commit** + +```bash +git add .extras/image-builder/test-image-builder.sh +git commit -m 'image-builder: lib.sh self-check' +``` + +--- + +## Task 3: bootstrap.sh (base image) + +**Files:** +- Create: `.extras/image-builder/bootstrap.sh` +- Reference: forge `docker-images/scripts/bootstrap.sh` + +This is the forge `bootstrap.sh` adapted: source `config`+`lib.sh`, drop i586, +`file://` fetch via `fetch()`, LAN registry, `sbo-base` name, mount guard. + +- [ ] **Step 1: Write bootstrap.sh** + +Create `.extras/image-builder/bootstrap.sh`. Start from the forge original and apply exactly these changes; everything else (the `download_pkgtxt`, `find_package`, rootfs configuration, cleanup, Dockerfile-from-scratch blocks) is copied verbatim from the forge script: + +1. Header block: + +```sh +#!/bin/bash +# bootstrap.sh — build the sbo-base:{ver} image FROM scratch from NAS trees. +# Run as root (installpkg). Adapted from forge slackware/docker-images. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +source "${HERE}/config" +LOG_TAG=bootstrap +source "${HERE}/lib.sh" + +REGISTRY_IMAGE="${REGISTRY}/sbo-base" +[[ "${EUID}" -eq 0 ]] || _err "run as root." +mkdir -p "${HASH_DIR}" +``` + +2. Argument parsing: keep `--force` and `--version` only; **remove `--arch`** + (x86_64 fixed). Variant list comes from config: + +```sh +FORCE=false +OPT_VERSION="" +while [[ $# -gt 0 ]]; do + case "$1" in + --version) OPT_VERSION="$2"; shift 2 ;; + --force) FORCE=true; shift ;; + *) _err "unknown argument: $1" ;; + esac +done +if [[ -n "${OPT_VERSION}" ]]; then + BUILD_VARIANTS=("${OPT_VERSION}") +else + BUILD_VARIANTS=("${VARIANTS[@]}") +fi +``` + +3. Copy the forge `PACKAGES=( ... )` array verbatim (lines 80–187 of the forge + original — the full base package list). + +4. Replace `changelog_changed`: read ChangeLog via `fetch` from the NAS, + x86_64-only repo key: + +```sh +# changelog_changed VERSION -> 0 if changed (or --force), else 1 +changelog_changed() { + local version="$1" + local repo_key="slackware64-${version}" + local hash_file="${HASH_DIR}/${repo_key}.sha256" + local url; url="$(mirror_path "${MIRROR}" "${repo_key}/ChangeLog.txt")" + local tmp; tmp="$(mktemp)" + if ! fetch "${url}" "${tmp}"; then + rm -f "${tmp}" + _warn "${repo_key}: cannot read ChangeLog (${url}); skipping." + return 1 + fi + local live; live=$(sha256sum "${tmp}" | cut -d' ' -f1); rm -f "${tmp}" + if [[ "${FORCE}" == "true" ]]; then + echo "${live}" > "${hash_file}"; return 0 + fi + local stored=""; [[ -f "${hash_file}" ]] && stored=$(cat "${hash_file}") + if [[ "${live}" == "${stored}" ]]; then + _log "${repo_key}: ChangeLog unchanged; skipping."; return 1 + fi + _log "${repo_key}: ChangeLog changed."; echo "${live}" > "${hash_file}"; return 0 +} +``` + +5. Copy `download_pkgtxt` and `find_package` verbatim from the forge original, + but change the two `curl -sfL ... "${URL}" -o "${DEST}"` download lines in + `download_pkgtxt` to `fetch "${URL}" "${DEST}"` (drops the http assumption). + +6. `build_variant`: take `VERSION` only (no ARCH). Set: + +```sh +build_variant() { + local VERSION="$1" + require_mount "${VERSION}" + local REPO_KEY="slackware64-${VERSION}" + local PKG_PATH; PKG_PATH="$(mirror_path "${MIRROR}" "${REPO_KEY}")" + local TAG="${REGISTRY_IMAGE}:${VERSION}" + local SLACKPKG_MIRROR="https://slackware.nl/slackware/slackware64-${VERSION}/" + ... +} +``` + + The `SLACKPKG_MIRROR` written into the image stays an https slackware.nl URL + (it is used later by slackpkg inside the container, which has internet; the + NAS `file://` mount only feeds the host-side build). Keep the rest of the + forge `build_variant` body verbatim, except: + - the package **download loop** uses `fetch "${URL}" "${PKGCACHE}/${FILENAME}"` + instead of `curl`. + - the final `docker build ... && docker push` targets `${TAG}` as above. + +7. Main loop over `BUILD_VARIANTS`, one arg: + +```sh +for VERSION in "${BUILD_VARIANTS[@]}"; do + if changelog_changed "${VERSION}"; then + build_variant "${VERSION}" + fi +done +``` + +- [ ] **Step 2: Syntax check** + +Run: `bash -n .extras/image-builder/bootstrap.sh` +Expected: no output, exit 0. + +- [ ] **Step 3: shellcheck (if available)** + +Run: `shellcheck -x .extras/image-builder/bootstrap.sh || true` +Expected: no errors (warnings about unreachable `_err` exit are fine). If shellcheck absent, skip. + +- [ ] **Step 4: Commit** + +```bash +git add .extras/image-builder/bootstrap.sh +git commit -m 'image-builder: bootstrap.sh base image from NAS' +``` + +--- + +## Task 4: build-full-image.sh + +**Files:** +- Create: `.extras/image-builder/build-full-image.sh` +- Reference: forge `docker-images/scripts/build-full-image.sh` + +Forge `build-full-image.sh` adapted: source config+lib, drop i586, LAN +registry, `sbo-base`→`sbo-full` names. + +- [ ] **Step 1: Write build-full-image.sh** + +Create `.extras/image-builder/build-full-image.sh`. From the forge original, apply exactly: + +1. Header: + +```sh +#!/bin/bash +# build-full-image.sh — sbo-full:{ver} FROM sbo-base:{ver}, all series. +# Adapted from forge slackware/docker-images. No root needed. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +source "${HERE}/config" +LOG_TAG=full +source "${HERE}/lib.sh" + +BASE_IMAGE="${REGISTRY}/sbo-base" +FULL_IMAGE="${REGISTRY}/sbo-full" +DIGEST_LABEL="sbo.full.base-digest" +``` + +2. Arg parsing: `--force` and `--version` only (no `--arch`), variant list from + config — identical shape to bootstrap Task 3 Step 1.2, but `BUILD_VARIANTS`. + +3. `derive_tags VERSION`: + +```sh +derive_tags() { + local VERSION="$1" + BASE_TAG="${BASE_IMAGE}:${VERSION}" + FULL_TAG="${FULL_IMAGE}:${VERSION}" +} +``` + +4. Keep `needs_rebuild` verbatim from the forge original (it already takes + BASE_TAG/FULL_TAG args and uses `_log`/`_warn`/`FORCE`). + +5. `build_variant VERSION`: drop the ARCH/ARCH_SUFFIX lines; call + `derive_tags "${VERSION}"`. Keep the forge Dockerfile heredoc **verbatim** + (the `slackpkg update` + all-series install + cleanup + labels). Only the + `LABEL org.opencontainers.image.title/description` strings lose the + `(${ARCH})` suffix; leave them or hardcode `(x86_64)`. + +6. Main loop over `BUILD_VARIANTS`: + +```sh +for VERSION in "${BUILD_VARIANTS[@]}"; do + build_variant "${VERSION}" +done +``` + +- [ ] **Step 2: Syntax check** + +Run: `bash -n .extras/image-builder/build-full-image.sh` +Expected: exit 0. + +- [ ] **Step 3: shellcheck (if available)** + +Run: `shellcheck -x .extras/image-builder/build-full-image.sh || true` + +- [ ] **Step 4: Commit** + +```bash +git add .extras/image-builder/build-full-image.sh +git commit -m 'image-builder: build-full-image.sh' +``` + +--- + +## Task 5: build-sbo-testbuild.sh + +**Files:** +- Create: `.extras/image-builder/build-sbo-testbuild.sh` + +New script (no forge equivalent). FROM sbo-full, installpkg the two .txz, +gate on full digest AND .txz-set hash. + +- [ ] **Step 1: Write build-sbo-testbuild.sh** + +Create `.extras/image-builder/build-sbo-testbuild.sh`: + +```sh +#!/bin/bash +# build-sbo-testbuild.sh — sbo-testbuild:{ver} FROM sbo-full:{ver}. +# Adds sbopkg + sbo-maintainer-tools from prebuilt .txz in PKGDIR. +# Rebuilds when the full image OR the .txz set changes. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +source "${HERE}/config" +LOG_TAG=testbuild +source "${HERE}/lib.sh" + +FULL_IMAGE="${REGISTRY}/sbo-full" +TB_IMAGE="${REGISTRY}/sbo-testbuild" +DIGEST_LABEL="sbo.testbuild.full-digest" +PKGS_LABEL="sbo.testbuild.pkgs-hash" + +FORCE=false +OPT_VERSION="" +while [[ $# -gt 0 ]]; do + case "$1" in + --version) OPT_VERSION="$2"; shift 2 ;; + --force) FORCE=true; shift ;; + *) _err "unknown argument: $1" ;; + esac +done +if [[ -n "${OPT_VERSION}" ]]; then + BUILD_VARIANTS=("${OPT_VERSION}") +else + BUILD_VARIANTS=("${VARIANTS[@]}") +fi + +# Verify the required .txz are present before doing anything. +require_pkgs() { + [[ -d "${PKGDIR}" ]] || _err "PKGDIR missing: ${PKGDIR}" + compgen -G "${PKGDIR}/sbopkg-*.txz" >/dev/null \ + || _err "no sbopkg-*.txz in ${PKGDIR}" + compgen -G "${PKGDIR}/sbo-maintainer-tools-*.txz" >/dev/null \ + || _err "no sbo-maintainer-tools-*.txz in ${PKGDIR}" +} + +# needs_rebuild FULL_TAG TB_TAG PKGS_HASH -> 0 if rebuild needed +needs_rebuild() { + local full_tag="$1" tb_tag="$2" pkgs_hash="$3" + [[ "${FORCE}" == "true" ]] && { _log " --force."; return 0; } + + local full_digest + full_digest=$(docker inspect --format '{{index .RepoDigests 0}}' \ + "${full_tag}" 2>/dev/null || echo "") + [[ -n "${full_digest}" ]] || { _warn " no full digest; rebuilding."; return 0; } + + local rec_digest rec_pkgs + rec_digest=$(docker inspect \ + --format "{{index .Config.Labels \"${DIGEST_LABEL}\"}}" \ + "${tb_tag}" 2>/dev/null || echo "") + rec_pkgs=$(docker inspect \ + --format "{{index .Config.Labels \"${PKGS_LABEL}\"}}" \ + "${tb_tag}" 2>/dev/null || echo "") + + if [[ "${full_digest}" == "${rec_digest}" && "${pkgs_hash}" == "${rec_pkgs}" ]]; then + _log " full image and .txz set unchanged; skipping." + return 1 + fi + _log " full image or .txz set changed; rebuilding." + return 0 +} + +build_variant() { + local VERSION="$1" + local FULL_TAG="${FULL_IMAGE}:${VERSION}" + local TB_TAG="${TB_IMAGE}:${VERSION}" + _log "=== ${TB_TAG} ===" + + _log " Pulling ${FULL_TAG}..." + docker pull "${FULL_TAG}" + + local PKGS_HASH; PKGS_HASH="$(txz_hash "${PKGDIR}")" + + if ! needs_rebuild "${FULL_TAG}" "${TB_TAG}" "${PKGS_HASH}"; then + return 0 + fi + + local FULL_DIGEST + FULL_DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "${FULL_TAG}") + + local WORKDIR; WORKDIR="$(mktemp -d /tmp/sbo-testbuild.XXXXXX)" + trap "rm -rf '${WORKDIR}'" RETURN + mkdir -p "${WORKDIR}/pkgs" + cp "${PKGDIR}"/sbopkg-*.txz "${PKGDIR}"/sbo-maintainer-tools-*.txz "${WORKDIR}/pkgs/" + + cat > "${WORKDIR}/Dockerfile" <<DOCKERFILE +FROM ${FULL_TAG} +LABEL maintainer="danix <danix@danix.xyz>" +COPY pkgs/*.txz /tmp/pkgs/ +RUN installpkg /tmp/pkgs/*.txz && rm -rf /tmp/pkgs +RUN /sbin/ldconfig +LABEL org.opencontainers.image.created="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +LABEL org.opencontainers.image.title="SBo test-build env ${VERSION} (x86_64)" +LABEL org.opencontainers.image.description="Slackware ${VERSION} full + sbopkg + sbo-maintainer-tools" +LABEL slackware.version="${VERSION}" +LABEL ${DIGEST_LABEL}="${FULL_DIGEST}" +LABEL ${PKGS_LABEL}="${PKGS_HASH}" +CMD ["/bin/bash"] +DOCKERFILE + + local BUILD_FLAGS=() + [[ "${FORCE}" == "true" ]] && BUILD_FLAGS+=(--no-cache) + _log " Building ${TB_TAG}..." + docker build "${BUILD_FLAGS[@]}" -t "${TB_TAG}" "${WORKDIR}" + _log " Pushing ${TB_TAG}..." + docker push "${TB_TAG}" + _log "=== Done: ${TB_TAG} ===" +} + +require_pkgs +for VERSION in "${BUILD_VARIANTS[@]}"; do + build_variant "${VERSION}" +done +``` + +- [ ] **Step 2: Syntax check** + +Run: `bash -n .extras/image-builder/build-sbo-testbuild.sh` +Expected: exit 0. + +- [ ] **Step 3: shellcheck (if available)** + +Run: `shellcheck -x .extras/image-builder/build-sbo-testbuild.sh || true` + +- [ ] **Step 4: Commit** + +```bash +git add .extras/image-builder/build-sbo-testbuild.sh +git commit -m 'image-builder: build-sbo-testbuild.sh' +``` + +--- + +## Task 6: per-variant isolation + +The spec requires one variant failing not to block the other, non-zero exit at +the end. Add this to all three scripts' main loops. + +**Files:** +- Modify: `.extras/image-builder/bootstrap.sh` (main loop) +- Modify: `.extras/image-builder/build-full-image.sh` (main loop) +- Modify: `.extras/image-builder/build-sbo-testbuild.sh` (main loop) + +- [ ] **Step 1: Wrap each main loop** + +In each script, replace the plain `for VERSION ...` main loop with an +isolation wrapper. For **bootstrap.sh** (which also has the changelog gate): + +```sh +rc=0 +for VERSION in "${BUILD_VARIANTS[@]}"; do + if changelog_changed "${VERSION}"; then + if ! ( build_variant "${VERSION}" ); then + _warn "variant ${VERSION} failed; continuing." + rc=1 + fi + fi +done +exit "${rc}" +``` + +For **build-full-image.sh** and **build-sbo-testbuild.sh** (no changelog gate): + +```sh +rc=0 +for VERSION in "${BUILD_VARIANTS[@]}"; do + if ! ( build_variant "${VERSION}" ); then + _warn "variant ${VERSION} failed; continuing." + rc=1 + fi +done +exit "${rc}" +``` + +Note: `build_variant` runs in a subshell `( )` so a `set -e` abort inside it +(or an `_err` exit) is contained and the loop proceeds to the next variant. +For build-sbo-testbuild.sh keep the `require_pkgs` call **before** the loop +(a missing .txz is fatal for all variants, not per-variant). + +- [ ] **Step 2: Syntax check all three** + +Run: `for f in bootstrap build-full-image build-sbo-testbuild; do bash -n .extras/image-builder/$f.sh; done` +Expected: exit 0, no output. + +- [ ] **Step 3: Re-run lib self-check (unaffected, sanity)** + +Run: `bash .extras/image-builder/test-image-builder.sh` +Expected: `FAIL: 0`. + +- [ ] **Step 4: Commit** + +```bash +git add .extras/image-builder/bootstrap.sh .extras/image-builder/build-full-image.sh .extras/image-builder/build-sbo-testbuild.sh +git commit -m 'image-builder: per-variant isolation, non-zero exit on failure' +``` + +--- + +## Task 7: README + +**Files:** +- Create: `.extras/image-builder/README` + +- [ ] **Step 1: Write README** + +Create `.extras/image-builder/README` with the VM-side setup, verbatim: + +``` +sbo-testbuild image builder +=========================== + +Builds the docker images that .extras/test-build consumes: + docker.noland.dnx:5000/sbo-testbuild:current + docker.noland.dnx:5000/sbo-testbuild:15.0 + +Three scripts, chained (see .extras/docs/specs/2026-07-13-image-builder-design.md): + bootstrap.sh sbo-base:{ver} FROM scratch, base pkgs from NAS + build-full-image.sh sbo-full:{ver} FROM base, all series + build-sbo-testbuild.sh sbo-testbuild:{ver} FROM full, + sbopkg + tools + +All settings live in ./config. + +VM setup (docker.noland.dnx, Slackware x86_64, 4 vCPU / 4 GB / 80 GB) +-------------------------------------------------------------------- +1. Install docker; enable the daemon. + +2. NFS-mount the two NAS trees read-only, named to match: + /mnt/nas/slackware64-current -> -current mirror tree + /mnt/nas/slackware64-15.0 -> 15.0 mirror tree + Each is a full mirror (PACKAGES.TXT, ChangeLog.txt, slackware64/, patches/, + extra/). Root must be able to read them (bootstrap runs installpkg as root). + +3. Run a LAN registry: + docker run -d --restart=always -p 5000:5000 \ + -v /opt/registry/data:/var/lib/registry --name registry registry:2 + +4. Mark the registry insecure (plain HTTP) on the VM AND every pulling client + (this dev box, the buildsystem VM). In /etc/docker/daemon.json: + { "insecure-registries": ["docker.noland.dnx:5000"] } + then restart docker. + +5. Drop the two prebuilt packages (built once, re-drop on upstream bumps): + /opt/sbo-testbuild/pkgs/sbopkg-*.txz + /opt/sbo-testbuild/pkgs/sbo-maintainer-tools-*.txz + +6. Install the nightly cron (root): + 0 4 * * * /path/to/.extras/image-builder/bootstrap.sh >> /var/log/sbo-testbuild.log 2>&1 + 15 4 * * * /path/to/.extras/image-builder/build-full-image.sh >> /var/log/sbo-testbuild.log 2>&1 + 30 4 * * * /path/to/.extras/image-builder/build-sbo-testbuild.sh >> /var/log/sbo-testbuild.log 2>&1 + +7. Ensure docker.noland.dnx resolves on the LAN (static IP or DNS). + +Manual first run +---------------- + ./bootstrap.sh --version current --force + ./build-full-image.sh --version current --force + ./build-sbo-testbuild.sh --version current --force +Then confirm: + docker pull docker.noland.dnx:5000/sbo-testbuild:current + docker run --rm docker.noland.dnx:5000/sbo-testbuild:current sbopkg -V + +Flags: --force (rebuild unconditionally), --version <current|15.0> (one variant). + +Tests +----- + bash test-image-builder.sh # pure-logic self-check, no docker +``` + +- [ ] **Step 2: Commit** + +```bash +git add .extras/image-builder/README +git commit -m 'image-builder: README with VM setup checklist' +``` + +--- + +## Task 8: executable bits + final check + +**Files:** +- Modify (mode): the four `.sh` scripts + +- [ ] **Step 1: chmod +x the scripts** + +```bash +chmod +x .extras/image-builder/bootstrap.sh \ + .extras/image-builder/build-full-image.sh \ + .extras/image-builder/build-sbo-testbuild.sh \ + .extras/image-builder/test-image-builder.sh +``` + +- [ ] **Step 2: Run the self-check once more** + +Run: `bash .extras/image-builder/test-image-builder.sh` +Expected: `PASS: N FAIL: 0`, exit 0. + +- [ ] **Step 3: Syntax-check all four** + +Run: `for f in .extras/image-builder/*.sh; do bash -n "$f" || echo "SYNTAX FAIL: $f"; done` +Expected: no `SYNTAX FAIL` lines. + +- [ ] **Step 4: Commit** + +```bash +git add .extras/image-builder +git commit -m 'image-builder: mark scripts executable' +``` + +--- + +## Deferred (not in this plan) + +- **Real end-to-end chain** on the VM (bootstrap→full→testbuild producing a + pushed `sbo-testbuild:current`): the user runs it once the VM/registry/mounts + exist, and reports back. Mirrors `.extras/test-build` Task 10. +- **`installed_in_base`** in `.extras/test-build`: now unblockable (query the + built image's package db) but tracked separately in the project memory + `test-build-image-builder-todo`. +- **i586** variants. +- **Registry auth/TLS** (LAN plain HTTP for now). +``` |
