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:
146
.gitea/workflows/build-iso-linux.yaml
Normal file
146
.gitea/workflows/build-iso-linux.yaml
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
name: Build SilverMetal Linux ISO (reproducibility-gated)
|
||||||
|
|
||||||
|
# M1.1 exit-criterion check. Builds the ISO twice from a clean checkout in
|
||||||
|
# isolated directories and gates on byte-identical SHA256. On a tag push, the
|
||||||
|
# verified ISO and its SHA256SUMS are attached to a Gitea release.
|
||||||
|
#
|
||||||
|
# The release-upload pattern (create-if-not-exists then attach asset) is
|
||||||
|
# lifted from SilverLABS/SilverVPN/.gitea/workflows/build-linux-client.yaml
|
||||||
|
# lines 77-117. Keep them in sync if either changes.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'linux/**'
|
||||||
|
- 'shared/branding/linux-iso-meta.yaml'
|
||||||
|
- '.gitea/workflows/build-iso-linux.yaml'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'linux/**'
|
||||||
|
- 'shared/branding/linux-iso-meta.yaml'
|
||||||
|
- '.gitea/workflows/build-iso-linux.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Two reproducibility-gated builds in flight at once would just compete for
|
||||||
|
# loop devices on the privileged runner. Serialise per ref.
|
||||||
|
concurrency:
|
||||||
|
group: build-iso-linux-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-verify:
|
||||||
|
# Self-hosted, privileged-capable. Setup procedure documented in
|
||||||
|
# linux/build/README.md ("Self-hosted runner setup").
|
||||||
|
runs-on: silvermetal-builder
|
||||||
|
timeout-minutes: 240
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout (with submodules)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0 # need history so SOURCE_DATE_EPOCH = HEAD commit time
|
||||||
|
|
||||||
|
- name: Show pinned inputs
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
echo "commit=$(git rev-parse HEAD)"
|
||||||
|
cat linux/build/config/snapshot-pin.env
|
||||||
|
echo "builder image:"
|
||||||
|
grep -E '^FROM |^ARG APT_SNAPSHOT_URL' linux/build/docker/Dockerfile.builder
|
||||||
|
|
||||||
|
- name: Build A
|
||||||
|
env:
|
||||||
|
BUILD_DIR: ${{ github.workspace }}/build-a
|
||||||
|
run: linux/build/scripts/build.sh
|
||||||
|
|
||||||
|
- name: Build B (clean second build)
|
||||||
|
env:
|
||||||
|
BUILD_DIR: ${{ github.workspace }}/build-b
|
||||||
|
run: linux/build/scripts/build.sh
|
||||||
|
|
||||||
|
- name: Compare SHA256
|
||||||
|
id: compare
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
A=$(sha256sum "${{ github.workspace }}/build-a"/*.iso | cut -d' ' -f1)
|
||||||
|
B=$(sha256sum "${{ github.workspace }}/build-b"/*.iso | cut -d' ' -f1)
|
||||||
|
echo "A=${A}"
|
||||||
|
echo "B=${B}"
|
||||||
|
echo "iso_sha256=${A}" >> "${GITHUB_OUTPUT}"
|
||||||
|
if [ "${A}" != "${B}" ]; then
|
||||||
|
echo "::error::ISO SHA256 mismatch — A=${A} B=${B}"
|
||||||
|
ISO_A="$(ls "${{ github.workspace }}/build-a"/*.iso | head -n1)" \
|
||||||
|
ISO_B="$(ls "${{ github.workspace }}/build-b"/*.iso | head -n1)" \
|
||||||
|
REPORT_DIR="${{ github.workspace }}/divergence" \
|
||||||
|
linux/build/scripts/diagnose-divergence.sh
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Reproducibility gate PASSED at ${A}"
|
||||||
|
|
||||||
|
- name: Upload divergence report on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: divergence-report-${{ github.run_id }}
|
||||||
|
path: ${{ github.workspace }}/divergence
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Stage release artefacts
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
mkdir -p release
|
||||||
|
cp "${{ github.workspace }}/build-a"/*.iso release/
|
||||||
|
cp "${{ github.workspace }}/build-a"/SHA256SUMS release/
|
||||||
|
cp "${{ github.workspace }}/build-a"/BUILD_INFO release/
|
||||||
|
cp "${{ github.workspace }}/build-a"/snapshot-pin.env release/snapshot-pin.env
|
||||||
|
ls -la release/
|
||||||
|
|
||||||
|
- name: Upload to Gitea release (tag only)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
TAG="${{ github.ref_name }}"
|
||||||
|
API="${{ github.server_url }}/api/v1"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
|
||||||
|
# Create-if-not-exists, then attach assets. Pattern lifted from
|
||||||
|
# SilverLABS/SilverVPN build-linux-client.yaml:89-115.
|
||||||
|
RELEASE_ID=$(curl -s "${API}/repos/${REPO}/releases/tags/${TAG}" \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
| jq -r '.id // empty' 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "0" ]; then
|
||||||
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API}/repos/${REPO}/releases" \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"SilverMetal Linux ${TAG}\",\"body\":\"Reproducibility-verified ISO. SHA256 in SHA256SUMS asset.\",\"draft\":false,\"prerelease\":true}")
|
||||||
|
HTTP_CODE=$(echo "${RESPONSE}" | tail -1)
|
||||||
|
BODY=$(echo "${RESPONSE}" | sed '$d')
|
||||||
|
if [ "${HTTP_CODE}" -ge 400 ]; then
|
||||||
|
echo "ERROR: Failed to create release (${HTTP_CODE}): ${BODY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
RELEASE_ID=$(echo "${BODY}" | jq -r '.id')
|
||||||
|
echo "Created release ID: ${RELEASE_ID}"
|
||||||
|
else
|
||||||
|
echo "Found existing release ID: ${RELEASE_ID}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for asset in release/*; do
|
||||||
|
name=$(basename "${asset}")
|
||||||
|
echo "Attaching ${name}"
|
||||||
|
curl -sf -X POST \
|
||||||
|
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${name}" \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@${asset}"
|
||||||
|
done
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "linux/build/derivative-maker"]
|
||||||
|
path = linux/build/derivative-maker
|
||||||
|
url = https://github.com/Kicksecure/derivative-maker.git
|
||||||
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
|
||||||
37
shared/branding/linux-iso-meta.yaml
Normal file
37
shared/branding/linux-iso-meta.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# SilverMetal Linux — ISO branding metadata
|
||||||
|
#
|
||||||
|
# Single source of truth for ISO label, /etc/os-release, GRUB title.
|
||||||
|
# Referenced by linux/build/config/silvermetal-base.conf and any later overlay.
|
||||||
|
# Do not duplicate these values elsewhere — read them from here.
|
||||||
|
|
||||||
|
schema_version: 1
|
||||||
|
|
||||||
|
# Phase-1 milestone 1.1 ships a *base* (un-hardened) Kicksecure derivative.
|
||||||
|
# The hardened variant lands in M1.2 and SHOULD use a different `id` so that
|
||||||
|
# os-release reflects what the user actually has installed.
|
||||||
|
id: silvermetal-linux-base
|
||||||
|
name: SilverMetal Linux (Base)
|
||||||
|
short_name: SilverMetal
|
||||||
|
pretty_name: SilverMetal Linux Base
|
||||||
|
version_codename: bookworm
|
||||||
|
home_url: https://silvermetal.silverlabs.uk
|
||||||
|
support_url: https://silvermetal.silverlabs.uk/support
|
||||||
|
bug_report_url: https://git.silverlabs.uk/SilverLABS/SilverMetal/issues
|
||||||
|
privacy_policy_url: https://silvermetal.silverlabs.uk/privacy
|
||||||
|
|
||||||
|
# Versioning: <phase>.<milestone>.<patch>[-tag]
|
||||||
|
# M1.1 produces 1.1.0-alpha builds until two clean builds match SHA256.
|
||||||
|
version: 1.1.0-alpha
|
||||||
|
|
||||||
|
# ISO9660 volume id is capped at 32 chars, uppercase, no spaces.
|
||||||
|
iso_label: SILVERMETAL_LINUX_BASE
|
||||||
|
|
||||||
|
# Used by GRUB / isolinux menu rendering.
|
||||||
|
grub_title: SilverMetal Linux (Base) — live
|
||||||
|
grub_distributor: SilverMetal
|
||||||
|
|
||||||
|
# os-release fields that aren't covered by the standardised keys above.
|
||||||
|
os_release_extra:
|
||||||
|
ANSI_COLOR: "1;36" # cyan, matches SilverLABS visual identity
|
||||||
|
LOGO: silvermetal
|
||||||
|
DOCUMENTATION_URL: https://silvermetal.silverlabs.uk/docs
|
||||||
Reference in New Issue
Block a user