Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,64 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.18.2] — 2026-08-18

### 新增

- **`kind = "shared"` 在 MSVC ABI 上可用了 —— mcpp 自己生成 `.def`。**

MSVC 在没有 `__declspec(dllexport)`、也没有 `.def` 时,DLL 什么都不导出;
导入库为空,消费者拿到一堆 unresolved externals,而符号明明就在对象里。
**拒绝的理由成立,结论不成立** —— CMake 的 `WINDOWS_EXPORT_ALL_SYMBOLS` 自 3.4
起就是这么做的,而且它的 `bindexplib` **直接读 COFF、不依赖 dumpbin**。这一点是
决定性的:`dumpbin` 只在 Visual Studio 开发者环境里,而 mcpp 在 Windows 的默认
工具链是 clang,`mcpp build` 根本不在那个环境里。

`mcpp.build.coff_exports` 是那个读取器,写成**对字节的纯函数**,于是它能在任何
平台上被测试 —— 16 条单测逐字节构造对象(那是唯一能按需改变存储类的办法),
外加一个**真实的 mingw-cross 对象**:只喂自己测试输出的读取器,只会与自己一致。
超过 **65535** 个可导出符号时**拒绝而不截断**:被截断的导出表能干净链完,
然后在「恰好需要那个掉出去的符号」的消费者那里失败。

**标注优先。** 对象里已带 `/EXPORT:` 指令(即 `__declspec(dllexport)` 的产物)时,
mcpp 让开、不生成任何东西 —— 再加一份列表会把同名符号导出两次(`LNK4197`),
更糟的是把其余所有符号也导出,用「全部」替换掉作者选定的公开面。
**这件事靠检测而不是配置**:为「我标注过了」加一个 manifest 键,就是给对象已经
说过的事再加一个说法,而两者可以不一致。

仍有两条限制是工具消不掉的(与 CMake 记录的同两条):导出的**数据**在消费端仍需
`__declspec(dllimport)`;**vtable 被引用的类**要整类标注。两者都写进了 docs/12。

- **打包库可以被原生 `cl.exe` 消费。** 生成的 manifest 现在同时带方言中立的
`[target.<pred>.runtime]`(`link_library_dirs` / `libraries`),mcpp 会按 target
渲染成 `/LIBPATH:` + `<n>.lib` 或 `-L` + `-l<n>`。**两种拼写都带**:旧版 mcpp
只读 `ldflags` 并静默忽略新段,去掉它会让旧客户端一个 flag 都拿不到;
而新版读到中立形式时**忽略**同腿的 `ldflags` 而不是叠加 —— 叠加会把 `-L`
送回 `cl` 的命令行。

### 修复

- **`mcpp pack` 现在跟着工程的 `module_extensions` 走,不需要再配一次。**

实测:接口是 `.ixx` 的库打出来的包是**静默错的** ——



两半都错且都不出声:没有接口,消费者 `import` 不了;而**空的发布集合正是打包器
判定「C 表面」的依据**,于是这个包同时不再约束 C++ ABI,兼容性闸门也不再检查
编译器与标准库。真因是 lib root 约定把 `.cppm` 写死了。现在按**已声明的每个扩展名**
各给一个候选并取存在的那个;生成的 manifest 也会**自己声明** `module_extensions`
—— 从**发布的文件**算出来,因此不可能与 `sources` 不一致。

- **`sources` 命中却分类不出角色的文件,现在当场拒绝。**

未声明的 `.ixx` 过去会编译出一个**没人链接的对象**,报错是
`undefined reference to mk::answer@mathkit()` —— 既不点名扩展名也不点名那条键。
根因是「这是不是模块接口」有**两个答题者**:扫描器读到 `export module` 记下
`provides`(所以边上挂了 `bmi_out`),而分类器说 `Other`,链接集合只读后者。

⚠️ 这是**行为变化**:`sources` 里混进 `.md` / `.txt` 的工程会开始报错。

## [2026.8.18.1] — 2026-08-18

