# 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`. `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. **Overlay lifecycle** - `setup_overlay` (echoes tmpdir, mounts overlay + binds), `teardown_overlay` (idempotent, correct reverse order), `cleanup_trap` (EXIT/INT/TERM, unwinds all live overlays). 8. **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. 9. **run_target** - fresh overlay per target, builds resolved chain in order, marks `BLOCKED-BY-DEP` on dependents of failures, tears down. 10. **print_summary** - color screen recap + plain `summary.log`. 11. **main** - parse, validate, 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), and the package-cache logic (`cache_decision`/`cache_path`/`cache_store`/`version_of`). 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). ## 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.