Skip to content

Commit 4cd01a5

Browse files
feat(doctor): nvcc's unreachable back-end, and the post-release verification that found it (#560)
* docs(accel): record the post-release verification, and remove an emoji from a comment Section 14 was written when the engine-side change was merged but not yet released. Section 15 records what the released 2026.9.5.1 was measured to do: six criteria run against the published binary in a fresh subos, and a control establishing that the driver becomes reachable because of the index package rather than because of host leakage. It also records that the first run of those criteria was a false green. The script was correct; the invocation removed its first line, which was `set -euo pipefail`, in order to work around a quoting problem. Every assertion then became advisory and the closing banner printed regardless. The rule this yields is that assertions must not rest on `set -e` alone. The comment added to the runtime.artifacts whitelist carried a warning emoji. Emphasis is carried by a capitalised phrase instead. * feat(doctor): report when nvcc cannot reach its own back-end Verifying the release inside a sandbox found the CUDA example failing where it succeeds on the host. The difference is one line: nvcc runs cicc, cudafe++, ptxas and fatbinary as bare names on a PATH it prepends itself from an `nvcc.profile` beside its binary, Debian-family packaging makes that profile a symlink into /etc, and a sandbox that replaces /etc removes it. nvcc then keeps the ambient PATH and reports `sh: 1: cicc: not found`, which names neither nvcc nor the profile. Nothing about the toolkit is missing, so every obvious check passes. The plan is asked for rather than assumed. `nvcc --dryrun` prints the stages and the PATH nvcc will use without compiling anything; parse_dryrun reads that plan, and the doctor resolves each bare name against the path nvcc states, reporting the first that does not resolve. A dryrun that yields no plan -- there is no nvcc, or the output is not one -- produces no finding, on the same principle as the host-compiler bound: a probe that reaches no answer must not invent one. Measured on both sides, because a check that is always silent and a check that is always loud look alike from one run. With a working nvcc the warning does not appear; with nvcc copied to a directory holding no profile it appears and names `cicc` -- the same stage the real failure names, because Debian ships cudafe++, ptxas and fatbinary in /usr/bin and cicc only in the toolkit's own directory. No e2e accompanies this. It would need nvcc, which CI does not have, and a `# requires: nvcc` script skips and exits 0 on every shard. The coverage is five unit tests over two real dryrun transcripts differing in exactly that PATH line, plus the two-sided control recorded in the design's section 15.7. * docs(accel): note the new check in the stage table * docs(accel): why L1/L2 stops at one index package, measured --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 960f5b4 commit 4cd01a5

8 files changed

Lines changed: 480 additions & 3 deletions

File tree

.agents/docs/2026-09-05-accelerator-support-design.md

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,9 +1090,9 @@ accelerators = ["cuda", "rocm"] # [提议] 同一形状:支持面声明
10901090
| 3 `SourceKind::Device` || 单测 6 条;`.cuh` 进 header 轴 |
10911091
| 4 `accelerator` 多值 layer || 单测 7 条;语义按 §3.4 修正后的一行 |
10921092
| 5 逐 glob 收窄 | ❌ 未做 | 依赖 device target(阶段外),本轮 device 编译走规则包 |
1093-
| 6 `-ccbin` 配对 | ⚠️ 部分 | 引擎侧读上界并在 `mcpp self doctor` 报告;**钉定**发生在规则包里,不在引擎里 |
1093+
| 6 `-ccbin` 配对 | ⚠️ 部分 | 引擎侧读上界并在 `mcpp self doctor` 报告;**钉定**发生在规则包里,不在引擎里。PR #560 追加一条同族检查:nvcc 能否够到自己的后端(§15.7) |
10941094
| 7 device link | ❌ 未做 | 依赖 device target |
1095-
| L1/L2 索引包 | ⚠️ 部分 | `compat.cuda-runtime` 已合入并发布并沙箱验证;`rules-cuda` 目前是 `examples/` 里的 path 包,尚未收录进索引 |
1095+
| L1/L2 索引包 | ⚠️ 部分 | `compat.cuda-runtime` 已合入并发布并沙箱验证;`compat.cublas``rules-cuda` 各有一条**实测出来的阻碍**,见 §15.8 —— 缺的是授权不是实现 |
10961096
| L0 / L3 / 8 | ❌ 未做 | 见上 |
10971097

