# Docker test-build 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:** A single bash script `.extras/test-build` that verifies an already-published SBo package still builds cleanly on `-current` or `15.0` inside a throwaway docker container, resolving and building its SBo deps from the local tree, caching built deps per image digest, and reporting per-package status + lint. **Architecture:** Host resolves the dep tree from the local SBo tree (logic ported from `sbo-batch-test`), applies current-vs-stable override rules, prints the build order and confirms, then runs one `docker run --rm` that builds deps (or installs them from a host cache) and the target, lints the target, and streams results back. The container is the disposable environment (no overlayfs). Images are built by a separate job and consumed by tag. **Tech Stack:** Bash, docker, Slackware `installpkg`/`makepkg`, `sbo-maintainer-tools` (`sbopkglint`). Self-check is plain bash (`test-logic.sh`), no framework. --- ## Reference source Port from `/home/danix/Programming/GIT/sbo-batch-tester/sbo-batch-test`. These functions are lifted with minimal change (they are pure or host-side): - `find_slackbuild_dir`, `category_of`, `pkg_key`, `read_requires`, `version_of`, `installed_in_base` - resolver: `_resolve_visit`, `resolve_target`, globals `RESOLVED_ORDER`, `UNMET`, `CYCLES`, `HAS_README`, `_vstate` - cache: `_cache_ver_of`, `cache_decision`, `cache_path`, `cache_store`, `cache_label` - `depends_on_failed`, `print_summary`, `lint_pkg`, `init_color` - self-check patterns from `/home/danix/Programming/GIT/sbo-batch-tester/test-logic.sh` What is NEW or CHANGED in this repo: - overlay lifecycle (`setup_overlay`/`teardown_overlay`/`cleanup_trap`) is REMOVED. The container replaces it. - `build_one` is rewritten to run its build steps via `docker run` instead of `chroot`. - image + tree selection by version (`--stable`). - override engine (drop / rename / fetch) applied to the resolved order. - cache namespaced by image digest. - config keys differ (see Task 2). ## File Structure - Create: `/home/danix/Programming/GIT/sbo-slackbuilds/.extras/test-build` (the script, executable) - Create: `/home/danix/Programming/GIT/sbo-slackbuilds/.extras/test-build-config.example` (config template) - Create: `/home/danix/Programming/GIT/sbo-slackbuilds/.extras/overrides.example` (override-rules template) - Create: `/home/danix/Programming/GIT/sbo-slackbuilds/.extras/test-logic.sh` (self-check) All work happens in `.extras/`. Run every command from the repo root `/home/danix/Programming/GIT/sbo-slackbuilds`. Note on committing: this repo has a pre-commit hook that runs `sbolint` on staged packages. `.extras/` files are not packages, but to be safe every commit in this plan uses `SBOLINT=no git commit ...`. Commits are GPG-signed automatically; do not disable signing. --- ## Task 1: Scaffold the script + smoke self-check **Files:** - Create: `.extras/test-build` - Create: `.extras/test-logic.sh` - [ ] **Step 1: Write the failing self-check** Create `.extras/test-logic.sh`: ```bash #!/bin/bash # # Logic self-check for .extras/test-build. Covers the pure, host-side parts: # dependency resolution, override application, unknown-dep -> UNMET, cache # decisions, and BLOCKED-BY-DEP propagation. No docker. # # Run: bash .extras/test-logic.sh # set -uo pipefail SCRIPT="$(dirname "$0")/test-build" T=$(mktemp -d) BASE_DB=$(mktemp -d); mkdir -p "$BASE_DB" cleanup() { rm -rf "$T" "$BASE_DB"; } trap cleanup EXIT # Source the script without running main(). Sourcing re-runs the CONFIG block, # so set test vars AFTER the source. LIB=$(mktemp) sed '/^main "\$@"$/d' "$SCRIPT" > "$LIB" # shellcheck disable=SC1090 source "$LIB" 2>/dev/null rm -f "$LIB" pass=0; fail=0 ok() { echo " ok: $1"; ((pass++)); return 0; } bad() { echo " FAIL: $1"; ((fail++)); return 0; } # Placeholder assertion so this file runs before any logic exists. ok "script sources without executing main" echo echo "$pass passed, $fail failed" [[ $fail -eq 0 ]] || exit 1 echo "ALL LOGIC CHECKS PASS" ``` - [ ] **Step 2: Run it to verify it fails** Run: `bash .extras/test-logic.sh` Expected: FAIL. `$SCRIPT` does not exist yet, `sed`/`source` on a missing file errors, no "ALL LOGIC CHECKS PASS". - [ ] **Step 3: Write the minimal script skeleton** Create `.extras/test-build`: ```bash #!/bin/bash # # test-build - verify an already-published SBo package still builds on a target # Slackware version inside a throwaway docker container. Resolves + builds its # SBo deps from the local tree, caches built deps per image digest, reports # per-package status and lints the target. # # Dependency-resolution, cache, and summary logic are adapted from sbo-batch-test # (github: danixland). The overlay chroot is replaced by a docker container: the # container IS the disposable environment, so no overlayfs. # # No em dashes in prose by author convention. # ============================================================================= # CONFIG (do not edit here; real values live in the external config file) # ============================================================================= SBO_TREE_CURRENT="" SBO_TREE_STABLE="" IMAGE_CURRENT="" IMAGE_STABLE="" LOG_ROOT="/var/log/sbo-test-build" PKG_CACHE="" TB_CONFIG="${TB_CONFIG:-$HOME/.config/sbo-testbuild/config}" if [[ -f "$TB_CONFIG" ]]; then # shellcheck disable=SC1090 source "$TB_CONFIG" fi TB_OVERRIDES="${TB_OVERRIDES:-$HOME/.config/sbo-testbuild/overrides}" # ============================================================================= set -uo pipefail # Not -e: a package build failing is a handled outcome, not a script crash. main() { : } main "$@" ``` - [ ] **Step 4: Run the self-check to verify it passes** Run: `bash .extras/test-logic.sh` Expected: PASS, ending `ALL LOGIC CHECKS PASS`. - [ ] **Step 5: Commit** ```bash chmod +x .extras/test-build git add .extras/test-build .extras/test-logic.sh SBOLINT=no git commit -m 'test-build: scaffold script + self-check' ``` --- ## Task 2: Config, arg parsing, version selection **Files:** - Modify: `.extras/test-build` - Create: `.extras/test-build-config.example` - Test: `.extras/test-logic.sh` - [ ] **Step 1: Write the failing test** Append to `.extras/test-logic.sh` before the `--- result ---` block (the `echo; "$pass passed..."` lines at the end): ```bash # --- version selection ------------------------------------------------------ SBO_TREE_CURRENT="/trees/current" SBO_TREE_STABLE="/trees/stable" IMAGE_CURRENT="sbo-testbuild:current" IMAGE_STABLE="sbo-testbuild:15.0" VERSION_ID="current"; select_version_paths [[ "$ACTIVE_TREE" == "/trees/current" ]] && ok "current -> current tree" || bad "current tree wrong: [$ACTIVE_TREE]" [[ "$ACTIVE_IMAGE" == "sbo-testbuild:current" ]] && ok "current -> current image" || bad "current image wrong: [$ACTIVE_IMAGE]" VERSION_ID="15.0"; select_version_paths [[ "$ACTIVE_TREE" == "/trees/stable" ]] && ok "15.0 -> stable tree" || bad "stable tree wrong: [$ACTIVE_TREE]" [[ "$ACTIVE_IMAGE" == "sbo-testbuild:15.0" ]] && ok "15.0 -> stable image" || bad "stable image wrong: [$ACTIVE_IMAGE]" ``` - [ ] **Step 2: Run test to verify it fails** Run: `bash .extras/test-logic.sh` Expected: FAIL, `select_version_paths: command not found`. - [ ] **Step 3: Implement config globals, flags, parse_args, select_version_paths** In `.extras/test-build`, replace the `main() { : }` line and everything up to `main "$@"` with the flags/globals and functions below (keep the CONFIG block above untouched): ```bash # ---- flags / globals -------------------------------------------------------- USE_COLOR=1 # --no-color or non-TTY disables DRY_RUN=0 # --dry-run: resolve + print order, do not build ASSUME_YES=0 # --yes: skip the confirm prompt (still prints the order) USE_CACHE=1 # --no-cache disables the dep cache for one run VERSION_ID="current" # "current" | "15.0"; set by --stable TARGET_ARG="" ACTIVE_TREE="" # selected SBo tree (by version) ACTIVE_IMAGE="" # selected image tag (by version) RUN_DIR="" # timestamped log dir for this run # Status tracking. Keyed by "category/prog". Parallel assoc arrays. declare -A ST_STATUS=() declare -A ST_REASON=() declare -A ST_TIME=() declare -A ST_README=() usage() { cat <<'EOF' test-build - verify an SBo package builds on a target Slackware in docker USAGE: test-build [OPTIONS] OPTIONS: -h, --help This text. --stable Target Slackware 15.0 (image + tree). Default is -current. --dry-run Resolve, apply overrides, print the build order, do not build. --yes Skip the confirm prompt (the order is still printed first). --no-cache Rebuild all deps this run, ignore/refresh the cache. --no-color Disable ANSI color (auto-disabled when stdout is not a TTY). EOF } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0 ;; --stable|15.0) VERSION_ID="15.0"; shift ;; --dry-run) DRY_RUN=1; shift ;; --yes) ASSUME_YES=1; shift ;; --no-cache) USE_CACHE=0; shift ;; --no-color) USE_COLOR=0; shift ;; -*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; *) if [[ -n "$TARGET_ARG" ]]; then echo "Only one target accepted (got '$TARGET_ARG' and '$1')." >&2 exit 2 fi TARGET_ARG="$1"; shift ;; esac done if [[ -z "$TARGET_ARG" ]]; then echo "No target given." >&2; usage >&2; exit 2 fi } init_color() { if [[ $USE_COLOR -eq 1 && -t 1 ]]; then C_RED=$'\e[31m'; C_GRN=$'\e[32m'; C_YEL=$'\e[33m'; C_RST=$'\e[0m' else C_RED=""; C_GRN=""; C_YEL=""; C_RST="" fi } # Map VERSION_ID to the active tree + image. No I/O, unit-testable. select_version_paths() { if [[ "$VERSION_ID" == "15.0" ]]; then ACTIVE_TREE="$SBO_TREE_STABLE"; ACTIVE_IMAGE="$IMAGE_STABLE" else ACTIVE_TREE="$SBO_TREE_CURRENT"; ACTIVE_IMAGE="$IMAGE_CURRENT" fi } # require_config: the external config must exist and set the version's paths. require_config() { if [[ ! -f "$TB_CONFIG" ]]; then cat >&2 <&2 exit 1 fi if [[ ! -d "$ACTIVE_TREE" ]]; then echo "SBo tree for '$VERSION_ID' does not exist: $ACTIVE_TREE" >&2 exit 1 fi } main() { parse_args "$@" init_color require_config } main "$@" ``` - [ ] **Step 4: Run test to verify it passes** Run: `bash .extras/test-logic.sh` Expected: PASS (the new version-selection checks included). - [ ] **Step 5: Write the config example** Create `.extras/test-build-config.example`: ```bash # test-build config. Copy to ~/.config/sbo-testbuild/config and edit. # Override the path with the TB_CONFIG environment variable. # Local SBo trees, one per target Slackware version. SBO_TREE_CURRENT="/home/danix/SBo/current" SBO_TREE_STABLE="/home/danix/SBo/15.0" # Ready image tags (built by a separate job, consumed here by tag). IMAGE_CURRENT="sbo-testbuild:current" IMAGE_STABLE="sbo-testbuild:15.0" # Where per-run logs land. LOG_ROOT="/home/danix/.cache/sbo-test-build" # Dependency package cache (host dir). Empty disables caching. # Namespaced internally by image digest, so it self-invalidates on image update # and keeps current vs 15.0 separate. PKG_CACHE="/home/danix/.cache/sbo-test-build/pkgcache" ``` - [ ] **Step 6: Commit** ```bash git add .extras/test-build .extras/test-build-config.example .extras/test-logic.sh SBOLINT=no git commit -m 'test-build: config, arg parsing, version selection' ``` --- ## Task 3: Port the SBo-tree lookup + resolver + self-check **Files:** - Modify: `.extras/test-build` - Test: `.extras/test-logic.sh` - [ ] **Step 1: Write the failing test** Append to `.extras/test-logic.sh` before the result block: ```bash # --- resolution ------------------------------------------------------------- # Fake SBo tree under one category. mk "". mk() { mkdir -p "$T/cat/$1"; echo "REQUIRES=\"$2\"" > "$T/cat/$1/$1.info"; } mk c "" mk b "c" mk a "b %README%" mk d "nonexistentpkg" mk e "f" mk f "e" mk g "b" SBO_TREE_ROOTS=("$T") # resolver reads this global (set by select in real runs) resolve_target "$T/cat/a" order=""; for x in "${RESOLVED_ORDER[@]}"; do order+="$(basename "$x") "; done order="${order% }" [[ "$order" == "c b a" ]] && ok "topo order c b a" || bad "topo order, got: [$order]" [[ "${HAS_README[$T/cat/a]:-}" == "1" ]] && ok "%README% recorded" || bad "%README% not recorded" [[ ${#UNMET[@]} -eq 0 ]] && ok "no false unmet" || bad "unexpected unmet" resolve_target "$T/cat/d" [[ ${#UNMET[@]} -eq 1 ]] && ok "unmet-dep caught" || bad "unmet-dep missed" resolve_target "$T/cat/e" [[ ${#CYCLES[@]} -ge 1 ]] && ok "cycle caught" || bad "cycle missed" ``` Note: the base-installed check (`installed_in_base`) is exercised in real runs against the container, not here. In the self-check `BASE_DB` is empty, so a dep that is neither in the tree nor a known override reads as UNMET (which is the behavior we test with `d`). - [ ] **Step 2: Run test to verify it fails** Run: `bash .extras/test-logic.sh` Expected: FAIL, `resolve_target: command not found`. - [ ] **Step 3: Implement lookup + resolver (ported)** Add to `.extras/test-build` above `main()`: ```bash # ============================================================================= # SBo tree lookup # ============================================================================= # In this tool there is one active tree per run, but the resolver reads an array # named SBO_TREE_ROOTS so the ported logic and its self-check match sbo-batch-test. # main() sets SBO_TREE_ROOTS=("$ACTIVE_TREE") after require_config. declare -a SBO_TREE_ROOTS=() find_slackbuild_dir() { local prog="$1" root d for root in "${SBO_TREE_ROOTS[@]}"; do [[ -d "$root" ]] || continue for d in "$root"/*/"$prog"; do if [[ -d "$d" && -f "$d/$prog.info" ]]; then echo "$d"; return 0 fi done done return 1 } category_of() { basename "$(dirname "$1")"; } pkg_key() { echo "$(category_of "$1")/$(basename "$1")"; } read_requires() { local info="$1" # shellcheck disable=SC1090 ( set +u; source "$info"; echo "${REQUIRES:-}" ) } version_of() { local dir="$1" local info="$dir/$(basename "$dir").info" [[ -f "$info" ]] || return local v; v="$(grep -m1 '^VERSION=' "$info" | cut -d'"' -f2)" echo "$v" } # ============================================================================= # DEPENDENCY RESOLUTION (topo sort + cycle detection, LOCAL tree only) # ============================================================================= declare -a RESOLVED_ORDER=() declare -A UNMET=() declare -a CYCLES=() declare -A HAS_README=() declare -A _vstate=() # Is a prog already present in the container base? Overridden at build time to # consult the image; in resolution we treat "in base" as a callback so the pure # topo logic stays testable. Default: not in base (self-check has no base db). installed_in_base() { return 1; } _resolve_visit() { local dir="$1" parent="$2" local key; key="$(basename "$dir")" if [[ "${_vstate[$dir]:-}" == "1" ]]; then return 0; fi if [[ "${_vstate[$dir]:-}" == "0" ]]; then CYCLES+=("cycle involving $key (pulled in via $parent)") return 1 fi _vstate["$dir"]=0 local info="$dir/$(basename "$dir").info" local req tok depdir rc=0 req="$(read_requires "$info")" for tok in $req; do if [[ "$tok" == "%README%" ]]; then HAS_README["$dir"]=1 continue fi if depdir="$(find_slackbuild_dir "$tok")"; then _resolve_visit "$depdir" "$key" || rc=1 elif installed_in_base "$tok"; then : else UNMET["$tok"]="needed by $key" rc=1 fi done _vstate["$dir"]=1 RESOLVED_ORDER+=("$dir") return $rc } resolve_target() { local dir="$1" RESOLVED_ORDER=() CYCLES=() UNMET=() _vstate=() _resolve_visit "$dir" "(top)" } ``` Update `main()` to set the resolver's tree array after `require_config`: ```bash main() { parse_args "$@" init_color require_config SBO_TREE_ROOTS=("$ACTIVE_TREE") } ``` - [ ] **Step 4: Run test to verify it passes** Run: `bash .extras/test-logic.sh` Expected: PASS (topo order, %README%, unmet, cycle checks green). - [ ] **Step 5: Commit** ```bash git add .extras/test-build .extras/test-logic.sh SBOLINT=no git commit -m 'test-build: port SBo lookup + dependency resolver' ``` --- ## Task 4: Override engine (drop / rename / fetch) + UNMET stop **Files:** - Modify: `.extras/test-build` - Create: `.extras/overrides.example` - Test: `.extras/test-logic.sh` The override file encodes current-vs-stable deltas. It is applied ONLY when `VERSION_ID == current` (15.0 is the SBo baseline). Rules: - `drop: ` remove from the build order (already in -current base) - `rename: -> ` rewrite the dep name before it is looked up - `fetch: ` mark this dep to come from sbopkg in-container, not the local tree - [ ] **Step 1: Write the failing test** Append to `.extras/test-logic.sh` before the result block: ```bash # --- overrides -------------------------------------------------------------- OV=$(mktemp) cat > "$OV" <<'EOF' # comment ignored drop: dropme rename: oldname -> newname fetch: fetchme EOF TB_OVERRIDES="$OV" load_overrides [[ "${OV_DROP[dropme]:-}" == "1" ]] && ok "override drop parsed" || bad "drop not parsed" [[ "${OV_RENAME[oldname]:-}" == "newname" ]] && ok "override rename parsed" || bad "rename not parsed" [[ "${OV_FETCH[fetchme]:-}" == "1" ]] && ok "override fetch parsed" || bad "fetch not parsed" # rename maps a token [[ "$(apply_rename oldname)" == "newname" ]] && ok "apply_rename maps" || bad "apply_rename wrong: [$(apply_rename oldname)]" [[ "$(apply_rename untouched)" == "untouched" ]] && ok "apply_rename passthrough" || bad "apply_rename mangled untouched" # drop filters the order (build a fake order of dirs; dropme is removed) mk dropme "" mk keepme "" RESOLVED_ORDER=("$T/cat/dropme" "$T/cat/keepme") apply_overrides_to_order kept=""; for x in "${RESOLVED_ORDER[@]}"; do kept+="$(basename "$x") "; done kept="${kept% }" [[ "$kept" == "keepme" ]] && ok "drop removes from order" || bad "drop failed, order=[$kept]" rm -f "$OV" # overrides only apply on current; on 15.0 they are a no-op VERSION_ID="15.0" TB_OVERRIDES="$OV" # file gone; must not error, must clear maps load_overrides [[ ${#OV_DROP[@]} -eq 0 ]] && ok "overrides inert on 15.0" || bad "overrides applied on 15.0" VERSION_ID="current" ``` - [ ] **Step 2: Run test to verify it fails** Run: `bash .extras/test-logic.sh` Expected: FAIL, `load_overrides: command not found`. - [ ] **Step 3: Implement the override engine** Add to `.extras/test-build` above `main()`: ```bash # ============================================================================= # current-vs-stable overrides. Parsed from $TB_OVERRIDES. Applied only when # targeting -current (15.0 is the SBo baseline, no deltas). # ============================================================================= declare -A OV_DROP=() # prog -> 1 declare -A OV_RENAME=() # old -> new declare -A OV_FETCH=() # prog -> 1 load_overrides() { OV_DROP=(); OV_RENAME=(); OV_FETCH=() # 15.0 is the baseline: no overrides. [[ "$VERSION_ID" == "15.0" ]] && return [[ -f "$TB_OVERRIDES" ]] || return local line kind rest while IFS= read -r line; do line="${line%%#*}" # strip comments line="${line#"${line%%[![:space:]]*}"}" # ltrim [[ -z "$line" ]] && continue kind="${line%%:*}"; rest="${line#*:}" kind="${kind//[[:space:]]/}" rest="${rest#"${rest%%[![:space:]]*}"}" # ltrim value case "$kind" in drop) OV_DROP["${rest//[[:space:]]/}"]=1 ;; fetch) OV_FETCH["${rest//[[:space:]]/}"]=1 ;; rename) # rest is "old -> new" local old new old="${rest%%->*}"; new="${rest##*->}" old="${old//[[:space:]]/}"; new="${new//[[:space:]]/}" [[ -n "$old" && -n "$new" ]] && OV_RENAME["$old"]="$new" ;; *) echo "WARN: unknown override rule: $line" >&2 ;; esac done < "$TB_OVERRIDES" } # Map a dep token through rename rules (identity if no rule). apply_rename() { local tok="$1" echo "${OV_RENAME[$tok]:-$tok}" } # Remove dropped packages from RESOLVED_ORDER in place. apply_overrides_to_order() { local d prog keep=() for d in "${RESOLVED_ORDER[@]}"; do prog="$(basename "$d")" [[ "${OV_DROP[$prog]:-}" == "1" ]] && continue keep+=("$d") done RESOLVED_ORDER=("${keep[@]:-}") } ``` Wire `apply_rename` into the resolver so renamed deps look up under the new name. In `_resolve_visit`, change the dep-lookup line: ```bash for tok in $req; do if [[ "$tok" == "%README%" ]]; then HAS_README["$dir"]=1 continue fi tok="$(apply_rename "$tok")" # <-- add this line if [[ "${OV_FETCH[$tok]:-}" == "1" ]]; then FETCH_DEPS["$tok"]=1 # satisfied via sbopkg at build time continue fi if depdir="$(find_slackbuild_dir "$tok")"; then _resolve_visit "$depdir" "$key" || rc=1 elif installed_in_base "$tok"; then : else UNMET["$tok"]="needed by $key" rc=1 fi done ``` Add the `FETCH_DEPS` global with the other resolver globals: ```bash declare -A FETCH_DEPS=() # prog -> 1: resolve via sbopkg in-container ``` And reset it in `resolve_target`: ```bash resolve_target() { local dir="$1" RESOLVED_ORDER=() CYCLES=() UNMET=() FETCH_DEPS=() _vstate=() _resolve_visit "$dir" "(top)" } ``` Load overrides in `main()` (before any resolve happens, after tree setup): ```bash main() { parse_args "$@" init_color require_config SBO_TREE_ROOTS=("$ACTIVE_TREE") load_overrides } ``` - [ ] **Step 4: Run test to verify it passes** Run: `bash .extras/test-logic.sh` Expected: PASS (all override checks green). - [ ] **Step 5: Write the overrides example** Create `.extras/overrides.example`: ``` # current-vs-stable dep overrides for test-build. # Copy to ~/.config/sbo-testbuild/overrides and edit. Applied only when # targeting -current (15.0 is the SBo baseline). One rule per line. # # drop: dep is in 15.0 but already in the -current base # rename: -> dep was renamed on -current # fetch: dep was removed from the -current tree; pull via sbopkg # # Start light: add rules only when a real package needs them. # drop: rust # rename: python3-foo -> foo # fetch: somelib ``` - [ ] **Step 6: Commit** ```bash git add .extras/test-build .extras/overrides.example .extras/test-logic.sh SBOLINT=no git commit -m 'test-build: current-vs-stable override engine' ``` --- ## Task 5: Port the dep cache, namespaced by image digest **Files:** - Modify: `.extras/test-build` - Test: `.extras/test-logic.sh` The cache is the ported sbo-batch-test cache with one change: the cache root seen by `cache_*` is `$PKG_CACHE/`, computed once per run. A new digest gives a fresh namespace (old deps ignored), which is the invalidation. - [ ] **Step 1: Write the failing test** Append to `.extras/test-logic.sh` before the result block: ```bash # --- cache ------------------------------------------------------------------ PKG_CACHE=$(mktemp -d) CACHE_ROOT="$PKG_CACHE/sha256-deadbeef" # simulate a resolved digest namespace mkc() { mkdir -p "$CACHE_ROOT/$1/$2"; : > "$CACHE_ROOT/$1/$2/$3"; } mkc net libfoo "libfoo-1.1-x86_64-1_danix.txz" [[ "$(cache_decision net libfoo 1.1)" == "cached" ]] && ok "cache hit on version match" || bad "cache_decision got [$(cache_decision net libfoo 1.1)]" [[ "$(cache_decision net libfoo 1.2)" == "bump:1.1:1.2" ]] && ok "cache bump reported" || bad "cache bump got [$(cache_decision net libfoo 1.2)]" [[ "$(cache_decision net libbar 1.0)" == "new" ]] && ok "cache new for absent" || bad "cache new got [$(cache_decision net libbar 1.0)]" hit="$(cache_path net libfoo 1.1)" [[ "$hit" == "$CACHE_ROOT/net/libfoo/libfoo-1.1-x86_64-1_danix.txz" ]] && ok "cache_path returns hit" || bad "cache_path got [$hit]" [[ -z "$(cache_path net libfoo 9.9)" ]] && ok "cache_path empty on miss" || bad "cache_path not empty on miss" srctmp=$(mktemp -d); : > "$srctmp/libfoo-1.2-x86_64-1_danix.txz" cache_store net libfoo "$srctmp/libfoo-1.2-x86_64-1_danix.txz" count=$(find "$CACHE_ROOT/net/libfoo" -name '*.t?z' | wc -l) [[ "$count" -eq 1 ]] && ok "cache_store evicts to one file" || bad "cache_store left $count files" [[ -e "$CACHE_ROOT/net/libfoo/libfoo-1.2-x86_64-1_danix.txz" ]] && ok "cache_store stored new file" || bad "cache_store did not store" rm -rf "$srctmp" # disabled cache -> new CACHE_ROOT_SAVE="$CACHE_ROOT"; USE_CACHE=0 [[ "$(cache_decision net libfoo 1.2)" == "new" ]] && ok "--no-cache disables (new)" || bad "disabled cache got [$(cache_decision net libfoo 1.2)]" USE_CACHE=1; CACHE_ROOT="$CACHE_ROOT_SAVE" ``` - [ ] **Step 2: Run test to verify it fails** Run: `bash .extras/test-logic.sh` Expected: FAIL, `cache_decision: command not found`. - [ ] **Step 3: Implement the cache (ported, keyed on CACHE_ROOT + USE_CACHE)** Add to `.extras/test-build` above `main()`: ```bash # ============================================================================= # Dependency cache. Layout: $CACHE_ROOT///--...txz where # CACHE_ROOT = $PKG_CACHE/ (set per run by resolve_cache_root). # Key is prog+version. --no-cache (USE_CACHE=0) or empty PKG_CACHE disables. # ============================================================================= CACHE_ROOT="" # set by resolve_cache_root once the image digest is known # True when the cache is usable this run. _cache_on() { [[ $USE_CACHE -eq 1 && -n "$PKG_CACHE" && -n "$CACHE_ROOT" ]]; } _cache_ver_of() { local prog="$1" base="$2" base="${base#"$prog"-}" echo "${base%%-*}" } # cache_decision -> cached | bump:OLD:NEW | new cache_decision() { local cat="$1" prog="$2" version="$3" _cache_on || { echo new; return; } local dir="$CACHE_ROOT/$cat/$prog" local f newest="" for f in "$dir/$prog"-*.t?z; do [[ -e "$f" ]] || continue [[ -z "$newest" || "$f" -nt "$newest" ]] && newest="$f" done [[ -z "$newest" ]] && { echo new; return; } local have; have="$(_cache_ver_of "$prog" "$(basename "$newest")")" if [[ "$have" == "$version" ]]; then echo cached; else echo "bump:$have:$version"; fi } cache_path() { local cat="$1" prog="$2" version="$3" _cache_on || return local dir="$CACHE_ROOT/$cat/$prog" local f newest="" for f in "$dir/$prog"-*.t?z; do [[ -e "$f" ]] || continue [[ -z "$newest" || "$f" -nt "$newest" ]] && newest="$f" done [[ -z "$newest" ]] && return [[ "$(_cache_ver_of "$prog" "$(basename "$newest")")" == "$version" ]] && echo "$newest" } cache_store() { local cat="$1" prog="$2" src="$3" _cache_on || return local dir="$CACHE_ROOT/$cat/$prog" mkdir -p "$dir" rm -f "$dir"/*.t?z cp -a "$src" "$dir/" } cache_label() { local dir="$1" is_target="$2" local cat prog ver dec cat="$(category_of "$dir")"; prog="$(basename "$dir")"; ver="$(version_of "$dir")" dec="$(cache_decision "$cat" "$prog" "$ver")" local label case "$dec" in cached) label="cached ($ver)" ;; bump:*) label="rebuild: ${dec#bump:}"; label="${label/:/ -> }" ;; *) label="build (new)" ;; esac if [[ "$is_target" == "1" ]]; then case "$dec" in cached) label="build (cached $ver, rebuilt as target)" ;; esac echo "target, $label" else echo "$label" fi } # Compute CACHE_ROOT from the image's digest. Falls back to the tag if the # digest cannot be read (still isolates per image reference). resolve_cache_root() { [[ -z "$PKG_CACHE" ]] && { CACHE_ROOT=""; return; } local digest digest="$(docker image inspect --format '{{index .Id}}' "$ACTIVE_IMAGE" 2>/dev/null)" [[ -z "$digest" ]] && digest="tag-${ACTIVE_IMAGE//[^a-zA-Z0-9._-]/_}" digest="${digest//[^a-zA-Z0-9._-]/_}" CACHE_ROOT="$PKG_CACHE/$digest" mkdir -p "$CACHE_ROOT" } ``` - [ ] **Step 4: Run test to verify it passes** Run: `bash .extras/test-logic.sh` Expected: PASS (all cache checks green). - [ ] **Step 5: Commit** ```bash git add .extras/test-build .extras/test-logic.sh SBOLINT=no git commit -m 'test-build: port dep cache, namespaced by image digest' ``` --- ## Task 6: BLOCKED-BY-DEP propagation (ported) **Files:** - Modify: `.extras/test-build` - Test: `.extras/test-logic.sh` - [ ] **Step 1: Write the failing test** Append to `.extras/test-logic.sh` before the result block: ```bash # --- BLOCKED-BY-DEP (depends_on_failed) ------------------------------------- dead=(b) if depends_on_failed "$T/cat/g" dead; then ok "g blocked when b dead"; else bad "g should block on b"; fi if depends_on_failed "$T/cat/a" dead; then ok "a blocked when b dead (direct)"; else bad "a should block on b"; fi dead=(c) if depends_on_failed "$T/cat/a" dead; then bad "a wrongly blocked on c"; else ok "a not directly blocked by c"; fi if depends_on_failed "$T/cat/b" dead; then ok "b blocked when c dead"; else bad "b should block on c"; fi dead=() if depends_on_failed "$T/cat/a" dead; then bad "a blocked with empty dead"; else ok "no block when nothing dead"; fi dead=("%README%") if depends_on_failed "$T/cat/a" dead; then bad "%README% treated as dep"; else ok "%README% not treated as dep"; fi ``` - [ ] **Step 2: Run test to verify it fails** Run: `bash .extras/test-logic.sh` Expected: FAIL, `depends_on_failed: command not found`. - [ ] **Step 3: Implement depends_on_failed (ported)** Add to `.extras/test-build` above `main()`: ```bash # Does SlackBuild dir $1 directly require any prog in the dead list (nameref $2)? # Direct-requires check only; transitive blocking works because run_target # iterates in topo order, propagating a failure one hop per package. depends_on_failed() { local dir="$1"; local -n failed="$2" local info="$dir/$(basename "$dir").info" local req tok f req="$(read_requires "$info")" for tok in $req; do [[ "$tok" == "%README%" ]] && continue for f in "${failed[@]:-}"; do [[ "$tok" == "$f" ]] && return 0 done done return 1 } ``` - [ ] **Step 4: Run test to verify it passes** Run: `bash .extras/test-logic.sh` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add .extras/test-build .extras/test-logic.sh SBOLINT=no git commit -m 'test-build: port BLOCKED-BY-DEP propagation' ``` --- ## Task 7: The container build step (build_one via docker) **Files:** - Modify: `.extras/test-build` No self-check here (docker/build is out of the pure-logic reach, same boundary as sbo-batch-test). Verified by a real run in Task 10. - [ ] **Step 1: Implement build_one + lint_pkg** Add to `.extras/test-build` above `main()`. `lint_pkg` is ported verbatim: ```bash # lint_pkg -> run sbopkglint on a built package. Host-side, target # only, fail-soft (skip if absent, never change SUCCESS). lint_pkg() { local txz="$1" logf="$2" if ! command -v sbopkglint >/dev/null 2>&1; then echo " sbopkglint not installed on host, skipping lint" return 0 fi echo " sbopkglint $(basename "$txz") ..." echo "===== sbopkglint: $(basename "$txz") =====" >> "$logf" local out rc out="$(sbopkglint "$txz" 2>&1)"; rc=$? printf '%s\n' "$out" >> "$logf" if [[ $rc -eq 0 ]]; then echo " lint: ${C_GRN}clean${C_RST}" else echo " lint: ${C_RED}findings${C_RST} (see $(basename "$logf")):" printf '%s\n' "$out" | sed 's/^/ /' fi } ``` `build_one` runs the same download/md5/build/installpkg steps as sbo-batch-test, but inside a container via `docker run` instead of `chroot`. The container gets: the SlackBuild dir mounted read-only, a work dir for output, and (for deps) the cached .txz mounted in. `sbopkglint` runs host-side on the copied-out package. ```bash # build_one [container-name] # Runs the build in a throwaway container. Sets ST_STATUS/ST_REASON/ST_TIME. # Successful builds copy their package to a host workdir; deps are cached and # the target is linted. Returns 0 on SUCCESS/CACHED, 1 otherwise. build_one() { local dir="$1" is_target="${2:-0}" local prog cat key prog="$(basename "$dir")"; cat="$(category_of "$dir")"; key="$cat/$prog" local logf="$RUN_DIR/${cat}_${prog}.log" local start; start=$(date +%s) local version; version="$(version_of "$dir")" [[ "${HAS_README[$dir]:-}" == "1" ]] && ST_README["$key"]=1 # Dep with a version-matching cached package: installpkg it into the shared # dep-package dir; no build. The target never takes this path. if [[ "$is_target" != "1" ]]; then local cached; cached="$(cache_path "$cat" "$prog" "$version")" if [[ -n "$cached" ]]; then cp -a "$cached" "$DEPS_DIR/" { echo "===== test-build: $prog (from cache) =====" echo "cached package: $(basename "$cached")" } >> "$logf" ST_TIME["$key"]=$(( $(date +%s) - start )) ST_STATUS["$key"]="CACHED" return 0 fi fi # Build in a container. Mounts: # $dir -> /sbo/pkg (ro, the SlackBuild) # $DEPS_DIR -> /sbo/deps (rw, already-built dep .txz to installpkg first) # $BUILD_OUT -> /sbo/out (rw, where the built package is copied out) # The in-container script installs any deps present, then builds the target, # writes a status token to /sbo/out/$prog.status, and copies the package out. local statf="$BUILD_OUT/$prog.status" rm -f "$statf" # fetch deps (removed from -current tree): let the container's sbopkg build # them first. FETCH_DEPS is the set collected during resolution. local fetch_list="" local fp for fp in "${!FETCH_DEPS[@]}"; do fetch_list+="$fp "; done docker run --rm \ -v "$dir":/sbo/pkg:ro \ -v "$DEPS_DIR":/sbo/deps \ -v "$BUILD_OUT":/sbo/out \ -e PROG="$prog" \ -e FETCH_LIST="$fetch_list" \ "$ACTIVE_IMAGE" /bin/bash -s >>"$logf" 2>&1 <<'CONTAINER_EOF' set -uo pipefail prog="$PROG" statf="/sbo/out/$prog.status" # 0. install already-built dependency packages (order guaranteed by the host). for d in /sbo/deps/*.t?z; do [[ -e "$d" ]] || continue installpkg --terse "$d" || { echo "INSTALL-FAILED (dep $d)"; echo INSTALL-FAILED > "$statf"; exit 1; } done # 0b. fetch-from-SBo deps via sbopkg (removed from the -current tree). for f in $FETCH_LIST; do echo "sbopkg-building fetch dep: $f" sbopkg -B -i "$f" || { echo "BUILD-FAILED (fetch dep $f)"; echo BUILD-FAILED > "$statf"; exit 1; } done # copy the SlackBuild out of the read-only mount so it can write there. cp -a /sbo/pkg /sbo/build cd /sbo/build || { echo BUILD-FAILED > "$statf"; exit 1; } . ./"$prog".info export OUTPUT=/sbo/out mkdir -p "$OUTPUT" echo "===== test-build: $prog =====" echo "PRGNAM=${PRGNAM:-$prog} VERSION=${VERSION:-?} BUILD=${BUILD:-?} TAG=${TAG:-?}" echo "uname -m: $(uname -m) OUTPUT=$OUTPUT" echo "REQUIRES=${REQUIRES:-}" echo "=================================" if [ "$(uname -m)" = "x86_64" ] && [ -n "${DOWNLOAD_x86_64:-}" ] && [ "${DOWNLOAD_x86_64}" != "UNSUPPORTED" ] && [ "${DOWNLOAD_x86_64}" != "UNTESTED" ]; then DL="$DOWNLOAD_x86_64"; MD="$MD5SUM_x86_64" else DL="$DOWNLOAD"; MD="$MD5SUM" fi for u in $DL; do wget -c --tries=3 "$u" || { echo DOWNLOAD-FAILED > "$statf"; exit 1; } done set -- $MD for u in $DL; do f="$(basename "$u")" want="$1"; shift got="$(md5sum "$f" | cut -d' ' -f1)" if [ "$got" != "$want" ]; then echo "MD5 mismatch on $f: want $want got $got" echo MD5-MISMATCH > "$statf"; exit 1 fi done chmod +x ./"$prog".SlackBuild if ! ./"$prog".SlackBuild; then echo BUILD-FAILED > "$statf"; exit 1 fi pkg="$(ls -t "$OUTPUT"/"$prog"-*.t?z 2>/dev/null | head -n1)" if [ -z "$pkg" ]; then echo "No package produced in $OUTPUT" echo BUILD-FAILED > "$statf"; exit 1 fi if ! installpkg --terse "$pkg"; then echo INSTALL-FAILED > "$statf"; exit 1 fi echo "===== installed files: $(basename "$pkg") =====" pkgname="$(basename "$pkg")"; pkgname="${pkgname%.t?z}" cat "/var/log/packages/$pkgname" 2>/dev/null || echo "(package db entry not found)" echo "=================================" echo SUCCESS > "$statf" CONTAINER_EOF local status="BUILD-FAILED" [[ -f "$statf" ]] && status="$(cat "$statf")" ST_TIME["$key"]=$(( $(date +%s) - start )) ST_STATUS["$key"]="$status" if [[ "$status" == "SUCCESS" ]]; then # locate the built package copied to the host workdir local built newest="" for built in "$BUILD_OUT/${prog}"-*.t?z; do [[ -e "$built" ]] || continue [[ -z "$newest" || "$built" -nt "$newest" ]] && newest="$built" done if [[ -n "$newest" ]]; then if [[ "$is_target" != "1" ]]; then cache_store "$cat" "$prog" "$newest" # make the dep available to later builds in this run cp -a "$newest" "$DEPS_DIR/" else lint_pkg "$newest" "$logf" fi fi return 0 fi ST_REASON["$key"]="see $(basename "$logf")" return 1 } ``` - [ ] **Step 2: Sanity-check bash syntax** Run: `bash -n .extras/test-build` Expected: no output (syntax OK). - [ ] **Step 3: Run the self-check still passes (no regression)** Run: `bash .extras/test-logic.sh` Expected: PASS (build_one is not exercised, but sourcing must not break). - [ ] **Step 4: Commit** ```bash git add .extras/test-build SBOLINT=no git commit -m 'test-build: container build step (build_one via docker)' ``` --- ## Task 8: run_target orchestration + summary **Files:** - Modify: `.extras/test-build` - [ ] **Step 1: Implement run_target + print_summary + confirm** Add to `.extras/test-build` above `main()`. `print_summary` is ported verbatim (status set is the same). `run_target` replaces the overlay lifecycle with the per-run docker workdirs and adds the printed-order + Y/n confirm. ```bash # confirm_order: print the resolved order (overrides marked) and ask to proceed. # --yes skips the prompt but the order is still printed. --dry-run never reaches # here. Returns 0 to proceed, 1 to abort. confirm_order() { local target_dir="$1" echo " build order (${VERSION_ID}):" local d for d in "${RESOLVED_ORDER[@]}"; do local it=0; [[ "$d" == "$target_dir" ]] && it=1 local rdm=""; [[ "${HAS_README[$d]:-}" == 1 ]] && rdm=" [%README%]" printf " %-30s %s%s\n" "$(pkg_key "$d")" "$(cache_label "$d" "$it")" "$rdm" echo "$(pkg_key "$d")" >> "$RUN_DIR/build-order.txt" done # note fetch deps (built via sbopkg in-container, not in the order list) local fp for fp in "${!FETCH_DEPS[@]}"; do printf " %-30s %s\n" "$fp" "fetch (sbopkg in container)" done [[ $ASSUME_YES -eq 1 ]] && return 0 local reply read -rp " Proceed? [Y/n] " reply [[ -z "$reply" || "$reply" =~ ^[Yy]$ ]] } # run_target run_target() { local target_dir="$1" local tkey; tkey="$(pkg_key "$target_dir")" echo echo "=== Target: $tkey (${VERSION_ID}) ===" resolve_target "$target_dir" apply_overrides_to_order # Hard resolution failures: report and stop, do not build. if [[ ${#CYCLES[@]} -gt 0 || ${#UNMET[@]} -gt 0 ]]; then local why="" if [[ ${#UNMET[@]} -gt 0 ]]; then local u for u in "${!UNMET[@]}"; do why+="unmet:$u(${UNMET[$u]}) "; done fi [[ ${#CYCLES[@]} -gt 0 ]] && why+="${CYCLES[*]}" ST_STATUS["$tkey"]="UNMET-DEP" ST_REASON["$tkey"]="$why" echo " resolution failed: $why" echo " add an override rule ($TB_OVERRIDES) and rerun, or fix the tree." >&2 return fi if [[ $DRY_RUN -eq 1 ]]; then echo " build order (dry-run):" local d for d in "${RESOLVED_ORDER[@]}"; do local it=0; [[ "$d" == "$target_dir" ]] && it=1 local rdm=""; [[ "${HAS_README[$d]:-}" == 1 ]] && rdm=" [%README%]" printf " %-30s %s%s\n" "$(pkg_key "$d")" "$(cache_label "$d" "$it")" "$rdm" echo "$(pkg_key "$d")" >> "$RUN_DIR/build-order.txt" done local fp for fp in "${!FETCH_DEPS[@]}"; do printf " %-30s %s\n" "$fp" "fetch (sbopkg in container)" done return fi if ! confirm_order "$target_dir"; then echo " aborted." ST_STATUS["$tkey"]="ABORTED" return fi # Per-run docker workdirs (host side, discarded after the run). DEPS_DIR="$RUN_DIR/deps"; BUILD_OUT="$RUN_DIR/out" mkdir -p "$DEPS_DIR" "$BUILD_OUT" local d failed_progs=() for d in "${RESOLVED_ORDER[@]}"; do local key; key="$(pkg_key "$d")" local prog; prog="$(basename "$d")" if depends_on_failed "$d" failed_progs; then ST_STATUS["$key"]="BLOCKED-BY-DEP" ST_REASON["$key"]="blocked by failed dep" [[ "${HAS_README[$d]:-}" == "1" ]] && ST_README["$key"]=1 echo " $key: BLOCKED-BY-DEP" failed_progs+=("$prog") continue fi local it=0; [[ "$d" == "$target_dir" ]] && it=1 echo " building $key ..." if build_one "$d" "$it"; then echo " $key: ${ST_STATUS[$key]} (${ST_TIME[$key]}s)" else echo " $key: ${ST_STATUS[$key]} (${ST_TIME[$key]}s)" failed_progs+=("$prog") fi done } # ============================================================================= # SUMMARY (ported) # ============================================================================= print_summary() { local total=$SECONDS local succ=0 fail=0 blocked=0 cached=0 local summary="$RUN_DIR/summary.log" { echo "test-build run summary" echo "target: $TARGET_ARG version: $VERSION_ID" echo } > "$summary" echo echo "================ SUMMARY ================" local key for key in "${!ST_STATUS[@]}"; do local st="${ST_STATUS[$key]}" rsn="${ST_REASON[$key]:-}" t="${ST_TIME[$key]:-0}" local rd=""; [[ "${ST_README[$key]:-}" == "1" ]] && rd=" [%README%]" local col="$C_YEL" case "$st" in SUCCESS) col="$C_GRN"; ((succ++)) ;; CACHED) col="$C_GRN"; ((cached++)) ;; BLOCKED-BY-DEP|UNMET-DEP|ABORTED) col="$C_YEL"; ((blocked++)) ;; *) col="$C_RED"; ((fail++)) ;; esac printf "%s%-30s %-16s%s %s%s (%ss)\n" "$col" "$key" "$st" "$C_RST" "$rsn" "$rd" "$t" printf "%-30s %-16s %s%s (%ss)\n" "$key" "$st" "$rsn" "$rd" "$t" >> "$summary" done echo "----------------------------------------" printf "%s%d succeeded%s, %s%d failed%s, %s%d blocked%s, %s%d cached%s, total %ss\n" \ "$C_GRN" "$succ" "$C_RST" "$C_RED" "$fail" "$C_RST" "$C_YEL" "$blocked" "$C_RST" \ "$C_GRN" "$cached" "$C_RST" "$total" echo "logs: $RUN_DIR" if [[ $fail -eq 0 && $blocked -eq 0 ]]; then echo "${C_GRN}All green.${C_RST} Safe to build the SBo submission tarball on the host." fi { echo echo "$succ succeeded, $fail failed, $blocked blocked, $cached cached, total ${total}s" echo "logs: $RUN_DIR" } >> "$summary" } ``` Add the two workdir globals near the other globals (top of the flags block): ```bash DEPS_DIR="" # per-run host dir of built dep .txz, mounted into the container BUILD_OUT="" # per-run host dir where built packages are copied out ``` - [ ] **Step 2: Sanity-check syntax** Run: `bash -n .extras/test-build` Expected: no output. - [ ] **Step 3: Self-check no regression** Run: `bash .extras/test-logic.sh` Expected: PASS. - [ ] **Step 4: Commit** ```bash git add .extras/test-build SBOLINT=no git commit -m 'test-build: run_target orchestration + summary + confirm' ``` --- ## Task 9: Wire main(), image presence check, target lookup **Files:** - Modify: `.extras/test-build` - [ ] **Step 1: Implement the full main()** Replace the current `main()` in `.extras/test-build` with: ```bash # Verify the selected image exists locally. It is built by a separate job; this # script only consumes it. (Later: a local-registry docker pull slots in here.) require_image() { if ! docker image inspect "$ACTIVE_IMAGE" >/dev/null 2>&1; then cat >&2 </dev/null 2>&1 || { echo "docker not found in PATH." >&2; exit 1; } require_image fi resolve_cache_root RUN_DIR="$LOG_ROOT/$(date +%Y-%m-%d_%H-%M-%S)" mkdir -p "$RUN_DIR" : > "$RUN_DIR/build-order.txt" # Single-package mode: resolve the name in the active tree. local tdir if ! tdir="$(find_slackbuild_dir "$TARGET_ARG")"; then echo "Program not found in the SBo tree ($ACTIVE_TREE): $TARGET_ARG" >&2 exit 1 fi run_target "$tdir" print_summary } ``` - [ ] **Step 2: Sanity-check syntax** Run: `bash -n .extras/test-build` Expected: no output. - [ ] **Step 3: Dry-run smoke test against the real tree (no docker needed)** This needs a real config. If `~/.config/sbo-testbuild/config` is not set up, create a throwaway one pointing at an existing local SBo tree, then: Run: `.extras/test-build --dry-run ` Expected: prints `=== Target: ... ===`, a build order, exits 0, no docker call. - [ ] **Step 4: Self-check no regression** Run: `bash .extras/test-logic.sh` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add .extras/test-build SBOLINT=no git commit -m 'test-build: wire main, image-presence check, target lookup' ``` --- ## Task 10: Real end-to-end build (user-run) + README **Files:** - Modify: `.extras/test-build` (only if the real run surfaces a bug) - Create: `.extras/README.test-build` (short usage note) This task needs a real image and the buildsystem. Per repo rules the throwaway docker build is Claude-OK, but the IMAGE must exist first (separate job, not yet built). So this task is: document usage, and record the end-to-end run as a manual verification step the user performs once an image is available. - [ ] **Step 1: Write the usage README** Create `.extras/README.test-build`: ```markdown # test-build Verify an already-published SBo package still builds on a target Slackware version inside a throwaway docker container. ## Setup mkdir -p ~/.config/sbo-testbuild cp .extras/test-build-config.example ~/.config/sbo-testbuild/config cp .extras/overrides.example ~/.config/sbo-testbuild/overrides $EDITOR ~/.config/sbo-testbuild/config Set the two SBo tree paths, the two image tags, LOG_ROOT, and PKG_CACHE. The images (`sbo-testbuild:current` / `:15.0`) are FULL Slackware installs plus `sbo-maintainer-tools` and `sbopkg`, built by a separate job (not this script). The script errors if the tagged image is not present locally. ## Usage .extras/test-build # build on -current (default) .extras/test-build --stable # build on 15.0 .extras/test-build --dry-run # show the build order, no build .extras/test-build --yes # skip the confirm (order still shown) .extras/test-build --no-cache # rebuild all deps this run A green run means it is safe to build the SBo submission tarball on the host. ## current-vs-stable overrides `~/.config/sbo-testbuild/overrides` (see `overrides.example`) encodes deltas that only apply when building on -current: drop: already in the -current base rename: -> renamed on -current fetch: removed from the -current tree; built via sbopkg Start light; add rules only when a real package needs them. ## Self-check bash .extras/test-logic.sh ``` - [ ] **Step 2: Commit the docs** ```bash git add .extras/README.test-build SBOLINT=no git commit -m 'test-build: usage README' ``` - [ ] **Step 3: Manual end-to-end verification (user, once an image exists)** Documented for the user to run when a `sbo-testbuild:*` image is available: ```bash # pick a small already-published package with a dep, e.g. one already in the repo .extras/test-build --yes ``` Expected: build order printed, deps built (or CACHED on a second run), target built, `sbopkglint` result shown, summary green, `All green.` line printed. A second run of the same package should show its deps as `CACHED`. Record the outcome. If a real bug surfaces (mount path, sbopkg invocation, package glob), fix it in `.extras/test-build` and re-run; add a self-check case if the bug was in pure logic. --- ## Self-Review Run this checklist against the spec after the plan is written: **1. Spec coverage:** - purpose / one-target-per-run: Task 2 (`parse_args`), Task 9 (`main`) - covered - two versions, current default: Task 2 (`--stable`, `select_version_paths`) - covered - resolve local tree + topo: Task 3 - covered - overrides drop/rename/fetch + UNMET stop: Task 4, stop in Task 8 `run_target` - covered - external config + overrides file: Task 2, Task 4 - covered - external image, presence check, no bake: Task 9 `require_image` - covered - container build (download/md5/build/installpkg): Task 7 - covered - fetch via sbopkg: Task 7 (`FETCH_LIST` loop) - covered - dep cache, digest-namespaced, --no-cache, target always fresh: Task 5, wired Task 7/8 - covered - target lint: Task 7 `lint_pkg`, called in Task 7 build_one - covered - confirm order + --yes still prints: Task 8 `confirm_order` - covered - --dry-run: Task 8 `run_target`, Task 9 skips image check - covered - report + logs (per-pkg, summary, build-order): Task 8 `print_summary`, Task 7 logs - covered - status values incl CACHED: Task 8 `print_summary` - covered - self-check parity: Tasks 1-6 - covered **2. Placeholder scan:** no TBD/TODO in code steps; every code step shows full code. The one deferred item (real end-to-end run) is explicit and gated on an external image, not a placeholder. **3. Type consistency:** function + global names match across tasks: `RESOLVED_ORDER`, `FETCH_DEPS`, `OV_DROP/OV_RENAME/OV_FETCH`, `CACHE_ROOT`, `ACTIVE_TREE/ACTIVE_IMAGE`, `DEPS_DIR/BUILD_OUT`, `cache_decision/cache_path/ cache_store/cache_label`, `select_version_paths`, `apply_overrides_to_order`, `apply_rename`, `load_overrides`, `resolve_cache_root`, `require_image`, `build_one(dir,is_target)`, `run_target`, `confirm_order`, `print_summary`. Consistent. ## Notes on deliberate simplifications - Single-package mode only (no category-folder / all / queue modes). This repo bumps one package at a time. Extension point is `main`'s target lookup. - `-j` parallelism dropped entirely (was a no-op stub upstream). Builds serial. - fetch deps are not topo-ordered among themselves; sbopkg resolves their own deps. Start light per the spec; revisit if a fetch dep needs another fetch dep. ```