### 新增
Expand Down
80 changes: 59 additions & 21 deletions docs/12-binary-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,32 +264,46 @@ package published to a mixed-version audience.
| `kind = "shared"` on Linux/ELF | ✅ — the package carries both the link name and the SONAME |
| `kind = "shared"` on PE / MinGW (`*-windows-gnu`) | ✅ — the package carries the `.dll` **and** its import library |
| `kind = "shared"` on Mach-O (`*-macos`) | ✅ — install name is `@rpath/<file>`, so the `.dylib` relocates |
| `kind = "shared"` on PE / MSVC (`*-windows-msvc`) | ❌ refused — see below |
| `kind = "shared"` on PE / MSVC (`*-windows-msvc`) | ✅ — mcpp generates the `.def`; see below |
| `kind = "shared"` on `*-musl` | ❌ a musl target links statically |
| shipping prebuilt BMIs | ❌ not attempted; BMIs are compiler-build-exact |
| bundling dependencies into the package | ❌ declare them instead (above) |
| consuming a package with **native `cl.exe`** | ❌ see below |

### Why MSVC refuses `kind = "shared"`
### Exports on the MSVC ABI

Not the linker — `link /DLL /IMPLIB:` works. **Symbol export.** MSVC exports
nothing from a DLL unless the source says `__declspec(dllexport)` or a `.def`
file lists the symbols, so the import library comes out empty and every consumer
fails with unresolved externals naming symbols that are plainly in the object
files. mcpp refuses rather than produce a diagnostic that points nowhere near its
cause:
MSVC exports nothing from a DLL unless the source says `__declspec(dllexport)` or
a `.def` file lists the symbols. Without either, the import library comes out
empty and every consumer fails with unresolved externals for symbols that are
plainly in the object files. MinGW's linker auto-exports and hides this
entirely; lld-link's MSVC flavour does not, deliberately, because PE caps
exports at 65535.

```
target 'mathkit': kind = "shared" is not supported for the MSVC ABI (x86_64-windows-msvc).
MSVC exports nothing from a DLL unless the source says `__declspec(dllexport)`
...
Use kind = "lib" for this target, or build it for *-windows-gnu (MinGW),
where the linker auto-exports.
```
mcpp generates the `.def` from the objects, which is what CMake's
`WINDOWS_EXPORT_ALL_SYMBOLS` has done since 3.4. It is a build-graph node whose
inputs are the same objects the link consumes, so the exported surface cannot
drift from what was compiled, and it reads COFF directly rather than shelling out
to `dumpbin` — that tool lives in a Visual Studio developer environment, and
mcpp's default Windows toolchain is clang.

**Two limits survive that no tool can remove**, and they are the same two CMake
documents for the same mechanism:

| | |
|---|---|
| exported **data** | the consumer still needs `__declspec(dllimport)` on its declaration; without it the linker reads a call thunk instead of the value |
| a class whose **vtable** is referenced | the whole class must be marked, e.g. in a delegating constructor of a class with virtual functions |

MinGW's linker auto-exports, which is why `*-windows-gnu` is supported and
`*-windows-msvc` is not. Closing this needs a generated `.def` — a symbol scan
over the objects — which is a build-graph node, not a flag.
Both are answered by annotation, and **annotation wins**: an object that already
carries `/EXPORT:` directives — which is what `__declspec(dllexport)` emits —
makes mcpp stand down and generate nothing. Adding a list on top would export the
same names twice (`LNK4197`) and export everything else besides, replacing a
chosen public surface with all of it. Nothing is configured for this; the objects
say it.

Past 65535 exportable symbols mcpp refuses rather than truncating. A truncated
export table links cleanly and then fails at whichever consumer needed the symbol
that fell off the end.

### A package's link flags are GNU-spelled

