Skip to content

Commit fe62962

Browse files
committed
feat(build,pack): kind="shared" beyond ELF, and stop building unservable targets
Four things, and the first one explains why the rest went unnoticed. THE GUARD HAD A HOLE IN THE CASE THAT MATTERED. make_plan refused SharedLibrary targets with `!targetTriple.empty() && os != "linux"`, and targetTriple is EMPTY for a native build. So it turned away a cross build to macOS — unservable anyway, i.e. unreachable — while letting a NATIVE macOS or native Windows build walk straight into the unverified paths it was written to keep people out of. It reads the resolved target now. PE WAS MISSING ITS IMPORT LIBRARY. A PE shared library is two files: the `.dll` the loader opens and an archive of stubs the LINKER consumes. mcpp wrote only the first, and consumers linked the `.dll` directly — which mingw's ld tolerates and no other linker does, so the tolerant case was hiding the broken one. The link edge now declares the import library as an implicit output (one command writes both; a second edge would run the link twice), `import_library_for` owns its name, the dialect table owns the flag spelling beside `archiveRemoveArg`, and the package ships it. One more thing was needed and the diagnostic for it names nothing useful: mcpp gives PE executables `-static`, which puts ld in static-only mode where it refuses an import library and says `have you installed the static version of the mathkit library?` — so the emitted manifest switches to dynamic mode for that one `-l`. MACH-O WAS MISSING ITS INSTALL NAME. A `.dylib` records the path it was LINKED at, so emitting `-install_name` only when the manifest declared a `soname` left every other dylib recording a build directory: perfect on the machine that built it, `image not found` anywhere else. It is now `@rpath/<file>` unconditionally. The choice was also being made with `#if defined(__APPLE__)` on the HOST, so a cross link emitted the wrong flag or none — it comes from the target now, as `target_output` already did. MSVC STAYS REFUSED, FOR THE REAL REASON. Not the linker: `link /DLL /IMPLIB:` has been in the rule table all along. Symbol export — MSVC exports nothing from a DLL without `__declspec(dllexport)` or a `.def`, so the import library comes out empty and consumers fail with unresolved externals naming symbols that are visibly in the objects. Refusing beats producing a diagnostic that points nowhere near its cause, and the message names MinGW as the way forward. AND `--target` NO LONGER ACCEPTS WHAT THIS HOST CANNOT PRODUCE. Measured on Linux: `mcpp build --target x86_64-windows-msvc` resolved the native g++, wrote target/x86_64-linux-gnu/, and reported success — an ELF delivered as a Windows build, which is the failure the neighbouring typo check calls the worst one. The vocabulary tier says "mcpp supports this target"; host_can_serve answers "can this machine produce it", and the error lists what it can. An explicit `[target.X] toolchain` stays the escape hatch. Also fixed on the way: consuming a distribution package whose target is `shared` died with `ninja: multiple rules generate bin/libmathkit.dll`, because the dependency loop created a link unit for a library that is already built — and relinking it would have produced a library missing every implementation unit the publisher withheld, since a distribution package's `sources` are its interface. Verified end to end on Linux via mingw-cross + wine (e2e 257: both files, the implicit output, the package, deployment beside the exe, `ok=42`). macOS (259) and the MSVC refusal (258) are CI's to confirm; 259 deletes the producer's build tree before consuming, so the install-name assertion is load-bearing rather than decorative.
1 parent 4c7aeb5 commit fe62962

19 files changed

Lines changed: 1027 additions & 54 deletions

