From 3cd610c212076d438f2d1103e0fc584a310011d5 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 14 Jul 2026 16:59:44 +0200 Subject: initial commit: docker test-build toolchain Extracted from the .extras/ dir of the sbo-slackbuilds package repo, where it grew from a throwaway helper into a standalone tool. Git history starts fresh; the original 41-commit development log is preserved in HISTORY.md. Contents: the test-build CLI (resolves a local SlackBuild's deps from a configured SBo tree and builds it in a throwaway container, lints, caches deps), the image-builder chain that produces the images it consumes, their pure-logic self-checks, design specs/plans, an install.sh for ~/bin, and docs. Licensed GPLv2-only. Co-Authored-By: Claude Opus 4.8 --- test-build | 876 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 876 insertions(+) create mode 100755 test-build (limited to 'test-build') diff --git a/test-build b/test-build new file mode 100755 index 0000000..eb4009f --- /dev/null +++ b/test-build @@ -0,0 +1,876 @@ +#!/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. + +# ============================================================================= +# 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 +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. + --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. + --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 ;; + --keep) KEEP_TARGET=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 + + run_target "$tdir" + print_summary +} + +main "$@" -- cgit v1.2.3