## 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 -->
4.7 KiB
build/ — the guest rootfs
Builds a bootable Artix/OpenRC image for the box's virtio-blk root: Mesa
(virtio-gpu native context) plus the four open guest components —
nescope, neshub,
neswire, nescapture — laid out
the way borealis
lays out its build/: one big multi-stage Dockerfile, --target picks the
flavor, etc/ holds the files that get overlaid onto the image verbatim.
build/
├── Dockerfile everything, in stages: mesa-build, nestri-build,
│ os-base, runtime, runtime_prod, runtime_debug
├── etc/ overlaid onto the image's /etc as-is
├── scripts/
│ └── mkimage.sh docker export → raw ext4, for nesbox's virtio-blk
├── Makefile
└── output/ `make image` writes here (gitignored)
make build # docker build --target runtime_prod → ghcr.io/nestrilabs/nestri/base:latest
make build-debug # docker build --target runtime_debug → ghcr.io/nestrilabs/nestri/base:debug
make image # + pack into output/rootfs.ext4
make image-debug # + pack into output/rootfs-debug.ext4
Design notes
Three things worth knowing about how this is put together:
-
No privileged host chroot. A bare
chrootinto a hand-extracted rootfs needs/proc,/sys,/devbind-mounted in first — they don't exist inside a chroot target until something puts them there.os-basehere isFROM artixlinux/artixlinux:base-openrcdirectly, withpacman -Sas plainRUNsteps — a Docker build step already runs inside a real container with its own/proc,/sys,/dev, so that whole bind-mount mechanism has nothing to do. -
No host-side ownership bug to guard against.
COPY --from=runs as root inside the build with no host user in the loop, so there's no invoking-user uid getting stamped onto/,/usr/bin, or anywhere else a build step touches — a failure mode some overlay approaches need an explicit sanity check for doesn't exist here to check for. -
One
cargo build --release --workspace, not one stage per binary.nescope,neshub,neswireandnescaptureshare one Cargo workspace and oneCargo.lock— a BuildKit cache mount ontarget/gives cargo's own incremental compiler per-crate isolation without needing a separate Docker stage (and a separate full rebuild ofnesprotocol) per binary.
What is deliberately not here: Proton, and Valve's steamclient.so.
nestri/CLAUDE.md is explicit — "Nothing closed may enter this repo. Not
source, not a dependency, not a directory that 'looked convenient'." Both
are closed. runtime_prod from this Dockerfile — tagged
ghcr.io/nestrilabs/nestri/base:latest — is a complete, bootable, Steam-less guest image,
and also the shared foundation other builds start from: nesbox's jailer
image (see nesbox/build/) extracts Mesa and virglrenderer from it so the
guest and host sides of the virtio-gpu native-context protocol never drift
apart. Whatever layers Proton and the Steam client on top of it is a closed
build outside this repo, by design — not something this repo names, links
to, or depends on.
The nesinit gap
Nothing in this image starts a payload. The old nestri-guest-hub did that
— per its own commit message, the open neshub "loses --proton,
--steamclient-so, --root and the game uid/gid, and no longer ends by
handing the process to a controller... Deciding when the box is finished
belongs to nesinit." nesinit — the guest init/session-supervisor that
would actually launch nescope -- <payload> and power the box down — is
referenced in commit messages and nesbox/PROGRESS.md but does not exist as
open code in either repo.
So /etc/init.d/nescope here starts nescope in plain-compositor mode
(no command after --): it comes up, provides the Wayland/X11 environment,
and waits for something to connect. That makes the image genuinely bootable
and testable — neshub, neswire, nescope all come up under OpenRC and
you get a real Wayland socket to point a client at — but running an actual
game is still nesinit's job, and nesinit isn't part of this build. Whoever
picks that up next should read apps/neshub/README.md's "What it does not
do" section first.
Network defaults
/etc/init.d/guest-net reads nestri.ip=/nestri.gw= off the kernel
command line, falling back to 172.30.0.2/24 via 172.30.0.1 if neither is
set — nesbox's own default tap addressing. Keep these in step if that
changes on the nesbox side.