Skip to content

feat: [resources] 与版本身份 —— 两处「模型比生态少一层」(#365, #363) - #369

Merged
Sunrisepeak merged 8 commits into
mainfrom
feat/windows-resources-and-version-identity
Aug 7, 2026
Merged

feat: [resources] 与版本身份 —— 两处「模型比生态少一层」(#365, #363)#369
Sunrisepeak merged 8 commits into
mainfrom
feat/windows-resources-and-version-identity

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 6, 2026

Copy link
Copy Markdown
Member

实施 .agents/docs/2026-08-07-windows-resources-and-version-identity-design.md。两个 issue 领域无关,失效形状同一条:生态已经产出的东西,mcpp 的模型表达不了,于是走到一条「不报错但结果是错的」路径上。

Closes #365, closes #363(第 3 条 lock「权威化」除外,见下)。


#365 — Windows 资源

⚠️ issue 结尾那条归因是错的,已实测证伪

issue 请作者注意「llvm-rc 生成的 VERSIONINFO 不被 GetFileVersionInfo 解析」,并建议 mcpp 侧特殊处理或换编译器。不是 llvm-rc 的 bug。

VS_VERSION_INFOverrsrc.h(随 windows.h)的宏,值为 1。没有它时,rc 语法允许标识符出现在资源位置,于是资源被存成字符串名 "VS_VERSION_INFO";而 GetFileVersionInfo 查的是序号 1。资源类型两种情况都是 RT_VERSION(16)——这正是为什么 llvm-readobj 看着一切正常。

用 issue 里那份 .rc 实测(llvm-rc 22.1.8,.res 偏移 40 处的 type+name):

1 VERSIONINFO                 ff ff 10 00  ff ff 01 00     (480 字节)  ✅
VS_VERSION_INFO VERSIONINFO   ff ff 10 00  56 00 53 00 …   (508 字节)  ❌ UTF-16 "VS_VERSION_INFO"

llvm-readobj --coff-resources 同样能区分,而且更可读:Name: (ID 1) vs Name: VS_VERSION_INFO

⇒ mcpp 不需要绕任何工具 bug;合成脚本写字面 1,构造性正确。顺带:「exe 里有 VERSIONINFO 资源」这个断言在失效场景下恒为真,是假绿,所以 e2e 断言的是名字而不是类型。

用法

[resources]
icon = "assets/app.ico"

FILEVERSION / ProductName / FileDescription / CompanyName / LegalCopyright[package] 取默认值,资源脚本由 mcpp 生成。自写 .rcfiles = [...],mcpp 编译并跟踪它(改图标会重链——原来的 ldflags workaround 得到的是 ninja: no work to do)。

三条规则:

  • 只有 PE 目标消费。Linux/macOS 上「不适用」——不是降级、不是带警告地跳过:没有消费者,构建逐字节不变,也不说话。所以不需要(也不能)写 cfg(windows)[target.'cfg(…)'.build] 的键表是封闭的,且「条件通道只载 BuildInputs」是明文设计立场。
  • 节名不叫 [windows]:图标作为概念不是 Windows 专有的,只有格式与嵌入机制是;将来 macOS .icns 扩同一节。代价是 manifest 里看不出平台,所以 PE 下生效时打一行 Embedding status 作补偿。
  • 声明了却不存在的文件是硬错误(对 issue 第 3 条的有意偏离,见下)。

role = "object":补齐角色表

BuildAction::Role 原本三格接在「编译输入 / 无 / 链接输出」,缺的正是链接输入。后果不是理论上的:预编译对象只能塞进 [build].ldflags,而那是链接命令里的一串字符、不是图里的文件。可选 .target("name"),省略 = 声明包的全部镜像;未知名字报错。


#363 — 版本身份

resolve_semver 一直把索引的字面版本键读到手里,然后 return parsed[i].str()——从解析出的数字重造一个地址。渲染器复现不了的东西就成了不存在的地址。下表每一行都是真实 xim-pkgindex 今天在发的形状:

包 / 键 旧行为
compat.imgui 1.92.8-docking 截断成 1.92.8,与非 docking 那个塌成同一个可比较版本(两个不同 tarball、不同 sha256)
jdk-corretto 25.0.4.7.1 五段截断成 25.0.4.7 —— 索引里没有这个键
jdk-temurin 25.0.4 = { ref = "25.0.4+7" } 别名被当成版本,与它自己的目标构成一次平局
khistory pre-v0.0.5(唯一的发布) 静默跳过,然后报 no valid versions in index——把责任推给一个发布得好好的包

现在字面键与序一起传递,version_req 只负责排序。连带:

  • 预发布按 SemVer 排序;范围按 npm/Cargo 规则看不见预发布,除非约束自己在同一数值元组上带预发布。同一条规则顺带修掉 ^1.2.3 会漏进 2.0.0-alpha
  • 数值段改为任意长度(固定长度本身是缺陷,加到第五段只是把下一次推迟)。
  • 别名条目不再是范围候选;精确寻址不变(= latest 照旧)。
  • 不可排序键成为一等公民的一类:只参与精确匹配,范围下给指名的错误 + 可粘贴的 pin 行。
  • 真平局(只差 build metadata)硬错;但 =1.0.0+b 这种精确形式直接按字面命中,不算歧义。

lock

记的一直是约束本身version = "^1.92.8"),Compiling compat.imgui v^1.92.8 也一样。两者读的都是 m->dependencies(未解析的输入、且只有直接依赖),而解析结果 ResolvedRecord 早就覆盖整张图——修法是把两个消费者都指过去,不是补第三处回写。

lock 本批不「权威化」(决策):它写真实版本但仍不参与解析。所以文件头自己声明了这一点,并有 e2e 断言——改成权威时那条断言会红,提醒同批删掉。mcpp updaterequested 字段单开一批。


实施中撞出来的(设计里没有)

  1. ⚠️ 非 ASCII 元数据会让 rc 编译器拒绝整个脚本Non-ASCII 8-bit codepoint can't be interpreted in the current codepage。触发它的是 mcpp 自己生成的 copyright 里一个 em dash;而 [package].description 是用户文本,中文项目必然命中 ⇒ 必须传 UTF-8 codepage(/C 65001 / --codepage=65001),否则中文描述的项目根本构建不了。同时把生成文本收敛成纯 ASCII(单测断言)——两道,防的是两件事。
  2. 五段键在真实索引里存在jdk-corretto),不是假想。
  3. .res 资源头偏移是 40 不是 32(32 字节全零头 + dataSize/headerSize)。字节断言必须核对偏移,否则恒真/恒假。
  4. ⚠️ .rc 穿不过工程级 fast path(Windows CI 抓到的,本机没有)。 一次只改 res/app.rc 的构建报 Finished dev in 0.15ssources_newer_than 只扫 src/**/* 的 C++ 扩展名。这不只是「警告没打」——.rc 的 implicit input 集合来自扫描它、扫描在 prepare 里,所以往脚本加一行 #include "ids.h",那个头文件永远不会被跟踪。与 build.mcppcodegen 类库的使用者体验:依赖声明从 4 条降到 1 条、proto 支持通配符(tools 传播 + 目录级 rerun) #359 的 glob 输入是同一形状的第三次;前两次的注释是举例不是判据,所以没能让第三次被预见。已把判据写进设计文档 §E。只扫 filesicon/extra-inputs 已经是 ninja 的 implicit input,改它们不改变图的形状。
  5. 我自己写了两条假绿断言。 图标断言原本搜 4 字节,在 MB 级二进制里撞上是常事 —— Linux 通过很可能就是撞上了,而它同时掩盖了第 4 条(Windows 上本该在那一步暴露)。改成 4 像素 icon 的 16 字节高熵标记 + 「旧标记必须消失」。判据:断言必须能失败

