aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-22 12:18:19 +0200
committerDanilo M. <danix@danix.xyz>2026-06-22 12:18:19 +0200
commite39d54ff7dcd17a3ab64c66aba6a2e9f75585485 (patch)
tree3f97a769e8a8dec2df0a0c928956d9b5ba675061
downloadsbo-batch-tester-e39d54ff7dcd17a3ab64c66aba6a2e9f75585485.tar.gz
sbo-batch-tester-e39d54ff7dcd17a3ab64c66aba6a2e9f75585485.zip
Initial commit: sbo-batch-test
Batch-test SlackBuilds against a clean Slackware 15.0 overlay chroot. Non-interactive, local-tree-only dependency resolution with topological sort, per-target disposable overlay, persistent per-package logs, and a color summary. Includes README.md, CLAUDE.md working notes, the reference overlay-chroot.sh, the original spec, and test-logic.sh (resolver + BLOCKED-BY-DEP self-check, 12 checks passing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--CLAUDE.md129
-rw-r--r--README.md161
-rw-r--r--claude-code-prompt-sbo-batch-tester.txt289
-rw-r--r--overlay-chroot.sh257
-rwxr-xr-xsbo-batch-test668
-rwxr-xr-xtest-logic.sh111
6 files changed, 1615 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..8484a50
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,129 @@
+# 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 NFS and is a package SOURCE only. overlayfs over NFS
+ lowerdir is fragile. The script guards against pointing base under the mirror.
+- **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** - `SLACKWARE_BASE`, `LOCAL_MIRROR_15`, `SBO_TREE_ROOTS[]`,
+ `CHROOT_LOCATION`, `LOG_ROOT`, `VERSION`. Currently placeholder paths (we are
+ not in the VM, no real values yet). User fills these in.
+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. **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.
+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 DOWNLOAD-FAILED MD5-MISMATCH BUILD-FAILED INSTALL-FAILED
+BLOCKED-BY-DEP UNMET-DEP`. `%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 check builds a fake SBo tree and sources the script with
+ config overridden AFTER sourcing (sourcing re-runs the CONFIG block, so test
+ vars must be set after the `source`). 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).
+- **NOT runnable-tested** (needs the VM): overlay mounts, chroot build flow,
+ base patching, installpkg. Logic mirrors the reference script.
+
+## 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-output cache** (deferred, user wants it next): keep built `.tgz`
+ outside the overlay, `installpkg` a cached dep instead of rebuilding. Fixes
+ the "shared deps rebuilt per target" cost in category mode. Likely shape: a
+ per-run stash in `$RUN_DIR/pkgcache` keyed `prog-version`, reused across
+ targets within one run. NOT implemented yet.
+- **"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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b98197a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,161 @@
+# sbo-batch-test
+
+Batch-test SlackBuilds against a clean Slackware 15.0 overlay chroot.
+
+Resolves the full SBo dependency tree (locally, never network), topologically
+sorts it, builds and installs every package in a fresh disposable overlay over
+a read-only 15.0 base, captures persistent per-package logs, and prints a
+color-coded summary.
+
+Built for an SBo maintainer whose daily driver is Slackware64-current but whose
+packages target 15.0 stable. It verifies that SlackBuilds BUILD and install
+cleanly against the 15.0 userland/toolchain/libraries, which is where
+current-vs-15.0 drift bites.
+
+## Scope and limits
+
+- Shares the host kernel (the current VM's kernel, not a 15.0 kernel). It does
+ NOT test kernel-module packages or anything tied to the running kernel
+ version. Those still want a real 15.0 VM.
+- Resolution is LOCAL-tree-only. No network, no sbopkg. The local SBo tree is
+ the single source of truth (it contains unpublished personal/pentesting
+ packages that do not exist upstream).
+- Does not integrate with, wrap, or drive slackrepo. Separate, independent tool.
+- Built packages are throwaway. The point is "does it build on clean 15.0",
+ not producing redistributable output.
+
+## Prerequisites
+
+You need three things in place, all configured in the CONFIG block at the top
+of `sbo-batch-test`:
+
+1. **`SLACKWARE_BASE`** - a LOCAL (non-NFS) full Slackware 15.0 install tree.
+ This is the overlay lowerdir. It MUST be local (ext4/xfs). overlayfs over an
+ NFS lowerdir is fragile and a known source of intermittent failures.
+
+2. **`LOCAL_MIRROR_15`** - the NFS-mounted Slackware 15.0 mirror. Used only as a
+ package SOURCE to populate and patch `SLACKWARE_BASE`. Read-only is fine.
+
+3. **`SBO_TREE_ROOTS`** - one or more LOCAL SBo tree roots, resolved in order
+ (first match wins). Standard SBo layout
+ `<root>/<category>/<prog>/{prog.SlackBuild,prog.info,...}`. Read in place,
+ never copied or synced.
+
+Also: run as root (overlay + chroot require it).
+
+## Populating SLACKWARE_BASE
+
+Populate from the NFS mirror with the FULL package set, not a minimal install.
+A minimal base causes false "missing dependency" results that would not happen
+on a normal user's full Slackware install.
+
+```sh
+mkdir -p /sbo-base/15.0
+for p in /mnt/nfs/slackware64-15.0/slackware64/*/*.t?z; do
+ installpkg --root /sbo-base/15.0 "$p"
+done
+```
+
+Adjust the two paths to your `SLACKWARE_BASE` and `LOCAL_MIRROR_15`. The tool
+will not auto-bootstrap the base unattended; if the base is missing or
+incomplete it fails fast with this exact hint.
+
+The base is kept patched automatically on each run: when the mirror's
+ChangeLog head differs from the recorded marker, new packages from
+`patches/packages/` are applied to the local base via `upgradepkg`.
+
+## Usage
+
+```
+sbo-batch-test [OPTIONS] <program-name>
+sbo-batch-test [OPTIONS] <category-folder>
+```
+
+### Single-package mode
+
+Resolve a program's full dep tree, build+install every dep in order, then the
+target. The whole chain shares ONE overlay, torn down at the end.
+
+```sh
+sbo-batch-test playwright-cli
+```
+
+### Category-folder mode
+
+Every SlackBuild dir inside the folder is an independent target. Each target
+gets its OWN fresh overlay against pristine 15.0, so one target's installed
+deps never leak into the next. Deps may live in other categories.
+
+```sh
+sbo-batch-test ./network
+sbo-batch-test /path/to/SBo-danix/pentesting
+```
+
+### Dry run
+
+Resolve and print the build order, build nothing.
+
+```sh
+sbo-batch-test --dry-run playwright-cli
+```
+
+### Options
+
+| Option | Effect |
+|-------------------|--------------------------------------------------------------|
+| `-h`, `--help` | Usage text. |
+| `--no-color` | Disable ANSI color (auto-disabled when stdout is not a TTY). |
+| `--dry-run` | Resolve and print build order, do not build. |
+| `--with-x` | Enable X passthrough (`xhost +local:hosts`). Headless by default. Security caveat: allows local non-network connections to your X server. |
+| `-j`, `--jobs N` | Reserved. No-op stub today (builds are serial). |
+
+## Per-package status values
+
+| Status | Meaning |
+|------------------|-----------------------------------------------------------|
+| `SUCCESS` | Built and installed. |
+| `DOWNLOAD-FAILED`| Source download failed. |
+| `MD5-MISMATCH` | Source MD5 did not match the .info. Hard failure. |
+| `BUILD-FAILED` | The .SlackBuild failed or produced no package. |
+| `INSTALL-FAILED` | `installpkg` of the resulting package failed. |
+| `BLOCKED-BY-DEP` | A dependency failed, so this was not attempted. |
+| `UNMET-DEP` | A required package is neither in the SBo tree nor in base. Also covers dependency cycles. |
+
+Packages whose REQUIRES carry `%README%` are flagged in the summary as a
+reminder to check manual/optional configuration steps. `%README%` is not a
+package and is skipped for build ordering.
+
+## Logs
+
+Written outside the overlay so they survive teardown, under `LOG_ROOT`:
+
+```
+$LOG_ROOT/2026-06-22_14-30-05/
+ <category>_<prog>.log per-package full build/install output
+ summary.log plain-text recap (same facts as the screen summary, no color)
+ build-order.txt the resolved topological order actually used
+```
+
+Each per-package log captures the full build/install output, the resolved
+`.info` build context (PRGNAM/VERSION/BUILD/TAG/REQUIRES), and the installed
+file list from the package db. That is everything worth inspecting, so the
+overlay is always disposed and there is no keep-overlay option.
+
+## Design tradeoffs (on the record)
+
+- **Shared deps are rebuilt per target.** Each category target starts from a
+ clean base, so a dep shared by N targets is rebuilt N times. Correct for
+ isolation, slow for heavy shared deps (qt5, boost). A future optional
+ package-output cache (keep built `.tgz` outside the overlay, `installpkg` a
+ cached dep when its SlackBuild + version are unchanged) is marked TODO in the
+ source, not yet implemented.
+- **overlayfs sharp edges.** A build that fails ONLY in the overlay but works on
+ bare 15.0 is more likely an overlayfs quirk (rename/whiteout) than a real
+ build bug. Noted near the build step in the source.
+
+## Reference
+
+Overlay/chroot mount + teardown machinery follows the patterns in
+`overlay-chroot.sh` by Jeremy Hansen (bassmadrigal). The teardown ordering
+(pts, dev/proc/sys, resolv.conf, dbus machine-id, overlay last) is preserved
+deliberately.
diff --git a/claude-code-prompt-sbo-batch-tester.txt b/claude-code-prompt-sbo-batch-tester.txt
new file mode 100644
index 0000000..7a2d0e5
--- /dev/null
+++ b/claude-code-prompt-sbo-batch-tester.txt
@@ -0,0 +1,289 @@
+================================================================================
+PROMPT FOR CLAUDE CODE
+Build a batch SlackBuild testing tool for Slackware 15.0 stable
+================================================================================
+
+CONTEXT
+-------
+I am an SBo (SlackBuilds.org) package maintainer. My daily-driver machine runs
+Slackware64-current, but SBo targets Slackware 15.0 stable. I need to test my
+SlackBuilds against a clean 15.0 environment WITHOUT booting a VM, because a
+build that works on -current can fail on 15.0 (different toolchain, library
+sonames, Python version, etc.).
+
+There is an existing reference script by Jeremy Hansen (bassmadrigal) called
+overlay-chroot.sh that solves the isolation problem using an overlayfs-backed
+chroot: it keeps a read-only 15.0 base tree as the overlay lowerdir, layers a
+disposable writable upper dir on top, binds the needed system dirs (dev, proc,
+sys, /dev/pts, resolv.conf, dbus machine-id), drops the user into an interactive
+chroot, and on exit unmounts everything and discards the upper layer. I want to
+REUSE that proven mounting/teardown machinery but build a NEW, purpose-built
+tool around it. Treat overlay-chroot.sh as a reference for the mechanics, not as
+a base to lightly patch. The new tool's job is different: non-interactive,
+automated, batch testing with dependency resolution, persistent logs, and a
+color summary.
+
+NOTE ON ISOLATION SCOPE (so expectations are correct): the chroot shares the
+host kernel, which here is the -current VM's kernel, not a 15.0 kernel. This is
+acceptable and expected: the tool verifies that SlackBuilds BUILD and install
+cleanly against the 15.0 userland/toolchain/libraries, which is where -current
+vs 15.0 drift actually bites. It does NOT claim to test kernel-module packages
+or anything depending on the running kernel version; those few cases still want
+a real 15.0 VM and are out of scope for this tool.
+
+I will provide overlay-chroot.sh alongside this prompt (or you can ask me to
+paste it). Study its overlay setup, bind mounts, X-access handling, and its
+robust cleanup/unmount ordering and reuse those patterns faithfully. Its cleanup
+ordering in particular is correct and worth preserving (pts before dev, overlay
+last, etc.).
+
+================================================================================
+GOAL
+================================================================================
+A single root-run bash script that takes either ONE package name or a CATEGORY
+FOLDER, resolves and builds the full SBo dependency tree in correct order inside
+a clean, disposable Slackware 15.0 overlay chroot, installs each resulting
+package, captures persistent per-package logs, and prints a color-coded summary
+at the end recapping exactly what happened.
+
+================================================================================
+INVOCATION / ARGUMENTS
+================================================================================
+The script must accept:
+
+ 1. A single program name, e.g.:
+ sbo-batch-test playwright-cli
+ -> Resolve its full SBo dependency tree, topologically sort it, build and
+ install every dependency first (in order), then build/install the
+ target. Test as a unit; if a dependency fails, the target is reported
+ as blocked/failed and the reason is the failed dep.
+
+ 2. A category folder from my LOCAL SBo repository, e.g.:
+ sbo-batch-test ./network
+ sbo-batch-test /path/to/SBo-danix/pentesting
+ -> Treat every SlackBuild directory inside that folder as a target. For
+ EACH target, resolve+build its dep tree (deps may live in OTHER
+ categories) then build the target. Each target is an independent test
+ unit; one target failing must not abort the whole run.
+
+Design the argument parsing so additional modes can be added later (e.g. "all",
+a queue/list file). Do not implement those now, but leave a clean extension
+point and mention it in --help.
+
+Standard niceties:
+ --help / -h usage text
+ --keep do not destroy the overlay on completion (for inspection)
+ --no-color disable ANSI color in summary (auto-disable if stdout is
+ not a TTY)
+ --jobs / -j N reserved/optional; if non-trivial, stub it and note it
+ A dry-run flag that resolves and PRINTS the build order without building.
+
+================================================================================
+DEPENDENCY RESOLUTION (this is the hard part — get it right)
+================================================================================
+- Parse REQUIRES from each package's .info file.
+- Resolve TRANSITIVELY: a dep's deps must also be built first.
+- TOPOLOGICALLY SORT the resulting graph so every package is built only after
+ all its dependencies. Detect and clearly report dependency CYCLES instead of
+ looping forever.
+- The "%README%" token in REQUIRES is NOT a package. It is a marker meaning the
+ user must read the README for manual configuration/optional deps. Skip it for
+ build ordering but RECORD that the package carries a %README% so it shows in
+ the summary (maintainers should be reminded).
+- A dependency may already be satisfied by the base Slackware install (i.e. it
+ is part of stock 15.0, not an SBo package). Such deps will NOT be found in the
+ SBo tree. Do NOT treat "not in SBo tree" as a hard failure by default; check
+ whether it is already installed in the chroot (look in
+ /var/log/packages). If it is neither in the SBo tree nor installed, THAT is a
+ real unmet-dependency error — report it.
+- SOURCE OF TRUTH for resolution: my LOCAL on-disk SBo tree (my SBo-danix
+ checkout and/or a local SBo 15.0 clone), NOT the network. The script should
+ take the SBo tree root as a configurable variable at the top. If a needed
+ SlackBuild directory cannot be located in the configured tree(s), report it as
+ a resolution failure with the missing package name — do not silently skip.
+ This is settled: resolution is LOCAL-tree-only (the SBO_TREE_ROOTS in the VM).
+ Do NOT add network/sbopkg resolution. My SBo-danix tree contains unpublished
+ personal and pentesting packages that do not exist upstream, so the local tree
+ is the only correct source of truth.
+
+================================================================================
+BUILD + INSTALL FLOW (per package, inside the chroot)
+================================================================================
+For each package in sorted order:
+ 1. Copy its SlackBuild directory into a working location inside the overlay.
+ 2. Source the .info; download the source(s) to the build dir.
+ 3. Verify the MD5SUM from the .info. A mismatch is a hard failure for that
+ package (and blocks anything depending on it).
+ 4. Run the .SlackBuild non-interactively. Capture full stdout+stderr to that
+ package's log file.
+ 5. installpkg the resulting package from /tmp (or $OUTPUT).
+ 6. Record: SUCCESS / BUILD-FAILED / DOWNLOAD-FAILED / MD5-MISMATCH /
+ INSTALL-FAILED / BLOCKED-BY-DEP / UNMET-DEP, plus elapsed build time.
+ 7. If a package fails, mark every not-yet-built package that depends on it as
+ BLOCKED-BY-DEP (do not attempt them) and continue with independent targets.
+
+The chroot must run the build NON-INTERACTIVELY (no dropping into a shell).
+Reuse the reference script's chroot/env/PS1 mounting approach but execute a
+command string instead of an interactive login shell. Preserve and propagate
+exit codes correctly.
+
+OVERLAY LIFECYCLE — THIS IS THE INTENDED DESIGN, NOT A QUESTION:
+The purpose of this tool is purely to verify that SlackBuilds BUILD cleanly
+against stable 15.0. I do NOT keep or redistribute the resulting packages for
+stable, so installed packages are throwaway. Therefore:
+ - Use a FRESH, disposable overlay PER TARGET, destroyed after that target's
+ build chain completes (success or failure).
+ - Single-package mode: one overlay covers the whole dependency-tree-plus-target
+ chain, then is torn down.
+ - Category-folder mode: EACH target package gets its OWN fresh overlay against
+ a pristine 15.0 base. A previous target's built/installed deps must NOT leak
+ into the next target. This guarantees every target is tested against clean
+ stable, which is the whole point.
+ - --keep should suppress teardown ONLY for the final/most-recent overlay (or
+ make its exact behavior clear in --help); by default everything is destroyed.
+
+Accepted tradeoff (document it, do not optimize it away now): shared
+dependencies will be rebuilt for every target that requires them, since each
+target starts from a clean base. This is correct for isolation. Leave a TODO
+marker for a future optional package-output cache (keep built .tgz outside the
+overlay, installpkg a cached dep when its SlackBuild + version are unchanged) so
+this optimization is on record but NOT implemented now.
+
+================================================================================
+ENVIRONMENT / BASE TREE (my actual setup — wire these as config vars)
+================================================================================
+DEPLOYMENT CONTEXT — read this carefully, it constrains the design:
+This tool runs INSIDE my Slackware64-CURRENT VM, the same VM where my SBo-danix
+SlackBuild repo lives and where I use slackrepo to build my redistributable
+-current packages. The SBo tree is therefore a LOCAL path in the VM and is the
+single source of truth. There is NO second copy of the repo and the tool must
+NOT clone, sync, or duplicate it. It reads the local tree in place.
+
+The Slackware 15.0 STABLE material (mirror) is provided over NFS, mounted into
+the VM. This keeps the big generic distro bits out of the VM's own storage.
+
+CRITICAL OVERLAYFS CONSTRAINT — do not get this wrong:
+The overlay LOWERDIR (SLACKWARE_BASE) must be a LOCAL filesystem path in the VM
+(ext4/xfs/etc), NOT the NFS mount. overlayfs over an NFS lowerdir is fragile and
+a known source of intermittent, hard-to-debug failures. Therefore:
+ - NFS provides the 15.0 MIRROR only (a read-only package SOURCE).
+ - SLACKWARE_BASE is a LOCAL directory in the VM, populated FROM that mirror
+ with `installpkg -root`. The overlay lowerdir points at this local tree.
+Make this separation explicit in the config comments so the lowerdir is never
+accidentally pointed at the NFS path.
+
+Expose clear, well-commented config variables at the top:
+ SLACKWARE_BASE -> LOCAL (non-NFS) read-only 15.0 base install tree;
+ this is the overlay lowerdir. MUST be local.
+ LOCAL_MIRROR_15 -> the NFS-mounted 15.0 mirror (package source for
+ populating/patching the base). Read-only is fine.
+ SBO_TREE_ROOTS -> one or more LOCAL SBo tree roots in the VM to
+ resolve from (my SBo-danix checkout, plus optionally
+ a local SBo 15.0 clone for upstream deps). In-place,
+ never copied or synced.
+ CHROOT_LOCATION -> where overlays are created (default /tmp), LOCAL.
+ LOG_ROOT -> where persistent logs are written (see below).
+- Keep the base patched: reuse the reference script's logic that upgrades the
+ base from the mirror's patches/ when the ChangeLog head differs. Point it at
+ LOCAL_MIRROR_15 (the NFS mirror). The base tree it WRITES to is the local
+ SLACKWARE_BASE.
+- STARTUP VALIDATION (fail fast, clear messages):
+ * Verify SLACKWARE_BASE is a LOCAL path and looks like a real Slackware
+ install (/var/log/packages present, key dirs exist). If it does not exist
+ or is incomplete, FAIL with a precise, copy-pasteable hint for populating
+ it from LOCAL_MIRROR_15 via `installpkg -root` (full package set, not
+ minimal — a minimal base yields false "missing dependency" results that
+ would not occur on a normal user's full Slackware install).
+ * Verify LOCAL_MIRROR_15 is actually mounted/reachable (it is NFS); if the
+ mount is absent, fail with a clear message rather than proceeding against
+ an empty path.
+ * Verify each SBO_TREE_ROOTS path exists locally.
+ Do NOT attempt to auto-bootstrap the base unattended.
+
+DO NOT integrate with, wrap, or drive slackrepo. slackrepo is my separate
+release pipeline (clean reproducible builds, upstream revision tracking,
+slackpkg+ output for redistribution). THIS tool is only a disposable pre-flight
+"does it build on clean 15.0 stable" check. Keep them fully independent.
+
+================================================================================
+LOGGING (non-volatile — must survive overlay teardown)
+================================================================================
+- Logs MUST be written OUTSIDE the overlay so they persist after cleanup.
+- Per RUN: create a timestamped directory under LOG_ROOT, e.g.
+ $LOG_ROOT/2026-06-22_14-30-05/
+- Inside it:
+ <category>_<prog>.log per-package full build/install log
+ summary.log plain-text recap of the whole run
+ build-order.txt the resolved topological order actually used
+- The plain-text summary.log must be greppable and contain the same facts as
+ the on-screen color summary (no color codes in the file).
+
+================================================================================
+END-OF-RUN COLOR SUMMARY (on screen)
+================================================================================
+Print a clear, color-coded recap to the terminal:
+ - GREEN = success
+ - RED = failed (with the failure category)
+ - YELLOW = blocked-by-dep, unmet-dep, or skipped
+ - Note packages carrying a %README% (so I remember to check manual steps).
+Include, per package: status, failure reason if any, and build time.
+End with totals: X succeeded, Y failed, Z blocked, total wall time, and the
+path to the run's log directory.
+Auto-disable color when stdout is not a TTY or when --no-color is passed.
+
+================================================================================
+SAFETY, CORRECTNESS, ROBUSTNESS
+================================================================================
+- Must run as root (overlay + chroot). Check EUID and exit cleanly if not.
+- Use `set -euo pipefail` thoughtfully, but DO NOT let one package's build
+ failure kill the whole batch — isolate per-package execution so the run
+ continues. A failed build is an expected, handled outcome, not a script crash.
+- Cleanup must be bulletproof and idempotent: trap EXIT/INT/TERM and unmount in
+ the correct reverse order (pts, dev/proc/sys, resolv.conf, dbus machine-id,
+ overlay last), exactly as the reference script does, even if the run aborts
+ mid-way. Never leave dangling mounts or overlay dirs (unless --keep). Because
+ overlays are created and destroyed PER TARGET, the teardown routine must be a
+ reusable function called after each target, AND registered in the trap so an
+ abort mid-target still unmounts cleanly. Track all active overlay mounts so the
+ trap can unwind whichever one is live when an interrupt hits.
+- Be aware overlayfs has occasional sharp edges with some builds; if a build
+ fails ONLY in the overlay, that is worth a comment in the code near the build
+ step, but do not try to work around it speculatively.
+- X server access: default to NO X (these are headless batch builds). Only wire
+ optional X passthrough behind a flag, and comment the xhost security caveat.
+- Quote variables, handle paths with spaces, use mktemp for temp dirs.
+- Comment generously. Put all user-tunable config in one clearly marked block at
+ the top.
+
+================================================================================
+DELIVERABLES
+================================================================================
+1. The new script (suggest name: sbo-batch-test). Single self-contained bash
+ file, heavily commented.
+2. A short README section (can be a comment block or separate .md) covering:
+ prerequisites (the 15.0 base tree, the mirror, the SBo tree roots), how to
+ populate SLACKWARE_BASE, and usage examples for both the single-package and
+ category-folder modes, plus the dry-run.
+3. Inline TODO markers for the deferred features (e.g. "all" mode, queue-file
+ mode, -j parallelism) at the extension points.
+
+================================================================================
+WORKING STYLE I WANT FROM YOU
+================================================================================
+- ASK ME QUESTIONS before coding if anything is ambiguous — especially the
+ dependency-resolution source-of-truth assumption above, the exact layout of my
+ SBo tree, and whether my SlackBuilds follow stock SBo conventions.
+- Do NOT default to agreement. If any choice in this prompt is wrong, suboptimal,
+ or fragile (e.g. overlay quirks, dep-resolution edge cases, the
+ build-then-install-then-leave-installed question), tell me and propose a
+ better alternative with reasoning.
+- Build incrementally: I'd rather review the dependency resolver and the
+ mount/teardown core first, confirm they're solid, then layer on logging and
+ the summary.
+
+A note on my conventions: when writing any prose, comments, README text, or
+commit messages, do NOT use the em dash character. Use commas or periods.
+
+================================================================================
+END OF PROMPT
+================================================================================
diff --git a/overlay-chroot.sh b/overlay-chroot.sh
new file mode 100644
index 0000000..332311b
--- /dev/null
+++ b/overlay-chroot.sh
@@ -0,0 +1,257 @@
+#!/bin/bash
+#
+# Copyright 2023-2025 Jeremy Hansen <jebrhansen -at- gmail.com>
+# All rights reserved.
+#
+# Redistribution and use of this script, with or without modification, is
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of this script must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# -----------------------------------------------------------------------------
+
+# Create a chroot from a maintained and updated Slackware stable base to allow
+# easy testing of SlackBuild scripts in a clean environment. Ensure that base
+# is updated every time this script is ran along with updating sbopkg and
+# running sqg to update all queues. Offer to remove chroot files when exiting
+# the chroot.
+
+# Supports passing "cleanup" to remove any existing chroots and their files.
+
+# Supports passing "update" to update the base image with slackpkg and sbopkg
+# and then exiting without starting the chroot.
+
+# TODO
+# Currently empty
+
+# -----------------------------------------------------------------------------
+
+# --------------------------Global Settings Beginning--------------------------
+
+# Set where you want the chroot located and the base name of the folder
+CHROOT_LOCATION=/tmp/
+CHROOT_TEMPLATE_BASE="chroot"
+
+# Set variables for the base image and location of the local Slackware mirror
+VERSION=15.0
+SLACKWARE_BASE=/share/gothrough/sbo-build/$VERSION
+LOCAL_MIRROR=/share/gothrough/slackware-mirrors/slackware64-$VERSION/
+
+# To allow you to open GUI programs from within the chroot, you need to
+# allow "remote" access to the x server. This could possibly open up
+# security issues, but it is limited to non-network local connections.
+# Change to "no" if you want this disabled or pass ACCESS=no to the script.
+ACCESS=${ACCESS:-yes}
+
+# ---------------------------Global Settings Ending----------------------------
+
+# --------------------------Custom Commands Beginning--------------------------
+# This will allow you to add custom aliases or functions into the chroot
+# Make sure you escape single quotes and variables and that all commands are on
+# their own lines
+custom_cmd='
+# Check all listed deps in a .info to see if they are installed
+alias checkdeps=". *.info; for i in \$REQUIRES; do ls /var/log/packages/*SBo* | cut -d/ -f5 | rev | cut -d- -f4- | rev | grep ^\$i$; done"
+'
+# ---------------------------Custom Commands Ending----------------------------
+
+# Check that we're root
+if [ "$EUID" -ne 0 ]; then
+ echo "Please run as root"
+ exit
+fi
+
+# Provide an easy cleanup for older tmp files and exit
+if [ "$1" == "cleanup" ]; then
+
+ for i in "$CHROOT_LOCATION"/"$CHROOT_TEMPLATE_BASE".*; do
+ if [ -d "$i" ]; then
+ echo "Found $i"
+ else
+ echo "No chroots to clean up."
+ exit 2
+ fi
+
+ if mountpoint -q "$i"/chroot/dev/pts; then
+ printf "\tUnmounting %s/chroot/dev/pts\n" "$i"
+ umount "$i"/chroot/dev/pts
+ fi
+ for j in dev proc sys; do
+ if mountpoint -q "$i"/chroot/$j; then
+ printf "\tUnmounting %s/chroot/%s\n" "$i" "$j"
+ umount "$i"/chroot/$j
+ fi
+ done
+
+ if mountpoint -q "$i"/chroot/etc/resolv.conf; then
+ printf "\tUnmounting %s/chroot/etc/resolv.conf\n" "$i"
+ umount "$i"/chroot/etc/resolv.conf
+ fi
+
+ if mountpoint -q "$i"/chroot/var/lib/dbus/machine-id; then
+ printf "\tUnmounting %s/chroot/var/lib/dbus/machine-id\n" "$i"
+ umount "$i"/chroot/var/lib/dbus/machine-id
+ fi
+
+ # umount overlayfs
+ if mountpoint -q "$i"/chroot; then
+ printf "\tUnmounting %s/chroot/%s/chroot\n" "$i" "$j"
+ umount "$i"/chroot
+ fi
+
+ # Remove dirs
+ if [ -d "$i" ]; then
+ printf "\tRemoving %s.\n" "$i"
+ rm -r "$i"
+ fi
+ done
+ echo "Cleanup complete"
+ exit
+fi
+
+# Track the latest updates to prevent attempting to update system
+# packages and rebuilding sbopkg's queues
+touch $SLACKWARE_BASE/last-base-update
+
+# Make sure the base image is up-to-date
+if [ "$(head -n1 $LOCAL_MIRROR/ChangeLog.txt)" != "$(cat $SLACKWARE_BASE/last-base-update)" ]; then
+ for i in "$LOCAL_MIRROR"/patches/packages/*.t?z; do
+ if [ ! -e "$SLACKWARE_BASE"/var/lib/pkgtools/packages/"$(basename "${i%.*}")" ]; then
+ ROOT=$SLACKWARE_BASE upgradepkg --install-new "$i"
+ fi
+ done
+ echo "Slackware has been updated with local mirror."
+ head -n1 $LOCAL_MIRROR/ChangeLog.txt > $SLACKWARE_BASE/last-base-update
+else
+ echo "Slackware is up-to-date with the local mirror."
+fi
+
+# Set up directories for the chroot
+echo "Creating required directories for the overlay"
+TMPDIR=$(mktemp -d "$CHROOT_LOCATION"/"$CHROOT_TEMPLATE_BASE".XXXXX)
+mkdir "$TMPDIR"/{changes,tmp,chroot}
+
+# Mount the overlayfs
+echo "Mounting the overlay"
+mount -t overlay overlay -olowerdir="$SLACKWARE_BASE",upperdir="$TMPDIR"/changes,workdir="$TMPDIR"/tmp "$TMPDIR"/chroot
+
+# Bind mount the pertinent system dirs
+echo "Binding required directories"
+mkdir -p "$TMPDIR"/changes/{dev,proc,sys}
+for i in dev proc sys; do
+ mount -o bind /$i "$TMPDIR"/chroot/$i
+done
+
+# Mount /dev/pts for sudo
+mkdir -p "$TMPDIR"/changes/dev/pts
+mount -o bind /dev/pts "$TMPDIR"/chroot/dev/pts
+
+# Give the chroot internet
+echo "Setting up internet"
+mount -o bind /etc/resolv.conf "$TMPDIR"/chroot/etc/resolv.conf
+chroot "$TMPDIR"/chroot /bin/bash -c "/usr/sbin/update-ca-certificates --fresh > /dev/null"
+
+# Setting up DBUS binding required for certain apps
+echo "Binding DBUS to local machine"
+touch "$TMPDIR"/chroot/var/lib/dbus/machine-id
+mount -o bind /var/lib/dbus/machine-id "$TMPDIR"/chroot/var/lib/dbus/machine-id
+
+# Update sbopkg (if installed) and queues
+# Do it in the chroot to prevent GPG errors, but copy files back to the
+# base image so we only need to do it during updates.
+if [ -e "$SLACKWARE_BASE"/usr/sbin/sbopkg ]; then
+ echo "Checking for SBo updates for sbopkg"
+ # Get the latest changelog date from server
+ SERVDATE="$(wget -qO- https://slackbuilds.org/slackbuilds/15.0/ChangeLog.txt | head -n1)"
+ if [ -z "$SERVDATE" ]; then
+ echo "Upstream address did not provide a changelog."
+ echo "Please validate internet is working and address is correct"
+ echo "This will continue in 5 seconds. Ctrl+C if you'd like to exit."
+ sleep 5
+ fi
+ # Get latest changelog date on local copy
+ LOCALDATE="$(head -n1 $SLACKWARE_BASE/var/lib/sbopkg/SBo/15.0/ChangeLog.txt)"
+ # If they don't match, update sbopkg and run sqg. Copy updates back to base image.
+ if [ "$SERVDATE" != "$LOCALDATE" ]; then
+ chroot "$TMPDIR"/chroot /bin/bash -c "/usr/sbin/sbopkg -r; /usr/sbin/sqg -a"
+ rsync -a --delete "$TMPDIR"/chroot/var/lib/sbopkg/ "$SLACKWARE_BASE"/var/lib/sbopkg
+ rsync -a --delete "$TMPDIR"/chroot/root/.gnupg "$SLACKWARE_BASE"/root/
+ else
+ echo "sbopkg is up-to-date."
+ fi
+fi
+
+# Only set up X server access and launch the chroot if update isn't passed
+if [ "$1" != "update" ]; then
+ # Checking if we can add local connection access
+ if [ "$ACCESS" == "yes" ]; then
+ echo "Setting up X server access"
+ echo "STATUS: $(xhost +local:hosts)"
+ fi
+
+ # Check if there are custom commands to add by checking for text
+ if [[ "$custom_cmd" =~ [a-zA-Z] ]]; then
+ echo "Adding custom commands to chroot's /etc/profile.d/chroot_custom_cmds.sh"
+ echo "$custom_cmd" > "$TMPDIR"/chroot/etc/profile.d/chroot_custom_cmds.sh
+ chmod +x "$TMPDIR"/chroot/etc/profile.d/chroot_custom_cmds.sh
+ fi
+
+ # Let's save the following in the root of the chroot structure to allow
+ # the user to enter into that chroot from another prompt and/or if they
+ # accidentally leave the chroot.
+ # Then we'll just execute the file to actually enter the chroot.
+ cat << EOH > "$TMPDIR"/start-chroot.sh
+#!/bin/bash
+# Time to actually chroot and do our work
+# Need to type 'exit' to leave the chroot and start the cleanup
+# Use custom PS1 so we know we're in the chroot
+echo "Entering chroot. Please type \"exit\" to exit it."
+echo "You can add files to the chroot by placing them in $TMPDIR/chroot/"
+chroot "$TMPDIR"/chroot env PS1="\[\e[41m\]\u\[\e[49m\]@\[\e[33m\]$(basename "$TMPDIR")\[\e[0m\]:\w$ " bash -l
+EOH
+
+ # Start the chroot
+ bash "$TMPDIR"/start-chroot.sh
+fi
+
+# Start cleanup
+
+# Undo bind mounts
+umount "$TMPDIR"/chroot/dev/pts
+for i in dev proc sys; do
+ umount "$TMPDIR"/chroot/$i
+done
+
+umount "$TMPDIR"/chroot/etc/resolv.conf
+umount "$TMPDIR"/chroot/var/lib/dbus/machine-id
+
+# umount overlayfs
+umount "$TMPDIR"/chroot
+
+# Only ask to delete chroot if update isn't passed, otherwise delete without asking
+if [ "$1" != "update" ]; then
+ # Ask if tmp dirs should be removed
+ # Could be kept to review filesystem changes
+ echo -n "Would you like to remove the unneeded overlay directories? y/N "
+ read -r answer
+ # If anything other than y, rm them
+ if ! /usr/bin/grep -qi "y" <<< "$answer"; then
+ echo "Temp overlay dirs will not be removed. They can be found at $TMPDIR."
+ else
+ rm -r "$TMPDIR"
+ fi
+else
+ rm -r "$TMPDIR"
+fi
diff --git a/sbo-batch-test b/sbo-batch-test
new file mode 100755
index 0000000..d6ca1ef
--- /dev/null
+++ b/sbo-batch-test
@@ -0,0 +1,668 @@
+#!/bin/bash
+#
+# sbo-batch-test - batch-test SlackBuilds against a clean Slackware 15.0
+# overlay chroot. Non-interactive, dependency-resolving, per-target disposable
+# overlay, persistent logs, color summary.
+#
+# Overlay/chroot mount + teardown machinery follows the patterns in
+# overlay-chroot.sh by Jeremy Hansen (bassmadrigal). The teardown ordering
+# (pts, dev/proc/sys, resolv.conf, dbus machine-id, overlay last) is preserved
+# deliberately, it is the correct unwind order.
+#
+# Runs INSIDE a Slackware64-current VM. Verifies SlackBuilds BUILD and install
+# cleanly against 15.0 userland. Does NOT test kernel modules (shares host
+# kernel). Resolution is LOCAL-tree-only, never network. Does not touch
+# slackrepo.
+#
+# No em dashes in prose by author convention.
+
+# =============================================================================
+# CONFIG (edit these for your VM)
+# =============================================================================
+
+# LOCAL (non-NFS) read-only 15.0 base install tree. This is the overlay
+# lowerdir. MUST be a local filesystem (ext4/xfs). NEVER point this at the NFS
+# mirror: overlayfs over NFS lowerdir is fragile and fails intermittently.
+SLACKWARE_BASE="/sbo-base/15.0"
+
+# NFS-mounted 15.0 mirror. Package SOURCE only, used to populate/patch
+# SLACKWARE_BASE. Read-only is fine. Never used as the overlay lowerdir.
+LOCAL_MIRROR_15="/mnt/nfs/slackware64-15.0"
+
+# One or more LOCAL SBo tree roots, resolved in order (first match wins).
+# Standard SBo layout: <root>/<category>/<prog>/{prog.SlackBuild,prog.info,...}.
+# Read in place, never copied or synced.
+SBO_TREE_ROOTS=(
+ "/home/danix/SBo-danix"
+ "/home/danix/slackbuilds-15.0"
+)
+
+# Where overlays are created. LOCAL. One disposable overlay per target lives here.
+CHROOT_LOCATION="/tmp"
+
+# Where persistent logs are written (outside the overlay, survives teardown).
+LOG_ROOT="/var/log/sbo-batch-test"
+
+# Slackware version, used for the mirror ChangeLog / patches path.
+VERSION="15.0"
+
+# =============================================================================
+# END CONFIG
+# =============================================================================
+
+set -uo pipefail
+# Note: NOT using -e globally. One package build failing is an expected,
+# handled outcome, not a script crash. Per-package execution is isolated.
+
+# ---- flags / globals --------------------------------------------------------
+USE_COLOR=1 # --no-color or non-TTY disables
+DRY_RUN=0 # resolve + print build order, do not build
+WITH_X=0 # --with-x: optional X passthrough (default headless)
+JOBS=1 # -j: reserved, see TODO
+TARGET_ARG=""
+
+RUN_DIR="" # timestamped log dir for this run
+declare -a ACTIVE_MOUNTS=() # overlay chroot roots currently mounted (for trap unwind)
+CURRENT_OVERLAY="" # the $TMPDIR of the overlay being built in now
+
+# Status tracking. Keyed by "category/prog". Parallel assoc arrays.
+declare -A ST_STATUS=() # SUCCESS|BUILD-FAILED|DOWNLOAD-FAILED|MD5-MISMATCH|INSTALL-FAILED|BLOCKED-BY-DEP|UNMET-DEP
+declare -A ST_REASON=()
+declare -A ST_TIME=() # elapsed seconds
+declare -A ST_README=() # 1 if package carries %README%
+
+# =============================================================================
+# usage
+# =============================================================================
+usage() {
+ cat <<'EOF'
+sbo-batch-test - batch-test SlackBuilds against a clean Slackware 15.0 overlay chroot
+
+USAGE:
+ sbo-batch-test [OPTIONS] <program-name>
+ sbo-batch-test [OPTIONS] <category-folder>
+
+MODES:
+ <program-name> Resolve its full SBo dependency tree, build+install every
+ dep in topological order, then build+install the target.
+ <category-folder> Treat every SlackBuild dir inside as an independent target.
+ Each target gets its OWN fresh overlay against pristine 15.0.
+
+OPTIONS:
+ -h, --help This text.
+ --no-color Disable ANSI color (auto-disabled when stdout is not a TTY).
+ --dry-run Resolve and print the build order, do not build.
+ --with-x Enable X passthrough via xhost +local:hosts (security caveat:
+ allows local non-network connections to your X server).
+ Default is headless (no X).
+ -j, --jobs N Reserved. Currently a no-op stub (builds are serial).
+
+EXTENSION POINTS (not implemented, see TODOs in source):
+ - "all" mode (build every package across all SBo roots).
+ - queue/list-file mode (build a named list of targets).
+ - -j parallelism.
+
+EOF
+}
+
+# =============================================================================
+# color helpers
+# =============================================================================
+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
+}
+
+# =============================================================================
+# arg parsing
+# =============================================================================
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h|--help) usage; exit 0 ;;
+ --no-color) USE_COLOR=0; shift ;;
+ --dry-run) DRY_RUN=1; shift ;;
+ --with-x) WITH_X=1; shift ;;
+ -j|--jobs) JOBS="${2:-1}"; shift 2 ;; # TODO: implement parallelism
+ -*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
+ *)
+ if [[ -n "$TARGET_ARG" ]]; then
+ echo "Only one target argument 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
+}
+
+# =============================================================================
+# startup validation (fail fast, copy-pasteable hints)
+# =============================================================================
+validate_env() {
+ if [[ $EUID -ne 0 ]]; then
+ echo "This tool must run as root (overlay + chroot)." >&2
+ exit 1
+ fi
+
+ # SLACKWARE_BASE: local, looks like a real Slackware install.
+ if [[ ! -d "$SLACKWARE_BASE" || ! -d "$SLACKWARE_BASE/var/log/packages" ]]; then
+ cat >&2 <<EOF
+SLACKWARE_BASE is missing or incomplete: $SLACKWARE_BASE
+It must be a LOCAL (non-NFS) full Slackware 15.0 install tree.
+Populate it from the mirror with the FULL package set (not minimal,
+a minimal base causes false "missing dependency" results):
+
+ mkdir -p "$SLACKWARE_BASE"
+ for p in "$LOCAL_MIRROR_15"/slackware64/*/*.t?z; do
+ installpkg --root "$SLACKWARE_BASE" "\$p"
+ done
+EOF
+ exit 1
+ fi
+ # Guard against the classic mistake: base sitting on the NFS mirror path.
+ case "$SLACKWARE_BASE" in
+ "$LOCAL_MIRROR_15"*)
+ echo "SLACKWARE_BASE must NOT live under LOCAL_MIRROR_15 (NFS). overlayfs over NFS lowerdir is unsupported here." >&2
+ exit 1 ;;
+ esac
+
+ # LOCAL_MIRROR_15: NFS, must be mounted/reachable.
+ if [[ ! -d "$LOCAL_MIRROR_15" || ! -f "$LOCAL_MIRROR_15/ChangeLog.txt" ]]; then
+ echo "LOCAL_MIRROR_15 not reachable (NFS mount absent?): $LOCAL_MIRROR_15" >&2
+ echo "Expected a Slackware 15.0 mirror with ChangeLog.txt at its root." >&2
+ exit 1
+ fi
+
+ # SBo roots exist locally.
+ local r found=0
+ for r in "${SBO_TREE_ROOTS[@]}"; do
+ if [[ -d "$r" ]]; then found=1; else
+ echo "SBO_TREE_ROOTS path does not exist: $r" >&2
+ fi
+ done
+ if [[ $found -eq 0 ]]; then
+ echo "No valid SBo tree root found. Fix SBO_TREE_ROOTS." >&2
+ exit 1
+ fi
+}
+
+# =============================================================================
+# keep base patched from the mirror (reuse reference logic, point at NFS mirror,
+# write to local base). Skipped on --dry-run.
+# =============================================================================
+update_base() {
+ local marker="$SLACKWARE_BASE/last-base-update"
+ touch "$marker"
+ local head_now; head_now="$(head -n1 "$LOCAL_MIRROR_15/ChangeLog.txt")"
+ if [[ "$head_now" == "$(cat "$marker")" ]]; then
+ echo "Base is up-to-date with the mirror."
+ return
+ fi
+ echo "Patching base from mirror..."
+ local p
+ for p in "$LOCAL_MIRROR_15"/patches/packages/*.t?z; do
+ [[ -e "$p" ]] || continue
+ if [[ ! -e "$SLACKWARE_BASE/var/lib/pkgtools/packages/$(basename "${p%.*}")" ]]; then
+ ROOT="$SLACKWARE_BASE" upgradepkg --install-new "$p"
+ fi
+ done
+ echo "$head_now" > "$marker"
+ echo "Base patched."
+}
+
+# =============================================================================
+# SBo tree lookup. Find the SlackBuild dir for a prog name across roots.
+# Echoes the dir path, returns 0 if found, 1 otherwise.
+# =============================================================================
+find_slackbuild_dir() {
+ local prog="$1" root d
+ for root in "${SBO_TREE_ROOTS[@]}"; do
+ [[ -d "$root" ]] || continue
+ # <root>/<category>/<prog>
+ 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 a SlackBuild dir = its parent dir name.
+category_of() { basename "$(dirname "$1")"; }
+# Key used in status maps and log filenames.
+pkg_key() { echo "$(category_of "$1")/$(basename "$1")"; }
+
+# Read REQUIRES from a .info, stripped. Echoes space-separated tokens.
+read_requires() {
+ local info="$1"
+ # shellcheck disable=SC1090
+ ( set +u; source "$info"; echo "${REQUIRES:-}" )
+}
+
+# =============================================================================
+# DEPENDENCY RESOLUTION
+# Builds a topological order over the local SBo tree.
+# - transitive REQUIRES
+# - %README% recorded, not built
+# - deps already in base (installed in chroot base) are satisfied, not built
+# - deps neither in SBo tree nor installed => UNMET-DEP
+# - cycles detected and reported, no infinite loop
+#
+# Outputs the order into the global array RESOLVED_ORDER (dir paths).
+# Records unmet deps into UNMET (prog -> requiring pkg) and cycle errors.
+# Returns 1 if any hard resolution failure (unmet dep, cycle) for the target set.
+# =============================================================================
+declare -a RESOLVED_ORDER=()
+declare -A UNMET=() # prog -> "needed by X"
+declare -a CYCLES=() # human-readable cycle descriptions
+declare -A HAS_README=() # dir -> 1 if its REQUIRES carried %README%
+
+# Is a prog already installed in the base tree? (stock 15.0 or otherwise present)
+installed_in_base() {
+ local prog="$1"
+ # Package db entries look like prog-version-arch-build[ tag]
+ ls "$SLACKWARE_BASE"/var/log/packages/ 2>/dev/null \
+ | grep -qE "^${prog}-[^-]+-[^-]+-[^-]+"
+}
+
+# DFS topo sort with cycle detection.
+# visit_state: dir -> 0 visiting (on stack), 1 done
+declare -A _vstate=()
+_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
+ : # satisfied by base, nothing to build
+ else
+ UNMET["$tok"]="needed by $key"
+ rc=1
+ fi
+ done
+
+ _vstate["$dir"]=1
+ RESOLVED_ORDER+=("$dir")
+ return $rc
+}
+
+# Resolve a single target dir into RESOLVED_ORDER (deps first, target last).
+resolve_target() {
+ local dir="$1"
+ RESOLVED_ORDER=()
+ CYCLES=()
+ UNMET=()
+ _vstate=()
+ _resolve_visit "$dir" "(top)"
+}
+
+# =============================================================================
+# OVERLAY LIFECYCLE (per target). Patterns from overlay-chroot.sh.
+# setup_overlay -> echoes the chroot root path, sets CURRENT_OVERLAY.
+# teardown_overlay <tmpdir> -> idempotent unwind in correct reverse order.
+# =============================================================================
+setup_overlay() {
+ local tmpdir; tmpdir="$(mktemp -d "$CHROOT_LOCATION"/sbo-bt.XXXXXX)"
+ mkdir "$tmpdir"/{changes,tmp,chroot}
+
+ # overlayfs: read-only 15.0 base as lowerdir, disposable upper on top.
+ mount -t overlay overlay \
+ -olowerdir="$SLACKWARE_BASE",upperdir="$tmpdir/changes",workdir="$tmpdir/tmp" \
+ "$tmpdir/chroot"
+
+ # bind system dirs
+ mkdir -p "$tmpdir"/changes/{dev,proc,sys}
+ local i
+ for i in dev proc sys; do
+ mount -o bind "/$i" "$tmpdir/chroot/$i"
+ done
+ # /dev/pts (sudo/pty)
+ mkdir -p "$tmpdir"/changes/dev/pts
+ mount -o bind /dev/pts "$tmpdir/chroot/dev/pts"
+ # internet
+ mount -o bind /etc/resolv.conf "$tmpdir/chroot/etc/resolv.conf"
+ chroot "$tmpdir/chroot" /bin/bash -c "/usr/sbin/update-ca-certificates --fresh >/dev/null 2>&1" || true
+ # dbus machine-id
+ touch "$tmpdir/chroot/var/lib/dbus/machine-id"
+ mount -o bind /var/lib/dbus/machine-id "$tmpdir/chroot/var/lib/dbus/machine-id"
+
+ CURRENT_OVERLAY="$tmpdir"
+ ACTIVE_MOUNTS+=("$tmpdir")
+ echo "$tmpdir"
+}
+
+# Idempotent teardown. Safe to call twice, safe mid-abort.
+teardown_overlay() {
+ local tmpdir="$1"
+ [[ -n "$tmpdir" && -d "$tmpdir" ]] || return 0
+ local c="$tmpdir/chroot"
+
+ mountpoint -q "$c/dev/pts" && umount "$c/dev/pts"
+ local i
+ for i in dev proc sys; do
+ mountpoint -q "$c/$i" && umount "$c/$i"
+ done
+ mountpoint -q "$c/etc/resolv.conf" && umount "$c/etc/resolv.conf"
+ mountpoint -q "$c/var/lib/dbus/machine-id" && umount "$c/var/lib/dbus/machine-id"
+ mountpoint -q "$c" && umount "$c"
+
+ # Remove from ACTIVE_MOUNTS
+ local n=() m
+ for m in "${ACTIVE_MOUNTS[@]}"; do [[ "$m" != "$tmpdir" ]] && n+=("$m"); done
+ ACTIVE_MOUNTS=("${n[@]:-}")
+
+ rm -rf "$tmpdir"
+}
+
+# Trap: unwind every live overlay on abort. Reverse order of creation.
+cleanup_trap() {
+ [[ $WITH_X -eq 1 ]] && xhost -local:hosts >/dev/null 2>&1 || true
+ local i
+ for (( i=${#ACTIVE_MOUNTS[@]}-1; i>=0; i-- )); do
+ teardown_overlay "${ACTIVE_MOUNTS[$i]}"
+ done
+}
+trap cleanup_trap EXIT INT TERM
+
+# =============================================================================
+# BUILD + INSTALL one package inside an existing overlay chroot.
+# Args: <chroot-tmpdir> <slackbuild-dir>
+# Sets ST_STATUS / ST_REASON / ST_TIME for the package key.
+# Returns 0 on SUCCESS, 1 otherwise.
+# =============================================================================
+build_one() {
+ local tmpdir="$1" dir="$2"
+ local c="$tmpdir/chroot"
+ 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)
+
+ # 1. copy SlackBuild dir into the overlay
+ local workroot="/sbo-work"
+ mkdir -p "$c$workroot"
+ cp -a "$dir" "$c$workroot/$prog"
+
+ # %README% reminder carries through to summary
+ [[ "${HAS_README[$dir]:-}" == "1" ]] && ST_README["$key"]=1
+
+ # 2-5. download, md5, build, install all run INSIDE the chroot non-interactively.
+ # The heredoc script writes a status token to a known file we read back out.
+ # overlayfs note: if a build fails ONLY here and works on bare 15.0, suspect
+ # an overlayfs sharp edge (rename/whiteout quirks) rather than a real build bug.
+ local statf="$c$workroot/$prog.status"
+ chroot "$c" /bin/bash -s <<CHROOT_EOF >>"$logf" 2>&1
+set -uo pipefail
+cd "$workroot/$prog" || { echo "BUILD-FAILED: cannot cd"; echo BUILD-FAILED > "$workroot/$prog.status"; exit 1; }
+. ./$prog.info
+
+# Log resolved build context so the overlay never needs to be kept.
+echo "===== sbo-batch-test: $prog ====="
+echo "PRGNAM=\${PRGNAM:-$prog} VERSION=\${VERSION:-?} BUILD=\${BUILD:-?} TAG=\${TAG:-?}"
+echo "uname -m: \$(uname -m) OUTPUT=\${OUTPUT:-/tmp}"
+echo "REQUIRES=\${REQUIRES:-}"
+echo "================================="
+
+# pick arch-specific download/md5 if present (x86_64 VM)
+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
+
+# 2. download
+for u in \$DL; do
+ wget -c --tries=3 "\$u" || { echo DOWNLOAD-FAILED > "$workroot/$prog.status"; exit 1; }
+done
+
+# 3. verify md5
+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 > "$workroot/$prog.status"; exit 1
+ fi
+done
+
+# 4. build non-interactively
+chmod +x ./$prog.SlackBuild
+if ! ./$prog.SlackBuild; then
+ echo BUILD-FAILED > "$workroot/$prog.status"; exit 1
+fi
+
+# 5. installpkg the resulting package (SlackBuilds write to \${OUTPUT:-/tmp})
+out="\${OUTPUT:-/tmp}"
+pkg="\$(ls -t "\$out"/${prog}-*.t?z 2>/dev/null | head -n1)"
+if [ -z "\$pkg" ]; then
+ echo "No package produced in \$out"
+ echo BUILD-FAILED > "$workroot/$prog.status"; exit 1
+fi
+if ! installpkg "\$pkg"; then
+ echo INSTALL-FAILED > "$workroot/$prog.status"; exit 1
+fi
+# Log the installed file list (from the package db) so the overlay is disposable.
+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: \$pkgname)"
+echo "================================="
+echo SUCCESS > "$workroot/$prog.status"
+CHROOT_EOF
+
+ local status="BUILD-FAILED"
+ [[ -f "$statf" ]] && status="$(cat "$statf")"
+ local end; end=$(date +%s)
+ ST_TIME["$key"]=$(( end - start ))
+ ST_STATUS["$key"]="$status"
+ [[ "$status" == "SUCCESS" ]] && return 0
+ ST_REASON["$key"]="see $(basename "$logf")"
+ return 1
+}
+
+# =============================================================================
+# Run one target: fresh overlay, build its resolved chain, teardown.
+# Args: <target-slackbuild-dir>
+# =============================================================================
+run_target() {
+ local target_dir="$1"
+ local tkey; tkey="$(pkg_key "$target_dir")"
+
+ echo
+ echo "=== Target: $tkey ==="
+ resolve_target "$target_dir"
+ local resolve_rc=$?
+
+ # Hard resolution failures: mark target and (newly seen) deps, skip building.
+ 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"
+ return
+ fi
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ echo " build order:"
+ local d
+ for d in "${RESOLVED_ORDER[@]}"; do
+ echo " $(pkg_key "$d")$([[ "${HAS_README[$d]:-}" == 1 ]] && echo " [%README%]")"
+ echo "$(pkg_key "$d")" >> "$RUN_DIR/build-order.txt"
+ done
+ return
+ fi
+
+ echo " build order: ${RESOLVED_ORDER[*]##*/}"
+ for d in "${RESOLVED_ORDER[@]}"; do echo "$(pkg_key "$d")" >> "$RUN_DIR/build-order.txt"; done
+
+ local tmpdir; tmpdir="$(setup_overlay)"
+
+ # Build chain in order. On a failure, mark dependents BLOCKED-BY-DEP.
+ local d failed_progs=()
+ for d in "${RESOLVED_ORDER[@]}"; do
+ local key; key="$(pkg_key "$d")"
+ local prog; prog="$(basename "$d")"
+
+ # already blocked because an earlier dep it needs failed?
+ 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
+
+ echo " building $key ..."
+ if build_one "$tmpdir" "$d"; 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
+
+ # Overlay is always torn down: the per-package logs capture everything worth
+ # inspecting (full build/install output, resolved .info env, installed file
+ # list), so there is no reason to retain the filesystem.
+ teardown_overlay "$tmpdir"
+}
+
+# Does SlackBuild dir $1 directly require any prog in failed list (nameref $2)?
+# ponytail: direct REQUIRES check only; transitive blocking still works because
+# this runs in topo order, so a dep's failure propagates up one hop at a time.
+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
+}
+
+# =============================================================================
+# SUMMARY (screen color + plain summary.log)
+# =============================================================================
+print_summary() {
+ local total=$SECONDS
+ local succ=0 fail=0 blocked=0
+ local summary="$RUN_DIR/summary.log"
+
+ {
+ echo "sbo-batch-test run summary"
+ echo "target: $TARGET_ARG"
+ 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++)) ;;
+ BLOCKED-BY-DEP|UNMET-DEP) 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, total %ss\n" \
+ "$C_GRN" "$succ" "$C_RST" "$C_RED" "$fail" "$C_RST" "$C_YEL" "$blocked" "$C_RST" "$total"
+ echo "logs: $RUN_DIR"
+ {
+ echo
+ echo "$succ succeeded, $fail failed, $blocked blocked, total ${total}s"
+ echo "logs: $RUN_DIR"
+ } >> "$summary"
+}
+
+# =============================================================================
+# main
+# =============================================================================
+main() {
+ parse_args "$@"
+ init_color
+ validate_env
+
+ RUN_DIR="$LOG_ROOT/$(date +%Y-%m-%d_%H-%M-%S)"
+ mkdir -p "$RUN_DIR"
+ : > "$RUN_DIR/build-order.txt"
+
+ [[ $DRY_RUN -eq 0 ]] && update_base
+
+ [[ $WITH_X -eq 1 ]] && { echo "X passthrough on (xhost +local:hosts). Security: allows local connections to your X server."; xhost +local:hosts >/dev/null; }
+
+ # Collect targets.
+ local -a targets=()
+ if [[ -d "$TARGET_ARG" ]]; then
+ # category-folder mode: every subdir with a matching .info is a target
+ local d prog
+ for d in "$TARGET_ARG"/*; do
+ [[ -d "$d" ]] || continue
+ prog="$(basename "$d")"
+ [[ -f "$d/$prog.info" ]] && targets+=("$d")
+ done
+ if [[ ${#targets[@]} -eq 0 ]]; then
+ echo "No SlackBuild targets found in folder: $TARGET_ARG" >&2
+ exit 1
+ fi
+ else
+ # single-package mode: resolve the name in the tree
+ local tdir
+ if tdir="$(find_slackbuild_dir "$TARGET_ARG")"; then
+ targets+=("$tdir")
+ else
+ echo "Program not found in any SBo tree root: $TARGET_ARG" >&2
+ exit 1
+ fi
+ fi
+
+ # TODO: "all" mode and queue/list-file mode plug in here (populate `targets`).
+
+ local t
+ for t in "${targets[@]}"; do
+ run_target "$t"
+ done
+
+ print_summary
+}
+
+main "$@"
diff --git a/test-logic.sh b/test-logic.sh
new file mode 100755
index 0000000..13d97f8
--- /dev/null
+++ b/test-logic.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+#
+# Logic self-check for sbo-batch-test. Covers the pure, VM-independent parts:
+# dependency resolution (topo order, %README%, unmet-dep, cycles) and
+# BLOCKED-BY-DEP propagation (depends_on_failed). No overlay, no chroot.
+#
+# Run: bash test-logic.sh
+#
+set -uo pipefail
+
+SCRIPT="$(dirname "$0")/sbo-batch-test"
+T=$(mktemp -d)
+BASE=$(mktemp -d); mkdir -p "$BASE/var/log/packages"
+
+cleanup() { rm -rf "$T" "$BASE"; }
+trap cleanup EXIT
+
+# Fake SBo tree under one category. mk <prog> "<REQUIRES>".
+mk() { mkdir -p "$T/cat/$1"; echo "REQUIRES=\"$2\"" > "$T/cat/$1/$1.info"; }
+
+# Graph:
+# c (no deps)
+# b -> c
+# a -> b, %README%
+# d -> nonexistentpkg (unmet)
+# e -> f, f -> e (cycle)
+# g -> b (for blocked-by-dep: if b fails, g blocks)
+mk c ""
+mk b "c"
+mk a "b %README%"
+mk d "nonexistentpkg"
+mk e "f"
+mk f "e"
+mk g "b"
+
+# Source the script without running main(), then override config AFTER the
+# source (sourcing re-runs the CONFIG block, which would clobber test vars).
+LIB=$(mktemp)
+sed '/^main "\$@"$/d' "$SCRIPT" > "$LIB"
+# shellcheck disable=SC1090
+source "$LIB" 2>/dev/null
+rm -f "$LIB"
+SBO_TREE_ROOTS=("$T")
+SLACKWARE_BASE="$BASE"
+
+pass=0; fail=0
+ok() { echo " ok: $1"; ((pass++)); return 0; }
+bad() { echo " FAIL: $1"; ((fail++)); return 0; }
+
+# --- resolution -------------------------------------------------------------
+resolve_target "$T/cat/a"
+order=""; for x in "${RESOLVED_ORDER[@]}"; do order+="$(basename "$x") "; done
+order="${order% }" # trim trailing space
+[[ "$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"
+
+# --- BLOCKED-BY-DEP (depends_on_failed) -------------------------------------
+# depends_on_failed <slackbuild-dir> <nameref-to-dead-prog-array>
+# returns 0 if the dir directly REQUIRES any prog in the dead list.
+
+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 dep)"; else bad "a should block on b"; fi
+
+dead=(c)
+# a does NOT directly require c (a->b->c). One-hop check must say no here.
+if depends_on_failed "$T/cat/a" dead; then bad "a wrongly blocked on c (not a direct dep)"; else ok "a not directly blocked by c"; fi
+# but b DOES directly require c.
+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 list"; else ok "no block when nothing dead"; fi
+
+# %README% token in REQUIRES must not be treated as a dead dep.
+dead=("%README%")
+if depends_on_failed "$T/cat/a" dead; then bad "%README% treated as dep"; else ok "%README% not treated as dep"; fi
+
+# Propagation invariant note: depends_on_failed is a DIRECT-requires check only.
+# Transitive blocking works because run_target iterates in topo order and adds
+# each blocked package's own prog name to `dead`, so the failure of c blocks b
+# (direct), then b's name enters `dead`, which then blocks a (direct on b).
+# Simulate that one-hop cascade here:
+dead=(c)
+# topo order for a is: c b a. c "fails" -> seed dead=(c).
+chain=(c b a)
+declare -A blocked=()
+for p in "${chain[@]}"; do
+ [[ "$p" == "c" ]] && continue # c is the original failure, already in dead
+ if depends_on_failed "$T/cat/$p" dead; then
+ blocked[$p]=1
+ dead+=("$p") # mark dependents-of-this as blockable next hop
+ fi
+done
+if [[ "${blocked[b]:-}" == "1" && "${blocked[a]:-}" == "1" ]]; then
+ ok "transitive cascade c->b->a via one-hop propagation"
+else
+ bad "cascade dead: b=${blocked[b]:-0} a=${blocked[a]:-0}"
+fi
+
+# --- result -----------------------------------------------------------------
+echo
+echo "$pass passed, $fail failed"
+[[ $fail -eq 0 ]] || exit 1
+echo "ALL LOGIC CHECKS PASS"