Files
netris-nestri/build/etc/init.d/neswire
KAAL1 (Bingus) 270304bca5 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 -->
2026-09-01 15:05:48 +03:00

94 lines
3.3 KiB
Plaintext

#!/sbin/openrc-run
description="Nestri pipewire audio sink"
nestri_export_env
: "${NESTRI_UID:=1000}"
: "${NESTRI_USER:=nestri}"
command="/usr/bin/neswire"
command_user="${NESTRI_USER}:${NESTRI_USER}"
command_background="yes"
pidfile="/run/nestri/neswire.pid"
output_log="/nestri/logs/neswire.log"
error_log="/nestri/logs/neswire.log"
respawn="yes"
respawn_delay="2"
respawn_max="2"
export XDG_RUNTIME_DIR="/run/user/${NESTRI_UID}"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${NESTRI_UID}/bus"
depend() {
# wireplumber is a hard dependency, not a nicety: start_post below pins
# the default sink through the `default` metadata object, and
# WirePlumber is what owns that object. Started concurrently, the pin
# lands in an object pw-metadata created itself, which is destroyed the
# moment it exits.
need xdg-runtime dbus dbus-session pipewire wireplumber
use neshub
after pipewire wireplumber neshub
}
start_pre() {
checkpath -d -m 0755 -o "${NESTRI_USER}:${NESTRI_USER}" /run/nestri
}
# Point the graph at neswire's sink, by name.
#
# `neswire` registers itself as media.class = Audio/Sink, node.name = neswire
# ("Neswire Cloud Gaming Audio Sink"). With the hardware monitors off it is
# the only sink, so WirePlumber's find-best hook should land on it anyway --
# this makes it explicit rather than a consequence of there being nothing
# else, so adding a second sink later cannot silently steal the default.
#
# `default.configured.audio.sink` is the metadata WirePlumber's find-selected
# hook reads, and it takes a name; `wpctl set-default` takes a numeric object
# id that changes every boot, which is why this uses pw-metadata instead.
#
# Not fatal if it fails: find-best still has one candidate. A game playing
# into the wrong sink is silent, and a warning here is the only place that
# would say so.
start_post() {
local i=0
while [ $i -lt 50 ]; do
pw-dump 2>/dev/null | grep -q '"node.name": "neswire"' && break
sleep 0.1
i=$((i+1))
done
if ! pw-dump 2>/dev/null | grep -q '"node.name": "neswire"'; then
ewarn "neswire started but its sink never appeared in the graph"
return 0
fi
# Wait for WirePlumber to own the `default` metadata before writing to it.
#
# `need wireplumber` only guarantees its script returned, not that it has
# built its objects. Writing too early is silently useless rather than an
# error: pw-metadata creates the object, sets the key, exits 0, and the
# object dies with the client. So wait for the object to exist, and say
# so if it never does.
i=0
while [ $i -lt 50 ]; do
pw-metadata -n default >/dev/null 2>&1 && break
sleep 0.1
i=$((i+1))
done
if ! pw-metadata -n default >/dev/null 2>&1; then
ewarn "no 'default' metadata: WirePlumber is not running, sink not pinned"
return 0
fi
pw-metadata -n default 0 default.configured.audio.sink '{ "name": "neswire" }' \
>/dev/null 2>&1
# Read it back. A write that did not stick is the failure this whole
# sequence exists to catch, and it is invisible unless checked.
if pw-metadata -n default 2>/dev/null | grep -q "neswire"; then
einfo "default sink pinned to neswire"
else
ewarn "pinned neswire as default sink but it did not stick"
fi
}