.github/workflows/ci-windows-e2e.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,33 @@ jobs:
5757
"$MCPP_SELF" --version
5858
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
5959
60+
# MinGW-w64 GCC (xim:mingw-gcc). Installed here so the `mingw` capability
61+
# is GRANTED rather than left to whatever the shared sandbox cache happens
62+
# to carry: e2e 256 packs an MSVC leg and a MinGW leg into one package,
63+
# which is the only place `lib/` keyed by triple is proven with two
64+
# DIFFERENT artifact names (mathkit.lib beside libmathkit.a). Without this
65+
# step that test skips, and a skipped test in a green suite reads exactly
66+
# like a passing one.
67+
#
68+
# Not `|| true`: if the payload cannot be installed the capability quietly
69+
# disappears and the coverage goes with it, which is the failure mode this
70+
# step exists to prevent.
71+
- name: "Toolchain: MinGW payload for the fat-package e2e"
72+
shell: bash
73+
run: |
74+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
75+
"$MCPP_SELF" toolchain install mingw 16.1.0
76+
# Verified through the SAME two locations run_all.sh probes — checking
77+
# only one of them would let the step pass while the capability stays
78+
# ungranted, which is the shape of a green run that tested nothing.
79+
found=""
80+
for c in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-mingw-gcc/*/bin/g++.exe \
81+
"$HOME"/.xlings/data/xpkgs/xim-x-mingw-gcc/*/bin/g++.exe; do
82+
[[ -x "$c" ]] && { found="$c"; break; }
83+
done
84+
test -n "$found" || { echo "FAIL: mingw installed but not where run_all.sh looks"; exit 1; }
85+
echo "mingw payload: $found"
86+
6087
- name: E2E suite
6188
shell: bash
6289
# Fail-fast on hung tests instead of burning the whole job budget.

CHANGELOG.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,49 @@
3434
同一个闭包反过来决定归档里要删哪些对象,按 `.m.o` 删则会删掉真代码、
3535
三个平台全部链接失败。两条清单都会打印出来。
3636

37-
详见 `docs/12-binary-distribution.md``examples/05-lib-dist``examples/06-lib-consume`
37+
详见 `docs/12-binary-distribution.md``examples/05-lib-distribution`
38+
39+
- **`kind = "shared"` 不再只有 Linux:PE/MinGW 与 Mach-O 都能产、能打包、能跑。**
40+
41+
过去这条路只在 ELF 上验证过,而那道「非 Linux 就拒绝」的守卫**恰好在最要紧的
42+
情形下失效**:它写的是 `!targetTriple.empty() && os != "linux"`,而**原生构建的
43+
targetTriple 是空的** —— 于是它拦住了一个交叉到 macOS 的构建(那个本来就不可服务),
44+
却让**原生 macOS / 原生 Windows** 直接走进它本该拦住的未验证路径。
45+
46+
真正缺的东西各不相同,而且都不是 flag 拼写:
47+
48+
- **PE 缺导入库。** 一个 PE 共享库是**两个文件**:加载器打开的 `.dll`,以及
49+
链接器消费的桩归档。mcpp 只写了前者,消费者直接链 `.dll` —— mingw 的 ld 容忍这个,
50+
别的链接器都不容忍,于是**能用的那种情况把坏掉的那种遮住了**。现在链接边把导入库
51+
作为隐式输出声明出来,包里两个都带,生成的 manifest 指向导入库。
52+
另外 PE 可执行文件带 `-static`,而 `-static` 会让 ld 进入纯静态模式并拒绝导入库,
53+
报的是 `have you installed the static version of the mathkit library?` ——
54+
既没点 DLL 也没点 `-static`;所以那条 `-l` 之前要先 `-Wl,-Bdynamic`
55+
- **Mach-O 缺 install name。** `.dylib` 记录的是**链接时的路径**,所以只在声明了
56+
`soname` 时才发 `-install_name` 意味着**其他每一个 `.dylib` 都把构建目录烙了进去** ——
57+
在打包机上完好,换个地方就 `image not found`。现在无条件发 `@rpath/<file>`
58+
而且这个选择原先是用宿主的 `#if defined(__APPLE__)` 做的,交叉链接会发错(或不发);
59+
现在按 target 决定,和 `target_output` 早就做的一样。
60+
- **PE/MSVC 仍然拒绝,但换了个理由,而且是真理由。** 不是链接器 ——
61+
`link /DLL /IMPLIB:` 一直都在规则表里。是**符号导出**:没有 `__declspec(dllexport)`
62+
`.def`,MSVC 的 DLL 什么都不导出 ⇒ 导入库是空的 ⇒ 消费者拿到一堆
63+
unresolved externals,而那些符号明明在对象里。产出这个比拒绝更糟。
64+
65+
- **`--target` 不能服务时直接拒绝,而不是悄悄按宿主构建。**
66+
67+
实测(Linux):`mcpp build --target x86_64-windows-msvc` 解析到**原生 g++**
68+
写进 `target/x86_64-linux-gnu/`、报告成功 —— 一个 ELF 被当成 Windows 构建交付。
69+
词表的 tier 说的是「mcpp 支持这个 target」,从来没说「这台机器能产出它」;
70+
后者是 `host_can_serve` 的问题,现在 `prepare.cppm` 会问它,并把
71+
**这台宿主能构建的清单**列进错误信息。逃生口保留:显式
72+
`[target.X] toolchain = "…"` 表示交叉链是你自己提供的,mcpp 的载荷矩阵无权否决。
73+
74+
- **`[package] platforms` 会与实际打出的腿对账(设计里承诺过、实现里没有)。**
75+
76+
四种比较只有两种值得打印:**打了却没声明**永远可行动;**声明了却没打,
77+
且这台宿主本来能构建它**才可行动。正常发布流程是 CI 上每平台各跑一次 `mcpp pack`,
78+
所以 Linux runner 不产 macOS 腿不是遗漏、是每一次 —— **永远触发的告警会把真正该看的
79+
那条盖掉**。所以判据用的是 `host_can_serve`,与 `--target` 是同一个函数。
3880

