diff --git a/.agents/docs/2026-08-10-graphics-closure-acceptance.md b/.agents/docs/2026-08-10-graphics-closure-acceptance.md new file mode 100644 index 00000000..9613bf64 --- /dev/null +++ b/.agents/docs/2026-08-10-graphics-closure-acceptance.md @@ -0,0 +1,307 @@ +# 验收记录:图形栈闭合与分发档位(2026.8.10.2) + +> 日期:2026-08-10 +> 被测:`feat/graphics-closure-and-distribution-tiers`(PR #408) +> 本机:x86_64-linux-gnu / gcc 16.1.0 / NVIDIA RTX 4080 / 550.144.03 / X11 `:1` +> 设计:`2026-08-10-graphics-closure-and-distribution-tiers-design.md` +> +> **本文只写实测。** 每一条结论都带命令与输出;凡未实测的一律标 **未验**, +> 凡本机环境导致的一律与代码结论分开。 + +--- + +## 0. 一句话 + +**mcpp 那一半全部验到了;GPU 那一半在本机验不了 —— 而"验不了"和"验过是好的"必须分开写, +这正是本轮到处在建的那条判据。** + +--- + +## 1. #405 —— 在真实 imgui 模板上端到端验过 + +不是构造的 fixture,是 issue 里那个模板本身。它生成的 `main.cpp` 只有: + +```cpp +import imgui.core; +import imgui.app; +``` + +**自己不 `import std`** —— 这正是复现所需的形状。 + +```console +$ rm -rf ~/.mcpp/build-cache/v1/pkg/mcpplibs/imgui@* # 强制第一次 MISS +$ mcpp new a --template imgui:window && (cd a && mcpp build) + Compiling imgui v0.0.6 + Finished dev [unoptimized + debuginfo] in 1.15s ← 缓存 MISS,一直是好的 + +$ mcpp new b --template imgui:window && (cd b && mcpp build) + Cached imgui v0.0.6 (9 units) ← 缓存 HIT + Finished dev [unoptimized + debuginfo] in 0.78s ← 修复前这里必挂 +``` + +修复前的输出(同一形状,e2e `212` 的 RED 实录): + +``` +std: error: failed to read compiled module: No such file or directory +std: note: compiled module file is 'gcm.cache/std.gcm' +mcpplibs.cmdline: error: failed to read compiled module: Bad import dependency +``` + +**判据达成。** 顺带,这条也复核了 issue 里「第一个人好、后面都坏」的形状: +`a` 与 `b` 是同一个 mcpp、同一个 home、同一分钟内建的,与版本无关。 + +--- + +## 2. 加载器标签 —— 在真实图形产物上验过 + +`b` 的产物,原生解析动态段: + +``` +form= executable tag= RPATH +``` + +`resolution.json` 的 `loader_tags`(rule E 的记录,共 13 条): + +| 对象 | form | required | actual | status | +|---|---|---|---|---| +| `bin/b` | executable | DT_RPATH | **DT_RPATH** | ok | +| `bin/libX11.so` | shared_library | DT_RUNPATH | DT_RUNPATH | ok | +| `bin/libXcursor.so` … `bin/libxcb.so`(共 12 个) | shared_library | DT_RUNPATH | DT_RUNPATH | ok | + +**一分为二的两侧都验到了**:可执行 RPATH、12 个库全部 RUNPATH,零 violation。 + +产物的 DT_RPATH 内容,四条全部存在(逐条核过): + +``` +/xim-x-glibc/2.39/lib64 +/xim-x-gcc/16.1.0/lib64 +/compat-x-glx-runtime/2026.08.08/mcpp_generated/glx_runtime/lib ← 52 个文件 +$ORIGIN +``` + +> ⚠️ **一次我自己的测量错误,记在这里。** 我一度报告第三条"指向一个不存在的版本" +> ——那是 `find … | head -1` 先返回了同目录下的 `2026.06.03` 造成的。 +> `2026.08.08` 存在且完整。**`head -1` 不是判据。** + +--- + +## 3. GPU 那一半:本机 **NOT_EXERCISED**,不是 PASS 也不是 FAIL + +程序能起来,窗口创建失败: + +```console +$ ./b +imgui.app: window creation failed: GLFW error 65545 (GLX: Failed to find a suitable GLXFBConfig) +``` + +**宿主 GL 本身是好的** —— 所以这不是"这台机器没有显卡": + +```console +$ DISPLAY=:1 glxinfo -B +direct rendering: Yes +OpenGL vendor string: NVIDIA Corporation +OpenGL renderer string: NVIDIA GeForce RTX 4080/PCIe/SSE2 +OpenGL core profile version string: 4.6.0 NVIDIA 550.144.03 +``` + +派发与 vendor 也都在载荷里(`libGLX_nvidia.so.0 → 550.144.03`,与宿主驱动同版本)。 + +**但这台机器的载荷是被污染的**(见 §5),而 `/lib` 里一个 GL 都没有、 +`.wiring` 记录不存在 —— 也就是说**这个 home 从来没有被接线过**。 + +> **所以本机的诚实判决是 `NOT_EXERCISED`。** +> mcpp 侧的三条职责(标签对、路径通且存在、不打包不该打包的)全部验到; +> 「桥有没有搭上宿主驱动」需要一台接过线的机器,本轮**未验**。 +> 这正是设计 §6.3 写的:那部分是 `xlings doctor` 的事,mcpp 该报 `NOT_EXERCISED`。 + +`mcpp why runtime` 的实际输出(逐字),把 L3 的缺口直接摆出来: + +``` +requirements: + - capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required) + - capability:opengl.glx.driver [run] <- compat.glx-runtime@2026.08.08 (required) + … +providers: + - opengl.glx.driver -> compat.glx-runtime@2026.08.08 [index+compat@2026.08.08] + - x11.display -> compat.glx-runtime@2026.08.08 [index+compat@2026.08.08] +artifacts: + (not declared by the environment — nothing to verify) + note: a resolved provider with no artifact is UNVERIFIED, + not verified-good +validation: pass (source post_link) + - bin/b: pass ← 13 个对象逐条 pass(rule E + 闭包) + … +provider and host-service re-diagnostics are owned by xlings; run `xlings doctor` +``` + +**一个 provider 按名字解析成功、身后一个物都没有** —— 这就是设计里那句 +「provider 有名无物,所以没有任何东西可以校验」的现场。 +身份判决因此无事可做,而它**说出来了**,没有伪装成 `(none declared)` +那种可以被读成"没问题"的措辞。 + +--- + +## 4. 分发档位 + +| 档 | 判据 | 结果 | +|---|---|---| +| `vendored` | bundle 内**每个** ELF 无构建机 store 路径;可执行 RPATH、库 RUNPATH | ✅ e2e `215` | +| `self-contained` | 有 run 期能力需求时 plan 期硬拒并给出 `vendored` 出路 | ✅ e2e `216` | +| `static` | 同上 | ✅ e2e `216` | +| `self-contained`(无能力需求) | 仍然可打包、`run.sh` 可运行 | ✅ e2e `30` | + +`215` 的 RED 实录(撤掉 F2 之后),正是设计里描述的那条: + +``` +lib/libgcc_s.so.1 shared_library RUNPATH + /xim-x-glibc/2.39/lib : /xim-x-gcc/16.1.0/lib64 +FAIL: bundled object still points at the BUILD MACHINE's store +``` + +**一个真回归,由 `30_pack_modes` 抓到并已修**:第一版 F2 把 **动态加载器本身** +也 patchelf 了。它不是被搜索的库,它是执行搜索的程序 —— 改它让 `self-contained` 档 +在 `main` 之前段错误。修法是把加载器排除在重写之外。 + +--- + +## 5. 本机环境的三个缺陷(与本 PR 无关,但解释了本地 e2e 的红) + +本地 e2e:**183 通过 / 25 失败 / 8 跳过**。 +**25 条里 24 条用已发布的 `2026.8.8.2` 逐条复现** —— 同一个根因,三种表现: + +### 5.1 共享 gcc 载荷的 specs 被历史安装污染 + +``` +--dynamic-linker → /xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2 ← 该目录已被改名为 2.44.aside +rpath → 约 40 条 /tmp/tmp.XXXXXX/mcpphome/... (全部来自已删除的 e2e 沙箱) +``` + +后果:每一个 `build.mcpp` helper 的 PT_INTERP 指向不存在的加载器, +`posix_spawnp` 返回 **ENOENT**,报成 `exited with 127`。 +这打掉了 112 / 124 / 125 / 179 / 181 / 186–194 全部。 + +> **这是设计 §3.1「specs 是共享可变状态,不能承载契约」的现场证据**, +> 也正是 `mcpp-clean-link.specs` 存在的原因 —— mcpp 自己的构建因此不受影响, +> 而**独立编译的 `build.mcpp` helper 走不到那条防线**。这条值得单独跟。 + +### 5.2 `-print-search-dirs` 指向另一个工程的 subos + +``` +libraries: … /home/speak/workspace/github/openxlings/xim-pkgindex-fromsource/.xlings/subos/default/lib/ … +``` + +与本工程毫无关系。同一族。 + +### 5.3 xim binutils 的 shim 指向已删除的会话目录 + +``` +[error] xlings: executable 'as' not found +[error] path: /tmp/claude-1000/…/accept-run1/home/.mcpp/registry/data/xpkgs/xim-x-binutils/2.42/bin +``` + +与记忆里 #293 同族。**因此本轮所有 ELF 判据都用原生解析,不 shell out** —— +一个坏掉的 `readelf` 会让标签断言静默空转。 + +**CI 是这部分的真判据**(干净机器,四平台)—— 结果回填: + +``` +e2e 1/2 (linux x86_64): 95 passed, 0 failed, 13 skipped +e2e 2/2 (linux x86_64): 97 passed, 0 failed, 11 skipped +``` + +**192 通过、0 失败**,并且五条新用例逐条确认真的跑了(不是被 `# requires:` 跳过): + +``` +PASS: 212_cached_dep_std_is_ordered.sh +PASS: 214_executable_carries_dt_rpath.sh +PASS: 216_selfcontained_refuses_host_capability.sh ← shard 1 +PASS: 213_build_after_test_is_not_the_test_graph.sh +PASS: 215_pack_has_no_build_machine_paths.sh ← shard 2 +``` + +> **这一条特意查了**:`# requires:` 里一个不认识的 token 会让用例**从不运行** +> 而不报错(记忆里 `65_*` 就这样从未在 CI 跑过)。所以不是看总数, +> 是看这五个名字逐个出现在 `PASS:` 行上。 + +18 项 PR 检查全绿,含 macOS 与 Windows —— 也就是说加载器契约没有扰动非 ELF 平台。 + +--- + +## 6. 未验 / 明确不做 + +| 项 | 状态 | +|---|---| +| 图形程序真正拿到 GPU | **未验**,需要一台接过线的机器 | +| pack 产物在没有 xlings 的机器上运行 | **未验**,需要第二台机器;e2e 只能验到「产物里没有构建机路径」 | +| rule E 转硬门禁 | **不做**,结论只来自一台 NVIDIA/X11/x86_64 机器(与 xlings E5 同一笔欠账) | +| `.wiring` 读取 | **不做**,主判据已改为 mcpp 自算;见设计 §2.1 | + +--- + +--- + +## 7. 生态验收(用**发布出去的**那份,不是本地构建) + +发布之后按生态路径重做了一遍 —— 因为「本地构建能跑」和「用户装到的能跑」是两个断言。 + +```console +$ xlings update && xlings install mcpp@2026.8.10.2 -y + ✓ xim:mcpp@2026.8.10.2 done + xim:mcpp@2026.8.10.2 installed, but 'mcpp' still resolves to 2026.8.8.2 +$ xlings use mcpp 2026.8.10.2 +[xlings] mcpp -> 2026.8.10.2 (xim:mcpp 2026.8.8.2 -> 2026.8.10.2) +$ mcpp --version +mcpp 2026.8.10.2 +``` + +> ⚠️ `install` **不会**自动切换已装的旧版本,它会明说。只看 `install` 的成功输出 +> 就以为切过去了,是这一步最容易的误读。 + +用这个生态装出来的 mcpp 重跑图形验收: + +| 项 | 结果 | +|---|---| +| imgui 模板 A(缓存 MISS) | ✅ | +| imgui 模板 B(缓存 **HIT** —— #405 的那一格) | ✅ `Cached imgui v0.0.6 (9 units)` | +| 产物标签 | ✅ 可执行 `DT_RPATH`,11 个库全 `DT_RUNPATH`,rule E 零 violation | +| `artifacts` | `[]` —— 环境未声明,报 `NOT_DECLARED`(见 §3) | + +### 顺带验到 2026.8.10.2 的一个不足(已由 2026.8.10.3 修) + +同一个 imgui 工程,`mcpp pack --mode self-contained`: + +```console +# 生态里的 2026.8.10.2 + Packed b-0.1.0-x86_64-linux-gnu-bundle-all.tar.gz ← 照打不误,也没有 HOST-REQUIREMENTS + +# 带 .3 修复的构建 +error: --mode self-contained cannot be used by a program that needs the host to + provide abi:glibc, opengl.glx.driver, x11.display. + use: --mode vendored — … +``` + +原因:`.2` 的那道门读的是**根 manifest**,而这条能力来自依赖 +(`capability:opengl.glx.driver <- compat.glfw@3.4`)。**真实工程恰恰是这个形态。** +`216` 的 fixture 当时在根工程声明能力,所以它通过的理由比它声称的范围窄 —— +这一轮反复写的那条判据,这次落在自己头上。已改成依赖声明 + 前置断言。 + +### 身份判决的渲染也验了(环境没声明,就自己造一个) + +符号链接仍指向 `0.1.1`、声明 `xim:vendor@0.1.2`: + +``` +artifacts: + - library …/current/lib/libvendor.so <- … [xim:vendor@0.1.2; identity=mismatch] + ^ STALE BINDING: this resolves into a different version than the one declared. +``` + +正是设计里描述的那一格,而且是纯路径事实 —— 不需要认识 GL。 + +## 附:今天重复了三次的同一条 + +> **要说"验过了",先说清楚验的是哪一片。** + +- e2e 25 红里 24 红是环境的 —— 不逐条对照已发布二进制,就会把它们当成回归, + 或者更糟,当成"本来就红"而放过其中真的那一条(`30_pack_modes`)。 +- `find | head -1` 让我报了一个不存在的缺陷。 +- 宿主 `glxinfo` 好、程序拿不到 context —— 只报前者是撒谎,只报后者也是。 diff --git a/.agents/docs/2026-08-10-graphics-closure-and-distribution-tiers-design.md b/.agents/docs/2026-08-10-graphics-closure-and-distribution-tiers-design.md index 53719ffa..e80d5040 100644 --- a/.agents/docs/2026-08-10-graphics-closure-and-distribution-tiers-design.md +++ b/.agents/docs/2026-08-10-graphics-closure-and-distribution-tiers-design.md @@ -8,6 +8,16 @@ > (三层故障的分层)、xlings `2026-08-10-graphics-stack-design.md`(标签契约与 E1–E5) > > 本文所有"已验"结论都在本机跑过,命令与输出在正文里。凡未实测的一律标 **未验**。 +> +> **实施状态(2026-08-10):A–J 全部已实施,PR #408,版本 `2026.8.10.2`。** +> 实施计划见 `2026-08-10-graphics-closure-implementation-plan.md`, +> 验收实测见 `2026-08-10-graphics-closure-acceptance.md`。 +> 与本文的两处出入,以实施为准: +> - **§2.1 的 `.wiring` 增强没有做**。主判据(mcpp 自算路径身份)已实施并可独立成立; +> 读别人一份条件语义未表达的记录属于净增耦合,收益不足以抵。 +> - **§3.F 的 `discovery` 不由 mcpp 推断**。第一版按能力名推断,被既有守卫 +> `test_runtime_contract` 当场拦下 —— 那是把 provider 专属知识写进 mcpp。 +> 改为声明式(`[[runtime.requirements]] discovery`),未声明报 `unknown`。**守卫是对的。** --- diff --git a/CHANGELOG.md b/CHANGELOG.md index d59e0d27..e54ed5bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,29 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.8.10.3] — 2026-08-10 + +### 修复 + +- **宿主能力清单从「解析后的图」取,不再从根 manifest 取。** `2026.8.10.2` 引入的 + 「自带 libc 的档拒绝宿主能力」只在**根工程自己声明**能力时生效 —— 而几乎没有应用 + 会自己声明 `capability:opengl.glx.driver`,它依赖某个声明了的包(glfw / SDL 封装 / + GL runtime),resolver 会给每条需求盖上请求者身份。读根 manifest 回答的是 + 「作者写没写」(几乎总是没写),而该问的是「解析出来的图需不需要」。 + + 实测:一个真实 imgui 工程的 `mcpp why runtime` 列着 + `capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)`, + 而 `mcpp pack --mode self-contained` **照打不误**。修复后它正确拒绝,并列出 + `abi:glibc, opengl.glx.driver, x11.display` 三条;`--mode vendored` 正常打包 + 并把三条写进 `HOST-REQUIREMENTS`。 + + 同一处也修好了 `mcpp pack` 的 `HOST-REQUIREMENTS`:此前对真实工程是空的 + (空文件会被读成「什么都不需要」,而它现在根本不写空文件)。 + + **为什么原来的测试没抓到:它的 fixture 在根工程声明了能力 —— 恰恰是真实工程 + 唯一不具备的形态。** `216` 已改成依赖声明、消费方什么都不声明,并加了一条 + 前置断言:那条需求必须先出现在 `mcpp why runtime` 里,否则测试等于什么都没验。 + ## [2026.8.10.2] — 2026-08-10 图形栈闭合与分发档位。完整设计与实施计划见 diff --git a/mcpp.toml b/mcpp.toml index bb6e7ecb..eb611b5c 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.8.10.2" +version = "2026.8.10.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/pack/host_requirements.cppm b/src/pack/host_requirements.cppm index ac86fb88..d37f339b 100644 --- a/src/pack/host_requirements.cppm +++ b/src/pack/host_requirements.cppm @@ -70,8 +70,22 @@ inline constexpr std::string_view kFileName = "HOST-REQUIREMENTS"; // A requirement counts when it must be satisfied at RUN time by something // outside the artifact. Link-phase requirements are consumed during the build // and say nothing about the target machine. +// +// TAKES THE RESOLVED LIST, NOT A MANIFEST. Almost no application declares +// `capability:opengl.glx.driver` itself — it depends on something that does +// (glfw, an SDL wrapper, a GL runtime), and the resolver stamps each +// requirement with the exact requester. Reading the ROOT manifest's `[runtime]` +// therefore answers "did the author write it down", which is nearly always no, +// while the honest question is "does the resolved graph need it". +// +// Measured: a real imgui project whose `mcpp why runtime` lists +// `capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)` produced +// an EMPTY list from its own manifest — so `--mode self-contained` packaged it +// happily. The fixture-based test passed because the fixture declared the +// capability at the root, which is the one shape real projects do not have. std::vector -host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) { +host_requirements_of(std::span requirements, + std::span legacyCapabilities = {}) { std::vector out; auto add = [&](std::string capability, std::string discovery, bool required) { if (capability.empty()) return; @@ -80,7 +94,7 @@ host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) { return; out.push_back({std::move(capability), std::move(discovery), required}); }; - for (auto const& req : runtime.requirements) { + for (auto const& req : requirements) { if (req.phase != "run") continue; if (req.kind != "capability") continue; add(req.value, req.discovery, req.required); @@ -89,12 +103,19 @@ host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) { // compatibility train; a package that has not migrated must not silently // produce an empty list. It has no place to declare a mechanism, so those // rows say `unknown` — accurately. - for (auto const& capability : runtime.capabilities) + for (auto const& capability : legacyCapabilities) add(capability, /*discovery=*/{}, /*required=*/true); std::ranges::sort(out, {}, &HostRequirement::capability); return out; } +// Convenience for callers that only have a manifest (e.g. `mcpp emit xpkg` +// describing the package's OWN declarations rather than a resolved graph). +std::vector +host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) { + return host_requirements_of(runtime.requirements, runtime.capabilities); +} + // Render. One requirement per line, `key=value` fields, so the format can be // read by a shell one-liner as well as by a program — a manifest nobody can // grep is a manifest nobody reads. diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index b86f184c..2832a6e0 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -72,7 +72,11 @@ make_plan(const mcpp::manifest::Manifest& manifest, const Options& opts, const std::filesystem::path& builtBinary, const std::filesystem::path& projectRoot, - std::string_view triple); + std::string_view triple, + // The RESOLVED run-time requirements, from BuildPlan. Not the root + // manifest's: an application almost never declares a host capability + // itself, it depends on something that does. + std::span resolvedRequirements = {}); // Execute the plan: copies binary + .so + extra files, runs patchelf, // writes the final tarball or directory. @@ -167,7 +171,8 @@ make_plan(const mcpp::manifest::Manifest& manifest, const Options& opts, const std::filesystem::path& builtBinary, const std::filesystem::path& projectRoot, - std::string_view triple) + std::string_view triple, + std::span resolvedRequirements) { Plan p; p.opts = opts; @@ -177,7 +182,12 @@ make_plan(const mcpp::manifest::Manifest& manifest, p.packageName = manifest.package.name; p.packageVersion = manifest.package.version; p.triple = std::string(triple); - p.hostRequirements = host_requirements_of(manifest.runtimeConfig); + // Resolved graph first; the manifest's own declarations are the fallback + // for callers that have no plan (and remain covered by the legacy vector). + p.hostRequirements = resolvedRequirements.empty() + ? host_requirements_of(manifest.runtimeConfig) + : host_requirements_of(resolvedRequirements, + manifest.runtimeConfig.capabilities); // A MODE THAT CARRIES ITS OWN libc CANNOT CONSUME A HOST CAPABILITY. // diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 4657b788..5596cc2f 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -102,7 +102,11 @@ export int build_and_pack(Options opts, bool modeFromUser) { // ─── Build the plan + run ──────────────────────────────────────── auto plan = mcpp::pack::make_plan(ctx->manifest, *cfg, opts, - mainBinary, ctx->projectRoot, ctx->tc.targetTriple); + mainBinary, ctx->projectRoot, ctx->tc.targetTriple, + // From the RESOLVED graph. `mcpp why runtime` on a real imgui project + // lists `capability:opengl.glx.driver <- compat.glfw@3.4` — none of + // which appears in the project's own manifest. + ctx->plan.runtimeRequirements); if (!plan) { mcpp::ui::error(plan.error().message); return 1; } mcpp::ui::info("Packing", std::format("{} v{} ({})", diff --git a/src/version.cppm b/src/version.cppm index 887f49ea..3b7bbac1 100644 --- a/src/version.cppm +++ b/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.8.10.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.8.10.3"; } // namespace mcpp diff --git a/tests/e2e/216_selfcontained_refuses_host_capability.sh b/tests/e2e/216_selfcontained_refuses_host_capability.sh index e50c3e41..d1a6f2fd 100755 --- a/tests/e2e/216_selfcontained_refuses_host_capability.sh +++ b/tests/e2e/216_selfcontained_refuses_host_capability.sh @@ -23,27 +23,108 @@ # users route around; `vendored` is the mode that actually works here, and the # test asserts the message says so. set -e +source "$(dirname "$0")/_host_path.sh" TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT export MCPP_HOME=$HOME/.mcpp -cd "$TMP" -"$MCPP" new gfxapp > /dev/null -cd gfxapp -cat >> mcpp.toml <<'EOF' +# ── the capability comes from a DEPENDENCY, not from this project ─────────── +# +# THIS IS THE SHAPE REAL PROJECTS HAVE, and getting it wrong is how the first +# version of this gate shipped half-working. Almost no application declares +# `capability:opengl.glx.driver` itself — it depends on glfw / an SDL wrapper / +# a GL runtime that does, and the resolver stamps each requirement with its +# requester. A gate that reads the ROOT manifest answers "did the author write +# it down" (nearly always no) instead of "does the resolved graph need it". +# +# Measured on a real imgui project: `mcpp why runtime` listed +# `capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)`, and +# `--mode self-contained` packaged it happily — while THIS test passed, because +# its fixture declared the capability at the root. The fixture had the one +# shape real projects do not. +INDEX_DIR="$TMP/local-index" +# The manifest is read by mcpp, not by the shell, so the path has to be in HOST +# spelling — an MSYS `/c/...` written into mcpp.toml is a path Windows cannot +# open. `00_fixture_path_hygiene` enforces this statically. +INDEX_DIR_HOST="$(host_path "$INDEX_DIR")" +mkdir -p "$INDEX_DIR/pkgs/g" +cat > "$INDEX_DIR/pkgs/g/gfx-runtime.lua" <<'EOF' +package = { + spec = "1", + name = "gfx-runtime", + description = "A dependency that needs the host to provide a driver", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/gfx-runtime-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = true, + sources = { "src/**/*.cppm" }, + targets = { ["gfx-runtime"] = { kind = "lib" } }, + deps = {}, + runtime = { + requirements = { + { kind = "capability", value = "opengl.glx.driver", phase = "run", + required = true, discovery = "rpath-of-dispatch" }, + }, + }, + }, +} +EOF + +mkdir -p "$TMP/gfxapp/src" +mkdir -p "$TMP/gfxapp/.mcpp/.xlings/data/xpkgs/local-dev.gfx-runtime/1.0.0/src" +cd "$TMP/gfxapp" +cat > .mcpp/.xlings/data/xpkgs/local-dev.gfx-runtime/1.0.0/src/lib.cppm <<'EOF' +export module gfx.runtime; +export int gfx_ready() { return 1; } +EOF +cat > src/main.cpp <<'EOF' +import gfx.runtime; +int main() { return gfx_ready() == 1 ? 0 : 1; } +EOF +# The application itself declares NOTHING. That is the point. +cat > mcpp.toml < "$TMP/build.log" 2>&1 || { cat "$TMP/build.log"; exit 1; } +"$MCPP" why runtime > "$TMP/why.log" 2>&1 || { cat "$TMP/why.log"; exit 1; } +grep -q 'capability:opengl.glx.driver' "$TMP/why.log" || { + echo "FAIL: the dependency's capability never reached the resolved graph —" + echo " this test would then prove nothing about the gate" + cat "$TMP/why.log" + exit 1 +} +grep -q 'gfx-runtime' "$TMP/why.log" || { + echo "FAIL: the requirement is not attributed to the dependency" + cat "$TMP/why.log" + exit 1 +} +echo " requirement arrives from the dependency, not from this project" + # ── the two modes that carry their own libc must refuse ───────────────────── for mode in self-contained static; do if "$MCPP" pack --mode "$mode" > "$TMP/$mode.log" 2>&1; then