Expand All @@ -301,9 +315,33 @@ ldflags = ["-Llib/x86_64-windows-msvc", "-lmathkit"]
```

Every driver mcpp uses accepts that — including clang on the MSVC ABI, which is
Windows' default here. **Native `cl.exe` does not**: it rejects `-L`. So a
consumer that pins `[toolchain] windows = "msvc@system"` cannot link a packaged
library today.
Windows' default here. **Native `cl.exe` does not**: it rejects `-L`. So the
package carries the same statement a second time, without a dialect:

```toml
[target.'cfg(all(arch = "x86_64", os = "windows", env = "msvc"))'.runtime]
link_library_dirs = ["lib/x86_64-windows-msvc"]
libraries = ["mathkit"]
```

mcpp renders those as `/LIBPATH:` + `<name>.lib` or `-L` + `-l<name>` from the
target, so a `cl.exe` consumer links the package. They are not new vocabulary —
`[runtime]` has had both keys at the top level all along; this makes them
per-target.

**Both spellings are emitted, and a newer mcpp drops the leg's library
references rather than adding to them.** An older mcpp reads only the `ldflags`
and silently ignores the `runtime` block, so dropping the `ldflags` would leave
every older client with no link line at all; adding both would put `-L` back on
the `cl` command line, which is the thing being avoided.

One leg is deliberately left out of this: a **PE/MinGW shared** leg links with
`-L… -Wl,-Bdynamic -lmathkit`, and `-Wl,-Bdynamic` only works immediately before
the `-l` it enables — mcpp gives PE executables `-static`, which otherwise leaves
the linker in static-only mode where it refuses an import library. The neutral
form cannot say "switch link mode first", so that leg keeps the spelling that
works. It costs nothing: a PE/MinGW leg is not an MSVC-ABI leg, and `cl.exe`
never reads it.

Naming the file by path instead (`lib/<triple>/mathkit.lib`) is the spelling
every driver takes, and it does not work either: ninja runs link commands with
Expand Down
64 changes: 47 additions & 17 deletions docs/zh/12-binary-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,29 +239,38 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"]
| `kind = "shared"` on Linux/ELF | ✅ —— 包里同时带链接名与 SONAME |
| `kind = "shared"` on PE / MinGW(`*-windows-gnu`) | ✅ —— 包里同时带 `.dll` **和它的导入库** |
| `kind = "shared"` on Mach-O(`*-macos`) | ✅ —— install name 是 `@rpath/<file>`,`.dylib` 可重定位 |
| `kind = "shared"` on PE / MSVC(`*-windows-msvc`) | ❌ 拒绝 —— 见下 |
| `kind = "shared"` on PE / MSVC(`*-windows-msvc`) | —— mcpp 生成 `.def`;见下 |
| `kind = "shared"` on `*-musl` | ❌ musl target 是静态链接的 |
| 发布预编译 BMI | ❌ 未尝试;BMI 与编译器构建逐位绑定 |
| 把依赖打包进去 | ❌ 改为声明依赖(见上) |
| 用**原生 `cl.exe`** 消费这种包 | ❌ 见下 |

### MSVC 拒绝 `kind = "shared"` 的原因
### MSVC ABI 上的符号导出

**不是链接器的问题** —— `link /DLL /IMPLIB:` 本来就能用。是**符号导出**:
MSVC 在没有 `__declspec(dllexport)`、也没有 `.def` 列出符号时,DLL **什么都不导出**,
于是导入库是空的,每个消费者都会拿到一堆 unresolved externals,而那些符号
明明就在对象文件里 —— 报错点离病因很远。mcpp 选择拒绝,而不是产出这种诊断:
MSVC 在源码没有 `__declspec(dllexport)`、也没有 `.def` 列出符号时,DLL 什么都不导出。
两者皆无时导入库为空,每个消费者都会拿到一堆 unresolved externals,而那些符号
明明就在对象文件里。MinGW 的链接器会自动导出,把这个问题整个遮住;lld-link 的
MSVC 形态刻意不这么做,因为 PE 的导出上限是 65535。

```
target 'mathkit': kind = "shared" is not supported for the MSVC ABI (x86_64-windows-msvc).
...
Use kind = "lib" for this target, or build it for *-windows-gnu (MinGW),
where the linker auto-exports.
```
mcpp 从对象生成 `.def` —— 这正是 CMake 的 `WINDOWS_EXPORT_ALL_SYMBOLS` 自 3.4 起
在做的事。它是一个构建图节点,输入就是链接所消费的那批对象,因此导出面不会与
「实际编译了什么」发生漂移;而且它**直接读 COFF**,不调 `dumpbin` —— 那个工具在
Visual Studio 开发者环境里才有,而 mcpp 在 Windows 上的默认工具链是 clang。

**有两条限制是任何工具都消不掉的**,与 CMake 为同一机制记录的是同两条:

| | |
|---|---|
| 导出的**数据** | 消费者的声明仍需 `__declspec(dllimport)`;否则链接器读到的是调用桩而不是值 |
| **vtable** 被引用的类 | 整个类都要标注,例如带虚函数的类的委托构造函数 |

两者都靠标注解决,而且**标注优先**:对象里若已带 `/EXPORT:` 指令(那正是
`__declspec(dllexport)` 产生的),mcpp 就让开,不生成任何东西。在其之上再加一份
列表会把同名符号导出两次(`LNK4197`),更糟的是把其余所有符号也一并导出 ——
用「全部」替换掉作者选定的公开面。这件事没有任何开关:对象自己说了算。

MinGW 的链接器会自动导出,这就是 `*-windows-gnu` 支持而 `*-windows-msvc` 不支持的
全部原因。要补齐它需要生成 `.def`(对对象做一次符号扫描)—— 那是一个构建图节点,
不是一个 flag。
可导出符号超过 65535 时,mcpp 拒绝而不是截断。被截断的导出表能干净地链接完成,
随后在「恰好需要那个掉出去的符号」的消费者那里失败。

### 包里的链接 flag 是 GNU 拼写

Expand All @@ -273,8 +282,29 @@ ldflags = ["-Llib/x86_64-windows-msvc", "-lmathkit"]
```