3981
- **消费预编译包时的两道闸门。** 都是不检查就会静默出错的:
4082

@@ -50,6 +92,21 @@
5092

5193
### 修复
5294

95+
- **「谁也没判定过」这个状态过去被拼成了「是接口」,于是闭源实现分区会静默发布出去。**
96+
97+
一个分区的源码能不能发布,取决于一个关键字:`export module M:api;` 可以走,
98+
`module M:impl;` 不能。而三条建图路径里有两条读不到它:
99+
`[scan_overrides."<glob>"]` 声明了文件提供哪些模块,**没有地方能说它是否 export**;
100+
P1689 的 `is-interface` 是可选键,mcpp 把它解析进了一个**从来没人读**的字段。
101+
102+
两者都以 `providesInterface = true` 到达,而字段自己的注释把这叫「保守方向」,
103+
理由是「这个标志只会产生一条警告」。**这恰好说反了**:`true` 正是那个**不产生
104+
任何警告**的值 —— 于是用 `[scan_overrides]` 声明的实现分区被**一声不响地发布**
105+
106+
现在它是三态的,每条路径只说自己真的知道的事:文本扫描器读关键字并显式写 true/false;
107+
P1689 读取器把编译器的答案(**包括它的沉默**)原样带过来;`scan_overrides`
108+
**留空**,因为 schema 表达不了。未知会告警,而且和已知那条**说的是不同的话**
109+
53110
- **实现分区(`module M:part;`)在 Windows 上构建不了,而根因在扫描器里。**
54111

55112
`module M:part;``module M;` 共用一个拼写,却是两种不同的声明,而扫描器把

docs/08-toolchain-internals.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,32 @@ libc++ linkage handling; Windows has no rpath — mcpp deploys runtime DLLs
558558
next to the produced exe, which is the platform's native equivalent of
559559
everything §3–§4 does for ELF.
560560

561+
**Shared libraries a project PRODUCES** (`kind = "shared"`) do differ per format,
562+
and the difference is not a flag spelling — it is what the artifact records about
563+
itself:
564+
565+
| format | what the producer emits | what the consumer links |
566+
|---|---|---|
567+
| ELF | `-Wl,-soname,<n>` when declared | `-L` + `-l`, `-Wl,-rpath,$ORIGIN` |
568+
| Mach-O | `-Wl,-install_name,@rpath/<file>` **always** | `-L` + `-l`, `-Wl,-rpath,@loader_path` |
569+
| PE / MinGW | `-Wl,--out-implib,<lib>` | the **import library**, `-Wl,-Bdynamic` first |
570+
| PE / MSVC | refused (no auto-export; see docs/12) ||
571+
572+
Two of those are recent corrections. Mach-O's install name defaults to the path
573+
the library was LINKED at, so emitting it only when a `soname` was declared left
574+
every other `.dylib` recording a build directory — fine on the machine that built
575+
it, `image not found` anywhere else. And the choice was made with `#if
576+
defined(__APPLE__)` on the HOST, so a cross link emitted the wrong one (or none).
577+
It is decided from the target now, like `target_output` already was.
578+
579+
An **unservable target is refused** rather than quietly built for the host:
580+
`--target x86_64-windows-msvc` on Linux used to resolve the native `g++`, write
581+
`target/x86_64-linux-gnu/`, and report success. The vocabulary tier says "mcpp
582+
supports this target"; `host_can_serve` (`registry.cppm`) answers the different
583+
question "can this machine produce it", and `prepare.cppm` now asks it — with an
584+
explicit `[target.X] toolchain = "…"` as the escape hatch for a cross toolchain
585+
you supply yourself.
586+
561587
## 8. Source map
562588

