Skip to content

Commit 2451ab8

Browse files
committed
fix(pack): 宿主能力清单从解析后的图取,不再从根 manifest 取 (2026.8.10.3)
`2026.8.10.2` 的「自带 libc 的档拒绝宿主能力」只在**根工程自己声明**能力时生效。 而几乎没有应用会自己声明 `capability:opengl.glx.driver` —— 它依赖某个声明了的包, resolver 给每条需求盖上请求者身份。读根 manifest 回答的是「作者写没写」 (几乎总是没写),该问的是「解析出来的图需不需要」。 ## 实测 真实 imgui 工程,`mcpp why runtime` 明明白白列着: capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required) 而修复前 `mcpp pack --mode self-contained` **照打不误**,`HOST-REQUIREMENTS` 也是空的 (而空文件会被读成「什么都不需要」)。修复后: 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 — … `--mode vendored` 正常打包,三条全部写进清单。 ## 为什么原来的测试没抓到 `216` 的 fixture **在根工程声明了能力** —— 恰恰是真实工程唯一不具备的形态。 测试通过的理由比它声称覆盖的范围窄,而窄在哪里没有被说出来:本轮反复写的就是这条, 这次轮到我自己。 `216` 已改成:能力由**依赖**声明、消费方什么都不声明,并加一条前置断言 —— 那条需求必须先出现在 `mcpp why runtime` 里,否则「两档都拒绝」可能只是因为 根本没有需求可拒,测试等于空转。 顺带验到 `discovery` 的声明式链路端到端可用:依赖描述符里写的 `discovery = "rpath-of-dispatch"` 原样出现在消费方的 `HOST-REQUIREMENTS` 里。 unit 77/77 通过。
1 parent 516a13b commit 2451ab8

7 files changed

Lines changed: 156 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,29 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.8.10.3] — 2026-08-10
7+
8+
### 修复
9+
10+
- **宿主能力清单从「解析后的图」取,不再从根 manifest 取。** `2026.8.10.2` 引入的
11+
「自带 libc 的档拒绝宿主能力」只在**根工程自己声明**能力时生效 —— 而几乎没有应用
12+
会自己声明 `capability:opengl.glx.driver`,它依赖某个声明了的包(glfw / SDL 封装 /
13+
GL runtime),resolver 会给每条需求盖上请求者身份。读根 manifest 回答的是
14+
「作者写没写」(几乎总是没写),而该问的是「解析出来的图需不需要」。
15+
16+
实测:一个真实 imgui 工程的 `mcpp why runtime` 列着
17+
`capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)`,
18+
`mcpp pack --mode self-contained` **照打不误**。修复后它正确拒绝,并列出
19+
`abi:glibc, opengl.glx.driver, x11.display` 三条;`--mode vendored` 正常打包
20+
并把三条写进 `HOST-REQUIREMENTS`
21+
22+
同一处也修好了 `mcpp pack``HOST-REQUIREMENTS`:此前对真实工程是空的
23+
(空文件会被读成「什么都不需要」,而它现在根本不写空文件)。
24+
25+
**为什么原来的测试没抓到:它的 fixture 在根工程声明了能力 —— 恰恰是真实工程
26+
唯一不具备的形态。** `216` 已改成依赖声明、消费方什么都不声明,并加了一条
27+
前置断言:那条需求必须先出现在 `mcpp why runtime` 里,否则测试等于什么都没验。
28+
629
## [2026.8.10.2] — 2026-08-10
730

