Skip to content

Commit f431285

Browse files
committed
feat(example): the CUDA example takes its toolkit from a payload, not from the host
The example shipped in 2026.9.5.1 wrote `-L/usr/local/cuda/lib64` in its manifest and probed `/usr/local/cuda/bin/nvcc` and `/usr/include/crt/host_config.h` in its rule package. It was the project's own demonstration of how to do this, and what it demonstrated was reaching for the host. The project now names the toolkit: [xlings.workspace] "xim:cuda-nvcc" = "12.9.86" "xim:cuda-cudart" = "12.9.79" and the rule resolves it with `mcpp::xpkg_dir`, building the whole invocation from what it finds — compiler, include directories, and library search paths through `mcpp::link_search`. The manifest names libraries and no locations. Host paths remain in the rule as a last fallback so a machine with only a distribution toolkit still builds. ⚠️ THE PAYLOAD'S HEADERS HAVE TO BE NAMED. nvcc adds `<its own directory>/../include` by itself, and on the 12.x line that holds `crt/` but not `cuda_runtime.h` — which is in the `cuda-cudart` component. The first revision of this change left it out: nvcc resolved `cuda_runtime.h` from /usr/include and then read the HOST's `crt/host_config.h` beside it, and the build failed with the host toolkit's complaint (`clang version must be less than 15`) while using the payload's compiler. Naming the payload include directories is what makes "uses the payload" true rather than nearly true. Verified: `mcpp run` prints `12 24 36 48`, and $ mcpp build -v | grep -c '/usr/local/cuda\|/usr/bin/nvcc|-I/usr/include' 0 nvcc and both include directories resolve under `registry/data/xpkgs/xim-x-cuda-*`. 100 test binaries pass.
1 parent ad167f4 commit f431285

3 files changed

Lines changed: 112 additions & 10 deletions

File tree

examples/09-cuda-kernel/README.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,52 @@ fallback without any consumer of `app.saxpy` changing, and the one place a
3535
`cfg(accelerator = ...)` section has to apply. Remove it and every importer
3636
becomes backend-specific.
3737

38+
## Where the toolkit comes from
39+
40+
The project names it:
41+
42+
```toml
43+
[xlings.workspace]
44+
"xim:cuda-nvcc" = "12.9.86"
45+
"xim:cuda-cudart" = "12.9.79"
46+
```
47+
48+
These are payloads, so the version is the project's choice and not the machine's.
49+
The rule package resolves them with `mcpp::xpkg_dir` and builds the whole
50+
invocation from what it finds — the compiler, the include directories and the
51+
library search paths. **No path in this example is absolute**, and a build here
52+
touches nothing of the host's CUDA:
53+
54+
```
55+
$ mcpp build -v | grep -c '/usr/local/cuda\|/usr/bin/nvcc'
56+
0
57+
```
58+
59+
Host locations remain in the rule as a last fallback, so a machine that has only
60+
a distribution toolkit still builds. They are a fallback, not the design.
61+
62+
⚠️ **The payload's headers have to be named.** nvcc adds
63+
`<its own directory>/../include` by itself, and on the 12.x line that holds
64+
`crt/` but not `cuda_runtime.h` — which lives in the `cuda-cudart` component. An
65+
earlier revision of this rule left it out, and nvcc resolved `cuda_runtime.h`
66+
from `/usr/include` and then read the **host's** `crt/host_config.h` beside it.
67+
The build failed with the host toolkit's complaint while using the payload's
68+
compiler.
69+
3870
## The rule package, and why nvcc's host compiler is its problem
3971

4072
nvcc refuses host compilers newer than a bound it states in its own
4173
`crt/host_config.h`, and mcpp's toolchain payload is routinely newer than that
42-
bound. The rule reads the bound, selects a host compiler that satisfies it, and
43-
says which one it chose:
74+
bound. The rule reads the bound — from the payload, which states a newer one
75+
than a distribution toolkit does — selects a host compiler that satisfies it,
76+
and says which one it chose:
4477

4578
```
46-
example.rules.cuda: nvcc /usr/bin/nvcc with -ccbin /usr/bin/clang++-14
79+
example.rules.cuda: nvcc …/xpkgs/xim-x-cuda-nvcc/12.9.86/bin/nvcc with -ccbin
4780
```
4881

