summaryrefslogtreecommitdiffstats
path: root/image-builder/build-full-image.sh
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-14 16:59:44 +0200
committerDanilo M. <danix@danix.xyz>2026-07-14 16:59:44 +0200
commit3cd610c212076d438f2d1103e0fc584a310011d5 (patch)
treeb82bd71ff139408dc84f685345862f944653966b /image-builder/build-full-image.sh
downloadsbo-dockerbuild-3cd610c212076d438f2d1103e0fc584a310011d5.tar.gz
sbo-dockerbuild-3cd610c212076d438f2d1103e0fc584a310011d5.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'image-builder/build-full-image.sh')
-rwxr-xr-ximage-builder/build-full-image.sh245
1 files changed, 245 insertions, 0 deletions
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. <danix@danix.xyz>
+#
+# 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" <<DOCKERFILE
+FROM ${BASE_TAG}
+LABEL maintainer="Eric Hameleers <alien@slackware.com>"
+
+# Import the Slackware GPG key against THIS mirror first: without it slackpkg
+# cannot verify CHECKSUMS.md5 and silently installs almost nothing (only the
+# packages already in base). 'yes YES |' answers the import prompt.
+RUN yes YES | LC_ALL=C slackpkg -batch=on -default_answer=y update gpg
+
+# Update the slackpkg package list before installing so that we always
+# get the versions current at build time, not the versions cached in the
+# base image.
+RUN LC_ALL=C slackpkg -batch=on -default_answer=y update
+
+# Download and install each individual package (thanks aclemons):
+RUN sed -i 's/DOWNLOAD_ALL=on/DOWNLOAD_ALL=off/' /etc/slackpkg/slackpkg.conf
+
+# We don't care about a big firmware package:
+RUN mv /etc/slackpkg/blacklist{,.keep} && \\
+ echo kernel-firmware > /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}"