Skip to content

Commit 0287b14

Browse files
committed
fix(e2e,backend): fixture path spelling, a check that could only fail, and a guard
Three things the local suite and a read-through caught: * 255/257/259 interpolated `$(host_path …)` straight into a manifest heredoc. 00_fixture_path_hygiene requires the value to come from a named `*_HOST` variable, and the rule is Windows': a shell-spelled /tmp path is read by a native mcpp.exe as "root of the current drive". * 259 tested `[[ -f "$pkg/lib/"*"/libmathkit.dylib" ]]`. `[[ ]]` does not path-expand, so that compares against a literal string containing an asterisk and is false for every real package — a check that can only fail, which on macOS-only coverage would have looked like a product bug. * the import-library flag substituted `{}` without checking it is there. Both dialect rows have one; a row without it would have thrown at `replace(npos)` instead of simply not emitting a flag. Also documents, in docs/12 (+ zh), how much of this is verified and where — including the row that is honest rather than flattering: the REAL old-client check has been run by hand and not by CI, because the boot entry each job bootstraps from is an xvm shim that answers "not installed" under the e2e environment. Local suite after this: 236 passed, 1 failed, 14 skipped. The remaining failure is 22_doctor_cache_publish, and the previous RELEASE binary fails it identically on this machine — a local environment fact, not a regression.
1 parent 5468cc3 commit 0287b14

6 files changed

Lines changed: 109 additions & 8 deletions

File tree

docs/12-binary-distribution.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,50 @@ warning: secret.cppm is an implementation partition, and the published interface
333333
> Build order was unconstrained: GCC and macOS clang recovered through their own
334334
> dependency scan, Windows clang failed with `failed to read compiled module`.
335335
> If you have been avoiding implementation partitions on Windows, that was why.
336+
337+
### A partition mcpp cannot classify
338+
339+
`[scan_overrides."<glob>"]` says which modules a file provides and has nowhere to
340+
say whether the declaration carries `export`; a P1689 scanner may omit
341+
`is-interface`. Either way the source is published — the consumer cannot build the
342+
BMI without it — and `mcpp pack` says which of the two situations you are in:
343+
344+
```
345+
warning: secret.cppm provides a module PARTITION and mcpp cannot tell which kind:
346+
the unit is declared in `[scan_overrides]`, which has nowhere to say
347+
whether the declaration carries `export`, …
348+
```
349+
350+
> Until 2026.8.17.2 that arrived as "it is an interface" — the answer that
351+
> produces **no** warning — so an implementation partition declared that way was
352+
> published in silence. Publishing too few sources fails the consumer's compile
353+
> and names the module; publishing too many ships private source and nothing
354+
> fails at all. Unknown has to be loud.
355+
356+
## How much of this is verified, and where
357+
358+
The e2e suite gates each test on host capabilities, so "the suite is green" and
359+
"this ran" are different statements. What runs where:
360+
361+
| claim | linux | macOS | windows |
362+
|---|---|---|---|
363+
| layout, both interface modes, closure, the two gates, workspace root, named target, `sources = []`, bare-triple predicate ||||
364+
| fat package, two legs one artifact name (`gnu` + `musl`) || *impossible* ||
365+
| fat package, two legs **two** artifact names (`msvc` + `mingw`) || *impossible* ||
366+
| fat package crossing an OS boundary (a PE leg) ||||
367+
| `lib.exe /REMOVE:` really removing ||||
368+
| PE shared library: build, pack, link, run | ✅ (wine) |||
369+
| Mach-O shared library relocating out of its build tree ||||
370+
| MSVC refusing `kind = "shared"` for the export reason ||||
371+
| a released mcpp consuming a package this one produced | local only | local only | local only |
372+
373+
*impossible* is not a gap: a macOS host can serve exactly one target
374+
(`host_can_serve`, `registry.cppm`), so a package with two legs cannot be produced
375+
there at all.
376+
377+
The last row is honest about a real hole: each CI job bootstraps from a released
378+
mcpp, but that entry is an xvm **shim**, and under the e2e suite's environment it
379+
answers "not installed". So the static half of the old-client check (the generated
380+
manifest uses no section a previous mcpp cannot read) runs everywhere, and the
381+
real half — build against the package with the previous release — has been run by
382+
hand, not by CI.