mcpp 用到的每个 driver 都吃这一套 —— 包括 Windows 上默认的、面向 MSVC ABI 的
clang。**原生 `cl.exe` 不吃**:它不认 `-L`。所以固定了
`[toolchain] windows = "msvc@system"` 的消费者目前链不上打包库。
clang。**原生 `cl.exe` 不吃**:它不认 `-L`。所以包里会把同一句话再写一遍,
这一遍不带方言:

```toml
[target.'cfg(all(arch = "x86_64", os = "windows", env = "msvc"))'.runtime]
link_library_dirs = ["lib/x86_64-windows-msvc"]
libraries = ["mathkit"]
```

mcpp 会按 target 把它们渲染成 `/LIBPATH:` + `<name>.lib` 或 `-L` + `-l<name>`,
于是 `cl.exe` 的消费者也能链上。这两个键不是新词表 —— `[runtime]` 顶层一直就有,
这里只是让它们可以按 target 给。

**两种拼写都会写出来,而新版 mcpp 读到中立形式时会丢掉同一条腿的库引用,
而不是叠加。** 旧版 mcpp 只读 `ldflags` 并静默忽略 `runtime` 段,所以去掉
`ldflags` 会让所有旧客户端一个链接 flag 都拿不到;而两者都应用又会把 `-L` 送回
`cl` 的命令行 —— 那正是要避免的事。

有一条腿被刻意排除在外:**PE/MinGW 的动态库腿**链接行是
`-L… -Wl,-Bdynamic -lmathkit`,而 `-Wl,-Bdynamic` 只有**紧邻它所启用的那个 `-l`**
时才有效 —— mcpp 给 PE 可执行文件加 `-static`,否则链接器停在纯静态模式并拒绝
导入库。中立形式没法表达「先切换链接模式」,所以那条腿保留能用的拼写。
这不付出任何代价:PE/MinGW 的腿不是 MSVC ABI 的腿,`cl.exe` 永远读不到它。

**改成直接写文件路径也不行**(`lib/<triple>/mathkit.lib` 才是每个 driver 都吃的
拼写):ninja 执行链接命令时 cwd 是**输出目录**,而只有 include 家族前缀
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "2026.8.18.1"
version = "2026.8.18.2"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
Loading
Loading