feat(build): borealis-style multi-stage rootfs for the open guest components (#309)

## What

Adds `build/` — a Dockerfile with `mesa-build`, `nestri-build`,
`os-base`, `runtime`, `runtime_prod` and `runtime_debug` stages, plus
the `etc/` overlay, `mkimage.sh` and a `Makefile` — laid out the way
[borealis](https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/main/project-borealis)
lays out its own `build/`. This moves the guest rootfs formula into this
repo, targeting the four open guest components already here: `nescope`,
`neshub`, `neswire`, `nescapture`.

## Two structural properties worth calling out

- **No privileged host chroot.** A bare `chroot` into a hand-extracted
rootfs needs `/proc`, `/sys`, `/dev` bind-mounted in first. `os-base`
here is `FROM artixlinux/artixlinux:base-openrc` directly with `pacman
-S` as plain `RUN` steps — a Docker build step already has its own
`/proc`/`/sys`/`/dev`.
- **No host-side ownership bug to guard against.** `COPY --from=` runs
as root inside the build with no invoking-user uid in the loop.

## Scope boundary

**Deliberately excludes Proton and Valve's `steamclient.so`** — both
closed, and `CLAUDE.md` forbids closed content in this repo.
`runtime_prod`, tagged `nestrilabs/nestri:base`, is a complete,
bootable, Steam-less image — and also the shared foundation other builds
start from. Whatever layers Proton/Steam on top of it is a closed build
outside this repo, by design.

## Known gap

Nothing starts a payload yet — `nesinit` isn't open code — so
`/etc/init.d/nescope` boots it in plain-compositor mode (no command
after `--`) rather than running a game. Real and testable, just not a
full session yet. Details in `build/README.md`.

## Status

Built and tagged locally as `nestrilabs/nestri:base` (podman, no
`--no-cache` issues, greptile's three findings all fixed and verified
against a real build). Not yet packed into a disk image or run inside
nesbox.

🤖 Generated with [Claude Code](https://claude.com/claude-code)






<!-- greptile_comment -->

<h3>Greptile Summary</h3>

The PR adds a multi-stage Artix/OpenRC guest-rootfs build for the open
Nestri components, with production and debug image flavors.
- Builds patched Mesa and the Rust workspace in dedicated builder
stages.
- Assembles and configures the bootable guest environment and OpenRC
services.
- Packs a selected container image into an ext4 root filesystem while
retaining rootless container storage access.

<h3>Confidence Score: 5/5</h3>

The PR appears safe to merge.

No blocking failure remains.

<h3>Important Files Changed</h3>




| Filename | Overview |
|----------|----------|
| build/Dockerfile | Defines the complete multi-stage build, overlays
repository-root-relative configuration paths, and creates production and
debug runtime targets. |
| build/Makefile | Provides consistent image build and packing targets
using a repository-root context and matching image tags. |
| build/scripts/mkimage.sh | Keeps container-runtime operations in the
invoking user's storage while escalating only filesystem creation and
mounting operations. |
| build/etc/conf.d/nestri-user-env | Supplies the shared service
environment and export function required by the OpenRC service scripts.
|
| build/etc/init.d/guest-net | Configures optional guest networking from
kernel parameters or stable defaults. |
| build/etc/init.d/neswire | Starts the audio sink after its
dependencies and pins it as the PipeWire default once the graph is
ready. |


<h3>Flowchart</h3>

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Arch builder] --> B[Mesa build]
  A --> C[Nestri workspace build]
  D[Artix OpenRC base] --> E[Common runtime]
  B --> E
  C --> E
  F[build/etc overlay] --> E
  E --> G[runtime_prod]
  E --> H[runtime_debug]
  G --> I[Container image]
  H --> J[Debug container image]
  I --> K[mkimage.sh]
  J --> K
  K --> L[ext4 rootfs]
```

<sub>Reviews (6): Last reviewed commit: ["refactor(build): rename the
published
im..."](6fecc8cc31)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=58981739)</sub>

<!-- /greptile_comment -->
This commit is contained in:
KAAL1 (Bingus)
2026-09-01 15:05:48 +03:00
committed by GitHub
parent 84c97156f8
commit 270304bca5
26 changed files with 1190 additions and 1 deletions

67
build/scripts/mkimage.sh Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Pack a built container image into a raw ext4 disk for nesbox's virtio-blk.
#
# Docker/Buildx has no native "export a raw disk image" step, so this is the
# one part of the pipeline that still has to run outside the Dockerfile.
#
# Run this as yourself, not under sudo. Only mkfs/mount/umount actually need
# root, and those are escalated individually below — `sudo bash mkimage.sh`
# for the whole script is exactly the wrong shape for rootless Podman: the
# image `make build` produced lives in *your* rootless storage, and running
# `podman create` as root afterwards looks in root's separate storage, where
# the tag does not exist. `sudo -v` up front just avoids being prompted
# mid-script for the escalated calls that follow.
set -euo pipefail
IMAGE="${1:?usage: mkimage.sh <image-tag> <output-path> [size]}"
OUT="${2:?usage: mkimage.sh <image-tag> <output-path> [size]}"
SIZE="${3:-5G}"
if [[ "$(id -u)" -eq 0 ]]; then
echo "mkimage.sh should run as yourself, not root/sudo — see the comment at the top of this script" >&2
exit 1
fi
CONTAINER_RT="$(command -v docker || command -v podman || true)"
[[ -n "$CONTAINER_RT" ]] || { echo "Neither docker nor podman found in PATH" >&2; exit 1; }
sudo -v # cache credentials once, rather than prompting mid-pipeline
WORK="$(mktemp -d)"
cleanup() {
mountpoint -q "$WORK/mnt" 2>/dev/null && sudo umount "$WORK/mnt"
rm -rf "$WORK"
}
trap cleanup EXIT
echo "Exporting ${IMAGE}..."
cid="$("$CONTAINER_RT" create "$IMAGE")"
"$CONTAINER_RT" export "$cid" -o "$WORK/rootfs.tar"
"$CONTAINER_RT" rm -f "$cid" >/dev/null
echo "Creating ${SIZE} ext4 image at ${OUT}..."
mkdir -p "$(dirname "$OUT")"
truncate -s "$SIZE" "$OUT"
sudo mkfs.ext4 -q -L nestri-root "$OUT"
mkdir -p "$WORK/mnt"
sudo mount -o loop "$OUT" "$WORK/mnt"
# Same excludes as the old bootstrap extraction: .dockerenv is Docker's own
# marker file, and /dev is devtmpfs at boot, populated by the kernel — a
# tarred copy of the build container's /dev would just be dead weight.
#
# Root, deliberately: the rootfs's own files are owned by root (that is
# correct — it is the guest's root filesystem), and only root can write
# root-owned files onto the loop-mounted ext4.
sudo tar -xf "$WORK/rootfs.tar" -C "$WORK/mnt" --exclude='.dockerenv' --exclude='dev/*'
sudo umount "$WORK/mnt"
# The image *file* itself was created by `truncate` above as the invoking
# user and never needs to change hands — mkfs/mount/umount touch its
# contents, not its ownership. Asserted, not assumed, since a stray `sudo`
# reordering above would silently hand root ownership of a file the rest of
# this pipeline expects to read and delete without sudo.
[[ "$(stat -c '%U' "$OUT")" == "$(id -un)" ]] || {
echo "warning: ${OUT} is not owned by $(id -un) — sudo chown $(id -un) ${OUT}" >&2
}
echo "Wrote ${OUT}"