Skip to content

Commit d1bdf61

Browse files
committed
docs(b3): resolve both open questions with a traced consumer chain
Q1 — windows-gnu static lib foo.lib -> libfoo.a: DO IT IN THE SAME PR. The worry was that it changes a host build's output name. Tracing every consumer shows the blast radius is empty: - mcpp package deps link at OBJECT level (plan.cppm:836 splices dependency .o files into lu.objects). A static library is never produced OR read as part of an internal dependency edge. - external prebuilt libs come through free-form ldflags; the name is written in the package descriptor, mcpp never spells it. - [runtime] library_dirs is a directory, not a name; the Windows side only filters on the .dll extension for runtime deployment. - fingerprint.cppm carries no artifact name but does carry MCPP_VERSION, so any bump already rotates target/<triple>/<fp>/. No mixed state, no cache migration, no advice needed. So the only consumer is whoever takes the artifact outside mcpp — and the name they get today is wrong: mingw's ar emits a GNU archive named foo.lib, claiming an MSVC convention it does not satisfy. That is a pre-existing correctness bug, not a nice-to-have, and shipping the (os, env) rule half-way would leave a state harder to explain than the bug. Q2 — PE import libs: DO NOT MODEL THEM YET. Draw the boundary instead. All five shared-library e2e tests declare , and is only added on the Linux branch of run_all.sh — Darwin gets , Windows gets . Shared libraries have therefore never been verified end to end on PE *or* Mach-O. This reframes the question: it is not a missing feature, it is a path that was never walked while the code carries branches that look like it was. Those branches are speculation: mingw tolerates linking a .dll directly, MSVC's link.exe cannot — and the branch keys on , a host constant, so it points the wrong way under cross-compilation anyway. Recommends rejecting SharedLibrary on non-ELF targets with a clear error before attempting to support it. An untested branch that also refuses to say no is the hardest kind of debt — it can neither be trusted nor deleted, because nobody knows who depends on it. Same shape as the offline-first code that a TTL gate had quietly made unreachable. Splits the work into three PRs accordingly; import lib support gets its own design doc, gated on shared-library coverage existing for PE and Mach-O first.
1 parent 72c14eb commit d1bdf61

1 file changed

Lines changed: 137 additions & 10 deletions

File tree

.agents/docs/2026-08-03-b3-target-aware-artifact-naming.md

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -247,25 +247,152 @@ GCC 决定为 `.exe`,修复只是让 **ninja 的声明** 追上事实;Windows→
247247

248248
## 8. 分期
249249

250+
**三个 PR,不要合并**(依据见 §9):
251+
252+
#### PR-1 — B3 主体(本文档的 §5)
253+
250254
单 PR,提交分层(与 B2 同款,理由见 [[windows-host-linux-cross-canadian]]):
251255

252256
| commit | 内容 |
253257
|---|---|
254258
| 1 | `artifact_naming()` + 判据 1/2 的断言,**实现为返回 host 常量的桩** ⇒ 测试红 |
255-
| 2 | 实现按 (os, env) 求值 ⇒ 测试绿 |
259+
| 2 | 实现按 (os, env) 求值 ⇒ 测试绿(含 `windows-gnu``libfoo.a`,§9.1) |
256260
| 3 | `target_output` / `runtime_aliases_for_target` 接受 `ArtifactNaming` |
257261
| 4 | `shared_library_link_flags` 按 target 求值 |
258262
| 5 | e2e:判据 3 的「不重链」断言 |
259263