验证

  • 单测 61 个二进制全过,新增 test_build_resources.cppversion_reqxpkg版本模型:精确键能表达上游的一切,范围表达不了 —— 且 lock 记的是范围本身 #363 用例(全部照抄真实索引形状)。
  • 本机装了 mingw 交叉链跑通 GNU/windres 全路径.rc → COFF → 链进 PE,icon 与元数据的字节都在 exe 里;改 icon、改 [package].description 都到达 exe;L0→L1 cmp 字节相同;lint 触发;缺文件硬错;非 PE 目标零警告无 res 单元。
  • 本机跑通全量 e2e:173 passed / 9 skipped / 15 failed,而这 15 个已用 git worktree 构建 main 逐个对照,在 main 上同样失败 ⇒ 环境性,非本 PR 引入。
  • fast path 那条单独验过对比:同工程连构两次(第二次 Finished in 0.00s、无 Inferred sources ⇒ prepare 被跳过),只改 .rc 后第三次 prepare 重跑且诊断触发。
  • 新增 196(版本身份 + lock,跑在所有平台)、197(Windows 原生,# requires: windows)、198(Linux→Windows,# requires: mingw-cross)。
  • ⚠️ cross-build-test.yml 的 mingw job 按文件名逐个调用 e2e,不跑 run_all.sh ⇒ 198 已显式加进 workflow,否则 GNU 这一半在 CI 里一次都不会跑。

生态可见的行为变化

  • cc-connect1.3.2 + 1.3.3-beta.1):^1.31.3.3-beta.1 改为 1.3.2(方向正确——范围不该悄悄给 beta)。
  • jdk-corretto / jdk-temurin:范围解析改为选中真条目而非别名,store 目录名随之变化。
  • 已用本机 xim-pkgindex 全表(161 个描述符)扫过:无「只有预发布键」的包。发版前要对当时的索引重扫一遍。

