mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
feat: resident guest init (#333)
Get this thing going..
<!-- greptile_comment -->
<!-- greptile_summary -->
<h2><a
href="https://app.greptile.com/api/retrigger?id=63134761"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=1"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"
align="right"></picture></a>Confidence Score: 5/5</h2>
The PR appears safe to merge; all previous findings are resolved and the
latest readiness change introduces no established actionable regression.
<h3>Summary</h3>
- Establishes required guest filesystems, runtime directories, device
permissions, and service processes.
- Reports initialization and service deaths over the lifecycle channel.
- Supports launch, restart, and shutdown commands for a resident guest.
- Separates service and workload identities and configures per-launch
runtime environments.
- Removes the currently inactive nescope screenshot option and makes
capture-chain verification fail explicitly when compositor readback is
unavailable.
- Reworks the guest image around `nesinit` as PID 1 without a
distribution service manager.
<h3>Diagram</h3>
```mermaid
sequenceDiagram
participant Host
participant Init as nesinit
participant FS as Guest filesystems
participant Services as Service stack
participant Workload
Init->>Host: Ready(protocol version)
Host->>Init: Boot(mount descriptors)
Init->>FS: Establish and mount shares
Init->>Services: Spawn services in order
Services-->>Init: Required sockets ready
Init->>Host: Initialized(service names)
Host->>Init: Launch(id, exec, on_exit)
Init->>Workload: Spawn with isolated UID/runtime
Init->>Host: Started(id)
Workload-->>Init: Exit status
Init->>Host: WorkloadExited(id, status)
Host->>Init: Launch / Restart / Shutdown
```
<sub>Reviews (4) · Last reviewed commit: ["fix(nesinit): readiness is a
socket
that..."](731d34df9d)</sub>
<!-- /greptile_comment -->
---------
Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
ec8b13d0c9
commit
8246aa5538
573
build/Containerfile
Normal file
573
build/Containerfile
Normal file
@@ -0,0 +1,573 @@
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# nestri guest rootfs — the open half
|
||||
#
|
||||
# Builds a bootable Arch image containing Mesa (virtio-gpu native context)
|
||||
# and the five open guest components: nesinit, nescope, neshub, neswire,
|
||||
# nescapture. Two leaf targets, selected with `--target`:
|
||||
#
|
||||
# runtime_prod stripped, root locked (default: `make build`)
|
||||
# runtime_debug debug tools, autologin root (`make build-debug`)
|
||||
#
|
||||
# There is no service manager, no init scripts and no udev. `nesinit` is PID 1
|
||||
# and brings the box's services up from a table compiled into it, which is why
|
||||
# this image is plain Arch rather than a distribution chosen for its init.
|
||||
# ref(d-0064)
|
||||
#
|
||||
# Proton is here, and it is not a closed component: it is proton-cachyos built
|
||||
# from source with --enable-wow64, which is what removes the need for a whole
|
||||
# 32-bit host stack. Valve's steamclient.so is a different thing and is NOT
|
||||
# here — that one is closed, and nestri/CLAUDE.md is explicit that nothing
|
||||
# closed enters this repo. Whatever layers it on top of runtime_prod is a
|
||||
# closed build outside this repo — see build/README.md.
|
||||
#
|
||||
# Build from the repo root, not from build/:
|
||||
# docker build -f build/Containerfile --target runtime_prod -t nestri-guest .
|
||||
# (`make build` in this directory does exactly that.)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
# Declared here and not beside the stage that uses it, because an ARG a FROM
|
||||
# expands has to precede the *first* FROM in the file. Anywhere else it is
|
||||
# scoped to one stage instead, `FROM ${PROTON_IMAGE}` expands to nothing, and
|
||||
# the build fails with "no FROM statement found" — which says nothing about
|
||||
# the actual mistake. See the Proton stage below for what this is.
|
||||
ARG PROTON_IMAGE=ghcr.io/nestrilabs/proton-cachyos-native-wow64:11.0-20260703
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# initial / builder
|
||||
#
|
||||
# The same distribution the guest is now, which it did not use to be: the
|
||||
# guest was Artix, chosen for an init system this image no longer contains.
|
||||
# Only build artifacts leave these stages.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM docker.io/archlinux:base-devel AS initial
|
||||
RUN pacman -Syu --noconfirm
|
||||
|
||||
FROM initial AS builder
|
||||
RUN pacman -S --noconfirm --needed \
|
||||
cmake meson ninja git pkgconf \
|
||||
python python-mako python-yaml python-packaging python-ply \
|
||||
bison flex \
|
||||
libpciaccess libepoxy libglvnd \
|
||||
libx11 libxext libxrandr libxshmfence libxfixes libxxf86vm libxcb \
|
||||
xcb-util-keysyms xorgproto \
|
||||
wayland wayland-protocols \
|
||||
expat zlib zstd libxml2 lm_sensors \
|
||||
llvm clang libclc spirv-tools spirv-llvm-translator glslang \
|
||||
elfutils libva libdrm directx-headers \
|
||||
rust rust-bindgen cbindgen \
|
||||
curl openssl \
|
||||
pixman libxkbcommon \
|
||||
vulkan-headers vulkan-icd-loader \
|
||||
pipewire shaderc opus \
|
||||
libinput \
|
||||
&& pacman -Scc --noconfirm
|
||||
WORKDIR /build
|
||||
ENV ARTIFACTS=/artifacts
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# Mesa — the only piece still fetched from outside this tree
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM builder AS mesa-build
|
||||
|
||||
ARG MESA_GIT=https://gitlab.freedesktop.org/mesa/mesa.git
|
||||
ARG MESA_COMMIT=b316485dd75ca6ab6c16c113480fb94c57d86c95
|
||||
ARG JOBS=
|
||||
|
||||
RUN test -n "$JOBS" || JOBS=$(nproc) && \
|
||||
git clone --depth=1 --revision="${MESA_COMMIT}" "${MESA_GIT}" /build/mesa-src && \
|
||||
cd /build/mesa-src && \
|
||||
meson setup builddir \
|
||||
-Dprefix=/usr \
|
||||
-Dbuildtype=release \
|
||||
-Dplatforms=wayland \
|
||||
-Dgallium-drivers=zink \
|
||||
-Dvulkan-drivers=amd,intel \
|
||||
-Damdgpu-virtio=true \
|
||||
-Dintel-virtio-experimental=true \
|
||||
-Dvideo-codecs=all \
|
||||
-Degl=disabled \
|
||||
-Dglx=disabled \
|
||||
-Dgles1=disabled \
|
||||
-Dgles2=disabled \
|
||||
-Dgbm=disabled \
|
||||
-Dgallium-va=disabled \
|
||||
-Db_ndebug=true && \
|
||||
ninja -C builddir -j${JOBS:-$(nproc)} && \
|
||||
DESTDIR=/artifacts/mesa ninja -C builddir install && \
|
||||
rm -rf /build/mesa-src && \
|
||||
find /artifacts/mesa -type f -printf '/%P\n' > /artifacts/mesa/.manifest
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# nestri workspace — same repo now, so this is COPY, not a private clone
|
||||
#
|
||||
# One `cargo build --release` over the guest members rather than one stage per
|
||||
# binary:
|
||||
# that per-repo splitting existed because nescope/neswire/nescapture/the hub
|
||||
# were four separate private repos and a stage boundary was the only way to
|
||||
# stop bumping one from invalidating the others' build cache. They are one
|
||||
# Cargo workspace with one Cargo.lock now, so a BuildKit cache mount on
|
||||
# target/ gives the same isolation — cargo's own incremental compiler
|
||||
# already knows nescope changing does not touch nesprotocol's .rlib — without
|
||||
# four copies of every shared dependency getting compiled once per stage.
|
||||
#
|
||||
# The members are named rather than `--workspace`, because the workspace holds
|
||||
# one crate that is not part of a guest — `nesdoctor` runs on a stranger's own
|
||||
# machine — and building it here would compile something this image will never
|
||||
# contain. Naming them also means adding a member does not silently add a
|
||||
# binary to the image.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM builder AS nestri-src
|
||||
WORKDIR /build/nestri
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/nesprotocol crates/nesprotocol
|
||||
# Not a guest component and not installed below — it runs on a stranger's own
|
||||
# machine. It is here because `cargo` loads every workspace member's manifest
|
||||
# before it builds anything, so a member missing from the context fails the
|
||||
# build outright with `failed to read .../Cargo.toml`. Copying it costs a few
|
||||
# files; the member list is the thing that decides, not this build.
|
||||
COPY apps/nesdoctor apps/nesdoctor
|
||||
COPY apps/nesinit apps/nesinit
|
||||
COPY apps/nescope apps/nescope
|
||||
COPY apps/neshub apps/neshub
|
||||
COPY apps/neswire apps/neswire
|
||||
COPY apps/nescapture apps/nescapture
|
||||
|
||||
FROM nestri-src AS nestri-build
|
||||
RUN --mount=type=cache,target=/root/.cargo/registry \
|
||||
--mount=type=cache,target=/build/nestri/target \
|
||||
cargo build --release \
|
||||
-p nesinit -p nescope -p neshub -p neswire -p nescapture && \
|
||||
mkdir -p /artifacts/nestri/usr/bin /artifacts/nestri/usr/lib \
|
||||
/artifacts/nestri/usr/share/vulkan/implicit_layer.d && \
|
||||
install -Dm755 target/release/nesinit /artifacts/nestri/usr/bin/nesinit && \
|
||||
install -Dm755 target/release/nescope /artifacts/nestri/usr/bin/nescope && \
|
||||
install -Dm755 target/release/neshub /artifacts/nestri/usr/bin/neshub && \
|
||||
install -Dm755 target/release/neswire /artifacts/nestri/usr/bin/neswire && \
|
||||
install -Dm755 target/release/libnescapture_layer.so \
|
||||
/artifacts/nestri/usr/lib/libnescapture_layer.so && \
|
||||
install -Dm644 apps/nescapture/manifest/VK_LAYER_nescapture.json \
|
||||
/artifacts/nestri/usr/share/vulkan/implicit_layer.d/VK_LAYER_nescapture.json && \
|
||||
find /artifacts/nestri -type f -printf '/%P\n' > /artifacts/nestri/.manifest
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# Proton — pulled, not built here
|
||||
#
|
||||
# Building it takes hours and it changes only when its own tag moves, so it
|
||||
# has a cadence of its own and an image of its own. The published image is
|
||||
# `FROM scratch` over the tree, so its root *is* the tree and there is nothing
|
||||
# in it to run — only something to copy from.
|
||||
#
|
||||
# Built with `--enable-wow64`, which is the whole reason it is a build of ours
|
||||
# rather than the distribution's package. wow64 runs 32-bit Windows code
|
||||
# inside a 64-bit unix process, so a box needs no lib32 anything: no 32-bit
|
||||
# glibc, no second Mesa for i686, and — the one that matters most here — no
|
||||
# second capture layer, because the game is a 64-bit process and loads the
|
||||
# 64-bit Vulkan loader the existing layer already sits in. The distribution's
|
||||
# package is built without the flag, which is exactly why it depends on
|
||||
# lib32-*.
|
||||
#
|
||||
# Override to build it yourself; the tag is a version and moves deliberately.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM ${PROTON_IMAGE} AS proton
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# os-base — the Arch rootfs itself
|
||||
#
|
||||
# `FROM archlinux:base` directly, and `pacman -S` as plain RUN steps — not a
|
||||
# privileged host `chroot` into a hand-extracted tarball, which would need
|
||||
# /proc, /sys and /dev bind-mounted in first (they don't exist inside a chroot
|
||||
# target until something puts them there). A Containerfile RUN step already
|
||||
# executes inside a real container with its own /proc, /sys, /dev, so there is
|
||||
# no bind-mount step to write at all.
|
||||
#
|
||||
# # systemd goes; systemd-libs stays
|
||||
#
|
||||
# Nothing in the package list below depends on `systemd`, and two things in it
|
||||
# — dbus-daemon and wireplumber — link `libsystemd.so.0`, which comes from the
|
||||
# separate `systemd-libs` package. So the removal is `-Rdd` of `systemd` and
|
||||
# `systemd-sysvcompat` only, which is normal rather than a compromise: keeping
|
||||
# the library while having no PID 1 from it is exactly how a distribution
|
||||
# without systemd ships these same programs.
|
||||
#
|
||||
# Removing the package also removes its pacman hooks, which is the point. The
|
||||
# hooks call `systemd-tmpfiles`, `systemd-sysusers` and `udevadm`; leaving them
|
||||
# behind while deleting what they call is how a later transaction fails
|
||||
# obscurely, and a previous attempt at this image lost two services to exactly
|
||||
# that.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM docker.io/archlinux:base AS os-base
|
||||
|
||||
# The base image ships an unsigned local keyring, so upgrading
|
||||
# `archlinux-keyring` runs a hook that reports `There is no secret key
|
||||
# available to sign with` and then `error: command failed to execute
|
||||
# correctly`. It is cosmetic and it is also every Arch container's build log.
|
||||
# One line fixes it, and it is worth the seconds: a build that always prints an
|
||||
# error is a build nobody reads an error out of.
|
||||
RUN pacman-key --init
|
||||
|
||||
# Installed first and removed second, so every dependency resolves normally
|
||||
# before anything is taken out from under it.
|
||||
#
|
||||
# Gone with the init system: `openrc`, `udev`, `dbus-openrc`. `udev` is not
|
||||
# replaced by anything — `devtmpfs` creates the nodes and init sets the two
|
||||
# modes that matter, because the compositor takes input through Wayland and
|
||||
# opens nothing udev provides. ref(d-0064)
|
||||
#
|
||||
# `logrotate` is also gone, and that one is not about the init system: a box
|
||||
# keeps no logs to rotate. What is worth reading leaves over the control
|
||||
# channel, and `/var/log` is a small tmpfs that is discarded with the box.
|
||||
# `mesa` is not in this list, and the two `--assume-installed` flags are why.
|
||||
#
|
||||
# The distribution's Mesa used to be installed so that every runtime dependency
|
||||
# of *a* Mesa was present and correctly versioned, and ours was then overlaid
|
||||
# on top. That worked for the unversioned filenames and not for the versioned
|
||||
# one: `libgallium-<version>.so` from the package sat beside ours, 53 MB of it,
|
||||
# referenced by nothing. Installing it to overwrite most of it was always the
|
||||
# roundabout way round; telling pacman the dependency is already satisfied is
|
||||
# the direct one.
|
||||
#
|
||||
# Exactly two flags are needed and both were checked by dropping each in turn:
|
||||
# `mesa` is the name two packages depend on, and `opengl-driver` is a virtual
|
||||
# provide `libglvnd` requires that only a real driver package satisfies. The
|
||||
# other three names Mesa provides — `mesa-libgl`, `libva-driver`,
|
||||
# `libva-mesa-driver` — change nothing here, so they are not listed.
|
||||
#
|
||||
# What makes this safe is that our Mesa is a superset for this image's
|
||||
# purposes: it builds the drivers a box can actually use and the package's
|
||||
# other ones (apple, asahi, armada, d3d12) are for hardware no box has. What it
|
||||
# does *not* build is a software rasteriser, so there is no llvmpipe fallback —
|
||||
# a box with no working GPU path now fails instead of rendering slowly, which
|
||||
# is the honest outcome for something that exists to stream frames.
|
||||
#
|
||||
# Two packages below are explicit *because* Mesa is gone, and both used to
|
||||
# arrive as its dependencies: `llvm-libs`, which the radeonsi driver links for
|
||||
# shader compilation, and `lm_sensors`, which it links for `libsensors.so.5`.
|
||||
# The second was found by the check further down rather than by reading the
|
||||
# list — dropping a package takes its dependency tree with it, and the part of
|
||||
# that tree something else was quietly using is not visible from here.
|
||||
RUN pacman -Syu --noconfirm --needed \
|
||||
--assume-installed mesa --assume-installed opengl-driver \
|
||||
dbus \
|
||||
iptables iproute2 \
|
||||
libglvnd libdrm libepoxy libxxf86vm libinput wayland \
|
||||
expat zlib llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \
|
||||
pixman libxkbcommon xcb-util-keysyms xorg-xwayland \
|
||||
pipewire pipewire-audio wireplumber opus \
|
||||
python libunwind \
|
||||
&& rm -f /usr/share/libalpm/hooks/dbus-reload.hook \
|
||||
&& pacman -Rdd --noconfirm systemd systemd-sysvcompat \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
# `libunwind` is Wine's, not ours. `ntdll.so` links it, so without it every
|
||||
# process Wine starts dies at `could not load ntdll.so`, which is the first
|
||||
# thing it loads and reads like Wine itself being broken. Found 2026-09-12,
|
||||
# after the prefix had already been created -- so the session got past every
|
||||
# check that Proton was present and usable.
|
||||
#
|
||||
# `python` is not a build dependency here -- the builder stage has its own for
|
||||
# Mesa -- it is a *runtime* one. The compatibility tool's entry point is a
|
||||
# Python script, so a box without an interpreter starts a game and the launch
|
||||
# ends with `env: 'python3': No such file or directory` and an exit status that
|
||||
# reads like an ordinary finish. Found 2026-09-12, on the first session that
|
||||
# got as far as launching one.
|
||||
|
||||
# `dbus-reload.hook` is deleted above, before the removal rather than after,
|
||||
# and it is the whole reason that line is there: the hook runs
|
||||
# `/usr/share/libalpm/scripts/systemd-hook`, which systemd owns, so the
|
||||
# transaction that removes systemd trips its own leftover on the way out —
|
||||
# `call to execv failed`, then `error: command failed to execute correctly`.
|
||||
# pacman treats a post-transaction hook failure as non-fatal, so the build
|
||||
# survives it and the image is fine; what it leaves is an error message in
|
||||
# every future transaction and a reader with no way to tell it from a real
|
||||
# one. Removing the hook first means the error never happens.
|
||||
|
||||
# Nothing left may point at a program that is not here.
|
||||
#
|
||||
# The specific case above is fixed; this is the general one, and it exists
|
||||
# because a hook calling a deleted binary is the exact shape of the failure
|
||||
# that took two services off a previous version of this image. A build error
|
||||
# is a much better place to find the next one than a log.
|
||||
RUN for hook in /usr/share/libalpm/hooks/*.hook; do \
|
||||
exec_line="$(awk -F'= *' '/^Exec/ { print $2; exit }' "$hook")"; \
|
||||
program="${exec_line%% *}"; \
|
||||
case "$program" in /*) ;; *) continue ;; esac; \
|
||||
test -e "$program" \
|
||||
|| { echo "$(basename "$hook") runs $program, which is not in the image" >&2; exit 1; }; \
|
||||
done
|
||||
|
||||
# The check, because the removal above is the kind of thing a later `pacman
|
||||
# -Syu` undoes quietly. A box with systemd's PID 1 back in it boots something
|
||||
# other than `nesinit`, and the symptom is a guest that never dials out.
|
||||
RUN test ! -e /usr/lib/systemd/systemd \
|
||||
|| { echo "systemd's PID 1 is back in the image" >&2; exit 1; }
|
||||
|
||||
# `groupadd -f`, because some of these already exist in the base image and the
|
||||
# rest have to. Nothing creates them at runtime any more: udev's rules did that
|
||||
# for device nodes, and with udev gone init sets the two modes that matter
|
||||
# directly. ref(d-0064)
|
||||
#
|
||||
# **Two users, and they must stay two.** `nestri` runs the services that come
|
||||
# with this image; `nesplay` is who a workload runs as. Sharing one user between
|
||||
# them is what lets workload code impersonate a service — it can replace the
|
||||
# socket a service listens on and answer in its place, and the answer that
|
||||
# matters is the address a client is told to connect to. Init refuses an address
|
||||
# served by the workload's own user, so a single shared user does not merely
|
||||
# weaken that check, it makes every session fail it.
|
||||
#
|
||||
# The uid a workload actually runs as is chosen by whoever asks for the box, not
|
||||
# here; this account exists so that the number has a home, a shell and a name in
|
||||
# `ps`, and so the separation has somewhere to be written down.
|
||||
RUN groupadd -f audio && groupadd -f video && groupadd -f input && groupadd -f render && \
|
||||
useradd -m -u 1000 -s /bin/bash nestri && \
|
||||
for g in audio video input render; do gpasswd -a nestri "$g" >/dev/null; done && \
|
||||
useradd -m -u 1001 -s /bin/bash nesplay && \
|
||||
for g in audio video input render; do gpasswd -a nesplay "$g" >/dev/null; done
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# runtime — everything common to debug and prod
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM os-base AS runtime
|
||||
|
||||
# This is what GHCR actually uses to connect a pushed image back to its
|
||||
# repo — not a setting to toggle after the fact, a label the image has to
|
||||
# carry. Without it a manually-pushed image shows no "used by" repo on its
|
||||
# package page even though this Containerfile is exactly what built it.
|
||||
LABEL org.opencontainers.image.source="https://github.com/nestrilabs/nestri"
|
||||
|
||||
# Our own builds, overlaid on the distro's mesa. The distro package landed
|
||||
# first (above) so every runtime dependency of *a* Mesa is present and
|
||||
# correctly versioned; this overwrites its .so files with ours.
|
||||
#
|
||||
# COPY --from runs as root inside this build with no invoking-user uid to
|
||||
# stamp onto / or /usr/bin, unlike a host-side `podman cp` + `cp -a` — so
|
||||
# there is no ownership-sanity-check to write here. Nothing to catch, on
|
||||
# purpose, not an oversight.
|
||||
COPY --from=mesa-build /artifacts/mesa/.manifest /tmp/mesa.manifest
|
||||
COPY --from=nestri-build /artifacts/nestri/.manifest /tmp/nestri.manifest
|
||||
RUN cat /tmp/mesa.manifest /tmp/nestri.manifest > /tmp/.strip-manifest && \
|
||||
rm -f /tmp/mesa.manifest /tmp/nestri.manifest
|
||||
COPY --from=mesa-build /artifacts/mesa /
|
||||
COPY --from=nestri-build /artifacts/nestri /
|
||||
|
||||
# The Proton tree, whose image root is the tree, so this lands it at
|
||||
# /usr/share/steam/compatibilitytools.d/proton-cachyos.
|
||||
#
|
||||
# Deliberately not in the strip manifest above: that list is our own build
|
||||
# output, and the two stripping decisions are not the same one. Proton ships
|
||||
# a Windows toolchain's worth of PE binaries that `strip` has no business
|
||||
# touching, and its unix side is already built the way its own packaging
|
||||
# builds it.
|
||||
COPY --from=proton / /
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
COPY build/etc/ /etc/
|
||||
|
||||
# `nesinit` is PID 1, and `/usr/bin/init` is the fallback for a kernel started
|
||||
# without an explicit `init=`. `systemd-sysvcompat` used to own that path and
|
||||
# was removed with the rest of systemd, so nothing else claims it.
|
||||
RUN ln -sf nesinit /usr/bin/init
|
||||
|
||||
# One id per boot, not one per image.
|
||||
#
|
||||
# `dbus-uuidgen --ensure=/etc/machine-id` used to run here, which baked one id
|
||||
# into the image and made every box built from it the same machine. Init writes
|
||||
# a fresh one to /run at boot instead, so both of these are symlinks into a
|
||||
# tmpfs — which is also the only place they could be, with a read-only root.
|
||||
RUN rm -f /etc/machine-id /var/lib/dbus/machine-id && \
|
||||
mkdir -p /var/lib/dbus && \
|
||||
ln -sf /run/machine-id /etc/machine-id && \
|
||||
ln -sf /run/machine-id /var/lib/dbus/machine-id
|
||||
|
||||
# Session mount points. The guest root is read-only at runtime, so a runtime
|
||||
# mkdir gets EROFS and takes a service down before it starts — these have to
|
||||
# already exist in the image.
|
||||
#
|
||||
# `/nestri/logs` is a mount point and nothing mounts it from in here any more:
|
||||
# a share arrives because the caller named it in the boot descriptor, which is
|
||||
# the same rule every other share follows. The directory stays so that naming
|
||||
# it works.
|
||||
RUN mkdir -p /nestri/install /nestri/user /nestri/work /nestri/game /nestri/logs && \
|
||||
chmod 0755 /nestri /nestri/install /nestri/user /nestri/work /nestri/game /nestri/logs && \
|
||||
mkdir -p /dev/shm && chmod 1777 /dev/shm && \
|
||||
mkdir -p /run/user/1000 /var/log && \
|
||||
# The distribution's own empty `fstab` goes with ours. Nothing in a box
|
||||
# reads either: init mounts what a box always needs, and every share comes
|
||||
# from the boot descriptor. A file that looks like it configures mounts and
|
||||
# is read by nothing is a file somebody edits expecting an effect.
|
||||
rm -f /etc/network/interfaces /etc/inittab /etc/fstab
|
||||
|
||||
# Nothing in this image may be an init system except `nesinit`.
|
||||
#
|
||||
# A service manager arriving as a dependency of something innocuous is the
|
||||
# failure this catches, and it is silent otherwise: the extra init does not run
|
||||
# — the kernel is told which one to start — it just sits there with its own
|
||||
# ideas about what the box should be doing, waiting for somebody to wire it in.
|
||||
RUN for intruder in /usr/lib/systemd/systemd /sbin/openrc-init /usr/bin/openrc-init \
|
||||
/sbin/runit-init /usr/bin/runit-init /sbin/dinit /usr/bin/dinit; do \
|
||||
test ! -e "$intruder" || { echo "a second init is in the image: $intruder" >&2; exit 1; }; \
|
||||
done
|
||||
|
||||
# What `nesinit` will look for at runtime, checked while there is somebody to
|
||||
# read the failure.
|
||||
#
|
||||
# It is a table compiled into a binary, so a missing program is not a build
|
||||
# error — it is a service that does not come up in a box somebody is waiting
|
||||
# on, reported over the control channel and read hours later. Checking here
|
||||
# turns that into a failed build.
|
||||
# Everything this image promises must resolve the libraries it links.
|
||||
#
|
||||
# This is the check the Mesa change needs: dropping a package that provided
|
||||
# libraries is how a binary ends up resolving nothing, and the symptom is not a
|
||||
# build failure — it is a service that will not start in a box somebody is
|
||||
# waiting on, or a render path that is missing rather than slow. It caught
|
||||
# exactly that on the first run, and the missing library was two levels down a
|
||||
# dependency tree nobody had reason to read.
|
||||
#
|
||||
# **Named rather than swept, and that is deliberate.** A sweep over everything
|
||||
# in /usr/lib fails on a stock image: a distribution ships optional plugins
|
||||
# whose optional dependencies are not installed — pinentry's Qt build, mpg123's
|
||||
# JACK output, libdecor's GTK backend — and every one of those was already
|
||||
# unresolved before this stage existed. A check that reports a dozen things
|
||||
# nobody intends to load is a check the next person deletes. This list is what
|
||||
# the image is *for*: the components, the services init starts, the chain
|
||||
# between a workload and the GPU, and Wine's own core.
|
||||
#
|
||||
# **Wine was added after it was missed**, and then narrowed twice, which is
|
||||
# worth recording so nobody widens it again.
|
||||
#
|
||||
# It was missed because the list covered everything this image ships *of ours*
|
||||
# and nothing of the compatibility tool's, so an unresolved `libunwind.so.8`
|
||||
# behind `ntdll.so` survived a build whose whole purpose is catching that, and
|
||||
# surfaced as a session that created a prefix and could not start one process
|
||||
# in it.
|
||||
#
|
||||
# The obvious fix -- sweep every `*-unix/*.so` -- is wrong in both directions.
|
||||
# It is noisy: those objects are Wine's optional backends, and their
|
||||
# dependencies are a camera library, a media stack, a VR loader, a smartcard
|
||||
# daemon and OpenCL, none of which belong in a box. And it cannot see what it
|
||||
# is checking: Wine's unix objects **link each other by soname** and are
|
||||
# resolved by Wine's own loader rather than by `ld.so`, so `ldd` reports
|
||||
# `ntdll.so` and `win32u.so` themselves as missing while they sit in the same
|
||||
# directory. Forty files, every one a false positive, hiding the one real
|
||||
# entry.
|
||||
#
|
||||
# So: the programs in `bin/`, which are ordinary ELF and resolve normally, and
|
||||
# `ntdll.so`, which is the first thing Wine loads and the one that linked the
|
||||
# missing library. That is exactly the failure that got through, with none of
|
||||
# the noise. The Windows-side DLLs beside them are not ELF and `ldd` skips them
|
||||
# anyway.
|
||||
# The output is one file per line with its own missing libraries under it, and
|
||||
# then every missing library once at the end. That last list is what somebody
|
||||
# acts on -- it is the set of packages to add -- and forty files each naming the
|
||||
# same two libraries is not that list. An earlier version printed one
|
||||
# comma-joined line and cut the wrong field out of `ldd`, so it named no
|
||||
# libraries at all: `ldd` indents with a tab, which `tr -s ' '` does not
|
||||
# collapse, so the second space-separated field is `=>`.
|
||||
RUN failed=0; \
|
||||
: > /tmp/missing-libs; \
|
||||
for f in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
|
||||
/usr/lib/libnescapture_layer.so \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/wireplumber /usr/bin/ip \
|
||||
/usr/lib/libgallium-*.so /usr/lib/libEGL_mesa.so.0 \
|
||||
/usr/lib/libvulkan_*.so /usr/lib/dri/*.so /usr/lib/gbm/*.so \
|
||||
/usr/share/steam/compatibilitytools.d/proton-cachyos/files/bin/* \
|
||||
/usr/share/steam/compatibilitytools.d/proton-cachyos/files/lib*/wine/*-unix/ntdll.so; do \
|
||||
[ -e "$f" ] || continue; \
|
||||
libs="$(ldd "$f" 2>/dev/null | awk '/not found/ { print $1 }')"; \
|
||||
[ -n "$libs" ] || continue; \
|
||||
failed=1; \
|
||||
printf ' %s\n' "$f" >&2; \
|
||||
printf ' %s\n' $libs >&2; \
|
||||
printf '%s\n' $libs >> /tmp/missing-libs; \
|
||||
done; \
|
||||
if [ "$failed" != 0 ]; then \
|
||||
echo "" >&2; \
|
||||
echo " every library above, once each -- this is the list to install:" >&2; \
|
||||
sort -u /tmp/missing-libs | sed 's/^/ /' >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
RUN for required in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/wireplumber /usr/bin/ip \
|
||||
/usr/bin/python3 \
|
||||
/usr/share/steam/compatibilitytools.d/proton-cachyos/proton; do \
|
||||
test -x "$required" || { echo "the image is missing $required" >&2; exit 1; }; \
|
||||
done
|
||||
|
||||
# An entry point that is executable is not an entry point that runs.
|
||||
#
|
||||
# The check above passed on an image whose compatibility tool was a Python
|
||||
# script with no interpreter behind it: `test -x` says the file may be
|
||||
# executed, and the kernel then fails to find what the shebang names. A session
|
||||
# got as far as launching a game and ended with
|
||||
# `env: 'python3': No such file or directory`.
|
||||
#
|
||||
# So every script this image promises resolves its own interpreter. `env` is
|
||||
# unwrapped where it is used, because a shebang of `#!/usr/bin/env python3`
|
||||
# names `env` and the thing that is actually missing is the argument.
|
||||
RUN for script in /usr/share/steam/compatibilitytools.d/proton-cachyos/proton; do \
|
||||
head -c 2 "$script" | grep -q '#!' || continue; \
|
||||
shebang="$(head -1 "$script" | sed 's/^#!//')"; \
|
||||
interpreter="${shebang%% *}"; \
|
||||
case "$interpreter" in \
|
||||
*/env) argument="${shebang#* }"; interpreter="$(command -v "${argument%% *}" || true)";; \
|
||||
esac; \
|
||||
test -n "$interpreter" && test -x "$interpreter" \
|
||||
|| { echo "$script needs an interpreter the image does not have: $shebang" >&2; exit 1; }; \
|
||||
done
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# runtime_prod — the default: `make build`
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM runtime AS runtime_prod
|
||||
|
||||
# No console is offered by either flavour: `nesinit` spawns no getty, because
|
||||
# the way into a guest that will not boot is `init=/bin/bash` on the kernel
|
||||
# command line, which needs nothing from the image but a shell. So the locked
|
||||
# root account is belt and braces rather than the only thing standing between
|
||||
# a box and a login prompt.
|
||||
RUN passwd -l root
|
||||
|
||||
RUN while IFS= read -r f; do \
|
||||
[ -f "$f" ] && strip --strip-unneeded "$f" 2>/dev/null || true; \
|
||||
done < /tmp/.strip-manifest; \
|
||||
rm -rf /tmp/.strip-manifest /var/cache/pacman/pkg/* /tmp/* /root/.cache \
|
||||
/usr/share/man /usr/share/doc /usr/share/locale \
|
||||
/usr/lib/cmake /usr/lib/pkgconfig /usr/share/pkgconfig /usr/include \
|
||||
/usr/share/gir-1.0 /usr/lib/udev; \
|
||||
find /usr/lib -name '*.a' -delete
|
||||
|
||||
RUN echo "NESTRI_STAGE=runtime_prod" >> /etc/os-release
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# runtime_debug — `make build-debug`
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM runtime AS runtime_debug
|
||||
|
||||
# `-Sy` and not `-Syu`: a full upgrade here can pull a package back in as
|
||||
# somebody's dependency, and the one that matters is systemd. The check below
|
||||
# catches it either way, but a debug image that fails to build is worse than
|
||||
# one that is a few days behind on versions it only uses for `vulkaninfo`.
|
||||
RUN pacman -Sy --noconfirm --needed vulkan-tools mesa-utils libva-utils && \
|
||||
pacman -Scc --noconfirm
|
||||
|
||||
# The same guard as the runtime stage, because the transaction above is exactly
|
||||
# the kind that quietly reinstates an init system.
|
||||
RUN test ! -e /usr/lib/systemd/systemd \
|
||||
|| { echo "systemd's PID 1 came back with the debug tools" >&2; exit 1; }
|
||||
|
||||
# Root has a password here and nothing offers a login prompt to type it into.
|
||||
# It is for `su` from an `init=/bin/bash` shell, which is the whole debug route.
|
||||
RUN echo 'root:nestri' | chpasswd
|
||||
|
||||
RUN echo "NESTRI_STAGE=runtime_debug" >> /etc/os-release
|
||||
40
build/Containerfile.containerignore
Normal file
40
build/Containerfile.containerignore
Normal file
@@ -0,0 +1,40 @@
|
||||
# The guest rootfs build's context.
|
||||
#
|
||||
# The name is load-bearing and it is not `.containerignore`. Podman looks for
|
||||
# an ignore file *adjacent to the Containerfile and named after it* — here,
|
||||
# `Containerfile.containerignore` — before falling back to one at the root of
|
||||
# the build context. The context is the repository root, so a bare
|
||||
# `build/.containerignore` sits in neither place and is silently read by
|
||||
# nothing: the build still works, it just sends the whole tree.
|
||||
#
|
||||
# Docker looks for the `.dockerignore` suffix only, so a docker build reads the
|
||||
# repository-root file instead of this one and sends more than it needs. That
|
||||
# is the cost of the container-agnostic name and it is only a cost in bytes.
|
||||
#
|
||||
# This file *replaces* the repository-wide ignore file rather than adding to
|
||||
# it, so the first block below is that file repeated. The second is what only
|
||||
# this build excludes.
|
||||
#
|
||||
# This build's context is the repository root (see `Makefile`), and it COPYs
|
||||
# the workspace manifests plus the Rust members and nothing else. The
|
||||
# TypeScript half is therefore dead weight in the context — a few megabytes
|
||||
# sent to the daemon versus the whole tree.
|
||||
.git
|
||||
node_modules
|
||||
target
|
||||
build/output
|
||||
.env
|
||||
.env.*
|
||||
.wrangler
|
||||
dist
|
||||
.output
|
||||
|
||||
docs
|
||||
apps/api
|
||||
apps/auth
|
||||
packages
|
||||
*.md
|
||||
deno.lock
|
||||
bun.lock
|
||||
.zed
|
||||
.github
|
||||
102
build/Containerfile.proton
Normal file
102
build/Containerfile.proton
Normal file
@@ -0,0 +1,102 @@
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# proton-cachyos, built wow64-only
|
||||
#
|
||||
# Separate from the guest Containerfile because it is a separate cadence:
|
||||
# hours to build, and only when PROTON_TAG moves. The guest image pulls the
|
||||
# result from a registry instead of rebuilding it, which is why this file is
|
||||
# not part of that build and is not reached by `make build`.
|
||||
#
|
||||
# The final stage is FROM scratch, so the image *is* the Proton tree — nothing
|
||||
# to run, only something to COPY --from.
|
||||
#
|
||||
# Build and publish with `make proton-image` / `make proton-push`. **Its
|
||||
# context is this directory**, not the repository root the guest build uses:
|
||||
# all it needs is the two scripts beside it, and a context of the whole tree
|
||||
# would hand it a multi-gigabyte `output/` for no reason.
|
||||
#
|
||||
# `PROTON_TAG` is the one thing to change, and the Makefile derives the
|
||||
# published image's version from it. They are the same number in two
|
||||
# spellings, and an image whose name does not say which Proton is inside it
|
||||
# is worse than no image.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
FROM docker.io/archlinux:base-devel AS builder
|
||||
|
||||
# proton-cachyos-native's makedepends, minus every lib32-* (that is the whole
|
||||
# point of --enable-wow64), with two substitutions Arch requires: ocl-icd
|
||||
# provides opencl-icd-loader, and mesa-libgl is folded into libglvnd.
|
||||
#
|
||||
# glib2-devel is the one that is easy to miss — it carries glib-mkenums, which
|
||||
# libsoup's meson looks up through glib-2.0's pkg-config variables and fails on
|
||||
# obscurely. unzip and zip are for the xalia dist step. afdko is not packaged at
|
||||
# all, and the fonts submodule needs it, so it comes from PyPI.
|
||||
RUN pacman -Syu --noconfirm --needed \
|
||||
alsa-lib clang cmake curl ffmpeg fontforge giflib git glib2-devel \
|
||||
glslang gnutls gtk3 libgphoto2 libglvnd libpulse libva libxcomposite \
|
||||
libxinerama libxxf86vm lld mesa meson ninja nasm \
|
||||
opencl-headers ocl-icd pcsclite perl perl-json python python-pip \
|
||||
python-pefile python-setuptools-scm rsync rust samba unixodbc \
|
||||
unzip zip v4l-utils vulkan-headers vulkan-icd-loader wayland \
|
||||
wayland-protocols wget xorg-util-macros \
|
||||
mingw-w64-gcc mingw-w64-binutils mingw-w64-crt mingw-w64-headers \
|
||||
mingw-w64-winpthreads \
|
||||
&& pip install --break-system-packages --no-cache-dir afdko \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
# widl generates vkd3d's public headers. Without it autoconf sets HAVE_WIDL to
|
||||
# false, the headers are silently not generated, and the build dies an hour
|
||||
# later on a missing vkd3d_d3dx9shader.h. Arch ships widl only inside `wine`,
|
||||
# which requires multilib; the AUR's mingw-w64-tools builds it standalone, so do
|
||||
# the same.
|
||||
#
|
||||
# The digest is not a formality. What is extracted here has its `configure` and
|
||||
# its makefiles run as root in this builder, and what they produce is copied
|
||||
# into the image a box runs -- so whoever can change these bytes can change what
|
||||
# runs on every host. SourceForge hands the request to whichever mirror it
|
||||
# likes, over a connection this builder does not pin, and the project publishes
|
||||
# no signature. The digest is the only thing that makes the mirror not matter.
|
||||
#
|
||||
# Taken 2026-09-14 from two different mirrors of v14.0.0, which agreed. A
|
||||
# mismatch here is not a thing to paper over by taking the new digest: it means
|
||||
# the bytes behind this exact version string changed, and that wants looking at
|
||||
# before it wants fixing.
|
||||
ARG MINGW_W64_VER=14.0.0
|
||||
ARG MINGW_W64_SHA256=6eaf921d9eb987d3820b364ea9775bc19b965ec81490b6fdd716526c28e1995c
|
||||
RUN curl -fL --retry 3 -o /tmp/mingw-w64.tar.bz2 \
|
||||
"https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/mingw-w64-v${MINGW_W64_VER}.tar.bz2/download" \
|
||||
&& echo "${MINGW_W64_SHA256} /tmp/mingw-w64.tar.bz2" | sha256sum -c - \
|
||||
&& tar xf /tmp/mingw-w64.tar.bz2 -C /tmp \
|
||||
&& for arch in i686-w64-mingw32 x86_64-w64-mingw32; do \
|
||||
mkdir -p "/tmp/widl-${arch}" && cd "/tmp/widl-${arch}" \
|
||||
&& "/tmp/mingw-w64-v${MINGW_W64_VER}/mingw-w64-tools/widl/configure" \
|
||||
--prefix=/usr --target="${arch}" --program-prefix="${arch}-" \
|
||||
&& make -j"$(nproc)" && make install; \
|
||||
done \
|
||||
&& rm -rf /tmp/mingw-w64* /tmp/widl-*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ARG PROTON_GIT=https://github.com/CachyOS/proton-cachyos.git
|
||||
ARG PROTON_TAG=cachyos-11.0-20260703-native
|
||||
ARG GECKO_VER=2.47.4
|
||||
ARG MONO_VER=11.2.0
|
||||
ARG XALIA_VER=0.4.9
|
||||
ARG JOBS=
|
||||
|
||||
# Fetch and build are separate layers on purpose: the submodule checkout runs
|
||||
# well past ten minutes, and a build that fails on a flag or a missing tool must
|
||||
# not pay for it again.
|
||||
COPY scripts/proton-fetch.sh /build/proton-fetch.sh
|
||||
RUN PROTON_GIT="${PROTON_GIT}" PROTON_TAG="${PROTON_TAG}" \
|
||||
GECKO_VER="${GECKO_VER}" MONO_VER="${MONO_VER}" XALIA_VER="${XALIA_VER}" \
|
||||
bash /build/proton-fetch.sh
|
||||
|
||||
COPY scripts/proton-build.sh /build/proton-build.sh
|
||||
RUN GECKO_VER="${GECKO_VER}" MONO_VER="${MONO_VER}" JOBS="${JOBS}" \
|
||||
bash /build/proton-build.sh
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# The publishable artifact: the Proton tree and nothing else
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
FROM scratch
|
||||
COPY --from=builder /artifacts/proton/ /
|
||||
15
build/Containerfile.proton.containerignore
Normal file
15
build/Containerfile.proton.containerignore
Normal file
@@ -0,0 +1,15 @@
|
||||
# This build's context is `build/`, not the repository root, because all it
|
||||
# needs is the two scripts beside the Containerfile.
|
||||
#
|
||||
# `output/` is the reason this file exists. It holds packed rootfs images —
|
||||
# multiple gigabytes each — and a build context is copied before the first
|
||||
# instruction runs, so without this line every Proton build starts by moving
|
||||
# the last one it produced.
|
||||
output
|
||||
|
||||
# Nothing else here is an input to this build.
|
||||
etc
|
||||
README.md
|
||||
Makefile
|
||||
Containerfile
|
||||
Containerfile.containerignore
|
||||
309
build/Dockerfile
309
build/Dockerfile
@@ -1,309 +0,0 @@
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# nestri guest rootfs — the open half
|
||||
#
|
||||
# Builds a bootable Artix/OpenRC image containing Mesa (virtio-gpu native
|
||||
# context) and the four open guest components: nescope, neshub, neswire,
|
||||
# nescapture. Two leaf targets, selected with `--target`:
|
||||
#
|
||||
# runtime_prod stripped, root locked (default: `make build`)
|
||||
# runtime_debug debug tools, autologin root (`make build-debug`)
|
||||
#
|
||||
# What is deliberately NOT here: Proton, Valve's steamclient.so, or anything
|
||||
# else closed. nestri/CLAUDE.md is explicit that nothing closed enters this
|
||||
# repo. Whatever layers those on top of runtime_prod is a closed build
|
||||
# outside this repo — see build/README.md.
|
||||
#
|
||||
# Build from the repo root, not from build/:
|
||||
# docker build -f build/Dockerfile --target runtime_prod -t nestri-guest .
|
||||
# (`make build` in this directory does exactly that.)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# initial / builder — Arch, not Artix
|
||||
#
|
||||
# The guest is Artix, but Artix's repos are Arch-derived and the toolchain/
|
||||
# glibc generation is the same, while the archlinux image is the
|
||||
# better-maintained of the two for actually compiling things. Only build
|
||||
# artifacts leave these stages.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM docker.io/archlinux:base-devel AS initial
|
||||
RUN pacman -Syu --noconfirm
|
||||
|
||||
FROM initial AS builder
|
||||
RUN pacman -S --noconfirm --needed \
|
||||
cmake meson ninja git pkgconf \
|
||||
python python-mako python-yaml python-packaging python-ply \
|
||||
bison flex \
|
||||
libpciaccess libepoxy libglvnd \
|
||||
libx11 libxext libxrandr libxshmfence libxfixes libxxf86vm libxcb \
|
||||
xcb-util-keysyms xorgproto \
|
||||
wayland wayland-protocols \
|
||||
expat zlib zstd libxml2 lm_sensors \
|
||||
llvm clang libclc spirv-tools spirv-llvm-translator glslang \
|
||||
elfutils libva libdrm directx-headers \
|
||||
rust rust-bindgen cbindgen \
|
||||
curl openssl \
|
||||
pixman libxkbcommon \
|
||||
vulkan-headers vulkan-icd-loader \
|
||||
pipewire shaderc opus \
|
||||
libinput \
|
||||
&& pacman -Scc --noconfirm
|
||||
WORKDIR /build
|
||||
ENV ARTIFACTS=/artifacts
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# Mesa — the only piece still fetched from outside this tree
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM builder AS mesa-build
|
||||
|
||||
ARG MESA_GIT=https://gitlab.freedesktop.org/mesa/mesa.git
|
||||
ARG MESA_COMMIT=b78fc73dd898a7dfa87448a4b5ff4459a870e21a
|
||||
|
||||
RUN git clone --depth=1 --revision="${MESA_COMMIT}" "${MESA_GIT}" /build/mesa-src && \
|
||||
cd /build/mesa-src && \
|
||||
meson setup builddir \
|
||||
-Dprefix=/usr \
|
||||
-Dbuildtype=release \
|
||||
-Dplatforms=wayland,x11 \
|
||||
-Dgallium-drivers=zink,radeonsi,iris \
|
||||
-Dvulkan-drivers=amd,intel \
|
||||
-Damdgpu-virtio=true \
|
||||
-Dintel-virtio-experimental=true \
|
||||
-Dvideo-codecs=all \
|
||||
-Degl=enabled \
|
||||
-Dglx=dri \
|
||||
-Dgles1=enabled \
|
||||
-Dgles2=enabled \
|
||||
-Dgbm=enabled \
|
||||
-Dgallium-va=enabled \
|
||||
-Db_ndebug=true && \
|
||||
ninja -C builddir && \
|
||||
DESTDIR=/artifacts/mesa ninja -C builddir install && \
|
||||
rm -rf /build/mesa-src && \
|
||||
find /artifacts/mesa -type f -printf '/%P\n' > /artifacts/mesa/.manifest
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# nestri workspace — same repo now, so this is COPY, not a private clone
|
||||
#
|
||||
# One `cargo build --release --workspace` rather than one stage per binary:
|
||||
# that per-repo splitting existed because nescope/neswire/nescapture/the hub
|
||||
# were four separate private repos and a stage boundary was the only way to
|
||||
# stop bumping one from invalidating the others' build cache. They are one
|
||||
# Cargo workspace with one Cargo.lock now, so a BuildKit cache mount on
|
||||
# target/ gives the same isolation — cargo's own incremental compiler
|
||||
# already knows nescope changing does not touch nesprotocol's .rlib — without
|
||||
# four copies of every shared dependency getting compiled once per stage.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM builder AS nestri-src
|
||||
WORKDIR /build/nestri
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/nesprotocol crates/nesprotocol
|
||||
COPY apps/nescope apps/nescope
|
||||
COPY apps/neshub apps/neshub
|
||||
COPY apps/neswire apps/neswire
|
||||
COPY apps/nescapture apps/nescapture
|
||||
|
||||
FROM nestri-src AS nestri-build
|
||||
RUN --mount=type=cache,target=/root/.cargo/registry \
|
||||
--mount=type=cache,target=/build/nestri/target \
|
||||
cargo build --release --workspace && \
|
||||
mkdir -p /artifacts/nestri/usr/bin /artifacts/nestri/usr/lib \
|
||||
/artifacts/nestri/usr/share/vulkan/implicit_layer.d && \
|
||||
install -Dm755 target/release/nescope /artifacts/nestri/usr/bin/nescope && \
|
||||
install -Dm755 target/release/neshub /artifacts/nestri/usr/bin/neshub && \
|
||||
install -Dm755 target/release/neswire /artifacts/nestri/usr/bin/neswire && \
|
||||
install -Dm755 target/release/libnescapture_layer.so \
|
||||
/artifacts/nestri/usr/lib/libnescapture_layer.so && \
|
||||
install -Dm644 apps/nescapture/manifest/VK_LAYER_nescapture.json \
|
||||
/artifacts/nestri/usr/share/vulkan/implicit_layer.d/VK_LAYER_nescapture.json && \
|
||||
find /artifacts/nestri -type f -printf '/%P\n' > /artifacts/nestri/.manifest
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# os-base — the Artix rootfs itself
|
||||
#
|
||||
# `FROM artixlinux/artixlinux:base-openrc` directly, and `pacman -S` as plain
|
||||
# RUN steps — not a privileged host `chroot` into a hand-extracted tarball,
|
||||
# which would need /proc, /sys and /dev bind-mounted in first (they don't
|
||||
# exist inside a chroot target until something puts them there). A
|
||||
# Dockerfile RUN step already executes inside a real container with its own
|
||||
# /proc, /sys, /dev, so there is no bind-mount step to write at all.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM docker.io/artixlinux/artixlinux:base-openrc AS os-base
|
||||
|
||||
RUN pacman -Syu --noconfirm --needed \
|
||||
base openrc udev dbus dbus-openrc \
|
||||
iptables iproute2 \
|
||||
mesa libglvnd libdrm libepoxy libxxf86vm libinput wayland \
|
||||
expat zlib llvm-libs elfutils libva shaderc vulkan-icd-loader \
|
||||
pixman libxkbcommon xcb-util-keysyms xorg-xwayland \
|
||||
pipewire pipewire-audio wireplumber dbus logrotate opus \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
# groupadd -f so this is idempotent whether or not udev's rules already
|
||||
# created these.
|
||||
#
|
||||
# **Two users, and they must stay two.** `nestri` runs the services that come
|
||||
# with this image; `nesplay` is who a workload runs as. Sharing one user between
|
||||
# them is what lets workload code impersonate a service — it can replace the
|
||||
# socket a service listens on and answer in its place, and the answer that
|
||||
# matters is the address a client is told to connect to. Init refuses an address
|
||||
# served by the workload's own user, so a single shared user does not merely
|
||||
# weaken that check, it makes every session fail it.
|
||||
#
|
||||
# The uid a workload actually runs as is chosen by whoever asks for the box, not
|
||||
# here; this account exists so that the number has a home, a shell and a name in
|
||||
# `ps`, and so the separation has somewhere to be written down.
|
||||
RUN groupadd -f audio && groupadd -f video && groupadd -f input && groupadd -f render && \
|
||||
useradd -m -u 1000 -s /bin/bash nestri && \
|
||||
for g in audio video input render; do gpasswd -a nestri "$g" >/dev/null; done && \
|
||||
useradd -m -u 1001 -s /bin/bash nesplay && \
|
||||
for g in audio video input render; do gpasswd -a nesplay "$g" >/dev/null; done
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# runtime — everything common to debug and prod
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM os-base AS runtime
|
||||
|
||||
# This is what GHCR actually uses to connect a pushed image back to its
|
||||
# repo — not a setting to toggle after the fact, a label the image has to
|
||||
# carry. Without it a manually-pushed image shows no "used by" repo on its
|
||||
# package page even though this Dockerfile is exactly what built it.
|
||||
LABEL org.opencontainers.image.source="https://github.com/nestrilabs/nestri"
|
||||
|
||||
# Our own builds, overlaid on the distro's mesa. The distro package landed
|
||||
# first (above) so every runtime dependency of *a* Mesa is present and
|
||||
# correctly versioned; this overwrites its .so files with ours.
|
||||
#
|
||||
# COPY --from runs as root inside this build with no invoking-user uid to
|
||||
# stamp onto / or /usr/bin, unlike a host-side `podman cp` + `cp -a` — so
|
||||
# there is no ownership-sanity-check to write here. Nothing to catch, on
|
||||
# purpose, not an oversight.
|
||||
COPY --from=mesa-build /artifacts/mesa/.manifest /tmp/mesa.manifest
|
||||
COPY --from=nestri-build /artifacts/nestri/.manifest /tmp/nestri.manifest
|
||||
RUN cat /tmp/mesa.manifest /tmp/nestri.manifest > /tmp/.strip-manifest && \
|
||||
rm -f /tmp/mesa.manifest /tmp/nestri.manifest
|
||||
COPY --from=mesa-build /artifacts/mesa /
|
||||
COPY --from=nestri-build /artifacts/nestri /
|
||||
RUN ldconfig
|
||||
|
||||
COPY build/etc/ /etc/
|
||||
RUN chmod +x /etc/init.d/*
|
||||
|
||||
# dbus-session, pipewire and wireplumber carry no conf.d of their own — they
|
||||
# just want the shared environment, so their conf.d is a symlink to it rather
|
||||
# than a copy. nescope, neshub and neswire are deliberately absent from this
|
||||
# loop: each has a conf.d file of its own (already landed by the COPY above)
|
||||
# that sources nestri-user-env, because each needs settings the shared file
|
||||
# does not carry. Symlinking them here would overwrite those.
|
||||
RUN for svc in dbus-session pipewire wireplumber; do \
|
||||
ln -sf nestri-user-env "/etc/conf.d/${svc}"; \
|
||||
done
|
||||
|
||||
RUN echo nesbox > /etc/hostname && dbus-uuidgen --ensure=/etc/machine-id
|
||||
|
||||
# Session mount points. The guest root is read-only at runtime, so a runtime
|
||||
# mkdir gets EROFS and takes a service down before it starts — these have to
|
||||
# already exist in the image.
|
||||
RUN mkdir -p /nestri/install /nestri/user /nestri/work /nestri/game /nestri/logs && \
|
||||
chmod 0755 /nestri /nestri/install /nestri/user /nestri/work /nestri/game /nestri/logs && \
|
||||
mkdir -p /dev/shm && chmod 1777 /dev/shm && \
|
||||
rm -f /etc/network/interfaces
|
||||
|
||||
# Serial console: the only way into a guest that will not boot.
|
||||
RUN rm -f /etc/inittab && \
|
||||
ln -sf agetty /etc/init.d/agetty.hvc0 && \
|
||||
{ grep -qx hvc0 /etc/securetty || echo hvc0 >> /etc/securetty; }
|
||||
|
||||
# Every init.d script calling nestri_export_env needs a conf.d that actually
|
||||
# defines it. Worth a build-time check: the failure at runtime is nearly
|
||||
# invisible — OpenRC sources the script, the undefined function is a
|
||||
# "command not found" on stderr, sourcing still returns 0, and the service
|
||||
# starts anyway with HOME and every XDG_* unset.
|
||||
RUN missing=""; \
|
||||
for svc_script in /etc/init.d/*; do \
|
||||
grep -qE '^[[:space:]]*nestri_export_env\b' "$svc_script" 2>/dev/null || continue; \
|
||||
svc="$(basename "$svc_script")"; \
|
||||
. "/etc/conf.d/$svc" >/dev/null 2>&1; \
|
||||
command -v nestri_export_env >/dev/null 2>&1 || missing="$missing $svc"; \
|
||||
done; \
|
||||
[ -z "$missing" ] || { echo "init scripts call nestri_export_env with no conf.d providing it:$missing" >&2; exit 1; }
|
||||
|
||||
# ── OpenRC service registration ──
|
||||
# sysinit
|
||||
RUN rc-update add devfs sysinit && \
|
||||
rc-update add dmesg sysinit && \
|
||||
rc-update add udev sysinit && \
|
||||
rc-update add udev-trigger sysinit && \
|
||||
(rc-update del kmod-static-nodes sysinit || true)
|
||||
|
||||
# boot — guest-net replaces the distro's networking scripts entirely; it
|
||||
# declares `provide net` and brings up lo itself.
|
||||
RUN (rc-update del networking boot || true) && \
|
||||
(rc-update del network-async boot || true) && rm -f /etc/init.d/network-async && \
|
||||
rc-update add guest-net boot && \
|
||||
(rc-update del net.lo boot || true) && \
|
||||
(rc-update del netmount boot || true) && \
|
||||
(rc-update del netmount default || true) && \
|
||||
rc-update add hostname boot && \
|
||||
rc-update add xdg-runtime boot && \
|
||||
rc-update add cgroups boot && \
|
||||
(rc-update del syslog boot || true)
|
||||
|
||||
# Boot-runlevel services with nothing to do in a freshly-built microVM: no
|
||||
# physical console, no swap, and fsck would be checking a filesystem nobody
|
||||
# has touched.
|
||||
RUN for svc in fsck keymaps save-keymaps termencoding save-termencoding swap binfmt seedrng hwclock swclock; do \
|
||||
(rc-update del "$svc" boot || true); \
|
||||
(rc-update del "$svc" shutdown || true); \
|
||||
done
|
||||
|
||||
# default — the user-facing stack. nescope is registered in plain-compositor
|
||||
# mode (no payload after `--`): it comes up and waits for something to
|
||||
# connect. Actually starting a payload is nesinit's job, and nesinit is not
|
||||
# open code yet — see build/README.md.
|
||||
RUN rc-update add agetty.hvc0 default && \
|
||||
for n in 1 2 3 4 5 6; do (rc-update del "agetty.tty${n}" default || true); done && \
|
||||
rc-update add dbus default && \
|
||||
rc-update add dbus-session default && \
|
||||
rc-update add pipewire default && \
|
||||
rc-update add wireplumber default && \
|
||||
rc-update add neshub default && \
|
||||
rc-update add neswire default && \
|
||||
rc-update add nescope default && \
|
||||
rm -f /etc/init.d/shared-root && (rc-update del shared-root boot || true)
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# runtime_prod — the default: `make build`
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM runtime AS runtime_prod
|
||||
|
||||
RUN passwd -l root
|
||||
COPY build/etc/conf.d/agetty.hvc0.prod /etc/conf.d/agetty.hvc0
|
||||
|
||||
RUN while IFS= read -r f; do \
|
||||
[ -f "$f" ] && strip --strip-unneeded "$f" 2>/dev/null || true; \
|
||||
done < /tmp/.strip-manifest; \
|
||||
rm -rf /tmp/.strip-manifest /var/cache/pacman/pkg/* /tmp/* /root/.cache \
|
||||
/usr/share/man /usr/share/doc /usr/share/locale \
|
||||
/usr/lib/cmake /usr/lib/pkgconfig /usr/share/pkgconfig /usr/include
|
||||
|
||||
RUN echo "NESTRI_STAGE=runtime_prod" >> /etc/os-release
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# runtime_debug — `make build-debug`
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM runtime AS runtime_debug
|
||||
|
||||
RUN pacman -Syu --noconfirm --needed vulkan-tools mesa-utils libva-utils && \
|
||||
pacman -Scc --noconfirm
|
||||
RUN echo 'root:nestri' | chpasswd
|
||||
COPY build/etc/conf.d/agetty.hvc0.debug /etc/conf.d/agetty.hvc0
|
||||
|
||||
RUN echo "NESTRI_STAGE=runtime_debug" >> /etc/os-release
|
||||
@@ -1,29 +0,0 @@
|
||||
# The guest rootfs build's context.
|
||||
#
|
||||
# A `<Dockerfile>.dockerignore` *replaces* the repository-wide `.dockerignore`
|
||||
# rather than adding to it, so the first block below is that file repeated. The
|
||||
# second is what only this build excludes.
|
||||
#
|
||||
# This build's context is the repository root (see `Makefile`), and it COPYs
|
||||
# the workspace manifests plus the Rust members and nothing else. The
|
||||
# TypeScript half is therefore dead weight in the context — a few megabytes
|
||||
# sent to the daemon versus the whole tree.
|
||||
.git
|
||||
node_modules
|
||||
target
|
||||
build/output
|
||||
.env
|
||||
.env.*
|
||||
.wrangler
|
||||
dist
|
||||
.output
|
||||
|
||||
docs
|
||||
apps/api
|
||||
apps/auth
|
||||
packages
|
||||
*.md
|
||||
deno.lock
|
||||
bun.lock
|
||||
.zed
|
||||
.github
|
||||
@@ -1,12 +1,12 @@
|
||||
SHELL := /bin/bash
|
||||
.PHONY: build build-debug image image-debug clean help
|
||||
.PHONY: build build-debug image image-debug proton-image proton-push clean help
|
||||
|
||||
CONTAINER_RT := $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
|
||||
CONTAINER_RT := $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null)
|
||||
ifeq ($(CONTAINER_RT),)
|
||||
$(error "Neither docker nor podman found in PATH")
|
||||
endif
|
||||
|
||||
# The Dockerfile COPYs from apps/ and crates/, so the build context is the
|
||||
# The Containerfile COPYs from apps/ and crates/, so the build context is the
|
||||
# repo root, not this directory — same reason borealis's build/ *is* its own
|
||||
# context: here the guest source lives one level up instead of inside build/.
|
||||
CONTEXT := ..
|
||||
@@ -27,17 +27,47 @@ CONTEXT := ..
|
||||
# where it does not exist. One name, always pullable, beats a local-only
|
||||
# short name and a different published one.
|
||||
IMAGE_NAME := ghcr.io/nestrilabs/nestri/base
|
||||
|
||||
# Proton is the one artifact worth publishing on its own: it takes hours to
|
||||
# build and changes only when its tag moves, so the guest build pulls it by
|
||||
# name instead of rebuilding it.
|
||||
#
|
||||
# `PROTON_TAG` is the single place to change it. The published version is
|
||||
# derived from the tag rather than written twice, because the two are the same
|
||||
# number in two spellings and an image whose name does not say which Proton is
|
||||
# inside it is worse than no image at all. Changing the tag by hand and
|
||||
# forgetting the version is exactly the mistake this removes.
|
||||
PROTON_TAG ?= cachyos-11.0-20260703-native
|
||||
PROTON_VERSION := $(PROTON_TAG:cachyos-%-native=%)
|
||||
PROTON_IMAGE ?= ghcr.io/nestrilabs/proton-cachyos-native-wow64
|
||||
PROTON_REF := $(PROTON_IMAGE):$(PROTON_VERSION)
|
||||
OUTPUT_DIR := output
|
||||
ROOTFS_SIZE ?= 5G
|
||||
ROOTFS_SIZE ?= 3G
|
||||
FORCE_REBUILD ?=
|
||||
|
||||
build:
|
||||
DOCKER_BUILDKIT=1 $(CONTAINER_RT) build $(if $(FORCE_REBUILD),--no-cache,) \
|
||||
-f Dockerfile -t $(IMAGE_NAME):latest --target runtime_prod $(CONTEXT)
|
||||
--build-arg PROTON_IMAGE=$(PROTON_REF) \
|
||||
-f Containerfile -t $(IMAGE_NAME):latest --target runtime_prod $(CONTEXT)
|
||||
|
||||
build-debug:
|
||||
DOCKER_BUILDKIT=1 $(CONTAINER_RT) build $(if $(FORCE_REBUILD),--no-cache,) \
|
||||
-f Dockerfile -t $(IMAGE_NAME):debug --target runtime_debug $(CONTEXT)
|
||||
--build-arg PROTON_IMAGE=$(PROTON_REF) \
|
||||
-f Containerfile -t $(IMAGE_NAME):debug --target runtime_debug $(CONTEXT)
|
||||
|
||||
# Hours, and only when PROTON_TAG moves. Its context is this directory rather
|
||||
# than the repository root: the two scripts beside the Containerfile are the
|
||||
# whole input, and the root would hand it everything else for nothing.
|
||||
proton-image:
|
||||
DOCKER_BUILDKIT=1 $(CONTAINER_RT) build $(if $(FORCE_REBUILD),--no-cache,) \
|
||||
--build-arg PROTON_TAG=$(PROTON_TAG) \
|
||||
-f Containerfile.proton -t $(PROTON_REF) .
|
||||
@echo "Built $(PROTON_REF)"
|
||||
|
||||
# Publishing is what makes `make build` cheap for everyone else, since that
|
||||
# build pulls this by name. Push before expecting anyone to use a new tag.
|
||||
proton-push: proton-image
|
||||
$(CONTAINER_RT) push $(PROTON_REF)
|
||||
|
||||
# Not `sudo make image`/sudo'd in here: mkimage.sh runs as you and escalates
|
||||
# only the specific commands that need root. Under rootless Podman, `make
|
||||
@@ -62,5 +92,8 @@ help:
|
||||
@echo " make image Build + pack runtime_prod into output/rootfs.ext4"
|
||||
@echo " make image-debug Build + pack runtime_debug into output/rootfs-debug.ext4"
|
||||
@echo " make clean Remove output/"
|
||||
@echo " make proton-image Build Proton from source (hours)"
|
||||
@echo " make proton-push Build it and publish it"
|
||||
@echo " make FORCE_REBUILD=1 ... Rebuild from scratch, no layer cache"
|
||||
@echo " make PROTON_TAG=... ... Use a different proton-cachyos tag"
|
||||
@echo " make ROOTFS_SIZE=8G image Override the packed image size (default 5G)"
|
||||
|
||||
230
build/README.md
230
build/README.md
@@ -1,16 +1,17 @@
|
||||
# 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`](../apps/nescope), [`neshub`](../apps/neshub),
|
||||
[`neswire`](../apps/neswire), [`nescapture`](../apps/nescapture) — laid out
|
||||
Builds a bootable Arch image for the box's virtio-blk root: Mesa (virtio-gpu
|
||||
native context) plus the five open guest components —
|
||||
[`nesinit`](../apps/nesinit), [`nescope`](../apps/nescope),
|
||||
[`neshub`](../apps/neshub), [`neswire`](../apps/neswire),
|
||||
[`nescapture`](../apps/nescapture) — laid out
|
||||
the way [borealis](https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/main/project-borealis)
|
||||
lays out its `build/`: one big multi-stage `Dockerfile`, `--target` picks the
|
||||
lays out its `build/`: one big multi-stage `Containerfile`, `--target` picks the
|
||||
flavor, `etc/` holds the files that get overlaid onto the image verbatim.
|
||||
|
||||
```
|
||||
build/
|
||||
├── Dockerfile everything, in stages: mesa-build, nestri-build,
|
||||
├── Containerfile everything, in stages: mesa-build, nestri-build,
|
||||
│ os-base, runtime, runtime_prod, runtime_debug
|
||||
├── etc/ overlaid onto the image's /etc as-is
|
||||
├── scripts/
|
||||
@@ -24,6 +25,9 @@ make build # docker build --target runtime_prod → ghcr.io/nestrilab
|
||||
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
|
||||
|
||||
make proton-image # build Proton from source — hours
|
||||
make proton-push # build it and publish it
|
||||
```
|
||||
|
||||
## Design notes
|
||||
@@ -33,10 +37,10 @@ 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.
|
||||
here is `FROM archlinux:base` 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
|
||||
@@ -50,44 +54,194 @@ Three things worth knowing about how this is put together:
|
||||
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`.**
|
||||
**What is deliberately not here: 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,
|
||||
source, not a dependency, not a directory that 'looked convenient'."* That one
|
||||
is closed, so whatever layers it on top is a build outside this repo — not
|
||||
something this repo names, links to, or depends on.
|
||||
|
||||
**Proton is here, and this paragraph used to say it was not.** The old wording
|
||||
put Proton and `steamclient.so` together and called both closed, which is
|
||||
wrong about Proton: it is compiled from source, which is not a thing you can
|
||||
do with closed software. Keeping it out cost a box the only way it has to run
|
||||
a Windows title, for a rule that did not apply to it.
|
||||
|
||||
What it is: **proton-cachyos built with `--enable-wow64`**, pulled by tag as a
|
||||
published image rather than rebuilt here, because it takes hours and moves
|
||||
only when its own tag does. `PROTON_IMAGE` overrides the tag, and it has to be
|
||||
declared before the first `FROM` — an `ARG` a `FROM` expands is global or it
|
||||
is nothing, and getting that wrong fails with `no FROM statement found`, which
|
||||
says nothing about the actual mistake. wow64 is the whole reason it is a build of ours
|
||||
and not the distribution's package — it runs 32-bit Windows code inside a
|
||||
64-bit unix process, so a box needs no lib32 glibc, no second Mesa for i686,
|
||||
and no second capture layer for 32-bit titles to be captured. The
|
||||
distribution's package is built without the flag, which is exactly why it
|
||||
depends on `lib32-*`.
|
||||
|
||||
It costs about 1.4 GB of image, and it is the one thing in here that is
|
||||
payload-shaped: a compatibility layer for Windows games in an image that is
|
||||
otherwise indifferent to what it runs. The guest components stay indifferent
|
||||
regardless — none of them branches on it, and the init does not know it
|
||||
exists. What names it is the command a caller sends.
|
||||
|
||||
`runtime_prod` from this Containerfile — tagged
|
||||
`ghcr.io/nestrilabs/nestri/base:latest` — is a complete, bootable 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.
|
||||
## Two packages that look droppable and are not
|
||||
|
||||
## The nesinit gap
|
||||
`llvm-libs` is 164 MB, the largest single thing in the image after Proton, and
|
||||
`lm_sensors` is only there because something links `libsensors`. Both look like
|
||||
leftovers of a Mesa configuration that has since been trimmed, and both have
|
||||
been checked rather than reasoned about: **`libgbm` links them**, and the
|
||||
compositor needs GBM. Trimming the Mesa build does not reach them.
|
||||
|
||||
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.
|
||||
`lm_sensors` in particular was found the hard way. It used to arrive as a
|
||||
dependency of the distribution's Mesa package, and dropping that package took
|
||||
it away — leaving our own Mesa unable to resolve `libsensors.so.5`. Nothing in
|
||||
a package list says that; the check below is what said it.
|
||||
|
||||
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.
|
||||
## Proton has its own cadence, and its own Containerfile
|
||||
|
||||
`make build` **pulls** Proton by tag; it does not build it. Building it takes
|
||||
hours and it changes only when its tag moves, so it is one image published
|
||||
once and copied into every guest image after that. `Containerfile.proton` is
|
||||
that build, and it lives here so the published tag stays reproducible from
|
||||
this tree rather than from somebody's laptop.
|
||||
|
||||
```sh
|
||||
make proton-image # the current tag
|
||||
make PROTON_TAG=cachyos-11.1-20261115-native proton-image
|
||||
```
|
||||
|
||||
**`PROTON_TAG` is the only thing to change.** The published version is derived
|
||||
from it in the `Makefile` rather than written a second time, because the two
|
||||
are the same number in two spellings — and an image whose name does not say
|
||||
which Proton is inside it is worse than no image. The `Containerfile`'s own
|
||||
`PROTON_IMAGE` default is a fallback for a bare container build; going through
|
||||
`make` is what keeps them in step.
|
||||
|
||||
Its **context is `build/`**, not the repository root the guest build uses. All
|
||||
it needs is the two scripts beside it, and `Containerfile.proton.containerignore`
|
||||
keeps `output/` out of that context — a build context is copied before the
|
||||
first instruction runs, so without it every Proton build would begin by moving
|
||||
the last rootfs image it produced.
|
||||
|
||||
Two things in the recipe are worth knowing before changing it:
|
||||
|
||||
- **Fetch and build are separate layers on purpose.** The submodule checkout
|
||||
runs well past ten minutes, and a build that fails on a flag or a missing
|
||||
tool must not pay for that again. Keep anything that can fail *fast* in
|
||||
`proton-build.sh`.
|
||||
- **`widl` is built by hand from the mingw-w64 release.** Without it autoconf
|
||||
quietly sets `HAVE_WIDL` to false, vkd3d's public headers are never
|
||||
generated, and the build dies an hour later on a missing header. Arch ships
|
||||
`widl` only inside `wine`, which wants multilib — which is the thing
|
||||
`--enable-wow64` exists to avoid.
|
||||
|
||||
## There is no init system in here, and that is the design
|
||||
|
||||
`nesinit` is PID 1. The image carries **no service manager, no init scripts,
|
||||
no `udev` and no systemd** — `systemd-libs` stays, because `dbus-daemon` and
|
||||
`wireplumber` link `libsystemd.so.0`, but nothing in the image can be PID 1
|
||||
except `nesinit`, and the build fails if anything that could be turns up.
|
||||
|
||||
That is why this is plain Arch. The image used to be Artix, chosen for OpenRC,
|
||||
and every cost of that choice — no `eudev`, no `agetty-openrc`, `udev` being
|
||||
systemd's anyway, a runlevel edit not stopping a service another one still
|
||||
needs — was paid for an init system that is no longer here.
|
||||
|
||||
**What replaced fourteen `rc-update` lines and nine init scripts:**
|
||||
|
||||
| was | now |
|
||||
|---|---|
|
||||
| `devfs`, `dmesg`, `udev`, `udev-trigger` | `devtmpfs` makes the nodes; init sets the two modes that matter. The compositor takes input through Wayland and opens nothing `udev` provides |
|
||||
| `guest-net`, `hostname`, `xdg-runtime`, `cgroups` | init, before it dials out |
|
||||
| `dbus`, `dbus-session`, `pipewire`, `wireplumber`, `neshub`, `neswire` | a table compiled into `nesinit` |
|
||||
| `nescope` in the `default` runlevel | **not a service.** It wraps the workload and is started by a launch, with that launch's geometry, and dies with it |
|
||||
| `agetty` on `hvc0` | nothing. See below |
|
||||
| `/etc/fstab` | init's own mounts, and shares named in the boot descriptor |
|
||||
|
||||
**A box is launched into, not booted into something.** Init mounts what the
|
||||
descriptor names, brings the table up, says it is ready, and then takes
|
||||
commands — so an image on its own runs nothing at all, which is the point:
|
||||
this image is payload-independent and there is no payload in it.
|
||||
|
||||
### Getting into a guest that will not boot
|
||||
|
||||
`init=/bin/bash` on the kernel command line. Nothing in the image offers a
|
||||
login prompt — there is no getty in either flavour — and that is cheaper than
|
||||
carrying one: `nesinit` is an ordinary program, so from that shell you can run
|
||||
it by hand and watch it fail. `make build-debug` adds `vulkaninfo` and friends
|
||||
and gives root a password for `su`; it does not add a console.
|
||||
|
||||
**A shell is not a booted box, and the difference bites immediately.** Nothing
|
||||
the init does has happened: the root is read-only, `/run` and `/tmp` are still
|
||||
directories on it rather than tmpfs, `/run/user/1000` is unwritable, and the
|
||||
hostname is `(none)` rather than `nesbox` — which is the quickest way to tell
|
||||
the two states apart. A compositor started in that shell fails on its own
|
||||
socket, and the error names the runtime directory rather than the cause.
|
||||
|
||||
So run `nesinit` first. It mounts, prepares the directories, brings the
|
||||
services up, then fails to reach a control channel that is not there and
|
||||
exits — **leaving everything it prepared behind**, which is exactly what makes
|
||||
the hand-run useful. Then start what you came to debug.
|
||||
|
||||
It is safe to run outside a box, and that took fixing: the shutdown path signals
|
||||
every process it may signal and then powers the machine off, which is right for
|
||||
PID 1 of a box and catastrophic anywhere else. Both steps are refused when it is
|
||||
not PID 1, and it says so rather than doing it quietly.
|
||||
|
||||
If you would rather not run it at all, the two mounts it does that a compositor
|
||||
needs are:
|
||||
|
||||
```sh
|
||||
mount -t tmpfs -o mode=1777,size=64m tmpfs /tmp
|
||||
mount -t tmpfs -o mode=755,size=32m tmpfs /run
|
||||
mkdir -p /run/user/1000 && chown 1000:1000 /run/user/1000 && chmod 0700 /run/user/1000
|
||||
```
|
||||
|
||||
Making `/run/user/1000` writable in the *image* does not help, and is worth
|
||||
saying because it is the obvious first thing to try: before the init runs, the
|
||||
directory is on a read-only root, so its ownership is not what stops a write;
|
||||
after the init runs, a fresh tmpfs is mounted over `/run` and the image's copy
|
||||
of the directory is hidden underneath it.
|
||||
|
||||
The one thing this does not reach is a failure *before* the shell. If that
|
||||
happens the evidence is on `console=hvc0` and nowhere else.
|
||||
|
||||
### Two build-time checks worth knowing about
|
||||
|
||||
Both exist because the failure they catch is invisible at runtime rather than
|
||||
loud, which is the same reason the old build checked its `conf.d` files:
|
||||
|
||||
- **No hook may point at a program that is not in the image.** Removing
|
||||
systemd removes the script `dbus-reload.hook` calls, and a leftover hook
|
||||
produces `error: command failed to execute correctly` on every future pacman
|
||||
transaction — indistinguishable, in a log, from something that matters.
|
||||
- **Nothing `nesinit` will look for may be missing.** Its service table is
|
||||
compiled in, so an absent `dbus-daemon` is not a build error by itself; it
|
||||
is a service that does not come up in a box somebody is waiting on.
|
||||
|
||||
## 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.
|
||||
`nesinit` 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 — the
|
||||
host's own default tap addressing. Keep these in step if that changes on the
|
||||
host side. A box started with no network device at all is a valid box and
|
||||
boots without one.
|
||||
|
||||
The address is a per-boot parameter rather than an image setting because the
|
||||
alternative makes every box built from this image the same host on the
|
||||
network, and two of them collide the moment they run together. Same reasoning
|
||||
for `/etc/machine-id`, which is a symlink into a tmpfs that init fills at
|
||||
boot — the previous image baked one in, so every box built from it was the
|
||||
same machine to anything that asked.
|
||||
|
||||
This is also the one thing in the image that keeps `iproute2` installed:
|
||||
init runs `ip` rather than talking netlink, which is a hundred lines of
|
||||
`unsafe` saved for an interface configured once.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
baud="115200"
|
||||
term_type="vt100"
|
||||
agetty_options="--autologin root --noclear"
|
||||
@@ -1,3 +0,0 @@
|
||||
baud="115200"
|
||||
term_type="vt100"
|
||||
agetty_options="--noclear"
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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
|
||||
@@ -1,34 +0,0 @@
|
||||
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 — currently off, in both copies. Kept, commented, because
|
||||
# the reasoning still applies if it is ever needed again: capture happens
|
||||
# through a Vulkan layer, so a process reaching the GPU via native OpenGL
|
||||
# would render fine and capture nothing, and zink is what makes such a
|
||||
# process capturable at all. Nothing has needed it so far.
|
||||
#
|
||||
# /etc/profile.d/nestri-env.sh carries the same block, also commented, and
|
||||
# only runs for login shells — nothing here is one, every service below runs
|
||||
# from OpenRC. If these are ever re-enabled, re-enable both: one copy on and
|
||||
# one off means services and shells reach the GPU by different paths.
|
||||
#__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
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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
|
||||
@@ -1,35 +0,0 @@
|
||||
# `ro`, matching how a box is actually started: nesbox passes `ro` on the
|
||||
# kernel command line and marks the root device `is_read_only: true` (see
|
||||
# nesbox's examples/vm.json), so the virtio-blk device refuses writes at the
|
||||
# device level. Saying `rw` here does not make it writable — it only asks
|
||||
# OpenRC's `root` service to attempt a remount that the device must reject.
|
||||
# Everything a running box writes to is a tmpfs or a share below.
|
||||
/dev/vda / ext4 ro,relatime 0 1
|
||||
devtmpfs /dev devtmpfs rw,nosuid 0 0
|
||||
proc /proc proc rw,nosuid,nodev,noexec 0 0
|
||||
sysfs /sys sysfs rw,nosuid,nodev,noexec 0 0
|
||||
tmpfs /tmp tmpfs rw,nosuid,nodev,size=64M 0 0
|
||||
tmpfs /run tmpfs rw,nosuid,nodev,size=32M,mode=0755 0 0
|
||||
tmpfs /var/log tmpfs rw,nosuid,nodev,size=16M 0 0
|
||||
# POSIX shared memory. devtmpfs does not provide it and nothing else mounts it,
|
||||
# so without this entry /dev/shm does not exist at all and `shm_open` fails.
|
||||
#
|
||||
# PipeWire itself gets by: it allocates buffers with memfd_create and only
|
||||
# falls back to /dev/shm. Kept as a precaution for anything else that uses
|
||||
# POSIX shm/semaphores directly — Proton is the known example, and it is not
|
||||
# part of this image yet (see build/README.md), but a failure here is silent
|
||||
# rather than fatal, so it costs nothing to have ready.
|
||||
#
|
||||
# `nosuid,nodev` and a size cap because everything a game can write to should
|
||||
# have both.
|
||||
tmpfs /dev/shm tmpfs rw,nosuid,nodev,size=256M 0 0
|
||||
# The guest's logs, on a host directory that outlives the VM.
|
||||
#
|
||||
# Here rather than mounted by a service, because the failures worth reading
|
||||
# are the ones that happen *before* anything else has mounted something — a
|
||||
# log directory that appears only after a successful start cannot record an
|
||||
# unsuccessful one. The tag is fixed by nessh, so plain fstab works.
|
||||
#
|
||||
# `nofail` because a VM started without the share still has to boot: that is
|
||||
# how somebody gets a shell to find out why it has no share.
|
||||
logs /nestri/logs virtiofs rw,nofail 0 0
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="D-Bus session bus for nestri user"
|
||||
|
||||
nestri_export_env
|
||||
|
||||
command="/usr/bin/dbus-daemon"
|
||||
command_args="--session --address=unix:path=/run/user/1000/bus --nofork --nopidfile --print-address"
|
||||
command_user="nestri:nestri"
|
||||
command_background="yes"
|
||||
pidfile="/run/nestri/dbus-session.pid"
|
||||
|
||||
depend() {
|
||||
need xdg-runtime dbus
|
||||
before pipewire wireplumber
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath -d -m 0755 -o "nestri:nestri" /run/nestri
|
||||
if [ -e "/run/user/1000/bus" ]; then
|
||||
ewarn "Removing stale bus socket"
|
||||
rm -f "/run/user/1000/bus"
|
||||
fi
|
||||
}
|
||||
|
||||
start_post() {
|
||||
local i=0
|
||||
while [ ! -S /run/user/1000/bus ] && [ $i -lt 20 ]; do
|
||||
sleep 0.1
|
||||
i=$((i+1))
|
||||
done
|
||||
[ -S /run/user/1000/bus ] || { eerror "Bus didn't appear"; return 1; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="D-Bus system message bus"
|
||||
|
||||
command="/usr/bin/dbus-daemon"
|
||||
command_args="--system --nofork --nopidfile"
|
||||
command_background="yes"
|
||||
pidfile="/run/dbus/dbus.pid"
|
||||
|
||||
depend() {
|
||||
need localmount xdg-runtime
|
||||
before pipewire
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath -d -m 0755 -o root:root /run/dbus
|
||||
checkpath -d -m 0755 -o messagebus:messagebus /var/run/dbus 2>/dev/null || true
|
||||
dbus-uuidgen --ensure=/var/lib/dbus/machine-id
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Configure the guest's network to match the host's tap"
|
||||
|
||||
# Overridden from /etc/conf.d/guest-net if present. These defaults match
|
||||
# nesbox's own defaults; if you change the `network` section in the VM's
|
||||
# JSON, change these to match.
|
||||
: ${GUEST_IFACE:=eth0}
|
||||
: ${GUEST_IP:=172.30.0.2}
|
||||
: ${GUEST_PREFIX:=24}
|
||||
: ${GUEST_GATEWAY:=172.30.0.1}
|
||||
|
||||
depend() {
|
||||
need localmount
|
||||
provide net
|
||||
keyword -shutdown
|
||||
}
|
||||
|
||||
# Read one `nestri.<key>=<value>` from the kernel command line.
|
||||
#
|
||||
# The address has to come from somewhere per-boot, because the alternative --
|
||||
# baking it into the image -- makes every guest built from that image the
|
||||
# same host on the network. Two sandboxes then collide the moment they run
|
||||
# together.
|
||||
#
|
||||
# `nestri.`-prefixed rather than the kernel's own `ip=`: that one needs
|
||||
# CONFIG_IP_PNP and exists to configure NFS root, and the prefix makes it
|
||||
# obvious whose parameter this is.
|
||||
cmdline_value() {
|
||||
local key="$1" word
|
||||
for word in $(cat /proc/cmdline 2>/dev/null); do
|
||||
case "$word" in
|
||||
"nestri.${key}="*) printf '%s' "${word#nestri.${key}=}"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Bringing up loopback"
|
||||
ip link set lo up
|
||||
eend $?
|
||||
|
||||
# The command line wins over conf.d when it says anything, and conf.d is
|
||||
# the fallback so a hand-written VM config with no parameters keeps
|
||||
# working -- which is how a guest gets debugged.
|
||||
local source="/etc/conf.d/guest-net"
|
||||
local cmdline_ip
|
||||
if cmdline_ip="$(cmdline_value ip)"; then
|
||||
# Accepts address/prefix; a bare address keeps the configured prefix
|
||||
# rather than guessing one.
|
||||
case "$cmdline_ip" in
|
||||
*/*)
|
||||
GUEST_IP="${cmdline_ip%%/*}"
|
||||
GUEST_PREFIX="${cmdline_ip##*/}"
|
||||
;;
|
||||
*) GUEST_IP="$cmdline_ip" ;;
|
||||
esac
|
||||
source="kernel command line"
|
||||
fi
|
||||
|
||||
local cmdline_gw
|
||||
if cmdline_gw="$(cmdline_value gw)"; then
|
||||
GUEST_GATEWAY="$cmdline_gw"
|
||||
source="kernel command line"
|
||||
fi
|
||||
|
||||
# The VM may have been started with no network device at all, which is a
|
||||
# perfectly good configuration. Do not fail the boot over it.
|
||||
if [ ! -e "/sys/class/net/${GUEST_IFACE}" ]; then
|
||||
einfo "no ${GUEST_IFACE}: this VM has no network device"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Says which source won, because "the address is wrong" and "the address
|
||||
# came from somewhere unexpected" look identical from inside the guest.
|
||||
ebegin "Configuring ${GUEST_IFACE} as ${GUEST_IP}/${GUEST_PREFIX} via ${GUEST_GATEWAY} (from ${source})"
|
||||
# `replace` rather than `add` so a restart is not an error.
|
||||
ip link set "${GUEST_IFACE}" up &&
|
||||
ip addr replace "${GUEST_IP}/${GUEST_PREFIX}" dev "${GUEST_IFACE}" &&
|
||||
ip route replace default via "${GUEST_GATEWAY}" dev "${GUEST_IFACE}"
|
||||
eend $? "could not configure ${GUEST_IFACE}"
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [ -e "/sys/class/net/${GUEST_IFACE}" ]; then
|
||||
ebegin "Bringing down ${GUEST_IFACE}"
|
||||
ip link set "${GUEST_IFACE}" down
|
||||
eend 0
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Nestri headless compositor"
|
||||
|
||||
nestri_export_env
|
||||
|
||||
: "${NESTRI_UID:=1000}"
|
||||
: "${NESTRI_USER:=nestri}"
|
||||
|
||||
command="/usr/bin/nescope"
|
||||
# No command after `--`: plain-compositor mode. nescope comes up and waits
|
||||
# for something to connect rather than wrapping a payload — starting one is
|
||||
# nesinit's job, and nesinit is not open code yet. See build/README.md.
|
||||
command_user="${NESTRI_USER}:${NESTRI_USER}"
|
||||
command_background="yes"
|
||||
pidfile="/run/nestri/nescope.pid"
|
||||
output_log="/nestri/logs/nescope.log"
|
||||
error_log="/nestri/logs/nescope.log"
|
||||
respawn="yes"
|
||||
respawn_delay="2"
|
||||
respawn_max="2"
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/${NESTRI_UID}"
|
||||
|
||||
depend() {
|
||||
need xdg-runtime
|
||||
use neshub
|
||||
after xdg-runtime
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath -d -m 0755 -o "${NESTRI_USER}:${NESTRI_USER}" /run/nestri
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Nestri media hub"
|
||||
|
||||
nestri_export_env
|
||||
|
||||
: "${NESTRI_UID:=1000}"
|
||||
: "${NESTRI_USER:=nestri}"
|
||||
|
||||
command="/usr/bin/neshub"
|
||||
# Unprivileged. The old nestri-guest-hub ran as root and mounted the
|
||||
# session's filesystem itself; the open neshub does neither — per its own
|
||||
# README it only muxes the Unix sockets the other components dial into one
|
||||
# QUIC endpoint. Whatever ends up owning session mounts (nesinit, presumably)
|
||||
# is a separate, still-closed piece.
|
||||
command_user="${NESTRI_USER}:${NESTRI_USER}"
|
||||
command_background="yes"
|
||||
pidfile="/run/nestri/neshub.pid"
|
||||
|
||||
# Where neshub's output goes. /nestri/logs is a virtiofs share mounted from
|
||||
# /etc/fstab at boot, before this service starts, so a hub that fails
|
||||
# immediately still leaves a record, and the record survives the VM.
|
||||
output_log="/nestri/logs/neshub.log"
|
||||
error_log="/nestri/logs/neshub.log"
|
||||
respawn="yes"
|
||||
respawn_delay="2"
|
||||
respawn_max="2" # NO infinite — if it fails, it fails for a reason
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/${NESTRI_UID}"
|
||||
|
||||
depend() {
|
||||
need xdg-runtime
|
||||
use net
|
||||
after xdg-runtime
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath -d -m 0755 -o "${NESTRI_USER}:${NESTRI_USER}" /run/nestri
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="PipeWire multimedia daemon (system mode for nestri)"
|
||||
|
||||
nestri_export_env
|
||||
|
||||
: "${NESTRI_UID:=1000}"
|
||||
: "${NESTRI_USER:=nestri}"
|
||||
|
||||
command="/usr/bin/pipewire"
|
||||
command_user="${NESTRI_USER}:${NESTRI_USER}"
|
||||
command_background="yes"
|
||||
pidfile="/run/pipewire/pipewire.pid"
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/${NESTRI_UID}"
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${NESTRI_UID}/bus"
|
||||
|
||||
depend() {
|
||||
need xdg-runtime dbus
|
||||
use dbus
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath -d -m 0755 -o "${NESTRI_USER}:${NESTRI_USER}" /run/pipewire
|
||||
}
|
||||
|
||||
# `need pipewire` only waits for this script to return, and with
|
||||
# command_background that is the moment start-stop-daemon forks -- not the
|
||||
# moment PipeWire is accepting connections. With rc_parallel="YES" every
|
||||
# client (wireplumber, neswire) then races the socket and a client that
|
||||
# loses simply exits.
|
||||
#
|
||||
# dbus-session already solved exactly this for the bus socket. Same shape
|
||||
# here.
|
||||
start_post() {
|
||||
local i=0
|
||||
while [ ! -S "/run/user/${NESTRI_UID}/pipewire-0" ] && [ $i -lt 50 ]; do
|
||||
sleep 0.1
|
||||
i=$((i+1))
|
||||
done
|
||||
[ -S "/run/user/${NESTRI_UID}/pipewire-0" ] || {
|
||||
eerror "PipeWire socket never appeared"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="WirePlumber session manager for PipeWire"
|
||||
|
||||
nestri_export_env
|
||||
|
||||
: "${NESTRI_UID:=1000}"
|
||||
: "${NESTRI_USER:=nestri}"
|
||||
|
||||
command="/usr/bin/wireplumber"
|
||||
command_user="${NESTRI_USER}:${NESTRI_USER}"
|
||||
command_background="yes"
|
||||
pidfile="/run/pipewire/wireplumber.pid"
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/${NESTRI_UID}"
|
||||
|
||||
depend() {
|
||||
need pipewire
|
||||
after pipewire
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Create runtime directories (XDG_RUNTIME_DIR, X11 socket dir..)"
|
||||
|
||||
NESTRI_UID="${NESTRI_UID:-1000}"
|
||||
NESTRI_USER="${NESTRI_USER:-nestri}"
|
||||
|
||||
depend() {
|
||||
need localmount
|
||||
before dbus pipewire
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Preparing runtime directories"
|
||||
|
||||
if ! getent passwd "${NESTRI_USER}" >/dev/null 2>&1; then
|
||||
eerror "User ${NESTRI_USER} does not exist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Per-user XDG runtime dir
|
||||
mkdir -p "/run/user/${NESTRI_UID}"
|
||||
chown "${NESTRI_USER}:${NESTRI_USER}" "/run/user/${NESTRI_UID}"
|
||||
chmod 0700 "/run/user/${NESTRI_UID}"
|
||||
|
||||
# X11 socket dir (Xwayland + any X clients expect this)
|
||||
mkdir -p /tmp/.X11-unix
|
||||
chown root:root /tmp/.X11-unix
|
||||
chmod 1777 /tmp/.X11-unix
|
||||
|
||||
# ICE socket dir - some toolkits look for it
|
||||
mkdir -p /tmp/.ICE-unix
|
||||
chown root:root /tmp/.ICE-unix
|
||||
chmod 1777 /tmp/.ICE-unix
|
||||
|
||||
eend $?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Removing runtime directories"
|
||||
rm -rf "/run/user/${NESTRI_UID}"
|
||||
# Don't rm /tmp/.X11-unix on stop - other things may be using it
|
||||
eend 0
|
||||
}
|
||||
@@ -14,14 +14,22 @@ if [ "$(id -u)" = "1000" ]; then
|
||||
export XDG_SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
|
||||
export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-nestri}"
|
||||
|
||||
# Ensure proper VAAPI driver is used
|
||||
#export LIBVA_DRIVER_NAME="radeonsi"
|
||||
|
||||
# Force zink usage for OpenGL -> Vulkan translation
|
||||
#export __GLX_VENDOR_LIBRARY_NAME=mesa
|
||||
#export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||
#export GALLIUM_DRIVER=zink
|
||||
|
||||
# Ensure standard XDG dirs exist
|
||||
mkdir -p "${XDG_CONFIG_HOME}" "${XDG_DATA_HOME}" "${XDG_CACHE_HOME}" "${XDG_STATE_HOME}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# OpenGL -> Vulkan, for anybody who reaches a shell in here.
|
||||
#
|
||||
# Outside the uid test above on purpose, and duplicated from the init on
|
||||
# purpose. This file is only ever read by a person who got a shell in a box --
|
||||
# nothing a box runs is started from a login, so every service and every
|
||||
# workload is exec'd with a cleared environment and never sees this. The init
|
||||
# sets the same three for what it starts.
|
||||
#
|
||||
# It is here so that a debug shell renders the way a session does. A shell that
|
||||
# silently has no GL driver is how somebody concludes the image is broken while
|
||||
# the image is fine.
|
||||
export __GLX_VENDOR_LIBRARY_NAME=mesa
|
||||
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||
export GALLIUM_DRIVER=zink
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Nestri rootfs OpenRC config
|
||||
|
||||
rc_parallel="YES"
|
||||
rc_depend_strict="NO"
|
||||
rc_interactive="NO"
|
||||
rc_shell_timeout="0"
|
||||
# Every service's start/fail line, on the host directory that outlives the VM.
|
||||
#
|
||||
# Without this a guest nobody can log into reports nothing about its own boot:
|
||||
# a service that was skipped because a dependency failed looks exactly like a
|
||||
# service that was never registered. /nestri/logs is a virtiofs share, so the
|
||||
# record survives the guest — see the fstab entry below.
|
||||
rc_logger="YES"
|
||||
rc_log_path="/nestri/logs/rc.log"
|
||||
rc_verbose="NO"
|
||||
|
||||
# cgroup v2
|
||||
rc_cgroup_mode="unified"
|
||||
|
||||
# Don't bother trying to set hostname twice
|
||||
rc_hotplug="!net.*"
|
||||
|
||||
# Faster sulogin behavior on emergency
|
||||
rc_shell="/bin/sh"
|
||||
|
||||
# Default umask
|
||||
umask 022
|
||||
@@ -15,14 +15,14 @@ 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}"
|
||||
SIZE="${3:-3G}"
|
||||
|
||||
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)"
|
||||
CONTAINER_RT="$(command -v podman || command -v docker || 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
|
||||
|
||||
96
build/scripts/proton-build.sh
Executable file
96
build/scripts/proton-build.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds proton-cachyos from the tree proton-fetch.sh laid down. Container-only.
|
||||
#
|
||||
# The one thing that matters here is --enable-wow64: it builds wine so that
|
||||
# 32-bit Windows code runs inside a 64-bit unix process, thunking down to the
|
||||
# 64-bit host libraries. Without it, Proton needs a complete 32-bit host stack —
|
||||
# lib32 glibc, a second Mesa built for i686, and a second nescapture layer,
|
||||
# because a 32-bit game would load the 32-bit Vulkan loader and our 64-bit
|
||||
# capture layer would be invisible to it. With it, none of that exists.
|
||||
#
|
||||
# The cost is that the distro package cannot be used: proton-cachyos-native is
|
||||
# packaged without the flag, which is exactly why it depends on lib32-*.
|
||||
set -euo pipefail
|
||||
|
||||
: "${GECKO_VER:?}"
|
||||
: "${MONO_VER:?}"
|
||||
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
BUILD_NAME="proton-cachyos"
|
||||
SRC_DIR="/build/proton-cachyos"
|
||||
BUILD_DIR="/build/build"
|
||||
OUT_DIR="/artifacts/proton/usr/share/steam/compatibilitytools.d/${BUILD_NAME}"
|
||||
|
||||
[[ -d "${SRC_DIR}" ]] || { echo "no source tree — proton-fetch.sh did not run"; exit 1; }
|
||||
|
||||
# ── Toolchain wrappers ──────────────────────────────────
|
||||
# Proton's build calls the compiler by GNU triplet. Arch's gcc does not install
|
||||
# under those names, so stand in for them. The i686 set is generated too: with
|
||||
# wow64 nothing should reach for it, and if something does, failing on a missing
|
||||
# 32-bit header beats silently building a 32-bit unix library we then have to
|
||||
# ship libraries for.
|
||||
WRAP=/build/wrappers
|
||||
rm -rf "$WRAP" && mkdir -p "$WRAP"
|
||||
_wrappers() {
|
||||
local arch="$1" gccflag="$2" ldflag="$3" asflag="$4" stripfmt="$5"
|
||||
local l t
|
||||
for l in ar ranlib nm; do
|
||||
ln -sf "/usr/bin/gcc-${l}" "${WRAP}/${arch}-pc-linux-gnu-${l}"
|
||||
done
|
||||
for t in gcc g++; do
|
||||
printf '#!/usr/bin/bash\n/usr/bin/%s %s "$@"\n' "$t" "$gccflag" \
|
||||
> "${WRAP}/${arch}-pc-linux-gnu-${t}"
|
||||
chmod 755 "${WRAP}/${arch}-pc-linux-gnu-${t}"
|
||||
done
|
||||
printf '#!/usr/bin/bash\n/usr/bin/ld %s "$@"\n' "$ldflag" > "${WRAP}/${arch}-pc-linux-gnu-ld"
|
||||
printf '#!/usr/bin/bash\n/usr/bin/as %s "$@"\n' "$asflag" > "${WRAP}/${arch}-pc-linux-gnu-as"
|
||||
printf '#!/usr/bin/bash\n/usr/bin/strip -F %s "$@"\n' "$stripfmt" > "${WRAP}/${arch}-pc-linux-gnu-strip"
|
||||
chmod 755 "${WRAP}/${arch}-pc-linux-gnu-"{ld,as,strip}
|
||||
}
|
||||
_wrappers x86_64 "-m64" "-melf_x86_64" "--64" "elf64-x86-64"
|
||||
_wrappers i686 "-m32" "-melf_i386" "--32" "elf32-i386"
|
||||
export PATH="${WRAP}:${PATH}"
|
||||
|
||||
# ── Build ───────────────────────────────────────────────
|
||||
# -march=nocona matches the distro packaging: Proton has to run on whatever CPU
|
||||
# the guest is given, and the VMM does not promise a feature level.
|
||||
export CFLAGS="-O3 -march=nocona -mtune=core-avx2"
|
||||
export CXXFLAGS="${CFLAGS}"
|
||||
export RUSTFLAGS="-C opt-level=3 -C target-cpu=nocona"
|
||||
export LDFLAGS="-Wl,-O1,--sort-common,--as-needed"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
cd "${BUILD_DIR}"
|
||||
|
||||
ROOTLESS_CONTAINER="" \
|
||||
"${SRC_DIR}/configure.sh" \
|
||||
--container-engine="none" \
|
||||
--proton-sdk-image="" \
|
||||
--build-name="${BUILD_NAME}" \
|
||||
--without-extras=all \
|
||||
--without-vklayers=all \
|
||||
--without-steamrt-depends \
|
||||
--without-tts \
|
||||
--without-nvidia-libs \
|
||||
--enable-wow64
|
||||
|
||||
# The top-level make is serial by design; SUBJOBS is what it hands to each
|
||||
# component's build.
|
||||
SUBJOBS="${JOBS}" make -j1 dist
|
||||
|
||||
# ── Install ─────────────────────────────────────────────
|
||||
mkdir -p "${OUT_DIR}"
|
||||
cp -a "${BUILD_DIR}/dist/." "${OUT_DIR}/"
|
||||
|
||||
# Debug symbols in the bundled PE runtimes are dead weight in a guest image.
|
||||
cd "${OUT_DIR}/files"
|
||||
find "share/wine/gecko/wine-gecko-${GECKO_VER}-x86" -name '*.dll' -o -name '*.exe' 2>/dev/null \
|
||||
| xargs -r i686-w64-mingw32-strip --strip-debug 2>/dev/null || true
|
||||
find "share/wine/gecko/wine-gecko-${GECKO_VER}-x86_64" -name '*.dll' -o -name '*.exe' 2>/dev/null \
|
||||
| xargs -r x86_64-w64-mingw32-strip --strip-debug 2>/dev/null || true
|
||||
find "share/wine/mono/wine-mono-${MONO_VER}" -name '*.dll' -o -name '*.exe' 2>/dev/null \
|
||||
| xargs -r i686-w64-mingw32-strip --strip-debug 2>/dev/null || true
|
||||
|
||||
rm -rf "${BUILD_DIR}"
|
||||
echo "proton: installed to ${OUT_DIR}"
|
||||
57
build/scripts/proton-fetch.sh
Executable file
57
build/scripts/proton-fetch.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetches proton-cachyos' source and its bundled runtimes. Container-only.
|
||||
#
|
||||
# Deliberately its own script, and its own layer: the submodule checkout runs
|
||||
# well past ten minutes, and it must not be redone every time a build flag or a
|
||||
# missing dependency changes. Keep everything that can fail *fast* in
|
||||
# proton-build.sh instead.
|
||||
set -euo pipefail
|
||||
|
||||
: "${PROTON_GIT:?}"
|
||||
: "${PROTON_TAG:?}"
|
||||
: "${GECKO_VER:?}"
|
||||
: "${MONO_VER:?}"
|
||||
: "${XALIA_VER:?}"
|
||||
|
||||
SRC_DIR="/build/proton-cachyos"
|
||||
|
||||
git clone --branch "${PROTON_TAG}" --depth=1 "${PROTON_GIT}" "${SRC_DIR}"
|
||||
cd "${SRC_DIR}"
|
||||
# Relative submodule paths resolve against origin, so it has to be the real URL
|
||||
# even though we cloned by tag.
|
||||
git remote set-url origin "${PROTON_GIT}"
|
||||
# No --depth here: submodules are pinned to commits that are often not a branch
|
||||
# tip. --filter=tree:0 keeps the download down instead.
|
||||
git submodule update --init --filter=tree:0 --recursive
|
||||
|
||||
# Still needed with wow64: these are PE-side, and a 32-bit Windows program wants
|
||||
# the 32-bit gecko and mono regardless of how wine is built.
|
||||
mkdir -p contrib
|
||||
for url in \
|
||||
"https://dl.winehq.org/wine/wine-gecko/${GECKO_VER}/wine-gecko-${GECKO_VER}-x86.tar.xz" \
|
||||
"https://dl.winehq.org/wine/wine-gecko/${GECKO_VER}/wine-gecko-${GECKO_VER}-x86_64.tar.xz" \
|
||||
"https://github.com/madewokherd/wine-mono/releases/download/wine-mono-${MONO_VER}/wine-mono-${MONO_VER}-x86.tar.xz" \
|
||||
"https://github.com/madewokherd/xalia/releases/download/xalia-${XALIA_VER}/xalia-${XALIA_VER}-net48-mono.zip" \
|
||||
; do
|
||||
curl -fL --retry 3 -o "contrib/$(basename "$url")" "$url"
|
||||
done
|
||||
|
||||
# Proton's cargo rule runs `cargo build --locked --offline`, so every crate has
|
||||
# to be in CARGO_HOME before the build starts — including the git dependencies,
|
||||
# which is what the "you are in the offline mode" failure is really saying. The
|
||||
# error names a URL that is perfectly reachable; the build simply refuses to go
|
||||
# out and get it.
|
||||
#
|
||||
# gst-plugins-rs is the only cargo component in the tree. Both targets are
|
||||
# fetched: wow64 should mean nothing builds the i386 unix side, but a fetch is
|
||||
# metadata only and costs almost nothing next to being wrong about that.
|
||||
#
|
||||
# CARGO_HOME is left at its default so it lands in this layer and the build
|
||||
# layer inherits it.
|
||||
export CARGO_NET_GIT_FETCH_WITH_CLI=true
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cd "${SRC_DIR}/gst-plugins-rs"
|
||||
cargo fetch --locked --target x86_64-unknown-linux-gnu
|
||||
cargo fetch --locked --target i686-unknown-linux-gnu
|
||||
|
||||
echo "proton: source at ${SRC_DIR}"
|
||||
Reference in New Issue
Block a user