#!/bin/bash # # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # # test-build - verify an already-published SBo package still builds on a target # Slackware version inside a throwaway docker container. Resolves + builds its # SBo deps from the local tree, caches built deps per image digest, reports # per-package status and lints the target. # # Dependency-resolution, cache, and summary logic are adapted from sbo-batch-test # (github: danixland). The overlay chroot is replaced by a docker container: the # container IS the disposable environment, so no overlayfs. # # No em dashes in prose by author convention. PROJECT_VERSION="1.1.0" # bump via sed across all scripts; see CLAUDE.md Releases # ============================================================================= # CONFIG (do not edit here; real values live in the external config file) # ============================================================================= SBO_TREE_CURRENT="" SBO_TREE_STABLE="" IMAGE_CURRENT="" IMAGE_STABLE="" LOG_ROOT="/var/log/sbo-test-build" PKG_CACHE="" TB_CONFIG="${TB_CONFIG:-$HOME/.config/sbo-testbuild/config}" if [[ -f "$TB_CONFIG" ]]; then # shellcheck disable=SC1090 source "$TB_CONFIG" fi TB_OVERRIDES="${TB_OVERRIDES:-$HOME/.config/sbo-testbuild/overrides}" # ============================================================================= set -uo pipefail # Not -e: a package build failing is a handled outcome, not a script crash. # ---- flags / globals -------------------------------------------------------- USE_COLOR=1 # --no-color or non-TTY disables DRY_RUN=0 # --dry-run: resolve + print order, do not build ASSUME_YES=0 # --yes: skip the confirm prompt (still prints the order) USE_CACHE=1 # --no-cache disables the dep cache for one run KEEP_TARGET=0 # --keep: copy the built target package out to KEEP_DIR LOCAL_DEPS=0 # --local-deps: also resolve deps from the target's own repo VERSION_ID="current" # "current" | "15.0"; set by --stable TARGET_ARG="" ACTIVE_TREE="" # selected SBo tree (by version) ACTIVE_IMAGE="" # selected image tag (by version) RUN_DIR="" # timestamped log dir for this run DEPS_DIR="" # per-run host dir of built dep .txz, mounted into the container BUILD_OUT="" # per-run host dir where built packages are copied out # Status tracking. Keyed by "category/prog". Parallel assoc arrays. declare -A ST_STATUS=() declare -A ST_REASON=() declare -A ST_TIME=() declare -A ST_README=() usage() { cat <<'EOF' test-build - verify an SBo package builds on a target Slackware in docker USAGE: test-build [OPTIONS] OPTIONS: -h, --help This text. -V, --version Print version and exit. --stable Target Slackware 15.0 (image + tree). Default is -current. --dry-run Resolve, apply overrides, print the build order, do not build. --yes Skip the confirm prompt (the order is still printed first). --no-cache Rebuild all deps this run, ignore/refresh the cache. --keep Copy the built target package out to a kept/ dir (path is printed), so it can be installed on the host. The build is otherwise throwaway. --local-deps Also resolve deps from the target's own repo (its grandparent dir), for packages whose deps are siblings in the same repo and not on SBo. The configured SBo tree is still searched after. --no-color Disable ANSI color (auto-disabled when stdout is not a TTY). EOF } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0 ;; -V|--version) echo "test-build $PROJECT_VERSION"; exit 0 ;; --stable|15.0) VERSION_ID="15.0"; shift ;; --dry-run) DRY_RUN=1; shift ;; --yes) ASSUME_YES=1; shift ;; --no-cache) USE_CACHE=0; shift ;; --keep) KEEP_TARGET=1; shift ;; --local-deps) LOCAL_DEPS=1; shift ;; --no-color) USE_COLOR=0; shift ;; -*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; *) if [[ -n "$TARGET_ARG" ]]; then echo "Only one target accepted (got '$TARGET_ARG' and '$1')." >&2 exit 2 fi TARGET_ARG="$1"; shift ;; esac done if [[ -z "$TARGET_ARG" ]]; then echo "No target given." >&2; usage >&2; exit 2 fi } init_color() { if [[ $USE_COLOR -eq 1 && -t 1 ]]; then C_RED=$'\e[31m'; C_GRN=$'\e[32m'; C_YEL=$'\e[33m'; C_RST=$'\e[0m' else C_RED=""; C_GRN=""; C_YEL=""; C_RST="" fi } # Map VERSION_ID to the active tree + image. No I/O, unit-testable. select_version_paths() { if [[ "$VERSION_ID" == "15.0" ]]; then ACTIVE_TREE="$SBO_TREE_STABLE"; ACTIVE_IMAGE="$IMAGE_STABLE" else ACTIVE_TREE="$SBO_TREE_CURRENT"; ACTIVE_IMAGE="$IMAGE_CURRENT" fi } # require_config: the external config must exist and set the version's paths. require_config() { if [[ ! -f "$TB_CONFIG" ]]; then cat >&2 <&2 exit 1 fi if [[ ! -d "$ACTIVE_TREE" ]]; then echo "SBo tree for '$VERSION_ID' does not exist: $ACTIVE_TREE" >&2 exit 1 fi } # ============================================================================= # SBo tree lookup # ============================================================================= # In this tool there is one active tree per run, but the resolver reads an array # named SBO_TREE_ROOTS so the ported logic and its self-check match sbo-batch-test. # main() sets SBO_TREE_ROOTS=("$ACTIVE_TREE") after require_config. declare -a SBO_TREE_ROOTS=() find_slackbuild_dir() { local prog="$1" root d for root in "${SBO_TREE_ROOTS[@]}"; do [[ -d "$root" ]] || continue for d in "$root"/*/"$prog"; do if [[ -d "$d" && -f "$d/$prog.info" ]]; then echo "$d"; return 0 fi done done return 1 } category_of() { basename "$(dirname "$1")"; } pkg_key() { echo "$(category_of "$1")/$(basename "$1")"; } # Resolve the TARGET to test from CWD or a path (repo-agnostic). The target is # the SlackBuild you are editing, wherever it lives; its deps come from the # configured SBo tree (see find_slackbuild_dir), NOT the other way around. # - a path (absolute, or containing '/', or '.'/'./x'): use that dir directly # - a bare name: .// under CWD, else CWD itself if it IS / # Prints the resolved absolute dir on success; returns 1 (with a message) if the # dir has no matching .info. resolve_target_dir() { local arg="$1" dir prog if [[ "$arg" == /* || "$arg" == .* || "$arg" == */* ]]; then dir="${arg%/}"; prog="$(basename "$dir")" elif [[ -f "./$arg/$arg.info" ]]; then dir="./$arg"; prog="$arg" elif [[ "$(basename "$PWD")" == "$arg" && -f "./$arg.info" ]]; then dir="."; prog="$arg" else echo "Target '$arg' not found: no ./$arg/$arg.info, and CWD is not $arg/." >&2 return 1 fi if [[ ! -f "$dir/$prog.info" ]]; then echo "Target dir '$dir' has no $prog.info (expected a SlackBuild package dir)." >&2 return 1 fi ( cd "$dir" && pwd ) # emit absolute path } read_requires() { local info="$1" # shellcheck disable=SC1090 ( set +u; source "$info"; echo "${REQUIRES:-}" ) } version_of() { local dir="$1" local info="$dir/$(basename "$dir").info" [[ -f "$info" ]] || return local v; v="$(grep -m1 '^VERSION=' "$info" | cut -d'"' -f2)" echo "$v" } # ============================================================================= # DEPENDENCY RESOLUTION (topo sort + cycle detection, LOCAL tree only) # ============================================================================= declare -a RESOLVED_ORDER=() declare -A UNMET=() declare -a CYCLES=() declare -A HAS_README=() declare -A _vstate=() declare -A FETCH_DEPS=() # prog -> 1: resolve via sbopkg in-container # Is a prog already present in the container base? Kept as a callback so the # pure topo logic stays testable. # # Populated lazily from the active image's package db on first call: each entry # in /var/log/packages is named ---, so strip the last # three dash-fields to get the bare package name and match the SBo token # against that. TB_BASE_PKGS can be pre-seeded by tests to bypass docker. declare -A TB_BASE_PKGS=() TB_BASE_LOADED="${TB_BASE_LOADED:-0}" load_base_pkgs() { [[ "$TB_BASE_LOADED" == "1" ]] && return 0 TB_BASE_LOADED=1 [[ -n "$ACTIVE_IMAGE" ]] || return 0 local entry name while IFS= read -r entry; do [[ -n "$entry" ]] || continue # strip trailing --- name="${entry%-*-*-*}" TB_BASE_PKGS["$name"]=1 done < <(docker run --rm "$ACTIVE_IMAGE" \ /bin/sh -c 'ls /var/log/packages' 2>/dev/null) } installed_in_base() { load_base_pkgs [[ -n "${TB_BASE_PKGS[$1]:-}" ]] } _resolve_visit() { local dir="$1" parent="$2" local key; key="$(basename "$dir")" if [[ "${_vstate[$dir]:-}" == "1" ]]; then return 0; fi if [[ "${_vstate[$dir]:-}" == "0" ]]; then CYCLES+=("cycle involving $key (pulled in via $parent)") return 1 fi _vstate["$dir"]=0 local info="$dir/$(basename "$dir").info" local req tok depdir rc=0 req="$(read_requires "$info")" for tok in $req; do if [[ "$tok" == "%README%" ]]; then HAS_README["$dir"]=1 continue fi tok="$(apply_rename "$tok")" # drop: satisfied by the -current base (e.g. a 15.0-only dep like rust-opt # whose payload ships in current's full-install image). Skip it entirely: # no recurse, no UNMET, no order entry. Inert on 15.0 (overrides are). if [[ "${OV_DROP[$tok]:-}" == "1" ]]; then continue fi if [[ "${OV_FETCH[$tok]:-}" == "1" ]]; then FETCH_DEPS["$tok"]=1 continue fi if depdir="$(find_slackbuild_dir "$tok")"; then _resolve_visit "$depdir" "$key" || rc=1 elif installed_in_base "$tok"; then : else UNMET["$tok"]="needed by $key" rc=1 fi done _vstate["$dir"]=1 RESOLVED_ORDER+=("$dir") return $rc } resolve_target() { local dir="$1" RESOLVED_ORDER=() CYCLES=() UNMET=() FETCH_DEPS=() _vstate=() _resolve_visit "$dir" "(top)" } # ============================================================================= # current-vs-stable overrides. Parsed from $TB_OVERRIDES. Applied only when # targeting -current (15.0 is the SBo baseline, no deltas). # ============================================================================= declare -A OV_DROP=() # prog -> 1 declare -A OV_RENAME=() # old -> new declare -A OV_FETCH=() # prog -> 1 load_overrides() { OV_DROP=(); OV_RENAME=(); OV_FETCH=() # 15.0 is the baseline: no overrides. [[ "$VERSION_ID" == "15.0" ]] && return [[ -f "$TB_OVERRIDES" ]] || return local line kind rest while IFS= read -r line; do line="${line%%#*}" # strip comments line="${line#"${line%%[![:space:]]*}"}" # ltrim [[ -z "$line" ]] && continue kind="${line%%:*}"; rest="${line#*:}" kind="${kind//[[:space:]]/}" rest="${rest#"${rest%%[![:space:]]*}"}" # ltrim value case "$kind" in drop) OV_DROP["${rest//[[:space:]]/}"]=1 ;; fetch) OV_FETCH["${rest//[[:space:]]/}"]=1 ;; rename) # rest is "old -> new" local old new old="${rest%%->*}"; new="${rest##*->}" old="${old//[[:space:]]/}"; new="${new//[[:space:]]/}" [[ -n "$old" && -n "$new" ]] && OV_RENAME["$old"]="$new" ;; *) echo "WARN: unknown override rule: $line" >&2 ;; esac done < "$TB_OVERRIDES" } # Map a dep token through rename rules (identity if no rule). apply_rename() { local tok="$1" echo "${OV_RENAME[$tok]:-$tok}" } # Remove dropped packages from RESOLVED_ORDER in place. apply_overrides_to_order() { local d prog keep=() for d in "${RESOLVED_ORDER[@]}"; do prog="$(basename "$d")" [[ "${OV_DROP[$prog]:-}" == "1" ]] && continue keep+=("$d") done # Assign without "${keep[@]:-}": under set -u that fallback yields a # one-element array holding an empty string when keep is empty (all deps # dropped), which the build loop would then iterate over as d="". Guard it. if [[ ${#keep[@]} -eq 0 ]]; then RESOLVED_ORDER=() else RESOLVED_ORDER=("${keep[@]}") fi } # ============================================================================= # Dependency cache. Layout: $CACHE_ROOT///--...txz where # CACHE_ROOT = $PKG_CACHE/ (set per run by resolve_cache_root). # Key is prog+version. --no-cache (USE_CACHE=0) or empty PKG_CACHE disables. # ============================================================================= CACHE_ROOT="" # set by resolve_cache_root once the image digest is known # True when the cache is usable this run. _cache_on() { [[ $USE_CACHE -eq 1 && -n "$PKG_CACHE" && -n "$CACHE_ROOT" ]]; } _cache_ver_of() { local prog="$1" base="$2" base="${base#"$prog"-}" echo "${base%%-*}" } # cache_decision -> cached | bump:OLD:NEW | new cache_decision() { local cat="$1" prog="$2" version="$3" _cache_on || { echo new; return; } local dir="$CACHE_ROOT/$cat/$prog" local f newest="" for f in "$dir/$prog"-*.t?z; do [[ -e "$f" ]] || continue [[ -z "$newest" || "$f" -nt "$newest" ]] && newest="$f" done [[ -z "$newest" ]] && { echo new; return; } local have; have="$(_cache_ver_of "$prog" "$(basename "$newest")")" if [[ "$have" == "$version" ]]; then echo cached; else echo "bump:$have:$version"; fi } cache_path() { local cat="$1" prog="$2" version="$3" _cache_on || return local dir="$CACHE_ROOT/$cat/$prog" local f newest="" for f in "$dir/$prog"-*.t?z; do [[ -e "$f" ]] || continue [[ -z "$newest" || "$f" -nt "$newest" ]] && newest="$f" done [[ -z "$newest" ]] && return [[ "$(_cache_ver_of "$prog" "$(basename "$newest")")" == "$version" ]] && echo "$newest" } cache_store() { local cat="$1" prog="$2" src="$3" _cache_on || return local dir="$CACHE_ROOT/$cat/$prog" mkdir -p "$dir" rm -f "$dir"/*.t?z cp -a "$src" "$dir/" } cache_label() { local dir="$1" is_target="$2" local cat prog ver dec cat="$(category_of "$dir")"; prog="$(basename "$dir")"; ver="$(version_of "$dir")" dec="$(cache_decision "$cat" "$prog" "$ver")" local label case "$dec" in cached) label="cached ($ver)" ;; bump:*) label="rebuild: ${dec#bump:}"; label="${label/:/ -> }" ;; *) label="build (new)" ;; esac if [[ "$is_target" == "1" ]]; then case "$dec" in cached) label="build (cached $ver, rebuilt as target)" ;; esac echo "target, $label" else echo "$label" fi } # Compute CACHE_ROOT from the image's digest, namespaced by variant. Falls back # to the tag if the digest cannot be read (still isolates per image reference). # The dir is "-", so an image rebuild changes the digest and # self-invalidates the cache (deps built against the old base are never reused). # # Prune stale digests of THIS variant on the way in: a base update leaves the # old - dir orphaned, so drop it. Scoped to the variant so a # --current run does not wipe the 15.0 cache (both live caches survive, and # testing both trees back-to-back still reuses built deps). resolve_cache_root() { [[ -z "$PKG_CACHE" ]] && { CACHE_ROOT=""; return; } local digest digest="$(docker image inspect --format '{{index .Id}}' "$ACTIVE_IMAGE" 2>/dev/null)" [[ -z "$digest" ]] && digest="tag-${ACTIVE_IMAGE//[^a-zA-Z0-9._-]/_}" digest="${digest//[^a-zA-Z0-9._-]/_}" local name="${VERSION_ID}-${digest}" CACHE_ROOT="$PKG_CACHE/$name" mkdir -p "$CACHE_ROOT" # drop superseded caches for this variant only local d for d in "$PKG_CACHE/${VERSION_ID}-"*; do [[ -d "$d" ]] || continue [[ "$(basename "$d")" == "$name" ]] && continue rm -rf "$d" done } # Does SlackBuild dir $1 directly require any prog in the dead list (nameref $2)? # Direct-requires check only; transitive blocking works because run_target # iterates in topo order, propagating a failure one hop per package. depends_on_failed() { local dir="$1"; local -n failed="$2" local info="$dir/$(basename "$dir").info" local req tok f req="$(read_requires "$info")" for tok in $req; do [[ "$tok" == "%README%" ]] && continue for f in "${failed[@]:-}"; do [[ "$tok" == "$f" ]] && return 0 done done return 1 } # build_one [container-name] # Runs the build in a throwaway container. Sets ST_STATUS/ST_REASON/ST_TIME. # Successful builds copy their package to a host workdir; deps are cached and # the target is linted. Returns 0 on SUCCESS/CACHED, 1 otherwise. build_one() { local dir="$1" is_target="${2:-0}" local prog cat key prog="$(basename "$dir")"; cat="$(category_of "$dir")"; key="$cat/$prog" local logf="$RUN_DIR/${cat}_${prog}.log" local start; start=$(date +%s) local version; version="$(version_of "$dir")" [[ "${HAS_README[$dir]:-}" == "1" ]] && ST_README["$key"]=1 # Dep with a version-matching cached package: installpkg it into the shared # dep-package dir; no build. The target never takes this path. if [[ "$is_target" != "1" ]]; then local cached; cached="$(cache_path "$cat" "$prog" "$version")" if [[ -n "$cached" ]]; then cp -a "$cached" "$DEPS_DIR/" { echo "===== test-build: $prog (from cache) =====" echo "cached package: $(basename "$cached")" } >> "$logf" ST_TIME["$key"]=$(( $(date +%s) - start )) ST_STATUS["$key"]="CACHED" return 0 fi fi # Build in a container. Mounts: # $dir -> /sbo/pkg (ro, the SlackBuild) # $DEPS_DIR -> /sbo/deps (rw, already-built dep .txz to installpkg first) # $BUILD_OUT -> /sbo/out (rw, where the built package is copied out) # The in-container script installs any deps present, then builds the target, # writes a status token to /sbo/out/$prog.status, and copies the package out. local statf="$BUILD_OUT/$prog.status" rm -f "$statf" # fetch deps (removed from -current tree): let the container's sbopkg build # them first. FETCH_DEPS is the set collected during resolution. local fetch_list="" local fp for fp in "${!FETCH_DEPS[@]}"; do fetch_list+="$fp "; done # -i is required: without it docker does not attach stdin, so `bash -s` reads # nothing and the heredoc script is silently discarded (exit 0, empty log). docker run --rm -i \ -v "$dir":/sbo/pkg:ro \ -v "$DEPS_DIR":/sbo/deps \ -v "$BUILD_OUT":/sbo/out \ -e PROG="$prog" \ -e FETCH_LIST="$fetch_list" \ -e IS_TARGET="$is_target" \ "$ACTIVE_IMAGE" /bin/bash -s >>"$logf" 2>&1 <<'CONTAINER_EOF' set -uo pipefail prog="$PROG" statf="/sbo/out/$prog.status" # 0. install already-built dependency packages (order guaranteed by the host). for d in /sbo/deps/*.t?z; do [[ -e "$d" ]] || continue installpkg --terse "$d" || { echo "INSTALL-FAILED (dep $d)"; echo INSTALL-FAILED > "$statf"; exit 1; } done # 0b. fetch-from-SBo deps via sbopkg (removed from the -current tree). for f in $FETCH_LIST; do echo "sbopkg-building fetch dep: $f" sbopkg -B -i "$f" || { echo "BUILD-FAILED (fetch dep $f)"; echo BUILD-FAILED > "$statf"; exit 1; } done # copy the SlackBuild out of the read-only mount so it can write there. cp -a /sbo/pkg /sbo/build cd /sbo/build || { echo BUILD-FAILED > "$statf"; exit 1; } . ./"$prog".info export OUTPUT=/sbo/out mkdir -p "$OUTPUT" echo "===== test-build: $prog =====" echo "PRGNAM=${PRGNAM:-$prog} VERSION=${VERSION:-?} BUILD=${BUILD:-?} TAG=${TAG:-?}" echo "uname -m: $(uname -m) OUTPUT=$OUTPUT" echo "REQUIRES=${REQUIRES:-}" echo "=================================" if [ "$(uname -m)" = "x86_64" ] && [ -n "${DOWNLOAD_x86_64:-}" ] && [ "${DOWNLOAD_x86_64}" != "UNSUPPORTED" ] && [ "${DOWNLOAD_x86_64}" != "UNTESTED" ]; then DL="$DOWNLOAD_x86_64"; MD="$MD5SUM_x86_64" else DL="$DOWNLOAD"; MD="$MD5SUM" fi for u in $DL; do wget -c --tries=3 "$u" || { echo DOWNLOAD-FAILED > "$statf"; exit 1; } done set -- $MD for u in $DL; do f="$(basename "$u")" want="$1"; shift got="$(md5sum "$f" | cut -d' ' -f1)" if [ "$got" != "$want" ]; then echo "MD5 mismatch on $f: want $want got $got" echo MD5-MISMATCH > "$statf"; exit 1 fi done # Source /etc/profile.d so dep-provided env is live (google-go-lang sets GOROOT # + PATH to its go here, rust-opt sets cargo, etc.). This heredoc runs in a # non-login shell, which does NOT read profile.d, so a bare `go build` would # otherwise pick the system gccgo instead of the installed google-go-lang. set +u # profile.d scripts routinely reference unset vars for pf in /etc/profile.d/*.sh; do [ -r "$pf" ] && . "$pf" done set -u chmod +x ./"$prog".SlackBuild if ! ./"$prog".SlackBuild; then echo BUILD-FAILED > "$statf"; exit 1 fi pkg="$(ls -t "$OUTPUT"/"$prog"-*.t?z 2>/dev/null | head -n1)" if [ -z "$pkg" ]; then echo "No package produced in $OUTPUT" echo BUILD-FAILED > "$statf"; exit 1 fi if ! installpkg --terse "$pkg"; then echo INSTALL-FAILED > "$statf"; exit 1 fi echo "===== installed files: $(basename "$pkg") =====" pkgname="$(basename "$pkg")"; pkgname="${pkgname%.t?z}" cat "/var/log/packages/$pkgname" 2>/dev/null || echo "(package db entry not found)" echo "=================================" # lint the target here in the container: sbopkglint is baked into the image and # runs as root, so it needs no host sudo. Fail-soft: findings never fail the build. if [ "${IS_TARGET:-0}" = "1" ] && command -v sbopkglint >/dev/null 2>&1; then echo "===== sbopkglint: $(basename "$pkg") =====" if sbopkglint "$pkg"; then echo "LINT-CLEAN" else echo "LINT-FINDINGS" fi echo "=================================" fi echo SUCCESS > "$statf" CONTAINER_EOF local status="BUILD-FAILED" [[ -f "$statf" ]] && status="$(cat "$statf")" ST_TIME["$key"]=$(( $(date +%s) - start )) ST_STATUS["$key"]="$status" if [[ "$status" == "SUCCESS" ]]; then # locate the built package copied to the host workdir local built newest="" for built in "$BUILD_OUT/${prog}"-*.t?z; do [[ -e "$built" ]] || continue [[ -z "$newest" || "$built" -nt "$newest" ]] && newest="$built" done if [[ -n "$newest" ]]; then if [[ "$is_target" != "1" ]]; then cache_store "$cat" "$prog" "$newest" # make the dep available to later builds in this run cp -a "$newest" "$DEPS_DIR/" else # lint ran in-container (see IS_TARGET block); surface its verdict from # the log so the host summary can show clean/findings. if grep -q '^LINT-FINDINGS$' "$logf" 2>/dev/null; then echo " lint: ${C_RED}findings${C_RST} (see $(basename "$logf"))" elif grep -q '^LINT-CLEAN$' "$logf" 2>/dev/null; then echo " lint: ${C_GRN}clean${C_RST}" fi # --keep: copy the built target package to a durable dir (sibling of the # logs tree, not inside a throwaway run dir) so it can be installed on # the host, e.g. to regenerate post-install artifacts. if [[ $KEEP_TARGET -eq 1 ]]; then local keepdir; keepdir="$(dirname "$LOG_ROOT")/kept" mkdir -p "$keepdir" cp -a "$newest" "$keepdir/" echo " kept: $keepdir/$(basename "$newest")" fi fi fi return 0 fi ST_REASON["$key"]="see $(basename "$logf")" return 1 } # confirm_order: print the resolved order (overrides marked) and ask to proceed. # --yes skips the prompt but the order is still printed. --dry-run never reaches # here. Returns 0 to proceed, 1 to abort. confirm_order() { local target_dir="$1" echo " build order (${VERSION_ID}):" local d for d in "${RESOLVED_ORDER[@]}"; do local it=0; [[ "$d" == "$target_dir" ]] && it=1 local rdm=""; [[ "${HAS_README[$d]:-}" == 1 ]] && rdm=" [%README%]" printf " %-30s %s%s\n" "$(pkg_key "$d")" "$(cache_label "$d" "$it")" "$rdm" echo "$(pkg_key "$d")" >> "$RUN_DIR/build-order.txt" done # note fetch deps (built via sbopkg in-container, not in the order list) local fp for fp in "${!FETCH_DEPS[@]}"; do printf " %-30s %s\n" "$fp" "fetch (sbopkg in container)" done [[ $ASSUME_YES -eq 1 ]] && return 0 local reply read -rp " Proceed? [Y/n] " reply [[ -z "$reply" || "$reply" =~ ^[Yy]$ ]] } # run_target run_target() { local target_dir="$1" local tkey; tkey="$(pkg_key "$target_dir")" echo echo "=== Target: $tkey (${VERSION_ID}) ===" resolve_target "$target_dir" apply_overrides_to_order # Hard resolution failures: report and stop, do not build. if [[ ${#CYCLES[@]} -gt 0 || ${#UNMET[@]} -gt 0 ]]; then local why="" if [[ ${#UNMET[@]} -gt 0 ]]; then local u for u in "${!UNMET[@]}"; do why+="unmet:$u(${UNMET[$u]}) "; done fi [[ ${#CYCLES[@]} -gt 0 ]] && why+="${CYCLES[*]}" ST_STATUS["$tkey"]="UNMET-DEP" ST_REASON["$tkey"]="$why" echo " resolution failed: $why" echo " add an override rule ($TB_OVERRIDES) and rerun, or fix the tree." >&2 return fi if [[ $DRY_RUN -eq 1 ]]; then echo " build order (dry-run):" local d for d in "${RESOLVED_ORDER[@]}"; do local it=0; [[ "$d" == "$target_dir" ]] && it=1 local rdm=""; [[ "${HAS_README[$d]:-}" == 1 ]] && rdm=" [%README%]" printf " %-30s %s%s\n" "$(pkg_key "$d")" "$(cache_label "$d" "$it")" "$rdm" echo "$(pkg_key "$d")" >> "$RUN_DIR/build-order.txt" done local fp for fp in "${!FETCH_DEPS[@]}"; do printf " %-30s %s\n" "$fp" "fetch (sbopkg in container)" done return fi if ! confirm_order "$target_dir"; then echo " aborted." ST_STATUS["$tkey"]="ABORTED" return fi # Per-run docker workdirs (host side, discarded after the run). DEPS_DIR="$RUN_DIR/deps"; BUILD_OUT="$RUN_DIR/out" mkdir -p "$DEPS_DIR" "$BUILD_OUT" local d failed_progs=() for d in "${RESOLVED_ORDER[@]}"; do local key; key="$(pkg_key "$d")" local prog; prog="$(basename "$d")" if depends_on_failed "$d" failed_progs; then ST_STATUS["$key"]="BLOCKED-BY-DEP" ST_REASON["$key"]="blocked by failed dep" [[ "${HAS_README[$d]:-}" == "1" ]] && ST_README["$key"]=1 echo " $key: BLOCKED-BY-DEP" failed_progs+=("$prog") continue fi local it=0; [[ "$d" == "$target_dir" ]] && it=1 echo " building $key ..." if build_one "$d" "$it"; then echo " $key: ${ST_STATUS[$key]} (${ST_TIME[$key]}s)" else echo " $key: ${ST_STATUS[$key]} (${ST_TIME[$key]}s)" failed_progs+=("$prog") fi done } # ============================================================================= # SUMMARY (ported) # ============================================================================= print_summary() { local total=$SECONDS local succ=0 fail=0 blocked=0 cached=0 local summary="$RUN_DIR/summary.log" { echo "test-build run summary" echo "target: $TARGET_ARG version: $VERSION_ID" echo } > "$summary" echo echo "================ SUMMARY ================" local key for key in "${!ST_STATUS[@]}"; do local st="${ST_STATUS[$key]}" rsn="${ST_REASON[$key]:-}" t="${ST_TIME[$key]:-0}" local rd=""; [[ "${ST_README[$key]:-}" == "1" ]] && rd=" [%README%]" local col="$C_YEL" case "$st" in SUCCESS) col="$C_GRN"; ((succ++)) ;; CACHED) col="$C_GRN"; ((cached++)) ;; BLOCKED-BY-DEP|UNMET-DEP|ABORTED) col="$C_YEL"; ((blocked++)) ;; *) col="$C_RED"; ((fail++)) ;; esac printf "%s%-30s %-16s%s %s%s (%ss)\n" "$col" "$key" "$st" "$C_RST" "$rsn" "$rd" "$t" printf "%-30s %-16s %s%s (%ss)\n" "$key" "$st" "$rsn" "$rd" "$t" >> "$summary" done echo "----------------------------------------" printf "%s%d succeeded%s, %s%d failed%s, %s%d blocked%s, %s%d cached%s, total %ss\n" \ "$C_GRN" "$succ" "$C_RST" "$C_RED" "$fail" "$C_RST" "$C_YEL" "$blocked" "$C_RST" \ "$C_GRN" "$cached" "$C_RST" "$total" echo "logs: $RUN_DIR" if [[ $fail -eq 0 && $blocked -eq 0 ]]; then echo "${C_GRN}All green.${C_RST} Safe to build the SBo submission tarball on the host." fi { echo echo "$succ succeeded, $fail failed, $blocked blocked, $cached cached, total ${total}s" echo "logs: $RUN_DIR" } >> "$summary" } # Ensure the selected image is available locally. It is built by a separate job # (the image-builder) and pushed to the LAN registry; this script only consumes # it. If it is not already local, pull it once (ACTIVE_IMAGE is a fully-qualified # registry ref). A stale local tag is not refreshed here. # ponytail: pull-if-missing only. If the registry's :current is rebuilt, the # local copy goes stale silently. Add a --pull force-flag if that bites. require_image() { docker image inspect "$ACTIVE_IMAGE" >/dev/null 2>&1 && return 0 echo "Image not present locally, pulling: $ACTIVE_IMAGE" >&2 docker pull "$ACTIVE_IMAGE" >&2 && return 0 cat >&2 </dev/null 2>&1 || { echo "docker not found in PATH." >&2; exit 1; } require_image fi resolve_cache_root RUN_DIR="$LOG_ROOT/$(date +%Y-%m-%d_%H-%M-%S)" mkdir -p "$RUN_DIR" : > "$RUN_DIR/build-order.txt" # The target is the local SlackBuild under test (CWD or a path); its deps are # resolved from the configured SBo tree during _resolve_visit. local tdir if ! tdir="$(resolve_target_dir "$TARGET_ARG")"; then exit 1 fi # --local-deps: search the target's own repo (grandparent of //) # before the SBo tree, so deps that are siblings in the same repo resolve. if [[ $LOCAL_DEPS -eq 1 ]]; then local repo_root; repo_root="$(dirname "$(dirname "$tdir")")" SBO_TREE_ROOTS=("$repo_root" "${SBO_TREE_ROOTS[@]}") fi run_target "$tdir" print_summary } main "$@"