diff --git a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md index 8844b72..ca95542 100644 --- a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md +++ b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md @@ -1,10 +1,10 @@ # 通用构建插件设计:`deps-vcpkg`、`rules-qt`、`deps-cmake` 及其 xlings 依赖 状态:第 4 版(实现版;§0.1 的更正块取代正文中与之冲突的内容),实施中 · 2026-09-26 · 实施计划见 `2026-09-26-deps-vcpkg-rules-qt-plan.md` · -基于 mcpp 2026.9.25.1、mcpp-plugins 0.12.0(`origin/main` dea1f09)、xim-pkgindex `origin/main`、GalTranslPP 上游 -`main` 2c7d6bf(2026-09-25)· 起因:在 GalTranslPP(mcpp 工作区 + vcpkg + Qt 6,Windows/MSVC)上调查 +基于 mcpp 2026.9.25.1、mcpp-plugins 0.12.0(`origin/main` dea1f09)、xim-pkgindex `origin/main`、验证工程上游 +`main` 2c7d6bf(2026-09-25)· 起因:在验证工程(mcpp 工作区 + vcpkg + Qt 6,Windows/MSVC)上调查 mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 mcpp 项目都能复用**的构建能力。§1–§6、§8 只设计这批 -通用能力;§7 用 GalTranslPP 做真实案例,核算"插件都做完以后,一个真实项目迁移要花多少、能省多少"。 +通用能力;§7 用验证工程做真实案例,核算"插件都做完以后,一个真实项目迁移要花多少、能省多少"。 标注约定:**【已核实】** 有出处(文档章节、索引文件、发布资产、CI 实测、源码计数);**【待验证】** 是设计依赖但尚未 实测的假设,全部列入 §9 Phase 0。 @@ -35,7 +35,7 @@ mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 | D6 | 2 | 附加模块能否做成一个包 | **能,建议做成一个包 `xim:qt-addons`**(依据与代价见 §6.2;待确认,§10-1) | | D7 | 2 | registry 共享克隆放哪 | vcpkg 的每用户目录(§3.6);第 4 版起不再有托管克隆,见 §0.1 | | D6′ | 3 | D6 确认 | `xim:qt-addons` 为一个包,**自有前缀**(xim 载荷私有),`rules-qt` 同时加入两个前缀 | -| D8 | 3 | 生态通用能力先行 | 通用缺口由 mcpp 以通用机制补上(SPEC-007,mcpp#702);插件按 SPEC-007 实现并等待该发布(§0.1-5);GalTranslPP 的 D 阶段在 fork 上初步验证 | +| D8 | 3 | 生态通用能力先行 | 通用缺口由 mcpp 以通用机制补上(SPEC-007,mcpp#702);插件按 SPEC-007 实现并等待该发布(§0.1-5);验证工程的 D 阶段在 fork 上初步验证 | | D9 | 3 | 免装 VS | 不是目标;删除 `toolchain::msvc_xim`,`xim:msvc` 由 mcpp 自动匹配 | ### 0.1 第 4 版更正(实现时的决定,取代正文中冲突的部分) @@ -43,7 +43,7 @@ mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 1. **registry 不再由插件管理**(取代 §3.6 的托管克隆、按工具检出与文件锁)。vcpkg-tool 每个发布附带 `vcpkg-standalone-bundle.tar.gz`(3.4 MB),其 `vcpkg-bundle.json` 为 `{"deployment":"OneLiner","usegitregistry":true}`, 脚本与该工具版本严格对应。`xim:vcpkg` = 工具 + 该 bundle,安装目录即 `VCPKG_ROOT`。【已核实】本机以此为根,对 - GalTranslPP 的 baseline 执行 `vcpkg install --dry-run`,输出 "Fetching registry information from + 验证工程的 baseline 执行 `vcpkg install --dry-run`,输出 "Fetching registry information from https://github.com/microsoft/vcpkg",解析出 `fmt 12.2.0`,registry 缓存 137 MB。继承的 `VCPKG_ROOT` 不使用, `options::vcpkg_root` 可显式指定。 2. **启动器合为一个 `mcpp-deps`**(取代 `mcpp-vcpkg`、`mcpp-cmake`),子命令 `vcpkg`、`cmake`;消费者写 @@ -92,7 +92,7 @@ mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 (`docs/30` "Current limitations");`mcpp::action` 是 ninja 边,只在 `mcpp build` 时执行,不受该上限约束 (`docs/30` "Declaring work instead of doing it")。 4. **缺环境不失败**。构建程序遇到缺失的前缀/工具时输出确定的路径并 `mcpp::warning`,真正的失败留给 action 边—— - 这样 IDE(mcppls 通过 emit 取模型)在一台尚未安装依赖的机器上也能拿到可信模型。反例是 GalTranslPP 现状:构建 + 这样 IDE(mcppls 通过 emit 取模型)在一台尚未安装依赖的机器上也能拿到可信模型。反例是验证工程现状:构建 程序缺 Qt 或缺一个 vcpkg `.lib` 就 `return 1`,emit 整体失败(CI 实测 `MCPP_BUILD_DATABASE_PLAN_FAILED`)。 5. **不写别人的东西**。插件只写 `out_dir`、自己管理的目录(§3.6 的托管克隆)和用户显式要求写的位置;用户已有的 vcpkg 克隆、项目源码树默认只读(lupdate 见 §4.3)。 @@ -155,7 +155,7 @@ makes the package's compile edges wait for it")。 管理(§3.6),只是不再承担环境设置。 - 目录隔离:`--x-buildtrees-root`、`--x-packages-root` 指向项目自己的 `out_dir`,`downloads/` 共享。共享克隆的默认 buildtrees/packages 在克隆内,两个项目同时编同一个 port 会互相覆盖;按项目隔离后与 vcpkg 自身的锁语义无关, - 一定安全。(GalTranslPP 的 CI 已经这样用 `--x-buildtrees-root`。) + 一定安全。(验证工程的 CI 已经这样用 `--x-buildtrees-root`。) - 输入:`vcpkg.json`、`vcpkg-configuration.json`、overlay ports/triplets 目录(`input()` + `rerun_if_changed_glob`)。 输出:stamp。只在这些输入变化时重跑;emit 永不执行;不受 600 s 限制。 - 二进制缓存:**不覆盖,用 vcpkg 的默认位置**(【已核实】vcpkg 文档 "Default binary cache":Windows @@ -168,7 +168,7 @@ makes the package's compile edges wait for it")。 (vcpkg 的默认值,已有项目的目录布局不变)。 - 链接:以 `options.libraries` 为准,**一律用确定的完整路径** `mcpp::link_flag(/lib/.lib)`(ELF/Mach-O 上 `lib.a/.so/.dylib`)。链接边排在编译边之后、编译边又等待安装边,所以链接时文件必然已存在,不必在规划期判断 - 存在与否。用完整路径的理由 GalTranslPP 的 `vcpkg_link.hpp` 写过:"An exact file path cannot resolve to a same-named + 存在与否。用完整路径的理由验证工程的 `vcpkg_link.hpp` 写过:"An exact file path cannot resolve to a same-named system library"。之所以要显式列表:首次构建时构建程序先于安装边运行,vcpkg 的 `info/*.list` 尚不存在。 - 校验:`info/*.list` 存在时,比对列表与实际产物,不一致给 `mcpp::warning`;`rerun_if_changed(stamp)` 让安装完成后 下一次构建程序按实际结果重新校验。 @@ -194,12 +194,12 @@ makes the package's compile edges wait for it")。 - 后两项由插件在 `out_dir` 生成 overlay triplet 与 chainload 工具链文件。【已核实】vcpkg 在 Windows 上用干净环境编 port,`INCLUDE`/`LIB` 要经 `VCPKG_ENV_PASSTHROUGH` 传入(文档同页);或者工具链文件直接写 `/I`、`/LIBPATH`。 哪种可行【待验证】。 -- **项目自带自定义 triplet 时**(GalTranslPP 的 `gpp-x64-windows-release` 就是):生成的 triplet 先 `include()` 项目 +- **项目自带自定义 triplet 时**(验证工程的 `gpp-x64-windows-release` 就是):生成的 triplet 先 `include()` 项目 triplet,再追加 chainload 设置,不替换它;triplet 名随之变为 `<原名>-mcpp-<选项>`,安装目录、映射一律按新名。 - 同时检查 triplet 的 `VCPKG_CRT_LINKAGE` 与项目运行库(如 `-fms-runtime-lib=dll`)是否一致,不一致则 warning。 **(5) 工作区**:`blocking` 的顺序是"per package"(【已核实】`docs/30`),被依赖成员的安装边管不到不依赖它的成员 -(GalTranslPP 的 `Updater` 不依赖 core,却要链接 `bit7z`,§7.1)。默认做法:**每个调用 `use()` 的成员各声明一条 +(验证工程的 `Updater` 不依赖 core,却要链接 `bit7z`,§7.1)。默认做法:**每个调用 `use()` 的成员各声明一条 安装边**,命令相同、stamp 各在自己的 `out_dir`。第一条真正安装,其余的 `vcpkg install` 发现已安装、几秒内结束; 并发时由 vcpkg 对安装目录加的锁串行化【待验证:manifest 模式对 `vcpkg_installed` 的锁】。锁不成立时退回"同一个 stamp 由每个成员声明为 `input()`、只由一个成员声明为 `output()`";根本方案是 §8 需求 3。 @@ -250,7 +250,7 @@ manifest 写法处理: | 项目写法 | vcpkg 的要求 | 本设计的做法 | |---|---|---| | `vcpkg-configuration.json` 的 `default-registry` 为 `git` 类型(带 `baseline`) | 工具自己把 registry 取到它的每用户 registries 缓存,所有项目共享 | 什么都不用做(`X_VCPKG_REGISTRIES_CACHE` 可覆盖)【待验证:各平台默认路径】 | -| `vcpkg.json` 的 `builtin-baseline`(GalTranslPP 就是这种) | `VCPKG_ROOT` 必须是含该 baseline 提交的 microsoft/vcpkg 克隆 | 见下 | +| `vcpkg.json` 的 `builtin-baseline`(验证工程就是这种) | `VCPKG_ROOT` 必须是含该 baseline 提交的 microsoft/vcpkg 克隆 | 见下 | `builtin-baseline` 的 `VCPKG_ROOT` 按顺序选: @@ -275,7 +275,7 @@ manifest 写法处理: 它要求的工具、已装工具三个版本,提示升级 `xim:vcpkg`;否则 baseline 一定在这个检出之前,两条都满足。这样**工具版本 决定脚本版本、baseline 决定 port 版本**,不会因为上游 registry 前进而让所有项目一起失败(第 2 版的问题,见 §11)。 -不按发布标签检出:【已核实】GalTranslPP 的 baseline `ea1a7396`(2026-08-08)比最新标签 `2026.07.29` 晚 129 个提交, +不按发布标签检出:【已核实】验证工程的 baseline `ea1a7396`(2026-08-08)比最新标签 `2026.07.29` 晚 129 个提交, 但要求的工具同样是 2026-07-27。按标签检出会把一个完全兼容的项目判为"baseline 太新"。项目在两个标签之间选 baseline 很常见。 @@ -351,7 +351,7 @@ int main() { - **uic / rcc**:`role = "source"`;rcc 的依赖用 `rcc --list` 在规划期取得资源清单作为 `input()`,或 depfile【待验证】。 - **lrelease**:`.ts` → `.qm`,`role = "source"`(产物不参与编译),再 `mcpp::deploy` 到 `translations/`。 - **lupdate**:会**改写源码树里的 `.ts`**,默认关闭;`o.translations.update_sources = true` 时作为 `role = "check"`、 - `blocking = true` 的 action,lrelease 依赖它的 stamp(GalTranslPP 现行做法,§7.2)。与 emit "never writes into the + `blocking = true` 的 action,lrelease 依赖它的 stamp(验证工程现行做法,§7.2)。与 emit "never writes into the project tree"(specs R2.1)的精神一致:只在显式要求的 build 中写。 - **链接**:`include_dir(/include/Qt)`、`link_search(/lib)`、`link_lib("Qt6")`、 `QT__LIB` 等定义;编译器族为 msvc/clang-cl 时加 `/Zc:__cplusplus`(Qt 6 对 MSVC 的要求;clang 驱动不需要); @@ -361,7 +361,7 @@ int main() { `platforms/` 等子目录(【已核实】构建程序的 `mcpp::deploy` 见 `docs/30` "Deploying what the program generated", pack 将其放在 `bin//`,README `dist-apple` 行;清单侧 `[runtime] deploy` 的 `to` 语义见 `docs/04` §2.11)。 `d3dcompiler_47.dll`、`opengl32sw.dll` 按 windeployqt 的默认行为可选部署(`o.deploy_software_gl`)。 -- **库成员的翻译**:库成员(如 GalTranslPP core)生成的 `.qm` 要进可执行成员的 `translations/`。【待验证】依赖方 +- **库成员的翻译**:库成员(如验证工程core)生成的 `.qm` 要进可执行成员的 `translations/`。【待验证】依赖方 构建程序的 `mcpp::deploy` 是否随依赖进入可执行文件的 run/pack 布局;不成立时由可执行成员的 `translations` 选项 直接列出库的 `.ts`。 @@ -374,7 +374,7 @@ int main() { ## 5. `deps-cmake`(第二批) -项目里常见"带一个 CMake 子项目"(submodule/vendored),今天只能靠外部脚本先编好(GalTranslPP 的 ElaWidgetTools +项目里常见"带一个 CMake 子项目"(submodule/vendored),今天只能靠外部脚本先编好(验证工程的 ElaWidgetTools 就是:一个 Python 脚本调 CMake,Qt 路径写死在脚本里,§7.1)。插件把 configure / build / install 作为三条 action (`role = "check"` 链,最后一条 blocking),安装到 `out_dir` 下的前缀,再按 §3.3(2) 的方式映射。 @@ -442,7 +442,7 @@ libraries are available under commercial licenses from The Qt Company, or under 由于规则只链接显式列出的模块,项目不会误用。包说明里写明这一点,并列出 LGPL 与 GPL-only 模块。 - 调试符号包(`debug_information`、`debug_info`)不收(基础包的调试符号解压 4.05 GB)。 -- **首个版本 6.11.1**(GalTranslPP 的要求),平台先做 win64_msvc2022_64,再补 linux、macos、arm64。 +- **首个版本 6.11.1**(验证工程的要求),平台先做 win64_msvc2022_64,再补 linux、macos、arm64。 - 【已核实】下载方式:6.11 起在线仓库按架构分目录(`qt6_6111/qt6_6111_msvc2022_64/Updates.xml`),aqtinstall 3.3.0 不认; 每个归档以安装前缀为根(`bin/`、`include/`、`lib/`)。本设计的验证 CI 已按此方式装成功。 - 许可:xim 只是从 Qt 官方仓库下载;包说明注明 Qt 的 LGPL 义务(动态链接、允许替换)。 @@ -460,14 +460,14 @@ libraries are available under commercial licenses from The Qt Company, or under - 许可:`7z.dll` 为 LGPL,RAR 解压部分带 unRAR 限制;包说明里注明。 - `xim:7zip` 要做的只有一件事:在包说明里写明 Windows 安装目录中 `7z.dll` 的位置是约定(不是偶然),这样项目可以依赖它。 -## 7. 真实案例:GalTranslPP 迁移的成本与收益 +## 7. 真实案例:验证工程迁移的成本与收益 -本节回答:"§3–§6 都做完以后,一个真实项目要花多少才能用上,能省下什么。"数据全部来自 GalTranslPP 上游 `main` -2c7d6bf 的源码计数、它的 `how-to-build.md`、子模块源码和本设计的验证 CI(Sunrisepeak/GalTranslPP PR #1)。 +本节回答:"§3–§6 都做完以后,一个真实项目要花多少才能用上,能省下什么。"数据全部来自验证工程上游 `main` +2c7d6bf 的源码计数、它的 `how-to-build.md`、子模块源码和本设计的验证 CI(验证工程 PR #1)。 ### 7.1 现状(【已核实】源码计数) -工作区 5 个成员:`GalTranslPP`(core,库)、`GPPVersion`(库)、`GPPCLI`、`GPPGUI`、`Updater`(可执行)。依赖关系: +工作区 5 个成员:`验证工程`(core,库)、`GPPVersion`(库)、`cli`、`gui`、`Updater`(可执行)。依赖关系: CLI、GUI 依赖 core;**Updater 不依赖 core,但自己链接 `bit7z`**。 | 部分 | 规模 | 做什么 | @@ -482,7 +482,7 @@ CLI、GUI 依赖 core;**Updater 不依赖 core,但自己链接 `bit7z`**。 | 子模块 ElaWidgetTools | CMake 项目 | 用自带的 `build.py` 编;Qt 路径 `D:/Qt/6.11.1/msvc2022_64` **写死在脚本里**;需要 `WidgetsPrivate` | | 子模块 pybind11(fork) | 附带 Python 头文件与 `python312.lib` | 与仓库内 `Python-3.12.10-embed-amd64.zip` 配套;代码里写死 `BaseConfig/Python-3.12.10-embed-amd64` | | vcpkg | 22 个 port,自定义 triplet `gpp-x64-windows-release`,2 个 overlay port(mecab、proxy) | `builtin-baseline` 写法 | -| Qt 用量 | 45 个含 `Q_OBJECT` 的头文件(全在 GPPGUI)、1 个 `.qrc`、4 个 `.ts`、0 个 `.ui` | 模块:Core、Gui、Widgets、Network;无附加模块 | +| Qt 用量 | 45 个含 `Q_OBJECT` 的头文件(全在 gui)、1 个 `.qrc`、4 个 `.ts`、0 个 `.ui` | 模块:Core、Gui、Widgets、Network;无附加模块 | 新开发者的准备工作(`how-to-build.md`,111 行): @@ -512,10 +512,10 @@ CI(验证 workflow):Qt 靠一段 25 行的内联 Python 从 Qt 仓库下 | vcpkg 克隆 + bootstrap | `xim:vcpkg` + 托管克隆(§3.6) | 无 | | VS Build Tools | mcpp 侧 `sysroot = "xim:msvc@…"`;vcpkg 侧**只有** `toolchain::msvc_xim` 能免装 VS | §3.3(4)【待验证】chainload 细节 | | CMake、Python(开发者手装) | CMake 由插件声明;Python 只剩 embed 包解压(可改成一条用 `xim:7zip` 的 action) | 无 | -| **不替代**:`runtime-stage` + 发布布局代码 | 保留。`Release/GPPCLI`、`GPPGUI`、`GUICORE`、私有镜像目录是这个项目的发布约定,不是通用能力 | 只有改为从 `mcpp pack` 产物派生布局,或引擎提供 §8 需求 2,`runtime-stage` 才能删 | +| **不替代**:`runtime-stage` + 发布布局代码 | 保留。`Release/cli`、`gui`、`GUICORE`、私有镜像目录是这个项目的发布约定,不是通用能力 | 只有改为从 `mcpp pack` 产物派生布局,或引擎提供 §8 需求 2,`runtime-stage` 才能删 | | **不替代**:pybind11 fork 附带的 Python | 保留。换成 `xim:python` 会让头文件(3.12.13)与运行时 embed 包(3.12.10)的补丁版本不一致,而版本号写死在代码里 | 若要换,先把 embed 包也改由 xim 提供,属项目自己的决定 | -### 7.3 迁移成本(插件发布之后,GalTranslPP 一侧,一人) +### 7.3 迁移成本(插件发布之后,验证工程一侧,一人) | 阶段 | 内容 | 人日 | 前提 | |---|---|---|---| @@ -525,7 +525,7 @@ CI(验证 workflow):Qt 靠一段 25 行的内联 Python 从 Qt 仓库下 | D(可选) | 发布改为从 `mcpp pack` 派生,删 `runtime-stage`;vcpkg 改 `toolchain::msvc_xim` 实现免装 VS | 1–2 | §8 需求 2 或 pack 方案;§3.3(4) 验证通过 | | **合计** | A–C 必做 | **3.5–4.5** | D 另计 1–2 | -对比:通用插件本身 27–37 人日(§9),GalTranslPP 只占其中 3.5–4.5。第二个及以后的项目不再分摊插件开发,只付 +对比:通用插件本身 27–37 人日(§9),验证工程只占其中 3.5–4.5。第二个及以后的项目不再分摊插件开发,只付 自己的 A–C。 ### 7.4 收益(A–C 完成后) @@ -557,12 +557,12 @@ CI(验证 workflow):Qt 靠一段 25 行的内联 Python 从 Qt 仓库下 | # | 需求 | 为什么 | 没有它时的退路 | |---|---|---|---| | 1 | 构建程序可感知"规划模式"(或 emit 时构建程序失败只降级) | 原则 3/4 目前靠插件自律;任何项目的构建程序出错都会让 IDE 退回兜底模型 | 插件自律:缺失只 warning | -| 2 | 构建程序声明运行时搜索目录(run 的 PATH、pack 的 PE 闭包),且项目能在 build 时取得闭包结果 | vcpkg/Qt 的 DLL 在各自 `bin/`;GalTranslPP 的 `runtime-stage` 就是在自己补这一块 | 逐个 `mcpp::deploy`;项目保留自己的闭包工具 | +| 2 | 构建程序声明运行时搜索目录(run 的 PATH、pack 的 PE 闭包),且项目能在 build 时取得闭包结果 | vcpkg/Qt 的 DLL 在各自 `bin/`;验证工程的 `runtime-stage` 就是在自己补这一块 | 逐个 `mcpp::deploy`;项目保留自己的闭包工具 | | 3 | blocking action 对依赖方也生效,或工作区级 setup action | 多成员工作区共用一次安装 | 每个成员各一条安装边(§3.3(5)) | | 4 | `mcpp::action` 设置环境变量 | 外部工具普遍靠环境变量配置 | 插件自带启动器(本设计采用) | 另:`[workspace.profile.*]` 继承与本插件无关,但同一消费者会遇到(【已核实】`docs/07` §4.1、specs §9 只允许三类继承; -GalTranslPP 5 个成员重复写了同样的 `[profile.release]`/`[profile.fast-release]`)。 +验证工程5 个成员重复写了同样的 `[profile.release]`/`[profile.fast-release]`)。 ## 9. 计划与工作量(一人估算,含 fixtures 与 CI,Windows 优先) @@ -572,10 +572,10 @@ GalTranslPP 5 个成员重复写了同样的 `[profile.release]`/`[profile.fast- | Phase 1 | `xim:vcpkg`、`xim:qt`(Windows)、`deps-vcpkg` + `mcpp-vcpkg`(registry 选择、按工具检出、锁、目录隔离、三种工具链)+ fixtures | 9–12 | | Phase 2 | `rules-qt` + fixtures;`xim:qt-addons`;`xim:qt*` Linux/macOS | 8–10 | | Phase 3 | `deps-cmake` + `mcpp-cmake` + fixtures | 3.5–5.5 | -| 消费者验证 | GalTranslPP 迁移 A–C(§7.3,fork 分支 + 其 CI) | 3.5–4.5 | +| 消费者验证 |验证工程迁移 A–C(§7.3,fork 分支 + 其 CI) | 3.5–4.5 | | **合计** | | **27–37**(第 2 版 24–33;增加的部分见 §11) | -**验证场已就绪**:Sunrisepeak/GalTranslPP PR #1 的 workflow 已在 windows-2025 上搭好 xlings、mcpp 2026.9.25.1、 +**验证场已就绪**:验证工程 PR #1 的 workflow 已在 windows-2025 上搭好 xlings、mcpp 2026.9.25.1、 LLVM 22.1.8、Qt 6.11.1(直接读 Qt 仓库)、vcpkg(`gpp-x64-windows-release`,22 个 port);【已核实】vcpkg 冷安装 51.6 分钟,结果已缓存。 @@ -595,7 +595,7 @@ LLVM 22.1.8、Qt 6.11.1(直接读 Qt 仓库)、vcpkg(`gpp-x64-windows-rele 1. **托管克隆的检出版本**(§3.6):第 2 版让克隆跟随上游最新、再比较工具版本。上游 registry 一旦要求比 `xim:vcpkg` 更新的工具,**所有项目会同时失败**,直到 xim 跟进。改为检出"主干上与已装工具匹配的最后一个提交"。本版起草时 - 曾写成"与工具匹配的最新发布标签",用 GalTranslPP 的 baseline 实测(比最新标签晚 129 个提交、工具要求相同)后改掉。 + 曾写成"与工具匹配的最新发布标签",用验证工程的 baseline 实测(比最新标签晚 129 个提交、工具要求相同)后改掉。 2. **用户已有的 `VCPKG_ROOT` 改为只读**(§3.6、原则 5):第 2 版会对它 `git fetch`,等于改写用户的仓库。 3. **按项目隔离 buildtrees/packages**(§3.3(1)):第 2 版没提,共享克隆时两个项目同时编同一个 port 会互相覆盖。 4. **二进制缓存改用 vcpkg 默认位置**(§3.3(1)):第 2 版放在 mcpp 缓存目录,与 D7 的"复用用户已有的 vcpkg"矛盾。 @@ -603,9 +603,9 @@ LLVM 22.1.8、Qt 6.11.1(直接读 Qt 仓库)、vcpkg(`gpp-x64-windows-rele 目录位置带来的实际好处是与默认二进制缓存同级。 5. **"免装 VS"的真实条件**(§3.3(4)):第 2 版把"工具链一致"当作纯可选项,并在 §6.1 暗示 `xim:msvc` 就能免装 VS。 已核实 vcpkg 默认路线必须找到 VS 实例,只有 chainload 能绕开;新增 `toolchain::msvc_xim`。 -6. **工作区默认做法**(§3.3(5)):由"共享 stamp 技巧"改为"每个成员各一条安装边",更简单,且 GalTranslPP 的 +6. **工作区默认做法**(§3.3(5)):由"共享 stamp 技巧"改为"每个成员各一条安装边",更简单,且验证工程的 Updater 正好需要。 -7. **§7 重写**:第 2 版的 GalTranslPP 样例写了 `xim:python = 3.12.13`(与 embed 包 3.12.10 不一致)、称可删除约 600 行 +7. **§7 重写**:第 2 版的验证工程样例写了 `xim:python = 3.12.13`(与 embed 包 3.12.10 不一致)、称可删除约 600 行 (`runtime-stage` 和发布布局其实删不掉)、完全没提 ElaWidgetTools(`deps-cmake` 对这个项目不是可有可无)。 8. 附加模块合为一个包(D6),§0、§4.2、§4.3、§6.1、§6.2 同步;新增 `rules-qt-xim-addons`。 9. 小项:自定义 triplet 包装;`use()` 返回前缀;`private_modules`;库成员翻译;rcc 依赖;fixtures 增加 4 个。 diff --git a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md index 4dd1ca1..7ccd70c 100644 --- a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md +++ b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md @@ -7,12 +7,12 @@ | # | 决定 | 对设计的影响 | |---|---|---| | D6 | 附加模块合为一个包 | 确认 `xim:qt-addons`;按 xim 的载荷私有规则,它有**自己的前缀**,不写入 `xim:qt` 的目录 | -| D8 | 先做 mcpp / xlings 生态的通用能力;GalTranslPP 的 D 阶段只在 fork 上做初步验证 | 引擎需求 2 在 mcpp 中实现:构建程序声明运行时库目录 | +| D8 | 先做 mcpp / xlings 生态的通用能力;验证工程的 D 阶段只在 fork 上做初步验证 | 引擎需求 2 在 mcpp 中实现:构建程序声明运行时库目录 | | D9 | 免装 VS 不是目标 | 删除 `toolchain::msvc_xim`;`xim:msvc` 由 mcpp 自动匹配,插件不声明 | 另有一项由实测带来的简化:vcpkg-tool 每个发布附带 `vcpkg-standalone-bundle.tar.gz`(3.4 MB,脚本与该工具 版本严格对应,`vcpkg-bundle.json` 含 `"usegitregistry": true`)。以它为 `VCPKG_ROOT` 时,`builtin-baseline` -清单经 git registry 解析,registry 落在 vcpkg 的每用户 registries 缓存。已在本机实测:对 GalTranslPP 的 +清单经 git registry 解析,registry 落在 vcpkg 的每用户 registries 缓存。已在本机实测:对验证工程的 baseline `ea1a7396` 执行 `vcpkg install --dry-run` 解析出 `fmt 12.2.0`,缓存 137 MB。因此设计第 3 版 §3.6 的托管克隆、 按工具检出与文件锁全部取消。 @@ -24,14 +24,14 @@ baseline `ea1a7396` 执行 `vcpkg install --dry-run` 解析出 `fmt 12.2.0`, | openxlings/xim-pkgindex | `xim:vcpkg` 2026.7.27(工具 + standalone bundle)、`xim:qt` 6.11.1、`xim:qt-addons` 6.11.1;`xim:7zip` 说明 `7z.dll` 位置;测试 | 合入即发布索引;`xlings-res/vcpkg` 资源用 `gh` 与 `gtc` 双端上传 | | mcpp-community/mcpp-plugins | `0.13.0`:`deps-vcpkg` + `mcpp-vcpkg`、`rules-qt` + `rules-qt-xim` + `rules-qt-xim-addons`、`deps-cmake` + `mcpp-cmake`;fixtures;CI 引擎版本;README | tag `v0.13.0`,GitHub release,`gtc` 上传 `mcpp-res/mcpp-plugins` | | mcpp-community/mcpp-index | 登记 `mcpp:plugins 0.13.0` | 合入即发布 | -| Sunrisepeak/GalTranslPP(fork) | 临时 PR 2:迁移 A–C,D 阶段初步验证 | 不合入 | +| 验证工程(fork) | 临时 PR 2:迁移 A–C,D 阶段初步验证 | 不合入 | ## 3. 依赖关系 ``` T2 xim 包(vcpkg/qt/qt-addons)──► T2r 索引合入(xim-pkgindex#878,已合入) T3 插件按 SPEC-007 实现(PR 已开,等待)──┐ -T1 mcpp#702 实现并发布(mcpp 侧)─────────┴──► T3c 插件 CI 全绿 ──► T3r 插件发布 0.13.0 ──► T4 mcpp-index ──► T5 GalTranslPP PR 2 +T1 mcpp#702 实现并发布(mcpp 侧)─────────┴──► T3c 插件 CI 全绿 ──► T3r 插件发布 0.13.0 ──► T4 mcpp-index ──► T5验证工程PR 2 ``` - T1、T2、T3 并行:T3 在本机用 T1 的源码构建验证 Linux 路径;Windows 与 macOS 路径只能由 CI 验证。 @@ -70,9 +70,9 @@ T1 mcpp#702 实现并发布(mcpp 侧)─────────┴──► 位于插件仓库之内,因此其安装位于 `target/` 之下,检查脚本的日志写入 `target/ci/`,第二次构建以 `--profile dev` 绕过快路径,使 Linux 与 Windows、macOS 走同一条规划路径。从索引取得插件的项目不受影响。 -## 6. 0.14.0:运行时数据与精简 Qt(GalTranslPP 验证后的第二轮) +## 6. 0.14.0:运行时数据与精简 Qt(验证工程验证后的第二轮) -GalTranslPP 在 0.13.1 上构建成功后仍有两个手动步骤(解压嵌入式 Python、运行 `Release.py`),Qt 下载量为 +验证工程在 0.13.1 上构建成功后仍有两个手动步骤(解压嵌入式 Python、运行 `Release.py`),Qt 下载量为 完整基础包。第二轮把固定步骤程序化,并补齐 windeployqt 的 Qt 翻译: | 交付 | 仓库 | 机制 | @@ -100,5 +100,5 @@ libstdc++,mcpp 的 clang 使用 libc++,两者的 `std::` 符号互不链接 判据(Linux CI `vcpkg-libcxx`):llvm 下 `libfmt.a` 含 `std::__1::` 符号且程序运行;默认工具链构建后 libc++ 前缀仍在;切回 llvm 不重跑安装;deps-cmake 子项目的 `CMAKE_CXX_COMPILER` 为 mcpp 的 clang。 -GalTranslPP 上游仅支持 Windows(README 所述,代码直接调用 WinAPI),Linux 的判据由本仓库的 fixture 承担; -GalTranslPP PR2 作为 Windows 回归验证。 +验证工程上游仅支持 Windows(README 所述,代码直接调用 WinAPI),Linux 的判据由本仓库的 fixture 承担; +验证工程PR2 作为 Windows 回归验证。 diff --git a/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md b/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md index 1a2338d..903b03a 100644 --- a/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md +++ b/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md @@ -17,7 +17,7 @@ 6. 缺少插件开发规范;README 过长。 约束: -- GalTranslPP 上游仅支持 Windows。Linux 与 macOS 的判据由插件仓库的 fixture 承担,GalTranslPP PR2 作为 Windows 回归。 +-验证工程上游仅支持 Windows。Linux 与 macOS 的判据由插件仓库的 fixture 承担,验证工程PR2 作为 Windows 回归。 - mcpp 最新版本为 2026.9.26.2。 - 0.14.1 与 0.15.0 合并为一个版本 0.15.0。每个仓库一个 PR,仅在无法同时验证时拆分(§8)。 @@ -145,7 +145,7 @@ rules-qt 的 SDK 查找顺序为 `options::root` → `QT_ROOT_DIR`(不采用 ` |---|---| | 删除 `rules-qt-xim*` | 改为 `features = ["rules-qt"]`,并在 `[xlings]` 中声明 `xim:qt-base` 或 `xim:qt` 及版本 | | 删除 `mcpp-deps` | 从依赖边删除 `tools = ["mcpp-deps"]` | -| vcpkg 前缀多一层 triplet | 改用返回的 `prefix`,不硬编码 `vcpkg_installed/`(GalTranslPP `gpp.build` 第 182 行) | +| vcpkg 前缀多一层 triplet | 改用返回的 `prefix`,不硬编码 `vcpkg_installed/`(验证工程`build` 第 182 行) | | 插件不再提供 glib 目录 | 使用外部 Qt 的 Linux 项目自行处理闭包 | 载荷的更新方式:xlings 以磁盘上的载荷与安装记录判定"已安装",不会对已存在的载荷调用配方的 `installed()`。因此含运行时闭包的载荷以新版本号 @@ -164,7 +164,7 @@ mcpp-plugins#32 → 0.15.0(Linux Widgets 与 Windows CRT 判据依赖上一步 │ 三平台 CI 全绿 → tag、GitHub release、gtc 上传 GitCode mcpp-index:登记 0.15.0 │ -GalTranslPP PR2:按 §7 迁移;Windows 回归(含 VC++ 运行时) +验证工程PR2:按 §7 迁移;Windows 回归(含 VC++ 运行时) 并行:mcpp issue E1、E2 ``` diff --git a/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md b/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md index 04d341a..45d4a95 100644 --- a/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md +++ b/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md @@ -44,7 +44,7 @@ bba733a 在云端从未构建。本地以 mcpp 2026.9.26.2、GCC 16.1 构建。 | e2e 802(#704) | 在默认工具链为 llvm@22.1.8 的机器上失败:测试断言 `NEEDED libstdc++`,而清单未固定工具链。在测试清单中写入 `[toolchain] default = "gcc@16.1.0"` 后三项断言全部通过,说明 #704 的修复正确,缺陷在测试本身 | 逐 issue 审查的结论:12 个 issue 的修复均对应 issue 的原始描述,未发现错误(#706 是已合入上游的文档提交,不在本 PR 中)。需要处理的是以下三点: -- **#714 是行为变更:** 未解析的 `workspace = true` 由静默变为空版本,改为加载时报错。在 mcpp-plugins、mcpp-index、GalTranslPP、mcpp-language-server 中均未发现依赖旧行为的清单。 +- **#714 是行为变更:** 未解析的 `workspace = true` 由静默变为空版本,改为加载时报错。在 mcpp-plugins、mcpp-index、验证工程、mcpp-language-server 中均未发现依赖旧行为的清单。 - **#710 覆盖不足:** 非成员宿主工具包的工具链路径只有单元测试,没有 e2e。 - **note 代码改名:** `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT` 改为 `..._DEFERRED`,没有兼容别名;mcpp-language-server 不读取这两个代码。 @@ -175,7 +175,7 @@ A1 至 A4 可以并行。B3 不依赖 B2:旧版 xlings 忽略 revision 字段 | xlings #620 | 先用 xlings 2026.9.26.3 安装 glibc(revision 0),再升级到 2026.9.27.1 并执行 `xlings install glibc` | 输出重装提示,载荷为 revision 1 | | xlings #621 | 执行 `setlocale(LC_ALL, "C.UTF-8")` 与 `iconv -f UTF-8 -t GBK`;qt-demo 执行 `mcpp run` | 两项调用成功;Qt 不再输出 UTF-8 locale 提示 | -**消费方验证:** GalTranslPP fork 以 mcpp 2026.9.27.1 与 plugins 0.16.0 运行 Windows CI,测量首次 emit、构建后再次 emit,并确认未安装 llvm@20.1.7。预期结果见 `2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md` §4 的 S4 一栏。该验证只更新 fork 分支,不向上游提 PR;是否基于上游最新重做,另行决定。 +**消费方验证:**验证工程fork 以 mcpp 2026.9.27.1 与 plugins 0.16.0 运行 Windows CI,测量首次 emit、构建后再次 emit,并确认未安装 llvm@20.1.7。预期结果见 `2026-09-27-validation-project-upstream-vs-pr2-outlook.md` §4 的 S4 一栏。该验证只更新 fork 分支,不向上游提 PR;是否基于上游最新重做,另行决定。 ### 3.6 阶段 E:收尾 diff --git a/.agents/docs/2026-09-27-plugins-0.15.2-plan.md b/.agents/docs/2026-09-27-plugins-0.15.2-plan.md index 349658e..950b2cf 100644 --- a/.agents/docs/2026-09-27-plugins-0.15.2-plan.md +++ b/.agents/docs/2026-09-27-plugins-0.15.2-plan.md @@ -1,12 +1,12 @@ # plugins 0.15.2 与生态收敛方案 -日期:2026-09-27。依据:`2026-09-27-versioning-assets-locale-and-galtranslpp-review.md` 及其后的讨论结论。 +日期:2026-09-27。依据:`2026-09-27-versioning-assets-locale-and-validation-project-review.md` 及其后的讨论结论。 ## 0. 目标 1. Qt 以上游版本 `6.11.1` 作为唯一版本键,消费方书写上游版本。 2. brotli 资源恢复规范文件名,GitHub 与 GitCode 逐字节一致。 -3. GalTranslPP PR2 达到当前生态能力下的最佳形态:版本单点声明、可配置项有文档、emit 开销可解释且可测。 +3.验证工程PR2 达到当前生态能力下的最佳形态:版本单点声明、可配置项有文档、emit 开销可解释且可测。 4. 需要 mcpp、xlings、xim glibc 改动的事项,以 issue 形式交给各自仓库。 非目标:修改 mcpp、xlings 或 glibc 配方本身。 @@ -46,7 +46,7 @@ D1 的索引改动只是删除冗余键;消费方的切换不依赖它先合 - 验收:三平台 CI 全绿。其中 Linux 的 widgets 打包检查要确认新缓存下装出的 `6.11.1` 带有运行时闭包(libdbus、xkbcommon、fontconfig),这正是 D1 的实证。 - 发布:tag v0.15.2、GitCode 镜像(sha256 比对)、mcpp-index 登记。 -### 2.4 GalTranslPP PR2 +### 2.4 the validation project PR2 | 项 | 内容 | 验收 | |---|---|---| @@ -70,7 +70,7 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 ``` 2.1 brotli 资源 ──► 2.2 xim PR ──► 索引发布 ──► 2.5-1 删除 -r1 -2.3 plugins 0.15.2 ──► 发布 + index 登记 ──► 2.4 GalTranslPP PR2 +2.3 plugins 0.15.2 ──► 发布 + index 登记 ──► 2.4验证工程PR2 ``` 两条链互不阻塞:2.3 与 2.4 使用 `6.11.1` 键,该键今天已存在且内容已修正(见 §1 关键事实)。 @@ -83,7 +83,7 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 | mcpp-community/mcpp#711 | 发布物无法依赖另一个包按目标平台构建的可执行文件,只能借宿主工具边 | 否:本机构建结果正确;交叉编译时才会出错 | | mcpp-community/mcpp#712 | `xpkg_dir` 把裸版本当字面 pin,xlings 则会匹配到四段版本键;已复现(libglvnd `1.7` → 装了 1.7.0.1,`xpkg_dir` 返回空) | 否:写精确版本键即可避开 | | openxlings/xlings#620 | 配方修订号独立于版本号 | 否:没有外部用户时就地修正(D4) | -| openxlings/xlings#621 | glibc 载荷没有 locale 数据,编译进去的 locale 路径不在载荷内 | 否:只是一条提示;GalTranslPP 仅构建 Windows,不受影响 | +| openxlings/xlings#621 | glibc 载荷没有 locale 数据,编译进去的 locale 路径不在载荷内 | 否:只是一条提示;验证工程仅构建 Windows,不受影响 | 不再提交:设置 `QT_ROOT_DIR` 时仍下载声明的 Qt。这是预期行为,按 E4 写进文档。 视 E2、E3 的结果,可能再提交"xlings 条目支持从工作区继承"。 @@ -92,7 +92,7 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 | 风险 | 缓解 | |---|---| -| 删除 `6.11.1.1` 后,仍书写 `6.11.1.1` 的消费方解析失败 | 已知消费方只有 plugins fixture 与 GalTranslPP,二者均在本方案中改为 `6.11.1` | +| 删除 `6.11.1.1` 后,仍书写 `6.11.1.1` 的消费方解析失败 | 已知消费方只有 plugins fixture 与验证工程,二者均在本方案中改为 `6.11.1` | | 某处缓存仍恢复出修正前的 `6.11.1` 载荷 | 两处 CI 缓存 key 均升级;Linux widgets 打包检查会在闭包缺失时失败 | | E2 或 E3 不被 mcpp 支持 | 回退为逐成员声明,不阻塞发布 | @@ -103,14 +103,14 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 | 2.1 brotli | GitCode 已上传规范名资源与 `SHA256SUMS`,两端逐字节一致;GitHub 的 `-r1` 已删除;GitCode 的 `-r1` 待用户删除 | | 2.2 xim | openxlings/xim-pkgindex#891 已合入(4c1dc850);发布后的索引中 qt、qt-base、qt-addons 只列出 `6.11.1`;brotli 按规范名安装,带头文件 | | 2.3 plugins | mcpp-community/mcpp-plugins#34 已合入(53c57d5),三平台 CI 全绿;tag v0.15.2;GitCode mcpp-res/mcpp-plugins 0.15.2 与 GitHub 逐字节一致(sha256 `a66e349d…76ad`);mcpplibs/mcpp-index#478 已合入,`latest` 为 0.15.2 | -| E2 | 成员的 `.workspace = true` 可以与 `features`、`host-module` 同用。非成员的 `gpp.build` 不能继承(mcpp#714),改为自带版本;若把它设为成员,根目录的 `mcpp build` 会把它当作目标构建并失败 | +| E2 | 成员的 `.workspace = true` 可以与 `features`、`host-module` 同用。非成员的 `build` 不能继承(mcpp#714),改为自带版本;若把它设为成员,根目录的 `mcpp build` 会把它当作目标构建并失败 | | E3 | 不成立:三种写法(带 cfg 的 target 表、`[target.linux]`、`[xlings.workspace]`)写在根清单时,成员的构建程序都看不到载荷(mcpp#713)。回退为逐成员声明 | -| E6 警告来源 | 21 条 "no Qt SDK" 全部来自 `gpp.build`:它启用 `rules-qt` 只为导入模块,自己没有 build.mcpp,mcpp 仍为它合成规则程序(mcpp#715)。0.15.2 让这种情况不再输出提示;新增 fixture `qt-import-only` | -| Qt 路径单点 | GalTranslPP 的 `gpp::qt_root` 与 `gpp::qt_options()` 汇集到一处:规则、发布布局、ElaWidgetTools 的 `QT_SDK_DIR` 都取同一个 SDK | +| E6 警告来源 | 21 条 "no Qt SDK" 全部来自 `build`:它启用 `rules-qt` 只为导入模块,自己没有 build.mcpp,mcpp 仍为它合成规则程序(mcpp#715)。0.15.2 让这种情况不再输出提示;新增 fixture `qt-import-only` | +| Qt 路径单点 |验证工程的 `gpp::qt_root` 与 `gpp::qt_options()` 汇集到一处:规则、发布布局、ElaWidgetTools 的 `QT_SDK_DIR` 都取同一个 SDK | 新发现并已提交的 mcpp issue:#713(根清单的 xlings 声明对成员不可见)、#714(非成员的 `.workspace = true` 变成空版本)、 #715(没有设备源也合成规则程序)、#716(provisioning 印记在载荷删除后仍然有效)。 -| 2.4 GalTranslPP | Sunrisepeak/GalTranslPP 提交 43ee88f,run 36272623612 全绿。首次 emit 258 s;GPPCLI 6.2 min;GPPGUI 18.2 min;pack 0.9 min;无改动的第二次构建 0.8 min,未触发安装;构建后再次 emit 62 s。所有日志中 "no Qt SDK" 为 0 条;成员记录的 SDK 事实为 `xlings: …/xim-x-qt-base/6.11.1` | +| 2.4验证工程| 验证工程 提交 43ee88f,run 36272623612 全绿。首次 emit 258 s;cli 6.2 min;gui 18.2 min;pack 0.9 min;无改动的第二次构建 0.8 min,未触发安装;构建后再次 emit 62 s。所有日志中 "no Qt SDK" 为 0 条;成员记录的 SDK 事实为 `xlings: …/xim-x-qt-base/6.11.1` | 构建后再次 emit 需要 62 s,原因有二: diff --git a/.agents/docs/2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md b/.agents/docs/2026-09-27-validation-project-upstream-vs-pr2-outlook.md similarity index 81% rename from .agents/docs/2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md rename to .agents/docs/2026-09-27-validation-project-upstream-vs-pr2-outlook.md index 2c7c830..030f871 100644 --- a/.agents/docs/2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md +++ b/.agents/docs/2026-09-27-validation-project-upstream-vs-pr2-outlook.md @@ -1,31 +1,31 @@ -# GalTranslPP:PR2 与上游最新的差异,及后续构建优化的上限 +#验证工程:PR2 与上游最新的差异,及后续构建优化的上限 日期:2026-09-27。 | 对象 | 提交 | 说明 | |---|---|---| | 共同基点 | `8a2c76a` | PR2 分叉处 | -| 上游最新 | julixian/GalTranslPP `e64f93e`(2026-09-27 03:45,"update build system") | 基点之后 2 个提交,48 个文件,+1319/−730 | -| PR2 | Sunrisepeak/GalTranslPP `43ee88f`(`ci/mcpp-plugins-deps-qt`) | 基点之后 15 个提交,21 个文件,+771/−567;plugins 0.15.2,CI run 36272623612 全绿 | +| 上游最新 | 验证工程的上游 `e64f93e`(2026-09-27 03:45,"update build system") | 基点之后 2 个提交,48 个文件,+1319/−730 | +| PR2 | 验证工程 `43ee88f`(`ci/mcpp-plugins-deps-qt`) | 基点之后 15 个提交,21 个文件,+771/−567;plugins 0.15.2,CI run 36272623612 全绿 | ## 1. 结论 1. **构建架构已趋同,依赖获取策略相反。** - - 上游采纳了 PR2 的构建结构:共享构建逻辑作为模块 `gpp.build` 导入,rules-qt 的翻译流程,deps-vcpkg 与 deps-cmake 的机制,以 `${mcpp.self} stage` 动作写出 Release 布局,runtime-stage。两边都删除了旧的 5 个头文件与 `qt-root.txt`。 + - 上游采纳了 PR2 的构建结构:共享构建逻辑作为模块 `build` 导入,rules-qt 的翻译流程,deps-vcpkg 与 deps-cmake 的机制,以 `${mcpp.self} stage` 动作写出 Release 布局,runtime-stage。两边都删除了旧的 5 个头文件与 `qt-root.txt`。 - 分歧在取得方式:上游要求开发者预装 vcpkg、CMake、Qt,并在源码中填写 Qt 路径;Python 解压、`Release.py`、`windeployqt` 保留为构建后的手动步骤。PR2 由 xlings 取得全部工具,构建本身完成部署。 2. **差异规模大,PR2 不能直接变基。** 两边改动重叠 17 个文件;试合并产生 10 处冲突:4 个成员的 `mcpp.toml` 与 `build.mcpp`、`how-to-build.md`、`Release.py`。下一版应基于 `e64f93e` 重做,而不是合并 PR2。 3. **上游有三处优于 PR2,应当吸收:** - - 插件经 `reexport = true` 只在 `gpp.build` 声明一次; + - 插件经 `reexport = true` 只在 `build` 声明一次; - Windows 专属的 defines、系统库、链接选项放进 `[target.windows.*]` 或按驱动生成; - 源码中的 `#pragma comment(lib, …)` 已删除,系统库由清单声明。 4. **上游复制了插件的约 508 行代码(`deps/{tools,vcpkg,cmake}.ixx`),暴露了插件的缺口。** 起因是 `deps-vcpkg`、`deps-cmake` 这两个 feature 无条件声明 `xim:vcpkg`、`xim:cmake`,只要启用就会下载。上游为了用本机工具,只能改用内部 feature `deps`(清单注释写明"消费方不应直接使用"),再在本地复制并修改这两个模块。复制的代码依赖插件内部接口(`absolute_from_root`、`deploy_after`、`link_libraries`),插件升级时可能失效。 -5. **本次新验证的事实:GalTranslPP 今天就能把所有声明集中到一处。** 见 §3。 +5. **本次新验证的事实:验证工程今天就能把所有声明集中到一处。** 见 §3。 ## 2. 差异表 | 维度 | 上游 `e64f93e` | PR2 `43ee88f` | 更优 | |---|---|---|---| -| 插件版本 | 0.15.1;只在 `gpp.build` 声明,`reexport = true` 转交各成员(1 处) | 0.15.2;根 `[workspace.dependencies]`、`gpp.build` 各写一次(2 处);GPPCLI、GPPGUI、Updater 与 `gpp.build` 各列 features | 上游 | +| 插件版本 | 0.15.1;只在 `build` 声明,`reexport = true` 转交各成员(1 处) | 0.15.2;根 `[workspace.dependencies]`、`build` 各写一次(2 处);cli、gui、Updater 与 `build` 各列 features | 上游 | | 插件 features | `deps`(内部 feature)、`rules-qt` | `deps-vcpkg`、`deps-cmake`、`deps-archive`、`rules-qt` | PR2(公开接口) | | vcpkg | 本机安装;`gpp-build.ixx` 中的 `vcpkg_root` 或 PATH;使用复制并修改的模块 | `xim:vcpkg`,可用 `options.root` 改用本机 | 各有取向 | | CMake | 本机安装;`cmake_executable` 或 PATH | `xim:cmake` | 各有取向 | @@ -35,7 +35,7 @@ | BaseConfig、SampleProject | `Release.py` 复制 | 构建部署,并写入 Release 布局 | PR2 | | 7z.dll | 仓库内二进制 `3rdParty/7z.dll`(1.9 MB),`Release.py` 与 runtime-stage 复制 | `xim:7zip`,已从仓库删除 | PR2 | | OpenCC 数据 | 构建复制到 `Release/<成员>/BaseConfig/opencc` | 构建部署到程序旁,并写入 Release 布局 | 相同(PR2 另支持 `mcpp run`) | -| Updater 发布 | Updater 自身的构建程序写入 GPPGUI(`Updater.exe`)与 GUICORE(`Updater_new.exe`) | GPPGUI 取工具边产物 `dep_bin("gpp.updater")` 再复制 | 上游(不依赖宿主工具产物,避开 mcpp#711) | +| Updater 发布 | Updater 自身的构建程序写入 gui(`Updater.exe`)与 GUICORE(`Updater_new.exe`) | gui 取工具边产物 `dep_bin("updater")` 再复制 | 上游(不依赖宿主工具产物,避开 mcpp#711) | | Windows 专属设置 | 各成员 `[target.windows.build]`;`/DEBUG`、`/OPT` 按驱动在代码中生成;runtime-stage 在 `[target.windows.build-dependencies]` | defines 放在 `[workspace.build]`,所有目标通用;`-Wl,/DEBUG` 写在 profile;runtime-stage 无条件声明 | 上游 | | 源码 | 删除 `#pragma comment`;`Tool.cpp` 的 `#ifdef` 改为包住整个函数;Updater 改用 `QCoreApplication` 与 `import boost`;GUI 的 GIL 释放移出 try;ElaWidgetTools 子模块更新 | 未改源码 | 上游 | | vcpkg 目录 | `vcpkg-scripts/{ports,triplets}`,`install_root` 显式 | 原 `ports/`,默认 `install_root` | 上游(目录更清晰) | @@ -45,9 +45,9 @@ | 仓库内构建代码 | 1139 行(其中复制的插件代码 508 行,`Release.py` 43 行) | 741 行 | PR2 | | 首次构建所需手动操作 | 约 13 步 | 4 步 | PR2 | -## 3. 本次验证:声明可集中到 `gpp.build` +## 3. 本次验证:声明可集中到 `build` -**探针**:一个工作区,成员 `app` 以 `host-module` 依赖一个非成员的路径包 `shared`(对应 `gpp.build`)。mcpp 2026.9.26.2,plugins 0.15.2。工程位于 `scratchpad/reexport-probe`。 +**探针**:一个工作区,成员 `app` 以 `host-module` 依赖一个非成员的路径包 `shared`(对应 `build`)。mcpp 2026.9.26.2,plugins 0.15.2。工程位于 `scratchpad/reexport-probe`。 | `shared` 中的声明 | 成员构建程序观察到的结果 | |---|---| @@ -56,10 +56,10 @@ | `[xlings.workspace] "xim:qt-base" = "6.11.1"` | `xpkg_dir("xim", "qt-base")` 与 `rules::qt::root()` 均为 `…/xim-x-qt-base/6.11.1` | | 删除上一行(对照组) | 两者均为空 | -**推论**:插件版本、features、runtime-stage、`xim:qt-base` 与 `xim:7zip` 都可以只在 `gpp.build` 声明一次。 +**推论**:插件版本、features、runtime-stage、`xim:qt-base` 与 `xim:7zip` 都可以只在 `build` 声明一次。 - PR2 为 mcpp#713、#714 所做的逐成员声明因此不再需要。这两个 issue 对根清单仍然成立,但不再阻碍本项目。 -- 附带发现:`gpp.build` 声明了 Qt 之后,插件合成的规则程序会走"有 SDK"的分支。在 Linux 上它会输出 libc++ 提示;在 Windows 上预计不输出。0.15.2 的静默条件只覆盖"无 SDK",应推广为"无 build.mcpp 且无设备源时,不论有无 SDK 都直接返回"(见 §4 的 P2)。 +- 附带发现:`build` 声明了 Qt 之后,插件合成的规则程序会走"有 SDK"的分支。在 Linux 上它会输出 libc++ 提示;在 Windows 上预计不输出。0.15.2 的静默条件只覆盖"无 SDK",应推广为"无 build.mcpp 且无设备源时,不论有无 SDK 都直接返回"(见 §4 的 P2)。 ## 4. 后续优化能做到什么地步 @@ -69,7 +69,7 @@ |---|---|---|---| | S0 | 上游 `e64f93e` 现状 | — | — | | S1 | PR2(参照) | mcpp 2026.9.26.2、plugins 0.15.2 | 已完成 | -| S2 | 基于 `e64f93e` 重做,保留其结构与本机工具模式;吸收 PR2 的自动部署(Python、BaseConfig、SampleProject、Qt 插件与翻译、7z);所有声明集中到 `gpp.build`;Qt 按 `qt_root` → `QT_ROOT_DIR` → `xim:qt-base` 的顺序选取 | 现有发布版即可 | 我方(GalTranslPP 新 PR) | +| S2 | 基于 `e64f93e` 重做,保留其结构与本机工具模式;吸收 PR2 的自动部署(Python、BaseConfig、SampleProject、Qt 插件与翻译、7z);所有声明集中到 `build`;Qt 按 `qt_root` → `QT_ROOT_DIR` → `xim:qt-base` 的顺序选取 | 现有发布版即可 | 我方(验证工程新 PR) | | S3 | S2 + plugins 0.16:P1、P2 | 插件发布 | 我方(mcpp-plugins) | | S4 | S3 + 下一版 mcpp:#705、#707、#710、#711、#716、#712 | mcpp 发布;mcpp main 在 2026.9.26.2 之后只有一个文档提交,尚无相关修复 | mcpp 上游 | | S5 | 长期:不再需要 VS Build Tools(让 vcpkg 通过 chainload 使用 mcpp 的 clang);action 可设环境与工作目录(#708);feature 可声明自身所需的工具(#709) | vcpkg triplet 工作、mcpp 能力 | 生态 | @@ -96,7 +96,7 @@ | 自动模式 ⇄ 本机模式 | 仅本机 | 仅自动(vcpkg/Qt 可指定路径,但仍会下载) | Qt 可切换;vcpkg/CMake 仅本机 | 全部可切换,注释掉一行即可 | 同 S3 | 同 S3 | | `mcpp run` / `mcpp pack` | 否 | 是 | 是 | 是 | 是 | 是 | | 首次 emit(包缓存为空) | 未测 | 258 s(实测) | 约同 S1 | 约同 S1 | 下降(预计):不再安装第二套 LLVM(#710),emit 不再完整构建宿主工具(#707);幅度待实测 | 同 S4 | -| GPPCLI / GPPGUI 编译 | 未测 | 6.2 / 18.2 min(实测) | 约同 S1 | 约同 S1 | 约同 S1 | 约同 S1 | +| cli / gui 编译 | 未测 | 6.2 / 18.2 min(实测) | 约同 S1 | 约同 S1 | 约同 S1 | 约同 S1 | | 无改动的第二次构建 | 未测 | 0.8 min,无安装(实测) | 约同 S1 | 约同 S1 | 约同 S1 | 约同 S1 | | 构建后再次 emit | 约同 S1(同样存在 Updater 工具边) | 62 s(实测) | 约同 S1 | 约同 S1 | 秒级(预计,同 profile 时):#705 修复后,Updater 不再被重建 | 同 S4 | | 删除载荷后的恢复 | — | 需手动删除 provisioning 印记 | 同 S1 | 同 S1 | 自动重新安装(#716) | 同 S4 | @@ -105,15 +105,15 @@ ### 4.3 上限的解读 - **S2 是现有发布版下的上限。** 首次构建只需 4 步(安装 xlings、安装 mcpp、克隆、`mcpp build`),所有版本只写在一处,同时保留上游偏好的本机工具模式。它唯一的缺口是 vcpkg 与 CMake 无法按需改由 xim 取得。 -- **S3 使两种模式对称。** 同一份仓库既能零配置构建,也能使用本机工具,切换方式是注释掉 `gpp.build` 中的一行;上游复制的插件代码可以全部删除。 +- **S3 使两种模式对称。** 同一份仓库既能零配置构建,也能使用本机工具,切换方式是注释掉 `build` 中的一行;上游复制的插件代码可以全部删除。 - **S4 只改变耗时与健壮性,不改变操作步骤。** 收益集中在 emit:构建后的 emit 从 62 s 降到秒级,首次 emit 省去第二套 LLVM。编译时间由源码规模决定,各层不变;要继续缩短,只能靠二进制缓存(vcpkg 已缓存)或减少 LTO/opt,与构建系统无关。 - **S5 才能去掉 VS Build Tools。** 这取决于 vcpkg 的 triplet 能否通过 chainload 使用 mcpp 的 clang,以及 ElaWidgetTools 的 CMake 构建能否同样改用 clang,不在短期范围内。 ## 5. 建议的下一步 -1. **GalTranslPP 新 PR(S2)**: +1. **验证工程新 PR(S2)**: - 从 `e64f93e` 开分支,保留上游结构、源码修正与 `reexport`; - - 把插件、runtime-stage、`xim:qt-base`、`xim:7zip` 集中到 `gpp.build`; + - 把插件、runtime-stage、`xim:qt-base`、`xim:7zip` 集中到 `build`; - Qt 选取顺序:`qt_root` 为空时交给 rules-qt 的默认顺序; - 迁入 PR2 的自动部署,删除 `Release.py`; - `how-to-build.md` 以 `> 注:` 并列两种模式; diff --git a/.agents/docs/2026-09-27-versioning-assets-locale-and-galtranslpp-review.md b/.agents/docs/2026-09-27-versioning-assets-locale-and-validation-project-review.md similarity index 92% rename from .agents/docs/2026-09-27-versioning-assets-locale-and-galtranslpp-review.md rename to .agents/docs/2026-09-27-versioning-assets-locale-and-validation-project-review.md index 89b5212..986f448 100644 --- a/.agents/docs/2026-09-27-versioning-assets-locale-and-galtranslpp-review.md +++ b/.agents/docs/2026-09-27-versioning-assets-locale-and-validation-project-review.md @@ -1,6 +1,6 @@ -# 版本号、资源命名、locale 与 GalTranslPP 构建体验评估 +# 版本号、资源命名、locale 与验证工程构建体验评估 -日期:2026-09-27。范围:0.15.x 交付遗留的三个问题的最佳形态,以及 GalTranslPP PR2 相对 VS 与早期 mcpp 构建的对比。 +日期:2026-09-27。范围:0.15.x 交付遗留的三个问题的最佳形态,以及验证工程PR2 相对 VS 与早期 mcpp 构建的对比。 所有结论均经本地实验或源码核对;证据列于各节末尾。 ## 1. Qt 版本号 6.11.1.1 @@ -18,7 +18,7 @@ ### 1.2 结论(2026-09-27 修订) 6.11.1 与 6.11.1.1 均于 2026-09-26 发布,项目 CI 之外没有消费者。持有修正前 6.11.1 载荷的只有三处: -plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订号的价值在于保护已有用户,此处不存在该前提。 +plugins CI 缓存、验证工程CI 缓存与本机沙箱,均可清除。修订号的价值在于保护已有用户,此处不存在该前提。 - **最佳形态:只保留一个键 `6.11.1`,内容为修正后的配方;删除 `6.11.1.1`。** 消费方写上游版本 `6.11.1`,xlings 与 mcpp 都按字面解析,两侧一致。 - 清理:两处 CI 的缓存 key 升级,本机沙箱删除旧载荷。 @@ -59,7 +59,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 ### 3.2 影响 -- GalTranslPP:无影响(仅 Windows)。 +-验证工程:无影响(仅 Windows)。 - Linux 上的 Qt 程序:Qt 6 在 Unix 上的 local8Bit 固定为 UTF-8,Qt 自身文本处理不受影响;受影响的是程序直接调用的 C 库多字节与宽字符函数(`mbstowcs`、`std::locale("")` 等)。 ### 3.3 最佳形态 @@ -71,7 +71,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 两项完成后无需 `LOCPATH`,`mcpp run` 与 `mcpp pack` 产物行为一致。不采用 `LOCPATH` 注入方案:它对打包后由用户直接启动的程序无效。 -## 4. GalTranslPP:VS → mcpp → 优化后的 mcpp +## 4.验证工程:VS → mcpp → 优化后的 mcpp ### 4.1 首次构建所需操作(Windows) @@ -80,7 +80,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 | 手动安装 | VS 2026 IDE(指定 MSVC v14.50)、CMake、git、vcpkg(克隆、bootstrap、integrate)、Qt 在线安装器(需账户)、Qt VS Tools 插件 | xlings、mcpp、VS Build Tools、CMake、Python、git、vcpkg(克隆、bootstrap、PATH)、Qt 在线安装器 | git、xlings、VS Build Tools | | Qt 路径配置 | 3 处:Qt VS Tools 导入、ElaWidgetTools `CMakeLists.txt`、vcxproj `QtInstall` | 2 处:`build.py`、`qt-root.txt` | 0 处 | | 依赖构建 | 手动运行 `build.bat` 编译 ElaWidgetTools,并检查 3 个产物 | 手动运行 `build.py` 与 `vcpkg install --triplet …` | 由构建完成(`deps-cmake`、`deps-vcpkg`) | -| 构建命令 | IDE 中切换 Release 并逐项目生成 | `mcpp build -p GPPCLI/GPPGUI` | 同左 | +| 构建命令 | IDE 中切换 Release 并逐项目生成 | `mcpp build -p cli/gui` | 同左 | | 运行时部署 | `Release.bat` | 解压 Python、`Release.py`、`windeployqt` ×2 | 由构建完成:Qt 库、插件与翻译、Ela、vcpkg DLL、7z.dll、Python、OpenCC、BaseConfig、VC++ 运行时 | | 分发 | 手动整理 | 手动整理 | `mcpp pack --format dir` | | 手动步骤合计 | 约 12 | 约 13 | 4(安装 xlings、安装 mcpp、克隆、`mcpp build`) | @@ -91,18 +91,18 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 |---|---|---|---| | vcpkg 二进制缓存 | 未命中,独立安装 51 分钟 | 命中 678 MB,缺失部分在 emit 中源码编译(保存后增至 999 MB) | 命中 999 MB,完整 | | `mcpp emit`(全新 checkout) | 0.4 分钟(依赖已预装) | 53 分钟 | 4.8 分钟 | -| GPPCLI(fast-release) | — | 6.5 分钟 | 6.3 分钟 | -| GPPGUI(fast-release) | — | 18.0 分钟 | 19.7 分钟 | +| cli(fast-release) | — | 6.5 分钟 | 6.3 分钟 | +| gui(fast-release) | — | 18.0 分钟 | 19.7 分钟 | | 无改动的第二次构建 | — | 0.8 分钟,无安装 | 0.8 分钟,无安装 | 更正:53 → 4.8 分钟的主要原因是 vcpkg 二进制缓存从部分命中变为完整命中,而非移除 mcpp-deps。 全新 checkout 上 emit 的 4.8 分钟包含: -1. **宿主工具构建**:GPPGUI 以 `tools = ["Updater"]` 取得 Updater.exe,emit 为此以宿主身份完整构建 Updater(Qt 与 vcpkg 程序),runtime_stage 同理。 +1. **宿主工具构建**:gui 以 `tools = ["Updater"]` 取得 Updater.exe,emit 为此以宿主身份完整构建 Updater(Qt 与 vcpkg 程序),runtime_stage 同理。 2. **第二套 LLVM**:宿主工具构建不继承工作区的 `[toolchain] windows = "llvm@22.1.8"`,日志显示 "no toolchain configured — installing llvm@20.1.7",因此额外下载一套 LLVM 并编译其 std 模块。 3. **vcpkg 安装**:Updater 的构建会执行 vcpkg install,此时从二进制缓存恢复约 22 个库。 -4. **构建程序**:5 个成员的 build.mcpp 及其 host 模块(mcpp.plugins、gpp.build)各编译一次。 +4. **构建程序**:5 个成员的 build.mcpp 及其 host 模块(mcpp.plugins、build)各编译一次。 已构建过一次后,宿主工具按"包源码 + 宿主工具链"缓存,vcpkg 前缀已存在,构建程序显示 "up to date (cached)",因此 emit 预计为秒级。 同条件下的"第二次构建"为 0.8 分钟,可作旁证;emit 本身未单独测量。 @@ -130,7 +130,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 | G4 | 仍需手动安装 VS Build Tools(vcpkg 的 MSVC triplet 与 ElaWidgetTools) | 长期项:vcpkg chainload mcpp 的 clang;现阶段保留为文档前提 | | G5 | 成员清单中 7z 的注释位于 Qt 条目之上 | 随 G1 修正 | | G6 | PR2 为验证分支(`ci(temporary)` 提交) | 向上游提交时压缩为一次迁移提交,CI 工作流按上游需要保留或删除 | -| G7 | emit 的首次开销来自宿主工具与第二套 LLVM(§4.2) | mcpp:宿主工具继承工作区工具链;提供目标产物依赖。在此之前,GalTranslPP 可先在 CI 中固定默认工具链 | +| G7 | emit 的首次开销来自宿主工具与第二套 LLVM(§4.2) | mcpp:宿主工具继承工作区工具链;提供目标产物依赖。在此之前,验证工程可先在 CI 中固定默认工具链 | ### 4.5 结论 @@ -156,7 +156,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 | §2 brotli | 两端均为规范名,内容一致;GitHub 的 `-r1` 已删除 | | §3 locale | 已提交 openxlings/xlings#621 | | G1、G5 | 已完成:`how-to-build.md` 用 `> 注:` 列出 Qt、vcpkg、vcpkg 缓存、工具链与插件版本的配置方式 | -| G2 | 部分成立:成员以 `.workspace = true` 继承插件版本;`gpp.build` 不是成员,无法继承(mcpp#714);`xim:qt-base` 仍逐成员声明(mcpp#713) | +| G2 | 部分成立:成员以 `.workspace = true` 继承插件版本;`build` 不是成员,无法继承(mcpp#714);`xim:qt-base` 仍逐成员声明(mcpp#713) | | G3 | 属预期行为:不使用 xim 提供的 Qt 时,注释掉声明,再设置 `QT_ROOT_DIR` 或 `gpp::qt_root` | | G4 | 不变:仍需 VS Build Tools | | G7 | 已提交 mcpp#710(宿主工具不使用工作区工具链)、#711(缺少目标产物依赖) | diff --git a/.github/scripts/check-compat-retirement.sh b/.github/scripts/check-compat-retirement.sh new file mode 100755 index 0000000..13eca91 --- /dev/null +++ b/.github/scripts/check-compat-retirement.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Compatibility units retire themselves (mcpp#734, design §3.7). +# +# A compatibility unit keeps a behaviour for six months after the release that +# replaced it. Its header states the date: +# +# // COMPATIBILITY UNIT (# COMPATIBILITY UNIT in mcpp.toml) +# // retires: 2027-03-28 +# +# This check lists every unit and fails when one is past its date, so that a +# deferral cannot outlive its reason unnoticed. Every file under a `compat/` +# directory must carry the header; a file there without one is reported, which +# keeps the list from being empty because the marker was misspelt. +# +# bash .github/scripts/check-compat-retirement.sh +# COMPAT_TODAY=2027-03-29 bash .github/scripts/check-compat-retirement.sh # the failure, on demand +set -euo pipefail +cd "$(dirname "$0")/../.." + +today="${COMPAT_TODAY:-$(date -u +%Y-%m-%d)}" +fail=0 +units=0 + +# Every file under a compat/ directory, outside build outputs and fixtures. +while IFS= read -r f; do + grep -qE '^(//|#) COMPATIBILITY UNIT$' "$f" || { echo "FAIL: $f is under compat/ and has no COMPATIBILITY UNIT header"; fail=1; } +done < <(find . -path ./tests -prune -o -path '*/target' -prune -o -path '*/compat/*' -type f -print | sort) + +# Every header, wherever it is: its retirement date. +while IFS=$'\t' read -r file date; do + units=$((units + 1)) + if [ -z "$date" ]; then + echo "FAIL: $file: a COMPATIBILITY UNIT without a 'retires:' date" + fail=1 + elif [[ "$today" > "$date" ]]; then + echo "FAIL: $file: retired on $date (today is $today); remove the unit and what it keeps" + fail=1 + else + echo "ok: $file retires on $date" + fi +done < <(find . -path ./tests -prune -o -path '*/target' -prune -o -path ./.git -prune -o \ + \( -name '*.cppm' -o -name 'mcpp.toml' \) -type f -print | sort | + xargs awk ' + /^(\/\/|#) COMPATIBILITY UNIT$/ { if (pending) print FILENAME "\t"; pending = 1; next } + pending && /retires:/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); + print FILENAME "\t" substr($0, RSTART, RLENGTH); pending = 0 } + ENDFILE { if (pending) print FILENAME "\t"; pending = 0 }') + +[ "$units" -gt 0 ] || { echo "FAIL: no COMPATIBILITY UNIT header found"; fail=1; } +echo "$units compatibility unit(s)" +exit "$fail" diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index 8320faf..a046e16 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -70,6 +70,29 @@ vcpkg_consumer() { fail "share/fmt/copyright was not deployed beside the program" echo "ok: a file of the prefix is deployed beside the program" + # THE MECHANISM (0.17.0). On the Visual Studio row the instance mcpp + # resolved is selected and the standard triplet is used, so the ABI hash is + # the one vcpkg computes by itself. On the Linux GCC row (this job's + # default) vcpkg's own detection is kept: the payload driver runs with + # flags only mcpp's command lines carry. On the clang rows (macOS here) a + # derived triplet names the resolved tools. + if is_windows; then + grep -rqs 'VCPKG_VISUAL_STUDIO_PATH=' target --include=build.ninja || + fail "the installation does not select the Visual Studio instance mcpp resolved" + [ -d target/vcpkg_installed/x64-windows/x64-windows ] || fail "the instance mechanism did not use x64-windows" + echo "ok: the installation selects the Visual Studio instance and keeps the standard triplet" + elif is_macos; then + ls -d target/vcpkg_installed/*-mcpp-*/*-mcpp-* > /dev/null 2>&1 || + fail "no derived -mcpp- prefix under target/vcpkg_installed" + grep -rqs 'MCPP_VCPKG_CXX=' target --include=build.ninja || fail "the installation does not hand vcpkg the resolved compiler" + echo "ok: the installation names the resolved compiler in a derived triplet" + else + ls -d target/vcpkg_installed/*-linux/*-linux > /dev/null 2>&1 || + fail "the GCC row did not keep the standard -linux triplet" + ! grep -rqs 'MCPP_VCPKG_CXX=' target --include=build.ninja || fail "the GCC row named its compiler to vcpkg" + echo "ok: the GCC row keeps vcpkg's detection and the standard triplet" + fi + if is_windows; then # The pack collects it from the runtime search directory. ls target/vcpkg_installed/x64-windows/x64-windows/bin/fmt.dll > /dev/null || fail "x64-windows built no fmt.dll" @@ -84,22 +107,25 @@ vcpkg_consumer() { } # LINUX UNDER A libc++ TOOLCHAIN. The host compiler vcpkg and CMake find uses -# libstdc++, whose `std::` symbols a libc++ program cannot link. The default -# triplet is then the generated `x64-linux-libcxx`, whose ports build with -# mcpp's clang; fmt's interface returns `std::string`, so the link itself is the -# criterion. The prefix is then shown to survive the default toolchain's own -# installation, which vcpkg would remove if the two triplets shared one. +# libstdc++, whose `std::` symbols a libc++ program cannot link. The ports +# build with mcpp's clang through a derived triplet `-linux-mcpp-` +# (0.17.0; `-linux-libcxx` before); fmt's interface returns +# `std::string`, so the link itself is the criterion. The prefix is then shown +# to survive the default toolchain's own installation, which vcpkg would remove +# if the two triplets shared one. vcpkg_libcxx() { - local llvm="${MCPP_LLVM:-llvm@22.1.8}" gen=x64-linux-libcxx - [ "$(uname -m)" = aarch64 ] && gen=arm64-linux-libcxx + local llvm="${MCPP_LLVM:-llvm@22.1.8}" gen cd "$ROOT/tests/vcpkg-consumer" rm -rf target vcpkg_installed mkdir -p target/ci "$MCPP" build --toolchain "$llvm" 2>&1 | tee target/ci/libcxx-build.log "$MCPP" run --toolchain "$llvm" | tee target/ci/libcxx-run.log grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/libcxx-run.log || fail "the libc++ program did not print through fmt" - local lib; lib=$(find target/vcpkg_installed -path "*/$gen/$gen/lib/libfmt.a" | head -1) - [ -n "$lib" ] || fail "no $gen prefix with libfmt.a" + local lib; lib=$(find target/vcpkg_installed -path "*-mcpp-*/lib/libfmt.a" | head -1) + [ -n "$lib" ] || fail "no derived -linux-mcpp- prefix with libfmt.a" + gen=$(basename "$(dirname "$(dirname "$lib")")") + find target -path '*deps-vcpkg/triplets/*' -name "$gen.cmake" -exec grep -l 'clang' {} + | grep -q . || + fail "the derived triplet $gen does not record the clang toolset" grep -q 'std::__1::' <(nm -C "$lib") || fail "$lib is not built against libc++" echo "ok: under $llvm the ports build with mcpp's clang and the program links them" @@ -107,6 +133,8 @@ vcpkg_libcxx() { "$MCPP" run | tee target/ci/default-run.log grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/default-run.log || fail "the default toolchain's program did not print through fmt" [ -f "$lib" ] || fail "the default toolchain's installation removed the $gen prefix" + [ "$(ls -d target/vcpkg_installed/*/ | wc -l)" -ge 2 ] || + fail "the default toolchain did not install a prefix of its own" local stamp; stamp=$(find target -path '*deps-vcpkg*' -name "$gen.stamp" | head -1) [ -n "$stamp" ] || fail "no $gen installation stamp" touch -r "$stamp" target/ci/before-switch-back @@ -143,7 +171,7 @@ vcpkg_workspace() { echo "ok: two members that share no dependency both installed and linked one prefix" # The two installations may run at once; vcpkg's own lock on the root makes # the second wait only when asked to, and fails it otherwise ("failed to - # take lock", measured on GalTranslPP under 0.15.0). + # take lock", measured on the validation project under 0.15.0). [ "$(grep -rl -- '--x-wait-for-lock' --include=build.ninja . | wc -l)" -ge 2 ] || fail "a member's installation does not wait for the root's lock" echo "ok: each member's installation waits for another one of the same root" @@ -340,6 +368,126 @@ qt_import_only() { echo "ok: a package that enables rules-qt only for its module runs no build program and reports nothing" } +# THE TEST KIT (0.17.0). The deps members' decisions -- which mechanism, which +# triplet, which C runtime -- tested against stated build contexts on every +# host, with nothing installed. The build program is the test; the verdicts +# are counted against the cases the fixture declares. +plugin_logic() { + cd "$ROOT/tests/plugin-logic" + rm -rf target + mkdir -p target/ci + # On the row without Visual Studio the build program itself needs the + # managed toolset, which that job names in MSVC_MANAGED. + "$MCPP" build ${MSVC_MANAGED:+--toolchain "$MSVC_MANAGED"} > target/ci/build.log 2>&1 || + { cat target/ci/build.log; fail "a plugin-logic case failed"; } + local results declared passed + results=$(find target -path '*plugins-testing/results.txt' | head -1) + [ -n "$results" ] || fail "the kit wrote no results.txt" + declared=$(grep -cE '^ \{ "' build.mcpp) + passed=$(grep -c '^PASS ' "$results") + cat "$results" + [ "$declared" -gt 0 ] && [ "$passed" -eq "$declared" ] || + fail "$passed of $declared declared cases passed" + echo "ok: $passed of $declared plugin-logic cases passed" +} + +# WINDOWS, THE PROGRAM'S C RUNTIME (0.17.0). Run on the Visual Studio row. A +# self-contained program links the C runtime statically, so its ports must be +# built against the same one: the default triplet becomes x64-windows-static, +# and a project triplet that says otherwise is refused naming both statements +# (a static library of the other runtime fails the link with /failifmismatch; +# a DLL of it puts a second C++ runtime into the process without a word). +vcpkg_crt() { + is_windows || { echo "skip: the C runtime linkage is an MSVC-ABI question"; return 0; } + local d="$ROOT/tests/.ci-vcpkg-crt" + rm -rf "$d" + cp -r "$ROOT/tests/vcpkg-consumer" "$d" + trap 'rm -rf "$ROOT/tests/.ci-vcpkg-crt"' RETURN + cd "$d" + rm -rf target vcpkg_installed + printf '\n[build]\ncxx_runtime = "self-contained"\n' >> mcpp.toml + mkdir -p target/ci + "$MCPP" build 2>&1 | tee target/ci/static-build.log + "$MCPP" run | tee target/ci/static-run.log + grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/static-run.log || fail "the self-contained program did not run" + [ -d target/vcpkg_installed/x64-windows-static/x64-windows-static ] || + fail "a self-contained program's ports were not installed with x64-windows-static" + ! find target -path '*/bin/*' -iname 'fmt.dll' | grep -q . || fail "a self-contained program received fmt.dll" + echo "ok: a self-contained program's ports link the C runtime statically, and the link agrees" + + sed -i 's|^ o.libraries = { "fmt" };| o.libraries = { "fmt" };\n o.triplet = "x64-windows";|' build.mcpp + grep -q 'o.triplet = "x64-windows";' build.mcpp || fail "the fixture edit did not apply" + if "$MCPP" build > target/ci/contradiction.log 2>&1; then + cat target/ci/contradiction.log; fail "a triplet contradicting the program's C runtime was accepted" + fi + grep -q "the triplet 'x64-windows' links the C runtime dynamic" target/ci/contradiction.log || + { cat target/ci/contradiction.log; fail "the refusal does not name the triplet's linkage"; } + echo "ok: a triplet that contradicts the program's C runtime is refused, naming both statements" +} + +# WINDOWS WITHOUT VISUAL STUDIO (0.17.0). Run on the row whose Visual Studio is +# masked, with the managed toolset `$MSVC_MANAGED` (xim:msvc@). The +# toolset is named in a derived triplet: a CMake port (fmt) builds, the program +# links it, and a CMake subproject builds with Ninja and the toolset's cl.exe. +vcpkg_managed() { + : "${MSVC_MANAGED:?MSVC_MANAGED names the managed toolset, e.g. xim:msvc@14.44.35207}" + cd "$ROOT/tests/vcpkg-consumer" + rm -rf target vcpkg_installed + mkdir -p target/ci + "$MCPP" build --toolchain "$MSVC_MANAGED" 2>&1 | tee target/ci/managed-build.log + "$MCPP" run --toolchain "$MSVC_MANAGED" | tee target/ci/managed-run.log + grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/managed-run.log || fail "the program did not print through fmt" + ls -d target/vcpkg_installed/x64-windows-mcpp-*/x64-windows-mcpp-*/bin/fmt.dll > /dev/null 2>&1 || + fail "no derived x64-windows-mcpp- prefix with fmt.dll" + grep -rqs -- '--host-triplet=x64-windows-mcpp-' target --include=build.ninja || fail "the host triplet is not the derived one" + echo "ok: without Visual Studio, a CMake port builds with the managed toolset named in a derived triplet" + + cd "$ROOT/tests/cmake-consumer" + rm -rf target + mkdir -p target/ci + "$MCPP" build --toolchain "$MSVC_MANAGED" 2>&1 | tee target/ci/managed-build.log + "$MCPP" run --toolchain "$MSVC_MANAGED" | grep -q '^cmake-consumer: greet says 42$' || fail "the cmake-consumer did not run" + grep -rqs 'CMAKE_GENERATOR:INTERNAL=Ninja' target --include=CMakeCache.txt || fail "the subproject was not configured with Ninja" + grep -rqsi 'CMAKE_CXX_COMPILER:[A-Z]*=.*xim-x-msvc.*cl.exe' target --include=CMakeCache.txt || + fail "the subproject was not configured with the managed toolset's cl.exe" + echo "ok: without Visual Studio, a CMake subproject builds with Ninja and the managed toolset" +} + +# The same row: a port that needs MSBuild is refused by name. +vcpkg_msbuild_refused() { + : "${MSVC_MANAGED:?MSVC_MANAGED names the managed toolset}" + cd "$ROOT/tests/vcpkg-msbuild-port" + rm -rf target + mkdir -p target/ci + if "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1; then + cat target/ci/build.log; fail "an MSBuild port built without Visual Studio" + fi + grep -q "builds with MSBuild" target/ci/build.log || + { tail -60 target/ci/build.log; fail "the MSBuild port failed without the plugin's reason"; } + echo "ok: an MSBuild port is refused by name without Visual Studio" +} + +# The same row: a port that configures with make under msys (icu) finds +# link.exe on the PATH vcpkg keeps. +vcpkg_make_port() { + : "${MSVC_MANAGED:?MSVC_MANAGED names the managed toolset}" + cd "$ROOT/tests/vcpkg-make-port" + rm -rf target + mkdir -p target/ci + if ! "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1; then + tail -80 target/ci/build.log + # autoconf records the invocation and the failing test in config.log, + # which vcpkg's own report does not show. + local cfg + cfg=$(find "$(cygpath -u "$LOCALAPPDATA" 2>/dev/null || echo "$HOME")/vcpkg/mcpp" -path '*icu*' -name config.log 2>/dev/null | head -1) + [ -n "$cfg" ] && { echo "--- $cfg"; head -40 "$cfg"; echo "..."; grep -n "invalid variable\|error" "$cfg" | head -20; } + fail "the make-based port did not build with the managed toolset" + fi + "$MCPP" run --toolchain "$MSVC_MANAGED" | tee target/ci/run.log + grep -qE '^vcpkg-make-port: icu [0-9]' target/ci/run.log || fail "the program did not read icu's version" + echo "ok: a make-based port builds with the managed toolset first on the kept PATH" +} + case "${1:-}" in vcpkg-consumer) vcpkg_consumer ;; vcpkg-libcxx) vcpkg_libcxx ;; @@ -350,5 +498,10 @@ case "${1:-}" in qt-widgets-consumer) qt_widgets_consumer ;; qt-sdk-consumer) qt_sdk_consumer ;; qt-import-only) qt_import_only ;; - *) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only"; exit 2 ;; + plugin-logic) plugin_logic ;; + vcpkg-crt) vcpkg_crt ;; + vcpkg-managed) vcpkg_managed ;; + vcpkg-msbuild-refused) vcpkg_msbuild_refused ;; + vcpkg-make-port) vcpkg_make_port ;; + *) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only|plugin-logic|vcpkg-crt|vcpkg-managed|vcpkg-msbuild-refused|vcpkg-make-port"; exit 2 ;; esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4fa593..1adc803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,11 @@ env: # applies the host's own `[target.]` row (mcpp#704), where the Qt # fixtures state their Linux C++ runtime, and a package with no Qt source # gets no synthesised build program (mcpp#715), which qt-import-only asserts. - MCPP_VERSION: 2026.9.27.1 + # + # 2026.9.28.3 IS WHAT 0.17.0 NEEDS, and the package floor states it: the + # build information `mcpp.plugins.toolset` reads, `mcpp::report`, and one + # placement edge for many files (mcpp#734). + MCPP_VERSION: 2026.9.28.3 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # # Empty on every push and pull request, so the steps run the release above. @@ -1441,7 +1445,7 @@ jobs: # incomplete while the extractor was what broke. # MEMBERS, NOT EVERY FEATURE. A member is a feature named for its # family -- `rules-*`, `tools-*`, `dist-*`, the README's naming table - # -- and a consumer names it. `surface` is not one: it carries the + # -- and a consumer names it. `plugins-core` is not one: it carries the # build-program half of the generated surface, every member implies # it, and a consumer never writes it. Keeping it out keeps this check # meaning "the fixture names every member a consumer can activate"; @@ -1639,6 +1643,21 @@ jobs: done [ "$fail" -eq 0 ] + # ── the general library and the test kit (0.17.0, mcpp#734) ─────────── + # + # The deps members' decisions -- which mechanism reaches the foreign + # system, which triplet, which C runtime -- tested against stated build + # contexts (a Visual Studio row, a managed MSVC row, the Linux GCC and + # libc++ rows) with nothing installed. The verdicts are counted against + # the cases the fixture declares. + - name: the deps members' decisions hold against stated build contexts (mcpp.plugins.testing) + run: bash .github/scripts/check-deps-and-qt.sh plugin-logic + + # A compatibility unit keeps a behaviour for six months after the release + # that replaced it, and this step fails once its date has passed. + - name: compatibility units retire themselves + run: bash .github/scripts/check-compat-retirement.sh + # ── deps-vcpkg, deps-cmake and rules-qt (0.13.0) ────────────────────── # # vcpkg keeps everything it reuses in one per-user directory -- its @@ -1845,7 +1864,7 @@ jobs: # `sources` once per feature and reported the fixture as # incomplete while the extractor was what broke. # MEMBERS, NOT EVERY FEATURE: a feature named for its family - # (`rules-*`, `tools-*`, `dist-*`), which a consumer names; `surface`, + # (`rules-*`, `tools-*`, `dist-*`), which a consumer names; `plugins-core`, # which every member implies and no consumer writes, is not one. By # name and not by "nothing implies it" since 0.11.0, when # `dist-apk-kotlin` and `dist-apk-maven` came to imply `dist-apk`. The @@ -2331,6 +2350,11 @@ jobs: exit 1; } echo "ok: $(grep -m1 'entries declared by dependencies' prov.log)" + # The test kit on this host: the same stated contexts, so a decision that + # depends on the host the build program runs on is caught here. + - name: the deps members' decisions hold against stated build contexts (mcpp.plugins.testing) + run: bash .github/scripts/check-deps-and-qt.sh plugin-logic + # ── deps-vcpkg, deps-cmake and rules-qt (0.13.0) ────────────────────── # # The same criteria as the Linux job, from the same script. On Windows the @@ -2357,6 +2381,13 @@ jobs: - name: deps-cmake builds a CMake subproject as an action and links it run: bash .github/scripts/check-deps-and-qt.sh cmake-consumer + # The C runtime linkage follows the program's contract (0.17.0): a + # self-contained program's ports are x64-windows-static, and a project + # triplet that contradicts it is refused. Windows only; the function + # skips elsewhere. + - name: deps-vcpkg follows the program's C runtime, and refuses a contradicting triplet + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-crt + - name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK the project declares run: bash .github/scripts/check-deps-and-qt.sh qt-consumer @@ -2371,3 +2402,146 @@ jobs: - name: a package that enables rules-qt only for its module builds without a report run: bash .github/scripts/check-deps-and-qt.sh qt-import-only + + # ── deps WITHOUT VISUAL STUDIO (0.17.0, mcpp#734) ─────────────────────────── + # + # The managed toolset `xim:msvc@14.44.35207` on a runner whose Visual Studio + # is masked: no instance exists, so deps-vcpkg and deps-cmake name the + # toolset (the `chain` mechanism). The legs are the ones the design measured + # before implementing it (design §2.2, M3a to M3c and M2c): a CMake port + # (fmt) and a CMake subproject build; a make-based port (icu) finds link.exe + # on the PATH vcpkg keeps; an MSBuild port (libusb) is refused by name. + deps-managed-msvc: + name: deps without Visual Studio (windows x86_64, managed msvc) + runs-on: windows-2022 + timeout-minutes: 150 + defaults: + run: + shell: bash + env: + MSVC_MANAGED: xim:msvc@14.44.35207 + steps: + - uses: actions/checkout@v4 + + - name: Pin MCPP_HOME under this runner's home + run: | + set -e + # The value has to be in the HOST's path syntax, not Git Bash's. + # `$HOME` there is `/c/Users/runneradmin`, and mcpp hands this + # variable to xlings, which is a native Windows program: it answered + # `The filename, directory name, or volume label syntax is + # incorrect.` and exited 1, from inside a provisioning step whose own + # message named a package rather than a path. + H="$HOME/.mcpp" + command -v cygpath >/dev/null && H=$(cygpath -m "$H") + echo "MCPP_HOME=$H" >> "$GITHUB_ENV" + + - name: Cache the mcpp sandbox + uses: actions/cache@v4 + with: + path: ~/.mcpp + key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}-${{ hashFiles('tests/vcpkg-consumer/mcpp.toml') }} + restore-keys: | + mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}- + + - name: Fetch the released mcpp + run: | + set -e + curl -L -fsS --retry 3 --retry-all-errors -o mcpp.pkg \ + "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-windows-x86_64.zip" + case "windows-x86_64.zip" in + *.zip) unzip -q mcpp.pkg ;; + *) tar -xzf mcpp.pkg ;; + esac + dir="mcpp-${MCPP_VERSION}-windows-x86_64" + MCPP="$PWD/$dir/bin/mcpp" + "$MCPP" --version + # The bundled xlings, named explicitly because MCPP_HOME is pinned + # away from the tarball. See the Linux job for the measurement. + # + # The suffix is the host's: the Windows distribution ships + # `registry/bin/xlings.exe`, and a variable pointing at a file that + # does not exist produces `error: xlings binary not found` naming + # three remedies, none of which is the one that applies. + # Two host differences, and the second one is the interesting one. + # The suffix: the Windows distribution ships `xlings.exe`. The PATH + # SHAPE: this job runs under Git Bash, whose `$PWD` is a POSIX path + # (`/d/a/...`), while `mcpp` is a native Windows program and reads + # this variable as a Windows path. Both failures report the same + # `error: xlings binary not found` and offer the same three remedies, + # none of which is "the path you gave is in the other syntax". + XL="$PWD/$dir/registry/bin/xlings" + [ -f "$XL" ] || XL="$XL.exe" + test -f "$XL" || { echo "no vendored xlings at $XL"; exit 1; } + command -v cygpath >/dev/null && XL=$(cygpath -m "$XL") + export MCPP_VENDORED_XLINGS="$XL" + "$MCPP" self config --mirror GLOBAL + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + + # THE ENGINE THE STEPS BELOW RUN: the release fetched above, or mcpp built + # from `MCPP_SOURCE_REF` (see the workflow's `env`). A cold build of mcpp + # is the longest step of the job, which is what its timeout is sized for. + - name: the mcpp under review, built from MCPP_SOURCE_REF when it is set + timeout-minutes: 45 + run: bash .github/scripts/mcpp-under-review.sh + + # THE CRITERION FOR THE STEP ABOVE, IN A LATER STEP, because what that step + # writes to `GITHUB_ENV` governs only the steps after it. + - name: the steps below run the mcpp under review + if: env.MCPP_SOURCE_REF != '' + timeout-minutes: 5 + run: | + set -eu + [ "$MCPP" = "$MCPP_UNDER_REVIEW" ] || { + echo "::error::the steps run $MCPP, and the mcpp built from $MCPP_SOURCE_REF is $MCPP_UNDER_REVIEW"; exit 1; } + version=$("$MCPP" --version | head -1) + [ "$version" = "$MCPP_UNDER_REVIEW_VERSION" ] || { + echo "::error::$MCPP prints '$version', and the build printed '$MCPP_UNDER_REVIEW_VERSION'"; exit 1; } + # A reading, not a criterion: the two engines' versions, which may be + # equal when the reference has not moved the version yet, so the + # criterion above is the path and not the version. + echo "READING released: $("$MCPP_RELEASED" --version | head -1); under review: $version" + echo "ok: the steps run $version, built from $MCPP_SOURCE_REF" + + - name: Install the managed MSVC toolset + run: | + set -e + "$MCPP" toolchain install msvc 14.44.35207 2>&1 | tail -5 + "$MCPP" toolchain list 2>&1 | grep -q 14.44.35207 || { echo "::error::msvc 14.44.35207 is not listed"; exit 1; } + + # The instance is made unreachable the way the measurement did it: vswhere + # and every instance's VC directory are renamed, and the step fails if a + # toolset is still found. + - name: Mask Visual Studio + shell: pwsh + run: | + $vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" } + Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" -ErrorAction SilentlyContinue | ForEach-Object { + try { Rename-Item -LiteralPath $_.Path -NewName "VC.masked" -ErrorAction Stop } + catch { Write-Host "could not rename $($_.Path): $_" } + } + $left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" -Directory -ErrorAction SilentlyContinue + if ($left) { Write-Host "FAIL: VC tools still present after masking"; exit 1 } + Write-Host "ok: no Visual Studio toolset is reachable" + + - name: Cache vcpkg's per-user directory + uses: actions/cache@v4 + with: + path: ~/AppData/Local/vcpkg + key: vcpkg-user-managed-${{ runner.os }}-${{ hashFiles('tests/vcpkg-*/vcpkg.json') }} + restore-keys: vcpkg-user-managed-${{ runner.os }}- + + - name: the test kit on this row + run: bash .github/scripts/check-deps-and-qt.sh plugin-logic + + - name: a CMake port and a CMake subproject build with the managed toolset named + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-managed + + - name: an MSBuild port is refused by name + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-msbuild-refused + + - name: a make-based port finds the toolset on the PATH vcpkg keeps + timeout-minutes: 90 + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-make-port diff --git a/README.md b/README.md index 624a8aa..ef8e47c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ module name the member declares, and configures it there. ```toml [build-dependencies.mcpp] -plugins = { version = "0.16.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.17.0", features = ["rules-spirv"], host-module = true } ``` ```cpp @@ -40,12 +40,17 @@ linked into the artifact (mcpp docs/05 §2.6.1). | deps | `mcpp.deps.` | where a library comes from | | identity | `mcpp.plugins` | the lib root, compiled before every member; it states the collection's version | -The `mcpp.` prefix is reserved for this package. The full account of the -families, and of how the engine routes a file to a rule, is in -[docs/engine-and-rules.md](docs/engine-and-rules.md). +The second segments `core`, `plugins`, `deps`, `rules`, `dist` and `tools` of +`mcpp.` belong to this package and the engine; a third-party plugin names its +modules `mcpp..*` ([docs/plugin-development.md](docs/plugin-development.md#3-layers-names-and-the-engine-floor)). +The full account of the families, and of how the engine routes a file to a +rule, is in [docs/engine-and-rules.md](docs/engine-and-rules.md). ## Members +From 0.17.0 the package as a whole needs mcpp 2026.9.28.3 (`[package] mcpp`); +the column states the release each member's behaviour first needed. + | feature | module | mcpp floor | what it does | doc | |---|---|---|---|---| | `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.6 | Compiles Ascend C (`*.asc`) with BiSheng in mixed mode, so the object joins the ordinary link. | [rules](docs/rules.md#rules-ascendc) | @@ -64,13 +69,23 @@ families, and of how the engine routes a file to a rule, is in | `dist-apple` | `mcpp.dist.apple` | 2026.9.14.2 | Lays out a macOS or iOS application bundle, signs it and writes a disk image. | [dist-apple](docs/dist-apple.md) | | `dist-web` | `mcpp.dist.web` | 2026.9.13.1 | Copies a `wasm32-emscripten` program and its files into a web directory with an `index.html`. | [dist](docs/dist.md#dist-web) | | `dist-apk` | `mcpp.dist.apk` | 2026.9.14.2 | Packs the native closure into a signed APK or App Bundle, with Java, Kotlin and Maven libraries. | [dist-apk](docs/dist-apk.md) | -| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.9.26.2 | Installs a `vcpkg.json` manifest as an action and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) | -| `deps-cmake` | `mcpp.deps.cmake` | 2026.9.26.2 | Builds and installs a CMake subproject as an action and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) | +| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.9.28.3 | Installs a `vcpkg.json` manifest as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) | +| `deps-cmake` | `mcpp.deps.cmake` | 2026.9.28.3 | Builds and installs a CMake subproject as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) | | `deps-archive` | `mcpp.deps.archive` | 2026.9.26.2 | Extracts a zip archive the project keeps and places its tree beside the program. | [deps](docs/deps.md#deps-archive) | Some features add a sub-capability to a member: `dist-apk-kotlin` and -`dist-apk-maven` (Kotlin sources, a Maven graph), and `surface` and `deps`, -which the members imply. A feature states a mechanism and the tools that +`dist-apk-maven` (Kotlin sources, a Maven graph), and `deps`, which the deps +members imply. + +**The general library (0.17.0).** `plugins-core` gives a build program +`mcpp.plugins.declare`, `mcpp.plugins.toolset` (the resolved toolchain, +translated for a foreign build system) and `mcpp.plugins.fs` (deterministic file +generation and placement). Every member implies it; a build program that uses +only these modules, and a third-party plugin built on them, name it on the +dependency edge. `plugins-testing` adds `mcpp.plugins.testing`, which runs a +plugin function against a stated build context and compares what it declared. +Both need mcpp 2026.9.28.3. `surface`, the name of `plugins-core` before +0.17.0, is kept until 2027-03-28. A feature states a mechanism and the tools that mechanism runs; the libraries and SDKs a program links are the project's declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). @@ -80,6 +95,12 @@ declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). mcpp.toml the package: one feature per member src/plugins.cppm export module mcpp.plugins; the version, mcpp::plugins::surface (the declarations a consumer names) and mcpp::plugins::xml +src/declare.cppm, the general library for build programs (`plugins-core`): +src/toolset.cppm, mcpp.plugins.declare, mcpp.plugins.toolset, mcpp.plugins.fs +src/fs.cppm +src/testing.cppm mcpp.plugins.testing (`plugins-testing`) +src/compat/, deps/compat/ + compatibility units, each retired on the date its header states rules/.cppm export module mcpp.rules.; tools/.cppm export module mcpp.tools.; dist/.cppm export module mcpp.dist.; diff --git a/deps/archive.cppm b/deps/archive.cppm index 29faab7..74eef89 100644 --- a/deps/archive.cppm +++ b/deps/archive.cppm @@ -32,6 +32,7 @@ import std; import mcpp; import mcpp.plugins; import mcpp.deps; +import mcpp.plugins.fs; export namespace mcpp::deps::archive { @@ -51,7 +52,7 @@ struct options { struct result { // One entry per file of the archive: the extracted copy and its path // beside the program. - std::vector files; + std::vector files; bool ok = false; explicit operator bool() const { return ok; } }; @@ -191,7 +192,7 @@ inline result unpack(const options& opt) { const std::string arc = mcpp::deps::generic(archive); const std::string dst = mcpp::deps::generic(into); const fs::path script = fs::path(mcpp::out_dir()) / "deps-archive" / (name + ".cmake"); - mcpp::deps::write_if_changed(script, + mcpp::plugins::fs::write_if_changed(script, "# Written by mcpp.deps.archive: extract " + archive.filename().string() + ".\n" "file(REMOVE_RECURSE " + bracket(dst) + ")\n" "file(MAKE_DIRECTORY " + bracket(dst) + ")\n" diff --git a/deps/cmake.cppm b/deps/cmake.cppm index b996c35..3808561 100644 --- a/deps/cmake.cppm +++ b/deps/cmake.cppm @@ -19,13 +19,17 @@ // include directory, libraries by full path, the shared libraries deployed // beside the program. // -// THE COMPILER IS CMAKE'S OWN CHOICE, EXCEPT WHERE ITS C++ LIBRARY DIFFERS. A -// subproject is configured the way its authors build it -- on Windows, CMake's -// default generator and the Visual Studio toolset it finds. On Linux under a -// libc++ toolchain the compilers are mcpp's own clang (`-DCMAKE_C_COMPILER`, -// `-DCMAKE_CXX_COMPILER`), because the host compiler CMake finds uses libstdc++ -// and the two do not link (`mcpp::deps::program_compilers`). A project that -// passes either compiler or a toolchain file through `cache_args` decides. +// THE COMPILER IS THE PROGRAM'S (0.17.0). What the subproject builds is linked +// into the program, so it is built with the toolset mcpp resolved +// (`mcpp.plugins.toolset`). A toolset from a Visual Studio instance keeps +// CMake's Visual Studio generator, pointed at that instance +// (`CMAKE_GENERATOR_INSTANCE`) and toolset version (`-T version=`); any other +// toolset is named by path, with the Ninja generator and mcpp's own ninja, and +// runs with the environment the engine runs it with. On the MSVC ABI the C +// runtime follows the program's (`CMAKE_MSVC_RUNTIME_LIBRARY`). A project that +// passes either compiler or a toolchain file through `cache_args` decides, +// and `options::toolset` set to `detected` keeps 0.16.0's behaviour -- CMake's +// default generator and the toolset it finds -- until 2027-03-28. // // `xim:cmake` is declared by this feature; `options::cmake` names another. @@ -35,6 +39,8 @@ import std; import mcpp; import mcpp.plugins; import mcpp.deps; +import mcpp.plugins.fs; +import mcpp.plugins.toolset; export namespace mcpp::deps::cmake { @@ -69,14 +75,32 @@ struct options { std::string cmake; // Files of the prefix placed beside the program, as `mcpp.deps.vcpkg` // takes them (`{"bin/tool.cfg", "."}`). - std::vector deploy; + std::vector deploy; + // Which toolset builds the subproject (0.17.0). The default `resolved` is + // the toolset mcpp builds the program with: a Visual Studio instance keeps + // CMake's Visual Studio generator, pointed at that instance and toolset + // version; any other toolset is named, with the Ninja generator and + // mcpp's own ninja. `detected` lets CMake find its own toolset, as 0.16.0 + // did, until 2027-03-28. Compilers or a toolchain file in `cache_args` + // decide instead. See docs/deps.md. + mcpp::plugins::toolset::choice toolset; + // `default` keeps the generator the mechanism implies; `ninja` uses the + // Ninja generator with mcpp's ninja under an instance as well. + enum class generator_kind { default_, ninja }; + generator_kind generator = generator_kind::default_; + // The C runtime linkage on the MSVC ABI, "static" or "dynamic" + // (`CMAKE_MSVC_RUNTIME_LIBRARY`). Empty follows the program's C++ runtime + // contract (`mcpp::msvc_crt_linkage()`). + std::string crt_linkage; }; // The prefix, by name (SPEC-007 R1.3). struct prefix { std::string root, include, lib, bin; // The copies `options::deploy` produced, for a project's own layout. - std::vector deployed; + std::vector deployed; + // How the toolset reached CMake: "instance", "chain" or "detected". + std::string mechanism; explicit operator bool() const { return !root.empty(); } }; @@ -96,6 +120,7 @@ inline std::string cmake_exe(const options& opt) { inline prefix use(const options& opt) { namespace fs = std::filesystem; + namespace ts = mcpp::plugins::toolset; constexpr std::string_view who = "mcpp.deps.cmake"; mcpp::fact("mcpp.plugins", std::string(mcpp::plugins::version).c_str()); @@ -108,16 +133,55 @@ inline prefix use(const options& opt) { "`git submodule update --init`.\n", who, opt.source); return {}; } + + // THE TOOLSET. Compilers or a toolchain file among the cache arguments + // are the project's decision, and so is a generator it names. + auto names = [&](std::initializer_list keys) { + return std::ranges::any_of(opt.cache_args, [&](const std::string& x) { + return std::ranges::any_of(keys, [&](std::string_view k) { return x.starts_with(k); }); + }); + }; + const bool chosen = std::ranges::any_of(opt.cache_args, [](const std::string& x) { + return x.contains("CMAKE_C_COMPILER") || x.contains("CMAKE_CXX_COMPILER") + || x.contains("CMAKE_TOOLCHAIN_FILE"); + }); + const bool generatorChosen = names({"-G", "-DCMAKE_GENERATOR"}); + const bool wantNinja = opt.generator == options::generator_kind::ninja; + auto tools = wantNinja ? ts::resolve_named(opt.toolset) : ts::resolve(opt.toolset); + if (!tools) { + std::cerr << std::format("{}: {}\n", who, tools.error()); + return {}; + } + const std::string crt = opt.crt_linkage.empty() ? tools->crt : opt.crt_linkage; + if (!crt.empty() && crt != "static" && crt != "dynamic") { + std::cerr << std::format("{}: options::crt_linkage is '{}'; it is \"static\" or \"dynamic\".\n", + who, crt); + return {}; + } + const std::string ninja = mcpp::ninja_program(); + const bool named = !chosen && tools->how == ts::mechanism::chain && !tools->cxx.empty(); + const bool useNinja = named && !generatorChosen && !ninja.empty(); + const bool instance = !chosen && !generatorChosen && tools->how == ts::mechanism::instance; + + // One build directory per toolset statement: CMake refuses a cache made + // with another generator or instance. What 0.16.0 configured stays in + // `build/`. + std::string key; + if (useNinja) key = "ninja;" + tools->identity + ";" + tools->cxx; + else if (named) key = "named;" + tools->identity + ";" + tools->cxx; + else if (instance) key = "instance;" + tools->instance_dir + ";" + tools->toolset_version; const std::string name = opt.name.empty() ? source.filename().string() : opt.name; const fs::path base = fs::path(mcpp::out_dir()) / "deps-cmake" / name; - const fs::path build = base / "build"; + const fs::path build = base / (key.empty() ? std::string("build") : "build-" + mcpp::deps::short_name(key)); const fs::path root = base / "install"; prefix p; - p.root = mcpp::deps::generic(root); - p.include = mcpp::deps::generic(root / opt.dirs.include); - p.lib = mcpp::deps::generic(root / opt.dirs.lib); - p.bin = mcpp::deps::generic(root / opt.dirs.bin); + p.root = mcpp::deps::generic(root); + p.include = mcpp::deps::generic(root / opt.dirs.include); + p.lib = mcpp::deps::generic(root / opt.dirs.lib); + p.bin = mcpp::deps::generic(root / opt.dirs.bin); + p.mechanism = std::string(ts::name(named ? ts::mechanism::chain + : instance ? ts::mechanism::instance : ts::mechanism::detected)); const std::string cmake = cmake_exe(opt); if (cmake.empty()) { @@ -146,16 +210,38 @@ inline prefix use(const options& opt) { } configure.push_back("-DCMAKE_PREFIX_PATH=" + joined); } - const bool chosen = std::ranges::any_of(opt.cache_args, [](const std::string& x) { - return x.contains("CMAKE_C_COMPILER") || x.contains("CMAKE_CXX_COMPILER") - || x.contains("CMAKE_TOOLCHAIN_FILE"); - }); - if (const auto cc = mcpp::deps::program_compilers(); cc && !chosen) { - configure.push_back("-DCMAKE_C_COMPILER=" + cc.c); - configure.push_back("-DCMAKE_CXX_COMPILER=" + cc.cxx); + if (useNinja) { + configure.push_back("-G"); + configure.push_back("Ninja"); + configure.push_back("-DCMAKE_MAKE_PROGRAM=" + ts::forward(ninja)); + } + if (named) { + configure.push_back("-DCMAKE_C_COMPILER=" + tools->cc); + configure.push_back("-DCMAKE_CXX_COMPILER=" + tools->cxx); + if (tools->msvc_abi) { + if (!tools->rc.empty()) configure.push_back("-DCMAKE_RC_COMPILER=" + tools->rc); + if (!tools->mt.empty()) configure.push_back("-DCMAKE_MT=" + tools->mt); + if (!tools->ld.empty()) configure.push_back("-DCMAKE_LINKER=" + tools->ld); + } + } + if (instance) { + // CMake's documented selection of a Visual Studio instance and of + // a toolset version within it. + configure.push_back("-DCMAKE_GENERATOR_INSTANCE=" + tools->instance_dir); + if (!tools->toolset_version.empty() && tools->toolset_version != tools->instance_default) { + configure.push_back("-T"); + configure.push_back("version=" + tools->toolset_version); + } + } + // The C runtime follows the program's (CMake 3.15+, policy CMP0091). + if (tools->msvc_abi && !crt.empty() && !names({"-DCMAKE_MSVC_RUNTIME_LIBRARY"})) { + configure.push_back("-DCMAKE_POLICY_DEFAULT_CMP0091=NEW"); + configure.push_back(std::string("-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded$<$:Debug>") + + (crt == "dynamic" ? "DLL" : "")); } for (auto const& x : opt.cache_args) configure.push_back(x); std::string text = "# Written by mcpp.deps.cmake: configure, build and install " + name + ".\n" + "# toolset: " + ts::describe(*tools) + "\n" "execute_process(COMMAND ${CMAKE_COMMAND}"; for (auto const& x : configure) text += "\n " + bracket(x); text += "\n RESULT_VARIABLE rc)\n" @@ -163,13 +249,32 @@ inline prefix use(const options& opt) { "execute_process(COMMAND ${CMAKE_COMMAND} --build " + bracket(mcpp::deps::generic(build)) + " --config " + bracket(opt.config) + " --target install --parallel\n RESULT_VARIABLE rc)\n" "if(NOT rc EQUAL 0)\n message(FATAL_ERROR \"build and install exited ${rc}\")\nendif()\n"; - mcpp::deps::write_if_changed(script, text); + mcpp::plugins::fs::write_if_changed(script, text); const std::string scriptS = mcpp::deps::generic(script); mcpp::action a; a.id = id.c_str(); a.role = mcpp::roles::prepare; a.description = desc.c_str(); a.arg(cmake.c_str()).arg("-P").arg(scriptS.c_str()); + // Named tools run with the environment the engine runs them with, and + // their directories first on PATH. + if (named) { + for (auto const& [k, v] : tools->env) a.env(k.c_str(), v.c_str()); + if (!tools->path_dirs.empty()) { + const char sep = ts::path_separator(); + std::string path; + for (auto const& d : tools->path_dirs) { + if (!path.empty()) path += sep; + std::string n = d; + if (ts::host_is_windows()) + for (std::size_t i = 0; i < n.size(); ++i) if (n[i] == '/') n[i] = '\\'; + path += n; + } + const char* cur = std::getenv("PATH"); + if (cur && *cur) { path += sep; path += cur; } + a.env("PATH", path.c_str()); + } + } a.input(cmake.c_str()); a.input(scriptS.c_str()); for (auto const& f : mcpp::deps::files_under(source)) a.input(f.c_str()); @@ -177,7 +282,7 @@ inline prefix use(const options& opt) { a.output(stamp.c_str()); a.output_dir(p.root.c_str()); a.submit(); - p.deployed = mcpp::deps::deploy_after("deps-cmake-" + name, stamp, fs::path(p.root), opt.deploy); + p.deployed = mcpp::plugins::fs::deploy_after("deps-cmake-" + name, stamp, fs::path(p.root), opt.deploy); } mcpp::include_dir(p.include.c_str()); diff --git a/deps/compat/names.cppm b/deps/compat/names.cppm new file mode 100644 index 0000000..3fbe9e7 --- /dev/null +++ b/deps/compat/names.cppm @@ -0,0 +1,64 @@ +// COMPATIBILITY UNIT +// keeps: the 0.16.0 names in `mcpp::deps`: `write_if_changed`, +// `deploy_entry`, `deployed_file`, `deploy_after`, +// `compilers` and `program_compilers` +// since: 0.17.0 (2026-09-28) +// retires: 2027-03-28 +// replacement: `mcpp::plugins::fs` (module `mcpp.plugins.fs`) for the first +// four; `mcpp::plugins::toolset::resolve` for the last two +// note: "mcpp.plugins: mcpp::deps::program_compilers is kept until +// 2027-03-28; ..." (printed by `program_compilers`; the other +// names are aliases, which print nothing) +// +// `mcpp.deps` re-exports this module, so a build program that imported +// `mcpp.deps` for these names keeps compiling. Retirement removes this file, +// its entry in `[features.deps]` and the re-export in `deps/deps.cppm`. +// `.github/scripts/check-compat-retirement.sh` fails once the date has passed. + +export module mcpp.deps.compat; + +import std; +import mcpp; +import mcpp.plugins.fs; + +export namespace mcpp::deps { + +using deploy_entry = mcpp::plugins::fs::deploy_entry; +using deployed_file = mcpp::plugins::fs::deployed_file; +using mcpp::plugins::fs::write_if_changed; +using mcpp::plugins::fs::deploy_after; + +struct compilers { + std::string c, cxx; + explicit operator bool() const { return !cxx.empty(); } +}; + +// 0.16.0's answer: mcpp's clang on the Linux libc++ row, nothing elsewhere. +// Computed here rather than through `mcpp.plugins.toolset`: `mcpp.deps` +// re-exports this unit, and with the toolset module in that chain GCC 16 +// emitted no `std::vector::push_back(std::string&&)` for a build +// program calling `mcpp::deps::archive::unpack` (undefined at its link). +inline compilers program_compilers() { + static bool noted = false; + if (!noted) { + noted = true; + mcpp::report({ + .severity = "note", + .message = "mcpp.plugins: mcpp::deps::program_compilers is kept until 2027-03-28", + .impact = "none until then; the name is removed afterwards", + .hint = "use mcpp::plugins::toolset::resolve({.cc = mcpp::plugins::toolset::compiler::row}), " + "which names the row's tools on every row", + }); + } + if (std::string_view(mcpp::target_os()) != "linux" + || std::string_view(mcpp::cxx_stdlib()) != "libc++" + || std::string_view(mcpp::compiler()) != "clang" + || std::string_view(mcpp::host()) != std::string_view(mcpp::target())) return {}; + const std::filesystem::path bin = std::filesystem::path(mcpp::toolchain_dir()) / "bin"; + std::error_code ec; + if (!std::filesystem::is_regular_file(bin / "clang++", ec) || !std::filesystem::is_regular_file(bin / "clang", ec)) + return {}; + return { mcpp::plugins::fs::generic(bin / "clang"), mcpp::plugins::fs::generic(bin / "clang++") }; +} + +} // namespace mcpp::deps diff --git a/deps/deps.cppm b/deps/deps.cppm index 6621f82..3c68c87 100644 --- a/deps/deps.cppm +++ b/deps/deps.cppm @@ -34,6 +34,9 @@ export module mcpp.deps; import std; import mcpp; import mcpp.plugins; +// The 0.16.0 names of the helpers that moved to `mcpp.plugins.fs` and +// `mcpp.plugins.toolset` (a compatibility unit, until 2027-03-28). +export import mcpp.deps.compat; export namespace mcpp::deps { @@ -204,108 +207,4 @@ inline std::string short_name(std::string_view text) { return std::format("{:08x}", std::uint32_t(h ^ (h >> 32))); } -// ── The compiler a subproject builds with ────────────────────────────────── -// -// A prefix's C++ libraries are linked into the program, so their C++ standard -// library must be the program's. On Windows every compiler of the MSVC ABI -// uses Microsoft's, and on macOS every compiler uses libc++; on Linux two -// incompatible libraries exist, the host compiler that vcpkg and CMake choose -// uses libstdc++, and mcpp's clang uses libc++ (`std::__1::`). So when the -// program's library is libc++ on Linux, a subproject builds with mcpp's own -// clang, whose configuration file names libc++ and the C library mcpp links -// against; otherwise the installer's own choice already agrees and nothing is -// stated. -struct compilers { - std::string c, cxx; - explicit operator bool() const { return !cxx.empty(); } -}; - -inline compilers program_compilers() { - namespace fs = std::filesystem; - if (std::string_view(mcpp::target_os()) != "linux" - || std::string_view(mcpp::cxx_stdlib()) != "libc++" - || std::string_view(mcpp::compiler()) != "clang" - || std::string_view(mcpp::host()) != std::string_view(mcpp::target())) return {}; - const fs::path bin = fs::path(mcpp::toolchain_dir()) / "bin"; - std::error_code ec; - if (!fs::is_regular_file(bin / "clang++", ec) || !fs::is_regular_file(bin / "clang", ec)) return {}; - return { generic(bin / "clang"), generic(bin / "clang++") }; -} - -// Writes `content` to `file` unless the file already holds it, so a file the -// build program generates keeps its time stamp from one plan to the next and -// the action that reads it does not run again. -inline void write_if_changed(const std::filesystem::path& file, const std::string& content) { - namespace fs = std::filesystem; - std::error_code ec; - if (fs::is_regular_file(file, ec)) { - std::ifstream in(file, std::ios::binary); - const std::string old{ std::istreambuf_iterator(in), std::istreambuf_iterator() }; - if (old == content) return; - } - fs::create_directories(file.parent_path(), ec); - std::ofstream(file, std::ios::binary | std::ios::trunc) << content; -} - -// ── Files beside the program ─────────────────────────────────────────────── -// -// Data a program reads at run time from beside itself -- a dictionary, a -// configuration, an embedded interpreter -- is DEPLOYED (`mcpp::deploy`): the -// engine places it beside the program after the build, `mcpp run` finds it -// there, and `mcpp pack` carries it. A deployed file must be one the build -// graph knows how to produce, and a file that a `prepare` action installs is -// not: its name is unknown to the graph until the action has run. So the file -// is first copied by an action that NAMES it as an output (SPEC-007 R3.2), and -// the copy is deployed. Everything here is declared before anything exists, so -// `mcpp emit build-database` plans it on a machine that never built. - -// One file of a prefix to place beside the program: `file` relative to the -// prefix root (`share/opencc/t2s.json`), `to` the directory beside the program -// it is placed in (`BaseConfig/opencc`; empty or `.` is the program's own -// directory). -struct deploy_entry { - std::string file; - std::string to; -}; - -// A file this package deploys: `path`, the copy the build produces (a node of -// the build graph, which a project's own actions may take as an input), and -// `to`, its path beside the program. -struct deployed_file { - std::string path; - std::string to; -}; - -// Copies each entry's file out of `root` once the action that writes `stamp` -// has run, and deploys the copy. `who` names the member in action ids. -inline std::vector deploy_after(std::string_view who, const std::string& stamp, - const std::filesystem::path& root, - std::span entries) { - namespace fs = std::filesystem; - std::vector out; - const fs::path base = fs::path(mcpp::out_dir()) / "deps-deploy" / std::string(who); - for (auto const& e : entries) { - const fs::path rel = fs::path(e.file).lexically_normal(); - const std::string src = generic(root / rel); - const std::string copy = generic(base / rel); - // Index loops over a string: GCC 16 fails to inline a string iterator - // inside a module's interface. - std::string key = rel.generic_string(); - for (std::size_t i = 0; i < key.size(); ++i) if (key[i] == '/') key[i] = '.'; - const std::string id = std::format("{}:deploy:{}", who, key); - const std::string desc = std::format("DEPLOY {}", rel.generic_string()); - mcpp::action a; - a.id = id.c_str(); - a.role = mcpp::roles::artifact; - a.description = desc.c_str(); - a.arg("${mcpp.self}").arg("stage").arg("--output").arg(copy.c_str()).arg(src.c_str()) - .input(stamp.c_str()).output(copy.c_str()).submit(); - const std::string dir = e.to.empty() ? std::string(".") : e.to; - mcpp::deploy(copy.c_str(), dir.c_str()); - const std::string to = generic(fs::path(dir) / rel.filename()); - out.push_back({copy, to}); - } - return out; -} - } // namespace mcpp::deps diff --git a/deps/vcpkg.cppm b/deps/vcpkg.cppm index 68bbf5c..e5ac791 100644 --- a/deps/vcpkg.cppm +++ b/deps/vcpkg.cppm @@ -42,15 +42,18 @@ import std; import mcpp; import mcpp.plugins; import mcpp.deps; +import mcpp.plugins.fs; +import mcpp.plugins.toolset; export namespace mcpp::deps::vcpkg { struct options { - // The vcpkg triplet. Empty derives it from the target: `x64-windows`, + // The vcpkg triplet. Empty derives it from the target: `x64-windows` + // (`x64-windows-static` for a statically linked C runtime), // `arm64-windows`, `x64-mingw-dynamic`, `x64-linux`, `arm64-linux`, - // `x64-osx`, `arm64-osx`; on Linux under a libc++ toolchain, the generated - // `x64-linux-libcxx` or `arm64-linux-libcxx`, whose ports build with mcpp's - // clang. A custom triplet is named here and found through the manifest's + // `x64-osx`, `arm64-osx`. Under the `chain` mechanism the installation uses + // a triplet derived from this one, which names the resolved tools. A + // custom triplet is named here and found through the manifest's // `overlay-triplets` like any other. std::string triplet; // Library names in link order: `fmt` denotes `lib/fmt.lib` on Windows and @@ -77,8 +80,23 @@ struct options { std::string vcpkg_root; // Files of the prefix the program reads at run time, placed beside it // (`{"share/opencc/t2s.json", "BaseConfig/opencc"}`): `mcpp run` finds them - // and `mcpp pack` carries them. See `mcpp::deps::deploy_after`. - std::vector deploy; + // and `mcpp pack` carries them. See `mcpp::plugins::fs::deploy_after`. + std::vector deploy; + // Which toolset builds the ports (0.17.0). The default `resolved` is the + // toolset mcpp builds the program with: a Visual Studio instance is + // selected with `VCPKG_VISUAL_STUDIO_PATH` (every port kind builds, and + // with the instance vcpkg would choose anyway nothing is rebuilt); any + // other toolset is named in a derived triplet `-mcpp-` + // (CMake and make ports build; an MSBuild port needs Visual Studio and is + // refused by name). `detected` lets vcpkg find its own toolset, as 0.16.0 + // did, until 2027-03-28. See docs/deps.md. + mcpp::plugins::toolset::choice toolset; + // The C runtime linkage of the ports on the MSVC ABI, "static" or + // "dynamic". Empty follows the program's C++ runtime contract + // (`mcpp::msvc_crt_linkage()`): a `self-contained` program links it + // statically, so the default triplet becomes `-windows-static`. A + // triplet named in `triplet` whose linkage contradicts it is refused. + std::string crt_linkage; }; // The prefix, by name: the installation fills it during the build, and the @@ -91,18 +109,26 @@ struct prefix { std::string share; // root/share std::string triplet; // The copies `options::deploy` produced, for a project's own layout. - std::vector deployed; + std::vector deployed; + // How the toolset reached vcpkg: "instance", "chain" or "detected". + std::string mechanism; explicit operator bool() const { return !root.empty(); } }; // ─── The triplet ─────────────────────────────────────────────────────────── -inline std::string default_triplet() { +// The target's standard triplet. `crt` is the C runtime linkage on the MSVC +// ABI: "static" selects `-windows-static`, whose C runtime and libraries +// are both static. +inline std::string default_triplet(std::string_view crt = {}) { const std::string os = mcpp::target_os(), arch = mcpp::target_arch(), env = mcpp::target_env(); const std::string a = arch == "x86_64" ? "x64" : arch == "aarch64" ? "arm64" : (arch == "i686" || arch == "x86") ? "x86" : arch; - if (os == "windows") return env == "gnu" ? a + "-mingw-dynamic" : a + "-windows"; + if (os == "windows") { + if (env == "gnu") return a + "-mingw-dynamic"; + return crt == "static" ? a + "-windows-static" : a + "-windows"; + } if (os == "macos") return a + "-osx"; if (os == "linux") return a + "-linux"; return {}; @@ -129,48 +155,151 @@ inline std::vector manifest_overlays(const std::filesyste return out; } -// Whether the triplet links libraries as shared objects. Read from the -// triplet file itself -- `set(VCPKG_LIBRARY_LINKAGE dynamic)` outside any -// `if()`, which is where a per-port exception lives -- and otherwise from -// vcpkg's convention: dynamic on Windows, static elsewhere, a `-dynamic` -// suffix for the community triplets that say so in their name. -inline bool shared_linkage(const std::string& triplet, - std::span search) { +// The file that defines `triplet`, searched in vcpkg's order: overlays first. +inline std::filesystem::path triplet_file(const std::string& triplet, + std::span search) { std::error_code ec; for (auto const& dir : search) { const auto file = dir / (triplet + ".cmake"); - if (!std::filesystem::is_regular_file(file, ec)) continue; - mcpp::rerun_if_changed(mcpp::deps::generic(file).c_str()); - std::ifstream in(file); - std::string line; - int depth = 0; - while (std::getline(in, line)) { - // By index: under GCC 16 a range-for over a non-const std::string in - // a module unit fails with "inlining failed in call to always_inline - // ... function body not available" (measured on this file). - std::string s(line.size(), ' '); - for (std::size_t i = 0; i < line.size(); ++i) - s[i] = char(std::tolower(static_cast(line[i]))); - const auto first = s.find_first_not_of(" \t"); - if (first == std::string::npos || s[first] == '#') continue; - s = s.substr(first); - if (s.starts_with("if(") || s.starts_with("if (")) ++depth; - else if (s.starts_with("endif(") || s.starts_with("endif (")) { if (depth) --depth; } - else if (depth == 0 && s.starts_with("set(vcpkg_library_linkage")) { - return s.find("dynamic") != std::string::npos; - } + if (std::filesystem::is_regular_file(file, ec)) return file; + } + return {}; +} + +inline std::string read_text(const std::filesystem::path& file) { + std::ifstream in(file, std::ios::binary); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +// The value a triplet file gives `variable` outside any `if()` -- where a +// per-port exception lives -- the last such `set()` winning, as in CMake. +// Empty when the file does not set it there. +inline std::string top_level_setting(const std::string& text, std::string_view variable) { + std::string want = "set("; + for (std::size_t i = 0; i < variable.size(); ++i) + want += char(std::tolower(static_cast(variable[i]))); + std::string value; + int depth = 0; + std::size_t pos = 0; + while (pos < text.size()) { + const auto nl = text.find('\n', pos); + const std::string line = text.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos); + pos = nl == std::string::npos ? text.size() : nl + 1; + // By index: under GCC 16 a range-for over a non-const std::string in + // a module unit fails with "inlining failed in call to always_inline + // ... function body not available" (measured on this file). + std::string s(line.size(), ' '); + for (std::size_t i = 0; i < line.size(); ++i) + s[i] = char(std::tolower(static_cast(line[i]))); + const auto first = s.find_first_not_of(" \t"); + if (first == std::string::npos || s[first] == '#') continue; + s = s.substr(first); + if (s.starts_with("if(") || s.starts_with("if (")) ++depth; + else if (s.starts_with("endif(") || s.starts_with("endif (")) { if (depth) --depth; } + else if (depth == 0 && s.starts_with(want)) { + std::string rest = s.substr(want.size()); + const auto close = rest.find(')'); + rest = rest.substr(0, close); + std::string v; + for (std::size_t i = 0; i < rest.size(); ++i) + if (rest[i] != ' ' && rest[i] != '\t' && rest[i] != '"') v += rest[i]; + value = v; } - break; + } + return value; +} + +// Whether the triplet links libraries as shared objects: its own +// `VCPKG_LIBRARY_LINKAGE`, and otherwise vcpkg's convention: dynamic on +// Windows, static elsewhere, a `-dynamic` suffix for the community triplets +// that say so in their name. +inline bool shared_linkage(const std::string& triplet, + std::span search) { + if (const auto file = triplet_file(triplet, search); !file.empty()) { + mcpp::rerun_if_changed(mcpp::deps::generic(file).c_str()); + const auto v = top_level_setting(read_text(file), "VCPKG_LIBRARY_LINKAGE"); + if (!v.empty()) return v == "dynamic"; } if (triplet.ends_with("-dynamic")) return true; if (triplet.ends_with("-static") || triplet.ends_with("-static-md")) return false; return mcpp::deps::is_windows(); } +// The vcpkg toolchain a chain-loaded toolchain file includes after naming the +// tools, so that ports keep vcpkg's standard flags for the target system. +inline std::string_view vcpkg_system_toolchain() { + const std::string_view os = mcpp::target_os(); + if (os == "windows") return std::string_view(mcpp::target_env()) == "gnu" ? "mingw" : "windows"; + if (os == "macos") return "osx"; + return "linux"; +} + +// The chain-loaded toolchain file. It holds no path: every tool is read from +// the installation's environment (`MCPP_VCPKG_*`, passed through untracked), +// so vcpkg's ABI hash sees the toolset's identity and its compilers and never +// where they are. A Windows path goes through `file(TO_CMAKE_PATH)`, since a +// backslash in a CMake string is an escape. +inline std::string chain_toolchain_text() { + std::string t = + "# Written by mcpp.deps.vcpkg: the tools mcpp resolved, read from the\n" + "# environment of the installation, then vcpkg's own toolchain for the\n" + "# target system.\n"; + for (auto const& [var, env] : { std::pair + {"CMAKE_C_COMPILER", "MCPP_VCPKG_CC"}, + {"CMAKE_CXX_COMPILER", "MCPP_VCPKG_CXX"}, + {"CMAKE_RC_COMPILER", "MCPP_VCPKG_RC"}, + {"CMAKE_MT", "MCPP_VCPKG_MT"} }) { + t += std::format("if(DEFINED ENV{{{0}}} AND NOT \"$ENV{{{0}}}\" STREQUAL \"\")\n" + " file(TO_CMAKE_PATH \"$ENV{{{0}}}\" z_mcpp_tool)\n" + " set({1} \"${{z_mcpp_tool}}\")\n" + "endif()\n", env, var); + } + t += "if(CMAKE_HOST_APPLE AND NOT CMAKE_OSX_SYSROOT)\n" + " # A compiler other than Apple's has no default SDK.\n" + " execute_process(COMMAND xcrun --show-sdk-path OUTPUT_VARIABLE z_mcpp_sdk\n" + " OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)\n" + " if(z_mcpp_sdk)\n set(CMAKE_OSX_SYSROOT \"${z_mcpp_sdk}\")\n endif()\n" + "endif()\n" + "file(TO_CMAKE_PATH \"$ENV{MCPP_VCPKG_ROOT}\" z_mcpp_vcpkg_root)\n"; + t += std::format("include(\"${{z_mcpp_vcpkg_root}}/scripts/toolchains/{}.cmake\")\n", + vcpkg_system_toolchain()); + return t; +} + +// What an MSBuild port meets under `chain`: vcpkg runs MSBuild with +// `/p:PlatformToolset=external` and fails without naming the cause ("msbuild: +// no such file or directory" when no Visual Studio exists). The MSBuild +// helpers read `VCPKG_PLATFORM_TOOLSET` to build that argument; the watch +// stops the port there and says why. The helper is recognised in the watch's +// CALL STACK, not its current file: a read inside a function reports the +// caller's file (the portfile), and the stack names the file that defines the +// function (measured with CMake 4.4). The pattern names the helper's own +// directory and files, so a project whose path contains "msbuild" is not +// refused. +inline std::string msbuild_refusal_text(std::string_view identity) { + return std::format( + "# mcpp.deps.vcpkg: a port that builds with MSBuild needs a Visual Studio\n" + "# instance, and this toolset comes from none.\n" + "if(PORT AND NOT DEFINED Z_MCPP_MSBUILD_WATCH)\n" + " set(Z_MCPP_MSBUILD_WATCH 1)\n" + " function(z_mcpp_msbuild_watch variable access value current_file stack)\n" + " if(access STREQUAL \"READ_ACCESS\" AND stack MATCHES " + "\"/vcpkg-msbuild/|vcpkg_install_msbuild\\\\.cmake|vcpkg_build_msbuild\\\\.cmake\")\n" + " message(FATAL_ERROR \"mcpp.deps.vcpkg: the port '${{PORT}}' builds with MSBuild, which " + "needs a Visual Studio instance; the toolset mcpp resolved ({}) comes from none. Build with " + "the toolchain msvc@system on a machine with Visual Studio, or set the deps-vcpkg option " + "toolset.toolset to detected for this project.\")\n" + " endif()\n" + " endfunction()\n" + " variable_watch(VCPKG_PLATFORM_TOOLSET z_mcpp_msbuild_watch)\n" + "endif()\n", identity); +} + // ─── The member ──────────────────────────────────────────────────────────── inline prefix use(const options& opt = {}) { namespace fs = std::filesystem; + namespace ts = mcpp::plugins::toolset; constexpr std::string_view who = "mcpp.deps.vcpkg"; mcpp::fact("mcpp.plugins", std::string(mcpp::plugins::version).c_str()); @@ -190,75 +319,175 @@ inline prefix use(const options& opt = {}) { return {}; } - // THE C++ LIBRARY. Where the host compiler's C++ library is not the - // program's -- libc++ on Linux -- the default triplet is a generated one, - // `-libcxx`, whose ports build with mcpp's own clang - // (`mcpp::deps::program_compilers`). A triplet the project names is used - // as it stands. - const mcpp::deps::compilers cc = opt.triplet.empty() ? mcpp::deps::program_compilers() - : mcpp::deps::compilers{}; - const std::string triplet = !opt.triplet.empty() ? opt.triplet - : cc ? default_triplet() + "-libcxx" : default_triplet(); - if (triplet.empty() || triplet == "-libcxx") { - std::cerr << std::format("{}: no default vcpkg triplet for the target '{}'; name one with " - "options::triplet.\n", who, std::string(mcpp::target())); + // THE TOOLSET, and how it reaches vcpkg (mcpp.plugins.toolset). + const auto tools = ts::resolve(opt.toolset); + if (!tools) { + std::cerr << std::format("{}: {}\n", who, tools.error()); return {}; } - const fs::path installRoot = opt.install_root.empty() - ? manifestRoot / "vcpkg_installed" : mcpp::deps::absolute_from_root(opt.install_root); - const fs::path tripletRoot = installRoot / triplet; - const fs::path root = tripletRoot / triplet; - - prefix p; - p.root = mcpp::deps::generic(root); - p.include = mcpp::deps::generic(root / "include"); - p.lib = mcpp::deps::generic(root / "lib"); - p.bin = mcpp::deps::generic(root / "bin"); - p.share = mcpp::deps::generic(root / "share"); - p.triplet = triplet; + // THE C RUNTIME. On the MSVC ABI a port's C runtime must be the program's: + // a static library built against the other one fails the link + // (`/failifmismatch`), and a DLL built against it puts a second C++ + // runtime into the process without a word. + const std::string crt = opt.crt_linkage.empty() ? tools->crt : opt.crt_linkage; + if (!crt.empty() && crt != "static" && crt != "dynamic") { + std::cerr << std::format("{}: options::crt_linkage is '{}'; it is \"static\" or \"dynamic\".\n", + who, crt); + return {}; + } // ── the tool ── const std::string vcpkgRoot = opt.vcpkg_root.empty() ? std::string(mcpp::xpkg_dir("xim", "vcpkg")) : mcpp::deps::generic(mcpp::deps::absolute_from_root(opt.vcpkg_root)); const fs::path exe = vcpkgRoot.empty() ? fs::path() - : fs::path(vcpkgRoot) / (std::string(mcpp::host()).find("windows") != std::string::npos - ? "vcpkg.exe" : "vcpkg"); + : fs::path(vcpkgRoot) / (ts::host_is_windows() ? "vcpkg.exe" : "vcpkg"); // ── the overlays: the manifest's own, then the project's extras ── std::vector overlayTriplets = manifest_overlays(manifestRoot, "overlay-triplets"); for (auto const& d : opt.overlay_triplets) overlayTriplets.push_back(mcpp::deps::absolute_from_root(d)); const std::vector overlayPorts = manifest_overlays(manifestRoot, "overlay-ports"); - - // The generated triplet and the toolchain file it chains: vcpkg's own - // Linux toolchain, with the compilers set first. - fs::path generated; - if (cc && !vcpkgRoot.empty()) { - generated = fs::path(mcpp::out_dir()) / "deps-vcpkg" / "triplets"; - const std::string arch = triplet.substr(0, triplet.find('-')); - const fs::path chain = generated / (triplet + ".toolchain.cmake"); - mcpp::deps::write_if_changed(generated / (triplet + ".cmake"), std::format( - "# Written by mcpp.deps.vcpkg: the ports build with the program's compiler,\n" - "# so their C++ standard library is the program's.\n" - "set(VCPKG_TARGET_ARCHITECTURE {})\n" - "set(VCPKG_CRT_LINKAGE dynamic)\n" - "set(VCPKG_LIBRARY_LINKAGE static)\n" - "set(VCPKG_CMAKE_SYSTEM_NAME Linux)\n" - "set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE \"{}\")\n", - arch, mcpp::deps::generic(chain))); - mcpp::deps::write_if_changed(chain, std::format( - "set(CMAKE_C_COMPILER \"{}\")\n" - "set(CMAKE_CXX_COMPILER \"{}\")\n" - "include(\"{}\")\n", - cc.c, cc.cxx, mcpp::deps::generic(fs::path(vcpkgRoot) / "scripts" / "toolchains" / "linux.cmake"))); - } - std::vector tripletSearch = overlayTriplets; - if (!generated.empty()) tripletSearch.insert(tripletSearch.begin(), generated); if (!vcpkgRoot.empty()) { tripletSearch.push_back(fs::path(vcpkgRoot) / "triplets"); tripletSearch.push_back(fs::path(vcpkgRoot) / "triplets" / "community"); } + + // ── the base triplet: the project's, or the target's standard one ── + const std::string base = !opt.triplet.empty() ? opt.triplet : default_triplet(tools->msvc_abi ? crt : ""); + if (base.empty()) { + std::cerr << std::format("{}: no default vcpkg triplet for the target '{}'; name one with " + "options::triplet.\n", who, std::string(mcpp::target())); + return {}; + } + const fs::path baseFile = triplet_file(base, tripletSearch); + const std::string baseText = baseFile.empty() ? std::string() : read_text(baseFile); + if (!baseFile.empty()) mcpp::rerun_if_changed(mcpp::deps::generic(baseFile).c_str()); + + // Two explicit statements that cannot both hold are an error: the + // project's triplet and the program's C runtime. + if (tools->msvc_abi && !crt.empty() && !opt.triplet.empty() && !baseFile.empty()) { + std::string tripletCrt = top_level_setting(baseText, "VCPKG_CRT_LINKAGE"); + if (tripletCrt.empty()) tripletCrt = "dynamic"; + if (tripletCrt != crt) { + std::cerr << std::format( + "{}: the triplet '{}' links the C runtime {} (VCPKG_CRT_LINKAGE), and the program " + "links it {} ({}).\n" + " A static library built against the other C runtime fails the link, and a DLL " + "built against it\n" + " puts a second C++ runtime into the process.\n" + " hint: name a triplet whose VCPKG_CRT_LINKAGE is {} ({}), or set " + "options::crt_linkage to state the linkage the ports use.\n", + who, base, tripletCrt, crt, + opt.crt_linkage.empty() ? std::format("the program's C++ runtime contract, {}", + std::string(mcpp::cxx_runtime())) + : std::string("options::crt_linkage"), + crt, crt == "static" ? default_triplet("static") : default_triplet("dynamic")); + return {}; + } + } + + // ── the triplet vcpkg installs with ── + // + // `detected`, and `instance` with the instance's own toolset version, use + // the base triplet as it stands. `chain`, and `instance` with another + // version, use a DERIVED triplet `-mcpp-`: the base inlined + // (vcpkg hashes a triplet file's content, not a file it includes), then + // the resolved toolset. Its name changes exactly when its content does. + // A project triplet that chain-loads its own toolchain has decided the + // tools, and is used as it stands. + const bool projectChains = !top_level_setting(baseText, "VCPKG_CHAINLOAD_TOOLCHAIN_FILE").empty(); + const bool chain = tools->how == ts::mechanism::chain && !projectChains; + const bool pinVersion = tools->how == ts::mechanism::instance && !tools->toolset_version.empty() + && !tools->instance_default.empty() + && tools->toolset_version != tools->instance_default; + std::string triplet = base; + fs::path generated; + std::vector> actionEnv; + std::vector untracked; + if (chain || pinVersion) { + generated = fs::path(mcpp::out_dir()) / "deps-vcpkg" / "triplets"; + std::string text = std::format( + "# Written by mcpp.deps.vcpkg: the triplet '{}', then the toolset mcpp resolved.\n" + "# toolset: {}\n", base, ts::describe(*tools)); + text += baseText; + if (!text.ends_with('\n')) text += '\n'; + text += "\n# ── mcpp.deps.vcpkg ──\n"; + if (tools->msvc_abi && !crt.empty()) text += std::format("set(VCPKG_CRT_LINKAGE {})\n", crt); + if (pinVersion) + text += std::format("set(VCPKG_PLATFORM_TOOLSET_VERSION {})\n", tools->toolset_version); + if (chain) { + const std::string chainFile = std::format("mcpp-chain-{}.cmake", vcpkg_system_toolchain()); + mcpp::plugins::fs::write_if_changed(generated / chainFile, chain_toolchain_text()); + text += std::format("set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE \"${{CMAKE_CURRENT_LIST_DIR}}/{}\")\n", chainFile); + auto put = [&](std::string name, const std::string& value) { + if (value.empty()) return; + actionEnv.emplace_back(name, value); + untracked.push_back(std::move(name)); + }; + const bool win = ts::host_is_windows(); + auto native = [&](std::string p) { + if (win) for (std::size_t i = 0; i < p.size(); ++i) if (p[i] == '/') p[i] = '\\'; + return p; + }; + put("MCPP_VCPKG_CC", native(tools->cc)); + put("MCPP_VCPKG_CXX", native(tools->cxx)); + put("MCPP_VCPKG_RC", native(tools->rc)); + put("MCPP_VCPKG_MT", native(tools->mt)); + put("MCPP_VCPKG_ROOT", native(vcpkgRoot)); + for (auto const& [k, v] : tools->env) put(k, v); + std::string list; + for (auto const& u : untracked) list += " " + u; + text += std::format("set(VCPKG_ENV_PASSTHROUGH_UNTRACKED ${{VCPKG_ENV_PASSTHROUGH_UNTRACKED}}{})\n", list); + // PATH: make-based ports find `link.exe` there, and vcpkg's clean + // environment keeps the caller's PATH only when told to. The tools' + // directories go first, then the system's own. + if (tools->msvc_abi && win) { + std::string path; + for (auto const& d : tools->path_dirs) { if (!path.empty()) path += ';'; path += native(d); } + const char* sr = std::getenv("SystemRoot"); + const std::string root = sr && *sr ? sr : "C:\\Windows"; + for (auto const& d : { root + "\\system32", root, root + "\\System32\\Wbem", + root + "\\System32\\WindowsPowerShell\\v1.0" }) + path += ";" + d; + actionEnv.emplace_back("PATH", path); + const char* keep = std::getenv("VCPKG_KEEP_ENV_VARS"); + actionEnv.emplace_back("VCPKG_KEEP_ENV_VARS", + keep && *keep ? std::string(keep) + ";PATH" : std::string("PATH")); + text += msbuild_refusal_text(tools->identity); + } + } + triplet = std::format("{}-mcpp-{}", base, mcpp::deps::short_name(text)); + mcpp::plugins::fs::write_if_changed(generated / (triplet + ".cmake"), text); + } + if (tools->how == ts::mechanism::instance) { + // The instance mcpp resolved, selected the way vcpkg documents. When + // it is the instance vcpkg would choose anyway, the ABI hash is + // unchanged and nothing is rebuilt. + std::string dir = tools->instance_dir; + for (std::size_t i = 0; i < dir.size(); ++i) if (dir[i] == '/') dir[i] = '\\'; + actionEnv.emplace_back("VCPKG_VISUAL_STUDIO_PATH", dir); + } + if (!generated.empty()) tripletSearch.insert(tripletSearch.begin(), generated); + + const fs::path installRoot = opt.install_root.empty() + ? manifestRoot / "vcpkg_installed" : mcpp::deps::absolute_from_root(opt.install_root); + const fs::path tripletRoot = installRoot / triplet; + const fs::path root = tripletRoot / triplet; + + prefix p; + p.root = mcpp::deps::generic(root); + p.include = mcpp::deps::generic(root / "include"); + p.lib = mcpp::deps::generic(root / "lib"); + p.bin = mcpp::deps::generic(root / "bin"); + p.share = mcpp::deps::generic(root / "share"); + p.triplet = triplet; + // A project triplet that chain-loads its own toolchain is reported as + // `detected`: this member names no tool for it. + p.mechanism = std::string(ts::name(chain ? ts::mechanism::chain + : tools->how == ts::mechanism::instance ? ts::mechanism::instance + : ts::mechanism::detected)); + const bool shared = shared_linkage(triplet, tripletSearch); // ── the installation, as an edge ── @@ -277,7 +506,7 @@ inline prefix use(const options& opt = {}) { // installation root itself (`/vcpkg/vcpkg-running.lock`), and // `--x-wait-for-lock` makes a second installation of the same root -- // two workspace members, run concurrently -- wait for the first - // instead of failing (measured on GalTranslPP under 0.15.0); its build + // instead of failing (measured on the validation project under 0.15.0); its build // and package trees go to a short directory under vcpkg's per-user // directory, because a port's build nests deep and Windows tools still // enforce MAX_PATH. @@ -289,6 +518,7 @@ inline prefix use(const options& opt = {}) { const fs::path user = mcpp::deps::vcpkg_user_dir(); const fs::path work = user / "mcpp" / mcpp::deps::short_name(mcpp::deps::generic(tripletRoot)); mcpp::rerun_if_env_changed("VCPKG_DOWNLOADS"); + mcpp::rerun_if_env_changed("VCPKG_KEEP_ENV_VARS"); const char* downloads = std::getenv("VCPKG_DOWNLOADS"); mcpp::action a; a.id = id.c_str(); @@ -305,6 +535,11 @@ inline prefix use(const options& opt = {}) { .arg(("--x-packages-root=" + mcpp::deps::generic(work / "pk")).c_str()) .arg("--clean-buildtrees-after-build") .arg("--clean-packages-after-build"); + // The host triplet is the derived one on the MSVC ABI: vcpkg would + // otherwise build its host ports with a standard triplet and look + // for Visual Studio, which the managed toolset does not come from. + if (chain && tools->msvc_abi && std::string_view(mcpp::host()) == std::string_view(mcpp::target())) + a.arg(("--host-triplet=" + triplet).c_str()); // Downloads are shared by every project on the machine; a user who has // moved them already (`VCPKG_DOWNLOADS`) keeps that. if (!downloads || !*downloads) @@ -318,6 +553,7 @@ inline prefix use(const options& opt = {}) { for (auto const& f : mcpp::deps::files_under(generated)) a.input(f.c_str()); } for (auto const& x : opt.install_args) a.arg(x.c_str()); + for (auto const& [k, v] : actionEnv) a.env(k.c_str(), v.c_str()); a.input(exeS.c_str()); a.input(mcpp::deps::generic(manifestFile).c_str()); if (fs::is_regular_file(configFile, ec)) a.input(mcpp::deps::generic(configFile).c_str()); @@ -334,7 +570,7 @@ inline prefix use(const options& opt = {}) { a.output(stamp.c_str()); a.output_dir(p.root.c_str()); a.submit(); - p.deployed = mcpp::deps::deploy_after("deps-vcpkg-" + triplet, stamp, root, opt.deploy); + p.deployed = mcpp::plugins::fs::deploy_after("deps-vcpkg-" + triplet, stamp, root, opt.deploy); } // ── the prefix, into the build ── diff --git a/docs/deps.md b/docs/deps.md index fb2a2d3..9d4e1fa 100644 --- a/docs/deps.md +++ b/docs/deps.md @@ -4,13 +4,13 @@ The `deps-*` members answer where a library comes from: a vcpkg manifest, a CMak ## `deps-vcpkg` -Module `mcpp.deps.vcpkg`; engine floor: 2026.9.26.2 (mcpp#702). +Module `mcpp.deps.vcpkg`; engine floor: 2026.9.28.3 (0.17.0, mcpp#734; 2026.9.26.2 before). **Needs and behaviour.** `xim:vcpkg` (the tool and the scripts released with it), which this feature declares on the host axis. From 0.13.0. Installs a `vcpkg.json` manifest as a `prepare` action and maps `//` into the build by name, its shared-library directory a runtime search directory; `mcpp emit build-database` installs nothing. See [the section below](#deps-vcpkg-the-libraries-a-vcpkg-manifest-names) ## `deps-cmake` -Module `mcpp.deps.cmake`; engine floor: 2026.9.26.2 (mcpp#702). +Module `mcpp.deps.cmake`; engine floor: 2026.9.28.3 (0.17.0, mcpp#734; 2026.9.26.2 before). **Needs and behaviour.** `xim:cmake`, which this feature declares on the host axis. From 0.13.0. Configures, builds and installs a CMake subproject as one `prepare` action whose inputs are the subproject's files, and maps the prefix as `deps-vcpkg` does @@ -24,7 +24,7 @@ Module `mcpp.deps.archive`; engine floor: 2026.9.26.2. ```toml [build-dependencies.mcpp] -plugins = { version = "0.16.0", features = ["deps-vcpkg"], host-module = true } +plugins = { version = "0.17.0", features = ["deps-vcpkg"], host-module = true } ``` ```cpp @@ -67,7 +67,9 @@ states every path it can, and the build is where the absence fails. | option | meaning | |---|---| -| `triplet` | empty derives it from the target: `x64-windows`, `arm64-windows`, `x64-mingw-dynamic`, `x64-linux`, `arm64-linux`, `x64-osx`, `arm64-osx`; on Linux under a libc++ toolchain the generated `x64-linux-libcxx` or `arm64-linux-libcxx` (0.15.0); a custom triplet is found through the manifest's `overlay-triplets` | +| `triplet` | the base triplet; empty derives it from the target: `x64-windows` (`x64-windows-static` when the program links the C runtime statically), `arm64-windows`, `x64-mingw-dynamic`, `x64-linux`, `arm64-linux`, `x64-osx`, `arm64-osx`. Under the `chain` mechanism the installation uses a triplet derived from it (below). A custom triplet is found through the manifest's `overlay-triplets` | +| `toolset` | which toolset builds the ports: `{.toolset = resolved or detected, .cc = abi_native or row}`; the default is `resolved` with `abi_native` (0.17.0, see [the toolset](#the-toolset-instance-chain-detected)) | +| `crt_linkage` | the ports' C runtime linkage on the MSVC ABI, `"static"` or `"dynamic"`; empty follows the program's C++ runtime contract (0.17.0) | | `libraries` | the link, in order; a name that matches no installed file fails the link, naming its path | | `manifest_root` | the directory holding `vcpkg.json` | | `install_root` | empty is vcpkg's default, `/vcpkg_installed`. Each triplet is its own vcpkg installation, `/`, because vcpkg's manifest mode removes from an installation the packages of every other triplet (0.15.0; 0.14.0's prefix `/` is no longer read) | @@ -93,17 +95,86 @@ workspace members installing one root run one after the other; without it the second fails, "failed to take lock" (0.15.1). vcpkg fetches its own CMake, Ninja and 7-Zip, and on Windows a portable git; on Linux and macOS its documented host prerequisites (git, curl, zip, unzip, tar, a C compiler) -are the host's. Ports are compiled with vcpkg's default toolchain for the -triplet: the Visual Studio toolset on Windows, the host compiler elsewhere. The -program links them, so its C++ standard library must be theirs. On Windows every -compiler of the MSVC ABI, mcpp's clang included, uses Microsoft's, and on macOS -every compiler uses libc++, so a port links as it stands. Linux has two -libraries that do not link with each other: the host compiler uses libstdc++, -and mcpp's clang uses libc++ (`std::__1::`). Under a libc++ toolchain the -default triplet is therefore a generated one, `-linux-libcxx`, whose -ports build with mcpp's clang through vcpkg's chain-loaded toolchain file; the -clang's own configuration names libc++ and the C library mcpp links against. -Under a gcc toolchain the default triplet is vcpkg's own (0.15.0). +are the host's. + +### The toolset: `instance`, `chain`, `detected` + +The ports are linked into the program, so they are built with the toolset mcpp +builds the program with (0.17.0). The member reads the toolset from the engine +(mcpp 2026.9.28.3, `mcpp::abi_tool`, `mcpp::tool_env`, `mcpp::toolset_identity`, +`mcpp::msvc_instance_dir`) through `mcpp.plugins.toolset`, and the way it reaches +vcpkg follows where the toolset came from: + +| mechanism | when | what vcpkg receives | port kinds that build | +|---|---|---|---| +| `instance` | an MSVC toolset from a Visual Studio instance (`msvc@system`, the default on a machine with Visual Studio) | `VCPKG_VISUAL_STUDIO_PATH` naming that instance; the standard triplet, or a derived one with `VCPKG_PLATFORM_TOOLSET_VERSION` when the resolved toolset is not the instance's default | CMake, make and MSBuild | +| `chain` | a managed MSVC toolset (`xim:msvc@`), and the clang toolsets of the Linux and macOS rows | a derived triplet `-mcpp-` that chain-loads a toolchain naming the tools, and the tools' environment | CMake and make; an MSBuild port is refused by name | +| `detected` | `options.toolset.toolset = detected`; and, under `resolved`, the Linux GCC row | nothing: vcpkg finds its own toolset, as in 0.16.0 | as vcpkg's own detection allows | + +**The Linux GCC row.** mcpp runs its GCC payload with a sysroot, a binutils +directory and a link model (the payload's dynamic linker and C library) that +its own command lines add; the driver alone is not a complete toolset, and +vcpkg's compiler detection fails with it on a machine whose host compiler does +not fill the gaps (measured on the plugins' CI). The clang payloads carry their +configuration in their own `.cfg` files and are complete. On the GCC row the +host compiler's libstdc++ is the program's C++ library, so `resolved` keeps +vcpkg's detection there and the member states why in `mcpp.plugins.toolset`'s +`reason`. + +**The derived triplet.** The base triplet's text is copied into it, not +included, because vcpkg hashes a triplet file's content and not the files it +includes. The member appends the chain-loaded toolchain +(`${CMAKE_CURRENT_LIST_DIR}/mcpp-chain-.cmake`), the environment +variables that pass through untracked (`MCPP_VCPKG_CC`, `MCPP_VCPKG_CXX`, +`MCPP_VCPKG_RC`, `MCPP_VCPKG_MT`, `MCPP_VCPKG_ROOT`, and on the MSVC ABI +`INCLUDE` and `LIB`), a comment with the toolset's identity, and on the MSVC ABI +`VCPKG_CRT_LINKAGE`. The toolchain file reads each tool from the environment +through `file(TO_CMAKE_PATH)` and then includes vcpkg's own +`scripts/toolchains/.cmake`, so ports keep vcpkg's standard flags. +Neither file holds a path, so the triplet's name, and vcpkg's ABI hash, depend +on the toolset's identity and its compilers and not on where they are +installed: the same toolset on another machine restores the same binary +packages. On the MSVC ABI the derived triplet is also the host triplet, and the +installation runs with the toolset's directories first on a `PATH` that vcpkg +keeps (`VCPKG_KEEP_ENV_VARS=PATH`), because a make-based port (icu) finds +`link.exe` there. + +**An MSBuild port under `chain`.** vcpkg would run MSBuild with +`/p:PlatformToolset=external` and fail without naming the cause. The derived +triplet stops such a port with a message saying that it needs a Visual Studio +instance, and that the toolchain `msvc@system` or `toolset = detected` builds +it. + +**The C runtime (MSVC ABI).** The ports' C runtime is the program's: the +default triplet is `x64-windows-static` for a program that links the C runtime +statically (`cxx_runtime = "self-contained"` or `linkage = "static"`), and a +triplet named in `triplet` whose `VCPKG_CRT_LINKAGE` contradicts the program's +is refused naming both statements. A static library built against the other C +runtime fails the link with `/failifmismatch`, and a DLL built against it puts +a second C++ runtime into the process without a word. `crt_linkage` states the +ports' linkage explicitly and wins. + +**Linux.** Linux has two C++ standard libraries that do not link with each +other. Under mcpp's clang the derived triplet names that clang, so the ports +use libc++ (`std::__1::`) as the program does; under mcpp's GCC the host +compiler's libstdc++ is the program's library, and vcpkg's detection is kept. + +**Upgrading from 0.16.0.** On the clang rows of Linux and macOS, and on Windows +with a managed toolset, the installation moves to a derived triplet, so each +port is built once more (or restored from a binary cache that already holds +it); the prefixes of 0.16.0 (`-linux-libcxx`) are left where they are. The +Linux GCC row is unchanged. With +Visual Studio and the dynamic C runtime nothing changes: the standard triplet +and the instance vcpkg selects by itself give the same ABI hash. A program that +links the C runtime statically moves to `x64-windows-static`. + +**`detected`** keeps the behaviour of 0.16.0 until 2027-03-28 (a compatibility +unit, `src/compat/detected_toolset.cppm`) and prints a note once per build. On +the Linux libc++ row it names mcpp's clang, as 0.16.0 did. + +**Binary caches.** vcpkg reads `VCPKG_BINARY_SOURCES` and +`VCPKG_DEFAULT_BINARY_CACHE` from the environment, and the installation passes +the environment on; whether to host a shared cache is the project's decision. Not supported: vcpkg's classic mode; the debug libraries under `debug/lib`. @@ -129,10 +200,21 @@ directory; its inputs are the script and the subproject's files, so an edit to t maps its own. `layout` names install directories other than `include/`, `lib/` and `bin/`; `prefix_path` becomes `CMAKE_PREFIX_PATH` (`mcpp::rules::qt::root()` for a subproject that finds Qt); `cache_args` carries `-D…`, `-G …` and a -toolchain file. The subproject is compiled with the toolchain CMake selects by -default unless `cache_args` names a compiler or a toolchain file; on Linux under -a libc++ toolchain the compilers are mcpp's clang, as `deps-vcpkg` builds its -ports (0.15.0). `deploy` places files of the prefix +toolchain file. The subproject is compiled with the toolset mcpp resolved +(0.17.0), by the mechanisms `deps-vcpkg` uses: under `instance` CMake's default +generator (Visual Studio) is kept and pointed at the instance +(`CMAKE_GENERATOR_INSTANCE`) and the toolset version (`-T version=`, when it is +not the instance's default); under `chain` the Ninja generator runs with mcpp's +own ninja (`mcpp::ninja_program()`), the compilers are named by path, and the +action runs with the tools' environment and their directories first on `PATH`. +On the MSVC ABI `CMAKE_MSVC_RUNTIME_LIBRARY` follows the program's C runtime +(policy CMP0091). Each toolset statement configures its own build directory, +because CMake refuses a cache made with another generator or instance; the +directory 0.16.0 configured stays `build/`. A compiler, a toolchain file or a +generator in `cache_args` is the project's decision and wins. The options are +`toolset` and `crt_linkage`, as `deps-vcpkg` takes them, and `generator` +(`default`, or `ninja` for the Ninja generator under an instance as well). +`deploy` places files of the prefix beside the program, as `deps-vcpkg` takes it. ## `deps-archive`: files a program reads at run time, from an archive diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 5fce06f..aa84663 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -29,7 +29,54 @@ mcpp is a general build engine with a framework for build plugins. Plugins come 9. **Every behaviour has a criterion.** Each documented behaviour has a CI check, run on every platform the plugin claims. 10. **Documentation is concise.** A plugin's page opens with one summary paragraph, followed by use, options and behaviour, in plain declarative sentences. The README is an index. -## 3. A new plugin +## 3. Layers, names and the engine floor + +| layer | modules | provided by | +|---|---|---| +| L1 `mcpp.core` | `mcpp.core`, spelled `mcpp` as well (the two are permanently equivalent) | the engine | +| L2 general library | `mcpp.plugins.declare`, `mcpp.plugins.toolset`, `mcpp.plugins.fs`; `mcpp.plugins.testing` | this package, feature `plugins-core` (`plugins-testing` for the kit) | +| L3 plugins | `mcpp.deps.*`, `mcpp.rules.*`, `mcpp.dist.*`, `mcpp.tools.*` here; `mcpp..*` elsewhere | this package, by feature; any package | + +A layer depends only on the layers below it. L2 knows no foreign tool: it turns +the engine's facts into what a plugin needs (`mcpp.plugins.toolset` decides how +a resolved toolset reaches a foreign build system, `instance`, `chain` or +`detected`), and the plugin writes the foreign system's own files. + +**Module names.** A third-party plugin names its modules `mcpp..*`, +where `` is its package's namespace (`mcpp.acme.protobuf`). The +second segments `core`, `plugins`, `deps`, `rules`, `dist` and `tools` belong to +namespace `mcpp`; the engine warns when another package uses them, and +mcpp-index refuses such a package. + +**Using L2 from another package.** + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.17.0", features = ["plugins-core"], host-module = true, reexport = true } +``` + +`reexport = true` is needed when the plugin's consumers import L2 modules in +their own build programs. + +**The engine floor.** A plugin states the first mcpp release it needs in +`[package] mcpp = ">="`; an older engine stops before any other work +and names the upgrade. This package's floor is 2026.9.28.3, the release that +states the build information L2 reads. + +**Testing a plugin.** `mcpp.plugins.testing` runs a plugin function in a child +process against a stated build context (`row::windows_visual_studio()`, +`row::windows_managed()`, `row::linux_gcc()`, `row::linux_libcxx()`, or a +context built with `set` and `file`) and hands the lines it emitted and the +files it wrote to a check. The test is a build program; a failed case fails the +build and prints the report. `tests/plugin-logic` is this package's own use. + +**Compatibility units.** A behaviour kept after the release that replaced it +lives in a `compat/` directory, one unit per behaviour, whose header states +what it keeps, since when, its retirement date (six months later), its +replacement and the note it prints once per build. +`.github/scripts/check-compat-retirement.sh` fails once a date has passed. + +## 4. A new plugin A plugin in this package consists of: - a module file under `rules/`, `tools/`, `dist/` or `deps/`; diff --git a/docs/rules-qt.md b/docs/rules-qt.md index 03bf139..337b024 100644 --- a/docs/rules-qt.md +++ b/docs/rules-qt.md @@ -6,7 +6,7 @@ ```toml [build-dependencies.mcpp] -plugins = { version = "0.16.0", features = ["rules-qt"], host-module = true } +plugins = { version = "0.17.0", features = ["rules-qt"], host-module = true } # The SDK and its version are the project's declaration. [target.'cfg(any(windows, linux, macos))'.xlings.workspace] diff --git a/mcpp.toml b/mcpp.toml index e0e30bf..f66961e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,10 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.16.0" +version = "0.17.0" +# The first mcpp release that states the build information (protocol 14), +# places files in one edge, and renders structured diagnostics (mcpp#734). +mcpp = ">=2026.9.28.3" description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature" license = "Apache-2.0" authors = ["mcpp-community"] @@ -17,21 +20,24 @@ import_std = true # the source set; mcpp compiles every interface unit among the resolved # sources as a host module under the name the unit declares (2026.9.5.3+). [build] -# THE LIB ROOT ALONE, AND `src/declare.cppm` BEHIND A FEATURE. +# THE LIB ROOT ALONE, AND THE GENERAL LIBRARY BEHIND A FEATURE. # -# The two units divide by what they may import: +# The units divide by what they may import: # -# src/plugins.cppm `mcpp.plugins.surface`, importing only `std`, so the -# same code compiles into `mcpp-embed` -- an ordinary -# program, which is what lets the generation be an ACTION -# and a payload be that action's declared input -# src/declare.cppm the build-program half: `mcpp::action`, `mcpp::generated` -# and the decision of how the generation reaches the graph +# src/plugins.cppm `mcpp.plugins`, importing only `std`, so the same code +# compiles into `mcpp-embed` -- an ordinary program, which +# is what lets the generation be an ACTION and a payload +# be that action's declared input +# src/declare.cppm, L2, the official general library for build programs +# src/toolset.cppm, (mcpp#734): `mcpp.plugins.declare`, +# src/fs.cppm `mcpp.plugins.toolset`, `mcpp.plugins.fs` +# src/testing.cppm `mcpp.plugins.testing`, the test kit for plugins # -# The second one imports `mcpp`, which exists only inside a build program, so -# listing it here would break the ORDINARY build of this package -- the build -# that produces the tool. `[features.surface]` scopes it to the consumers that -# have a build program, and every member implies it. +# The L2 units import `mcpp.core` (spelled `mcpp`), which exists only inside a +# build program, so listing them here would break the ORDINARY build of this +# package -- the build that produces the tool. `[features.plugins-core]` +# scopes them to the consumers that have a build program, and every family +# member implies it. # # It also needs mcpp 2026.9.8.1: before that release a package's host modules # were ordered by PATH, so `rules/spirv.cppm` was compiled before @@ -66,21 +72,45 @@ sources = ["src/plugins.cppm"] [features] default = [] -# The build-program half of the surface. Not a member a consumer names: every -# member that embeds a payload implies it, and a consumer that activates none -# of them compiles neither this nor anything that imports `mcpp`. +# L2: THE OFFICIAL GENERAL LIBRARY FOR BUILD PROGRAMS (0.17.0, mcpp#734). +# Every family member implies it, so a project that uses an official plugin +# writes nothing more; a build program that uses only L2, and a third-party +# plugin built on it, name `plugins-core` on the dependency edge. A consumer +# that activates none of them compiles nothing that imports `mcpp`. +# +# mcpp.plugins.declare how a generated surface reaches the build graph +# mcpp.plugins.toolset the resolved toolchain, translated for a foreign +# build system (`instance`, `chain`, `detected`) +# mcpp.plugins.fs deterministic file generation and placement +[features.plugins-core] +sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", + "src/compat/detected_toolset.cppm"] + +# The test kit: a plugin function runs against a stated build context, and its +# directives are compared. Separate from `plugins-core`, so that no build +# program compiles it unless it asks. +[features.plugins-testing] +sources = ["src/testing.cppm"] +implies = ["plugins-core"] + +# COMPATIBILITY UNIT +# keeps: the feature name `surface`, the name of `plugins-core` until 0.16.0 +# since: 0.17.0 (2026-09-28) +# retires: 2027-03-28 +# replacement: `plugins-core` +# note: none: a feature name is resolved before any build program runs [features.surface] -sources = ["src/declare.cppm"] +implies = ["plugins-core"] [features.rules-ascendc] sources = ["rules/ascendc.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.ascendc" device_extensions = [".asc", ".cce"] [features.rules-cuda] sources = ["rules/cuda.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.cuda" device_extensions = [".cu"] @@ -90,7 +120,7 @@ device_extensions = [".cu"] # locates the toolchain and refuses naming the command that found nothing. [features.rules-metal] sources = ["rules/metal.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.metal" device_extensions = [".metal"] @@ -100,19 +130,19 @@ device_extensions = [".metal"] # `rules-metal` gives: the compiler ships with Xcode and the Command Line Tools. [features.rules-swift] sources = ["rules/swift.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.swift" device_extensions = [".swift"] [features.rules-hip] sources = ["rules/hip.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.hip" device_extensions = [".hip"] [features.rules-slang] sources = ["rules/slang.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.slang" device_extensions = [".slang"] @@ -121,7 +151,7 @@ device_extensions = [".slang"] # message than the engine's "no rule compiles it". [features.rules-spirv] sources = ["rules/spirv.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.spirv" device_extensions = [".comp", ".vert", ".frag", ".geom", ".tesc", ".tese", ".mesh", ".task", ".rgen", ".rint", ".rahit", ".rchit", @@ -129,7 +159,7 @@ device_extensions = [".comp", ".vert", ".frag", ".geom", ".tesc", ".tese", [features.rules-sycl] sources = ["rules/sycl.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.sycl" device_extensions = [".sycl"] @@ -141,7 +171,7 @@ device_extensions = [".sycl"] # project's own `sources` names are routed. [features.rules-qt] sources = ["rules/qt.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.qt" device_extensions = [".ui", ".qrc", ".ts"] @@ -191,23 +221,23 @@ device_extensions = [".ui", ".qrc", ".ts"] # something true -- this member is not a rule. [features.dist-appimage] sources = ["dist/appimage.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-wix] sources = ["dist/wix.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-apple] sources = ["dist/apple.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-web] sources = ["dist/web.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-apk] sources = ["dist/apk.cppm"] -implies = ["surface"] +implies = ["plugins-core"] # `dist-apk` WITH THE KOTLIN COMPILER, AND WITH THE MAVEN RESOLVER (0.11.0). # Features of their own rather than payloads of `dist-apk`, for the reason @@ -241,11 +271,11 @@ implies = ["dist-apk"] # build by name, and its shared-library directory through # `mcpp::runtime_search_dir` -- both from the release that lands mcpp#702. # -# `deps` is the unit both members import, as `surface` is for the rest; a +# `deps` is the unit the members import, as `plugins-core` is for the rest; a # consumer never names it. [features.deps] -sources = ["deps/deps.cppm"] -implies = ["surface"] +sources = ["deps/deps.cppm", "deps/compat/names.cppm"] +implies = ["plugins-core"] [features.deps-vcpkg] sources = ["deps/vcpkg.cppm"] @@ -265,11 +295,11 @@ implies = ["deps"] [features.tools-embed] sources = ["tools/embed.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.tools-island] sources = ["tools/island.cppm"] -implies = ["surface"] +implies = ["plugins-core"] # ── The environment each rule needs (mcpp 2026.9.6.6+) ────────────────────── # diff --git a/src/compat/detected_toolset.cppm b/src/compat/detected_toolset.cppm new file mode 100644 index 0000000..a62278f --- /dev/null +++ b/src/compat/detected_toolset.cppm @@ -0,0 +1,47 @@ +// COMPATIBILITY UNIT +// keeps: `toolset = detected` in the deps members: vcpkg and CMake find +// their own toolset, as in 0.16.0 +// since: 0.17.0 (2026-09-28) +// retires: 2027-03-28 +// replacement: the engine's toolchain selection; `msvc@system` makes the +// default `resolved` use that Visual Studio instance +// note: "mcpp.plugins: toolset = detected is kept until 2027-03-28; ..." +// +// Retirement removes this file, its entry in `[features.plugins-core]`, the +// import in `src/toolset.cppm` and the `source::detected` branch there. +// `.github/scripts/check-compat-retirement.sh` fails once the date has passed. + +export module mcpp.plugins.compat.detected_toolset; + +import std; +import mcpp; + +export namespace mcpp::plugins::compat { + +struct detected_answer { + // 0.16.0 named mcpp's own clang on the Linux libc++ row, where the host + // compiler that detection finds uses libstdc++ and the two do not link. + bool name_compilers = false; +}; + +inline detected_answer detected_toolset() { + static bool noted = false; + if (!noted) { + noted = true; + mcpp::report({ + .severity = "note", + .message = "mcpp.plugins: `toolset = detected` is kept until 2027-03-28", + .impact = "vcpkg and CMake choose their own toolset, which may differ from the one " + "mcpp builds the program with", + .hint = "remove the option to use the toolset mcpp resolved; select a Visual Studio " + "instance with the toolchain `msvc@system`", + }); + } + detected_answer a; + a.name_compilers = std::string_view(mcpp::target_os()) == "linux" + && std::string_view(mcpp::cxx_stdlib()) == "libc++" + && std::string_view(mcpp::host()) == std::string_view(mcpp::target()); + return a; +} + +} // namespace mcpp::plugins::compat diff --git a/src/fs.cppm b/src/fs.cppm new file mode 100644 index 0000000..c5edb77 --- /dev/null +++ b/src/fs.cppm @@ -0,0 +1,102 @@ +// mcpp.plugins.fs -- deterministic file generation and placement for build +// programs (L2 of the build-plugin architecture, mcpp#734). +// +// Two things every plugin that writes or places files needs, and that none of +// them should write twice: +// +// 1. A GENERATED FILE KEEPS ITS TIME STAMP WHILE ITS CONTENT DOES NOT CHANGE. +// A build program runs on every plan; a file it rewrites unconditionally +// is a changed input of every action that reads it, and that action runs +// again on every build. `write_if_changed` writes only a changed text. +// +// 2. A FILE THAT AN ACTION PRODUCES IS PLACED BESIDE THE PROGRAM THROUGH A +// COPY THE GRAPH NAMES. `mcpp::deploy` takes a file the build graph knows +// how to produce. A file a `prepare` action installs is not one: its name +// is unknown to the graph until the action has run. `deploy_after` copies +// each named file out of such a directory by an action that names it as +// an output (SPEC-007 R3.2), and deploys the copy. Everything is declared +// before anything exists, so `mcpp emit build-database` plans it on a +// machine that never built. +// +// Until 0.17.0 these lived in `mcpp.deps`, which re-exports them under their +// old names through a compatibility unit (`deps/compat/names.cppm`). + +export module mcpp.plugins.fs; + +import std; +import mcpp; + +export namespace mcpp::plugins::fs { + +inline std::string generic(const std::filesystem::path& p) { + return p.lexically_normal().generic_string(); +} + +// Writes `content` to `file` unless the file already holds it, so a file the +// build program generates keeps its time stamp from one plan to the next and +// the action that reads it does not run again. Returns whether it wrote. +inline bool write_if_changed(const std::filesystem::path& file, const std::string& content) { + namespace stdfs = std::filesystem; + std::error_code ec; + if (stdfs::is_regular_file(file, ec)) { + std::ifstream in(file, std::ios::binary); + const std::string old{ std::istreambuf_iterator(in), std::istreambuf_iterator() }; + if (old == content) return false; + } + stdfs::create_directories(file.parent_path(), ec); + std::ofstream(file, std::ios::binary | std::ios::trunc) << content; + return true; +} + +// One file of a directory to place beside the program: `file` relative to the +// directory (`share/opencc/t2s.json`), `to` the directory beside the program +// it is placed in (`BaseConfig/opencc`; empty or `.` is the program's own +// directory). +struct deploy_entry { + std::string file; + std::string to; +}; + +// A file placed by `deploy_after`: `path`, the copy the build produces (a node +// of the build graph, which a project's own actions may take as an input), and +// `to`, its path beside the program. +struct deployed_file { + std::string path; + std::string to; +}; + +// Copies each entry's file out of `root` once the action that writes `stamp` +// has run, and deploys the copy. `who` names the plugin in action ids. Two or +// more deployed files become one placement edge in the engine (mcpp 2026.9.28.3, +// `mcpp stage --list`), so the count of entries costs no per-file process. +inline std::vector deploy_after(std::string_view who, const std::string& stamp, + const std::filesystem::path& root, + std::span entries) { + namespace stdfs = std::filesystem; + std::vector out; + const stdfs::path base = stdfs::path(mcpp::out_dir()) / "deps-deploy" / std::string(who); + for (auto const& e : entries) { + const stdfs::path rel = stdfs::path(e.file).lexically_normal(); + const std::string src = generic(root / rel); + const std::string copy = generic(base / rel); + // Index loops over a string: GCC 16 fails to inline a string iterator + // inside a module's interface. + std::string key = rel.generic_string(); + for (std::size_t i = 0; i < key.size(); ++i) if (key[i] == '/') key[i] = '.'; + const std::string id = std::format("{}:deploy:{}", who, key); + const std::string desc = std::format("DEPLOY {}", rel.generic_string()); + mcpp::action a; + a.id = id.c_str(); + a.role = mcpp::roles::artifact; + a.description = desc.c_str(); + a.arg("${mcpp.self}").arg("stage").arg("--output").arg(copy.c_str()).arg(src.c_str()) + .input(stamp.c_str()).output(copy.c_str()).submit(); + const std::string dir = e.to.empty() ? std::string(".") : e.to; + mcpp::deploy(copy.c_str(), dir.c_str()); + const std::string to = generic(stdfs::path(dir) / rel.filename()); + out.push_back({copy, to}); + } + return out; +} + +} // namespace mcpp::plugins::fs diff --git a/src/plugins.cppm b/src/plugins.cppm index 91014bc..f1e6b39 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -49,7 +49,7 @@ export namespace mcpp::plugins { // // One package, one version: the number lives in mcpp.toml, and the CI step // `the collection states its own version` compares the two. -inline constexpr std::string_view version = "0.16.0"; +inline constexpr std::string_view version = "0.17.0"; } // namespace mcpp::plugins diff --git a/src/testing.cppm b/src/testing.cppm new file mode 100644 index 0000000..6e1f187 --- /dev/null +++ b/src/testing.cppm @@ -0,0 +1,340 @@ +// mcpp.plugins.testing -- the test kit for build plugins (L2 of the +// build-plugin architecture, mcpp#734; feature `plugins-testing`). +// +// A plugin's logic is a function of the build context, and its effect is the +// set of directives and actions it emits, which are `mcpp:` lines on standard +// output. The kit runs a plugin function against a STATED context -- the +// target, the toolchain facts, the profile, the directories, files the +// function reads -- and hands the emitted lines, and the files the function +// wrote, to a check. No foreign tool is installed or run: a context that +// describes a Visual Studio row is tested on a Linux machine. +// +// A test is a build program: +// +// import std; +// import mcpp.plugins.testing; +// import mcpp.deps.vcpkg; +// namespace t = mcpp::plugins::testing; +// +// int main(int argc, char** argv) { +// return t::run(argc, argv, { +// { "the managed toolset is chained", +// t::row::windows_managed(), +// [] { return mcpp::deps::vcpkg::use({ .libraries = {"fmt"} }) ? 0 : 1; }, +// [](t::result const& r, t::checker& c) { +// c.expect(r.has_line("mcpp:action=", "--host-triplet="), "the host triplet is derived"); +// } }, +// }); +// } +// +// HOW. The build program is run by mcpp as usual; `run` starts the same +// executable once per case (`--mcpp-plugins-testing-case=`) with the case's +// context in its environment and its standard output in a file, and runs the +// check on what it wrote. A separate process per case is what makes the +// context complete: every accessor of `mcpp.core` reads the environment, and +// a function's `static` state starts fresh. A failed check fails the build +// program, which fails the build and prints the report. The verdicts are also +// written to `/plugins-testing/results.txt`, one `PASS` or `FAIL` +// line per case, for a CI step to count. + +module; +#include + +export module mcpp.plugins.testing; + +import std; +import mcpp; + +namespace mcpp::plugins::testing::detail { + +inline void set_env(const std::string& name, const std::string& value) { +#if defined(_WIN32) + ::_putenv_s(name.c_str(), value.c_str()); +#else + if (value.empty()) ::unsetenv(name.c_str()); + else ::setenv(name.c_str(), value.c_str(), 1); +#endif +} + +// Every key an accessor of `mcpp.core` reads that a plugin's logic depends on. +// A case sets each of them -- empty unless its context states a value -- so +// nothing of the real build reaches the function under test. +inline constexpr std::string_view kKeys[] = { + "MCPP_TARGET", "MCPP_TARGET_OS", "MCPP_TARGET_ARCH", "MCPP_TARGET_ENV", "MCPP_HOST", + "MCPP_PROFILE", "MCPP_ACCEL", "MCPP_OUT_DIR", "MCPP_MANIFEST_DIR", "MCPP_TOOLCHAIN_DIR", + "MCPP_TOOLCHAIN_SYSROOT", "MCPP_COMPILER", "MCPP_CXX_STDLIB", "MCPP_TARGET_SYSROOT", + "MCPP_PKG_NAME", "MCPP_PKG_NAMESPACE", "MCPP_PKG_VERSION", "MCPP_PACK_FORMAT", + "MCPP_TOOL_CC", "MCPP_TOOL_CXX", "MCPP_TOOL_LD", "MCPP_TOOL_AR", "MCPP_TOOL_RC", + "MCPP_TOOL_AS", "MCPP_TOOL_MT", + "MCPP_ABI_TOOL_CC", "MCPP_ABI_TOOL_CXX", "MCPP_ABI_TOOL_LD", "MCPP_ABI_TOOL_AR", + "MCPP_ABI_TOOL_RC", "MCPP_ABI_TOOL_AS", "MCPP_ABI_TOOL_MT", + "MCPP_TOOL_ENV", "MCPP_TOOLSET_IDENTITY", "MCPP_MSVC_INSTANCE_DIR", "MCPP_NINJA", + "MCPP_CXX_RUNTIME", "MCPP_MSVC_CRT_LINKAGE", +}; + +inline std::string read_file(const std::filesystem::path& p) { + std::ifstream in(p, std::ios::binary); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +} // namespace mcpp::plugins::testing::detail + +export namespace mcpp::plugins::testing { + +// A stated build context: environment values for the accessors, and files +// that exist before the function runs. `{root}` in a value or a path is the +// case's own scratch directory, created empty for every case. +struct context { + std::vector> values; + std::vector> files; + + context& set(std::string key, std::string value) { + for (auto& kv : values) if (kv.first == key) { kv.second = std::move(value); return *this; } + values.emplace_back(std::move(key), std::move(value)); + return *this; + } + context& file(std::string path, std::string content = {}) { + files.emplace_back(std::move(path), std::move(content)); + return *this; + } + // `xpkg_dir(ns, name)`: the directory of a declared payload. + context& xpkg(std::string_view ns, std::string_view name, std::string dir) { + std::string key = "MCPP_XPKG_"; + auto put = [&](std::string_view s) { + for (std::size_t i = 0; i < s.size(); ++i) { + const char c = s[i]; + key += (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A') + : ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_'; + } + }; + if (!ns.empty()) { put(ns); key += '_'; } + put(name); + key += "_DIR"; + return set(std::move(key), std::move(dir)); + } +}; + +// Contexts that describe the rows a plugin meets. Paths are under `{root}`, +// so they exist only where a case creates them. +namespace row { + +inline context linux_gcc() { + return context{} + .set("MCPP_TARGET", "x86_64-linux-gnu").set("MCPP_HOST", "x86_64-linux-gnu") + .set("MCPP_TARGET_OS", "linux").set("MCPP_TARGET_ARCH", "x86_64").set("MCPP_TARGET_ENV", "gnu") + .set("MCPP_PROFILE", "dev").set("MCPP_COMPILER", "gcc").set("MCPP_CXX_STDLIB", "libstdc++") + .set("MCPP_TOOLCHAIN_DIR", "{root}/gcc") + .set("MCPP_TOOL_CC", "{root}/gcc/bin/gcc").set("MCPP_TOOL_CXX", "{root}/gcc/bin/g++") + .set("MCPP_TOOL_LD", "{root}/gcc/bin/g++").set("MCPP_TOOL_AR", "{root}/gcc/bin/gcc-ar") + .set("MCPP_ABI_TOOL_CC", "{root}/gcc/bin/gcc").set("MCPP_ABI_TOOL_CXX", "{root}/gcc/bin/g++") + .set("MCPP_ABI_TOOL_LD", "{root}/gcc/bin/g++").set("MCPP_ABI_TOOL_AR", "{root}/gcc/bin/gcc-ar") + .set("MCPP_TOOLSET_IDENTITY", "gcc 16.1.0").set("MCPP_NINJA", "{root}/ninja/ninja") + .set("MCPP_CXX_RUNTIME", "toolchain-coupled") + .set("MCPP_OUT_DIR", "{root}/out").set("MCPP_MANIFEST_DIR", "{root}/pkg"); +} + +inline context linux_libcxx() { + return linux_gcc() + .set("MCPP_COMPILER", "clang").set("MCPP_CXX_STDLIB", "libc++") + .set("MCPP_TOOLCHAIN_DIR", "{root}/llvm") + .set("MCPP_TOOL_CC", "{root}/llvm/bin/clang").set("MCPP_TOOL_CXX", "{root}/llvm/bin/clang++") + .set("MCPP_TOOL_LD", "{root}/llvm/bin/clang++").set("MCPP_TOOL_AR", "{root}/llvm/bin/llvm-ar") + .set("MCPP_ABI_TOOL_CC", "{root}/llvm/bin/clang").set("MCPP_ABI_TOOL_CXX", "{root}/llvm/bin/clang++") + .set("MCPP_ABI_TOOL_LD", "{root}/llvm/bin/clang++").set("MCPP_ABI_TOOL_AR", "{root}/llvm/bin/llvm-ar") + .set("MCPP_TOOLSET_IDENTITY", "clang 22.1.8"); +} + +// The MSVC ABI with cl.exe from `tools`, the SDK's rc.exe and mt.exe, and the +// environment the engine runs them with. +inline context windows_msvc(const std::string& tools, const std::string& version) { + const std::string bin = tools + "/bin/Hostx64/x64"; + const std::string sdk = "{root}/sdk/bin/10.0.26100.0/x64"; + return context{} + .set("MCPP_TARGET", "x86_64-pc-windows-msvc").set("MCPP_HOST", "x86_64-pc-windows-msvc") + .set("MCPP_TARGET_OS", "windows").set("MCPP_TARGET_ARCH", "x86_64").set("MCPP_TARGET_ENV", "msvc") + .set("MCPP_PROFILE", "dev").set("MCPP_COMPILER", "msvc").set("MCPP_CXX_STDLIB", "msvc-stl") + .set("MCPP_TOOL_CC", bin + "/cl.exe").set("MCPP_TOOL_CXX", bin + "/cl.exe") + .set("MCPP_TOOL_LD", bin + "/link.exe").set("MCPP_TOOL_AR", bin + "/lib.exe") + .set("MCPP_TOOL_RC", sdk + "/rc.exe").set("MCPP_TOOL_MT", sdk + "/mt.exe") + .set("MCPP_ABI_TOOL_CC", bin + "/cl.exe").set("MCPP_ABI_TOOL_CXX", bin + "/cl.exe") + .set("MCPP_ABI_TOOL_LD", bin + "/link.exe").set("MCPP_ABI_TOOL_AR", bin + "/lib.exe") + .set("MCPP_ABI_TOOL_RC", sdk + "/rc.exe").set("MCPP_ABI_TOOL_MT", sdk + "/mt.exe") + .set("MCPP_TOOL_ENV", "INCLUDE=" + tools + "/include;{root}/sdk/Include/ucrt\n" + "LIB=" + tools + "/lib/x64;{root}/sdk/Lib/ucrt/x64\n" + "PATH=" + bin + ";" + sdk) + .set("MCPP_TOOLSET_IDENTITY", "msvc " + version + "; sdk 10.0.26100.0") + .set("MCPP_NINJA", "{root}/ninja/ninja.exe") + .set("MCPP_CXX_RUNTIME", "toolchain-coupled").set("MCPP_MSVC_CRT_LINKAGE", "dynamic") + .set("MCPP_OUT_DIR", "{root}/out").set("MCPP_MANIFEST_DIR", "{root}/pkg"); +} + +// `msvc@system`: the toolset of a Visual Studio instance at `{root}/vs`, which +// selects `default_version` by itself. +inline context windows_visual_studio(const std::string& version = "14.44.35207", + const std::string& default_version = "14.44.35207") { + return windows_msvc("{root}/vs/VC/Tools/MSVC/" + version, version) + .set("MCPP_MSVC_INSTANCE_DIR", "{root}/vs") + .file("vs/VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt", default_version + "\n"); +} + +// A managed MSVC toolset: no Visual Studio instance. +inline context windows_managed(const std::string& version = "14.44.35207") { + return windows_msvc("{root}/msvc/VC/Tools/MSVC/" + version, version); +} + +} // namespace row + +// What one case produced. +struct result { + int exit_code = 0; + std::vector lines; // standard output, one entry per line + std::string errors; // standard error + std::filesystem::path root; // the case's scratch directory + + // A line that starts with `prefix` and contains every one of `parts`. + bool has_line(std::string_view prefix, std::initializer_list parts = {}) const { + return !line(prefix, parts).empty(); + } + bool has_line(std::string_view prefix, std::string_view part) const { + return has_line(prefix, {part}); + } + std::string line(std::string_view prefix, std::initializer_list parts = {}) const { + for (auto const& l : lines) { + if (!l.starts_with(prefix)) continue; + bool all = true; + for (auto p : parts) if (l.find(p) == std::string::npos) { all = false; break; } + if (all) return l; + } + return {}; + } + std::size_t count(std::string_view prefix) const { + std::size_t n = 0; + for (auto const& l : lines) if (l.starts_with(prefix)) ++n; + return n; + } + // A file the function wrote, relative to the case's scratch directory. + std::string file(std::string_view relative) const { + return detail::read_file(root / std::filesystem::path(std::string(relative))); + } + // Every file under a directory of the scratch directory, relative to it. + std::vector files_under(std::string_view relative) const { + std::vector out; + std::error_code ec; + const auto dir = root / std::filesystem::path(std::string(relative)); + for (auto it = std::filesystem::recursive_directory_iterator(dir, ec); + !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) + if (it->is_regular_file(ec)) out.push_back(it->path().lexically_relative(root).generic_string()); + std::ranges::sort(out); + return out; + } +}; + +struct checker { + std::vector failures; + void expect(bool ok, std::string_view what) { + if (!ok) failures.emplace_back(what); + } +}; + +struct test_case { + std::string name; + context ctx; + std::function body; + std::function check; +}; + +namespace detail2 { +inline std::string expand(std::string text, const std::string& root) { + for (std::size_t at = text.find("{root}"); at != std::string::npos; at = text.find("{root}", at + root.size())) + text.replace(at, 6, root); + return text; +} +inline std::string quoted(const std::string& s) { return "\"" + s + "\""; } +} // namespace detail2 + +// Runs `cases` (see the header). Returns the build program's exit code: 0 +// when every case passes. +inline int run(int argc, char** argv, std::span cases) { + namespace fs = std::filesystem; + constexpr std::string_view flag = "--mcpp-plugins-testing-case="; + for (int i = 1; i < argc; ++i) { + const std::string_view a = argv[i]; + if (!a.starts_with(flag)) continue; + const std::size_t n = static_cast(std::stoul(std::string(a.substr(flag.size())))); + return n < cases.size() && cases[n].body ? cases[n].body() : 97; + } + + std::error_code ec; + const fs::path self = fs::absolute(fs::path(argc > 0 ? argv[0] : ""), ec); + const std::string out = mcpp::out_dir(); + const fs::path base = (out.empty() ? fs::temp_directory_path() : fs::path(out)) / "plugins-testing"; + fs::remove_all(base, ec); + fs::create_directories(base, ec); + + // The union of the keys any case states, beside the kit's own list. + std::vector keys; + for (auto k : detail::kKeys) keys.emplace_back(k); + for (auto const& c : cases) + for (auto const& kv : c.ctx.values) + if (std::ranges::find(keys, kv.first) == keys.end()) keys.push_back(kv.first); + + std::string report, verdicts; + int failed = 0; + for (std::size_t n = 0; n < cases.size(); ++n) { + const auto& c = cases[n]; + const fs::path root = base / std::format("case-{}", n); + fs::create_directories(root, ec); + const std::string rootS = root.generic_string(); + for (auto const& [path, content] : c.ctx.files) { + const fs::path f = root / fs::path(detail2::expand(path, rootS)); + fs::create_directories(f.parent_path(), ec); + std::ofstream(f, std::ios::binary) << detail2::expand(content, rootS); + } + for (auto const& k : keys) { + std::string v; + for (auto const& kv : c.ctx.values) if (kv.first == k) v = detail2::expand(kv.second, rootS); + detail::set_env(k, v); + } + const fs::path so = root / "stdout.txt", se = root / "stderr.txt"; + std::string cmd = detail2::quoted(self.string()) + " " + std::string(flag) + std::to_string(n) + + " > " + detail2::quoted(so.string()) + " 2> " + detail2::quoted(se.string()); +#if defined(_WIN32) + cmd = "\"" + cmd + "\""; // cmd.exe strips one outer pair of quotes +#endif + const int status = std::system(cmd.c_str()); + + result r; + r.exit_code = status; + r.root = root; + r.errors = detail::read_file(se); + std::istringstream in(detail::read_file(so)); + for (std::string l; std::getline(in, l); ) { + if (!l.empty() && l.back() == '\r') l.pop_back(); + r.lines.push_back(l); + } + checker ck; + if (c.check) c.check(r, ck); + if (ck.failures.empty()) { + verdicts += std::format("PASS {}\n", c.name); + } else { + ++failed; + verdicts += std::format("FAIL {}\n", c.name); + report += std::format("FAIL: {}\n", c.name); + for (auto const& f : ck.failures) report += std::format(" expected: {}\n", f); + report += std::format(" exit status {}; standard output ({} lines):\n", status, r.lines.size()); + for (auto const& l : r.lines) report += " " + l + "\n"; + if (!r.errors.empty()) report += " standard error:\n" + r.errors + "\n"; + } + } + std::ofstream(base / "results.txt", std::ios::binary) << verdicts; + for (auto const& k : keys) detail::set_env(k, ""); + std::cerr << report + << std::format("mcpp.plugins.testing: {} of {} cases passed\n", cases.size() - failed, cases.size()); + return failed == 0 ? 0 : 1; +} + +inline int run(int argc, char** argv, std::initializer_list cases) { + return run(argc, argv, std::span(cases.begin(), cases.size())); +} + +} // namespace mcpp::plugins::testing diff --git a/src/toolset.cppm b/src/toolset.cppm new file mode 100644 index 0000000..0e99fea --- /dev/null +++ b/src/toolset.cppm @@ -0,0 +1,269 @@ +// mcpp.plugins.toolset -- the resolved toolchain, translated for a foreign +// build system (L2 of the build-plugin architecture, mcpp#734). +// +// A plugin that drives vcpkg, CMake, Meson or make hands it the tools the +// program is built with, so that what it builds links with the program. The +// engine states the facts (mcpp 2026.9.28.3, protocol 14): each role's tool, +// the target ABI's native tools, the environment the engine runs them with, +// a path-free identity of the toolset, the Visual Studio instance it came +// from, and the C++ runtime contract. This module decides HOW those facts +// reach a foreign system, once for every plugin: +// +// instance An MSVC toolset from a Visual Studio instance (`msvc@system`) +// is used through that instance. The foreign system's own +// toolset loading stays in place, pointed at the instance mcpp +// resolved, so MSBuild is present and every kind of project +// builds. +// chain Any other toolset -- a managed MSVC toolset, and the clang +// toolsets of the other rows -- is named: absolute tool paths, the +// environment they run with, and the directories that go first +// on PATH. On the MSVC ABI without an instance no MSBuild exists. +// detected Nothing is named, and the foreign system finds its own +// toolset. `source::detected` asks for it (kept by +// `src/compat/detected_toolset.cppm` until 2027-03-28); `resolved` +// answers it on the Linux GCC row, whose payload driver is not a +// complete handover (see `resolve`). +// +// NONE OF THIS NAMES A FOREIGN SYSTEM. vcpkg's triplet and CMake's generator +// are written by the plugins that drive them (`mcpp.deps.vcpkg`, +// `mcpp.deps.cmake`); this module stops at the tools. + +export module mcpp.plugins.toolset; + +import std; +import mcpp; +import mcpp.plugins.compat.detected_toolset; + +export namespace mcpp::plugins::toolset { + +// Who chooses the tools: the engine's resolution (`resolved`), or the foreign +// system's own detection (`detected`, a compatibility behaviour). +enum class source { resolved, detected }; +// Which compiler `resolved` hands over. `abi_native` is the target ABI's own +// compiler (`cl.exe` on the MSVC ABI whichever driver the row uses; the row's +// compiler elsewhere). `row` is the compiler the row itself runs, for example +// clang on an LLVM row that targets the MSVC ABI. +enum class compiler { abi_native, row }; + +// Embedded in each plugin's options. +struct choice { + source toolset = source::resolved; + compiler cc = compiler::abi_native; +}; + +enum class mechanism { instance, chain, detected }; + +inline std::string_view name(mechanism m) { + return m == mechanism::instance ? "instance" : m == mechanism::chain ? "chain" : "detected"; +} + +struct resolved_tools { + mechanism how = mechanism::detected; + std::string instance_dir; // `instance`: the Visual Studio instance + std::string toolset_version; // `instance`: e.g. 14.44.35207; also set under `chain` on the MSVC ABI + std::string instance_default; // `instance`: the version the instance selects by itself + // Absolute paths, forward slashes. `chain` sets `cc` and `cxx` always and + // the others where the row has them; `detected` sets `cc` and `cxx` only + // where detection cannot agree with the program (the Linux libc++ row). + std::string cc, cxx, ld, ar, rc, mt; + // `chain` on the MSVC ABI: the environment the tools run with, without + // PATH (INCLUDE, LIB, LIBPATH, ...). + std::vector> env; + // `chain`: the tools' own directories, in order, without duplicates: the + // PATH a foreign system needs for them. + std::vector path_dirs; + std::string identity; // toolset_identity(): "msvc 14.44.35207; sdk 10.0.26100.0", "clang 22.1.8" + std::string crt; // msvc_crt_linkage(): "static", "dynamic", or empty off the MSVC ABI + bool msvc_abi = false; + // Why `resolved` answered `detected`: empty unless the resolved toolset + // cannot be handed over by its tools alone. + std::string reason; +}; + +inline bool msvc_abi() { + return std::string_view(mcpp::target_os()) == "windows" + && std::string_view(mcpp::target_env()) == "msvc"; +} + +inline bool host_is_windows() { + return std::string_view(mcpp::host()).find("windows") != std::string_view::npos; +} + +// `a;b` on a Windows host, `a:b` elsewhere. +inline char path_separator() { return host_is_windows() ? ';' : ':'; } + +inline std::string forward(std::string_view p) { + std::string s(p); + for (std::size_t i = 0; i < s.size(); ++i) if (s[i] == '\\') s[i] = '/'; + return s; +} + +// The toolset version out of an identity "msvc 14.44.35207; sdk 10.0.26100.0". +inline std::string msvc_version_of(std::string_view identity) { + if (!identity.starts_with("msvc ")) return {}; + identity.remove_prefix(5); + const auto end = identity.find(';'); + return std::string(identity.substr(0, end)); +} + +// The toolset version a Visual Studio instance selects when none is named: +// `VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt`, which the +// Visual Studio installer writes and CMake and vcpkg both read. +inline std::string instance_default_version(const std::filesystem::path& instance) { + std::ifstream in(instance / "VC" / "Auxiliary" / "Build" / "Microsoft.VCToolsVersion.default.txt"); + std::string v; + std::getline(in, v); + while (!v.empty() && (v.back() == '\r' || v.back() == ' ' || v.back() == '\n')) v.pop_back(); + return v; +} + +// tool_env() is one KEY=value per line. +inline std::vector> parse_env(std::string_view text) { + std::vector> out; + while (!text.empty()) { + const auto nl = text.find('\n'); + std::string_view line = text.substr(0, nl); + text = nl == std::string_view::npos ? std::string_view{} : text.substr(nl + 1); + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + const auto eq = line.find('='); + if (eq == std::string_view::npos || eq == 0) continue; + out.emplace_back(std::string(line.substr(0, eq)), std::string(line.substr(eq + 1))); + } + return out; +} + +inline bool same_key(std::string_view a, std::string_view b) { + if (a.size() != b.size()) return false; + for (std::size_t i = 0; i < a.size(); ++i) + if (std::tolower(static_cast(a[i])) != std::tolower(static_cast(b[i]))) + return false; + return true; +} + +// The value of `key` in `env`; Windows environment names compare without case. +inline std::string env_value(const std::vector>& env, + std::string_view key) { + for (auto const& [k, v] : env) if (same_key(k, key)) return v; + return {}; +} + +namespace detail { +inline std::string tool_of(compiler which, const char* role) { + return forward(which == compiler::abi_native ? mcpp::abi_tool(role) : mcpp::tool(role)); +} + +inline void add_dir(std::vector& dirs, const std::string& file) { + if (file.empty()) return; + const std::string dir = std::filesystem::path(file).parent_path().generic_string(); + if (dir.empty()) return; + for (auto const& d : dirs) if (same_key(d, dir)) return; + dirs.push_back(dir); +} + +inline std::expected chain(const choice& c, resolved_tools r) { + r.how = mechanism::chain; + r.cc = tool_of(c.cc, "cc"); + r.cxx = tool_of(c.cc, "cxx"); + r.ld = tool_of(c.cc, "ld"); + r.ar = tool_of(c.cc, "ar"); + r.rc = tool_of(c.cc, "rc"); + r.mt = tool_of(c.cc, "mt"); + if (r.cxx.empty()) + return std::unexpected(std::format( + "the resolved toolchain states no {} C++ compiler for target '{}'; the engine " + "states the tools from mcpp 2026.9.28.3 onward.", + c.cc == compiler::abi_native ? "ABI-native" : "row", std::string(mcpp::target()))); + if (r.cc.empty()) r.cc = r.cxx; + if (r.msvc_abi) { + // The environment WITHOUT its PATH. The engine's PATH is the cl.exe + // directory followed by the whole PATH of the process that ran mcpp -- + // under Git Bash that holds Git's own msys tools and runtime, and a + // make-based port configured against them failed (`configure: error: + // invalid variable name: '0'`, icu, the plugins' CI). The tools' + // own directories are the PATH a foreign system needs: the toolset's + // bin (cl, link, lib) and the SDK's (rc, mt). + for (auto const& kv : parse_env(mcpp::tool_env())) + if (!same_key(kv.first, "PATH")) r.env.push_back(kv); + for (auto const* t : { &r.cxx, &r.cc, &r.ld, &r.rc, &r.mt }) add_dir(r.path_dirs, *t); + } else { + for (auto const* t : { &r.cxx, &r.cc }) add_dir(r.path_dirs, *t); + } + return r; +} +} // namespace detail + +// The tools a foreign build system is handed, and how. +inline std::expected resolve(const choice& c = {}) { + resolved_tools r; + r.identity = mcpp::toolset_identity(); + r.crt = mcpp::msvc_crt_linkage(); + r.msvc_abi = msvc_abi(); + if (r.msvc_abi) r.toolset_version = msvc_version_of(r.identity); + + if (c.toolset == source::detected) { + const auto kept = mcpp::plugins::compat::detected_toolset(); + // Where detection cannot agree with the program -- the host compiler + // on Linux uses libstdc++ while the program uses libc++ -- 0.16.0 + // already named mcpp's clang, and so does this. + if (kept.name_compilers) return detail::chain({source::resolved, compiler::row}, r); + r.how = mechanism::detected; + return r; + } + if (r.identity.empty()) + return std::unexpected(std::string( + "the engine states no toolset identity: build information for build programs " + "arrived in mcpp 2026.9.28.3 (protocol 14). Pin \"mcpp\": \"2026.9.28.3\" or newer " + "in .xlings.json, or set the plugin's `toolset` option to `detected`.")); + + // THE GCC PAYLOAD IS NOT A COMPLETE HANDOVER. mcpp runs it with a sysroot, + // a binutils directory and a link model (`--sysroot`, `-B`, the payload's + // dynamic linker and C library) that its own command lines add and a + // foreign build system does not receive; given the driver alone, vcpkg's + // compiler detection failed on CI while it passed on a machine whose host + // compiler filled the gaps. The clang payloads carry their configuration in + // their own `.cfg` files and are complete. On the GCC row the host + // compiler's libstdc++ is the same C++ library, so the foreign system's + // detection agrees with the program, as it did before 0.17.0. + if (!r.msvc_abi && std::string_view(mcpp::compiler()) == "gcc") { + r.how = mechanism::detected; + r.reason = "the GCC payload runs with a sysroot, binutils and a link model that only mcpp's " + "own command lines carry; the host compiler's libstdc++ is the program's C++ library"; + return r; + } + + const std::string instance = forward(mcpp::msvc_instance_dir()); + if (r.msvc_abi && !instance.empty() && c.cc == compiler::abi_native) { + r.how = mechanism::instance; + r.instance_dir = instance; + r.instance_default = instance_default_version(std::filesystem::path(instance)); + r.cc = r.cxx = detail::tool_of(compiler::abi_native, "cxx"); + return r; + } + return detail::chain(c, r); +} + +// The tools named even where an instance exists: a plugin whose foreign +// system is driven without the instance's own loading (CMake's Ninja +// generator) asks for this. `detected` is answered as by `resolve`. +inline std::expected resolve_named(const choice& c = {}) { + auto r = resolve(c); + if (!r || r->how != mechanism::instance) return r; + resolved_tools base; + base.identity = r->identity; + base.crt = r->crt; + base.msvc_abi = r->msvc_abi; + base.toolset_version = r->toolset_version; + return detail::chain(c, base); +} + +// A one-line account of a resolution, for a plugin's own diagnostics and for +// the identity comment of a generated file. +inline std::string describe(const resolved_tools& r) { + std::string s = std::format("{} ({})", r.identity.empty() ? std::string("unknown toolset") : r.identity, + name(r.how)); + if (r.how == mechanism::instance) s += std::format(", instance {}", r.instance_dir); + if (!r.crt.empty()) s += std::format(", crt {}", r.crt); + return s; +} + +} // namespace mcpp::plugins::toolset diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp new file mode 100644 index 0000000..97a2cb3 --- /dev/null +++ b/tests/plugin-logic/build.mcpp @@ -0,0 +1,282 @@ +// The deps members' decisions, one case per decision (mcpp#734, design §6). +// Each case states a build context, runs the member, and reads the action it +// declared and the files it wrote. Nothing is installed or run. +import std; +import mcpp; +import mcpp.plugins.testing; +import mcpp.plugins.toolset; +import mcpp.deps; // the 0.16.0 names, for the compatibility case +import mcpp.deps.vcpkg; +import mcpp.deps.cmake; + +namespace t = mcpp::plugins::testing; +namespace ts = mcpp::plugins::toolset; + +namespace { + +// A vcpkg root with the three standard triplets the cases use, and a manifest. +t::context with_vcpkg(t::context c) { + const bool win = std::string_view(c.values.front().second).find("windows") != std::string_view::npos; + return c.xpkg("xim", "vcpkg", "{root}/vcpkg") + .file(win ? "vcpkg/vcpkg.exe" : "vcpkg/vcpkg") + .file("vcpkg/triplets/x64-windows.cmake", + "set(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE dynamic)\nset(VCPKG_LIBRARY_LINKAGE dynamic)\n") + .file("vcpkg/triplets/x64-windows-static.cmake", + "set(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE static)\nset(VCPKG_LIBRARY_LINKAGE static)\n") + .file("vcpkg/triplets/x64-linux.cmake", + "set(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE dynamic)\nset(VCPKG_LIBRARY_LINKAGE static)\n" + "set(VCPKG_CMAKE_SYSTEM_NAME Linux)\n") + .file("pkg/vcpkg.json", "{ \"dependencies\": [\"fmt\"] }\n"); +} + +t::context with_subproject(t::context c) { + return c.xpkg("xim", "cmake", "{root}/cmake") + .file("cmake/bin/cmake").file("cmake/bin/cmake.exe") + .file("pkg/greet/CMakeLists.txt", "project(greet C)\n"); +} + +int vcpkg_use(mcpp::deps::vcpkg::options o = {}) { + o.libraries = {"fmt"}; + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} + +int cmake_use(mcpp::deps::cmake::options o = {}) { + o.source = "greet"; + o.libraries = {"greet"}; + return mcpp::deps::cmake::use(o) ? 0 : 1; +} + +// The one generated triplet of a case, by file name. +std::string derived_triplet(const t::result& r) { + for (auto const& f : r.files_under("out/deps-vcpkg/triplets")) + if (f.find("-mcpp-") != std::string::npos) return f; + return {}; +} + +std::string stem(const std::string& path) { + return std::filesystem::path(path).stem().string(); +} + +// The derived triplet's name under two different roots: equal names mean the +// name, and the text it is hashed from, hold no path (design §6.6). +std::string first_linux_name; + +} // namespace + +int main(int argc, char** argv) { + return t::run(argc, argv, { + // ── deps-vcpkg ────────────────────────────────────────────────────── + { "vcpkg: a Visual Studio toolset is selected through its instance", + with_vcpkg(t::row::windows_visual_studio()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the member succeeds"); + const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:x64-windows\""}); + c.expect(!a.empty(), "the installation uses the standard triplet x64-windows"); + c.expect(a.find("VCPKG_VISUAL_STUDIO_PATH=") != std::string::npos, + "the action selects the instance with VCPKG_VISUAL_STUDIO_PATH"); + c.expect(a.find("--host-triplet") == std::string::npos, "the host triplet stays vcpkg's"); + c.expect(derived_triplet(r).empty(), "no triplet is derived"); + } }, + + { "vcpkg: another toolset version of the instance derives a triplet that names it", + with_vcpkg(t::row::windows_visual_studio("14.43.34808", "14.44.35207")), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + const auto f = derived_triplet(r); + c.expect(!f.empty(), "a triplet x64-windows-mcpp- is derived"); + c.expect(r.file(f).find("set(VCPKG_PLATFORM_TOOLSET_VERSION 14.43.34808)") != std::string::npos, + "the derived triplet names the resolved toolset version"); + c.expect(r.file(f).find("VCPKG_CHAINLOAD_TOOLCHAIN_FILE") == std::string::npos, + "the instance's own loading stays in place"); + c.expect(r.has_line("mcpp:action=", {"VCPKG_VISUAL_STUDIO_PATH="}), + "the action still selects the instance"); + } }, + + { "vcpkg: a managed toolset is named in a chain-loaded derived triplet", + with_vcpkg(t::row::windows_managed()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the member succeeds"); + const auto f = derived_triplet(r); + const auto name = stem(f); + c.expect(name.starts_with("x64-windows-mcpp-"), "the derived triplet is x64-windows-mcpp-"); + const auto text = r.file(f); + c.expect(text.find("set(VCPKG_LIBRARY_LINKAGE dynamic)") != std::string::npos, + "the base triplet is inlined, not included"); + c.expect(text.find("\"${CMAKE_CURRENT_LIST_DIR}/mcpp-chain-windows.cmake\"") != std::string::npos, + "the chain-loaded toolchain is found beside the triplet"); + c.expect(text.find("MCPP_VCPKG_CXX") != std::string::npos && text.find(" INCLUDE") != std::string::npos + && text.find(" LIB") != std::string::npos, + "the tools and their environment pass through untracked"); + c.expect(text.find("# toolset: msvc 14.44.35207; sdk 10.0.26100.0 (chain)") != std::string::npos, + "the triplet carries the toolset's identity"); + c.expect(text.find("variable_watch(VCPKG_PLATFORM_TOOLSET") != std::string::npos, + "an MSBuild port is refused by name"); + c.expect(text.find(r.root.generic_string()) == std::string::npos, "the triplet holds no path"); + const auto chain = r.file("out/deps-vcpkg/triplets/mcpp-chain-windows.cmake"); + c.expect(chain.find("file(TO_CMAKE_PATH \"$ENV{MCPP_VCPKG_CXX}\"") != std::string::npos, + "the toolchain reads each tool from the environment through TO_CMAKE_PATH"); + c.expect(chain.find("/scripts/toolchains/windows.cmake\")") != std::string::npos, + "the toolchain includes vcpkg's windows.cmake"); + c.expect(chain.find(r.root.generic_string()) == std::string::npos, "the toolchain holds no path"); + const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:" + name}); + c.expect(!a.empty(), "the installation uses the derived triplet"); + c.expect(a.find("\"--host-triplet=" + name + "\"") != std::string::npos, + "the host triplet is the derived one"); + c.expect(a.find("\"VCPKG_KEEP_ENV_VARS=PATH\"") != std::string::npos, + "vcpkg keeps the PATH the action sets"); + c.expect(a.find("\"PATH=") != std::string::npos && a.find("Hostx64") != std::string::npos, + "the toolset's directories are on that PATH"); + c.expect(a.find("VCPKG_VISUAL_STUDIO_PATH") == std::string::npos, "no instance is selected"); + } }, + + { "vcpkg: a self-contained program's ports link the C runtime statically", + with_vcpkg(t::row::windows_managed() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + const auto f = derived_triplet(r); + c.expect(stem(f).starts_with("x64-windows-static-mcpp-"), "the base triplet is x64-windows-static"); + c.expect(r.file(f).find("set(VCPKG_CRT_LINKAGE static)") != std::string::npos, + "the derived triplet states the static C runtime"); + } }, + + { "vcpkg: a project triplet that contradicts the program's C runtime is refused", + with_vcpkg(t::row::windows_visual_studio() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return vcpkg_use({.triplet = "x64-windows"}); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code != 0, "the member fails"); + c.expect(r.errors.find("the triplet 'x64-windows' links the C runtime dynamic") != std::string::npos + && r.errors.find("links it static (the program's C++ runtime contract, self-contained)") + != std::string::npos, + "the error names both statements"); + c.expect(r.count("mcpp:action=") == 0, "nothing is installed"); + } }, + + { "vcpkg: crt_linkage states the ports' linkage and wins over the contract", + with_vcpkg(t::row::windows_visual_studio() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return vcpkg_use({.triplet = "x64-windows", .crt_linkage = "dynamic"}); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the member succeeds"); + c.expect(r.has_line("mcpp:action=", {"deps-vcpkg:install:x64-windows\""}), + "the project's triplet is used"); + } }, + + { "vcpkg: detected leaves the toolset to vcpkg and says until when", + with_vcpkg(t::row::windows_visual_studio()), + [] { return vcpkg_use({.toolset = {.toolset = ts::source::detected}}); }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("mcpp:action=", {"deps-vcpkg:install:x64-windows\""}), + "the standard triplet is used"); + c.expect(!r.has_line("mcpp:action=", {"VCPKG_VISUAL_STUDIO_PATH"}), "no instance is selected"); + c.expect(r.has_line("mcpp:diagnostic=note", {"kept until 2027-03-28"}), + "the compatibility note is printed"); + } }, + + { "vcpkg: the Linux GCC row keeps vcpkg's detection, and says why", + with_vcpkg(t::row::linux_gcc()), + [] { + const auto r = ts::resolve(); + std::printf("reason=%s\n", r ? r->reason.c_str() : ""); + return vcpkg_use(); + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("mcpp:action=", {"deps-vcpkg:install:x64-linux\""}), + "the standard triplet x64-linux is used"); + c.expect(!r.has_line("mcpp:action=", {"MCPP_VCPKG_CXX="}), "no compiler is named"); + c.expect(r.has_line("reason=", {"GCC payload"}), "the resolution states why"); + } }, + + { "vcpkg: the Linux libc++ row hands over mcpp's clang through linux.cmake", + with_vcpkg(t::row::linux_libcxx()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + const auto name = stem(derived_triplet(r)); + c.expect(name.starts_with("x64-linux-mcpp-"), "the derived triplet is x64-linux-mcpp-"); + c.expect(r.file("out/deps-vcpkg/triplets/mcpp-chain-linux.cmake") + .find("/scripts/toolchains/linux.cmake\")") != std::string::npos, + "the toolchain includes vcpkg's linux.cmake"); + const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:" + name}); + c.expect(a.find("MCPP_VCPKG_CXX=") != std::string::npos && a.find("llvm/bin/clang++") != std::string::npos, + "clang++ is handed to vcpkg"); + c.expect(a.find("--host-triplet") == std::string::npos, "host tools keep vcpkg's host triplet"); + c.expect(a.find("\"PATH=") == std::string::npos, "PATH is left alone off the MSVC ABI"); + first_linux_name = name; + } }, + + { "vcpkg: the derived triplet's name does not depend on where the tools are", + with_vcpkg(t::row::linux_libcxx()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(!first_linux_name.empty() && stem(derived_triplet(r)) == first_linux_name, + "the same toolset under another root derives the same triplet"); + } }, + + // ── deps-cmake ────────────────────────────────────────────────────── + { "cmake: a Visual Studio toolset keeps the Visual Studio generator on that instance", + with_subproject(t::row::windows_visual_studio("14.43.34808", "14.44.35207")), + [] { return cmake_use(); }, + [](const t::result& r, t::checker& c) { + const auto script = r.file("out/deps-cmake/greet/greet.cmake"); + c.expect(script.find("[==[-DCMAKE_GENERATOR_INSTANCE=") != std::string::npos, + "CMAKE_GENERATOR_INSTANCE names the instance"); + c.expect(script.find("[==[version=14.43.34808]==]") != std::string::npos, + "-T version= names a toolset other than the instance's default"); + c.expect(script.find("[==[Ninja]==]") == std::string::npos, "the generator stays CMake's default"); + c.expect(script.find("MultiThreaded$<$:Debug>DLL") != std::string::npos, + "the C runtime follows the program's (dynamic)"); + } }, + + { "cmake: a managed toolset is named, with Ninja and mcpp's ninja", + with_subproject(t::row::windows_managed() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return cmake_use(); }, + [](const t::result& r, t::checker& c) { + std::string script; + for (auto const& f : r.files_under("out/deps-cmake/greet")) + if (f.ends_with("greet.cmake")) script = r.file(f); + c.expect(script.find("[==[Ninja]==]") != std::string::npos, "the Ninja generator is used"); + c.expect(script.find("-DCMAKE_MAKE_PROGRAM=") != std::string::npos + && script.find("ninja/ninja.exe") != std::string::npos, "mcpp's ninja runs it"); + c.expect(script.find("-DCMAKE_CXX_COMPILER=") != std::string::npos + && script.find("cl.exe") != std::string::npos, "cl.exe is named"); + c.expect(script.find("MultiThreaded$<$:Debug>]==]") != std::string::npos, + "the C runtime follows the program's (static)"); + const auto a = r.line("mcpp:action=", {"deps-cmake:greet"}); + c.expect(a.find("\"INCLUDE=") != std::string::npos && a.find("\"PATH=") != std::string::npos, + "the tools run with the engine's environment"); + c.expect(script.find("/greet/build-") != std::string::npos, + "the build directory is keyed by the toolset"); + } }, + + { "cmake: compilers in cache_args are the project's decision", + with_subproject(t::row::linux_gcc()), + [] { + mcpp::deps::cmake::options o; + o.cache_args = {"-DCMAKE_CXX_COMPILER=/opt/other/g++"}; + return cmake_use(o); + }, + [](const t::result& r, t::checker& c) { + const auto script = r.file("out/deps-cmake/greet/greet.cmake"); + c.expect(script.find("[==[Ninja]==]") == std::string::npos, "no generator is imposed"); + c.expect(script.find("gcc/bin/g++") == std::string::npos, "the row's compiler is not named"); + } }, + + // ── compatibility ─────────────────────────────────────────────────── + { "compat: program_compilers keeps its 0.16.0 answer and says until when", + t::row::linux_libcxx().file("llvm/bin/clang").file("llvm/bin/clang++"), + [] { + const auto cc = mcpp::deps::program_compilers(); + std::printf("answer=%s\n", cc.cxx.c_str()); + return cc ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("answer=", {"llvm/bin/clang++"}), "mcpp's clang++ on the libc++ row"); + c.expect(r.has_line("mcpp:diagnostic=note", {"program_compilers is kept until 2027-03-28"}), + "the compatibility note is printed"); + } }, + }); +} diff --git a/tests/plugin-logic/mcpp.toml b/tests/plugin-logic/mcpp.toml new file mode 100644 index 0000000..7e74781 --- /dev/null +++ b/tests/plugin-logic/mcpp.toml @@ -0,0 +1,24 @@ +# Fixture: the logic of the deps members, tested with `mcpp.plugins.testing` +# against stated build contexts -- a Visual Studio row, a managed MSVC row, a +# Linux GCC row and a Linux libc++ row -- on every host, with no foreign tool +# installed or run. The build program is the test: a failed case fails the +# build and prints the report; `target/**/plugins-testing/results.txt` holds +# one verdict per case. +[package] +name = "plugin-logic" +version = "0.1.0" +description = "Fixture: the deps members' toolset, triplet and runtime logic" +license = "Apache-2.0" +authors = ["mcpp-community"] + +[language] +standard = "c++23" +modules = true +import_std = true + +[build-dependencies.mcpp] +plugins = { path = "../..", features = ["deps-vcpkg", "deps-cmake", "plugins-testing"], host-module = true } + +[targets.plugin-logic] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/plugin-logic/src/main.cpp b/tests/plugin-logic/src/main.cpp new file mode 100644 index 0000000..76e8197 --- /dev/null +++ b/tests/plugin-logic/src/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tests/qt-consumer/mcpp.toml b/tests/qt-consumer/mcpp.toml index 55a805a..cbd0418 100644 --- a/tests/qt-consumer/mcpp.toml +++ b/tests/qt-consumer/mcpp.toml @@ -13,7 +13,7 @@ # qt_consumer_de.ts updated from src/ by lupdate -> lrelease, deployed # under translations/ # -# The `.ts` sits in the package root, as it does in GalTranslPP, so lupdate +# The `.ts` sits in the package root, as it does in the validation project, so lupdate # writes one file there and claims no directory. # # and the program prints what each produced: a signal delivered to a slot, a diff --git a/tests/qt-import-only/mcpp.toml b/tests/qt-import-only/mcpp.toml index 4c741a2..38a9bab 100644 --- a/tests/qt-import-only/mcpp.toml +++ b/tests/qt-import-only/mcpp.toml @@ -1,6 +1,6 @@ # Fixture: a package that enables `rules-qt` only to import `mcpp.rules.qt`, # the shape of a library of build logic that other packages' build programs -# import (GalTranslPP's `gpp.build`). It writes no `build.mcpp`, has no `.ui`, +# import (a project's own build module). It writes no `build.mcpp`, has no `.ui`, # `.qrc` or `.ts`, and declares no Qt, so mcpp synthesises no build program # for it and nothing reports a missing SDK (mcpp 2026.9.27.1; mcpp#715). [package] diff --git a/tests/vcpkg-make-port/build.mcpp b/tests/vcpkg-make-port/build.mcpp new file mode 100644 index 0000000..bf00975 --- /dev/null +++ b/tests/vcpkg-make-port/build.mcpp @@ -0,0 +1,10 @@ +// The installation is the criterion; the program links nothing from it. +import std; +import mcpp; +import mcpp.deps.vcpkg; + +int main() { + mcpp::deps::vcpkg::options o; + o.install_root = "target/vcpkg_installed"; + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} diff --git a/tests/vcpkg-make-port/mcpp.toml b/tests/vcpkg-make-port/mcpp.toml new file mode 100644 index 0000000..a47920a --- /dev/null +++ b/tests/vcpkg-make-port/mcpp.toml @@ -0,0 +1,22 @@ +# Fixture: a port that vcpkg builds with make under msys (icu). Its configure +# script finds `link.exe` on PATH, which vcpkg's clean environment drops; under +# the `chain` mechanism deps-vcpkg puts the toolset's directories first on a +# PATH that vcpkg keeps. Built on the Windows row whose Visual Studio is masked. +[package] +name = "vcpkg-make-port" +version = "0.1.0" +description = "Fixture: a make-based port under a managed MSVC toolset" +license = "Apache-2.0" +authors = ["mcpp-community"] + +[language] +standard = "c++23" +modules = true +import_std = true + +[build-dependencies.mcpp] +plugins = { path = "../..", features = ["deps-vcpkg"], host-module = true } + +[targets.vcpkg-make-port] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/vcpkg-make-port/src/main.cpp b/tests/vcpkg-make-port/src/main.cpp new file mode 100644 index 0000000..c001237 --- /dev/null +++ b/tests/vcpkg-make-port/src/main.cpp @@ -0,0 +1,3 @@ +#include +#include +int main() { std::printf("vcpkg-make-port: icu %s\n", U_ICU_VERSION); return 0; } diff --git a/tests/vcpkg-make-port/vcpkg.json b/tests/vcpkg-make-port/vcpkg.json new file mode 100644 index 0000000..4a9ac50 --- /dev/null +++ b/tests/vcpkg-make-port/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "vcpkg-make-port", + "version": "0.1.0", + "dependencies": ["icu"], + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d" +} diff --git a/tests/vcpkg-msbuild-port/build.mcpp b/tests/vcpkg-msbuild-port/build.mcpp new file mode 100644 index 0000000..bf00975 --- /dev/null +++ b/tests/vcpkg-msbuild-port/build.mcpp @@ -0,0 +1,10 @@ +// The installation is the criterion; the program links nothing from it. +import std; +import mcpp; +import mcpp.deps.vcpkg; + +int main() { + mcpp::deps::vcpkg::options o; + o.install_root = "target/vcpkg_installed"; + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} diff --git a/tests/vcpkg-msbuild-port/mcpp.toml b/tests/vcpkg-msbuild-port/mcpp.toml new file mode 100644 index 0000000..281a4ff --- /dev/null +++ b/tests/vcpkg-msbuild-port/mcpp.toml @@ -0,0 +1,23 @@ +# Fixture: a port that vcpkg builds with MSBuild (libusb). Under the `chain` +# mechanism -- a managed MSVC toolset, no Visual Studio instance -- no MSBuild +# exists, and deps-vcpkg refuses the port by name instead of letting MSBuild +# fail with `/p:PlatformToolset=external`. Built only on the Windows row whose +# Visual Studio is masked, where the refusal is the criterion. +[package] +name = "vcpkg-msbuild-port" +version = "0.1.0" +description = "Fixture: an MSBuild port under a managed MSVC toolset" +license = "Apache-2.0" +authors = ["mcpp-community"] + +[language] +standard = "c++23" +modules = true +import_std = true + +[build-dependencies.mcpp] +plugins = { path = "../..", features = ["deps-vcpkg"], host-module = true } + +[targets.vcpkg-msbuild-port] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/vcpkg-msbuild-port/src/main.cpp b/tests/vcpkg-msbuild-port/src/main.cpp new file mode 100644 index 0000000..2aa1184 --- /dev/null +++ b/tests/vcpkg-msbuild-port/src/main.cpp @@ -0,0 +1,2 @@ +#include +int main() { std::puts("vcpkg-msbuild-port: built"); return 0; } diff --git a/tests/vcpkg-msbuild-port/vcpkg.json b/tests/vcpkg-msbuild-port/vcpkg.json new file mode 100644 index 0000000..9b31f01 --- /dev/null +++ b/tests/vcpkg-msbuild-port/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "vcpkg-msbuild-port", + "version": "0.1.0", + "dependencies": ["libusb"], + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d" +}