264+
CHANGELOG 必须写出 Windows + mingw 静态库改名这条**对外可见的行为变更**
265+
266+
#### PR-2 — 非 ELF 目标上的共享库明确拒绝(§9.2 路线 B)
267+
268+
独立小 PR。`make_plan` 入口检查:target 非 ELF 且存在 `SharedLibrary` 目标 ⇒ 返回明确错误,
269+
指向追踪 issue。附一条 e2e:「PE 目标上声明 SharedLibrary 得到可读的 mcpp 错误,
270+
而不是一个链接不上的产物」。
271+
272+
**先于 PR-3,也可以先于 PR-1** —— 它不依赖命名改动,且立刻消除一类未定义行为。
273+
274+
#### PR-3 — import lib 完整支持(§9.2 路线 A)
275+
276+
**另开设计文档。** 前置条件是先补齐 PE 与 Mach-O 的共享库 e2e 覆盖,否则又是一批
277+
无验证代码。不要在没有覆盖的前提下加产物边。
278+
279+
---
280+
281+
## 9. 两个未决问题的深度分析(2026-08-03 补,基于代码实测)
282+
283+
---
284+
285+
### 9.1 `windows-gnu` 静态库 `foo.lib``libfoo.a`:**同期做,影响面实测为空**
286+
287+
原本的顾虑是「它会改变 host 构建的产物名」。把消费链逐段查完之后,这个顾虑站不住。
288+
289+
#### 谁真正消费一个静态库的名字
290+
291+
| 消费者 | 机制 | 受改名影响? |
292+
|---|---|---|
293+
| **mcpp 包间依赖** | **object 级内联** —— `plan.cppm:836` 把依赖包的 `.o` 直接塞进 `lu.objects`,**根本不产生也不读取 `.a`** | ❌ 无影响 |
294+
| **外部预编译库**(compat.* 等) | 自由形式 `ldflags`(`types.cppm:173`),库名由包描述符写死,mcpp 不参与命名 | ❌ 无影响 |
295+
| **`[runtime] library_dirs`** |**目录**而非库名(`types.cppm:321`);Windows 侧只按 `.dll` 扩展名做运行期部署(`plan.cppm:418-434`) | ❌ 无影响 |
296+
| **ninja 输出声明** | 来自 `target_output()` 本身 | ✅ 同步改,这正是修复 |
297+
| **最终用户 / 外部构建系统** | 直接引用产物路径 | ⚠️ 唯一真实影响面 |
298+
299+
**关键结构性事实:mcpp 的静态库不是内部链接单元。** 包依赖走 object 内联,所以 `Library`
300+
目标产出的 `.a`/`.lib` 只有一个消费者 —— **把产物拿去给 mcpp 之外的世界用的人**
301+
这让改名从「牵一发动全身」降级为「改一个对外文件名」。
302+
303+
#### 缓存与 fingerprint:影响为零(实测)
304+
305+
`toolchain/fingerprint.cppm:94-103` 的 8 个 part 里**没有产物名**,但**`MCPP_VERSION`**
306+
也就是说任何版本 bump 都会换一个 `target/<triple>/<fp>/` 目录 —— 改名后不会出现
307+
「旧目录里躺着旧名产物、新逻辑找不到」的混合态。**不需要任何缓存迁移或 `cache clean` 提示。**
308+
309+
#### 为什么必须同期做(架构角度)
310+
311+
把「命名由 (os, env) 决定」做成半截,会留下一个**比现状更难解释的状态**:
312+
`exe_suffix` 按 target 走了,`static_lib_ext` 还按 host —— 下一个读代码的人无从判断
313+
哪个常量能信。§4 那张表的价值恰恰在于它是**一条完整的规则**,而不是四个独立特例。
314+
315+
而且这不是「顺带做的优化」,它是**存量正确性缺陷**:今天在 Windows 主机上用 mingw 工具链
316+
构建静态库,产物叫 `foo.lib`,而 mingw 的 `ar` 产出的是 GNU archive —— 一个 MSVC 拿不去用、
317+
名字又冒充 MSVC 约定的文件。**这个错误与交叉编译无关,今天就在发生。**
318+
319+
#### 结论
320+
321+
**同期做。** 唯一需要的额外动作是在 CHANGELOG 明确写出这条行为变更(Windows + mingw
322+
静态库产物改名),因为它对外可见 —— 但它影响的是一个**今天就是错的**名字。
323+
324+
---
325+
326+
### 9.2 PE import lib:**先别建模,先把边界画出来**
327+
328+
调研到一个改变问题性质的事实。
329+
330+
#### 共享库在 PE 和 Mach-O 上是零验证覆盖
331+
332+
5 个共享库 e2e **全部**声明 `# requires: elf`:
333+
334+
```
335+
tests/e2e/08_shared_library.sh:2: # requires: elf
336+
tests/e2e/55_dependency_shared_artifact.sh:2:# requires: elf
337+
tests/e2e/56_transitive_shared_artifact.sh:2:# requires: elf
338+
tests/e2e/57_static_dep_shared_artifact.sh:2:# requires: elf
339+
tests/e2e/64_shared_soname_runtime_alias.sh:2:# requires: elf
340+
```
341+
342+
`elf` 这个 capability **只有 Linux 分支会加**(`run_all.sh:46`);
343+
Darwin 加的是 `macos`,Windows 加的是 `windows`
344+
345+
**所以 mcpp 的共享库支持是 Linux-only 的既成事实 —— 不只是 PE,连 macOS 的 Mach-O
346+
也从未被端到端验证过。**
347+
348+
这把问题从「缺一个 import lib 功能」重新定义为:**共享库这条路在非 ELF 平台上从来没走通过,
349+
而代码里却有看起来能走的分支。**
350+
351+
#### 那些分支是未验证的推测代码
352+
353+
`plan.cppm:236-243`:
354+
355+
```cpp
356+
if constexpr (mcpp::platform::is_windows) {
357+
flags.push_back(target_output(t).generic_string()); // 直接塞 foo.dll 的路径
358+
} else {
359+
flags.push_back("-L" + …); flags.push_back("-l" + t.name);
360+
```
361+
362+
- **mingw**:链接器确实容忍直接链 `.dll`(ld 会自动生成 import stub),所以**可能**能工作 ——
363+
但没有任何测试证明过。
364+
- **MSVC**:`link.exe` **无法**链接 `.dll`,它需要 `.lib` import library。这条路径必然失败。
365+
- 而且这个分支按 **host** 求值(`is_windows`),交叉时连方向都是错的 —— 与 B3 主体同病。
366+
367+
#### 长期架构:三条路,建议第 2 条
368+
369+
| 路线 | 内容 | 评价 |
370+
|---|---|---|
371+
| **A. 完整建模 import lib** | `ArtifactNaming` 增加 import lib 产物,ninja 加一条边,链接消费者改用 import lib;mingw 用 `-Wl,--out-implib`,MSVC 用 `link.exe` 自动产出的 `.lib` | 正确但**昂贵**:要同时补 PE 与 Mach-O 的共享库 e2e,否则又是一批无覆盖代码。**不该和 B3 混在一起。** |
372+
| **B. 先画边界:非 ELF 目标上共享库明确拒绝** ✅ | `SharedLibrary` 目标在 PE / Mach-O 目标上返回一条清晰的 mcpp 错误,指明「共享库目前仅支持 ELF 目标,追踪 issue: …」 | **静默产出不可用的东西,比明确拒绝坏得多。**成本极低,立刻消除一整类未定义行为 |
373+
| C. 维持现状 | 保留未验证分支 | ❌ 最差:代码看起来支持,实际未知,用户踩坑时离根因隔三层 |
374+
375+
**推荐 B,而且它应该先于 A。** 理由与 [[index-refresh-resolution-driven]] 里那条
376+
「offline-first 被 TTL 门压成不可达代码」同源:**一段没有测试覆盖、又没有明确拒绝的分支,
377+
是最难清理的技术债** —— 它既不能被信任,又不能被删除,因为没人知道谁在依赖它。
378+
379+
先把边界写死,`ArtifactNaming::sharedNeedsImportLib` 这个字段就有了明确语义:
380+
**它当前的唯一用途是驱动那条拒绝**,而不是假装支持。等真要做 A 时,再把它变成产物声明。
381+
382+
#### 与 B3 主体的关系
383+
384+
B3 主体(§5)**不依赖**这个决定:`target_output()` 对 `SharedLibrary` 仍按 (os, env) 给出
385+
正确的 `foo.dll` / `libfoo.so` / `libfoo.dylib`,该怎么命名怎么命名。路线 B 只是在
386+
`make_plan` 入口多一条前置检查。
387+
388+
**建议拆分:** B3 主体一个 PR(§8 的五段);路线 B 一个独立小 PR(带一条「PE 目标上声明
389+
SharedLibrary 得到明确错误」的 e2e);路线 A 另开设计文档。
390+
260391
---
261392
262-
## 9. 未决
393+
### 9.3 汇总:两个问题的处置
263394
264-
1. **`windows-gnu` 静态库从 `foo.lib` 改名为 `libfoo.a`** 是唯一一处会改变 **host 构建**
265-
产物名的改动(Windows 主机 + mingw 工具链)。它是修正存量缺陷,但确实是行为变更 ——
266-
是否要同期做,还是拆一个独立 PR?我倾向同期,因为把「命名由 (os,env) 决定」做成半截
267-
反而更难解释。
268-
2. **PE 的 import lib(`libfoo.dll.a` / `foo.lib`)目前 mcpp 完全没建模**。共享库交叉到
269-
Windows 时消费者拿什么链接,现在靠 `shared_library_link_flags` 直接塞产物路径蒙混过去。
270-
本方案的 `sharedNeedsImportLib` 只是给它留了个位置,**真正的 import lib 支持不在本期范围**,
271-
建议另开。
395+
| # | 问题 | 决定 | 依据 |
396+
|---|---|---|---|
397+
| 1 | `foo.lib` → `libfoo.a` | **同期做** | 消费链实测无影响(包依赖走 object 内联、外部库走 ldflags、fingerprint 不含产物名);且它是**存量正确性缺陷**,不是顺带优化 |
398+
| 2 | PE import lib | **本期不建模,改为明确拒绝** | 共享库在 PE/Mach-O 上**零 e2e 覆盖**,现有分支是未验证推测;静默产出不可用产物比明确报错坏得多。完整支持另开设计 |

0 commit comments

Comments
 (0)