feat(graphics): the discovery layer moves to the program, and WSL2 gets a GPU - #539
Closed
Sunrisepeak wants to merge 7 commits into
Closed
feat(graphics): the discovery layer moves to the program, and WSL2 gets a GPU#539Sunrisepeak wants to merge 7 commits into
Sunrisepeak wants to merge 7 commits into
Conversation
The generator used to model xvm / programs / archs in its core, which meant every other ecosystem's pages carried them too. They are xlings concepts, so they live in `.xpkgindex/plugins/xim.py` now. - identity stays the short name: in xlings a descriptor's `namespace` (`config`, `xim`) is a classification label, and package references resolve as `[index:]name[@Version]` against the index repo — so joining it into the install command, as the mcpp side must, would produce commands the client rejects. Spelled out rather than left to the core default so a change there cannot silently rewrite 155 install commands - a descriptor without a `namespace` is not un-namespaced: it resolves in the index's default namespace, so those 146 packages group under `xim` instead of a "—" bucket that does not exist - facets from the fields this index actually populates: kind, category, status - `card` listing layout, where the row leads with the copyable install command - theme retoned (teal) so the two consumer sites are visually distinct - stale links fixed: d2learn/* moved to openxlings/*
Same documents, one section name shared with the other consumer index. The older `guides` config key still works, so this is a rename rather than a migration.
Install xlings, install a package from this index, verify it runs — then searching, listing and updating. Every command is taken from this repo's own README rather than written from memory: notably each install is a single package, which is the only form the repo documents. Also: the About page's project link went to the website; the website stays under the docs link and "project" now points at the repository, matching how mcpp-index reads.
The index defaults to Chinese, so an English or Traditional visitor got a Chinese title, a Chinese quick-start card and Chinese doc names. Site title and lede, the install label, the card, the doc nav titles, the kind/category/ status axes and the Package/Credits/Keywords headings now carry zh / en / zh-Hant. Descriptor field names — programs, aliases, authors — stay as written, so a package page and its .lua still read as the same document. Also splits the two links that shared one entry: 'website' is xlings.d2learn.org (globe icon), 'docs' now points at the documentation itself.
…ts a GPU
The 22-package graphics stack renders in a container with no /usr at all, and
until this commit nothing could reach it: no package in the index depended on
`mesa`, and the environment mesa needs -- which driver module to load, which GL
vendors exist -- existed only inside a `subos use` sub-shell. `xlings install
godot && godot` in an ordinary terminal got the stack on RPATH and no way to
find a driver.
Surveyed six ecosystems first (Steam pressure-vessel, NixOS, nixGL,
nix-system-graphics, Snap gpu-2404, Flatpak, conda). Not one binds graphics
discovery to a shell. Nix wraps the program, Snap uses a command-chain wrapper,
Flatpak and pressure-vessel set it for the sandbox. And nixGL's README
independently documents the failure we already hit with LD_LIBRARY_PATH: env
vars propagate to every child, and system programs crash.
So the rule this commit applies: configuration binds to the NARROWEST scope
that works. Object (RPATH) < program (`xvm.add{envs}`) < shell (`subos.env`).
The stack used only the widest one; `xvm.add{envs}` has existed all along and
ten recipes use it.
## libs/graphics.lua -- one table, two renderings
`consumer_envs()` for a program's shim (`${XLINGS_DYNAMIC_SUBOS_DIR}`-relative,
resolved at DISPATCH against the active subos) and `declare_subos_env()` for
the shell (`${subosdir}`), generated from one table so the two cannot disagree.
Values are subos-relative rather than `${pkgdir}`: a payload path pins a
version directory, and the subos view is the stable indirection -- the role
/run/opengl-driver plays on NixOS. `mesa` now places `lib/dri` into
`<subos>/usr/lib/dri`, so it is complete by construction.
`usr/lib/dri`, not `lib/dri`, and that is not cosmetic: xlings only permits a
file asset whose destination starts with usr/etc/share, and a REJECTED
DESTINATION IS NOT AN ERROR -- the placement silently does not happen. A
`lib/dri` would have installed cleanly, pointed LIBGL_DRIVERS_PATH at nothing
and rendered on llvmpipe.
## A1: one shared glvnd vendor directory
libglvnd scans `__EGL_VENDOR_LIBRARY_DIRS` in list order and sorts files WITHIN
each directory by name. So with mesa and nvidia-gl-host-link each contributing
their own payload directory, the `10_nvidia` < `50_mesa` convention meant
nothing, and NVIDIA-before-mesa held only because xlings sorts providers by
binding string and `mesa@` sorts before `nvidia-gl-host-link@`. Correct by
alphabet. Both now place into one directory and declare that one, so the
filename decides -- as it does on the host.
Distributions went through this: before libglvnd they swapped the whole
libGL.so.1 with update-alternatives / eselect opengl, one vendor at a time.
Two directories is the halfway state.
## A3 + A9: one command, five host shapes, no conditionals
`graphics` (0 payload) depends on mesa plus two sentinels. Each probes for a
userspace half it does not own and succeeds having linked nothing when it is
absent -- so depending on both is correct everywhere and the dependency graph
adapts by itself. That is what conda gets from `__cuda`/`__glibc` virtual
packages, without a solver feature.
`wsl-gl-host-link` is new, and WSL2 was a platform this stack silently did not
support: there is no DRM node for the GPU, it is reached only through mesa's
`d3d12` gallium driver and /usr/lib/wsl/lib, so a subos on WSL2 rendered on
llvmpipe with no diagnostic.
It needs NO interposer, unlike the NVIDIA sentinel, and the reason is
structural: glvnd dlopens a vendor BY NAME, so libGLX_nvidia is its own
load-chain root with no RPATH of ours above it. `libd3d12core.so` is dlopened by
`d3d12_dri.so`, which is OUR file with OUR DT_RPATH -- verified on the shipped
payload, whose dri modules carry a full RPATH ending in the subos lib
directory. A symlink in the farm is enough.
It links the WHOLE set (R7: libd3d12core dlopens libdxcore, a Microsoft file
with no RPATH of ours), measures and records the borrowed set's glibc floor --
the line between "borrowing" and "chasing versions", and mcpp#352 is what the
wrong side of it looks like -- and declares GALLIUM_DRIVER=d3d12 only on WSL,
because mesa does not always select d3d12 by itself (microsoft/wslg#1332).
## A8: four answers to "where is the host's <soname>", three wrong
libs/hostlib.lua: ask `ldconfig -p` (the loader's own index, and the only thing
that knows /usr/lib/wsl/lib), filter by ELF class, first hit wins. There is no
layout to assume -- FHS biarch makes /usr/lib 32-bit, Debian declined that
clause so its /usr/lib is 64-bit, Arch is a third answer -- so a candidate list
is a decision to be wrong somewhere. That is mcpp#352.
nvidia-gl-host-link had it right; godot was right by ordering accident with no
ABI check; verify-host-link.sh pinned the Debian path and SKIPPED ITS OWN CHECK
4 IN SILENCE on every other distribution while still printing PASS. All now go
through hostlib. The new CI invariant found a fifth: libcuda-host-link, the
package the whole sentinel pattern came from.
The verifier now counts skips and reports them in the verdict, because "PASS: 12
checks" was compatible with two of them never having run.
## A6: the driver pair, checked at run time
A distribution makes "kernel module and driver userspace are one version" an
install-time invariant and DKMS rebuilds on kernel upgrade. We do not own the
kernel module, so the same invariant can only be a run-time check: the sentinel
stamps /sys/module/nvidia/version and ships `xlings-gl-doctor`, which compares
it to the live one, lists dangling links and reports the interposer fraction.
The install log now carries the driver version, because `4/4` is only true for
the driver it was measured against.
Not a doctor rule: this repo already has three report/repair pairs that drifted
because the check and the fix live in different files.
## A7: godot uses the stack
deps `graphics`, `envs = graphics.consumer_envs()`, and the host-directory probe
demoted to a fallback for a home without the stack -- kept, because removing it
breaks every existing install. The `xpm.linux` comment claiming a full
X11/mesa/wayland stack via xim is "infeasible" is corrected: it has been
falsified, and borrowing the host's Mesa is not merely unnecessary but the
failure mode, since it tracks the host's glibc forever (mcpp#352).
Tested: 779 static tests pass; libs/hostlib.lua has a plain-Lua behaviour
harness run against a FORGED BIARCH HOST (32-bit libGLX in /usr/lib, the real
one in /usr/lib64) -- the #352 shape -- 10/10, skipped rather than failed where
no lua interpreter exists.
Design: openxlings/xlings .agents/docs/2026-08-07-graphics-experience-industry-survey-and-plan.md
Refs: mcpp-community/mcpp#352
Measured: `xim:mesa@>=25.0.7` and `@>=25.0.7.1` both resolve to "package not found", while `@25.0.7.1` and a bare `xim:mesa` resolve. Every other range in these recipes (`@>=0.1`, `@>=1.8`, `@>=2.39`) works, so the resolver's comparison is fine and the four-component version is what it cannot parse -- and mesa's fourth component is deliberately ours. So the plan's "use lower bounds" is impossible for mesa specifically. Bare namespaced name, with the measurement recorded at the call site: the failure reads as a missing package rather than an unparseable constraint, which is the wrong thing to send the next reader looking for.
…mething else `S1: closure incomplete` is only a valid reading of a failure if the probe would have SUCCEEDED in the same container with the host present. Measured 2026-08-07: it does not. The same subos-built probe renders `llvmpipe (LLVM 20.1.7)` with RESULT=ok outside any container, and loads ZERO EGL vendors inside one -- identically with no unshare, --unshare-net, --unshare-user and --unshare-all, and identically with /usr, /etc and /lib bound. The vendor's DT_NEEDED closure resolves with 0 unresolved under our own loader, and the vendor JSON and .so are readable in the container. So the failure has nothing to do with the absence of /usr, which is the single variable this test exists to isolate -- and the message was sending the reader to look at the dependency closure. Now the sealed run is preceded by a CONTROL: the identical container with the host fully bound. If the control fails, the script exits 2 as INCONCLUSIVE and says what that means, instead of running the sealed case and mis-attributing the same failure. A test that reports a wrong cause is worse than one that reports nothing, because someone acts on it. Also names the mount-order trap in run_in_container, where the existing note about it was three paragraphs away from the code: --tmpfs /tmp must precede the home bind or a home under /tmp is shadowed and the binary 'does not exist'. My first namespace bisect hit exactly that and produced no output for every variant, which reads as a crash. Verified: on this home the script now exits 2 with the INCONCLUSIVE explanation rather than 1 with 'closure incomplete'.
Member
Author
|
Reopening as a fresh PR: the pull_request event never reached Actions during the outage, and only that event makes changed-files diff against base. A workflow_dispatch run went green having installed zero packages — the exact #532 shape this workflow's header warns about. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 22-package graphics stack renders in a container with no /usr at all, and
until this commit nothing could reach it: no package in the index depended on
mesa, and the environment mesa needs -- which driver module to load, which GLvendors exist -- existed only inside a
subos usesub-shell.xlings install godot && godotin an ordinary terminal got the stack on RPATH and no way tofind a driver.
Surveyed six ecosystems first (Steam pressure-vessel, NixOS, nixGL,
nix-system-graphics, Snap gpu-2404, Flatpak, conda). Not one binds graphics
discovery to a shell. Nix wraps the program, Snap uses a command-chain wrapper,
Flatpak and pressure-vessel set it for the sandbox. And nixGL's README
independently documents the failure we already hit with LD_LIBRARY_PATH: env
vars propagate to every child, and system programs crash.
So the rule this commit applies: configuration binds to the NARROWEST scope
that works. Object (RPATH) < program (
xvm.add{envs}) < shell (subos.env).The stack used only the widest one;
xvm.add{envs}has existed all along andten recipes use it.
libs/graphics.lua -- one table, two renderings
consumer_envs()for a program's shim (${XLINGS_DYNAMIC_SUBOS_DIR}-relative,resolved at DISPATCH against the active subos) and
declare_subos_env()forthe shell (
${subosdir}), generated from one table so the two cannot disagree.Values are subos-relative rather than
${pkgdir}: a payload path pins aversion directory, and the subos view is the stable indirection -- the role
/run/opengl-driver plays on NixOS.
mesanow placeslib/driinto<subos>/usr/lib/dri, so it is complete by construction.usr/lib/dri, notlib/dri, and that is not cosmetic: xlings only permits afile asset whose destination starts with usr/etc/share, and a REJECTED
DESTINATION IS NOT AN ERROR -- the placement silently does not happen. A
lib/driwould have installed cleanly, pointed LIBGL_DRIVERS_PATH at nothingand rendered on llvmpipe.
A1: one shared glvnd vendor directory
libglvnd scans
__EGL_VENDOR_LIBRARY_DIRSin list order and sorts files WITHINeach directory by name. So with mesa and nvidia-gl-host-link each contributing
their own payload directory, the
10_nvidia<50_mesaconvention meantnothing, and NVIDIA-before-mesa held only because xlings sorts providers by
binding string and
mesa@sorts beforenvidia-gl-host-link@. Correct byalphabet. Both now place into one directory and declare that one, so the
filename decides -- as it does on the host.
Distributions went through this: before libglvnd they swapped the whole
libGL.so.1 with update-alternatives / eselect opengl, one vendor at a time.
Two directories is the halfway state.
A3 + A9: one command, five host shapes, no conditionals
graphics(0 payload) depends on mesa plus two sentinels. Each probes for auserspace half it does not own and succeeds having linked nothing when it is
absent -- so depending on both is correct everywhere and the dependency graph
adapts by itself. That is what conda gets from
__cuda/__glibcvirtualpackages, without a solver feature.
wsl-gl-host-linkis new, and WSL2 was a platform this stack silently did notsupport: there is no DRM node for the GPU, it is reached only through mesa's
d3d12gallium driver and /usr/lib/wsl/lib, so a subos on WSL2 rendered onllvmpipe with no diagnostic.
It needs NO interposer, unlike the NVIDIA sentinel, and the reason is
structural: glvnd dlopens a vendor BY NAME, so libGLX_nvidia is its own
load-chain root with no RPATH of ours above it.
libd3d12core.sois dlopened byd3d12_dri.so, which is OUR file with OUR DT_RPATH -- verified on the shippedpayload, whose dri modules carry a full RPATH ending in the subos lib
directory. A symlink in the farm is enough.
It links the WHOLE set (R7: libd3d12core dlopens libdxcore, a Microsoft file
with no RPATH of ours), measures and records the borrowed set's glibc floor --
the line between "borrowing" and "chasing versions", and mcpp#352 is what the
wrong side of it looks like -- and declares GALLIUM_DRIVER=d3d12 only on WSL,
because mesa does not always select d3d12 by itself (microsoft/wslg#1332).
A8: four answers to "where is the host's ", three wrong
libs/hostlib.lua: ask
ldconfig -p(the loader's own index, and the only thingthat knows /usr/lib/wsl/lib), filter by ELF class, first hit wins. There is no
layout to assume -- FHS biarch makes /usr/lib 32-bit, Debian declined that
clause so its /usr/lib is 64-bit, Arch is a third answer -- so a candidate list
is a decision to be wrong somewhere. That is mcpp#352.
nvidia-gl-host-link had it right; godot was right by ordering accident with no
ABI check; verify-host-link.sh pinned the Debian path and SKIPPED ITS OWN CHECK
4 IN SILENCE on every other distribution while still printing PASS. All now go
through hostlib. The new CI invariant found a fifth: libcuda-host-link, the
package the whole sentinel pattern came from.
The verifier now counts skips and reports them in the verdict, because "PASS: 12
checks" was compatible with two of them never having run.
A6: the driver pair, checked at run time
A distribution makes "kernel module and driver userspace are one version" an
install-time invariant and DKMS rebuilds on kernel upgrade. We do not own the
kernel module, so the same invariant can only be a run-time check: the sentinel
stamps /sys/module/nvidia/version and ships
xlings-gl-doctor, which comparesit to the live one, lists dangling links and reports the interposer fraction.
The install log now carries the driver version, because
4/4is only true forthe driver it was measured against.
Not a doctor rule: this repo already has three report/repair pairs that drifted
because the check and the fix live in different files.
A7: godot uses the stack
deps
graphics,envs = graphics.consumer_envs(), and the host-directory probedemoted to a fallback for a home without the stack -- kept, because removing it
breaks every existing install. The
xpm.linuxcomment claiming a fullX11/mesa/wayland stack via xim is "infeasible" is corrected: it has been
falsified, and borrowing the host's Mesa is not merely unnecessary but the
failure mode, since it tracks the host's glibc forever (mcpp#352).
Tested: 779 static tests pass; libs/hostlib.lua has a plain-Lua behaviour
harness run against a FORGED BIARCH HOST (32-bit libGLX in /usr/lib, the real
one in /usr/lib64) -- the #352 shape -- 10/10, skipped rather than failed where
no lua interpreter exists.
Design: openxlings/xlings .agents/docs/2026-08-07-graphics-experience-industry-survey-and-plan.md
Refs: mcpp-community/mcpp#352