feat(linux/build): scaffold reproducible ISO build pipeline (M1.1)
Vendors Kicksecure derivative-maker as a pinned submodule (18.1.7.4), adds the wrapper + verify + diagnose scripts, the pinned builder image, and the reproducibility-gated Gitea Actions workflow. Base flavour only — no hardening overlay (that's M1.2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
140
linux/build/scripts/build.sh
Executable file
140
linux/build/scripts/build.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# SilverMetal Linux — ISO build wrapper.
|
||||
#
|
||||
# Runs the Kicksecure derivative-maker inside the pinned builder container
|
||||
# with the reproducibility levers locked down. This script is the single
|
||||
# entry point for both local developer builds and CI — there is no separate
|
||||
# CI-only path. If you need to debug, run *this*, not lb directly.
|
||||
#
|
||||
# Usage:
|
||||
# linux/build/scripts/build.sh # writes to linux/build/output/<commit>
|
||||
# BUILD_DIR=/tmp/build-a linux/build/scripts/build.sh # override output root
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 ISO produced and SHA256SUMS written
|
||||
# 1 argument / environment error
|
||||
# 2 derivative-maker submodule missing
|
||||
# 3 build failed
|
||||
# 4 post-build hash/manifest step failed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Locate repo root -------------------------------------------------------
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# --- Pinned builder image ---------------------------------------------------
|
||||
# Bumped together with linux/build/docker/Dockerfile.builder. The digest form
|
||||
# is required; refusing the tag-only form is what stops a silent host drift.
|
||||
BUILDER_IMAGE="${BUILDER_IMAGE:-docker-registry:5000/silvermetal-builder@sha256:REPLACE_WITH_PUSHED_DIGEST}"
|
||||
|
||||
if [[ "${BUILDER_IMAGE}" != *"@sha256:"* ]]; then
|
||||
echo "build.sh: BUILDER_IMAGE must be pinned by digest, got: ${BUILDER_IMAGE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Sanity: submodule present ---------------------------------------------
|
||||
if [[ ! -f "linux/build/derivative-maker/.git" && ! -d "linux/build/derivative-maker/.git" ]]; then
|
||||
echo "build.sh: linux/build/derivative-maker submodule is not initialised." >&2
|
||||
echo " Run: git submodule update --init --recursive" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# --- Compute SOURCE_DATE_EPOCH ---------------------------------------------
|
||||
# Order of preference:
|
||||
# 1. Explicit env var passed in (CI may set it for cross-runner consistency)
|
||||
# 2. config/source-date-epoch.env override (offline rebuilds)
|
||||
# 3. git commit timestamp of HEAD (default)
|
||||
# shellcheck disable=SC1091
|
||||
source linux/build/config/source-date-epoch.env || true
|
||||
if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then
|
||||
if [[ -n "${SOURCE_DATE_EPOCH_OVERRIDE:-}" ]]; then
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_OVERRIDE}"
|
||||
echo "build.sh: using SOURCE_DATE_EPOCH override = ${SOURCE_DATE_EPOCH}"
|
||||
else
|
||||
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
|
||||
fi
|
||||
fi
|
||||
export SOURCE_DATE_EPOCH
|
||||
|
||||
# --- Pinned snapshot timestamp ---------------------------------------------
|
||||
# shellcheck disable=SC1091
|
||||
source linux/build/config/snapshot-pin.env
|
||||
export SNAPSHOT_TIMESTAMP
|
||||
|
||||
# --- Resolve commit & output dir -------------------------------------------
|
||||
COMMIT_SHA="$(git rev-parse --short=12 HEAD)"
|
||||
BUILD_DIR="${BUILD_DIR:-${REPO_ROOT}/linux/build/output/${COMMIT_SHA}}"
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
|
||||
echo "build.sh: commit=${COMMIT_SHA} epoch=${SOURCE_DATE_EPOCH} snapshot=${SNAPSHOT_TIMESTAMP}"
|
||||
echo "build.sh: output -> ${BUILD_DIR}"
|
||||
|
||||
# --- Run the build inside the container ------------------------------------
|
||||
# --privileged is required because live-build mounts loop devices and chroots.
|
||||
# --network=host lets the container reach snapshot.debian.org without us
|
||||
# fighting CI proxy config; tighten if/when that becomes a concern.
|
||||
docker run --rm --privileged \
|
||||
--network=host \
|
||||
-e SOURCE_DATE_EPOCH \
|
||||
-e SNAPSHOT_TIMESTAMP \
|
||||
-e LC_ALL=C.UTF-8 \
|
||||
-e LANG=C.UTF-8 \
|
||||
-e TZ=UTC \
|
||||
-v "${REPO_ROOT}:/work:rw" \
|
||||
-v "${BUILD_DIR}:/out:rw" \
|
||||
-w /work \
|
||||
"${BUILDER_IMAGE}" \
|
||||
bash -euo pipefail -c '
|
||||
# shellcheck disable=SC1091
|
||||
source /work/linux/build/config/silvermetal-base.conf
|
||||
|
||||
cd /work/linux/build/derivative-maker
|
||||
|
||||
./derivative-maker \
|
||||
--build \
|
||||
--target "${DERIVATIVE_BUILD_TARGET}" \
|
||||
--flavour "${DERIVATIVE_FLAVOUR}" \
|
||||
--arch "${DERIVATIVE_TARGET_ARCH}" \
|
||||
--dist "${DERIVATIVE_DIST}" \
|
||||
--config /work/linux/build/config/silvermetal-base.conf
|
||||
|
||||
# derivative-maker writes into its own build/ dir; collect into /out.
|
||||
# Exact upstream output paths can shift between tags — keep this
|
||||
# tolerant. Anything matching *.iso under the tree is what we want.
|
||||
find . -maxdepth 6 -type f -name "*.iso" -print0 \
|
||||
| xargs -0 -I{} cp -av "{}" /out/
|
||||
|
||||
# Manifest of file metadata that lives inside the ISO. Useful when
|
||||
# diagnosing reproducibility regressions without re-extracting.
|
||||
find . -maxdepth 6 -type f -name "*.manifest" -print0 \
|
||||
| xargs -0 -I{} cp -av "{}" /out/ 2>/dev/null || true
|
||||
' || { echo "build.sh: derivative-maker failed"; exit 3; }
|
||||
|
||||
# --- Hash artefacts ---------------------------------------------------------
|
||||
# Run hashing on the host (not in the container) so a busted container image
|
||||
# can't tamper with the digests we publish.
|
||||
shopt -s nullglob
|
||||
ISO_FILES=("${BUILD_DIR}"/*.iso)
|
||||
shopt -u nullglob
|
||||
if (( ${#ISO_FILES[@]} == 0 )); then
|
||||
echo "build.sh: no ISO produced in ${BUILD_DIR}" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${BUILD_DIR}"
|
||||
sha256sum -- *.iso > SHA256SUMS
|
||||
cp -- "${REPO_ROOT}/linux/build/config/snapshot-pin.env" snapshot-pin.env
|
||||
{
|
||||
echo "commit=${COMMIT_SHA}"
|
||||
echo "source_date_epoch=${SOURCE_DATE_EPOCH}"
|
||||
echo "snapshot_timestamp=${SNAPSHOT_TIMESTAMP}"
|
||||
echo "builder_image=${BUILDER_IMAGE}"
|
||||
echo "host_uname=$(uname -srm)"
|
||||
} > BUILD_INFO
|
||||
)
|
||||
|
||||
echo "build.sh: SHA256SUMS:"
|
||||
cat "${BUILD_DIR}/SHA256SUMS"
|
||||
61
linux/build/scripts/diagnose-divergence.sh
Executable file
61
linux/build/scripts/diagnose-divergence.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# SilverMetal Linux — reproducibility-failure diagnostic.
|
||||
#
|
||||
# Invoked by verify-reproducibility.sh when two builds disagree, but also
|
||||
# safe to run by hand against any two ISOs:
|
||||
#
|
||||
# ISO_A=/path/a.iso ISO_B=/path/b.iso linux/build/scripts/diagnose-divergence.sh
|
||||
#
|
||||
# Produces a diffoscope report. The output is intentionally verbose — when
|
||||
# this script runs in anger we want everything we can get.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
: "${ISO_A:?ISO_A must point to the first ISO}"
|
||||
: "${ISO_B:?ISO_B must point to the second ISO}"
|
||||
|
||||
if [[ ! -f "${ISO_A}" || ! -f "${ISO_B}" ]]; then
|
||||
echo "diagnose: one of the ISOs is missing (A=${ISO_A} B=${ISO_B})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPORT_DIR="${REPORT_DIR:-${REPO_ROOT}/linux/build/output/_divergence-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||
mkdir -p "${REPORT_DIR}"
|
||||
|
||||
echo "diagnose: writing report to ${REPORT_DIR}"
|
||||
|
||||
# Quick wins first — these usually point straight at the culprit.
|
||||
sha256sum "${ISO_A}" "${ISO_B}" > "${REPORT_DIR}/sha256.txt"
|
||||
ls -la "${ISO_A}" "${ISO_B}" > "${REPORT_DIR}/sizes.txt" 2>&1 || true
|
||||
|
||||
# diffoscope — html if available (richer), text always.
|
||||
if command -v diffoscope >/dev/null 2>&1; then
|
||||
diffoscope --max-report-size 100000000 \
|
||||
--html "${REPORT_DIR}/diff.html" \
|
||||
--text "${REPORT_DIR}/diff.txt" \
|
||||
"${ISO_A}" "${ISO_B}" \
|
||||
|| true # non-zero exit just means "they differ"; that's why we're here
|
||||
elif command -v cmp >/dev/null 2>&1; then
|
||||
echo "diagnose: diffoscope not found, falling back to cmp" >&2
|
||||
cmp -l "${ISO_A}" "${ISO_B}" > "${REPORT_DIR}/cmp.txt" || true
|
||||
fi
|
||||
|
||||
# A first guess at the culprit, even when the diff is huge.
|
||||
{
|
||||
echo "## Likely-culprit checklist"
|
||||
echo ""
|
||||
echo "Walk these in order — most failures fall into the first two."
|
||||
echo ""
|
||||
echo " [ ] SOURCE_DATE_EPOCH was identical in both builds (compare BUILD_INFO files)"
|
||||
echo " [ ] snapshot.debian.org timestamp matched (compare snapshot-pin.env files)"
|
||||
echo " [ ] Same builder image digest (compare BUILD_INFO files)"
|
||||
echo " [ ] mksquashfs reproducibility flags survived (-no-exports -no-xattrs -reproducible)"
|
||||
echo " [ ] No build-id randomisation in kernel/initrd (look for differing .note.gnu.build-id)"
|
||||
echo " [ ] No host hostname/username leakage (grep for the runner host name)"
|
||||
echo " [ ] No locale drift (LC_ALL=C.UTF-8 enforced in container)"
|
||||
} > "${REPORT_DIR}/checklist.md"
|
||||
|
||||
echo "diagnose: done. See ${REPORT_DIR}/checklist.md and ${REPORT_DIR}/diff.{html,txt}"
|
||||
86
linux/build/scripts/verify-reproducibility.sh
Executable file
86
linux/build/scripts/verify-reproducibility.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# SilverMetal Linux — reproducibility gate.
|
||||
#
|
||||
# Builds the ISO twice from clean clones of HEAD and compares SHA256.
|
||||
# This is the M1.1 exit-criterion check; CI runs it on every push.
|
||||
#
|
||||
# Usage:
|
||||
# linux/build/scripts/verify-reproducibility.sh
|
||||
# COMMIT=abc1234 linux/build/scripts/verify-reproducibility.sh # specific SHA
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 Both builds produced byte-identical ISOs
|
||||
# 1 Mismatch — diagnose-divergence.sh has been invoked
|
||||
# 2 Setup / clone error
|
||||
# 3 One of the builds failed before producing an ISO
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
COMMIT="${COMMIT:-$(git -C "${REPO_ROOT}" rev-parse HEAD)}"
|
||||
WORKROOT="${WORKROOT:-/tmp/silvermetal-repro-$$}"
|
||||
DIR_A="${WORKROOT}/build-a"
|
||||
DIR_B="${WORKROOT}/build-b"
|
||||
|
||||
cleanup() {
|
||||
if [[ "${KEEP_BUILD_DIRS:-0}" != "1" ]]; then
|
||||
rm -rf "${WORKROOT}"
|
||||
else
|
||||
echo "verify: kept ${WORKROOT} for inspection"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "${DIR_A}" "${DIR_B}"
|
||||
|
||||
clone_one() {
|
||||
local dest="$1"
|
||||
git clone --recurse-submodules "${REPO_ROOT}" "${dest}/repo" >/dev/null 2>&1 \
|
||||
|| { echo "verify: clone -> ${dest} failed" >&2; exit 2; }
|
||||
git -C "${dest}/repo" checkout --quiet "${COMMIT}"
|
||||
git -C "${dest}/repo" submodule update --init --recursive --quiet
|
||||
}
|
||||
|
||||
build_one() {
|
||||
local dest="$1"
|
||||
BUILD_DIR="${dest}/out" "${dest}/repo/linux/build/scripts/build.sh" \
|
||||
|| { echo "verify: build in ${dest} failed" >&2; exit 3; }
|
||||
}
|
||||
|
||||
echo "verify: cloning two copies at ${COMMIT}"
|
||||
clone_one "${DIR_A}"
|
||||
clone_one "${DIR_B}"
|
||||
|
||||
echo "verify: building copy A"
|
||||
build_one "${DIR_A}"
|
||||
|
||||
echo "verify: building copy B"
|
||||
build_one "${DIR_B}"
|
||||
|
||||
ISO_A="$(ls "${DIR_A}/out"/*.iso 2>/dev/null | head -n1 || true)"
|
||||
ISO_B="$(ls "${DIR_B}/out"/*.iso 2>/dev/null | head -n1 || true)"
|
||||
|
||||
if [[ -z "${ISO_A}" || -z "${ISO_B}" ]]; then
|
||||
echo "verify: missing ISO (A=${ISO_A:-<none>} B=${ISO_B:-<none>})" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
HASH_A="$(sha256sum "${ISO_A}" | cut -d' ' -f1)"
|
||||
HASH_B="$(sha256sum "${ISO_B}" | cut -d' ' -f1)"
|
||||
|
||||
echo "verify: A = ${HASH_A} ${ISO_A}"
|
||||
echo "verify: B = ${HASH_B} ${ISO_B}"
|
||||
|
||||
if [[ "${HASH_A}" == "${HASH_B}" ]]; then
|
||||
echo "verify: PASS — reproducible at ${COMMIT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "verify: FAIL — running diffoscope"
|
||||
KEEP_BUILD_DIRS=1
|
||||
ISO_A="${ISO_A}" ISO_B="${ISO_B}" \
|
||||
"${REPO_ROOT}/linux/build/scripts/diagnose-divergence.sh" \
|
||||
|| true
|
||||
exit 1
|
||||
Reference in New Issue
Block a user