Files
netris-nestri/build
Wanjohi a76ca9ae81 fix(build): the root really is read-only, and two comments that overclaimed
Three places where build/ said something the tree does not do.

- fstab mounted `/` as `rw`. A box is started with `ro` on the kernel
  command line and `is_read_only: true` on the root device (every config in
  nesbox's tree agrees: examples/vm.json, test.json, run.local.json), so the
  virtio-blk device rejects writes whatever fstab asks for. `rw` here only
  made OpenRC's `root` service attempt a remount that has to fail. The
  Dockerfile already depended on the truth — it pre-creates /nestri/* at
  build time precisely because a runtime mkdir gets EROFS — so this makes
  fstab agree with the comment that was already right.

- build/README.md said nesbox's jail image "extracts Mesa and virglrenderer"
  from this base. It extracts only Mesa. virglrenderer is the host half of
  the native-context protocol and nesbox builds its own, patched, from
  nesbox/patches/; nothing in this image carries it at all.

- conf.d/nestri-user-env called the zink driver-forcing block "load-bearing"
  directly above three exports that are commented out, here and in the
  profile.d copy. Whether they should come back is a separate question; a
  comment insisting disabled lines are load-bearing tells the next reader
  the opposite of what the file does. The reasoning is kept, because it is
  still the reason to re-enable them, along with why both copies have to
  move together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 15:26:59 +03:00
..

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:

  1. No privileged host chroot. A bare chroot into a hand-extracted rootfs needs /proc, /sys, /dev bind-mounted in first — they don't exist inside a chroot target until something puts them there. os-base here is FROM artixlinux/artixlinux:base-openrc directly, with pacman -S as plain RUN steps — 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.

  2. 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.

  3. One cargo build --release --workspace, not one stage per binary. nescope, neshub, neswire and nescapture share one Cargo workspace and one Cargo.lock — a BuildKit cache mount on target/ gives cargo's own incremental compiler per-crate isolation without needing a separate Docker stage (and a separate full rebuild of nesprotocol) 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 jail image (see nesbox/build/) extracts Mesa from it so the guest and host sides of the virtio-gpu native-context protocol never drift apart. Only Mesa — virglrenderer is the host half of that protocol and nesbox builds its own, patched, from nesbox/patches/; nothing in this image carries it.

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.