deploy/cloudflare-iso-builder/scripts/build-iso.sh
back to source
#!/usr/bin/env bash
# Pack NumericalOS initramfs (+ optional hybrid ISO) inside a Linux container.
# Floor tools come from the graph-resolved distro.floor.json (numos-floor),
# not an ad-hoc busybox product layout.
# Does NOT claim a boot. Writes manifest.json with boot_verified=false.
set -euo pipefail
ARCH="${ARCH:-x86_64}"
WANT_ISO="${WANT_ISO:-true}"
SRC_TGZ="${SRC_TGZ:-/workspace/source.tgz}"
OUT_DIR="${OUT_DIR:-/workspace/out}"
KERNEL_URL="${KERNEL_URL:-}"
KERNEL_SHA256="${KERNEL_SHA256:-}"
FLOOR_URL="${FLOOR_URL:-}"
FLOOR_SHA256="${FLOOR_SHA256:-}"
BUILD_ID="${BUILD_ID:-manual}"
DISTRO_PROFILE="${DISTRO_PROFILE:-self-init}"
mkdir -p "$OUT_DIR" /workspace/src /workspace/rootfs /workspace/iso
log() { printf 'numos-cf-build: %s\n' "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }
# --- unpack sources ----------------------------------------------------------
if [[ ! -f "$SRC_TGZ" ]]; then
die "missing source tarball at $SRC_TGZ"
fi
tar -xzf "$SRC_TGZ" -C /workspace/src
BOOT_DIR=""
for cand in /workspace/src/boot /workspace/src; do
if [[ -f "$cand/bootstrap.sh" || -f "$cand/boot/bootstrap.sh" ]]; then
if [[ -f "$cand/bootstrap.sh" ]]; then
BOOT_DIR="$cand"
else
BOOT_DIR="$cand/boot"
fi
break
fi
done
[[ -n "$BOOT_DIR" ]] || die "bootstrap.sh not found in source tarball"
STATE_FILE=""
for cand in /workspace/src/dist/numos.state /workspace/src/numos.state \
/workspace/src/etc/numos.state; do
if [[ -f "$cand" ]]; then
STATE_FILE="$cand"
break
fi
done
[[ -n "$STATE_FILE" ]] || die "numos.state not found in source tarball"
FLOOR_JSON=""
for cand in /workspace/src/dist/distro.floor.json /workspace/src/distro.floor.json; do
if [[ -f "$cand" ]]; then
FLOOR_JSON="$cand"
break
fi
done
KERNEL_JSON=""
for cand in /workspace/src/dist/distro.kernel.json /workspace/src/distro.kernel.json; do
if [[ -f "$cand" ]]; then
KERNEL_JSON="$cand"
break
fi
done
# --- graph-resolved floor (numos-floor) --------------------------------------
case "$ARCH" in
x86_64|amd64) ARCH=x86_64 ;;
i686|i386|x86) ARCH=x86 ;;
esac
if [[ -n "$FLOOR_JSON" ]]; then
log "resolving floor from $FLOOR_JSON"
eval "$(python3 - "$FLOOR_JSON" "$ARCH" <<'PY'
import json, sys
path, arch = sys.argv[1], sys.argv[2]
with open(path) as f:
d = json.load(f)
# allow arch mismatch warning but use file values when profile matches
url = d.get("url") or ""
sha = d.get("sha256") or ""
name = d.get("install_name") or "numos-floor"
applets = d.get("applets") or []
lic = d.get("license") or ""
src = d.get("source_offer") or ""
prof = d.get("profile") or ""
def sh(s):
return "'" + str(s).replace("'", "'\\''") + "'"
print("FLOOR_URL_FROM_GRAPH=%s" % sh(url))
print("FLOOR_SHA_FROM_GRAPH=%s" % sh(sha))
print("FLOOR_NAME=%s" % sh(name))
print("FLOOR_LICENSE=%s" % sh(lic))
print("FLOOR_SOURCE_OFFER=%s" % sh(src))
print("FLOOR_PROFILE=%s" % sh(prof))
print("FLOOR_APPLETS=%s" % sh(" ".join(applets)))
PY
)"
FLOOR_URL="${FLOOR_URL:-$FLOOR_URL_FROM_GRAPH}"
FLOOR_SHA256="${FLOOR_SHA256:-$FLOOR_SHA_FROM_GRAPH}"
else
FLOOR_NAME="${FLOOR_NAME:-numos-floor}"
FLOOR_APPLETS="${FLOOR_APPLETS:-sh ash grep cut head sort sed tr mount umount sha256sum echo cat ls mkdir rm sleep printf uname od mv date dirname basename readlink wc test [ true false kill wget timeout ln chmod cp}"
FLOOR_LICENSE="${FLOOR_LICENSE:-GPL-2.0-only}"
FLOOR_SOURCE_OFFER="${FLOOR_SOURCE_OFFER:-https://busybox.net/downloads/}"
FLOOR_PROFILE="${FLOOR_PROFILE:-$DISTRO_PROFILE}"
fi
if [[ -z "$FLOOR_URL" ]]; then
# Last-resort published multi-call binary for x86_64 only (graph should pin).
if [[ "$ARCH" == "x86_64" ]]; then
FLOOR_URL="https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"
FLOOR_SHA256="${FLOOR_SHA256:-6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348}"
FLOOR_NAME="${FLOOR_NAME:-numos-floor}"
log "WARNING: no distro.floor.json; using pinned x86_64 floor URL"
else
die "no floor URL for arch=$ARCH; export distro.floor.json or set FLOOR_URL"
fi
fi
log "fetching floor ($FLOOR_NAME) from $FLOOR_URL"
curl -fsSL -o /workspace/numos-floor.bin "$FLOOR_URL"
if [[ -n "$FLOOR_SHA256" && "$FLOOR_SHA256" != "None" && "$FLOOR_SHA256" != "null" ]]; then
echo "$FLOOR_SHA256 /workspace/numos-floor.bin" | sha256sum -c -
else
log "WARNING: floor sha256 not supplied; recording digest only"
fi
FLOOR_DIGEST=$(sha256sum /workspace/numos-floor.bin | awk '{print $1}')
chmod +x /workspace/numos-floor.bin
# --- rootfs (initramfs layout) -----------------------------------------------
ROOT=/workspace/rootfs
rm -rf "$ROOT"
mkdir -p "$ROOT"/{bin,boot/lib,etc,proc,sys,dev,run,tmp,lib}
FLOOR_BIN="${FLOOR_NAME:-numos-floor}"
# Real multi-call binary (must be a file, not a later self-symlink).
install -m 0755 /workspace/numos-floor.bin "$ROOT/bin/$FLOOR_BIN"
# Compatibility alias only when product name is not already the file name.
if [[ "$FLOOR_BIN" != "numos-floor" ]]; then
ln -sfn "$FLOOR_BIN" "$ROOT/bin/numos-floor"
fi
# Optional legacy name (not the product identity)
if [[ "$FLOOR_BIN" != "busybox" ]]; then
ln -sfn "$FLOOR_BIN" "$ROOT/bin/busybox"
fi
# shellcheck disable=SC2086
for a in ${FLOOR_APPLETS}; do
# Never replace the floor binary itself with a symlink.
if [[ "$a" == "$FLOOR_BIN" || "$a" == "numos-floor" ]]; then
continue
fi
ln -sfn "$FLOOR_BIN" "$ROOT/bin/$a"
done
# Self-contained offline layout
mkdir -p "$ROOT/opt/numericalos" "$ROOT/boot/lib"
install -m 0755 "$BOOT_DIR/bootstrap.sh" "$ROOT/opt/numericalos/bootstrap.sh"
install -m 0755 "$BOOT_DIR/numinit.sh" "$ROOT/opt/numericalos/numinit.sh"
install -m 0755 "$BOOT_DIR/numinit.sh" "$ROOT/boot/numinit.sh"
if [[ -f "$BOOT_DIR/numctl" ]]; then
install -m 0755 "$BOOT_DIR/numctl" "$ROOT/bin/numctl"
install -m 0755 "$BOOT_DIR/numctl" "$ROOT/opt/numericalos/numctl"
fi
if [[ -f "$BOOT_DIR/lib/arch_table.sh" ]]; then
install -m 0644 "$BOOT_DIR/lib/arch_table.sh" "$ROOT/lib/arch_table.sh"
install -m 0644 "$BOOT_DIR/lib/arch_table.sh" "$ROOT/boot/lib/arch_table.sh"
mkdir -p "$ROOT/opt/numericalos/lib"
install -m 0644 "$BOOT_DIR/lib/arch_table.sh" "$ROOT/opt/numericalos/lib/arch_table.sh"
elif [[ -f "$BOOT_DIR/arch_table.sh" ]]; then
install -m 0644 "$BOOT_DIR/arch_table.sh" "$ROOT/lib/arch_table.sh"
else
die "arch_table.sh not found under $BOOT_DIR"
fi
install -m 0644 "$STATE_FILE" "$ROOT/etc/numos.state"
install -m 0644 "$STATE_FILE" "$ROOT/opt/numericalos/numos.state"
if [[ -n "$FLOOR_JSON" ]]; then
install -m 0644 "$FLOOR_JSON" "$ROOT/opt/numericalos/distro.floor.json"
fi
if [[ -f /workspace/src/dist/distro.profile.json ]]; then
install -m 0644 /workspace/src/dist/distro.profile.json \
"$ROOT/opt/numericalos/distro.profile.json"
fi
NI_SHA=$(sha256sum "$ROOT/opt/numericalos/numinit.sh" | awk '{print $1}')
NI_BYTES=$(wc -c < "$ROOT/opt/numericalos/numinit.sh" | tr -d ' ')
{
echo "V 1"
echo "F numinit.sh $NI_SHA $NI_BYTES"
} > "$ROOT/opt/numericalos/manifest.txt"
cat > "$ROOT/init" <<'EOF'
#!/bin/sh
export PATH=/bin:/usr/bin
export NUMOS_OFFLINE=1
export NUMOS_PREFIX=/opt/numericalos
export NUMOS_STATE="${NUMOS_STATE:-/etc/numos.state}"
export NUMOS_LIB="${NUMOS_LIB:-/lib}"
export NUMOS_BASE="${NUMOS_BASE:-https://numericalos.com}"
exec /bin/sh /opt/numericalos/bootstrap.sh
EOF
chmod 0755 "$ROOT/init"
# License notice for the floor implementation binary
cat > "$ROOT/boot/FLOOR-LICENSE.txt" <<EOF
NumericalOS tool floor is installed as ${FLOOR_NAME:-numos-floor}.
License: ${FLOOR_LICENSE:-see distro.floor.json}
Corresponding source / offer: ${FLOOR_SOURCE_OFFER:-see distro.floor.json}
Profile: ${FLOOR_PROFILE:-$DISTRO_PROFILE}
The image product identity is numos-floor (graph-resolved), not a busybox distro.
EOF
INITRD="$OUT_DIR/numericalos-initramfs-${ARCH}.cpio.gz"
( cd "$ROOT" && find . -print0 \
| cpio --null --create --format=newc --owner=root:root ) \
| gzip -9 > "$INITRD"
INITRD_SHA=$(sha256sum "$INITRD" | awk '{print $1}')
INITRD_SIZE=$(wc -c < "$INITRD" | tr -d ' ')
log "initramfs $INITRD_SIZE bytes sha256=$INITRD_SHA profile=${FLOOR_PROFILE:-$DISTRO_PROFILE}"
zcat "$INITRD" | cpio -t 2>/dev/null | head -50 >&2 || true
ISO_PATH=""
ISO_SHA=""
ISO_SIZE=""
FILE_ISO=""
KERNEL_SOURCE="none"
if [[ "$WANT_ISO" == "true" || "$WANT_ISO" == "1" ]]; then
KDIR=/workspace/kernel
mkdir -p "$KDIR"
VMLINUZ=""
# Graph-resolved kernel (distro.kernel.json) wins unless env KERNEL_URL set.
if [[ -z "$KERNEL_URL" && -n "$KERNEL_JSON" ]]; then
log "resolving kernel from $KERNEL_JSON"
eval "$(python3 - "$KERNEL_JSON" <<'PY'
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
def sh(s):
return "'" + str(s or "").replace("'", "'\\''") + "'"
print("KERNEL_URL_FROM_GRAPH=%s" % sh(d.get("url")))
print("KERNEL_SHA_FROM_GRAPH=%s" % sh(d.get("sha256")))
print("KERNEL_KIND_FROM_GRAPH=%s" % sh(d.get("kind") or "deb"))
print("KERNEL_PRODUCT=%s" % sh(d.get("product") or "third-party-kernel"))
PY
)"
KERNEL_URL="${KERNEL_URL:-$KERNEL_URL_FROM_GRAPH}"
KERNEL_SHA256="${KERNEL_SHA256:-$KERNEL_SHA_FROM_GRAPH}"
KERNEL_KIND="${KERNEL_KIND:-$KERNEL_KIND_FROM_GRAPH}"
log "graph kernel product=${KERNEL_PRODUCT:-third-party-kernel} kind=${KERNEL_KIND:-deb}"
fi
if [[ -n "$KERNEL_URL" ]]; then
log "fetching kernel from $KERNEL_URL"
curl -fsSL -o "$KDIR/kernel.dl" "$KERNEL_URL"
if [[ -n "$KERNEL_SHA256" && "$KERNEL_SHA256" != "None" && "$KERNEL_SHA256" != "null" ]]; then
echo "$KERNEL_SHA256 $KDIR/kernel.dl" | sha256sum -c -
else
log "WARNING: kernel sha256 not pinned in graph; recording fetch only"
fi
KERNEL_SOURCE="$KERNEL_URL"
case "${KERNEL_KIND:-}:${KERNEL_URL}" in
deb:*|*:*.deb)
log "extracting vmlinuz from Debian package (graph/kind deb)"
mkdir -p "$KDIR/extract"
dpkg-deb -x "$KDIR/kernel.dl" "$KDIR/extract"
VMLINUZ=$(find "$KDIR/extract/boot" -type f \( -name 'vmlinuz-*' -o -name vmlinuz \) | head -1 || true)
;;
*)
VMLINUZ="$KDIR/kernel.dl"
;;
esac
else
if [[ "$ARCH" != "x86_64" ]]; then
die "no KERNEL_URL and no distro.kernel.json for arch=$ARCH"
fi
# Last-resort pin (should match numos.distro.KERNEL_PROVIDERS x86_64).
DEFAULT_KERNEL_DEB="https://deb.debian.org/debian/pool/main/l/linux-signed-amd64/linux-image-6.1.0-50-amd64_6.1.176-1_amd64.deb"
log "WARNING: no distro.kernel.json; using pinned Debian kernel package"
if curl -fsSL -o "$KDIR/kernel.deb" "$DEFAULT_KERNEL_DEB"; then
mkdir -p "$KDIR/extract"
dpkg-deb -x "$KDIR/kernel.deb" "$KDIR/extract"
VMLINUZ=$(find "$KDIR/extract/boot" -type f \( -name 'vmlinuz-*' -o -name vmlinuz \) | head -1 || true)
KERNEL_SOURCE="$DEFAULT_KERNEL_DEB"
fi
fi
if [[ -z "${VMLINUZ:-}" || ! -f "$VMLINUZ" ]]; then
log "WARNING: no kernel obtained; writing initramfs-only (WANT_ISO skipped)"
WANT_ISO=false
else
ISO_TREE=/workspace/iso
rm -rf "$ISO_TREE"
mkdir -p "$ISO_TREE/boot/grub"
cp "$VMLINUZ" "$ISO_TREE/boot/vmlinuz"
cp "$INITRD" "$ISO_TREE/boot/initramfs.cpio.gz"
# default=0 serial entry for headless QEMU -cdrom -nographic.
# Serial entry: ONLY console=ttyS0 (last console= becomes /dev/console; do not
# append console=tty0 or userspace numos: lines vanish from serial capture).
# Prefer insmod serial when modules are present (grub-mkrescue tree).
cat > "$ISO_TREE/boot/grub/grub.cfg" <<'EOF'
insmod serial
insmod terminal
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_input serial console
terminal_output serial console
set timeout=1
set default=0
menuentry "NumericalOS self-init (serial)" {
linux /boot/vmlinuz console=ttyS0,115200n8 earlyprintk=serial,ttyS0,115200 NUMOS_OFFLINE=1 NUMOS_PREFIX=/opt/numericalos NUMOS_STATE=/etc/numos.state NUMOS_LIB=/lib
initrd /boot/initramfs.cpio.gz
}
menuentry "NumericalOS self-init (graphical)" {
linux /boot/vmlinuz console=tty0 NUMOS_OFFLINE=1 NUMOS_PREFIX=/opt/numericalos NUMOS_STATE=/etc/numos.state NUMOS_LIB=/lib
initrd /boot/initramfs.cpio.gz
}
EOF
ISO_PATH="$OUT_DIR/numericalos-${ARCH}.iso"
if command -v grub-mkrescue >/dev/null 2>&1; then
grub-mkrescue -o "$ISO_PATH" "$ISO_TREE" -- -volid NUMERICALOS
else
die "grub-mkrescue not found; rebuild container image"
fi
ISO_SHA=$(sha256sum "$ISO_PATH" | awk '{print $1}')
ISO_SIZE=$(wc -c < "$ISO_PATH" | tr -d ' ')
FILE_ISO=$(file -b "$ISO_PATH" || true)
log "iso $ISO_SIZE bytes sha256=$ISO_SHA file=$FILE_ISO"
fi
fi
MANIFEST="$OUT_DIR/manifest.json"
python3 - <<PY
import json, os, time
manifest = {
"build_id": os.environ.get("BUILD_ID", "$BUILD_ID"),
"arch": "$ARCH",
"distro_profile": """${FLOOR_PROFILE:-$DISTRO_PROFILE}""",
"floor": {
"name": """${FLOOR_NAME:-numos-floor}""",
"url": """$FLOOR_URL""",
"sha256": """$FLOOR_DIGEST""",
"license": """${FLOOR_LICENSE:-}""",
},
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"boot_verified": False,
"kernel_source": "$KERNEL_SOURCE",
"artifacts": {
"initramfs": {
"name": os.path.basename("$INITRD"),
"sha256": "$INITRD_SHA",
"bytes": int("$INITRD_SIZE"),
},
},
"notes": [
"Floor is graph-resolved numos-floor (not busybox product layout)",
"Profile self-init completes phase walk offline without eth0/ntpd",
"boot_verified is false until numericalos-verify-boot observes a boot",
"NumericalOS does not ship a kernel; kernel_source is third-party",
],
}
if "$WANT_ISO" in ("true", "1") and "$ISO_PATH":
manifest["artifacts"]["iso"] = {
"name": os.path.basename("$ISO_PATH"),
"sha256": "$ISO_SHA",
"bytes": int("$ISO_SIZE") if "$ISO_SIZE" else 0,
"file": """$FILE_ISO""",
}
with open("$MANIFEST", "w") as f:
json.dump(manifest, f, indent=2)
f.write("\n")
print(json.dumps(manifest, indent=2))
PY
log "done build_id=$BUILD_ID out=$OUT_DIR"
ls -la "$OUT_DIR" >&2