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 --- image-builder/build-full-image.sh | 245 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100755 image-builder/build-full-image.sh (limited to 'image-builder/build-full-image.sh') diff --git a/image-builder/build-full-image.sh b/image-builder/build-full-image.sh new file mode 100755 index 0000000..a2fdb18 --- /dev/null +++ b/image-builder/build-full-image.sh @@ -0,0 +1,245 @@ +#!/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. +# +# build-full-image.sh — sbo-full:{ver} FROM sbo-base:{ver}, all series. +# Adapted from forge slackware/docker-images. No root needed. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +source "${HERE}/config" +LOG_TAG=full +source "${HERE}/lib.sh" + +BASE_IMAGE="${REGISTRY}/sbo-base" +FULL_IMAGE="${REGISTRY}/sbo-full" +DIGEST_LABEL="sbo.full.base-digest" + +# ============================================================================ +# Argument parsing +# ============================================================================ + +FORCE=false +OPT_VERSION="" +while [[ $# -gt 0 ]]; do + case "$1" in + --version) OPT_VERSION="$2"; shift 2 ;; + --force) FORCE=true; shift ;; + *) _err "unknown argument: $1" ;; + esac +done +if [[ -n "${OPT_VERSION}" ]]; then + BUILD_VARIANTS=("${OPT_VERSION}") +else + BUILD_VARIANTS=("${VARIANTS[@]}") +fi + +# ============================================================================ +# Tag derivation +# ============================================================================ + +# derive_tags VERSION +# Sets globals BASE_TAG and FULL_TAG for the given variant. +derive_tags() { + local VERSION="$1" + BASE_TAG="${BASE_IMAGE}:${VERSION}" + FULL_TAG="${FULL_IMAGE}:${VERSION}" +} + +# ============================================================================ +# Rebuild detection +# +# After pulling the latest base image, its digest is compared with the +# digest stored in the DIGEST_LABEL on the current full image. If +# they differ the base image has been updated and the full must be +# rebuilt to incorporate slackpkg package updates shipped with the new base. +# ============================================================================ + +needs_rebuild() { + local BASE_TAG="$1" FULL_TAG="$2" + + if [[ "${FORCE}" == "true" ]]; then + _log " --force specified; rebuilding unconditionally." + return 0 + fi + + # Digest of the base image we just pulled + local BASE_DIGEST + BASE_DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' \ + "${BASE_TAG}" 2>/dev/null || echo "") + if [[ -z "${BASE_DIGEST}" ]]; then + _warn " Could not determine base image digest; rebuilding to be safe." + return 0 + fi + + # Digest recorded in the full image's label (empty if image doesn't exist) + local RECORDED_DIGEST + RECORDED_DIGEST=$(docker inspect \ + --format "{{index .Config.Labels \"${DIGEST_LABEL}\"}}" \ + "${FULL_TAG}" 2>/dev/null || echo "") + + if [[ "${BASE_DIGEST}" == "${RECORDED_DIGEST}" ]]; then + _log " Base image unchanged (${BASE_DIGEST:0:40}...); skipping." + return 1 + fi + + _log " Base image changed; rebuilding." + _log " Was : ${RECORDED_DIGEST:0:60}..." + _log " Now : ${BASE_DIGEST:0:60}..." + return 0 +} + +# ============================================================================ +# Build one variant +# ============================================================================ + +build_variant() { + local VERSION="$1" + local REPO_KEY="slackware64-${VERSION}" + require_mount "${VERSION}" # NFS must be up: we serve it over HTTP below + derive_tags "${VERSION}" + + _log "=== ${FULL_TAG} ===" + + # Pull the latest base image so digest comparison is against the current + # state of the registry, not a stale local cache. + _log " Pulling base image ${BASE_TAG}..." + docker pull "${BASE_TAG}" + + if ! needs_rebuild "${BASE_TAG}" "${FULL_TAG}"; then + return 0 + fi + + # Record the base image digest for the label + local BASE_DIGEST + BASE_DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "${BASE_TAG}") + + local WORKDIR + WORKDIR="$(mktemp -d /tmp/slackware-full.XXXXXX)" + + # Serve the NFS mirror over HTTP on the docker bridge for the duration of + # the build, so slackpkg in the build container fetches packages from LAN + # instead of the internet. Killed (with WORKDIR cleanup) on any exit. + local MIRROR_ROOT; MIRROR_ROOT="$(strip_scheme "$(mirror_path "${MIRROR}" "${REPO_KEY}")")" + [[ -f "${MIRROR_ROOT}/PACKAGES.TXT" ]] \ + || _err "mirror root has no PACKAGES.TXT: ${MIRROR_ROOT}" + python3 -m http.server "${HTTP_MIRROR_PORT}" \ + --bind "${HTTP_MIRROR_HOST}" --directory "${MIRROR_ROOT}" \ + >/dev/null 2>&1 & + local HTTP_PID=$! + # shellcheck disable=SC2064 + trap "kill ${HTTP_PID} 2>/dev/null; rm -rf '${WORKDIR}'" EXIT + # Wait for the server to accept connections before building. + local i + for i in 1 2 3 4 5 6 7 8 9 10; do + curl -sf -o /dev/null "http://${HTTP_MIRROR_HOST}:${HTTP_MIRROR_PORT}/PACKAGES.TXT" && break + [[ $i -eq 10 ]] && _err "HTTP mirror did not come up on ${HTTP_MIRROR_HOST}:${HTTP_MIRROR_PORT}" + sleep 0.5 + done + + # Generate the Dockerfile for this variant + cat > "${WORKDIR}/Dockerfile" < /etc/slackpkg/blacklist + +# Install the full Slackware: +# All packages are installed in a single RUN layer to keep the image lean. +RUN export LC_ALL=C && \\ + for series in a ap d e f k kde l n t tcl x xap xfce y ; do \\ + slackpkg -delall=on -batch=on -default_answer=y install "\$series"/* ; done + +# And back to defaults: +RUN sed -i 's/DOWNLOAD_ALL=off/DOWNLOAD_ALL=on/' /etc/slackpkg/slackpkg.conf +RUN mv /etc/slackpkg/blacklist{.keep,} + + +# Delete package cache: +RUN rm -rf /var/cache/packages/* + +# Rebuild the linker cache after adding new libraries +RUN /sbin/ldconfig + +LABEL org.opencontainers.image.created="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +LABEL org.opencontainers.image.title="Slackware Linux ${VERSION} full (x86_64)" +LABEL org.opencontainers.image.description="Slackware Linux ${VERSION} full image with all packages installed (x86_64)" +LABEL org.opencontainers.image.vendor="Slackware Linux" +LABEL org.opencontainers.image.licenses="GPL-2.0-or-later AND LGPL-2.1-or-later" +LABEL org.opencontainers.image.url="http://www.slackware.com" +LABEL org.opencontainers.image.source="https://forge.slackware.nl/slackware/docker-images" +LABEL org.opencontainers.image.revision="${VERSION}" +LABEL slackware.version="${VERSION}" +LABEL slackware.arch="x86_64" +LABEL ${DIGEST_LABEL}="${BASE_DIGEST}" +CMD ["/bin/bash"] +DOCKERFILE + + local BUILD_FLAGS=() + [[ "${FORCE}" == "true" ]] && BUILD_FLAGS+=(--no-cache) + + # Explicit exit checks: build_variant runs in an `if ! (...)` condition + # (see Main), which suppresses `set -e` inside this subshell, so a failed + # docker build would otherwise fall through to push a nonexistent tag and + # log "Done". Check each step and return non-zero on failure. + _log " Building ${FULL_TAG}..." + if ! docker build "${BUILD_FLAGS[@]}" \ + -t "${FULL_TAG}" \ + "${WORKDIR}"; then + _warn " build failed for ${FULL_TAG}; not pushing." + return 1 + fi + + _log " Pushing ${FULL_TAG}..." + if ! docker push "${FULL_TAG}"; then + _warn " push failed for ${FULL_TAG}." + return 1 + fi + + _log "=== Done: ${FULL_TAG} ===" +} + +# ============================================================================ +# Main +# ============================================================================ + +# A --force build passes --no-cache, so the stale build cache is dead weight: +# it still loads the containerd snapshotter's lease/lock bookkeeping and has +# raced the layer export ("failed to open writer: ref ... locked ... unavailable"). +# Prune it first to shrink that surface. Best-effort; never fail the build on it. +if [[ "${FORCE}" == "true" ]]; then + _log " --force: pruning build cache before rebuild" + docker builder prune -f >/dev/null 2>&1 || _warn " builder prune failed (ignored)" +fi + +rc=0 +for VERSION in "${BUILD_VARIANTS[@]}"; do + if ! ( build_variant "${VERSION}" ); then + _warn "variant ${VERSION} failed; continuing." + rc=1 + fi +done +exit "${rc}" -- cgit v1.2.3