#!/bin/bash # # test-build - verify an already-published SBo package still builds on a target # Slackware version inside a throwaway docker container. Resolves + builds its # SBo deps from the local tree, caches built deps per image digest, reports # per-package status and lints the target. # # Dependency-resolution, cache, and summary logic are adapted from sbo-batch-test # (github: danixland). The overlay chroot is replaced by a docker container: the # container IS the disposable environment, so no overlayfs. # # No em dashes in prose by author convention. # ============================================================================= # CONFIG (do not edit here; real values live in the external config file) # ============================================================================= SBO_TREE_CURRENT="" SBO_TREE_STABLE="" IMAGE_CURRENT="" IMAGE_STABLE="" LOG_ROOT="/var/log/sbo-test-build" PKG_CACHE="" TB_CONFIG="${TB_CONFIG:-$HOME/.config/sbo-testbuild/config}" if [[ -f "$TB_CONFIG" ]]; then # shellcheck disable=SC1090 source "$TB_CONFIG" fi TB_OVERRIDES="${TB_OVERRIDES:-$HOME/.config/sbo-testbuild/overrides}" # ============================================================================= set -uo pipefail # Not -e: a package build failing is a handled outcome, not a script crash. # ---- flags / globals -------------------------------------------------------- USE_COLOR=1 # --no-color or non-TTY disables DRY_RUN=0 # --dry-run: resolve + print order, do not build ASSUME_YES=0 # --yes: skip the confirm prompt (still prints the order) USE_CACHE=1 # --no-cache disables the dep cache for one run VERSION_ID="current" # "current" | "15.0"; set by --stable TARGET_ARG="" ACTIVE_TREE="" # selected SBo tree (by version) ACTIVE_IMAGE="" # selected image tag (by version) RUN_DIR="" # timestamped log dir for this run # Status tracking. Keyed by "category/prog". Parallel assoc arrays. declare -A ST_STATUS=() declare -A ST_REASON=() declare -A ST_TIME=() declare -A ST_README=() usage() { cat <<'EOF' test-build - verify an SBo package builds on a target Slackware in docker USAGE: test-build [OPTIONS] OPTIONS: -h, --help This text. --stable Target Slackware 15.0 (image + tree). Default is -current. --dry-run Resolve, apply overrides, print the build order, do not build. --yes Skip the confirm prompt (the order is still printed first). --no-cache Rebuild all deps this run, ignore/refresh the cache. --no-color Disable ANSI color (auto-disabled when stdout is not a TTY). EOF } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0 ;; --stable|15.0) VERSION_ID="15.0"; shift ;; --dry-run) DRY_RUN=1; shift ;; --yes) ASSUME_YES=1; shift ;; --no-cache) USE_CACHE=0; shift ;; --no-color) USE_COLOR=0; shift ;; -*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; *) if [[ -n "$TARGET_ARG" ]]; then echo "Only one target accepted (got '$TARGET_ARG' and '$1')." >&2 exit 2 fi TARGET_ARG="$1"; shift ;; esac done if [[ -z "$TARGET_ARG" ]]; then echo "No target given." >&2; usage >&2; exit 2 fi } init_color() { if [[ $USE_COLOR -eq 1 && -t 1 ]]; then C_RED=$'\e[31m'; C_GRN=$'\e[32m'; C_YEL=$'\e[33m'; C_RST=$'\e[0m' else C_RED=""; C_GRN=""; C_YEL=""; C_RST="" fi } # Map VERSION_ID to the active tree + image. No I/O, unit-testable. select_version_paths() { if [[ "$VERSION_ID" == "15.0" ]]; then ACTIVE_TREE="$SBO_TREE_STABLE"; ACTIVE_IMAGE="$IMAGE_STABLE" else ACTIVE_TREE="$SBO_TREE_CURRENT"; ACTIVE_IMAGE="$IMAGE_CURRENT" fi } # require_config: the external config must exist and set the version's paths. require_config() { if [[ ! -f "$TB_CONFIG" ]]; then cat >&2 <&2 exit 1 fi if [[ ! -d "$ACTIVE_TREE" ]]; then echo "SBo tree for '$VERSION_ID' does not exist: $ACTIVE_TREE" >&2 exit 1 fi } # ============================================================================= # SBo tree lookup # ============================================================================= # In this tool there is one active tree per run, but the resolver reads an array # named SBO_TREE_ROOTS so the ported logic and its self-check match sbo-batch-test. # main() sets SBO_TREE_ROOTS=("$ACTIVE_TREE") after require_config. declare -a SBO_TREE_ROOTS=() find_slackbuild_dir() { local prog="$1" root d for root in "${SBO_TREE_ROOTS[@]}"; do [[ -d "$root" ]] || continue for d in "$root"/*/"$prog"; do if [[ -d "$d" && -f "$d/$prog.info" ]]; then echo "$d"; return 0 fi done done return 1 } category_of() { basename "$(dirname "$1")"; } pkg_key() { echo "$(category_of "$1")/$(basename "$1")"; } read_requires() { local info="$1" # shellcheck disable=SC1090 ( set +u; source "$info"; echo "${REQUIRES:-}" ) } version_of() { local dir="$1" local info="$dir/$(basename "$dir").info" [[ -f "$info" ]] || return local v; v="$(grep -m1 '^VERSION=' "$info" | cut -d'"' -f2)" echo "$v" } # ============================================================================= # DEPENDENCY RESOLUTION (topo sort + cycle detection, LOCAL tree only) # ============================================================================= declare -a RESOLVED_ORDER=() declare -A UNMET=() declare -a CYCLES=() declare -A HAS_README=() declare -A _vstate=() declare -A FETCH_DEPS=() # prog -> 1: resolve via sbopkg in-container # Is a prog already present in the container base? Overridden at build time to # consult the image; in resolution we treat "in base" as a callback so the pure # topo logic stays testable. Default: not in base (self-check has no base db). installed_in_base() { return 1; } _resolve_visit() { local dir="$1" parent="$2" local key; key="$(basename "$dir")" if [[ "${_vstate[$dir]:-}" == "1" ]]; then return 0; fi if [[ "${_vstate[$dir]:-}" == "0" ]]; then CYCLES+=("cycle involving $key (pulled in via $parent)") return 1 fi _vstate["$dir"]=0 local info="$dir/$(basename "$dir").info" local req tok depdir rc=0 req="$(read_requires "$info")" for tok in $req; do if [[ "$tok" == "%README%" ]]; then HAS_README["$dir"]=1 continue fi tok="$(apply_rename "$tok")" 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 RESOLVED_ORDER=("${keep[@]:-}") } main() { parse_args "$@" init_color require_config SBO_TREE_ROOTS=("$ACTIVE_TREE") load_overrides } main "$@"