Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ members = [
"tests/examples/concurrentqueue",
"tests/examples/concurrentqueue-c-api",
"tests/examples/core",
"tests/examples/cuda-runtime",
"tests/examples/curl",
"tests/examples/eigen",
"tests/examples/eui-neo",
Expand Down
146 changes: 146 additions & 0 deletions pkgs/c/compat.cuda-runtime.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
-- compat.cuda-runtime — put the host NVIDIA driver on an mcpp binary's
-- runtime search path.
--
-- WHAT IT FIXES. An mcpp-built program runs under mcpp's OWN glibc
--
-- interp: .../xpkgs/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2
-- rpath : .../xim-x-glibc/2.44/lib64:.../xim-x-gcc/16.1.0/lib64:...
--
-- 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
--
-- cudaMalloc: CUDA driver version is insufficient for CUDA runtime version
--
-- which is a confusing way to say "not found". `runtime.library_dirs` below
-- puts a package-owned directory on that path, the same mechanism
-- compat.glx-runtime and compat.vulkan-runtime use for the same reason.
--
-- ⭐ THE PROBE IS NOT REPEATED HERE. xim's `libcuda-host-link` already owns the
-- question "where is the host's libcuda", and its own recipe states why that
-- must live in one place:
--
-- Single source of truth for "where is host libcuda" -> all GPU xpkgs read
-- from pkginfo.dep_install_dir("libcuda-host-link").."/lib/libcuda.so.1" and
-- don't reimplement ldconfig probing each.
--
-- An earlier draft of this package re-probed the host with its own candidate
-- directory list, which is exactly the drift that rule exists to prevent: xim's
-- hostlib module documents four such copies, three of which were wrong, and
-- each was the same reasonable-looking mistake of assuming a directory layout
-- that FHS, Debian multiarch and Arch each answer differently.
--
-- So the edge is declared instead. `xpm.<platform>.deps` rather than the
-- package's own `[xlings]`, because mcpp materialises `[xlings] deps` for the
-- ROOT project only and this must resolve when the package itself installs.
--
-- ⭐ ONLY libcuda.so.1 IS LINKED, and that is a measured claim rather than a
-- minimal-effort one. A draft also harvested libnvidia-ptxjitcompiler on the
-- theory that PTX JIT would otherwise fail. Measured on a machine with driver
-- 550.144.03: a binary built for `compute_80` alone, run with only this one
-- symlink reachable, JITs and produces the right answer on an sm_89 device.
-- The driver loads its own siblings through its own paths, which the private
-- loader does not interfere with. The extra patterns were unnecessary.
--
-- NOTHING IS REQUIRED. A machine with no NVIDIA driver is a legitimate
-- configuration -- every runner in this repository is one. The sentinel's
-- symlink is then dangling, the farm links a dead entry, and a program that
-- needs a device reports that itself.
package = {
spec = "1",
namespace = "compat",
name = "cuda-runtime",
description = "Host NVIDIA driver runtime adapter for mcpp Linux applications",
licenses = {"Apache-2.0"}, -- the recipe; libcuda.so.1 itself is NVIDIA's
repo = "https://github.com/openxlings/xim-pkgindex",
type = "package",

xpm = {
linux = {
-- The install-time edge. Materialised when THIS package installs,
-- which is what makes the sentinel's directory exist by the time
-- install() below reads it.
deps = { "xim:libcuda-host-link@0.0.1" },
["2026.09.05"] = {
-- Nothing downloaded matters: the content is the symlink this
-- install() creates. A stable, tiny anchor keeps the xpm entry
-- well-formed, the same trick compat.vulkan-runtime uses.
url = "https://raw.githubusercontent.com/NVIDIA/cuda-samples/v12.5/LICENSE",
sha256 = "b3e40c5bfed1fca5c62d2c1f2208bf51f8d2c910219f94c443f657ace9001be3",
},
["latest"] = { ref = "2026.09.05" },
},
},

mcpp = {
language = "c++23",
import_std = false,
c_standard = "c11",
sources = { "mcpp_generated/cuda_runtime_empty.c" },
targets = { ["cuda_runtime"] = { kind = "lib" } },
deps = {},
runtime = {
library_dirs = { "mcpp_generated/cuda_runtime/lib" },
capabilities = { "cuda.driver" },
provides = { "cuda.driver" },
},
},
}

import("xim.libxpkg.pkginfo")
import("xim.libxpkg.log")

-- The sentinel's install directory.
--
-- `pkginfo.install_dir` scans only the member-local xpkgs roots; a dependency
-- installed into the shared registry cache is invisible to it and comes back
-- nil, so the known roots are tried before giving up. This is the same fallback
-- compat.mysql-connector-cpp needs for the same reason.
local function sentinel_dir()
local dir = pkginfo.install_dir("xim:libcuda-host-link", "0.0.1")
if dir then return dir end
local roots = {}
local pfx = pkginfo.install_dir()
if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end
local home = (os.getenv and os.getenv("XLINGS_HOME")) or ""
if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.xlings" end
roots[#roots + 1] = path.join(home, "data/xpkgs")
for _, root in ipairs(roots) do
local cand = path.join(root, "xim-x-libcuda-host-link", "0.0.1")
if os.isdir(cand) then return cand end
end
return nil
end

function install()
os.tryrm(pkginfo.install_dir())
os.mkdir(pkginfo.install_dir())

local generated = path.join(pkginfo.install_dir(), "mcpp_generated")
os.mkdir(generated)
io.writefile(path.join(generated, "cuda_runtime_empty.c"),
"int mcpp_compat_cuda_runtime_anchor(void) { return 0; }\n")

local outdir = path.join(generated, "cuda_runtime", "lib")
os.mkdir(outdir)

local src = sentinel_dir()
if not src then
-- Reported, not fatal. The farm is empty, the link still succeeds, and
-- a program that needs a device says so itself -- which is the same
-- answer a machine with no driver gives.
log.warn("compat.cuda-runtime: libcuda-host-link not found; "
.. "the runtime library directory will be empty")
return true
end

-- Only the versioned soname. mcpp puts runtime.library_dirs on the LINK
-- line as well as the runtime path, so an unversioned libcuda.so here would
-- be picked up by -lcuda and bind a build to one machine's driver. A
-- versioned soname is invisible to the linker and is exactly what dlopen
-- asks for.
os.exec("ln -sf " .. path.join(src, "lib", "libcuda.so.1") .. " "
.. path.join(outdir, "libcuda.so.1"))
return true
end
12 changes: 12 additions & 0 deletions tests/examples/cuda-runtime/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# compat.cuda-runtime is a Linux-only adapter: 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. macOS and Windows have neither that loader arrangement nor an
# NVIDIA userspace driver in this shape, so the dependency is conditioned rather
# than the member being excluded — the member still builds on all three, which
# is what keeps the test source itself from rotting.
[package]
name = "cuda-runtime-tests"
version = "0.1.0"

[target.'cfg(linux)'.dependencies.compat]
cuda-runtime = "2026.09.05"
50 changes: 50 additions & 0 deletions tests/examples/cuda-runtime/tests/farm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// What compat.cuda-runtime is asserted to do, and what it is not.
//
// A machine with no NVIDIA driver is a legitimate configuration and is what
// every runner in this repository is, so the test cannot require a device. It
// asserts the two properties that hold on both kinds of machine:
//
// 1. The package resolves, builds and links. That alone covers the failure
// this package exists to prevent, because the failure is a LINK-time one:
// an unversioned libcuda.so harvested into a directory mcpp puts on the
// link line would be picked up by -lcuda and bind the build to one
// machine's driver. The patterns are versioned precisely so that cannot
// happen, and a build that links proves it did not.
//
// 2. Where a driver is present, the farm reaches it. Guarded on the driver
// actually being there rather than skipped by a marker, so the assertion
// is real on a machine with a GPU and vacuous on one without, and neither
// case is reported as a pass of the other.
#include <cstdio>

// The adapter under test exists only on Linux, so the assertion does too. The
// file still compiles everywhere, which is what keeps it from rotting silently
// on the two platforms that do not exercise it.
#ifndef __linux__
int main() {
std::printf("not applicable on this platform\n");
return 0;
}
#else
#include <dlfcn.h>

int main() {
// The driver's userspace library, by soname. The farm's whole job is to
// make this resolve from inside mcpp's own loader, which does not search
// the host's library path.
void* h = dlopen("libcuda.so.1", RTLD_LAZY);
if (!h) {
// No driver on this machine. The farm is empty, which is correct.
std::printf("no host driver: %s\n", dlerror());
return 0;
}
// Present: then the symbol every CUDA runtime looks for must be there too.
// A farm that linked a stale or wrong-class file would resolve the library
// and fail here, which is the difference between "found something" and
// "found the driver".
void* sym = dlsym(h, "cuInit");
std::printf("host driver present, cuInit=%p\n", sym);
dlclose(h);
return sym ? 0 : 1;
}
#endif
Loading