#!/usr/bin/env bash # build-kernel.sh — builds the PoC kernel: i686 bzImage with 9p statically linked # and NO embedded initramfs. This file is the GPL-2 "corresponding source" recipe: # pinned upstream version + checksums + the exact config deltas we apply. # # Why a custom kernel (see docs/adr/ADR-001, "Decisão de kernel (iteração 3)" and # "Diagnóstico corrigido"): # - Alpine's shipped kernel has CONFIG_9P_FS=m, so it cannot mount a 9p root without # an initramfs. # - copy/v86's buildroot bzImage DOES boot a 9p root (rdinit= to a missing path skips its # embedded initramfs), but it registers no ttyS0 console and carries ~6.6 MB of dead # initramfs. The custom kernel is optimisation + sovereignty, not a boot requirement. # We need CONFIG_9P_FS=y + CONFIG_NET_9P{,_VIRTIO}=y, CONFIG_INITRAMFS_SOURCE="" and # CONFIG_SERIAL_8250_CONSOLE=y (without it PID 1 has no stdio and `sh -i` exits 0). # # Why Debian amd64 + gcc-multilib (and not a linux/386 container): building 32-bit does # not require a 32-bit host. gcc biarch emits -m32 on x86_64, and on Apple Silicon the # amd64 container runs under Rosetta (fast) while a linux/386 container runs under full # qemu emulation (slow). The .config is distro-agnostic, so an Alpine config builds fine # under Debian. # # Usage: # ./scripts/build-kernel.sh # build (docker layer cache reused) # MB_KERNEL_NO_CACHE=1 ./scripts/build-kernel.sh # # Outputs (in poc-vm-operation/image/): # mb-kernel-9p-i686.bin the bzImage consumed by scripts/setup.mjs # mb-kernel-9p-i686.config the .config actually used (GPL-2 corresponding source) set -euo pipefail # ------------------------------------------------------------------ pinned inputs # Upstream kernel, matched to the Alpine config below so that `olddefconfig` is nearly # a no-op (no version drift). KERNEL_VERSION="6.18.50" KERNEL_SHA256="d2fc041dab4e11d9645e3ba53be058faa52b8ce28a8a97c889bb2cacee170461" # Base config: Alpine 3.24 linux-virt for **x86** (32-bit). Header of the file reads # "# Linux/x86 6.18.50 Kernel Configuration", and it already ships the virtio drivers # v86 exposes plus `# CONFIG_64BIT is not set`. ALPINE_BRANCH="v3.24" ALPINE_APK="linux-virt-6.18.50-r0.apk" ALPINE_APK_SHA256="06c7a4dbff92626ae0020de75d1ce9dc41a8bf9864fb7ee238417d5b5fa789b0" ALPINE_CONFIG_PATH="boot/config-6.18.50-0-virt" BUILDER_BASE="debian:bookworm-slim" IMAGE_TAG="mb-kernel-9p-i686:${KERNEL_VERSION}" BIN_NAME="mb-kernel-9p-i686.bin" CONFIG_NAME="mb-kernel-9p-i686.config" root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" out_dir="${root}/image" log() { printf '[build-kernel] %s\n' "$*"; } die() { printf '[build-kernel] ERRO: %s\n' "$*" >&2; exit 1; } command -v docker >/dev/null 2>&1 || die "docker não encontrado (OrbStack/Docker Desktop precisa estar rodando)" docker info >/dev/null 2>&1 || die "docker não responde — suba o OrbStack/Docker Desktop e tente de novo" ctx="$(mktemp -d)" trap 'rm -rf "${ctx}"' EXIT # ------------------------------------------------------- config deltas (the whole point) # Runs inside the container, on top of the Alpine config. cat > "${ctx}/apply-config.sh" <<'APPLY' #!/bin/sh # Forces the symbols the PoC requires on top of Alpine's config-virt, then reconciles # with `olddefconfig`. olddefconfig can demote a forced symbol (unmet dependency), so we # force → reconcile → check, up to 3 rounds, and fail loudly if it never sticks. set -eu force() { scripts/config --file .config \ `# --- the reason this kernel exists: 9p built-in, mounts root without initramfs` \ --enable NET_9P \ --enable NET_9P_VIRTIO \ --enable 9P_FS \ --enable NETFS_SUPPORT \ `# --- no embedded initramfs: the borrowed kernel ships ~6.6 MB of it, all dead weight` \ --set-str INITRAMFS_SOURCE "" \ `# --- transport + console the v86 machine exposes` \ --enable VIRTIO \ --enable VIRTIO_PCI \ --enable SERIAL_8250 \ --enable SERIAL_8250_CONSOLE \ `# --- earlyprintk: the whole point of the pair-test is seeing boot (or panic)` \ --enable EARLY_PRINTK \ --enable X86_VERBOSE_BOOTUP \ `# --- build-ability on Debian: Alpine signs modules with a key we do not have,` \ `# and gcc plugins would need gcc-N-plugin-dev. We do not build modules here.` \ --disable MODULE_SIG \ --disable MODULE_SIG_ALL \ --set-str MODULE_SIG_KEY "" \ --disable GCC_PLUGINS \ `# --- debug info only costs build time here; vmlinux symbols are not the deliverable` \ --enable DEBUG_INFO_NONE \ --disable DEBUG_INFO_DWARF5 } check() { grep -q '^CONFIG_NET_9P=y$' .config && grep -q '^CONFIG_NET_9P_VIRTIO=y$' .config && grep -q '^CONFIG_9P_FS=y$' .config && grep -q '^CONFIG_INITRAMFS_SOURCE=""$' .config && grep -q '^CONFIG_EARLY_PRINTK=y$' .config && grep -q '^CONFIG_SERIAL_8250=y$' .config && grep -q '^CONFIG_SERIAL_8250_CONSOLE=y$' .config } for round in 1 2 3; do force make ARCH=x86 olddefconfig >/dev/null if check; then echo "config OK na rodada ${round}" exit 0 fi echo "olddefconfig rebaixou algum símbolo — forçando de novo (rodada ${round})" done echo "ERRO: símbolos 9p/initramfs/console não fixaram depois de 3 rodadas" >&2 grep -E '^(# )?CONFIG_(NET_9P|NET_9P_VIRTIO|9P_FS|INITRAMFS_SOURCE|EARLY_PRINTK|SERIAL_8250|SERIAL_8250_CONSOLE)\b' .config >&2 || true exit 1 APPLY chmod +x "${ctx}/apply-config.sh" # ------------------------------------------------------------------ builder recipe cat > "${ctx}/Dockerfile" <<'DOCKERFILE' # syntax=docker/dockerfile:1 FROM debian:bookworm-slim AS build ARG KERNEL_VERSION ARG KERNEL_SHA256 ARG ALPINE_BRANCH ARG ALPINE_APK ARG ALPINE_APK_SHA256 ARG ALPINE_CONFIG_PATH ENV DEBIAN_FRONTEND=noninteractive # gcc-multilib is what makes a 32-bit kernel buildable on an amd64 host. RUN apt-get update && apt-get install -y --no-install-recommends \ gcc gcc-multilib make bc bison flex libelf-dev libssl-dev \ wget xz-utils ca-certificates cpio kmod \ && rm -rf /var/lib/apt/lists/* WORKDIR /src # Upstream source, checksum-verified. RUN wget -q "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${KERNEL_VERSION}.tar.xz" \ && echo "${KERNEL_SHA256} linux-${KERNEL_VERSION}.tar.xz" | sha256sum -c - \ && tar -xf "linux-${KERNEL_VERSION}.tar.xz" \ && rm "linux-${KERNEL_VERSION}.tar.xz" # Alpine's config-virt for x86, checksum-verified. An .apk is concatenated gzip members, # hence --ignore-zeros so GNU tar walks past the signature/control members. RUN wget -q "https://dl-cdn.alpinelinux.org/alpine/${ALPINE_BRANCH}/main/x86/${ALPINE_APK}" \ && echo "${ALPINE_APK_SHA256} ${ALPINE_APK}" | sha256sum -c - \ && tar -xzf "${ALPINE_APK}" --ignore-zeros "${ALPINE_CONFIG_PATH}" \ && rm "${ALPINE_APK}" \ && grep -q "Linux/x86 ${KERNEL_VERSION} Kernel Configuration" "${ALPINE_CONFIG_PATH}" \ && grep -q '^# CONFIG_64BIT is not set$' "${ALPINE_CONFIG_PATH}" WORKDIR /src/linux-${KERNEL_VERSION} COPY apply-config.sh /usr/local/bin/apply-config.sh RUN cp "/src/${ALPINE_CONFIG_PATH}" .config && apply-config.sh # bzImage only: no modules, no `make all`. RUN make -j"$(nproc)" ARCH=x86 bzImage RUN mkdir -p /out \ && cp arch/x86/boot/bzImage /out/bzImage \ && cp .config /out/config \ && ls -l /out && sha256sum /out/bzImage DOCKERFILE # ------------------------------------------------------------------ build + extract log "kernel ${KERNEL_VERSION} (i686) — base: Alpine ${ALPINE_BRANCH} ${ALPINE_CONFIG_PATH}" log "builder: ${BUILDER_BASE} em linux/amd64 (Rosetta no Apple Silicon) + gcc-multilib" docker build \ --platform linux/amd64 \ ${MB_KERNEL_NO_CACHE:+--no-cache} \ --build-arg "KERNEL_VERSION=${KERNEL_VERSION}" \ --build-arg "KERNEL_SHA256=${KERNEL_SHA256}" \ --build-arg "ALPINE_BRANCH=${ALPINE_BRANCH}" \ --build-arg "ALPINE_APK=${ALPINE_APK}" \ --build-arg "ALPINE_APK_SHA256=${ALPINE_APK_SHA256}" \ --build-arg "ALPINE_CONFIG_PATH=${ALPINE_CONFIG_PATH}" \ -t "${IMAGE_TAG}" \ "${ctx}" mkdir -p "${out_dir}" cid="$(docker create --platform linux/amd64 "${IMAGE_TAG}" /bin/true)" # shellcheck disable=SC2064 trap "docker rm -f '${cid}' >/dev/null 2>&1 || true; rm -rf '${ctx}'" EXIT docker cp "${cid}:/out/bzImage" "${out_dir}/${BIN_NAME}" docker cp "${cid}:/out/config" "${out_dir}/${CONFIG_NAME}" docker rm -f "${cid}" >/dev/null # ------------------------------------------------------------------ local verification bin="${out_dir}/${BIN_NAME}" cfg="${out_dir}/${CONFIG_NAME}" [ -s "${bin}" ] || die "bzImage não saiu do contêiner" bytes="$(wc -c < "${bin}" | tr -d ' ')" sha="$(shasum -a 256 "${bin}" | cut -d' ' -f1)" log "image/${BIN_NAME}" log " file: $(file -b "${bin}")" log " bytes: ${bytes} ($(awk -v b="${bytes}" 'BEGIN{printf "%.1f MB", b/1048576}'))" log " sha256: ${sha}" grep -E '^CONFIG_(9P_FS|NET_9P|NET_9P_VIRTIO|EARLY_PRINTK|SERIAL_8250_CONSOLE)=y$' "${cfg}" | sed 's/^/[build-kernel] /' grep -E '^CONFIG_INITRAMFS_SOURCE=""$' "${cfg}" | sed 's/^/[build-kernel] /' file -b "${bin}" | grep -q 'Linux kernel x86 boot executable bzImage' \ || die "o artefato não é um bzImage x86" log "pronto. Agora: node scripts/setup.mjs && npm run serve" log "o boot real (v86/Chrome) NÃO é verificado aqui — é pair-test do operador"