docs/zh/12-binary-distribution.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,44 @@ warning: secret.cppm is an implementation partition, and the published interface
301301
> 的单元」到「定义分区的单元」的边,构建顺序无约束:GCC 与 macOS clang 靠各自的
302302
> 依赖扫描兜住了,**Windows clang 以 `failed to read compiled module` 失败**
303303
> 如果你一直在 Windows 上回避实现分区,原因就是这个。
304+
305+
### mcpp 判不出类别的分区
306+
307+
`[scan_overrides."<glob>"]` 说的是文件提供哪些模块,**没有地方能说**那条声明是否
308+
`export`;P1689 扫描器也可能省略 `is-interface`。两种情况下源码都会被发布 ——
309+
消费者没有它就编不出 BMI —— 而 `mcpp pack` 会告诉你你处在哪一种:
310+
311+
```
312+
warning: secret.cppm provides a module PARTITION and mcpp cannot tell which kind:
313+
the unit is declared in `[scan_overrides]`, which has nowhere to say
314+
whether the declaration carries `export`, …
315+
```
316+
317+
> 在 2026.8.17.2 之前,这种情况以「它是接口」到达 —— 那个**不产生任何警告**的答案 ——
318+
> 于是这样声明的实现分区被一声不响地发布了。**发布得太少**会让消费者编译失败并点名
319+
> 模块;**发布得太多**会把私有源码发出去,而什么都不会失败。未知必须出声。
320+
321+
## 这些说法验证到哪一步、在哪台机器上
322+
323+
e2e 套件按宿主能力给每条测试开门,所以「套件是绿的」和「这条跑了」是两句不同的话。
324+
实际跑在哪里:
325+
326+
| 说法 | linux | macOS | windows |
327+
|---|---|---|---|
328+
| 布局、两种接口模式、闭包、两道闸门、workspace 根、指名 target、`sources = []`、裸三元组谓词 ||||
329+
| 胖包,两条腿**同一个**产物名(`gnu` + `musl`) || *不可能* ||
330+
| 胖包,两条腿**两个**产物名(`msvc` + `mingw`) || *不可能* ||
331+
| 跨 OS 边界的胖包(一条 PE 腿) ||||
332+
| `lib.exe /REMOVE:` 真的删掉了 ||||
333+
| PE 共享库:产出、打包、链接、运行 | ✅(wine) |||
334+
| Mach-O 共享库离开构建树仍可加载 ||||
335+
| MSVC 以「导出」为理由拒绝 `kind = "shared"` ||||
336+
| 已发布的 mcpp 消费本版产出的包 | 仅本机 | 仅本机 | 仅本机 |
337+
338+
*不可能* 不是缺口:macOS 宿主只能服务一个 target(`host_can_serve`,
339+
`registry.cppm`),那里根本产不出两条腿的包。
340+
341+
最后一行如实记录一个真的洞:每个 CI job 都从一份已发布的 mcpp 自举,但那个入口是
342+
xvm 的 **shim**,在 e2e 套件改过的环境里它回答「未安装」。所以老客户端检查的
343+
**静态那半**(生成的 manifest 不含任何旧 mcpp 读不了的段)到处都跑,而**真实那半** ——
344+
用上一版发布的 mcpp 去构建这个包 —— 是**手工跑的,不是 CI 跑的**

