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:
13
linux/build/.gitignore
vendored
Normal file
13
linux/build/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Build outputs and intermediate state — never committed.
|
||||
output/
|
||||
cache/
|
||||
chroot/
|
||||
binary/
|
||||
.build/
|
||||
|
||||
# diffoscope reports from the diagnose-divergence script.
|
||||
_divergence-*/
|
||||
|
||||
# Local configuration overrides for developer experimentation.
|
||||
config/local-*
|
||||
*.local
|
||||
116
linux/build/README.md
Normal file
116
linux/build/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# SilverMetal Linux — reproducible ISO build pipeline
|
||||
|
||||
**Milestone**: Phase 1 / M1.1 — *Kicksecure fork builds reproducibly*.
|
||||
**Exit criterion**: two clean builds of the same commit produce a byte-identical SHA256.
|
||||
|
||||
This directory holds everything that turns a SilverMetal commit into a SilverMetal Linux ISO. M1.1 ships only the *base* (un-hardened) Kicksecure derivative. Hardening overlay, kernel swap, AppArmor profiles, etc. land in M1.2+ and **must not** be added here in the M1.1 PR.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
linux/build/
|
||||
├── README.md (this file)
|
||||
├── derivative-maker/ git submodule -> Kicksecure/derivative-maker
|
||||
├── config/
|
||||
│ ├── silvermetal-base.conf derivative selection + branding
|
||||
│ ├── snapshot-pin.env pinned snapshot.debian.org timestamp
|
||||
│ └── source-date-epoch.env optional SOURCE_DATE_EPOCH override
|
||||
├── docker/
|
||||
│ └── Dockerfile.builder pinned debian:bookworm-slim builder image
|
||||
└── scripts/
|
||||
├── build.sh wrapper: container run -> derivative-maker
|
||||
├── verify-reproducibility.sh build twice, compare SHA256
|
||||
└── diagnose-divergence.sh diffoscope on mismatch
|
||||
```
|
||||
|
||||
## How reproducibility is achieved
|
||||
|
||||
The same levers any deterministic Debian build relies on, stacked together:
|
||||
|
||||
| Lever | Where it lives |
|
||||
|----------------------------------------|-----------------------------------------------|
|
||||
| Pinned `snapshot.debian.org` mirror | `config/snapshot-pin.env` |
|
||||
| `SOURCE_DATE_EPOCH` from commit time | `scripts/build.sh` (auto) |
|
||||
| Pinned builder image (by digest) | `docker/Dockerfile.builder` + `BUILDER_IMAGE` |
|
||||
| Deterministic `mksquashfs` flags | `MKSQUASHFS_OPTIONS` in base conf |
|
||||
| Pinned upstream toolchain | `derivative-maker/` submodule |
|
||||
| `LC_ALL=C.UTF-8`, `TZ=UTC` | `scripts/build.sh` |
|
||||
|
||||
`diffoscope` is the *diagnostic* tool used by `diagnose-divergence.sh`; the gate itself is plain `sha256sum`.
|
||||
|
||||
## Reproduce a release locally
|
||||
|
||||
> Procedure mirrors `docs/trust-model.md` § *Reproducible builds*.
|
||||
|
||||
Prerequisites: a Linux host (or WSL2) with Docker, ~30 GB free disk, ~8 GB RAM.
|
||||
|
||||
```bash
|
||||
# 1. Clone the repo at the release tag.
|
||||
git clone --recurse-submodules https://git.silverlabs.uk/SilverLABS/SilverMetal.git
|
||||
cd SilverMetal
|
||||
git checkout v1.1.0 # whichever release you want to verify
|
||||
|
||||
# 2. Build twice and compare. ~60-90 minutes per build.
|
||||
linux/build/scripts/verify-reproducibility.sh
|
||||
|
||||
# 3. Compare against the published release.
|
||||
sha256sum -c <(curl -fsSL https://git.silverlabs.uk/SilverLABS/SilverMetal/releases/download/v1.1.0/SHA256SUMS)
|
||||
```
|
||||
|
||||
Mismatch with the published artefact = supply-chain anomaly. Report channel: `security@silverlabs.uk`.
|
||||
|
||||
## Build once (no reproducibility check)
|
||||
|
||||
```bash
|
||||
linux/build/scripts/build.sh
|
||||
# Output lands in linux/build/output/<short-sha>/
|
||||
```
|
||||
|
||||
The wrapper requires `BUILDER_IMAGE` to be pinned by digest. Local dev that hasn't built and pushed an image yet should override:
|
||||
|
||||
```bash
|
||||
BUILDER_IMAGE=docker-registry:5000/silvermetal-builder@sha256:<digest> \
|
||||
linux/build/scripts/build.sh
|
||||
```
|
||||
|
||||
## Gitea Actions
|
||||
|
||||
The CI workflow (`.gitea/workflows/build-iso-linux.yaml`) is the authority for "did this commit build reproducibly?". It:
|
||||
|
||||
1. Checks out the commit with submodules.
|
||||
2. Runs `build.sh` twice in `${GITHUB_WORKSPACE}/build-{a,b}`.
|
||||
3. Fails the run if the two ISO SHA256s differ, and uploads a `diffoscope` report as an artefact.
|
||||
4. On a tag push, attaches the verified ISO + `SHA256SUMS` + `BUILD_INFO` to a Gitea release.
|
||||
|
||||
### Self-hosted runner setup
|
||||
|
||||
The workflow runs on `runs-on: silvermetal-builder`, a self-hosted, privileged-capable Gitea Actions runner. Create it before merging the workflow:
|
||||
|
||||
1. Provision a Debian 12 VM on the cluster with ≥ 8 vCPU, ≥ 16 GB RAM, ≥ 100 GB disk.
|
||||
2. Install Docker (`apt install docker.io`); ensure the runner user can run `docker run --privileged`.
|
||||
3. Register `act_runner` against `git.silverlabs.uk` with label `silvermetal-builder`.
|
||||
4. Pre-pull the builder image so the first reproducibility run isn't a cold start:
|
||||
`docker pull docker-registry:5000/silvermetal-builder:latest`
|
||||
5. Cache the apt snapshot in a Docker volume to avoid throttling:
|
||||
`docker volume create silvermetal-apt-cache`
|
||||
|
||||
The runner host name **must not** leak into ISO content. `LC_ALL=C.UTF-8` and a constant `TZ` in the wrapper guard against that, but spot-check with `diagnose-divergence.sh`.
|
||||
|
||||
## Bumping pinned inputs
|
||||
|
||||
Each of these is a deliberate, reviewed action — never automate:
|
||||
|
||||
- **`derivative-maker` submodule** — bump in its own PR, with a verification log showing two clean builds match.
|
||||
- **`snapshot-pin.env`** — same procedure.
|
||||
- **Builder image (`Dockerfile.builder` digest)** — rebuild, push, update `BUILDER_IMAGE` in `build.sh`, run reproducibility check, commit all four together.
|
||||
|
||||
## What this milestone is *not*
|
||||
|
||||
- No hardening overlay (M1.2)
|
||||
- No SilverBrowser/SilverVPN/SilverSync/SilverChat integration (M1.6–1.9)
|
||||
- No installer branding (M1.5)
|
||||
- No update server (M1.10)
|
||||
- No SBOM publication (M1.11)
|
||||
- No signing ceremony / MOK / Secure Boot wiring (separate milestone)
|
||||
|
||||
If a change to this directory expands its scope into one of those, push back — the M1.1 gate is intentionally narrow.
|
||||
46
linux/build/config/silvermetal-base.conf
Normal file
46
linux/build/config/silvermetal-base.conf
Normal file
@@ -0,0 +1,46 @@
|
||||
# SilverMetal Linux — Base build configuration (M1.1)
|
||||
#
|
||||
# Sourced by linux/build/scripts/build.sh inside the builder container.
|
||||
# Minimal SilverMetal-specific overrides on top of Kicksecure's
|
||||
# derivative-maker. NO hardening overlay, NO kernel swap, NO package
|
||||
# additions — that work is M1.2 and later.
|
||||
#
|
||||
# Bash-sourceable. Use POSIX-quoted values; no command substitution.
|
||||
|
||||
# --- Derivative selection ---------------------------------------------------
|
||||
DERIVATIVE_NAME="silvermetal-linux-base"
|
||||
DERIVATIVE_DIST="bookworm"
|
||||
DERIVATIVE_TARGET_ARCH="amd64"
|
||||
DERIVATIVE_BUILD_TARGET="iso"
|
||||
|
||||
# Kicksecure's derivative-maker exposes "build flavour" as the upstream
|
||||
# selector. We ride on the plain Kicksecure CLI flavour here. M1.2 will
|
||||
# switch this to a SilverMetal-Hardened flavour with our overlay.
|
||||
DERIVATIVE_FLAVOUR="kicksecure-cli"
|
||||
|
||||
# --- Branding (reads shared/branding/linux-iso-meta.yaml at script time) ----
|
||||
# These mirror the YAML; the wrapper script reconciles them so we don't have
|
||||
# two sources of truth for the same value. If they diverge, build.sh fails.
|
||||
BRANDING_META_FILE="shared/branding/linux-iso-meta.yaml"
|
||||
BRANDING_ID="silvermetal-linux-base"
|
||||
BRANDING_VERSION="1.1.0-alpha"
|
||||
BRANDING_ISO_LABEL="SILVERMETAL_LINUX_BASE"
|
||||
|
||||
# --- Reproducibility levers -------------------------------------------------
|
||||
# Set/overridden by build.sh; declared here so a stray invocation fails loudly
|
||||
# rather than silently picking up the host's clock / mirror.
|
||||
: "${SOURCE_DATE_EPOCH:?SOURCE_DATE_EPOCH must be set by build.sh}"
|
||||
: "${SNAPSHOT_TIMESTAMP:?SNAPSHOT_TIMESTAMP must be set by build.sh}"
|
||||
|
||||
# snapshot.debian.org URL pattern — derivative-maker honours APT_SNAPSHOT_URL
|
||||
# when present; if upstream renames it, update both here and build.sh.
|
||||
APT_SNAPSHOT_URL="https://snapshot.debian.org/archive/debian/${SNAPSHOT_TIMESTAMP}"
|
||||
APT_SECURITY_SNAPSHOT_URL="https://snapshot.debian.org/archive/debian-security/${SNAPSHOT_TIMESTAMP}"
|
||||
|
||||
# Deterministic squashfs flags. -no-exports kills inode-export tables
|
||||
# (host-dependent). -no-xattrs kills xattr ordering noise.
|
||||
MKSQUASHFS_OPTIONS="-no-exports -no-xattrs -reproducible -all-root -wildcards"
|
||||
|
||||
# --- Output -----------------------------------------------------------------
|
||||
# Relative to repo root; build.sh moves artefacts here from the container.
|
||||
OUTPUT_DIR="linux/build/output"
|
||||
14
linux/build/config/snapshot-pin.env
Normal file
14
linux/build/config/snapshot-pin.env
Normal file
@@ -0,0 +1,14 @@
|
||||
# Pinned snapshot.debian.org timestamp.
|
||||
#
|
||||
# This is the single value that determines which apt package versions land in
|
||||
# the build. Bumping it is a deliberate, reviewed action — never automate it.
|
||||
#
|
||||
# Format: YYYYMMDDTHHMMSSZ (UTC, ISO 8601 basic, snapshot.debian.org compatible)
|
||||
#
|
||||
# To bump:
|
||||
# 1. Pick a new timestamp from https://snapshot.debian.org/
|
||||
# 2. Run a full reproducibility check with the new value
|
||||
# 3. Commit the bump in its own PR with the verification log
|
||||
#
|
||||
# Initial pin: bookworm point-release era (M1.1 implementation date).
|
||||
SNAPSHOT_TIMESTAMP=20260415T000000Z
|
||||
14
linux/build/config/source-date-epoch.env
Normal file
14
linux/build/config/source-date-epoch.env
Normal file
@@ -0,0 +1,14 @@
|
||||
# SOURCE_DATE_EPOCH source-of-truth.
|
||||
#
|
||||
# By default build.sh derives SOURCE_DATE_EPOCH from `git log -1 --pretty=%ct`
|
||||
# of the build commit. That is the reproducible-by-default mode and what CI
|
||||
# uses.
|
||||
#
|
||||
# This file exists to allow a *deliberate* override for offline/local rebuilds
|
||||
# of historical commits where git history is not available (e.g. building
|
||||
# from a release tarball). When set here, build.sh prefers this value over
|
||||
# the git-derived one and prints a warning.
|
||||
#
|
||||
# Leave commented out for normal use.
|
||||
#
|
||||
# SOURCE_DATE_EPOCH_OVERRIDE=1735689600
|
||||
1
linux/build/derivative-maker
Submodule
1
linux/build/derivative-maker
Submodule
Submodule linux/build/derivative-maker added at 51358698cd
26
linux/build/derivative-maker.PIN.md
Normal file
26
linux/build/derivative-maker.PIN.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# derivative-maker submodule pin
|
||||
|
||||
The `derivative-maker/` submodule is pinned to a specific Kicksecure release tag. This is a deliberate, reviewed action — never auto-bump.
|
||||
|
||||
## Current pin
|
||||
|
||||
| Field | Value |
|
||||
|-------------------|----------------------------------------------------------------|
|
||||
| Upstream | https://github.com/Kicksecure/derivative-maker |
|
||||
| Tag | `18.1.7.4-developers-only` |
|
||||
| Mirror (optional) | https://git.silverlabs.uk/SilverLABS/derivative-maker (mirror) |
|
||||
|
||||
> Note: Kicksecure tags every developer iteration with the `-developers-only` suffix; this is their normal release convention, not a "use at your own risk" warning. Users of Kicksecure track this same tag space.
|
||||
|
||||
## Bumping the pin
|
||||
|
||||
1. Pick the new tag: `git -C linux/build/derivative-maker fetch --tags`
|
||||
2. `git -C linux/build/derivative-maker checkout <new-tag>`
|
||||
3. From the repo root: `git add linux/build/derivative-maker`
|
||||
4. Run `linux/build/scripts/verify-reproducibility.sh` to completion (must pass).
|
||||
5. Commit the bump on its own — *do not* combine with feature work.
|
||||
6. Open the PR with the verification log attached.
|
||||
|
||||
## Why a pin (and not "track main")
|
||||
|
||||
Reproducibility requires every input to the build to be content-addressed. A floating submodule pointer would break the M1.1 exit criterion the moment upstream pushes a commit between two CI runs.
|
||||
67
linux/build/docker/Dockerfile.builder
Normal file
67
linux/build/docker/Dockerfile.builder
Normal file
@@ -0,0 +1,67 @@
|
||||
# SilverMetal Linux — reproducible-build runner image.
|
||||
#
|
||||
# This image is the "build host" for the ISO. Pinning it by digest is the
|
||||
# only thing keeping host-toolchain drift out of the reproducibility gate, so
|
||||
# do NOT replace the FROM line with a tag-only reference.
|
||||
#
|
||||
# Build & push (run from repo root):
|
||||
# docker build \
|
||||
# -f linux/build/docker/Dockerfile.builder \
|
||||
# -t docker-registry:5000/silvermetal-builder:<commit> \
|
||||
# -t docker-registry:5000/silvermetal-builder:latest \
|
||||
# linux/build/docker
|
||||
# docker push docker-registry:5000/silvermetal-builder:<commit>
|
||||
#
|
||||
# To bump the base image: replace the digest, rebuild, push, update
|
||||
# BUILDER_IMAGE in linux/build/scripts/build.sh, run a full reproducibility
|
||||
# check, commit all four changes together.
|
||||
|
||||
# debian:bookworm-slim — pinned by digest.
|
||||
# TODO(M1.1): replace placeholder digest with the actual one resolved at
|
||||
# image-build time. The placeholder is intentionally invalid so a build that
|
||||
# forgets to update it fails fast rather than silently using "latest".
|
||||
FROM debian:bookworm-slim@sha256:0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# Reproducibility-friendly apt configuration.
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
LC_ALL=C.UTF-8 \
|
||||
LANG=C.UTF-8 \
|
||||
SOURCE_DATE_EPOCH=0
|
||||
|
||||
# Pinned package versions. These come from the same snapshot.debian.org
|
||||
# timestamp as the ISO build, so a Dockerfile rebuild against that snapshot
|
||||
# produces the same toolchain bit-for-bit. The actual snapshot URL is
|
||||
# substituted at build time via --build-arg APT_SNAPSHOT_URL=...
|
||||
ARG APT_SNAPSHOT_URL="https://snapshot.debian.org/archive/debian/20260415T000000Z"
|
||||
ARG APT_SECURITY_SNAPSHOT_URL="https://snapshot.debian.org/archive/debian-security/20260415T000000Z"
|
||||
|
||||
RUN set -eux; \
|
||||
rm -f /etc/apt/sources.list.d/*; \
|
||||
printf 'deb [check-valid-until=no] %s bookworm main\n' "$APT_SNAPSHOT_URL" > /etc/apt/sources.list; \
|
||||
printf 'deb [check-valid-until=no] %s bookworm-security main\n' "$APT_SECURITY_SNAPSHOT_URL" >> /etc/apt/sources.list; \
|
||||
apt-get -o Acquire::Check-Valid-Until=false update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
debootstrap \
|
||||
diffoscope-minimal \
|
||||
dosfstools \
|
||||
git \
|
||||
gnupg \
|
||||
isolinux \
|
||||
live-build \
|
||||
mtools \
|
||||
reprepro \
|
||||
rsync \
|
||||
squashfs-tools \
|
||||
syslinux-common \
|
||||
xorriso; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root user for the parts of the build that don't need privilege.
|
||||
# live-build itself still needs root inside the container for chroot/mount,
|
||||
# so build.sh runs the container as root; this user exists for diagnostic
|
||||
# tooling and matches uid 1000 to play nicely with bind mounts.
|
||||
RUN useradd --uid 1000 --create-home --shell /bin/bash builder
|
||||
|
||||
WORKDIR /work
|
||||
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