-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.sh
More file actions
executable file
·364 lines (329 loc) · 15 KB
/
build.sh
File metadata and controls
executable file
·364 lines (329 loc) · 15 KB
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
#!/usr/bin/env bash
# Multi-arch boot artifact builder for the Powernode system extension.
#
# Reference: Golden Eclipse plan M3. Produces six artifact families per arch:
# 1. kernel + initramfs bundle (PXE/iPXE network boot, libvirt direct)
# 2. raw disk image (.img — USB stick / SD card / direct dd)
# 3. ISO 9660 image (.iso — DVD/USB, IPMI virtual media)
# 4. iPXE chainload script (.ipxe — network-boot entry point)
# 5. cloud qcow2 image (.qcow2 — libvirt/QEMU pre-baked rootfs)
# 6. OCI image (bootc-compatible, container-image-as-OS)
#
# Usage:
# ./build.sh --arch amd64 [--variants kernel-initrd,raw,iso,ipxe,qcow2,oci]
# ./build.sh --arch arm64 [--variants ...]
#
# All variants are built by default. Use --variants to restrict.
#
# Outputs land at: build/<arch>/<variant>/...
#
# Pinning: BASE_IMAGE_DIGEST + KERNEL_PACKAGE_VERSION + COMPOSEFS_TOOLS_VERSION
# are injected from CI workflow inputs to honor the M1 reproducibility gate.
# Re-running the build with the same pins on the same source must produce
# identical SHA-256 digests.
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly DEFAULT_VARIANTS="kernel-initrd,raw,iso,ipxe,qcow2,oci,disk-image-rpi4,disk-image-arm64-uefi"
# Sentinel: build.sh fails loudly if a real digest isn't supplied (via
# BASE_IMAGE_DIGEST env, --base-image arg, or CI workflow input). Per M3
# reproducibility, no placeholder is ever permitted past the build gate.
readonly DEFAULT_BASE_IMAGE="REQUIRED_PIN_NOT_SET"
ARCH=""
VARIANTS="${DEFAULT_VARIANTS}"
BASE_IMAGE_DIGEST="${BASE_IMAGE_DIGEST:-${DEFAULT_BASE_IMAGE}}"
OUTPUT_DIR="${OUTPUT_DIR:-${SCRIPT_DIR}/build}"
usage() {
cat <<USAGE
Usage: $(basename "$0") --arch {amd64|arm64} [options]
Required:
--arch Target architecture (amd64 or arm64)
Optional:
--variants Comma-separated list of variants to build.
Default: ${DEFAULT_VARIANTS}
--output-dir Output root (default: \${SCRIPT_DIR}/build)
--base-image Pinned base image digest (default: \$BASE_IMAGE_DIGEST env)
--help Show this help
Examples:
$(basename "$0") --arch amd64
$(basename "$0") --arch arm64 --variants kernel-initrd,iso
USAGE
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--arch) ARCH="$2"; shift 2 ;;
--variants) VARIANTS="$2"; shift 2 ;;
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
--base-image) BASE_IMAGE_DIGEST="$2"; shift 2 ;;
--help|-h) usage 0 ;;
*) echo "Unknown arg: $1" >&2; usage 1 ;;
esac
done
[[ -z "${ARCH}" ]] && { echo "ERROR: --arch is required" >&2; usage 1; }
[[ "${ARCH}" =~ ^(amd64|arm64)$ ]] || { echo "ERROR: --arch must be amd64 or arm64" >&2; exit 1; }
readonly ARCH_OUT="${OUTPUT_DIR}/${ARCH}"
mkdir -p "${ARCH_OUT}"
log() { echo "[$(date -Iseconds)] [$ARCH] $*"; }
# ── Variant: kernel + initramfs bundle ─────────────────────────────────────
build_kernel_initrd() {
log "Building kernel + initramfs (dracut)…"
local out="${ARCH_OUT}/kernel-initrd"
mkdir -p "${out}"
# Kernel module list per architecture.
local arch_conf="${SCRIPT_DIR}/dracut.conf.d/powernode-${ARCH}.conf"
local shared_conf="${SCRIPT_DIR}/dracut.conf.d/powernode.conf"
# Compose dracut module path so /sbin/powernode-agent is embedded into initramfs
# along with the powernode module-setup hook (modules.d/90powernode/).
if ! command -v dracut >/dev/null 2>&1; then
log "WARN: dracut not in PATH — emitting placeholder for offline planning"
echo "placeholder kernel for ${ARCH}" >"${out}/kernel"
echo "placeholder initramfs for ${ARCH}" >"${out}/initramfs.cpio.zst"
return 0
fi
local kver
# Prefer KERNEL_VERSION env override → running kernel → newest kernel that
# has both modules and a /boot/vmlinuz-${kver}. Avoid orphaned module dirs
# (modules present but no kernel image — common after partial kernel removals).
if [[ -n "${KERNEL_VERSION:-}" ]] && [[ -d "/lib/modules/${KERNEL_VERSION}" ]] && [[ -f "/boot/vmlinuz-${KERNEL_VERSION}" ]]; then
kver="${KERNEL_VERSION}"
elif [[ -d "/lib/modules/$(uname -r)" ]] && [[ -f "/boot/vmlinuz-$(uname -r)" ]]; then
kver="$(uname -r)"
else
kver="$(for d in /lib/modules/*/; do
v=$(basename "$d"); [[ -f "/boot/vmlinuz-${v}" ]] && echo "$v";
done | sort -V | tail -n1)"
fi
if [[ -z "${kver}" ]]; then
log "ERROR: no kernel with both modules and /boot/vmlinuz-* found" >&2
return 1
fi
# The agent binary must be present before we can embed it into the initramfs.
# Two acceptable sources (checked in order):
# 1. scripts/powernode-agent-<arch> — staged by CI before invoking build.sh
# 2. ../agent/dist/powernode-agent-linux-<arch> — produced by `make -C agent build-<arch>`
#
# If neither exists, the build FAILS unless POWERNODE_OFFLINE_DEV=1 is set,
# in which case we invoke the Makefile target to build the agent. The legacy
# placeholder shim path is removed — a /bin/sh exec disguised as powernode-agent
# is never acceptable in production (silent non-functional boot).
local agent_candidates=(
"${SCRIPT_DIR}/scripts/powernode-agent-${ARCH}"
"${SCRIPT_DIR}/../agent/dist/powernode-agent-linux-${ARCH}"
)
local agent_bin=""
for c in "${agent_candidates[@]}"; do
[[ -x "$c" ]] && { agent_bin="$c"; break; }
done
if [[ -z "$agent_bin" ]]; then
if [[ "${POWERNODE_OFFLINE_DEV:-0}" == "1" ]]; then
log "OFFLINE-DEV: invoking 'make -C ../agent build-${ARCH}' to produce agent binary"
make -C "${SCRIPT_DIR}/../agent" "build-${ARCH}"
agent_bin="${SCRIPT_DIR}/../agent/dist/powernode-agent-linux-${ARCH}"
[[ -x "$agent_bin" ]] || { log "FATAL: make build-${ARCH} did not produce $agent_bin"; exit 1; }
else
log "FATAL: agent binary missing for ${ARCH}"
log " Looked at:"
for c in "${agent_candidates[@]}"; do log " - $c"; done
log " CI must stage the artifact before invoking build.sh."
log " For local dev, set POWERNODE_OFFLINE_DEV=1 to auto-build via make."
exit 1
fi
fi
cp "$agent_bin" /tmp/powernode-agent
local conf_args=("-c" "${shared_conf}")
[[ -f "${arch_conf}" ]] && conf_args+=("-c" "${arch_conf}")
# Drivers explicitly forced in: dracut config-file merging via repeated -c is
# unreliable across distros; CLI --force-drivers is the durable mechanism.
# qemu_fw_cfg exposes virtio-fw-cfg under /sys/firmware/ for the agent's
# identity package on libvirt/QEMU first boot.
# 9p / 9pnet_virtio: filesystem passthrough from host (used by the dev-mode
# local-fs module loader to expose /var/lib/powernode/modules into the guest).
# overlay: union mount for stacking module rootfs lowers.
local force_drivers="qemu_fw_cfg 9p 9pnet 9pnet_virtio overlay"
dracut \
"${conf_args[@]}" \
--modules "powernode" \
--kver "${kver}" \
--force-drivers "${force_drivers}" \
--include "/tmp/powernode-agent" "/sbin/powernode-agent" \
--compress zstd \
--force \
"${out}/initramfs.cpio.zst"
# Ubuntu ships /boot/vmlinuz-* with mode 0600 owned by root (since 2018,
# KASLR consideration). Use sudo for the read; the destination ends up
# owned by the build user so subsequent runs don't need elevated rights.
if [[ -r "/boot/vmlinuz-${kver}" ]]; then
cp "/boot/vmlinuz-${kver}" "${out}/kernel"
else
log "kernel image needs sudo (mode 0600 in /boot)…"
sudo cp "/boot/vmlinuz-${kver}" "${out}/kernel"
sudo chown "$(id -u):$(id -g)" "${out}/kernel"
fi
chmod 0644 "${out}/kernel" "${out}/initramfs.cpio.zst"
sha256sum "${out}/kernel" "${out}/initramfs.cpio.zst" >"${out}/SHA256SUMS"
log "kernel-initrd ✓ at ${out} (kver=${kver})"
}
# ── Variant: raw disk image (UEFI ESP + ext4 boot + ext4 persist) ──────────
build_raw() {
log "Building raw disk image…"
local out="${ARCH_OUT}/raw"
mkdir -p "${out}"
bash "${SCRIPT_DIR}/images/raw/build-raw.sh" --arch "${ARCH}" --output "${out}/installer.img"
sha256sum "${out}/installer.img" >"${out}/SHA256SUMS" 2>/dev/null || true
log "raw ✓ at ${out}"
}
# ── Variant: ISO (xorriso, hybrid EFI+BIOS for amd64; pure UEFI for arm64) ──
build_iso() {
log "Building ISO…"
local out="${ARCH_OUT}/iso"
mkdir -p "${out}"
bash "${SCRIPT_DIR}/images/iso/build-iso.sh" --arch "${ARCH}" --output "${out}/installer.iso"
sha256sum "${out}/installer.iso" >"${out}/SHA256SUMS" 2>/dev/null || true
log "iso ✓ at ${out}"
}
# ── Variant: iPXE chainload script (server-rendered template) ──────────────
build_ipxe() {
log "Building iPXE chainload template…"
local out="${ARCH_OUT}/ipxe"
mkdir -p "${out}"
cp "${SCRIPT_DIR}/images/ipxe/template.ipxe.erb" "${out}/template.ipxe.erb"
log "ipxe ✓ template copied to ${out} — server's NetbootService renders per-instance"
}
# ── Variant: qcow2 pre-baked cloud image ───────────────────────────────────
build_qcow2() {
log "Building qcow2 cloud image…"
local out="${ARCH_OUT}/qcow2"
mkdir -p "${out}"
bash "${SCRIPT_DIR}/images/qcow2/build-qcow2.sh" --arch "${ARCH}" --output "${out}/cloud.qcow2"
sha256sum "${out}/cloud.qcow2" >"${out}/SHA256SUMS" 2>/dev/null || true
log "qcow2 ✓ at ${out}"
}
# ── Variant: disk-image-rpi4 (RPi 4 SD card, MBR + FAT32 boot) ─────────────
# Plan: docs/plans/wondrous-yawning-anchor.md §3.
# Operator flashes onto SD, Pi boots, agent polls /node_api/claim.
# arm64-only (Pi 4 is arm64; 32-bit Pi support deferred).
build_disk_image_rpi4() {
if [[ "$ARCH" != "arm64" ]]; then
log "disk-image-rpi4 is arm64-only — skipping for $ARCH"
return 0
fi
log "Building RPi 4 disk image…"
local out="${ARCH_OUT}/disk-image-rpi4"
mkdir -p "${out}"
KERNEL_INITRD_DIR="${ARCH_OUT}/kernel-initrd" \
bash "${SCRIPT_DIR}/images/disk-image-rpi4/build-disk-image-rpi4.sh" \
--output "${out}/powernode-rpi4.img" \
${PLATFORM_URL:+--platform-url "$PLATFORM_URL"} \
${CA_PEM_FILE:+--ca-pem-file "$CA_PEM_FILE"} \
${RPI4_FIRMWARE_DIR:+--firmware-dir "$RPI4_FIRMWARE_DIR"}
sha256sum "${out}/powernode-rpi4.img" >"${out}/SHA256SUMS" 2>/dev/null || true
log "disk-image-rpi4 ✓ at ${out}"
}
# ── Variant: disk-image-arm64-uefi (Pi 5 / Ampere / generic UEFI arm64) ────
# Plan: docs/plans/wondrous-yawning-anchor.md §3.
# Wraps build-raw.sh + layers identity.cfg + ca.pem onto EFI partition.
build_disk_image_arm64_uefi() {
if [[ "$ARCH" != "arm64" ]]; then
log "disk-image-arm64-uefi is arm64-only — skipping for $ARCH"
return 0
fi
log "Building generic arm64 UEFI disk image…"
local out="${ARCH_OUT}/disk-image-arm64-uefi"
mkdir -p "${out}"
bash "${SCRIPT_DIR}/images/disk-image-arm64-uefi/build-disk-image-arm64-uefi.sh" \
--output "${out}/powernode-arm64-uefi.img" \
${PLATFORM_URL:+--platform-url "$PLATFORM_URL"} \
${CA_PEM_FILE:+--ca-pem-file "$CA_PEM_FILE"}
sha256sum "${out}/powernode-arm64-uefi.img" >"${out}/SHA256SUMS" 2>/dev/null || true
log "disk-image-arm64-uefi ✓ at ${out}"
}
# ── Variant: OCI image (bootc-compatible) ──────────────────────────────────
build_oci() {
log "Building OCI bootc image…"
local out="${ARCH_OUT}/oci"
mkdir -p "${out}"
if ! command -v buildah >/dev/null 2>&1; then
log "WARN: buildah not installed — OCI build skipped (install buildah for bootc)"
return 0
fi
buildah bud \
--platform "linux/${ARCH}" \
--build-arg "BASE_IMAGE_DIGEST=${BASE_IMAGE_DIGEST}" \
-t "powernode-bootc:${ARCH}" \
-f "${SCRIPT_DIR}/images/oci/Containerfile" \
"${SCRIPT_DIR}/images/oci"
log "oci ✓ tag=powernode-bootc:${ARCH}"
}
# ── Dispatch ───────────────────────────────────────────────────────────────
log "Starting build (variants: ${VARIANTS})"
IFS=',' read -ra VARIANT_LIST <<<"${VARIANTS}"
for v in "${VARIANT_LIST[@]}"; do
case "$v" in
kernel-initrd) build_kernel_initrd ;;
raw) build_raw ;;
iso) build_iso ;;
ipxe) build_ipxe ;;
qcow2) build_qcow2 ;;
oci) build_oci ;;
disk-image-rpi4) build_disk_image_rpi4 ;;
disk-image-arm64-uefi) build_disk_image_arm64_uefi ;;
*) log "WARN: unknown variant '$v' — skipped" ;;
esac
done
# ── Reproducibility manifest ──────────────────────────────────────────────
# Emit a machine-readable build-manifest.json that the M3 reproducibility
# gate diffs between two builds of the same source. Two back-to-back runs
# with identical pins MUST produce byte-identical manifests (modulo git_sha,
# which the gate strips before diffing).
if ! command -v jq >/dev/null 2>&1; then
log "FATAL: jq is required to emit build-manifest.json (apt install jq)"
exit 1
fi
artifacts_json="{}"
while IFS= read -r f; do
rel="${f#${ARCH_OUT}/}"
sha=$(sha256sum "$f" | awk '{print $1}')
size=$(stat -c %s "$f")
artifacts_json=$(jq -n \
--argjson acc "$artifacts_json" \
--arg k "$rel" --arg s "$sha" --argjson n "$size" \
'$acc + {($k): {sha256: $s, size_bytes: $n}}')
done < <(find "${ARCH_OUT}" -type f \
\( -name 'kernel' -o -name 'initramfs.cpio.zst' \
-o -name '*.img' -o -name '*.iso' -o -name '*.qcow2' \) \
| sort)
# OCI image digest (buildah inspect) if the oci variant ran.
oci_digest="null"
if command -v buildah >/dev/null 2>&1 \
&& buildah images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "powernode-bootc:${ARCH}"; then
d=$(buildah inspect --format '{{.FromImageDigest}}' "powernode-bootc:${ARCH}" 2>/dev/null)
[[ -n "$d" ]] && oci_digest=$(jq -nc --arg v "$d" '$v')
fi
# fs-verity root hash for the composefs blob (if Stage-2 composer produced one).
fsverity_hash="null"
if [[ -f "${ARCH_OUT}/oci/composefs.blob" ]] && command -v fsverity >/dev/null 2>&1; then
h=$(fsverity digest "${ARCH_OUT}/oci/composefs.blob" 2>/dev/null | awk '{print $1}')
[[ -n "$h" ]] && fsverity_hash=$(jq -nc --arg v "$h" '$v')
fi
jq -n \
--arg arch "${ARCH}" \
--arg base "${BASE_IMAGE_DIGEST}" \
--arg apt "${APT_SNAPSHOT:-unknown}" \
--arg kver "${KERNEL_VERSION:-$(uname -r)}" \
--arg git "$(git -C "${SCRIPT_DIR}" rev-parse HEAD 2>/dev/null || echo unknown)" \
--arg variants "${VARIANTS}" \
--argjson artifacts "$artifacts_json" \
--argjson oci "$oci_digest" \
--argjson fsverity "$fsverity_hash" \
'{schema_version: 1,
arch: $arch,
base_image_digest: $base,
apt_snapshot: $apt,
kernel_version: $kver,
git_sha: $git,
variants: ($variants | split(",")),
artifacts: $artifacts,
oci_digest: $oci,
fsverity_root_hash: $fsverity}' \
>"${ARCH_OUT}/build-manifest.json"
log "Build complete: ${ARCH_OUT} (manifest: build-manifest.json)"