未决 / 可回退

设计文档 §D 的 4/5/6 按推荐执行,都可回退:

  • 4 alias 排除 + 真平局硬错(VERIFY-B1:alias 排除会改变 store verdir 名,见上)
  • 5 .rc 输入靠扫描 + 指名缺口 + extra-inputs 兜底(工具无 depfile,实测)
  • 6 声明了却不存在 = 硬错误。issue 第 3 条要的「跨平台不炸」已由「非 PE 不适用」拿到;「缺文件就跳过」被拒绝的理由是它会把这个 feature 要消灭的失效模式写成规定行为——一个没有图标、没有版本信息、且什么都没说的正式二进制。要字面满足的话形状是现成的:icon = { path = "…", optional = true } 走既有 diag::degraded 通道(报告一次、--strict 拒绝)。

未实测:ld.lld 的 mingw 模式是否直接吃 .res(不影响本 PR 的两条路径:GNU 走 windres、MSVC/lld-link 走 .res)。


合入前 review 轮(同一 PR,commit c624a79

对上面的实施版本做了一次架构 / 稳定性 / 一致性 / 多平台 review。每条可疑点都真机复现,没有一条是纯代码推理——复现同时充当「断言能失败」的证明:五条新断言各自对应一段用修复前二进制跑出来的错误输出。完整论证见设计文档 §F。

四条实测缺陷

MSVC 下找不到 rc.exe 工具链 PATH 覆盖按 find_first_of(";:") 切,而 Windows 路径的盘符冒号就在下标 1——C:\Windows Kits\… 被切成 C 加一段当前盘相对路径(同盘侥幸命中、跨盘必挂)。而这条 PATH 遍历正是 msvc 下的主路径rc.exe 属于 Windows SDK,从不在 cl.exe 旁边。同一个 PR 里两个调用点各自推导同一条规则、且已经彼此不一致 ⇒ 收敛成 rsrc::split_env_list

role = "object" 默认目标集漏了测试二进制。

mcpp build → rc=0     mcpp run → rc=0
mcpp test  → ld.lld: error: undefined symbol: blob_value

而它没有可用的逃生口:测试链接单元是从 tests/*.cpp 发现出来的,名字不在 mcpp.toml 里;只写测试目标又会让 mcpp build 硬失败(names unknown target(s): t_core / targets in this build: [objrole])⇒ 目标名字空间是 mode-dependent 的,这一层设计没承认。默认集合加入 TestBinary。([resources] 反向决策——排除测试二进制——保持不变:图标属于「要发布的东西」,符号属于「要链接的东西」,两边互相写明理由。)

未知 target 只在「一个都没匹配上」时报错。 .target("objrole").target("no_such_target_TYPO")rc=0,日志零提及 —— 与 types.cppm 和 docs 明写的契约相反。⚠️ 但先修这条会当场废掉上一条的唯一逃生口,所以顺序是:先给默认集合,再收紧 per-target 校验。

mcpp.lockbuildtest 之间抖动。 lock 从读 m->dependencies 改成读 resolved 之后带进了 dev-deps,而只有 mcpp test 解析它们:

mcpp build → 1 条    mcpp test → 2 条    mcpp build → 又变回 1 条

ResolvedRecord 增加 devOnly 并沿依赖边传播(多消费者取 AND),lock 跳过。判据:lock 是 manifest 的函数,不是命令的函数。

三处口径

  • 「不适用」不该覆盖到校验。 [resources] 的「声明了必须存在」原本整块包在 is_pe() 里 ⇒ Linux 上 icon = "assets/DOES_NOT_EXIST.ico" 构建 rc=0、日志里 resource 出现 0 次。路径存不存在是关于工作树的事实,不是关于目标的事实。 校验提到 is_pe() 之前;新增 199_resources_validation.sh(无 requires,每个 shard 都跑)。
  • resources/versioninforesources/no-image 由 warning 改 degraded(impact 就是本 feature 要消灭的静默失效,--strict 必须看得见);role="object" 无消费者新增 action/no-target,同口径。
  • 优雅性:resources 编排抽成带早返回的 lambda(消灭 150 行不缩进的 else 块);删死字段 ResourceUnit::packageName;改 LinkUnit::objects 注释口径;resolve_semver 字面短路收紧为必须 = 前缀(裸 1.2.3 在本语法里是 caret,判据不能只活在调用方);try_merge_semver 相同约束不再拼成 =X,=X

测试自身的假绿

⚠️ 序号断言原本只对 .res 生效case *.res),而 windres 产 .o ⇒ GNU 那一半的核心断言可能一次都不跑。

改判据时先写成「全文搜 UTF-16 VS_VERSION_INFO」,并在正确的 windres 产物上当场误报——因为那是 VS_VERSIONINFO 结构自带的 szKey,正确资源里也有。真判据是资源目录llvm-readobj --coff-resources.o / .res / .exe 都能读:

Type: VERSIONINFO (ID 16)  →  Name: (ID 1)              ← Windows 找得到
Type: VERSIONINFO (ID 16)  →  Name: VS_VERSION_INFO     ← 找不到

配正向反证:故意构造坏脚本,断言它确实是字符串名 —— 两个方向在同一次运行里都被证明。另:改写 res/app.rc 前补 sleep 1(该段唯一的重跑 prepare 触发源就是它的 mtime)。

验证

  • 本机全量 e2e:181 passed / 8 failed / 9 skipped,8 个失败全部是已知环境性基线(03/09/20/33/59/98/178)+ 198(我这条错断言,已修并单独复跑通过)。
  • 61 个单测全过(test_build_resources 13 例,含新增的 split_env_list)。

顺带

README 的包索引链接由仓库地址改为 https://mcpplibs.github.io/mcpp-index/

两个 issue 领域无关,失效形状同一条:生态已经产出的东西 mcpp 的模型
表达不了,于是走到一条「不报错但结果是错的」路径上。

#365 Windows 资源
- 新增 `[resources]`:icon 与版本信息一行搞定,元数据从 [package] 取默认值;
  自写 .rc 走 files = [...],被当作构建输入跟踪。
- 只有 PE 目标消费;非 PE 上「不适用」(不做事、不警告、逐字节不变),
  因此不需要也不能用 cfg(windows) —— 条件通道只载 BuildInputs。
- 声明了却不存在的文件是硬错误(对 issue 第 3 条的有意偏离)。
- 新增 BuildAction::Role::Object:产出接到链接输入,补齐角色表原本缺的
  那一格;ldflags 塞路径不产生任何 implicit input,正是本 issue 的成因。

  ⚠️ issue 结尾「llvm-rc 生成的 VERSIONINFO 不被解析」的归因是错的,已实测
  证伪:VS_VERSION_INFO 是 <windows.h> 的宏,没有它时资源被存成字符串名而
  非序号 1,而类型两种情况都是 RT_VERSION(16) —— 这正是它看着正常的原因。
  合成脚本写字面 1,构造性正确;自写脚本命中这个形状时给出指名的警告。

#363 版本身份
- resolve_semver 返回索引的字面键,不再从解析出的数字重造地址。
  真实索引里被这条修好的:1.92.8-docking(预发布塌成 1.92.8)、
  25.0.4.7.1(五段截断成不存在的 25.0.4.7)、25.0.4={ref=…}(别名与
  自己的目标构成平局)、pre-v0.0.5(报「no valid versions in index」)。
- 预发布按 SemVer 排序 + npm 预发布可见性规则;数值段任意长度;
  别名不参与范围候选;不可排序键只精确匹配并给出可粘贴的 pin 行;
  只差 build metadata 的真平局硬错,而精确形式按字面命中。
- mcpp.lock 记录解析结果并覆盖传递依赖,Compiling 行同源;两者原本读的
  都是未解析的 m->dependencies,而 ResolvedRecord 早就覆盖整张图。
  lock 本批不「权威化」,文件头自己声明这一点(e2e 断言,改时会红)。

实施中撞出来的(设计里没有):非 ASCII 元数据会让 rc 编译器拒绝整个脚本,
必须传 UTF-8 codepage,否则中文描述的项目根本构建不了。

设计与实测证据:.agents/docs/2026-08-07-windows-resources-and-version-identity-design.md
Windows e2e 抓到的:一次只改 `res/app.rc` 的构建报 `Finished dev in 0.15s`
—— 工程级 fast path 短路了整个 prepare。

`sources_newer_than` 只扫 `src/**/*` 的 C++ 扩展名,`.rc` 既不在 src/ 下也不是
那些扩展名,完全看不见。这不只是「警告没打」:`.rc` 的 implicit input 集合来自
扫描它,而扫描发生在 prepare —— 于是用户往脚本里新加一行 `#include "ids.h"`,
那个头文件永远不会被跟踪。与 `build.mcpp`、glob 输入是同一类输入:**改了它,
图本身应该长得不一样**,而 mtime 扫描看不见。

只扫 `files`。`icon` 与 `extra-inputs` 已经是 ninja 的 implicit input,改它们
不会改变图的形状,为一次改图标强制走完整 prepare 买不到任何东西。

顺带修掉两处**我自己写的假绿**:
- 图标断言原来搜 4 字节(`00ff00ff`),在 MB 级二进制里撞上是常事 —— 换成
  4 像素 icon 的 16 字节高熵标记,并加断言「旧标记必须消失」。
  (Linux 上之所以过,很可能就是撞上了。)
- b3 之所以在 Windows 上没暴露 fast path 问题,正是因为那条弱断言。

验证:同一工程连构两次(第二次 0.00s、无 prepare),只改 .rc 后第三次
prepare 重跑且诊断触发。
对已实施版本做架构/稳定性/一致性/多平台 review,每条可疑点都真机复现
而不是代码推理。复现同时充当「断言能失败」的证明:每条新断言都对应一段
用修复前二进制跑出来的、看得见的错误输出。

## 缺陷(实测)

**MSVC 下找不到 rc.exe。** 工具链 PATH 覆盖按 `find_first_of(";:")` 切,
而 Windows 路径的盘符冒号就在下标 1 —— `C:\Windows Kits\…` 被切成 `C`
加一段当前盘相对路径。而这条 PATH 遍历正是 msvc 下的主路径(rc.exe 属于
Windows SDK,从不在 cl.exe 旁边)。同一个 PR 里两个调用点各自推导同一条
规则、且已经彼此不一致 ⇒ 收敛成 `rsrc::split_env_list`。

**`role = "object"` 默认目标集漏了测试二进制。** `mcpp build` 通过而
`mcpp test` 在这个 action 本来要提供的符号上报 undefined symbol。而它
没有可用的逃生口:测试链接单元是从 tests/*.cpp 发现出来的,名字不在
mcpp.toml 里,只写测试目标又会让 `mcpp build` 硬失败 ⇒ 目标名字空间是
mode-dependent 的,这一层设计没承认。默认集合加入 TestBinary。

**未知 target 只在「一个都没匹配上」时报错**,于是拼错的名字挨着一个
对的名字时被静默丢弃 —— 与 types.cppm 和 docs 明写的契约相反。改为逐个
校验。⚠️ 这条与上一条耦合:先修它会当场废掉上一条的唯一逃生口。

**mcpp.lock 在 build/test 之间抖动。** lock 从 `m->dependencies` 改读
`resolved` 后带进了 dev-deps,而只有 `mcpp test` 解析它们 ⇒ build/test/
build 写出三个不同的文件。ResolvedRecord 增加 devOnly 并沿依赖边传播
(多消费者取 AND),lock 跳过。判据:**lock 是 manifest 的函数,不是命令
的函数**。

## 口径

- `[resources]` 的「声明了必须存在」提到 `is_pe()` 之前:路径存不存在是
  关于工作树的事实,不是关于目标的事实。按 PE 设门让 Linux/macOS 完全
  看不见 icon 里的拼写错误。新增 199(无 requires,每个 shard 都跑)。
- `resources/versioninfo` 与 `resources/no-image` 由 warning 改 degraded
  (`--strict` 要看得见);`role="object"` 无消费者新增 `action/no-target`,
  与 resources/no-image 同口径。
- resources 编排抽成带早返回的 lambda(消灭 150 行不缩进的 else 块);
  删死字段 `ResourceUnit::packageName`;改 `LinkUnit::objects` 注释口径;
  `resolve_semver` 字面短路收紧为必须 `=` 前缀(裸 `1.2.3` 是 caret,
  判据不能只活在调用方);`try_merge_semver` 相同约束不再拼成 `=X,=X`。

## 测试

- e2e 序号断言改用 llvm-readobj 的资源目录,`.o` 与 `.exe` 同一判据 ——
  GNU 那一半原本被 `case *.res` 整个跳过。⚠️ 先写成「全文搜 UTF-16
  VS_VERSION_INFO」并在正确的 windres 产物上误报:那是 VS_VERSIONINFO
  结构自带的 szKey,正确资源里也有。改判据后加正向反证(故意构造坏脚本,
  断言它确实是字符串名),两个方向在同一次运行里都被证明。
- 改写 res/app.rc 前补 sleep 1 —— 该段唯一的重跑 prepare 触发源就是它的
  mtime。
- 188 补三条(部分拼错必须报错 / 测试二进制拿到 object / 无消费者要报);
  196 补 build→test→build 三段 cmp;新增 199。

## 其他

README 的包索引链接指向 https://mcpplibs.github.io/mcpp-index/
mingw-cross job 的 sandbox 里没有 llvm-readobj(CI 实证:
`llvm-readobj not found under /home/runner/.mcpp`)——它只装 mingw 工具链。
硬失败本身是对的(静默跳过正是 #365 的出厂方式),错的是判据选了一个
这条 job 拿不到的工具。

`windres -J coff -O rc` 把编译好的资源反读成 rc 源码,名字直接可见,
而且它在 GNU 这一支必然存在——它就是产出这个文件的工具。实测 `.o` 与
链接后的 `.exe` 都能读:

    1 VERSIONINFO                   ← Windows 找得到
    "VS_VERSION_INFO" VERSIONINFO   ← 找不到

rc 工具从 build.ninja 的 `rc =` 取,不走 PATH:mcpp 本来就是 payload
相对解析的(裸 windres 在 PATH 上是 xlings shim),问 PATH 会用另一个
工具去检查这个产物。msvc 那一支保留 llvm-readobj——能走到那一支的
配置里,LLVM 载荷就是默认工具链本身。
@Sunrisepeak
Sunrisepeak marked this pull request as ready for review August 7, 2026 07:58
Windows job 红在 `windres could not read back ...resapp.mcpp.o`:
`llvm-windres` 只是 llvm-rc 的单向包装、没有 `-J coff`,而它和 GNU
binutils 的 windres **都匹配 `*windres*`** ⇒ 按工具名分派是错的。改成
反读 → llvm-readobj 依次 TRY,只有两条都不可用才硬失败。

同一条失败还暴露:native Windows 默认工具链走的是 GNU 支不是 msvc 支
(target 目录 `x86_64-windows-msvc` 而产物是 `.o`)⇒ `.res` 那一支在
两个 CI job 里都不执行,e2e 的 `case *.res` 是死分支。这条已记进设计
文档 §F.11 作为明确缺口。
@Sunrisepeak
Sunrisepeak merged commit 8783350 into main Aug 7, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants