#!/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 PROJECT_VERSION="1.0.0" # bump via sed across all scripts; see CLAUDE.md Releases 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 -V) echo "build-full-image.sh $PROJECT_VERSION"; exit 0 ;; --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}"