563589
| Concern | File |

docs/12-binary-distribution.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,57 @@ you publish to a mixed audience.
262262
|---|---|
263263
| `kind = "lib"` (static) | ✅ every target, tested on all three |
264264
| `kind = "shared"` on Linux/ELF | ✅ — the package carries both the link name and the SONAME |
265-
| `kind = "shared"` on PE / Mach-O | ❌ refused — import libraries and install-names are not modelled yet |
265+
| `kind = "shared"` on PE / MinGW (`*-windows-gnu`) | ✅ — the package carries the `.dll` **and** its import library |
266+
| `kind = "shared"` on Mach-O (`*-macos`) | ✅ — install name is `@rpath/<file>`, so the `.dylib` relocates |
267+
| `kind = "shared"` on PE / MSVC (`*-windows-msvc`) | ❌ refused — see below |
266268
| `kind = "shared"` on `*-musl` | ❌ a musl target links statically |
267269
| shipping prebuilt BMIs | ❌ not attempted; BMIs are compiler-build-exact |
268270
| bundling dependencies into the package | ❌ declare them instead (above) |
271+
| consuming a package with **native `cl.exe`** | ❌ see below |
272+
273+
### Why MSVC refuses `kind = "shared"`
274+
275+
Not the linker — `link /DLL /IMPLIB:` works. **Symbol export.** MSVC exports
276+
nothing from a DLL unless the source says `__declspec(dllexport)` or a `.def`
277+
file lists the symbols, so the import library comes out empty and every consumer
278+
fails with unresolved externals naming symbols that are plainly in the object
279+
files. mcpp refuses rather than produce a diagnostic that points nowhere near its
280+
cause:
281+
282+
```
283+
target 'mathkit': kind = "shared" is not supported for the MSVC ABI (x86_64-windows-msvc).
284+
MSVC exports nothing from a DLL unless the source says `__declspec(dllexport)`
285+
...
286+
Use kind = "lib" for this target, or build it for *-windows-gnu (MinGW),
287+
where the linker auto-exports.
288+
```
289+
290+
MinGW's linker auto-exports, which is why `*-windows-gnu` is supported and
291+
`*-windows-msvc` is not. Closing this needs a generated `.def` — a symbol scan
292+
over the objects — which is a build-graph node, not a flag.
293+
294+
### A package's link flags are GNU-spelled
295+
296+
The generated manifest selects each leg with
297+
298+
```toml
299+
[target.'cfg(all(arch = "x86_64", os = "windows", env = "msvc"))'.build]
300+
ldflags = ["-Llib/x86_64-windows-msvc", "-lmathkit"]
301+
```
302+
303+
Every driver mcpp uses accepts that — including clang on the MSVC ABI, which is
304+
Windows' default here. **Native `cl.exe` does not**: it rejects `-L`. So a
305+
consumer that pins `[toolchain] windows = "msvc@system"` cannot link a packaged
306+
library today.
307+
308+
Naming the file by path instead (`lib/<triple>/mathkit.lib`) is the spelling
309+
every driver takes, and it does not work either: ninja runs link commands with
310+
cwd = the output directory, and only the include-family prefixes (`-I`, `-L`, …)
311+
are absolutized against the package root, so a prefix-less token is looked for
312+
in the wrong place — `ld: cannot find lib/x86_64-windows-gnu/libmathkit.a`. A
313+
manifest cannot carry an absolute path and stay relocatable. Closing this needs
314+
the conditional channel to carry `link_library_dirs` / `libraries`, which mcpp
315+
already renders per dialect, but only reads at the top level.
269316

