mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
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:
3
build/etc/conf.d/agetty.hvc0.debug
Normal file
3
build/etc/conf.d/agetty.hvc0.debug
Normal file
@@ -0,0 +1,3 @@
|
||||
baud="115200"
|
||||
term_type="vt100"
|
||||
agetty_options="--autologin root --noclear"
|
||||
3
build/etc/conf.d/agetty.hvc0.prod
Normal file
3
build/etc/conf.d/agetty.hvc0.prod
Normal file
@@ -0,0 +1,3 @@
|
||||
baud="115200"
|
||||
term_type="vt100"
|
||||
agetty_options="--noclear"
|
||||
16
build/etc/conf.d/nescope
Normal file
16
build/etc/conf.d/nescope
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configuration for nescope, the headless compositor.
|
||||
#
|
||||
# Sources nestri-user-env rather than symlinking it, for the same reason
|
||||
# neshub and neswire do.
|
||||
. /etc/conf.d/nestri-user-env
|
||||
|
||||
RUST_LOG="${RUST_LOG:-nescope=info}"
|
||||
export RUST_LOG
|
||||
|
||||
# Started in plain-compositor mode by /etc/init.d/nescope (no command after
|
||||
# `--`): it comes up and waits for something to connect rather than wrapping
|
||||
# a payload. Starting an actual payload — a game, a desktop — is nesinit's
|
||||
# job, and nesinit is not open code yet. See build/README.md.
|
||||
NESCOPE_SOCKET="${NESCOPE_SOCKET:-nescope-0}"
|
||||
NESCOPE_INPUT_IPC="${NESCOPE_INPUT_IPC:-/tmp/nestri-input.sock}"
|
||||
export NESCOPE_SOCKET NESCOPE_INPUT_IPC
|
||||
18
build/etc/conf.d/neshub
Normal file
18
build/etc/conf.d/neshub
Normal file
@@ -0,0 +1,18 @@
|
||||
# Configuration for neshub, the media hub.
|
||||
#
|
||||
# Not a symlink to nestri-user-env like pipewire and friends: neshub wants a
|
||||
# setting of its own, so it sources that file rather than replacing it — same
|
||||
# pattern as nescope and neswire below.
|
||||
. /etc/conf.d/nestri-user-env
|
||||
|
||||
# What neshub logs, and nothing else.
|
||||
#
|
||||
# Scoped and exported for the reason spelled out at length in the old
|
||||
# nestri-guest-hub conf.d this replaces: an unscoped `RUST_LOG=info` turns on
|
||||
# every crate linked in, iroh included, and OpenRC hands a service its own
|
||||
# environment, not the shell's locals — an unexported variable here goes
|
||||
# nowhere.
|
||||
#
|
||||
# Overridable, so a bad boot can be re-run with more without a rebuild.
|
||||
RUST_LOG="${RUST_LOG:-neshub=info}"
|
||||
export RUST_LOG
|
||||
30
build/etc/conf.d/nestri-user-env
Normal file
30
build/etc/conf.d/nestri-user-env
Normal file
@@ -0,0 +1,30 @@
|
||||
HOME="/home/nestri"
|
||||
USER="nestri"
|
||||
LOGNAME="nestri"
|
||||
XDG_RUNTIME_DIR="/run/user/1000"
|
||||
XDG_CONFIG_HOME="/home/nestri/.config"
|
||||
XDG_DATA_HOME="/home/nestri/.local/share"
|
||||
XDG_CACHE_HOME="/home/nestri/.cache"
|
||||
XDG_STATE_HOME="/home/nestri/.local/state"
|
||||
XDG_DATA_DIRS="/usr/local/share:/usr/share"
|
||||
XDG_SESSION_TYPE="wayland"
|
||||
XDG_SESSION_DESKTOP="nestri"
|
||||
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
|
||||
|
||||
# Driver forcing, mirrored from /etc/profile.d/nestri-env.sh. That file only
|
||||
# runs for login shells, and nothing here is a login shell — every service
|
||||
# below runs from OpenRC. Capture happens through a Vulkan layer, so a
|
||||
# process that reached the GPU via native OpenGL would render fine and
|
||||
# capture nothing; zink is what makes such a process capturable at all.
|
||||
# These are load-bearing. Keep them in step with the profile.d copy.
|
||||
#__GLX_VENDOR_LIBRARY_NAME="mesa"
|
||||
#MESA_LOADER_DRIVER_OVERRIDE="zink"
|
||||
#GALLIUM_DRIVER="zink"
|
||||
|
||||
nestri_export_env() {
|
||||
export HOME USER LOGNAME
|
||||
export XDG_RUNTIME_DIR XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME
|
||||
export XDG_DATA_DIRS XDG_SESSION_TYPE XDG_SESSION_DESKTOP
|
||||
export DBUS_SESSION_BUS_ADDRESS
|
||||
#export __GLX_VENDOR_LIBRARY_NAME MESA_LOADER_DRIVER_OVERRIDE GALLIUM_DRIVER
|
||||
}
|
||||
32
build/etc/conf.d/neswire
Normal file
32
build/etc/conf.d/neswire
Normal file
@@ -0,0 +1,32 @@
|
||||
# Configuration for the audio wire.
|
||||
#
|
||||
# Not a symlink to nestri-user-env like pipewire and friends, for the same
|
||||
# reason neshub is not: it needs a setting of its own -- and it still wants
|
||||
# that file's environment, so it sources it rather than replacing it.
|
||||
. /etc/conf.d/nestri-user-env
|
||||
|
||||
# What neswire logs, and nothing else.
|
||||
#
|
||||
# Without this neswire is *silent* -- it starts, fails, and exits leaving an
|
||||
# empty log, which is indistinguishable from never having been started at
|
||||
# all. That ambiguity is the reason this file exists.
|
||||
#
|
||||
# Scoped, and `export`ed, for the reasons spelled out at length in
|
||||
# conf.d/neshub: an unscoped `info` turns on every linked crate, and an
|
||||
# unexported one reaches the service's shell and not the service.
|
||||
#
|
||||
# Overridable, so a bad boot can be re-run with more without a rebuild.
|
||||
RUST_LOG="${RUST_LOG:-neswire=info}"
|
||||
export RUST_LOG
|
||||
|
||||
# neswire's own defaults, spelled out rather than inherited.
|
||||
#
|
||||
# All four are clap args with defaults (`neswire --help`), so the service
|
||||
# works without them. They are here because the IPC path is a contract with
|
||||
# neshub -- it reads this socket -- and a contract that lives only inside two
|
||||
# binaries' default values is one nobody can check.
|
||||
NESWIRE_IPC_PATH="/tmp/nestri-audio.sock"
|
||||
NESWIRE_CHANNELS="2"
|
||||
NESWIRE_PACKET_DURATION_MS="5"
|
||||
NESWIRE_BITRATE_PER_CHANNEL="64"
|
||||
export NESWIRE_IPC_PATH NESWIRE_CHANNELS NESWIRE_PACKET_DURATION_MS NESWIRE_BITRATE_PER_CHANNEL
|
||||
Reference in New Issue
Block a user