summaryrefslogtreecommitdiffstats
path: root/image-builder/build-full-image.sh
blob: a2fdb180ca2b388bf520dc4a11518573715713d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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}"