270317
### Implementation partitions
271318

docs/zh/12-binary-distribution.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,51 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"]
237237
|---|---|
238238
| `kind = "lib"`(静态) | ✅ 所有 target,三平台都测了 |
239239
| `kind = "shared"` on Linux/ELF | ✅ —— 包里同时带链接名与 SONAME |
240-
| `kind = "shared"` on PE / Mach-O | ❌ 拒绝 —— 导入库与 install-name 尚未建模 |
240+
| `kind = "shared"` on PE / MinGW(`*-windows-gnu`) | ✅ —— 包里同时带 `.dll` **和它的导入库** |
241+
| `kind = "shared"` on Mach-O(`*-macos`) | ✅ —— install name 是 `@rpath/<file>`,`.dylib` 可重定位 |
242+
| `kind = "shared"` on PE / MSVC(`*-windows-msvc`) | ❌ 拒绝 —— 见下 |
241243
| `kind = "shared"` on `*-musl` | ❌ musl target 是静态链接的 |
242244
| 发布预编译 BMI | ❌ 未尝试;BMI 与编译器构建逐位绑定 |
243245
| 把依赖打包进去 | ❌ 改为声明依赖(见上) |
246+
|**原生 `cl.exe`** 消费这种包 | ❌ 见下 |
247+
248+
### MSVC 为什么拒绝 `kind = "shared"`
249+
250+
**不是链接器的问题** —— `link /DLL /IMPLIB:` 本来就能用。是**符号导出**:
251+
MSVC 在没有 `__declspec(dllexport)`、也没有 `.def` 列出符号时,DLL **什么都不导出**,
252+
于是导入库是空的,每个消费者都会拿到一堆 unresolved externals,而那些符号
253+
明明就在对象文件里 —— 报错点离病因很远。mcpp 选择拒绝,而不是产出这种诊断:
254+
255+
```
256+
target 'mathkit': kind = "shared" is not supported for the MSVC ABI (x86_64-windows-msvc).
257+
...
258+
Use kind = "lib" for this target, or build it for *-windows-gnu (MinGW),
259+
where the linker auto-exports.
260+
```
261+
262+
MinGW 的链接器会自动导出,这就是 `*-windows-gnu` 支持而 `*-windows-msvc` 不支持的
263+
全部原因。要补齐它需要生成 `.def`(对对象做一次符号扫描)—— 那是一个构建图节点,
264+
不是一个 flag。
265+
266+
### 包里的链接 flag 是 GNU 拼写
267+
268+
生成的 manifest 用这种方式选腿:
269+
270+
```toml
271+
[target.'cfg(all(arch = "x86_64", os = "windows", env = "msvc"))'.build]
272+
ldflags = ["-Llib/x86_64-windows-msvc", "-lmathkit"]
273+
```
274+
275+
mcpp 用到的每个 driver 都吃这一套 —— 包括 Windows 上默认的、面向 MSVC ABI 的
276+
clang。**原生 `cl.exe` 不吃**:它不认 `-L`。所以固定了
277+
`[toolchain] windows = "msvc@system"` 的消费者目前链不上打包库。
278+
279+
**改成直接写文件路径也不行**(`lib/<triple>/mathkit.lib` 才是每个 driver 都吃的
280+
拼写):ninja 执行链接命令时 cwd 是**输出目录**,而只有 include 家族前缀
281+
(`-I``-L` …)会被 `normalize_include_flags` 相对包根绝对化 —— 没有前缀的 token
282+
就会到错误的地方去找:`ld: cannot find lib/x86_64-windows-gnu/libmathkit.a`
283+
而 manifest 里写绝对路径就不再可重定位了。要补齐它,需要让条件通道能承载
284+
`link_library_dirs` / `libraries` —— mcpp 已经能按方言渲染它们,只是只在顶层读。
244285

245286
### 实现分区
246287

0 commit comments

Comments
 (0)