NumericalOS

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