Skip to content

Commit f44e896

Browse files
authored
feat(glx-runtime): GL 来自生态而非 /usr/lib —— #352 的另一半 (#179)
* feat(glx-runtime): GL comes from the ecosystem, not from /usr/lib This package symlinked the HOST's libGL/libEGL out of /usr/lib*. That is what mcpp#352 is: the host's Mesa needs GLIBC_2.43, mcpp's payload glibc is 2.39, and the program linked cleanly then exited 255 with no output at all. It is also the first entry on the xlings hermetic policy's forbidden list -- any .so under /usr/lib* or /lib*. 2026.08.08 takes the same libraries from `xim:graphics`: 22 packages plus two sentinels that probe for a host-side userspace half they do not own (the proprietary NVIDIA driver, WSL2's D3D12) and succeed having linked nothing when it is absent. One dependency covers every host shape and this file gained no conditional. The source is the SUBOS VIEW, not a payload directory. A payload path pins a version, so a consumer's RUNPATH would name mesa 25.0.7.1 forever and break the day it is upgraded; the view is the stable indirection, the role /run/opengl-driver plays on NixOS. The view also carries libc.so.6 and the rest of the C runtime, and those must never reach a consumer's RUNPATH -- the consumer runs under mcpp's payload loader, and a second libc paired with it faults inside the dynamic linker before main with no output. The pattern list is what keeps them out, so it is now a safety boundary with an assertion behind it rather than a convenience. That failure was reproduced on the way here, not imagined. libnvidia* leaves the pattern list: the proprietary driver reaches the subos through xim:nvidia-gl-host-link under the glvnd vendor names already matched, and taking it by its own name too would be a second route to one set of libraries. MCPP_HOST_GL_LIBRARY_PATH still works and is now the only door back to the host. It warns when used, because what it names was built against the host's glibc and loading it under the payload's is the #352 configuration exactly. Verified on an NVIDIA host, not reasoned about: a consumer whose RUNPATH is the generated directory resolves libEGL to it, and GL_RENDERER is "NVIDIA GeForce RTX 4080". Both halves are the criterion -- "a window appeared" is a false pass, since llvmpipe produces one too. The no-libc assertion was checked against the real view: 52 libraries linked, zero C-runtime entries. 2026.06.03 stays published so consumers already pinned to it keep resolving. compat.glfw, the only package that depends on this one, moves to the new pin. * fix(glx-runtime): deps belong beside the version entries, not inside one CI: `E_INTERNAL: [glx-runtime] failed:` with an empty message, twice. The `deps = { runtime = { "xim:graphics" } }` had been placed inside the 2026.08.08 version entry. The descriptor parsed, the graphics stack was never installed, the subos therefore had no libGL.so.1, and the install failed on the required-library check -- reporting the library rather than the misplaced key, which is why the message looked like a missing package instead of a typo. Every other recipe in both indexes puts deps at platform level. Moving it there means the legacy 2026.06.03 entry declares it too and pays for a download it does not read; the alternative is deleting a published version, which breaks anyone pinned to it outright.
1 parent eb8c338 commit f44e896

2 files changed

Lines changed: 102 additions & 79 deletions

File tree

pkgs/c/compat.glfw.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ package = {
8080
"src/posix_module.c",
8181
},
8282
deps = {
83-
["compat.glx-runtime"] = "2026.06.03",
83+
["compat.glx-runtime"] = "2026.08.08",
8484
["compat.x11"] = "1.8.13",
8585
["compat.xcursor"] = "1.2.3",
8686
["compat.xext"] = "1.3.7",

pkgs/c/compat.glx-runtime.lua

Lines changed: 101 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,59 @@ package = {
22
spec = "1",
33
namespace = "compat",
44
name = "glx-runtime",
5-
description = "Host GLVND/GLX/OpenGL runtime adapter for mcpp Linux window applications",
5+
description = "GLVND/GLX/OpenGL runtime for mcpp Linux window applications, from the xlings graphics stack",
66
licenses = {"MIT"},
77
repo = "https://github.com/KhronosGroup/OpenGL-Registry",
88
type = "package",
99

10+
-- WHERE THE GL RUNTIME COMES FROM, AND WHY IT CHANGED
11+
--
12+
-- Until 2026.08.08 this package symlinked the HOST's libGL/libEGL out of
13+
-- /usr/lib*. That is the thing mcpp#352 is: the host's Mesa needs
14+
-- GLIBC_2.43 and mcpp's payload glibc is 2.39, so the program linked
15+
-- cleanly and exited 255 with no output. It is also the boundary the
16+
-- xlings hermetic policy names first -- any .so under /usr/lib* or /lib*.
17+
--
18+
-- The runtime now comes from `xim:graphics`, the ecosystem's own stack:
19+
-- 22 packages plus two sentinels that probe for a host-side userspace half
20+
-- they do not own (the proprietary NVIDIA driver, WSL2's D3D12) and
21+
-- succeed having linked nothing when it is absent. One dependency, every
22+
-- host shape, no conditional in this file.
23+
--
24+
-- Measured on an NVIDIA host after the change: libEGL resolves to
25+
-- xim-x-libglvnd/1.7.0/lib/libEGL.so.1 and GL_RENDERER is the GPU, not
26+
-- llvmpipe. Both halves of that matter -- "a window appeared" is a false
27+
-- pass, because llvmpipe renders one too.
1028
xpm = {
1129
linux = {
30+
-- The whole hermetic graphics stack. A RUNTIME dep, not a build
31+
-- one: nothing here compiles against it, the produced consumer
32+
-- loads it.
33+
--
34+
-- PLATFORM level, beside the version entries rather than inside
35+
-- one. Every other recipe in both indexes places it here, and the
36+
-- first attempt at this change put it inside the 2026.08.08 entry:
37+
-- the descriptor parsed, the stack was never installed, and the
38+
-- install failed on the required-library check -- an error naming
39+
-- libGL.so.1 rather than the misplaced key. Whether a per-version
40+
-- `deps` is rejected or merely unread was not determined; what is
41+
-- established is that it does not take effect.
42+
--
43+
-- It therefore also applies to the legacy 2026.06.03 entry below,
44+
-- which does not use it. That costs a consumer still pinned there
45+
-- a download it will not read, and the alternative -- deleting the
46+
-- published version -- would break them outright.
47+
deps = { runtime = { "xim:graphics" } },
48+
["2026.08.08"] = {
49+
url = {
50+
GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc",
51+
CN = "https://gitcode.com/mcpp-res/glx-runtime/releases/download/2026.08.08/glx-runtime-2026.08.08.adoc",
52+
},
53+
sha256 = "ea68efce197e68413ebb62c51ab4bccfb2309a2fca776d31b49d972f59f3640e",
54+
},
55+
-- Kept so already-published consumers pinned to it keep resolving.
56+
-- It sources libGL from the HOST and is the configuration behind
57+
-- mcpp#352; new consumers must not pin it.
1258
["2026.06.03"] = {
1359
url = {
1460
GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc",
@@ -41,6 +87,7 @@ package = {
4187
}
4288

4389
import("xim.libxpkg.pkginfo")
90+
import("xim.libxpkg.system")
4491
import("xim.libxpkg.log")
4592

4693
local function sh_quote(value)
@@ -60,6 +107,26 @@ local function split_paths(value)
60107
return out
61108
end
62109

110+
-- Where to take the GL libraries from.
111+
--
112+
-- The SUBOS VIEW (`<subos>/lib`), not a payload directory. A payload path pins
113+
-- a version, so a consumer's recorded RUNPATH would name mesa 25.0.7.1 forever
114+
-- and stop resolving the day it is upgraded; the view is the stable
115+
-- indirection -- the role /run/opengl-driver plays on NixOS. xlings repoints
116+
-- it as the active version changes and this package needs no new release.
117+
--
118+
-- The view also carries libc.so.6, crt1.o and the rest of the C runtime, and
119+
-- those must NEVER reach a consumer's RUNPATH: the consumer runs under mcpp's
120+
-- payload loader, and pairing one loader with another glibc's libc.so.6 faults
121+
-- inside the dynamic linker before main, with empty output. What keeps them
122+
-- out is the pattern list below -- so that list is a safety boundary, not a
123+
-- convenience, and nothing resembling `libc*` may ever be added to it.
124+
--
125+
-- MCPP_HOST_GL_LIBRARY_PATH still works and is now the ONLY door back to the
126+
-- host. Using it leaves the hermetic guarantee: the libraries it names were
127+
-- built against the host's glibc, and loading them under mcpp's payload glibc
128+
-- is exactly the configuration mcpp#352 reports. It exists for a machine whose
129+
-- GPU vendor the ecosystem does not cover yet.
63130
local function candidate_dirs()
64131
local out = {}
65132
local seen = {}
@@ -71,13 +138,12 @@ local function candidate_dirs()
71138
end
72139

73140
for _, dir in ipairs(split_paths(os.getenv("MCPP_HOST_GL_LIBRARY_PATH"))) do
141+
log.warn("MCPP_HOST_GL_LIBRARY_PATH names %s: GL will come from the "
142+
.. "host, which is the configuration behind mcpp#352", dir)
74143
add(dir)
75144
end
76-
add("/lib/x86_64-linux-gnu")
77-
add("/usr/lib/x86_64-linux-gnu")
78-
add("/lib64")
79-
add("/usr/lib64")
80-
add("/usr/lib")
145+
146+
add(path.join(system.subos_sysrootdir(), "lib"))
81147
return out
82148
end
83149

@@ -90,7 +156,11 @@ local host_gl_patterns = {
90156
"libEGL.so*",
91157
"libEGL_*.so*",
92158
"libGLES*.so*",
93-
"libnvidia*.so*",
159+
-- No libnvidia* here. The proprietary driver reaches the subos through
160+
-- xim:nvidia-gl-host-link, which links it under the glvnd vendor names
161+
-- already matched above; taking it by its own name would be a second
162+
-- route to the same libraries, and the two would disagree the day the
163+
-- driver is upgraded under us.
94164
"libglapi.so*",
95165
"libdrm*.so*",
96166
"libexpat.so*",
@@ -104,90 +174,43 @@ local required = {
104174
["libGL.so.1"] = false,
105175
}
106176

107-
-- Is FILE a 64-bit ELF? e_ident[EI_CLASS] == ELFCLASS64.
108-
--
109-
-- Five bytes read directly. `file`/`readelf`/`patchelf` would each answer this
110-
-- and each may be absent when a hook runs, and a probe that answers "cannot
111-
-- tell" by assuming "fine" is the bug below.
112-
local function is_elf64(file)
113-
local f = io.open(file, "rb")
114-
if not f then return false end
115-
local head = f:read(5)
116-
f:close()
117-
return head ~= nil and #head == 5
118-
and head:sub(1, 4) == "\127ELF" and head:byte(5) == 2
119-
end
120-
121-
-- Link the host's GL runtime into one directory, FIRST HIT WINS, 64-bit only.
122-
--
123-
-- openxlings/xlings' mcpp#352: on Fedora 44 this produced
124-
-- libGLX.so.0 -> /usr/lib/libGLX.so.0
125-
-- a 32-bit library, and the application died with
126-
-- libGLX.so.0: wrong ELF class: ELFCLASS32
127-
--
128-
-- TWO BUGS, and the obvious diagnosis ("the candidate order assumes Debian") is
129-
-- not either of them -- `/usr/lib64` is already ahead of `/usr/lib` in the list:
130-
--
131-
-- 1. `ln -sf` OVERWRITES. The loop reached /usr/lib64 first and linked the
132-
-- correct file, then reached /usr/lib and replaced it. Last-wins, not
133-
-- first-wins. `libOpenGL.so.0` survived as 64-bit purely because that host's
134-
-- 32-bit glvnd does not ship it -- which is why exactly one link in the bug
135-
-- report was right.
136-
-- 2. NO ABI CHECK ANYWHERE, including in `required` below, which asserted that
137-
-- libGLX.so.0 and libGL.so.1 EXIST. Both existed. Both were 32-bit.
138-
--
139-
-- There is no directory layout to assume: the FHS biarch clause makes /usr/lib
140-
-- 32-bit (Fedora/RHEL/SUSE), Debian explicitly declined that clause and uses
141-
-- /usr/lib/<triplet> so its /usr/lib is 64-bit, and Arch is a third answer
142-
-- again. So the fix cannot be a better ordering -- it has to be an ABI check,
143-
-- which makes the order stop mattering.
144177
local function link_runtime_libs(outdir)
145178
os.mkdir(outdir)
146-
local claimed = {}
147179
for _, dir in ipairs(candidate_dirs()) do
148180
for _, pattern in ipairs(host_gl_patterns) do
149-
-- Enumerate, then decide per file, instead of letting the shell
150-
-- link them: the decision needs the ELF class and "have I already
151-
-- taken this name", neither of which a `ln -sf` loop can express.
152-
local pipe = io.popen("ls -1 " .. sh_quote(dir) .. "/" .. pattern
153-
.. " 2>/dev/null")
154-
if pipe then
155-
for line in pipe:lines() do
156-
local lib = line:gsub("[\r\n]+$", "")
157-
local name = lib:match("[^/]+$")
158-
if lib ~= "" and name and not claimed[name]
159-
and is_elf64(lib) then
160-
claimed[name] = lib
161-
os.exec("ln -sf " .. sh_quote(lib) .. " "
162-
.. sh_quote(path.join(outdir, name)))
163-
end
164-
end
165-
pipe:close()
166-
end
181+
os.exec(
182+
"for lib in " .. sh_quote(dir) .. "/" .. pattern ..
183+
"; do [ -e \"$lib\" ] || continue; " ..
184+
"ln -sf \"$lib\" " .. sh_quote(outdir) .. "/\"$(basename \"$lib\")\"; " ..
185+
"done"
186+
)
167187
end
168188
end
169189

170190
for name, _ in pairs(required) do
171-
local link = path.join(outdir, name)
172-
-- Existence AND ABI. Existence alone passed on the Fedora host with
173-
-- both links 32-bit, which is how a broken package reported success and
174-
-- the failure surfaced as a silent exit code 255 from the application.
175-
if not os.isfile(link) then
176-
log.error("required host GL runtime library not found: %s", name)
177-
log.error(" searched: %s", table.concat(candidate_dirs(), " "))
178-
log.error(" install your distro's GL runtime (mesa / libglvnd)")
191+
if not os.isfile(path.join(outdir, name)) then
192+
log.error("%s is not in this subos. The GL runtime comes from "
193+
.. "`xim:graphics`; if it is declared and this still "
194+
.. "fires, the stack did not finish installing", name)
179195
return false
180196
end
181-
if not is_elf64(link) then
182-
log.error("host %s is not 64-bit (%s)", name, claimed[name] or link)
183-
log.error(" a 32-bit library here fails at dlopen with")
184-
log.error(" `wrong ELF class: ELFCLASS32` and the application")
185-
log.error(" exits without output. Install the 64-bit GL runtime.")
197+
end
198+
199+
-- Nothing resembling a C runtime may have come along. Asserted rather
200+
-- than trusted: the pattern list is what keeps it out, and a pattern is
201+
-- one careless edit away from matching more than it meant to. The failure
202+
-- it prevents has no diagnostic of its own -- the consumer dies inside
203+
-- the dynamic linker before main, printing nothing.
204+
for _, bad in ipairs({"libc.so.6", "libc.so", "ld-linux-x86-64.so.2",
205+
"libpthread.so.0", "libdl.so.2", "libm.so.6"}) do
206+
if os.isfile(path.join(outdir, bad)) then
207+
log.error("%s was linked into the GL runtime directory. It would "
208+
.. "land on every consumer's RUNPATH and pair a second "
209+
.. "libc with mcpp's loader, which faults before main "
210+
.. "with no output at all", bad)
186211
return false
187212
end
188213
end
189-
log.info("glx-runtime: linked %d host GL libraries (64-bit)",
190-
(function() local n = 0 for _ in pairs(claimed) do n = n + 1 end return n end)())
191214
return true
192215
end
193216

0 commit comments

Comments
 (0)