skills/numericalos-build-initramfs/SKILL.md
back to source
---
name: numericalos-build-initramfs
description: Build a NumericalOS initramfs (cpio.gz) whose /init is the graph-driven bootstrap. Use when numericalos-target-classifier routed here, or when the user asks for a NumericalOS initramfs, initrd, or the userspace half of bootable media. Assembles graph-resolved numos-floor (tool multi-call binary), the boot chain, and a verified numos.state — prefer profile self-init for offline complete boots.
---
# Build a NumericalOS initramfs
This produces the **userspace** half of a NumericalOS boot: a `cpio.gz` archive
whose `/init` is `bootstrap.sh`. It does not produce a kernel. Pairing it with a
kernel is `numericalos-build-iso`, or the user's own bootloader configuration.
**Prerequisite:** `numericalos-target-classifier` has run and handed you `ARCH`,
`REPO`, `STATE`, and probe results. Requires a Linux userspace - `cpio`, `gzip`,
`find`. On Windows this needs WSL2, a VM, or **`numericalos-build-iso-cf`**
(Cloudflare Linux container — preferred when the user refuses WSL). git-bash
cannot build a valid initramfs because it cannot create the required device
nodes or preserve permissions.
## The shape you are building
```
/init -> bootstrap.sh (kernel execs this as PID 1)
/bin/busybox -> static, arch-matched
/bin/sh -> busybox (plus the applet symlinks numinit needs)
/boot/numinit.sh
/boot/lib/arch_table.sh
/etc/numos.state
/proc /sys /dev /run /tmp (empty mount points)
```
Nothing else. The whole point of this project is that the configuration is one
file; resist adding a userland.
## Step 0 - export the self-init distro graph (preferred)
```sh
py -m numos.export_state --out dist --profile self-init --seed-only --arch x86_64
# writes numos.state, distro.floor.json, distro.profile.json
```
Profile **self-init** is the offline/QEMU-complete unit graph (mounts, identity,
`hold` longrun) — no eth0/ntpd/join. Profile **fleet** keeps net/clock/join for
topology nodes (expect degrade on bare QEMU).
Floor plan is graph-resolved (`distro.floor.json`): install as **`numos-floor`**,
not as a busybox product tree. The implementation binary may still be a pinned
multi-call static (URL+sha256 in the graph).
## Step 1 - get the floor binary for the target arch
`numinit.sh` is POSIX shell and needs a shell plus `grep`, `cut`, `head`, `sort`,
`sed`, `tr`, `mount`, `sha256sum`. Resolve the floor from the graph:
```sh
# From distro.floor.json: url, sha256, install_name, applets
```
Do not invent an ad-hoc busybox fetch. Prefer the graph URL + sha256:
```sh
# busybox.net publishes prebuilt 1.35.0 binaries for i686 and x86_64 ONLY.
# The path is the full target triple, not the registry's busybox_variant:
# https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
# https://busybox.net/downloads/binaries/1.35.0-i686-linux-musl/busybox
curl -fLo busybox "https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"
sha256sum busybox
```
**For every other architecture there is no published busybox to fetch.**
s390x, mips64el, loongarch64, armv6 and the rest are in the `ArchTarget`
registry and have a working *arch-resolution* path, but no upstream prebuilt
binary exists for them at 1.35.0. On those you must supply busybox yourself:
your distribution's `busybox-static` package, or a from-source build
(`make defconfig && make LDFLAGS=-static`). Say which you did in your report.
Do not report an initramfs as built for an architecture whose busybox you did
not actually obtain.
## GPLv2 obligations - not optional
busybox is licensed **GPLv2**. Putting it in an initramfs or an OCI image and
giving that image to anyone else is redistribution, and it carries obligations
this project has no standing to waive on your behalf:
- Ship the GPLv2 licence text alongside the artifact.
- Provide the corresponding source, or a written offer valid for three years.
- Keep the existing copyright notices intact.
If you are building only for yourself and distributing nothing, none of this
applies. If you hand the artifact to anyone, it does. State in your report
which case you are in - a build that quietly redistributes GPL software with
no notice is a real harm to the people whose work you are using, not a
paperwork detail.
The `busybox_variant` for the target arch comes from `numos/seed.py`'s
`ARCH_TARGETS` - it is a graph-derived field, not something to guess. Read it:
```sh
python3 -c "from numos import seed; print([ (t['canonical'], t['busybox_variant']) for t in seed.ARCH_TARGETS ])"
```
**Verify before trusting.** If the user has a known-good checksum, compare it. If
not, say plainly that you are shipping an unverified third-party binary into
their init, and offer the alternative: build busybox from source with
`make defconfig && make LDFLAGS=-static`, or use their distribution's
`busybox-static` package. Do not quietly ship an unverified binary into PID 1.
## Step 2 - export a state file
```sh
cd "$REPO"
python3 -m numos.export_state --out dist
```
Or seed-only if no IntrikataTopology instance is reachable:
```sh
python3 -c "from numos.export_state import export; export([], 'dist')"
```
Verify it before packing - a state that fails here will halt the machine at boot,
and finding out in QEMU costs far more than finding out now:
```sh
python3 -c "
from numos.state import verify
verify(open('dist/numos.state').read())
print('state verifies')
"
```
## Step 3 - assemble the tree
```sh
set -eu
BUILD=$(mktemp -d)
mkdir -p "$BUILD"/{bin,boot/lib,etc,proc,sys,dev,run,tmp}
install -m 0755 busybox "$BUILD/bin/busybox"
install -m 0755 boot/bootstrap.sh "$BUILD/init"
install -m 0755 boot/numinit.sh "$BUILD/boot/numinit.sh"
install -m 0755 boot/numctl "$BUILD/bin/numctl"
install -m 0644 boot/lib/arch_table.sh "$BUILD/boot/lib/arch_table.sh"
install -m 0644 dist/numos.state "$BUILD/etc/numos.state"
# Applet symlinks. numinit needs these by name.
for a in 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; do
ln -sf busybox "$BUILD/bin/$a"
done
```
**Two details that decide whether this boots at all:**
- **`/init` must be executable and must be the bootstrap.** The kernel execs
`/init` in the initramfs root. If it is not executable you get
`Kernel panic - not syncing: No working init found`.
- **`NUMOS_LIB` resolves via `dirname "$0"`.** When the kernel execs `/init`,
`$0` is `/init`, so `dirname` yields `/` and `NUMOS_LIB` becomes `/lib`. The
layout above puts `arch_table.sh` at `/boot/lib/`, which will **not** be found.
Either place it at `/lib/arch_table.sh`, or set `NUMOS_LIB=/boot/lib` in the
environment. Choose one and be explicit - this is the single most likely cause
of a first-boot halt.
The simplest correct fix:
```sh
mkdir -p "$BUILD/lib"
install -m 0644 boot/lib/arch_table.sh "$BUILD/lib/arch_table.sh"
```
## Step 4 - pack
```sh
( cd "$BUILD" && find . -print0 \
| cpio --null --create --format=newc --owner=root:root ) \
| gzip -9 > numericalos-initramfs-$ARCH.cpio.gz
ls -l numericalos-initramfs-$ARCH.cpio.gz
sha256sum numericalos-initramfs-$ARCH.cpio.gz
```
`--owner=root:root` matters: files owned by the building user's UID will have
that UID at boot, and a non-root `/init` may fail in ways that are hard to read.
Expect roughly 500 KB - 1.5 MB, dominated by busybox.
## Step 5 - inspect before you claim anything
```sh
zcat numericalos-initramfs-$ARCH.cpio.gz | cpio -t | head -30
```
Confirm `init` is present at the root, is not `./boot/init`, and that
`lib/arch_table.sh` (or wherever you put it) is where `bootstrap.sh` will look.
You have now built an artifact. **You have not booted it.** Do not describe it as
bootable, working, or verified. Route to `numericalos-verify-boot` to find out,
or hand it to `numericalos-build-iso` to pair with a kernel.
## Failure modes worth pre-empting
| Symptom at boot | Cause |
|---|---|
| `No working init found` | `/init` missing, not executable, or wrong path |
| `numos: HALT: unsupported architecture: <x>` | busybox arch does not match the kernel arch |
| `arch_table.sh: not found` | the `NUMOS_LIB` path problem in Step 3 |
| `numos: HALT: state hash mismatch` | `numos.state` was edited after export - re-export, never hand-edit |
| `numos: HALT: NUMOS_STATE is unset` | `bootstrap.sh` did not pass it through; set it in the kernel cmdline or the script |
| Immediate kernel panic after phase walk | PID 1 exited after the phase walk without remaining in steady state — e.g. all units oneshot and steady state not reached, or `NUMOS_MAX_TICKS` forced an exit |
That last case is not "the image is wrong" by default. After a successful phase
walk, `numinit` enters **`numos_steady_state`** (supervise longruns + health
ticks) and, on a real boot with no max-tick bound, never returns. A panic right
after `boot complete` means PID 1 still exited — check for a oneshot-only state
with no longrun, a forced tick bound, or a halt. A real deployment still wants
at least one `longrun` (seeded `join` is that shape). Do not describe a panic as
a build failure without reading the last `numos:` lines.
## Reporting
State: output path, size, sha256, target arch, busybox source and whether it was
checksum-verified, which state file was packed and whether it verified. Then the
boundary: "artifact built; not booted."
## Related skills
- `numericalos-target-classifier` - run first
- `numericalos-build-iso` - pairs this with a kernel into bootable media (local Linux)
- `numericalos-build-iso-cf` - same packing on Cloudflare when there is no local Linux/WSL
- `numericalos-verify-boot` - QEMU; the only skill that may claim a boot