src/build/ninja_backend.cppm

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,10 +1826,17 @@ std::string emit_ninja_string(const BuildPlan& plan) {
18261826
// agrees with — the name belongs to plan.cppm's import_library_for, and
18271827
// this is how it gets to the command. The SPELLING belongs to the
18281828
// dialect table, same as `archiveRemoveArg`.
1829-
if (!lu.importLibrary.empty() && !dial.sharedImportLibArg.empty()) {
1829+
if (!lu.importLibrary.empty()) {
18301830
std::string arg{ dial.sharedImportLibArg };
1831-
arg.replace(arg.find("{}"), 2, escape_ninja_path(lu.importLibrary));
1832-
out_line += " implib_flag = " + arg + "\n";
1831+
// `{}` or nothing: a row without the placeholder cannot say WHERE to
1832+
// write, so emitting its bare text would hand the linker a flag with
1833+
// no argument. Skipping is the honest reading of an empty row, and
1834+
// the implicit output above then fails loudly as a missing file
1835+
// rather than quietly linking against a stale one.
1836+
if (auto at = arg.find("{}"); at != std::string::npos) {
1837+
arg.replace(at, 2, escape_ninja_path(lu.importLibrary));
1838+
out_line += " implib_flag = " + arg + "\n";
1839+
}
18331840
}
18341841
{
18351842
// Per-unit C++ runtime link, by ROLE. The kind→role map is the

tests/e2e/255_pack_library_msvc_archiver.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,13 @@ EOF
119119
# outright. That is a real limitation of the generated manifest (recorded in
120120
# docs/12), and it is NOT what this test is about — pinning cl here would make
121121
# 255 fail for a reason that has nothing to do with the archiver.
122+
PKG_HOST="$(host_path "$TMP/mathkit/$pkg")"
122123
cat > app/mcpp.toml <<EOF
123124
[package]
124125
name = "app"
125126
version = "0.1.0"
126127
[dependencies]
127-
mathkit = { path = "$(host_path "$TMP/mathkit/$pkg")" }
128+
mathkit = { path = "$PKG_HOST" }
128129
[targets.app]
129130
kind = "bin"
130131
main = "src/main.cpp"

tests/e2e/257_shared_library_pe.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,13 @@ cat > app/src/main.cpp <<'EOF'
9898
import mathkit;
9999
int main(){ std::printf("ok=%d\n", mk::answer()); return 0; }
100100
EOF
101+
PKG_HOST="$(host_path "$pkg")"
101102
cat > app/mcpp.toml <<EOF
102103
[package]
103104
name = "app"
104105
version = "0.1.0"
105106
[dependencies]
106-
mathkit = { path = "$(host_path "$pkg")" }
107+
mathkit = { path = "$PKG_HOST" }
107108
[targets.app]
108109
kind = "bin"
109110
main = "src/main.cpp"

tests/e2e/259_shared_library_macho.sh

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ pkgrel="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0*' | head -1)
8080
# OUT of the producer's tree, so the next step can delete that tree entirely.
8181
pkg="$TMP/pkg"
8282
cp -R "$TMP/mathkit/$pkgrel" "$pkg"
83-
[[ -f "$pkg/lib/"*"/libmathkit.dylib" ]] 2>/dev/null || {
84-
find "$pkg" -type f -o -type l
83+
# `find`, not `[[ -f "$pkg/lib/"*"/libmathkit.dylib" ]]`: inside `[[ ]]` the `*`
84+
# is not path-expanded, so that form tests a literal string containing an
85+
# asterisk and is false for every real package — a check that can only fail.
86+
[[ -n "$(find "$pkg/lib" -name 'libmathkit.dylib' | head -1)" ]] || {
87+
find "$pkg" \( -type f -o -type l \)
8588
echo "FAIL: no dylib in the package"; exit 1; }
8689

8790
# ── 4. the producer's build tree is deleted, then the consumer runs ──────
@@ -97,12 +100,13 @@ cat > app/src/main.cpp <<'EOF'
97100
import mathkit;
98101
int main(){ std::printf("ok=%d\n", mk::answer()); return 0; }
99102
EOF
103+
PKG_HOST="$(host_path "$pkg")"
100104
cat > app/mcpp.toml <<EOF
101105
[package]
102106
name = "app"
103107
version = "0.1.0"
104108
[dependencies]
105-
mathkit = { path = "$(host_path "$pkg")" }
109+
mathkit = { path = "$PKG_HOST" }
106110
[targets.app]
107111
kind = "bin"
108112
main = "src/main.cpp"

0 commit comments

Comments
 (0)