# CLAUDE.md Working notes for Claude Code on this project. Read before editing. ## What this is `sbo-batch-test`: a single self-contained bash script that batch-tests SlackBuilds against a clean Slackware 15.0 overlay chroot. The user is an SBo (SlackBuilds.org) maintainer on Slackware64-current who needs to verify builds against 15.0 stable without booting a VM. Full spec lives in `claude-code-prompt-sbo-batch-tester.txt`. Reference mount machinery in `overlay-chroot.sh` (Jeremy Hansen / bassmadrigal). User-facing docs in `README.md`. ## Conventions (the user cares about these) - **No em dash character** in any prose, comments, README, or commit messages. Use commas or periods. This is a hard rule. - This session runs with ponytail (laziest working solution) and caveman (terse replies) modes. Code stays normal; chat is terse. - Build incrementally. The user wants to review the dep resolver and the mount/teardown core before trusting the rest. Do not balloon scope. - Do NOT default to agreement. If a choice is fragile or wrong, say so and propose better with reasoning. ## Hard constraints (do not violate) - **Resolution is LOCAL-tree-only.** Never add network/sbopkg resolution. The local SBo tree is the single source of truth (contains unpublished personal/pentesting packages). Settled, not open for revisiting. - **overlay lowerdir must be LOCAL.** `SLACKWARE_BASE` is a local fs path. `LOCAL_MIRROR_15` is the NFS mountpoint, a package SOURCE only (content read via the derived `MIRROR_TREE`). overlayfs over NFS lowerdir is fragile. The script guards against pointing base under the mirror. The mirror fstab entry is `noauto`; the script checks `mountpoint -q` and auto-mounts (`mount $LOCAL_MIRROR_15`) if absent, left mounted after the run. - **Do not wrap/drive slackrepo.** Separate pipeline, keep independent. - **Shares host kernel.** No kernel-module testing claims. - Runs as root only (overlay + chroot). - Per-target disposable overlay: each category target tests against pristine 15.0. Built packages are throwaway. ## Architecture (single file: sbo-batch-test) Top-to-bottom layout: 1. **CONFIG block** - empty defaults plus a `source` of the external config file. Real values live OUTSIDE the script in `$SBO_BATCH_CONFIG` (default `~/.config/sbo-batch-tester/config`, i.e. `/root/.config/...` since it runs as root); `config.example` in the repo is the template. The block sets empty defaults (`SLACKWARE_BASE LOCAL_MIRROR_15 SBO_TREE_ROOTS`) plus `CHROOT_LOCATION=/tmp LOG_ROOT PKG_CACHE='' VERSION=15.0`, sources the config if present (NOT an error if missing, so the script stays sourceable by `test-logic.sh`), then derives `MIRROR_TREE`. Also sets `SBO_RSYNC_URL` (empty default; rsync source for the SBo tree itself, e.g. `slackbuilds.org::slackbuilds/15.0`, the same source `sbopkg` uses, distinct from the NFS `LOCAL_MIRROR_15` which is a package source only) and `SBO_PERSONAL_TREES` (empty bash associative array; personal-subtree-dir-name -> git clone URL, cloned under `SBO_TREE_ROOTS[0]`). `require_config` (called by `validate_env` and `init_base`) is what hard-fails a real run when the config is absent or has not set the required paths. `LOCAL_MIRROR_15` is the NFS mountpoint; the tree is one level down, `MIRROR_TREE="$LOCAL_MIRROR_15/slackware64-$VERSION"` (holds ChangeLog.txt, slackware64/, patches/). All mirror content reads use `MIRROR_TREE`; the mountpoint check, auto-mount, and the under-mirror guard use `LOCAL_MIRROR_15`. 2. **Globals / flags** - `USE_COLOR DRY_RUN WITH_X JOBS`, status maps (`ST_STATUS ST_REASON ST_TIME ST_README`), `ACTIVE_MOUNTS[]`. 3. **usage / init_color / parse_args / validate_env** - fail-fast startup checks with copy-pasteable hints. 4. **init_base** - `--init-base` mode only. First-time populate: `installpkg --root` the full `slackware64/*/*.t?z` set into `SLACKWARE_BASE`, seed the `last-base-update` marker, exit. Runs its own checks (root, mirror mounted /auto-mount, ChangeLog.txt present, not under mirror) since the base does not exist yet; refuses to clobber a populated base. Wired in `main` before `validate_env`, exits 0. installpkg/upgradepkg run with `--terse`. **update_base** - patches local base from mirror's `patches/packages/` when ChangeLog head differs (reused from reference script). 5. **SBo tree lookup** - `find_slackbuild_dir`, `category_of`, `pkg_key`, `read_requires`. 6. **Dependency resolution** - `resolve_target` -> `_resolve_visit` (DFS topo sort + cycle detection). Outputs `RESOLVED_ORDER[]`, records `UNMET[]`, `CYCLES[]`, `HAS_README[]`. `installed_in_base` checks base package db. 7. **SBo tree lifecycle (`--setup-repo` / `--update`)** - `rsync_excludes` (echoes one anchored `--exclude=//` per `SBO_PERSONAL_TREES` key, the safety invariant that keeps `rsync --delete` from ever touching a personal clone), `rsync_tree` (runs the actual rsync from `SBO_RSYNC_URL` into the tree root with those excludes applied), `collect_shadows` (read-only scan: for each personal-subtree package, finds same-named upstream dirs elsewhere in the tree), `shadow_scan` (prints `collect_shadows` output, confirms interactively, then `rm -r`'s the confirmed set), `setup_repo` (`--setup-repo` entry point: root check, confirm, `rm -rf` the tree root, `rsync_tree`, clone each `SBO_PERSONAL_TREES` subtree, `shadow_scan`, exit; hard-fails on rsync/clone error), `update_personal_trees` (`--update` entry point, runs inline before a build: `rsync_tree` with `--delete` to refresh upstream and undo prior shadows, `git pull` each personal subtree, `shadow_scan` again; fail-soft, warns and continues on any network/git error). 8. **Overlay lifecycle** - `setup_overlay` (echoes tmpdir, mounts overlay + binds), `teardown_overlay` (idempotent, correct reverse order), `cleanup_trap` (EXIT/INT/TERM, unwinds all live overlays). 9. **build_one** - copies SlackBuild into overlay, runs download/md5/build/ installpkg INSIDE the chroot non-interactively via heredoc, reads back a status token file, sets status maps. Logs resolved .info env up front and the installed file list (from /var/log/packages) after installpkg, so the overlay is fully disposable. On a successful TARGET build, runs `lint_pkg` (host-side `sbopkglint` from `sbo-maintainer-tools`) against the built .txz as a final check: target only, fail-soft (skips with a note if the tool is absent, never changes the SUCCESS status). The one-line result is color-coded (`C_GRN` clean / `C_RED` findings); on findings the full sbopkglint output is echoed to the screen (indented) as well as the log, so problems are easy to catch. Called while the overlay is still mounted so the host path to the .txz is reachable; no cache dependency. 10. **run_target** - fresh overlay per target, builds resolved chain in order, marks `BLOCKED-BY-DEP` on dependents of failures, tears down. 11. **print_summary** - color screen recap + plain `summary.log`. 12. **main** - parse; `--setup-repo` runs and exits before `validate_env` (mirrors `--init-base`); `validate_env`; `--update` runs right after (`update_personal_trees`, before `update_base`); make `RUN_DIR`, update base, collect targets (single-package or category-folder), run each. ## Teardown order (do not reorder) pts -> dev/proc/sys -> resolv.conf -> dbus machine-id -> overlay last. Matches the reference script. Idempotent (mountpoint-guarded), trap-registered so a mid-target abort still unwinds. `ACTIVE_MOUNTS[]` tracks live overlays. ## Status values `SUCCESS CACHED DOWNLOAD-FAILED MD5-MISMATCH BUILD-FAILED INSTALL-FAILED BLOCKED-BY-DEP UNMET-DEP`. `CACHED` = a dependency installed from the persistent package cache instead of being rebuilt (target is never CACHED). `%README%` recorded separately as a reminder flag, not a status. ## What is verified vs not - **Verified by self-check** (`test-logic.sh` in repo, `bash test-logic.sh`, no VM needed): topo order, `%README%` recording, unmet-dep, cycle detection, and BLOCKED-BY-DEP propagation (`depends_on_failed`, including the transitive one-hop cascade), the package-cache logic (`cache_decision`/`cache_path`/`cache_store`/`version_of`), and the SBo tree-lifecycle helpers `rsync_excludes` (one anchored `--exclude=//` per `SBO_PERSONAL_TREES` key, empty when no personal trees) and `collect_shadows` (finds upstream dups of personal packages, excludes personal-tree paths themselves, ignores packages with no upstream twin). The check builds a fake SBo tree and sources the script with config overridden AFTER sourcing (sourcing re-runs the CONFIG block, which resets the vars to empty defaults and may source the external config, so test vars must be set after the `source`). The test runs as a normal user, so `~/.config/sbo-batch-tester/config` usually does not exist and is not sourced; even if it did, the post-source overrides win. Gotchas baked in: do not name the dead list `failed` (collides with `depends_on_failed`'s `local -n failed`), and `ok`/`bad` must `return 0` (`((x++))` returns nonzero when x was 0). - **Verified on the build system**: `--init-base` populate ran clean against the real mirror (mountpoint check, auto-mount of the noauto NFS entry, `MIRROR_TREE` derivation, external config sourcing, full installpkg --terse set). The external-config split (`require_config`, missing/incomplete errors) was checked in isolation too. The full build flow ran end to end on a real target (`feh` + its dep `imlib2`): overlay mount, chroot build, forced `OUTPUT=/sbo-work/output`, per-package installpkg, teardown, summary, and persistent logs. The package cache was verified live: a first run populated `$PKG_CACHE///` with one .txz per prog, a re-run installed the cached dep (status `CACHED`, ~1s vs ~27s build) while the target rebuilt fresh, and the cached/build-new/target labels and the cached count rendered correctly. `update_base` reported "Base is up-to-date" (no patch pending), so patching and the on-patch cache wipe ran their up-to-date branch only. - **NOT runnable-tested** (no occasion yet): an actual base patch via `update_base` (mirror was up-to-date) and therefore the on-patch cache wipe firing, and the `bump: OLD -> NEW` eviction path on a real version change (covered by the self-check, not yet seen on hardware). Logic mirrors the reference script. The `sbopkglint` target lint (`lint_pkg`) WAS verified live on buildsystem: a `feh` run ran `sbopkglint` against the built .txz after the target build (lint: clean, full output in the package log), fired for the target only (cached `imlib2` dep not linted), and left SUCCESS unaffected. Pure host I/O against an external tool, no self-check stub (trivial, no branch logic beyond the install check). - **NOT runnable-tested (repo lifecycle)**: `--setup-repo` and `--update` have not been run against the real rsync source or real personal-tree git remotes on hardware yet, so `rsync_tree`, `setup_repo`, `update_personal_trees`, the `git clone`/`git pull` paths, and the interactive `shadow_scan` confirm/`rm -r` step are unverified end to end. Only the pure logic underneath (`rsync_excludes`, `collect_shadows`) is self-checked. The load-bearing safety invariant, that `rsync_excludes` anchors each personal dir so `rsync --delete` in `rsync_tree` can never wipe a personal clone, is covered by the `rsync_excludes` self-check but has not been exercised against a live rsync run with `--delete`. ## Known shortcuts (ponytail: comments in source) - `depends_on_failed`: direct-REQUIRES check only. Transitive blocking still works because the loop runs in topo order, so a failure propagates one hop per package. ## Open items / TODO - **`--keep` removed (resolved).** No keep-overlay option. Overlay is always torn down. Rationale: per-package logs now capture full build/install output, resolved `.info` env, and the installed file list, so the overlay holds nothing worth retaining. Do not re-add `--keep` without a reason logs cannot cover. - **Package cache (DONE).** Persistent `$PKG_CACHE` (config var, empty = disabled), SBo-tree layout `//--...txz`, one .txz per prog. Key = prog+version (build/arch/tag ignored). The named target always builds fresh and refreshes the cache; deps install from cache on a version match (status `CACHED`), else build and cache. Cache wiped when `update_base` patches. Pure `cache_decision`/`cache_path`/`cache_store`/`version_of` covered by `test-logic.sh`. Spec: docs/superpowers/specs/2026-06-24-package-cache-design.md. - **"all" mode**: build every package across all SBo roots. Extension point in `main` where `targets` is populated. TODO marker in source. - **queue/list-file mode**: build a named list. Same extension point. - **`-j` parallelism**: flag parsed, currently no-op. TODO marker in source. ## How to run the self-check ```sh bash test-logic.sh ``` Covers resolution + BLOCKED-BY-DEP. Extend it (not /tmp scratch files) when adding logic. Anything VM-dependent (overlay, chroot, installpkg) is out of its reach by design.