Skip to content

Commit 1fe2ab8

Browse files
committed
feat(compat): cuda-runtime — reach the host NVIDIA driver from an mcpp binary
An mcpp-built program runs under mcpp's own glibc, so a bare-soname dlopen from inside it does not search the host's library path at all. A program that links the CUDA runtime statically therefore carries every redistributable component and still cannot start: the runtime cannot dlopen libcuda.so.1 and reports it as an insufficient driver version, which is a confusing way to say not found. This is the same problem compat.glx-runtime and compat.vulkan-runtime solve, and it takes the same shape: a package-owned directory placed on the artifact's runtime search path through runtime.library_dirs. Nothing is vendored. The driver's userspace library is in ABI lockstep with the kernel module and NVIDIA's licence forbids redistributing it, so it can only ever be a host capability. The host probe is NOT repeated here. xim's libcuda-host-link already owns the question of where the host's libcuda is, and its own recipe states why that has to live in one place: xim's hostlib module documents four copies of that probe, three of which were wrong, each making the same reasonable-looking assumption about a directory layout that FHS, Debian multiarch and Arch answer differently. This package declares an install-time edge to the sentinel and links through it. The edge is xpm.<platform>.deps rather than the package's own [xlings], because mcpp materialises [xlings] deps for the root project only and this has to resolve when the package itself installs. Only libcuda.so.1 is linked, and that is measured rather than minimal. A draft also harvested libnvidia-ptxjitcompiler on the theory that PTX JIT would otherwise fail. Measured on driver 550.144.03: a binary built for compute_80 alone, run with only that one symlink reachable, JITs and produces the correct result on an sm_89 device. The driver loads its own siblings through its own paths, which the private loader does not interfere with. The versioned soname is deliberate and the unversioned libcuda.so is deliberately absent: mcpp puts runtime.library_dirs on the link line as well as the runtime path, so a bare libcuda.so here would be picked up by -lcuda and bind a build to one machine's driver. The test example asserts what holds on a machine with a driver and on one without, since every runner here is the latter. It does not require a device: linking at all is the real assertion, because the failure this package prevents is a link-time one. The adapter is Linux-only: the problem it solves is that an mcpp binary on Linux runs under mcpp's own loader and therefore cannot see the host's driver. The test member conditions its dependency on the platform rather than being excluded from the workspace, so the source still compiles on macOS and Windows and cannot rot unnoticed on the two platforms that do not exercise it.
1 parent 1cf5768 commit 1fe2ab8

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ members = [
3333
"tests/examples/concurrentqueue",
3434
"tests/examples/concurrentqueue-c-api",
3535
"tests/examples/core",
36+
"tests/examples/cuda-runtime",
3637
"tests/examples/curl",
3738
"tests/examples/eigen",
3839
"tests/examples/eui-neo",

pkgs/c/compat.cuda-runtime.lua

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
-- compat.cuda-runtime — put the host NVIDIA driver on an mcpp binary's
2+
-- runtime search path.
3+
--
4+
-- WHAT IT FIXES. An mcpp-built program runs under mcpp's OWN glibc
5+
--
6+
-- interp: .../xpkgs/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2
7+
-- rpath : .../xim-x-glibc/2.44/lib64:.../xim-x-gcc/16.1.0/lib64:...
8+
--
9+
-- so a bare-soname dlopen from inside it does not search the host's library
10+
-- path at all. A program that links the CUDA runtime statically therefore
11+
-- carries every redistributable component and still cannot start: the runtime
12+
-- cannot dlopen libcuda.so.1 and reports
13+
--
14+
-- cudaMalloc: CUDA driver version is insufficient for CUDA runtime version
15+
--
16+
-- which is a confusing way to say "not found". `runtime.library_dirs` below
17+
-- puts a package-owned directory on that path, the same mechanism
18+
-- compat.glx-runtime and compat.vulkan-runtime use for the same reason.
19+
--
20+
-- ⭐ THE PROBE IS NOT REPEATED HERE. xim's `libcuda-host-link` already owns the
21+
-- question "where is the host's libcuda", and its own recipe states why that
22+
-- must live in one place:
23+
--
24+
-- Single source of truth for "where is host libcuda" -> all GPU xpkgs read
25+
-- from pkginfo.dep_install_dir("libcuda-host-link").."/lib/libcuda.so.1" and
26+
-- don't reimplement ldconfig probing each.
27+
--
28+
-- An earlier draft of this package re-probed the host with its own candidate
29+
-- directory list, which is exactly the drift that rule exists to prevent: xim's
30+
-- hostlib module documents four such copies, three of which were wrong, and
31+
-- each was the same reasonable-looking mistake of assuming a directory layout
32+
-- that FHS, Debian multiarch and Arch each answer differently.
33+
--
34+
-- So the edge is declared instead. `xpm.<platform>.deps` rather than the
35+
-- package's own `[xlings]`, because mcpp materialises `[xlings] deps` for the
36+
-- ROOT project only and this must resolve when the package itself installs.
37+
--
38+
-- ⭐ ONLY libcuda.so.1 IS LINKED, and that is a measured claim rather than a
39+
-- minimal-effort one. A draft also harvested libnvidia-ptxjitcompiler on the
40+
-- theory that PTX JIT would otherwise fail. Measured on a machine with driver
41+
-- 550.144.03: a binary built for `compute_80` alone, run with only this one
42+
-- symlink reachable, JITs and produces the right answer on an sm_89 device.
43+
-- The driver loads its own siblings through its own paths, which the private
44+
-- loader does not interfere with. The extra patterns were unnecessary.
45+
--
46+
-- NOTHING IS REQUIRED. A machine with no NVIDIA driver is a legitimate
47+
-- configuration -- every runner in this repository is one. The sentinel's
48+
-- symlink is then dangling, the farm links a dead entry, and a program that
49+
-- needs a device reports that itself.
50+
package = {
51+
spec = "1",
52+
namespace = "compat",
53+
name = "cuda-runtime",
54+
description = "Host NVIDIA driver runtime adapter for mcpp Linux applications",
55+
licenses = {"Apache-2.0"}, -- the recipe; libcuda.so.1 itself is NVIDIA's
56+
repo = "https://github.com/openxlings/xim-pkgindex",
57+
type = "package",
58+
59+
xpm = {
60+
linux = {
61+
-- The install-time edge. Materialised when THIS package installs,
62+
-- which is what makes the sentinel's directory exist by the time
63+
-- install() below reads it.
64+
deps = { "xim:libcuda-host-link@0.0.1" },
65+
["2026.09.05"] = {
66+
-- Nothing downloaded matters: the content is the symlink this
67+
-- install() creates. A stable, tiny anchor keeps the xpm entry
68+
-- well-formed, the same trick compat.vulkan-runtime uses.
69+
url = "https://raw.githubusercontent.com/NVIDIA/cuda-samples/v12.5/LICENSE",
70+
sha256 = "b3e40c5bfed1fca5c62d2c1f2208bf51f8d2c910219f94c443f657ace9001be3",
71+
},
72+
["latest"] = { ref = "2026.09.05" },
73+
},
74+
},
75+
76+
mcpp = {
77+
language = "c++23",
78+
import_std = false,
79+
c_standard = "c11",
80+
sources = { "mcpp_generated/cuda_runtime_empty.c" },
81+
targets = { ["cuda_runtime"] = { kind = "lib" } },
82+
deps = {},
83+
runtime = {
84+
library_dirs = { "mcpp_generated/cuda_runtime/lib" },
85+
capabilities = { "cuda.driver" },
86+
provides = { "cuda.driver" },
87+
},
88+
},
89+
}
90+
91+
import("xim.libxpkg.pkginfo")
92+
import("xim.libxpkg.log")
93+
94+
-- The sentinel's install directory.
95+
--
96+
-- `pkginfo.install_dir` scans only the member-local xpkgs roots; a dependency
97+
-- installed into the shared registry cache is invisible to it and comes back
98+
-- nil, so the known roots are tried before giving up. This is the same fallback
99+
-- compat.mysql-connector-cpp needs for the same reason.
100+
local function sentinel_dir()
101+
local dir = pkginfo.install_dir("xim:libcuda-host-link", "0.0.1")
102+
if dir then return dir end
103+
local roots = {}
104+
local pfx = pkginfo.install_dir()
105+
if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end
106+
local home = (os.getenv and os.getenv("XLINGS_HOME")) or ""
107+
if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.xlings" end
108+
roots[#roots + 1] = path.join(home, "data/xpkgs")
109+
for _, root in ipairs(roots) do
110+
local cand = path.join(root, "xim-x-libcuda-host-link", "0.0.1")
111+
if os.isdir(cand) then return cand end
112+
end
113+
return nil
114+
end
115+
116+
function install()
117+
os.tryrm(pkginfo.install_dir())
118+
os.mkdir(pkginfo.install_dir())
119+
120+
local generated = path.join(pkginfo.install_dir(), "mcpp_generated")
121+
os.mkdir(generated)
122+
io.writefile(path.join(generated, "cuda_runtime_empty.c"),
123+
"int mcpp_compat_cuda_runtime_anchor(void) { return 0; }\n")
124+
125+
local outdir = path.join(generated, "cuda_runtime", "lib")
126+
os.mkdir(outdir)
127+
128+
local src = sentinel_dir()
129+
if not src then
130+
-- Reported, not fatal. The farm is empty, the link still succeeds, and
131+
-- a program that needs a device says so itself -- which is the same
132+
-- answer a machine with no driver gives.
133+
log.warn("compat.cuda-runtime: libcuda-host-link not found; "
134+
.. "the runtime library directory will be empty")
135+
return true
136+
end
137+
138+
-- Only the versioned soname. mcpp puts runtime.library_dirs on the LINK
139+
-- line as well as the runtime path, so an unversioned libcuda.so here would
140+
-- be picked up by -lcuda and bind a build to one machine's driver. A
141+
-- versioned soname is invisible to the linker and is exactly what dlopen
142+
-- asks for.
143+
os.exec("ln -sf " .. path.join(src, "lib", "libcuda.so.1") .. " "
144+
.. path.join(outdir, "libcuda.so.1"))
145+
return true
146+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# compat.cuda-runtime is a Linux-only adapter: the problem it solves is that an
2+
# mcpp binary on Linux runs under mcpp's own loader and therefore cannot see the
3+
# host's driver. macOS and Windows have neither that loader arrangement nor an
4+
# NVIDIA userspace driver in this shape, so the dependency is conditioned rather
5+
# than the member being excluded — the member still builds on all three, which
6+
# is what keeps the test source itself from rotting.
7+
[package]
8+
name = "cuda-runtime-tests"
9+
version = "0.1.0"
10+
11+
[target.'cfg(linux)'.dependencies.compat]
12+
cuda-runtime = "2026.09.05"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// What compat.cuda-runtime is asserted to do, and what it is not.
2+
//
3+
// A machine with no NVIDIA driver is a legitimate configuration and is what
4+
// every runner in this repository is, so the test cannot require a device. It
5+
// asserts the two properties that hold on both kinds of machine:
6+
//
7+
// 1. The package resolves, builds and links. That alone covers the failure
8+
// this package exists to prevent, because the failure is a LINK-time one:
9+
// an unversioned libcuda.so harvested into a directory mcpp puts on the
10+
// link line would be picked up by -lcuda and bind the build to one
11+
// machine's driver. The patterns are versioned precisely so that cannot
12+
// happen, and a build that links proves it did not.
13+
//
14+
// 2. Where a driver is present, the farm reaches it. Guarded on the driver
15+
// actually being there rather than skipped by a marker, so the assertion
16+
// is real on a machine with a GPU and vacuous on one without, and neither
17+
// case is reported as a pass of the other.
18+
#include <cstdio>
19+
20+
// The adapter under test exists only on Linux, so the assertion does too. The
21+
// file still compiles everywhere, which is what keeps it from rotting silently
22+
// on the two platforms that do not exercise it.
23+
#ifndef __linux__
24+
int main() {
25+
std::printf("not applicable on this platform\n");
26+
return 0;
27+
}
28+
#else
29+
#include <dlfcn.h>
30+
31+
int main() {
32+
// The driver's userspace library, by soname. The farm's whole job is to
33+
// make this resolve from inside mcpp's own loader, which does not search
34+
// the host's library path.
35+
void* h = dlopen("libcuda.so.1", RTLD_LAZY);
36+
if (!h) {
37+
// No driver on this machine. The farm is empty, which is correct.
38+
std::printf("no host driver: %s\n", dlerror());
39+
return 0;
40+
}
41+
// Present: then the symbol every CUDA runtime looks for must be there too.
42+
// A farm that linked a stale or wrong-class file would resolve the library
43+
// and fail here, which is the difference between "found something" and
44+
// "found the driver".
45+
void* sym = dlsym(h, "cuInit");
46+
std::printf("host driver present, cuInit=%p\n", sym);
47+
dlclose(h);
48+
return sym ? 0 : 1;
49+
}
50+
#endif

0 commit comments

Comments
 (0)