831
图形栈闭合与分发档位。完整设计与实施计划见

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.8.10.2"
3+
version = "2026.8.10.3"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/pack/host_requirements.cppm

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,22 @@ inline constexpr std::string_view kFileName = "HOST-REQUIREMENTS";
7070
// A requirement counts when it must be satisfied at RUN time by something
7171
// outside the artifact. Link-phase requirements are consumed during the build
7272
// and say nothing about the target machine.
73+
//
74+
// TAKES THE RESOLVED LIST, NOT A MANIFEST. Almost no application declares
75+
// `capability:opengl.glx.driver` itself — it depends on something that does
76+
// (glfw, an SDL wrapper, a GL runtime), and the resolver stamps each
77+
// requirement with the exact requester. Reading the ROOT manifest's `[runtime]`
78+
// therefore answers "did the author write it down", which is nearly always no,
79+
// while the honest question is "does the resolved graph need it".
80+
//
81+
// Measured: a real imgui project whose `mcpp why runtime` lists
82+
// `capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)` produced
83+
// an EMPTY list from its own manifest — so `--mode self-contained` packaged it
84+
// happily. The fixture-based test passed because the fixture declared the
85+
// capability at the root, which is the one shape real projects do not have.
7386
std::vector<HostRequirement>
74-
host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) {
87+
host_requirements_of(std::span<const mcpp::manifest::RuntimeRequirement> requirements,
88+
std::span<const std::string> legacyCapabilities = {}) {
7589
std::vector<HostRequirement> out;
7690
auto add = [&](std::string capability, std::string discovery, bool required) {
7791
if (capability.empty()) return;
@@ -80,7 +94,7 @@ host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) {
8094
return;
8195
out.push_back({std::move(capability), std::move(discovery), required});
8296
};
83-
for (auto const& req : runtime.requirements) {
97+
for (auto const& req : requirements) {
8498
if (req.phase != "run") continue;
8599
if (req.kind != "capability") continue;
86100
add(req.value, req.discovery, req.required);
@@ -89,12 +103,19 @@ host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) {
89103
// compatibility train; a package that has not migrated must not silently
90104
// produce an empty list. It has no place to declare a mechanism, so those
91105
// rows say `unknown` — accurately.
92-
for (auto const& capability : runtime.capabilities)
106+
for (auto const& capability : legacyCapabilities)
93107
add(capability, /*discovery=*/{}, /*required=*/true);
94108
std::ranges::sort(out, {}, &HostRequirement::capability);
95109
return out;
96110
}
97111

112+
// Convenience for callers that only have a manifest (e.g. `mcpp emit xpkg`
113+
// describing the package's OWN declarations rather than a resolved graph).
114+
std::vector<HostRequirement>
115+
host_requirements_of(const mcpp::manifest::RuntimeConfig& runtime) {
116+
return host_requirements_of(runtime.requirements, runtime.capabilities);
117+
}
118+
98119
// Render. One requirement per line, `key=value` fields, so the format can be
99120
// read by a shell one-liner as well as by a program — a manifest nobody can
100121
// grep is a manifest nobody reads.

src/pack/pack.cppm

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ make_plan(const mcpp::manifest::Manifest& manifest,
7272
const Options& opts,
7373
const std::filesystem::path& builtBinary,
7474
const std::filesystem::path& projectRoot,
75-
std::string_view triple);
75+
std::string_view triple,
76+
// The RESOLVED run-time requirements, from BuildPlan. Not the root
77+
// manifest's: an application almost never declares a host capability
78+
// itself, it depends on something that does.
79+
std::span<const mcpp::manifest::RuntimeRequirement> resolvedRequirements = {});
7680

7781
// Execute the plan: copies binary + .so + extra files, runs patchelf,
7882
// writes the final tarball or directory.
@@ -167,7 +171,8 @@ make_plan(const mcpp::manifest::Manifest& manifest,
167171
const Options& opts,
168172
const std::filesystem::path& builtBinary,
169173
const std::filesystem::path& projectRoot,
170-
std::string_view triple)
174+
std::string_view triple,
175+
std::span<const mcpp::manifest::RuntimeRequirement> resolvedRequirements)
171176
{
172177
Plan p;
173178
p.opts = opts;
@@ -177,7 +182,12 @@ make_plan(const mcpp::manifest::Manifest& manifest,
177182
p.packageName = manifest.package.name;
178183
p.packageVersion = manifest.package.version;
179184
p.triple = std::string(triple);
180-
p.hostRequirements = host_requirements_of(manifest.runtimeConfig);
185+
// Resolved graph first; the manifest's own declarations are the fallback
186+
// for callers that have no plan (and remain covered by the legacy vector).
187+
p.hostRequirements = resolvedRequirements.empty()
188+
? host_requirements_of(manifest.runtimeConfig)
189+
: host_requirements_of(resolvedRequirements,
190+
manifest.runtimeConfig.capabilities);
181191

182192
// A MODE THAT CARRIES ITS OWN libc CANNOT CONSUME A HOST CAPABILITY.
183193
//

src/pack/pipeline.cppm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ export int build_and_pack(Options opts, bool modeFromUser) {
102102

103103
// ─── Build the plan + run ────────────────────────────────────────
104104
auto plan = mcpp::pack::make_plan(ctx->manifest, *cfg, opts,
105-
mainBinary, ctx->projectRoot, ctx->tc.targetTriple);
105+
mainBinary, ctx->projectRoot, ctx->tc.targetTriple,
106+
// From the RESOLVED graph. `mcpp why runtime` on a real imgui project
107+
// lists `capability:opengl.glx.driver <- compat.glfw@3.4` — none of
108+
// which appears in the project's own manifest.
109+
ctx->plan.runtimeRequirements);
106110
if (!plan) { mcpp::ui::error(plan.error().message); return 1; }
107111

108112
mcpp::ui::info("Packing", std::format("{} v{} ({})",

src/version.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.8.10.2";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.8.10.3";
3535

3636
} // namespace mcpp

tests/e2e/216_selfcontained_refuses_host_capability.sh

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,98 @@ TMP=$(mktemp -d)
2828
trap "rm -rf $TMP" EXIT
2929
export MCPP_HOME=$HOME/.mcpp
3030

31-
cd "$TMP"
32-
"$MCPP" new gfxapp > /dev/null
33-
cd gfxapp
34-
cat >> mcpp.toml <<'EOF'
31+
# ── the capability comes from a DEPENDENCY, not from this project ───────────
32+
#
33+
# THIS IS THE SHAPE REAL PROJECTS HAVE, and getting it wrong is how the first
34+
# version of this gate shipped half-working. Almost no application declares
35+
# `capability:opengl.glx.driver` itself — it depends on glfw / an SDL wrapper /
36+
# a GL runtime that does, and the resolver stamps each requirement with its
37+
# requester. A gate that reads the ROOT manifest answers "did the author write
38+
# it down" (nearly always no) instead of "does the resolved graph need it".
39+
#
40+
# Measured on a real imgui project: `mcpp why runtime` listed
41+
# `capability:opengl.glx.driver [run] <- compat.glfw@3.4 (required)`, and
42+
# `--mode self-contained` packaged it happily — while THIS test passed, because
43+
# its fixture declared the capability at the root. The fixture had the one
44+
# shape real projects do not.
45+
INDEX_DIR="$TMP/local-index"
46+
mkdir -p "$INDEX_DIR/pkgs/g"
47+
cat > "$INDEX_DIR/pkgs/g/gfx-runtime.lua" <<'EOF'
48+
package = {
49+
spec = "1",
50+
name = "gfx-runtime",
51+
description = "A dependency that needs the host to provide a driver",
52+
licenses = {"MIT"},
53+
type = "package",
54+
xpm = {
55+
linux = {
56+
["1.0.0"] = {
57+
url = "https://example.invalid/gfx-runtime-1.0.0.tar.gz",
58+
sha256 = "0000000000000000000000000000000000000000000000000000000000000000",
59+
},
60+
},
61+
},
62+
mcpp = {
63+
language = "c++23",
64+
import_std = true,
65+
sources = { "src/**/*.cppm" },
66+
targets = { ["gfx-runtime"] = { kind = "lib" } },
67+
deps = {},
68+
runtime = {
69+
requirements = {
70+
{ kind = "capability", value = "opengl.glx.driver", phase = "run",
71+
required = true, discovery = "rpath-of-dispatch" },
72+
},
73+
},
74+
},
75+
}
76+
EOF
77+
78+
mkdir -p "$TMP/gfxapp/src"
79+
mkdir -p "$TMP/gfxapp/.mcpp/.xlings/data/xpkgs/local-dev.gfx-runtime/1.0.0/src"
80+
cd "$TMP/gfxapp"
81+
cat > .mcpp/.xlings/data/xpkgs/local-dev.gfx-runtime/1.0.0/src/lib.cppm <<'EOF'
82+
export module gfx.runtime;
83+
export int gfx_ready() { return 1; }
84+
EOF
85+
cat > src/main.cpp <<'EOF'
86+
import gfx.runtime;
87+
int main() { return gfx_ready() == 1 ? 0 : 1; }
88+
EOF
89+
# The application itself declares NOTHING. That is the point.
90+
cat > mcpp.toml <<EOF
91+
[package]
92+
name = "gfxapp"
93+
version = "0.1.0"
3594
36-
[[runtime.requirements]]
37-
kind = "capability"
38-
value = "opengl.glx.driver"
39-
phase = "run"
40-
# DECLARED by the package, not inferred by mcpp: which mechanism a capability
41-
# uses is the provider's property. GLX is reached through the dispatch
42-
# library's own DT_RPATH; EGL through a JSON file holding an ABSOLUTE path — so
43-
# the two are not interchangeable, and a row without this is not actionable.
44-
discovery = "rpath-of-dispatch"
95+
[indices]
96+
local-dev = { path = "$INDEX_DIR" }
97+
98+
[dependencies]
99+
"local-dev.gfx-runtime" = "1.0.0"
100+
101+
[targets.gfxapp]
102+
kind = "bin"
103+
main = "src/main.cpp"
45104
EOF
46105

106+
# Guard: if the requirement never reaches the resolved graph, everything below
107+
# would pass by refusing nothing — so assert it arrived first.
108+
"$MCPP" build > "$TMP/build.log" 2>&1 || { cat "$TMP/build.log"; exit 1; }
109+
"$MCPP" why runtime > "$TMP/why.log" 2>&1 || { cat "$TMP/why.log"; exit 1; }
110+
grep -q 'capability:opengl.glx.driver' "$TMP/why.log" || {
111+
echo "FAIL: the dependency's capability never reached the resolved graph —"
112+
echo " this test would then prove nothing about the gate"
113+
cat "$TMP/why.log"
114+
exit 1
115+
}
116+
grep -q 'gfx-runtime' "$TMP/why.log" || {
117+
echo "FAIL: the requirement is not attributed to the dependency"
118+
cat "$TMP/why.log"
119+
exit 1
120+
}
121+
echo " requirement arrives from the dependency, not from this project"
122+
47123
# ── the two modes that carry their own libc must refuse ─────────────────────
48124
for mode in self-contained static; do
49125
if "$MCPP" pack --mode "$mode" > "$TMP/$mode.log" 2>&1; then

0 commit comments

Comments
 (0)