diff --git a/pkgs/c/compat.glfw.lua b/pkgs/c/compat.glfw.lua index e4ef66f..a4454b0 100644 --- a/pkgs/c/compat.glfw.lua +++ b/pkgs/c/compat.glfw.lua @@ -80,7 +80,7 @@ package = { "src/posix_module.c", }, deps = { - ["compat.glx-runtime"] = "2026.06.03", + ["compat.glx-runtime"] = "2026.08.08", ["compat.x11"] = "1.8.13", ["compat.xcursor"] = "1.2.3", ["compat.xext"] = "1.3.7", diff --git a/pkgs/c/compat.glx-runtime.lua b/pkgs/c/compat.glx-runtime.lua index 8cc15d1..9db30d4 100644 --- a/pkgs/c/compat.glx-runtime.lua +++ b/pkgs/c/compat.glx-runtime.lua @@ -2,13 +2,59 @@ package = { spec = "1", namespace = "compat", name = "glx-runtime", - description = "Host GLVND/GLX/OpenGL runtime adapter for mcpp Linux window applications", + description = "GLVND/GLX/OpenGL runtime for mcpp Linux window applications, from the xlings graphics stack", licenses = {"MIT"}, repo = "https://github.com/KhronosGroup/OpenGL-Registry", type = "package", + -- WHERE THE GL RUNTIME COMES FROM, AND WHY IT CHANGED + -- + -- Until 2026.08.08 this package symlinked the HOST's libGL/libEGL out of + -- /usr/lib*. That is the thing mcpp#352 is: the host's Mesa needs + -- GLIBC_2.43 and mcpp's payload glibc is 2.39, so the program linked + -- cleanly and exited 255 with no output. It is also the boundary the + -- xlings hermetic policy names first -- any .so under /usr/lib* or /lib*. + -- + -- The runtime now comes from `xim:graphics`, the ecosystem's own stack: + -- 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, every + -- host shape, no conditional in this file. + -- + -- Measured on an NVIDIA host after the change: libEGL resolves to + -- xim-x-libglvnd/1.7.0/lib/libEGL.so.1 and GL_RENDERER is the GPU, not + -- llvmpipe. Both halves of that matter -- "a window appeared" is a false + -- pass, because llvmpipe renders one too. xpm = { linux = { + -- The whole hermetic graphics stack. A RUNTIME dep, not a build + -- one: nothing here compiles against it, the produced consumer + -- loads it. + -- + -- PLATFORM level, beside the version entries rather than inside + -- one. Every other recipe in both indexes places it here, and the + -- first attempt at this change put it inside the 2026.08.08 entry: + -- the descriptor parsed, the stack was never installed, and the + -- install failed on the required-library check -- an error naming + -- libGL.so.1 rather than the misplaced key. Whether a per-version + -- `deps` is rejected or merely unread was not determined; what is + -- established is that it does not take effect. + -- + -- It therefore also applies to the legacy 2026.06.03 entry below, + -- which does not use it. That costs a consumer still pinned there + -- a download it will not read, and the alternative -- deleting the + -- published version -- would break them outright. + deps = { runtime = { "xim:graphics" } }, + ["2026.08.08"] = { + url = { + GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc", + CN = "https://gitcode.com/mcpp-res/glx-runtime/releases/download/2026.08.08/glx-runtime-2026.08.08.adoc", + }, + sha256 = "ea68efce197e68413ebb62c51ab4bccfb2309a2fca776d31b49d972f59f3640e", + }, + -- Kept so already-published consumers pinned to it keep resolving. + -- It sources libGL from the HOST and is the configuration behind + -- mcpp#352; new consumers must not pin it. ["2026.06.03"] = { url = { GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc", @@ -41,6 +87,7 @@ package = { } import("xim.libxpkg.pkginfo") +import("xim.libxpkg.system") import("xim.libxpkg.log") local function sh_quote(value) @@ -60,6 +107,26 @@ local function split_paths(value) return out end +-- Where to take the GL libraries from. +-- +-- The SUBOS VIEW (`/lib`), not a payload directory. A payload path pins +-- a version, so a consumer's recorded RUNPATH would name mesa 25.0.7.1 forever +-- and stop resolving the day it is upgraded; the view is the stable +-- indirection -- the role /run/opengl-driver plays on NixOS. xlings repoints +-- it as the active version changes and this package needs no new release. +-- +-- The view also carries libc.so.6, crt1.o 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 pairing one loader with another glibc's libc.so.6 faults +-- inside the dynamic linker before main, with empty output. What keeps them +-- out is the pattern list below -- so that list is a safety boundary, not a +-- convenience, and nothing resembling `libc*` may ever be added to it. +-- +-- MCPP_HOST_GL_LIBRARY_PATH still works and is now the ONLY door back to the +-- host. Using it leaves the hermetic guarantee: the libraries it names were +-- built against the host's glibc, and loading them under mcpp's payload glibc +-- is exactly the configuration mcpp#352 reports. It exists for a machine whose +-- GPU vendor the ecosystem does not cover yet. local function candidate_dirs() local out = {} local seen = {} @@ -71,13 +138,12 @@ local function candidate_dirs() end for _, dir in ipairs(split_paths(os.getenv("MCPP_HOST_GL_LIBRARY_PATH"))) do + log.warn("MCPP_HOST_GL_LIBRARY_PATH names %s: GL will come from the " + .. "host, which is the configuration behind mcpp#352", dir) add(dir) end - add("/lib/x86_64-linux-gnu") - add("/usr/lib/x86_64-linux-gnu") - add("/lib64") - add("/usr/lib64") - add("/usr/lib") + + add(path.join(system.subos_sysrootdir(), "lib")) return out end @@ -90,7 +156,11 @@ local host_gl_patterns = { "libEGL.so*", "libEGL_*.so*", "libGLES*.so*", - "libnvidia*.so*", + -- No libnvidia* here. The proprietary driver reaches the subos through + -- xim:nvidia-gl-host-link, which links it under the glvnd vendor names + -- already matched above; taking it by its own name would be a second + -- route to the same libraries, and the two would disagree the day the + -- driver is upgraded under us. "libglapi.so*", "libdrm*.so*", "libexpat.so*", @@ -104,90 +174,43 @@ local required = { ["libGL.so.1"] = false, } --- Is FILE a 64-bit ELF? e_ident[EI_CLASS] == ELFCLASS64. --- --- Five bytes read directly. `file`/`readelf`/`patchelf` would each answer this --- and each may be absent when a hook runs, and a probe that answers "cannot --- tell" by assuming "fine" is the bug below. -local function is_elf64(file) - local f = io.open(file, "rb") - if not f then return false end - local head = f:read(5) - f:close() - return head ~= nil and #head == 5 - and head:sub(1, 4) == "\127ELF" and head:byte(5) == 2 -end - --- Link the host's GL runtime into one directory, FIRST HIT WINS, 64-bit only. --- --- openxlings/xlings' mcpp#352: on Fedora 44 this produced --- libGLX.so.0 -> /usr/lib/libGLX.so.0 --- a 32-bit library, and the application died with --- libGLX.so.0: wrong ELF class: ELFCLASS32 --- --- TWO BUGS, and the obvious diagnosis ("the candidate order assumes Debian") is --- not either of them -- `/usr/lib64` is already ahead of `/usr/lib` in the list: --- --- 1. `ln -sf` OVERWRITES. The loop reached /usr/lib64 first and linked the --- correct file, then reached /usr/lib and replaced it. Last-wins, not --- first-wins. `libOpenGL.so.0` survived as 64-bit purely because that host's --- 32-bit glvnd does not ship it -- which is why exactly one link in the bug --- report was right. --- 2. NO ABI CHECK ANYWHERE, including in `required` below, which asserted that --- libGLX.so.0 and libGL.so.1 EXIST. Both existed. Both were 32-bit. --- --- There is no directory layout to assume: the FHS biarch clause makes /usr/lib --- 32-bit (Fedora/RHEL/SUSE), Debian explicitly declined that clause and uses --- /usr/lib/ so its /usr/lib is 64-bit, and Arch is a third answer --- again. So the fix cannot be a better ordering -- it has to be an ABI check, --- which makes the order stop mattering. local function link_runtime_libs(outdir) os.mkdir(outdir) - local claimed = {} for _, dir in ipairs(candidate_dirs()) do for _, pattern in ipairs(host_gl_patterns) do - -- Enumerate, then decide per file, instead of letting the shell - -- link them: the decision needs the ELF class and "have I already - -- taken this name", neither of which a `ln -sf` loop can express. - local pipe = io.popen("ls -1 " .. sh_quote(dir) .. "/" .. pattern - .. " 2>/dev/null") - if pipe then - for line in pipe:lines() do - local lib = line:gsub("[\r\n]+$", "") - local name = lib:match("[^/]+$") - if lib ~= "" and name and not claimed[name] - and is_elf64(lib) then - claimed[name] = lib - os.exec("ln -sf " .. sh_quote(lib) .. " " - .. sh_quote(path.join(outdir, name))) - end - end - pipe:close() - end + os.exec( + "for lib in " .. sh_quote(dir) .. "/" .. pattern .. + "; do [ -e \"$lib\" ] || continue; " .. + "ln -sf \"$lib\" " .. sh_quote(outdir) .. "/\"$(basename \"$lib\")\"; " .. + "done" + ) end end for name, _ in pairs(required) do - local link = path.join(outdir, name) - -- Existence AND ABI. Existence alone passed on the Fedora host with - -- both links 32-bit, which is how a broken package reported success and - -- the failure surfaced as a silent exit code 255 from the application. - if not os.isfile(link) then - log.error("required host GL runtime library not found: %s", name) - log.error(" searched: %s", table.concat(candidate_dirs(), " ")) - log.error(" install your distro's GL runtime (mesa / libglvnd)") + if not os.isfile(path.join(outdir, name)) then + log.error("%s is not in this subos. The GL runtime comes from " + .. "`xim:graphics`; if it is declared and this still " + .. "fires, the stack did not finish installing", name) return false end - if not is_elf64(link) then - log.error("host %s is not 64-bit (%s)", name, claimed[name] or link) - log.error(" a 32-bit library here fails at dlopen with") - log.error(" `wrong ELF class: ELFCLASS32` and the application") - log.error(" exits without output. Install the 64-bit GL runtime.") + end + + -- Nothing resembling a C runtime may have come along. Asserted rather + -- than trusted: the pattern list is what keeps it out, and a pattern is + -- one careless edit away from matching more than it meant to. The failure + -- it prevents has no diagnostic of its own -- the consumer dies inside + -- the dynamic linker before main, printing nothing. + for _, bad in ipairs({"libc.so.6", "libc.so", "ld-linux-x86-64.so.2", + "libpthread.so.0", "libdl.so.2", "libm.so.6"}) do + if os.isfile(path.join(outdir, bad)) then + log.error("%s was linked into the GL runtime directory. It would " + .. "land on every consumer's RUNPATH and pair a second " + .. "libc with mcpp's loader, which faults before main " + .. "with no output at all", bad) return false end end - log.info("glx-runtime: linked %d host GL libraries (64-bit)", - (function() local n = 0 for _ in pairs(claimed) do n = n + 1 end return n end)()) return true end