49-
On the machine this example was verified on, the toolkit is CUDA 12.0
50-
(`__GNUC__ > 12` is refused, clang must be below 15) and mcpp's payload is gcc
51-
16.1.0, so passing mcpp's own compiler through would fail. `mcpp self doctor`
52-
reports the same pairing independently.
82+
`mcpp self doctor` reports the same pairing independently, and reads the same
83+
payload.
5384

5485
Everything about nvcc's spelling lives in the rule package. The engine owns the
5586
graph, the artifact's identity and the architecture set; it does not own

examples/09-cuda-kernel/app/mcpp.toml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,19 @@ cuda-runtime = "2026.09.05"
2929
# mcpp a path it can put on the artifact's runtime search path — mcpp's private
3030
# loader does not consult /usr/lib, so without this the statically linked CUDA
3131
# runtime cannot dlopen the driver and reports it as missing.
32+
# The toolkit this project builds with, named rather than discovered.
33+
#
34+
# ⭐ These are PAYLOADS, so the version is the project's choice and not the
35+
# machine's. The 12.9 line is named on purpose: a runtime must not be newer than
36+
# the driver it will meet, and 12.x reaches every driver from r525 onward.
37+
# `mcpp self doctor` reports the pairing.
38+
#
39+
# The 12.x `cuda-nvcc` carries its own NVVM back end; on the 13.x line that is a
40+
# separate `libnvvm` alongside `cuda-crt`, and `cuda-nvcc`'s install hook brings
41+
# them. Either way the project names the compiler and gets a working one.
3242
[xlings.workspace]
43+
"xim:cuda-nvcc" = "12.9.86"
44+
"xim:cuda-cudart" = "12.9.79"
3345
"xim:libcuda-host-link" = { linux = "0.0.1" }
3446

3547
[build]
@@ -39,10 +51,14 @@ cuda-runtime = "2026.09.05"
3951
# redistributable half in leaves exactly one host dependency, libcuda.so.1,
4052
# which is the driver and genuinely cannot be redistributed — that is what the
4153
# libcuda-host-link sentinel package in xim exists for.
42-
accel = "cuda12.0+{sm_89} ptx>=89"
54+
accel = "cuda12.9+{sm_89} ptx>=89"
4355
include_dirs = ["include"]
44-
ldflags = ["-L/usr/local/cuda/lib64", "-L/usr/lib/x86_64-linux-gnu",
45-
"-lcudart_static", "-lrt", "-lpthread", "-ldl"]
56+
# ⭐ NO ABSOLUTE PATHS. The CUDA runtime comes from the `cuda-cudart` payload
57+
# named above, and the rule package puts its library directory on the link line
58+
# from `mcpp::xpkg_dir` -- so this manifest names libraries, never locations.
59+
# The one host component left is `libcuda.so.1`, which the driver owns and the
60+
# `compat.cuda-runtime` dependency reaches.
61+
ldflags = ["-lcudart_static", "-lrt", "-lpthread", "-ldl"]
4662

4763
[targets.cuda-saxpy]
4864
kind = "bin"

examples/09-cuda-kernel/rules-cuda/src/rules-cuda.cppm

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,29 @@ inline std::string first_existing(std::span<const std::string> candidates) {
5656
return {};
5757
}
5858