10981098
⚠️ **决定 3(逐 glob 收窄第一版就做)与决定 4(capability 边界先修)都没有落地。**
@@ -1213,3 +1213,155 @@ xim 的 CUDA 工具链载荷。每一项在 §12 都有独立判据。
12131213
⚠️ 仍未闭合的是另一件事:**引擎侧的新键要等 #559 发布之后,索引里的包才能使用**
12141214
`compat.cuda-runtime` 恰好一个新键都没用到,所以这一轮绕开了;
12151215
下一个用到 `accel` 字段的索引包不会这么幸运。
1216+
1217+
---
1218+
1219+
## 15. 发布后的验证(2026.9.5.1)
1220+
1221+
§14 写在合入之时,当时引擎侧的改动尚未发布。本节记发布之后**用发布物**跑完的
1222+
验证,以及它暴露的两件事。
1223+
1224+
### 15.1 发布物
1225+
1226+
`v2026.9.5.1`,GitHub 与 GitCode 两处资产齐备,sha256 逐个核对一致。
1227+
`ci-fresh-install` 在合入 commit `960f5b4e` 上覆盖 macOS(含 Homebrew)、
1228+
Windows(2022 / 2025 / 无 Visual Studio)与六个 Linux 发行版
1229+
(debian-11 / debian-testing / ubuntu-2004 / fedora / arch / tumbleweed)。
1230+
1231+
### 15.2 六项判据,全部用发布物,在新建的干净 subos 里
1232+
1233+
| # | 判据 | 结果 |
1234+
|---|---|---|
1235+
| A | 发布物自述版本 | `mcpp 2026.9.5.1` |
1236+
| B | CN mirror 可配置 | `Configured xlings mirror = CN` |
1237+
| C | `self doctor` 报出宿主/设备编译器配对 | `cuda will refuse this host compiler: gcc 13 exceeds the bound of 12 stated in /usr/include/crt/host_config.h` |
1238+
| D | `accel` 维拒绝不匹配的产物,并点名维度、两侧取值、该轴可行的补救 | 拒绝消息同时含 `accel``sm_90f``sm_86``--no-accel` |
1239+
| E | `--no-accel` 放行 | 通过 |
1240+
| F | 索引包 `compat.cuda-runtime` 让驱动可达,**无需任何环境变量** | `OK: driver reachable, cuInit=0x7a828447cbc0` |
1241+
1242+
### 15.3 ⭐ F 之后跑了一次对照
1243+
1244+
`dlopen` 成功既可能是包起了作用,也可能是宿主泄漏。去掉
1245+
`compat.cuda-runtime` 依赖、其余不变,得到
1246+
`CONTROL-OK: driver NOT reachable without the package`
1247+
**F 测的是包**。私有 loader 挡住了宿主的 `libcuda.so.1`,
1248+
只有该包的 `runtime.library_dirs` 让它可达 —— 这与 §14.2 的第一条互为正反面:
1249+
同一个机制既拒绝了我链宿主库,也让「声明了才可达」成为可核验的性质。
1250+
1251+
### 15.4 ⚠️⚠️ 第一次跑这套判据是假绿
1252+
1253+
把脚本塞进 `xlings subos use <name> --sandbox --cmd "$(...)"` 时遇到引号问题,
1254+
我用 `sed '1d'` 删掉了第一行 —— 而**第一行正是 `set -euo pipefail`**;
1255+
同时版本参数传成了空串。合起来:安装步骤什么都没装(跑的还是上一个发布版),
1256+
后面每一段失败都继续往下走,C/D/E 三段一个断言都没生效,
1257+
而脚本**照常打印出结尾的「全部完成」**
1258+
1259+
⭐ 两条修法,都要:**断言不能靠 `set -e` 独自承重**(每步显式 `|| fail "…"`),
1260+
**为传输问题改脚本时改的是引用方式,不是脚本内容**
1261+
重写后第一次运行立刻红在 A,这才引出下一节。
1262+
1263+
### 15.5 ⚠️ 沙箱里 PATH 上的 `mcpp` 仍解析到旧版
1264+
1265+
`xlings install mcpp@2026.9.5.1` 在沙箱里成功,但裸名 shim 仍指向 2026.9.4.3。
1266+
xlings 自己诚实报出了这一点(`xim/commands.cppm`):
1267+
1268+
```
1269+
xim:mcpp@2026.9.5.1 installed, but 'mcpp' still resolves to 2026.9.4.3
1270+
— `xlings use mcpp 2026.9.5.1` to switch
1271+
```
1272+
1273+
⭐ 因此**验证发布物一律直接用 store 路径**
1274+
`~/.xlings/data/xpkgs/xim-x-mcpp/<version>/bin/mcpp`,不要信 PATH。
1275+
这与 §14.4 记的 shim 剪除缺陷(openxlings/xlings#582)是同一个目录的两个问题:
1276+
**两个工具写同一个 shim 目录,而它的状态不是任何一方单独决定的。**
1277+
1278+
### 15.6 §14.6 末尾那条待办已解除
1279+
1280+
「引擎侧的新键要等 #559 发布之后,索引里的包才能使用」—— 2026.9.5.1 已发布,
1281+
`accel``[package] accelerators``cfg(accelerator=…)` 现在都可以出现在索引包里。
1282+
下一个用到它们的包需要在描述符里声明 mcpp 版本下限。
1283+
1284+
### 15.7 ⭐⭐ 又一条判据:从**发布的源码 tarball**跑示例,它红了
1285+
1286+
前六项用的是发布的引擎,但用的是工作树里的示例。补一条:
1287+
`mcpp-2026.9.5.1.tar.gz` 解出 `examples/09-cuda-kernel`,用发布的引擎构建。
1288+
**在沙箱里失败**:
1289+
1290+
```
1291+
sh: 1: cicc: not found
1292+
```
1293+
1294+
而同一个示例在宿主上输出 `12 24 36 48`。单变量隔离(同一条 nvcc 命令,只改一个东西):
1295+
1296+
| 环境 | 结果 |
1297+
|---|---|
1298+
| 沙箱默认 | `sh: 1: cicc: not found` |
1299+
| 沙箱 + `PATH=/usr/lib/nvidia-cuda-toolkit/bin:$PATH` | 产出 `.o` |
1300+
| 沙箱 + PATH + `NVVMIR_LIBRARY_DIR` | 产出 `.o` |
1301+
1302+
**唯一缺的是一条 PATH**。它来自 `/etc/nvcc.profile`,而 Debian 系把
1303+
`/usr/lib/nvidia-cuda-toolkit/bin/nvcc.profile` 做成指向 `/etc` 的符号链接 ——
1304+
subos 沙箱替换了 `/etc`,那条链接因此断开。
1305+
1306+
⚠️ **这不是 mcpp 的缺陷,但 mcpp 的诊断对它一言不发。** 工具包完整、nvcc 在
1307+
`PATH` 上、`crt/host_config.h` 读得到、`self doctor` 那一节照常打印,
1308+
**每一个显而易见的检查都通过**,而用户拿到的是一条既不提 nvcc 也不提 profile
1309+
的消息。这与 §5.2 的宿主编译器上界是同一个类别:失败很晚,消息指向一个用户
1310+
没有选择过的东西。
1311+
1312+
**已实现的检查**(`mcpp.toolchain.devicehost``parse_dryrun` + doctor 的
1313+
`unreachable_device_stage`):`nvcc --dryrun` 打印它将要运行的阶段与它将要使用的
1314+
`PATH` 而不编译任何东西;mcpp 解析这份计划,逐个解析其中的裸名,报出第一个解析
1315+
不到的。**判据取自 nvcc 自己,不是抄一张目录表** —— 与 §14.1 第 5 行读
1316+
`crt/host_config.h` 而非内置版本表是同一条原则。
1317+
1318+
**两侧对照都跑了**(否则这条判据可能永远沉默或永远报警):
1319+
1320+
| 对照 | `cannot reach its own back-end` |
1321+
|---|---|
1322+
| 真 nvcc(profile 可读) | 0 次 |
1323+
| 把 nvcc 复制到无 profile 的目录后放在 PATH 前 | 1 次,点名 `cicc` |
1324+
1325+
点名的是 `cicc` 而不是计划里更靠前的 `cudafe++`,因为 Debian 把
1326+
`cudafe++`/`ptxas`/`fatbinary` 装进了 `/usr/bin``cicc` 只在工具包目录里 ——
1327+
**与真实失败点名的那一个一致**
1328+
1329+
⚠️ **没有为它写 e2e。** 它需要 nvcc,而 CI 没有;一条 `# requires: nvcc`
1330+
e2e 会在两个 shard 上都跳过并退 0,那是一条永远不跑的判据(memory
1331+
`e2e-requires-llvm-never-runs-on-shards` 记的就是这个)。覆盖是:解析器的五条
1332+
单测(夹具是两份**真实**的 dryrun 输出,差别恰好是那一行 PATH),加上上表那次
1333+
手工两侧对照。
1334+
1335+
### 15.8 L1/L2 为什么停在一个包:两条实测出来的阻碍,都不是工作量
1336+
1337+
用户要求「补充**几个**流行的运行时/库/框架/SDK 到 mcpp index / xim 生态」。
1338+
本轮落地并验证的只有 `compat.cuda-runtime` 一个。停在这里的理由是测出来的:
1339+
1340+
**其一,`compat.cublas` 在索引现有的两种形态里都不成立。** 索引里的包分两类:
1341+
1342+
| 形态 | 例子 | 适用 |
1343+
|---|---|---|
1344+
| 从上游源码编译 | `compat.curl``compat.freetype``compat.ffmpeg` | 有源码 |
1345+
| farm 宿主库 + `runtime.library_dirs` + `dlopen_libs` | `compat.glx-runtime``compat.vulkan-runtime``compat.cuda-runtime` | 运行期 **dlopen** 的宿主驱动 |
1346+
1347+
cuBLAS 两者都不是:**没有源码**,而且是**必须链接**的闭源二进制。
1348+
⭐ 实测过链接路径确实可行 —— 闭包校验的拒绝
1349+
(*"Its PT_INTERP is a private loader"*)对**链接**的库与对 dlopen 的库一样发生,
1350+
而包自己声明 `runtime.library_dirs` 就能满足它。所以形态上做得出来。
1351+
⚠️ 但索引里 `ldflags` 的既有用法**全部**是系统级的
1352+
(`-pthread``-lrt``-ladvapi32`、macOS framework),**没有一个链接第三方宿主二进制库**
1353+
开这个先例会把一次构建钉死在某一台机器的 cuBLAS 版本上,与「少依赖 host」相反。
1354+
1355+
**正确形态是 xim 载荷**,携带 NVIDIA 的可再分发件。而那要回答 RK-3(体积)
1356+
与 RK-5(许可)两个问题,并向 GitCode 上传一份大体积的 NVIDIA 二进制 ——
1357+
**这是一个决定,不是一次实现**,不该由实施方单方面做。
1358+
1359+
**其二,`rules-cuda` 进索引要先有它自己的仓库。** 索引里已有的两个规则包
1360+
(`mcpplibs/clangtidy``grpcgen`)都是 `mcpp = "*/mcpp.toml"` 指向**独立仓库的
1361+
release tarball**,并各带一份 GitCode 镜像。把 `examples/09-cuda-kernel/rules-cuda`
1362+
搬进索引因此需要**新建一个公开仓库**并打 tag。
1363+
⚠️ 而且不能只做一半:索引里一份、示例里再留一份,就是第 §14.3 那个「重复了一处
1364+
本不该重复的东西」的形状 —— 要搬就要同时把示例改成消费索引里的那一个。
1365+
1366+
⇒ 两条都记在这里,而不是让 §12 的 L1/L2 行用「未做」含糊过去。
1367+
它们缺的是一个授权,不是一段实现。

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [Unreleased]
7+
8+
### `mcpp self doctor` 报出设备编译器够不到自己的后端
9+
10+
一个 CUDA 工具包可以安装完整、就在 `PATH` 上,而设备代码仍然在第一个阶段失败。
11+
nvcc 以裸名调用 `cicc``cudafe++``ptxas``fatbinary`,依赖的是它自己从紧邻
12+
其二进制的 `nvcc.profile` 前置进来的一条 `PATH`。Debian 系打包把那个 profile
13+
做成指向 `/etc` 的符号链接,于是任何替换了 `/etc` 的容器或沙箱都会移除它,
14+
nvcc 沿用环境原有的 `PATH` 并报出 `sh: 1: cicc: not found` —— 这条消息既不提
15+
nvcc 也不提 profile,而工具包一样不缺,所以显而易见的检查全部通过。
16+
17+
判据取自 nvcc 自己:`--dryrun` 打印它将要运行的各个阶段与它将要使用的 `PATH`,
18+
而不编译任何东西。mcpp 解析这份计划,逐个解析其中的裸名,报出第一个解析不到的。
19+
一次没有产生计划的 dryrun(没有 nvcc,或输出不是一份计划)不产生任何结论 ——
20+
与宿主编译器上界那条检查同一条原则:一个够不到答案的探测不应当发明一个。
21+
22+
这是本轮把发布物放进沙箱验证时暴露出来的:示例在宿主上跑通,在 `--sandbox`
23+
里失败,而唯一的差别就是那条 `PATH`
24+
625
## [2026.9.5.1] — 2026-09-05
726

827
### ⭐⭐ 加速器支持:设备编译单元、产物身份的加速器维、以及没人做的宿主编译器配对

docs/20-accelerators.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,35 @@ bound and therefore no claim.
8888
This is reported rather than enforced: a project that compiles no device code
8989
is unaffected by an incompatible pair.
9090

91+
## Whether the device compiler can reach its own back-end
92+
93+
A toolkit can be installed, complete and on `PATH` and still fail at its first
94+
stage. nvcc runs `cicc`, `cudafe++`, `ptxas` and `fatbinary` as bare names, on
95+
a `PATH` it prepends itself from an `nvcc.profile` beside its own binary. On
96+
Debian-family packaging that profile is a symlink into `/etc`, so a container
97+
or sandbox that replaces `/etc` removes it. nvcc then keeps the ambient `PATH`
98+
and reports:
99+
100+
```
101+
sh: 1: cicc: not found
102+
```
103+
104+
The message names neither nvcc nor the profile, and nothing about the toolkit
105+
is missing, so the obvious checks all pass. `mcpp self doctor` asks nvcc for
106+
its plan instead of assuming one:
107+
108+
```
109+
$ mcpp self doctor
110+
Checking device toolkit
111+
warning: nvcc cannot reach its own back-end: it invokes 'cicc' by name, and
112+
that name does not resolve on the search path it states.
113+
```
114+
115+
The plan comes from `nvcc --dryrun`, which prints the stages and the `PATH`
116+
nvcc will use without compiling anything. A dryrun that produces no plan --
117+
there is no nvcc, or the output is not one -- yields no finding, because a
118+
probe that reaches no answer must not invent one.
119+
91120
## Declaring what a build targets
92121

93122
```toml

docs/zh/20-accelerators.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,33 @@ warning: cuda will refuse this host compiler: gcc 13 exceeds the bound of 12
6969

7070
这是报告而非强制:一个不编译任何设备代码的工程,不受不兼容配对的影响。
7171

72+
## 设备编译器能否够到自己的后端
73+
74+
一个工具包可以安装完整、就在 `PATH` 上,却仍然在第一个阶段失败。
75+
nvcc 以裸名调用 `cicc``cudafe++``ptxas``fatbinary`,依赖的是它自己
76+
从紧邻其二进制的 `nvcc.profile` 前置进来的一条 `PATH`。在 Debian 系的打包里,
77+
那个 profile 是指向 `/etc` 的符号链接,于是任何替换了 `/etc` 的容器或沙箱都会移除它。
78+
nvcc 随即沿用环境里原有的 `PATH`,并报出:
79+
80+
```
81+
sh: 1: cicc: not found
82+
```
83+
84+
这条消息既没有提到 nvcc,也没有提到 profile,而工具包本身一样不缺,
85+
于是所有显而易见的检查都会通过。`mcpp self doctor` 因此去问 nvcc 要它的计划,
86+
而不是假设一份:
87+
88+
```
89+
$ mcpp self doctor
90+
Checking device toolkit
91+
warning: nvcc cannot reach its own back-end: it invokes 'cicc' by name, and
92+
that name does not resolve on the search path it states.
93+
```
94+
95+
计划来自 `nvcc --dryrun` —— 它打印各个阶段与 nvcc 将要使用的 `PATH`,
96+
而不编译任何东西。一次没有产生计划的 dryrun(没有 nvcc,或输出不是一份计划)
97+
不产生任何结论:一个够不到答案的探测不应当发明一个。
98+
7299
## 声明一次构建的目标
73100

74101
```toml

modules/manifest/src/toml.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1908,7 +1908,7 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
19081908
std::string path;
19091909
auto const& table = value.as_table();
19101910
for (auto const& [key, _] : table) {
1911-
// ⚠️ A CLOSED whitelist: an unrecognised key is an error here,
1911+
// THIS IS A CLOSED WHITELIST: an unrecognised key is an error here,
19121912
// unlike the published xpkg descriptor, whose reader collects
19131913
// and skips one. The two readers differ on purpose — a typo in
19141914
// a hand-written manifest should be loud — but it means adding

src/doctor.cppm

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,64 @@ export int env_report() {
102102
}
103103

104104
// `mcpp self doctor`.
105+
// Which back-end stage nvcc names but cannot resolve, if any.
106+
//
107+
// Nothing is compiled: `--dryrun` prints the plan and stops. std::nullopt
108+
// covers three unlike situations on purpose -- there is no nvcc, the dryrun
109+
// produced no plan, and every stage in the plan resolves -- because only a
110+
// named unresolvable stage is a finding. A probe that cannot reach an answer
111+
// must not manufacture one.
112+
std::optional<std::string> unreachable_device_stage() {
113+
namespace fs = std::filesystem;
114+
std::error_code ec;
115+
116+
const auto probe = fs::temp_directory_path(ec) / "mcpp-nvcc-dryrun";
117+
if (ec) return std::nullopt;
118+
fs::remove_all(probe, ec);
119+
ec.clear();
120+
fs::create_directories(probe, ec);
121+
if (ec) return std::nullopt;
122+
struct Cleanup {
123+
fs::path dir;
124+
~Cleanup() { std::error_code e; fs::remove_all(dir, e); }
125+
} const cleanup{probe};
126+
127+
const auto source = probe / "empty.cu";
128+
{ std::ofstream out(source); if (!out) return std::nullopt; }
129+
130+
// A spawn that fails because there is no nvcc yields text with no `#$`
131+
// lines, hence an empty plan, hence no finding. No separate check needed.
132+
const auto run = mcpp::platform::process::capture_exec(
133+
{"nvcc", "--dryrun", "-c", source.string(),
134+
"-o", (probe / "empty.o").string()});
135+
136+
const auto plan = mcpp::toolchain::parse_dryrun(run.output);
137+
if (plan.programs.empty()) return std::nullopt;
138+
139+
std::string search = plan.searchPath;
140+
if (search.empty())
141+
if (const char* p = std::getenv("PATH"); p) search = p;
142+
if (search.empty()) return std::nullopt;
143+
144+
std::vector<fs::path> dirs;
145+
for (std::size_t pos = 0; pos <= search.size(); ) {
146+
const auto sep = search.find(':', pos);
147+
const auto stop = sep == std::string::npos ? search.size() : sep;
148+
if (stop > pos) dirs.emplace_back(search.substr(pos, stop - pos));
149+
pos = stop + 1;
150+
}
151+
152+
for (auto const& program : plan.programs) {
153+
bool found = false;
154+
for (auto const& dir : dirs) {
155+
if (fs::exists(dir / program, ec)) { found = true; break; }
156+
ec.clear();
157+
}
158+
if (!found) return program;
159+
}
160+
return std::nullopt;
161+
}
162+
105163
export int doctor_report() {
106164
int warns = 0, errors = 0;
107165
auto ok = [](std::string_view m) { mcpp::ui::status("ok", m); };
@@ -650,6 +708,33 @@ export int doctor_report() {
650708
header->string()));
651709
}
652710
}
711+
712+
// WHETHER nvcc CAN REACH ITS OWN BACK-END
713+
//
714+
// A toolkit can be present, complete and on PATH and still fail
715+
// at the first stage, because nvcc resolves cicc, ptxas and
716+
// fatbinary as bare names on a PATH it prepends from an
717+
// `nvcc.profile` beside its binary. A container or sandbox that
718+
// replaces /etc removes that profile -- it is a symlink into it
719+
// on Debian-family packaging -- and nvcc then states no PATH and
720+
// reports `sh: 1: cicc: not found`. The message names neither
721+
// nvcc nor the profile, and nothing about the toolkit is missing,
722+
// so the user has nowhere to look.
723+
//
724+
// Asked rather than assumed: `--dryrun` prints the plan without
725+
// running it, so the answer is nvcc's own.
726+
if (auto missing = unreachable_device_stage(); missing) {
727+
warn(std::format(
728+
"nvcc cannot reach its own back-end: it invokes '{}' by "
729+
"name, and that name does not resolve on the search path "
730+
"it states.\n"
731+
" The toolkit is installed; what is missing is the "
732+
"`nvcc.profile` that prepends the toolkit's own bin "
733+
"directory. This is what a container or sandbox that "
734+
"replaces /etc removes. Device code will fail to compile "
735+
"with a message naming only '{}'.",
736+
*missing, *missing));
737+
}
653738
}
654739
}
655740

0 commit comments

Comments
 (0)