59+
// The toolkit directory this project declared, or empty.
60+
//
61+
// ⭐ PAYLOAD FIRST, AND THE PROJECT NAMES IT. `mcpp::xpkg_dir` answers for a
62+
// package the manifest declared under `[xlings.workspace]`, which is how a
63+
// build says which toolkit it wants instead of taking whichever one a machine
64+
// happens to have. The 13.x line splits the compiler across components, so the
65+
// pieces are looked up separately and joined here.
66+
inline std::vector<std::string> payload_roots() {
67+
std::vector<std::string> out;
68+
for (const char* name : { "cuda-nvcc", "cuda-crt", "cuda-cudart" })
69+
if (const char* d = mcpp::xpkg_dir("xim", name); d && *d)
70+
out.emplace_back(d);
71+
return out;
72+
}
73+
5974
inline std::string find_nvcc() {
6075
std::vector<std::string> c;
76+
for (auto const& r : payload_roots()) c.push_back(r + "/bin/nvcc");
77+
// ⚠️ HOST LOCATIONS ARE LAST AND ARE A FALLBACK, NOT THE DESIGN. A project
78+
// that declares the payload gets a toolkit whose version it chose and whose
79+
// host-compiler bound is far newer -- 12.9 accepts gcc 14 and 13.3 accepts
80+
// gcc 15, where a distribution's CUDA 12.0 stops at 12. These entries exist
81+
// so a machine that has only a distribution toolkit still builds.
6182
for (const char* var : { "CUDA_PATH", "CUDA_HOME" })
6283
if (const char* v = std::getenv(var)) c.push_back(std::string(v) + "/bin/nvcc");
6384
c.push_back("/usr/local/cuda/bin/nvcc");
@@ -67,6 +88,9 @@ inline std::string find_nvcc() {
6788

6889
inline std::string find_host_config(std::string_view nvcc) {
6990
std::vector<std::string> c;
91+
// 13.x moved this header into its own component, so the payload that has it
92+
// is not necessarily the one that has nvcc.
93+
for (auto const& r : payload_roots()) c.push_back(r + "/include/crt/host_config.h");
7094
if (!nvcc.empty()) {
7195
std::filesystem::path p{std::string(nvcc)};
7296
c.push_back((p.parent_path().parent_path() / "include/crt/host_config.h").string());
@@ -152,6 +176,24 @@ inline std::vector<edge> plan(std::span<const std::string> sources, options opt
152176
}
153177
std::println("example.rules.cuda: nvcc {} with -ccbin {}", nvcc, ccbin);
154178

179+
// ⭐ THE LINK LINE GETS ITS DIRECTORIES FROM HERE, NOT FROM THE MANIFEST.
180+
//
181+
// A manifest that writes `-L/usr/local/cuda/lib64` has decided where the
182+
// toolkit is, which is the machine's business and not the project's. The
183+
// rule knows: it just resolved the payload, and it puts that payload's
184+
// library directory on the link line. The manifest names libraries only.
185+
//
186+
// Emitted for every payload root, because the 13.x line splits the runtime
187+
// out of the compiler and a build may hold both.
188+
for (auto const& r : payload_roots()) {
189+
auto lib = r + "/lib";
190+
if (std::filesystem::is_directory(lib)) mcpp::link_search(lib.c_str());
191+
// Some components ship `lib64` instead; naming both costs nothing and
192+
// guessing wrong costs a link error that names a symbol.
193+
auto lib64 = r + "/lib64";
194+
if (std::filesystem::is_directory(lib64)) mcpp::link_search(lib64.c_str());
195+
}
196+
155197
for (auto const& src : sources) {
156198
const auto stem = std::filesystem::path(src).stem().string();
157199
const auto obj = opt.out_dir + "/" + stem + ".cu.o";
@@ -161,6 +203,19 @@ inline std::vector<edge> plan(std::span<const std::string> sources, options opt
161203
e.command = { nvcc, "-c", root + "/" + src, "-o", obj,
162204
"-ccbin", ccbin, "-std=c++17", "-O2",
163205
"--compiler-options", "-fPIC" };
206+
// ⚠️ THE PAYLOAD'S OWN HEADERS MUST BE NAMED, OR nvcc FINDS THE HOST'S.
207+
//
208+
// nvcc adds `<its own dir>/../include` automatically, and on the 12.x
209+
// line that directory holds `crt/` but NOT `cuda_runtime.h` -- that
210+
// lives in the `cuda-cudart` component. Without these flags nvcc
211+
// resolved `cuda_runtime.h` from /usr/include and then read the HOST's
212+
// `crt/host_config.h` beside it, which on this machine states a bound
213+
// three major versions older than the payload's. The build failed with
214+
// the host toolkit's complaint while using the payload's compiler.
215+
for (auto const& r : payload_roots()) {
216+
auto inc = r + "/include";
217+
if (std::filesystem::is_directory(inc)) e.command.push_back("-I" + inc);
218+
}
164219
for (auto const& inc : opt.includes)
165220
e.command.push_back("-I" + root + "/" + inc);
166221
for (auto const& a : opt.archs) {

0 commit comments

Comments
 (0)