diff --git a/.agents/docs/2026-10-30-ecosystem-part2-ide-and-language-service-design.md b/.agents/docs/2026-10-30-ecosystem-part2-ide-and-language-service-design.md index c23743214..54b37fc0b 100644 --- a/.agents/docs/2026-10-30-ecosystem-part2-ide-and-language-service-design.md +++ b/.agents/docs/2026-10-30-ecosystem-part2-ide-and-language-service-design.md @@ -5,7 +5,7 @@ status: active # 生态设计 part 2:mcpp 插件与 mcppls 的分层、入口、测试、版本感知与分发 -日期:2026-10-30(rev 7,实施)。基线:mcpp 2026.10.30.1(`13e2da04`)、mcpp-ide #3(0.2.0,`734d856` + `49ae757`)、 +日期:2026-10-30(rev 9,实施)。基线:mcpp 2026.10.30.1(`13e2da04`)、mcpp-ide #3(0.2.0,`734d856` + `49ae757`)、 mcppls #75(0.0.20,`4660eb6`)。前文:[part 1](2026-10-29-ecosystem-target-sets-packaging-and-editors-design.md)(§9 mcppls 单包、 §10 编辑器)。相关 issue:mcpp#868(锁文件键)、Sunrisepeak/mcppls-clangd#12(clangd 映像)。相关规范:mcpp SPEC-015 §4、docs/08、 docs/50;mcppls S3、S4;mcpp-ide protocol。 @@ -232,18 +232,23 @@ conformance 夹具固定(§13)。两者仍不一致时(例如 mcppls 设 "mcpp": { "version": "2026.10.30.1", "program": "" }, "problems": [{ "code": "kit-missing", "message": "…" }] }, - "otherCppFeatures": [{ "extension": "ms-vscode.cpptools", "active": true }] + "instances": [{ "pid": 4242, "editor": "vscode", "state": "ready", "since": "…" }], + "actions": [{ "id": "openLogDirectory", "title": "…", "vscode": "mcppls.revealCacheDirectory", "vscodeArguments": ["logs"] }] } ``` +其他 C++ 语言服务是否启用是编辑器的事实(VS Code 读取已安装扩展及其设置),命令行的 `status` 无从得知,不在文档中。 + - 运行中的服务器在工作区缓存目录维护自己的状态,`status` 读取它并补上静态事实(版本、payload、kit 的解析);状态文件是 mcppls 的实现细节,契约是命令的输出。没有服务器在运行时 `state` 为 `none`。 - 同一工作区可能有多个服务器(两个编辑器同时打开):输出 `instances[]`(`pid`、`editor`、`state`、`since`),进程已不存在的实例 忽略;`workspace.state` 取存活实例中最好的一个。 - `actions[]`:mcppls 声明自己的动作及其在各编辑器中的 id(`{"id": "restart", "vscode": "mcppls.restartServer", "jetbrains": - "…", "nvim": "…"}`)。动作 id 归 mcppls,mcpp-ide 不保存另一个产品的 id。 -- 后端的 `languageService/status` 改为执行 `mcppls status`(经后端已有的 mcppls 定位),返回版本、能力、状态与可用动作;三个 - IDE 显示同一份信息。VS Code 不再读 mcppls 扩展的 API 对象(F7)。 + "…", "nvim": "…"}`),VS Code 命令的参数为 `vscodeArguments`。动作 id 由 mcppls S3-8-7 列出;`installKit` 只在工作区的 + kit 缺失时出现,参数与命令行都写明目标。动作 id 归 mcppls,mcpp-ide 不保存另一个产品的 id。 +- 后端的 `languageService/status` 改为执行 `mcppls status`(经后端已有的 mcppls 定位,限时 5 s),返回版本、能力、状态与可用 + 动作(`statusSource` 为 `status`、`frozen` 或 `none`);三个 IDE 显示同一份信息。结果与上次不同(程序、版本、动作或工作区 + 状态)时后端发出一次 `languageService/changed`。VS Code 不再读 mcppls 扩展的 API 对象(F7)。 - 刷新由事件驱动:job 结束、窗口获得焦点、面板打开时执行一次;面板可见时另有不短于 30 s 的轮询,不可见时不轮询。一次执行包括 okrun 启动 okb,预算 200 ms 以内;实测 okrun 启动 15 MB 的 `mcppls.okb` 执行一条轻量命令约 35 ms(Linux x64,§20 D3)。 - `--root` 的比较按系统规范化:Windows 上盘符与分隔符统一、Windows 与 macOS 上不区分大小写;后端与 mcppls 用同一规则(S3 写明)。 @@ -254,15 +259,20 @@ conformance 夹具固定(§13)。两者仍不一致时(例如 mcppls 设 |---|---|---|---| | 重启、重启引擎、重置缓存、导出诊断包、显示日志 | 能力表转发 mcppls 命令(已有) | `ActionManager` 按 id 调用 mcppls 插件的 action;不在时隐藏 | 调用 mcppls-nvim 的命令 | | 安装 kit | 转发 `mcppls.installKit` | mcppls 插件的安装 action | `xlings install …` | -| 关闭其他 C++ 服务 | 转发(已有) | 不适用 | 不适用 | +| 关闭其他 C++ 服务 | 转发(已有) | mcppls 插件的 `UseMcpplsOnly` 与 `RestoreClionEngine` | 不适用 | -转发项的目标 id 来自 `mcppls status` 的 `actions[]`(§5.2);VS Code 现有的能力表(`contract.ts`)改为读这份数据,确认等级 -(`danger`)仍由 mcpp-ide 决定。没有 `status` 的 mcppls(0.0.18–0.0.19)用当前的能力表作为冻结数据。CLion 不再回答"本 IDE 不可用", -没有对应 action 的行不出现在菜单中(F14)。 +命令表(`spec/commands.json`)的转发行写 mcppls 的动作 id(`{"kind": "forward", "mcppls": "restart"}`),后端的 `commands/list` +按编辑器取出该动作在此编辑器中的 id(`action.command`,VS Code 另带 `arguments`),mcppls 没有给出 id 的行不列出;收到 +`languageService/changed` 的编辑器重新取命令表。VS Code 的能力表(`contract.ts`)以 mcppls 的动作 id 为键,只保存标题、确认 +等级(`danger`)与缺失时的说明,命令 id 与参数来自 `languageService/status` 的 `serverActions`。没有 `status` 的 mcppls +(0.0.18–0.0.19)由后端的冻结表给出这两个版本的动作 id。CLion 不再回答"本 IDE 不可用",没有对应 action 的行不出现在菜单中 +(F14);Neovim 只列出 mcppls-nvim 已定义的命令。 ### 5.4 升级规则 -1. 契约版本化(`schema` 加 `capabilities`),后端按能力启用功能;未知能力忽略,缺失能力隐藏对应功能。 +1. 契约版本化(`schema` 加 `capabilities`),后端按能力启用功能;未知能力忽略,缺失能力隐藏对应功能。构建后的刷新 + (`mcpp.languageService.refreshAfterBuild` 的 `auto`)不再转发:0.0.18 起 mcppls 读取结果记录后自行刷新;`reload` 与 + `restart` 仍转发对应动作。 2. 后端不持有 mcppls 进程,只在调用时执行;mcppls 升级、重启、路径改变不影响 mcpp 插件,路径变化经 VS Code `extensions.onDidChange` 与 CLion 的插件状态监听重新定位。 3. mcppls 被禁用或未安装时,mcpp 插件的构建、运行、测试、项目页面照常工作;面板显示"C++ 语言服务未启用"与启用或安装动作。 @@ -283,13 +293,22 @@ conformance 夹具固定(§13)。两者仍不一致时(例如 mcppls 设 ### 6.2 命令表声明位置 -`spec/commands.json` 每行新增 `placements`(`title`、`context`、`statusMenu`、`gutter`)。两个插件按表生成原生入口;新增命令时 -两边同时出现。VS Code 的清单由命令表生成(`mcpp-ide-dev gen vscode`),0.9.0 的一致性门禁允许新增、不允许改变:0.9.0 已有的条目 -(例如 `editor/title` 的 Run、Test 及其 `when` 中的 `mcpp.editorTitleButtons`)逐字保留,新增的入口另起条目。 +`spec/commands.json` 每行新增 `placements`,一个以位置为键、以次序为值的对象(`{"title": 3, "context": 2, "gutter": 2}`; +位置为 `title`、`context`、`gutter`,快速菜单仍由 `quickMenu` 表示)。两个插件按表生成原生入口:VS Code 的 `editor/title` +与 `editor/context`,CLion 编辑器右键菜单中的一组引用(`EditorPopupMenu`);新增命令时两边同时出现。VS Code 的清单由命令表生成(`mcpp-ide-dev gen vscode`),0.9.0 的一致性门禁允许新增、不允许改变:0.9.0 已有的条目 +(例如 `editor/title` 的 Run、Test 及其 `when` 中的 `mcpp.editorTitleButtons`)逐字保留,新增的入口另起条目。`mcpp.build` +在 0.9.0 的清单中没有图标,标题栏需要图标,因此标题栏的构建是新命令 `mcpp.editor.build`(只在 VS Code,不进命令面板), +`mcpp.build` 的条目不变。 + +CLion 的快捷键以两个随插件提供的 keymap 表示(`mcpp`,父为 `$default`;`mcpp (macOS)`,父为 macOS 的 keymap),由命令表的 +`keybinding` 生成:CLion 自带的 keymap 中这些组合键已有绑定(`Ctrl+Alt+B` 是 Go to Implementation),不覆盖;需要 VS Code +组合键的人在 Settings | Keymap 中选择。锤子(Build | Build Project)经 `ProjectTaskRunner` 对含 `mcpp.toml` 的项目执行 +`mcpp build` 作业。 ### 6.3 `main` 的判定 -`main` 的位置来自正在服务这个文件的语言服务:CLion 用 PSI,VS Code 用当前语言服务器(mcppls)给出的文档符号。mcppls 的命令行 +`main` 的位置来自正在服务这个文件的语言服务:CLion 用 PSI(▶ 落在后端读出的那一行上名为 `main` 的 PSI 元素),VS Code 用 +当前语言服务器(mcppls)给出的文档符号(`vscode.executeDocumentSymbolProvider` 中顶层名为 `main` 的函数)。mcppls 的命令行 查询(`query`)要启动引擎,不适合在打开文件时调用,不采用。后端新增 `source/entries`(文件 → 程序还是测试、测试名、框架),只回答 种类,不回答位置。CLion 的正则(F12)与 VS Code 的第 0 行(F11)都改为"位置取自语言服务、种类取自后端"。语言服务不在 (mcppls 被禁用)时,位置取自后端的文本扫描(与 §7.3 的扫描同一实现),并在提示中标注为近似。 @@ -331,8 +350,9 @@ Boost.Test)不适配,其测试按程序级运行。 - **用例的标识**:(项目、测试程序、框架、全名),全名取框架的写法(gtest 的 `Suite.Name`,Catch2 与 doctest 的测试名)。动态列举 的用例按全名挂到静态节点上:gtest 的参数化用例去掉实例前缀与参数后缀(`Inst/Suite.Name/0` → `Suite.Name`)后匹配;匹配不到的 挂在测试程序节点下。同一程序中重名的用例以出现顺序区分。 -- **过滤参数**按框架的语法转义:gtest 的 `:`、`*`、`?`、`-`,Catch2 测试名中的 `,`、`[`、`]`、`\`,doctest 的 `,`;参数以 argv - 传递,不经 shell。报告文件写在每次运行独立的临时目录,运行结束后删除。 +- **过滤参数**按框架的语法转义:Catch2 测试名中的 `,`、`[`、`]`、`*`、`\`,doctest 的 `,`;gtest 的全名是两个标识符,不含其 + 过滤语法的特殊字符,不转义。只来自源文件(未经列举)的 gtest 用例名同时匹配其实例:`S.N` 展开为 `S.N`、`*/S.N/*`(`TEST_P`) + 与 `S/*.N`(`TYPED_TEST`)。参数以 argv 传递,不经 shell。报告文件写在每次运行独立的临时目录,运行结束后删除。 - 列举与运行都执行项目的程序,只在受信任的工作区进行。 - 后端方法:`test/cases`(文件或程序 → 用例、位置、来源)、`test/runCases`(程序与用例 → job,结果按用例映射)。 @@ -343,6 +363,10 @@ Boost.Test)不适配,其测试按程序级运行。 | 用例节点 | Testing API 的子项,位置为宏所在行;▶ 与 ✓/✗ 由 VS Code 显示 | 每个宏一个 `RunLineMarkerContributor`;上次结果来自 `TestStateStorage` | | 结果树 | Testing 视图 | SMTestRunner,后端事件转为 TeamCity service message | | 调试一个用例 | 同一组参数,调试类型 `mcpp` | 同一组参数,CLion 调试器 | + +调试需要测试程序的启动描述:mcpp 2026.10.31.2 增加 `mcpp test --print-launch`(特性 `test-launch`,docs/50),输出与 +`mcpp run --print-launch` 相同的 `mcpp.launch` 信封,`--` 之后是框架的过滤参数。这是 F17 之外 mcpp 核心唯一的变化:测试的 +判定(退出码)与参数透传不变,只增加描述启动的方式。项目的 mcpp 没有 `test-launch` 时,测试只提供运行。 | 无框架的测试 | 粒度为程序,▶ 在 `main` 行 | 同左 | ## 8. 清单与构建程序的规则层 @@ -362,6 +386,13 @@ Boost.Test)不适配,其测试按程序级运行。 - **迁移验收**:以现有 TS 实现在固定样例集(`mcpp.toml`、`build.mcpp` 各一组,覆盖 7 类规则与全部补全位置)上生成黄金输出(诊断、 补全项、hover 文本、跳转位置),后端输出逐项一致后删除 TS 实现;设置开关(`mcpp.buildScript.*`、严重度)原样保留,作为参数 传给后端。 +- **实现(rev 9)**:VS Code 以 languageclient 连接(打包进 `dist/vendor`,VSIX 不带 node_modules);Neovim 以 `vim.lsp` + 连接并附着到 `mcpp.toml`、`build.mcpp` 的缓冲区;CLion 的平台 LSP API 总是自行启动它所连接的服务器进程,与"同一进程" + 冲突,故 CLion 经会话已有的连接发送 `textDocument/*`,以补全贡献者、外部标注器、文档提供者与跳转处理器呈现结果。规则层 + 合并时取两份实现中较完整者:表名经 schema 解读(`workspace.<表>` 即其镜像的表,选择器下的平台表即 `platform.<表>`,命名 + 条目取最长的已知前缀),只在选择器下出现的表带选择器给出,schema 自身的数组表不报错,依赖版本取自 `mcpp search --format + json` 的目录。黄金输出由 TS 实现在样例集的每个位置生成(含扫描器的结构与光标上下文、其解析器测试的全部文档),后端逐项一致 + 后删除 TS 规则层;`src/toml/parser.ts` 只保留供项目页读取声明的结构读取。 - **无后端时(F15)**:后端经 okrun 运行,okrun 覆盖 Linux x86_64 与 aarch64、macOS arm64、Windows x86_64;VS Code 还运行在 macOS x86_64、Windows arm64、Linux armhf 上。回退保留,定义为声明的降级模式:界面显示"后端不可用(本平台)",只提供构建、运行、 测试与项目页面,CI 有一个任务专门以无后端方式运行 Extension Host 测试。okrun 覆盖新平台后,降级模式在该平台自动不再出现。 @@ -738,8 +769,8 @@ openkal 的接口整体提供或整体不提供(§3),`openkal.space` 的 - 不提供可执行的文件映射:执行代码只经 `kal_exec_*`(W^X;§19.1 的 probe 中 Windows 拒绝可执行的文件映射)。 - 共享区域(openbus 的 region、进程间的大数据)由"授权目录下的文件 + 共享映射"构成,不另设操作。 -- 映射之后文件被截短时,访问文件末页之外的部分是一次故障:程序以故障终止(Linux、macOS 的 SIGBUS,Windows 的页内错误),实现不以零页代替;访问是内存读写而不是调用,故没有可返回的错误值。末页中文件末尾之后的字节为零,与各环境一致。 -- 一致性测试覆盖:只读与读写、私有与共享、偏移对齐的拒绝、两个映射之间的可见性、同步之后另一进程读到写入、截短之后的访问以故障终止(子进程中进行)。 +- 映射之后文件被截短时,**共享**区域有三种结果,取哪一种由环境决定:截短被拒绝(Windows);访问新末尾之后的整页是一次故障(Linux 的 SIGBUS);这样的页读作零。任何一种都不读到被截去的字节,末页中新末尾之后的字节为零。**私有**区域与文件的任何改动相同,可能继续显示它显示过的内容(实测 macOS 的私有区域读到被截去的字节)。原先"不以零页代替"的要求被实测否定:要求一种结果,实现就得维护被映射文件的登记表以拒绝截短,即条款 7.1 所排除的兼容层。 +- 一致性测试覆盖:只读与读写、私有与共享、偏移对齐的拒绝、两个映射之间的可见性、同步之后另一进程读到写入、截短之后不读到被截去的字节(子进程中进行)。 - 级联与依赖见 §25。 ### 23.3 openlifa 一轮(不在本轮,记录于 openlifa/openlifa#1) @@ -768,7 +799,7 @@ openkal 的接口整体提供或整体不提供(§3),`openkal.space` 的 | 10 | 可读性 | 文档按评审轮次累积,已定的条款分散 | §22、§23 汇总已定与待定;实现时以 §23.1 为规范清单 | -## 25. 实施计划与任务依赖(rev 7) +## 25. 实施计划与任务依赖(rev 8) ### 25.1 各视角对计划的约束 @@ -786,7 +817,8 @@ openkal 的接口整体提供或整体不提供(§3),`openkal.space` 的 | 任务 | 仓库(PR) | 内容 | 依赖 | |---|---|---|---| -| M | mcpp(新 PR,2026.10.31.1) | 检查点 ① `features` 与 `program`;② mcpp#868;③ §17;④ §11 与 docs/50 的 §4.0;⑤ `polyglot`;⑥ C1(§25.3) | — | +| M | mcpp(#869,2026.10.31.1) | 检查点 ① `features` 与 `program`;② mcpp#868;③ §17;④ §11 与 docs/50 的 §4.0;⑤ `polyglot`;⑥ C1(§25.3) | — | +| M2 | mcpp(2026.10.31.2) | `mcpp test --print-launch`(`test-launch`,§7.4);构建契约的目标侧(C2);Windows 启动器的平台(K5 的发现);本文 rev 8、rev 9 | M | | K1 | openkal(0.18.0) | `openkal.map`:SPEC 条款、头文件与模块、`SURFACE.txt`、一致性测试 | — | | K2 | openkal-linux、openkal-macos、openkal-windows | 实现 `openkal.map`;§25.3 定位到实现的修复 | K1 | | K3 | openkal-image | okrun 以 `openkal.map` 读入映像与 okb(§25.3 B1) | K2 | @@ -806,8 +838,8 @@ openkal 的接口整体提供或整体不提供(§3),`openkal.space` 的 | # | 阻塞项 | 事实 | 所属 | 做法 | 依赖 | |---|---|---|---|---|---| | B1 | 文件 mmap 与常驻内存(多 90–125 MiB) | (a) LLVM 对文件的 `mmap` 得到 `ENOSYS`,回退为读入;(b) okrun 的 `read_all` 以倍增读入整个文件(71.5 MB 的映像占 128 MiB 缓冲),映像运行期间不释放;(c) 映像的段复制进 `kal_exec` 区域 | (a) openkal 规范 → 三个实现 → openkal-musl;(b) openkal-image;(c) 不改,代码只经 `kal_exec_*` | (a) `openkal.map`,musl 的文件 `mmap` 经它;(b) okrun 映射映像文件,okb 中未压缩的成员直接在映射中装入,压缩成员解压到恰好大小的缓冲,装入后解除映射或释放;(c) 计入验收的基线 | K1 → K2 → K3、K4 | -| B2 | Windows 路径 | probe 第 8 条:路径风格在编译时决定,`C:\`、`C:/` 被当作相对名,资源目录找不到;第 12 条:默认三元组在编译时决定;第 9 条:形式正确的 `C:/…` 读取得到 `EINVAL`,发生在 LLVM 之下(openkal-musl 的 `root_length` 已认盘符) | 第 8、12 条:LLVM 补丁(mcppls-clangd);第 9 条:openkal-musl 或 openkal-windows | 补丁:`Style::native` 与默认三元组在运行时由 `kal_env_system` 决定;第 9 条以 probe 的 llvmprobe 映像在 Windows 上逐步(`open`、`fstat`、`read`、`lseek`、`MemoryBuffer::getFile`)隔离,修在所属仓库并加一致性测试 | 补丁无依赖;第 9 条的修复进入 K2 或 K4 | -| B3 | Linux 的 SIGSEGV | 两个架构都在解析 libstdc++ 头文件时崩溃;同一份 aarch64 映像在 macOS(SDK 的 libc++ 头文件)通过;openkal-linux 按请求的大小给任务栈(`stack_for`) | 未定:openkal-linux 或 okrun 的 Linux 部分;或与系统无关的一层(LLVM 补丁、openkal-musl、openkal-llvm-runtime) | 判别实验:在 macOS 上让同一映像解析 Linux 的 libstdc++ 头文件,崩溃则与系统无关,否则在 Linux 一侧;okrun 报告故障地址、所在任务与其栈界;修在所属仓库并加回归测试 | 修复进入 K2、K4 或 C | +| B2 | Windows 路径 | probe 第 8 条:路径风格在编译时决定,`C:\`、`C:/` 被当作相对名,资源目录找不到;第 12 条:默认三元组在编译时决定;第 9 条(隔离结果,mcppls-clangd run 41040892276):`open`、`fstat`、`read`、`lseek`、`MemoryBuffer::getFile` 对 `D:/…`、`D:\…`、相对名全部成功,`EINVAL` 不在 LLVM 之下,而是 `is_absolute` 为假使 `make_absolute` 把当前目录接在 `D:/…` 之前 | LLVM 补丁(mcppls-clangd) | 补丁:`is_style_posix`、`is_style_windows` 在 `__OPENKAL__` 下于运行时由 `kal_env_system` 决定(`Style::native`);clangd 的 `absoluteParent` 与大小写折叠同样按系统;默认三元组在运行时决定 | — | +| B3 | Linux 的 SIGSEGV | 两个架构都在解析 libstdc++ 头文件时崩溃;同一份 aarch64 映像在 macOS(SDK 的 libc++ 头文件)通过。在 openkal-platform 0.18.0(openkal-musl 0.22.9,`pread` 与 `__cxa_thread_atexit`)上重建的映像不再崩溃(gdb 下运行至结束),余下的是 7 个错误:`stddef.h` 找不到。映像的自身名(`kal_env_image`)是 okrun 收到的相对名,资源目录随之为相对路径,clangd 按编译命令的目录解析它 | LLVM 补丁(mcppls-clangd) | `getMainExecutable` 在 `__OPENKAL__` 下把映像名以启动时的当前目录变为绝对路径(openkal 规定该名按程序得到的外部名使用);回归:Linux 上 `--check` 解析 libstdc++ 的标准库用例为 0 个错误 | — | | B4 | 模块 worker | probe 第 11 条:worker 直接运行 `.oke` 而崩溃;`getrlimit`、`setrlimit` 不可用 | LLVM 补丁(series 0017) | worker 经 `kal_env_loader` 以"加载器 + 映像"启动;openkal 没有地址空间上限,worker 的内存上限在 ok 映像上由监督方按常驻内存执行 | — | | C1 | 链接输入 | probe 第 13 条:`[build] ldflags` 中的预构建归档不是链接输入,改动它不重新链接 | mcpp | 链接参数中存在的文件记为链接步骤的隐式输入 | — | | C2 | 构建途径 | probe 第 14 条:约定从编译命令读出,工具链文件由 Python 生成 | mcppls-clangd | 约定取自 `${mcpp.contract}`;生成器是一个小的 mcpp 项目 | — | @@ -834,3 +866,16 @@ mcpplibs.os 不直接涉及:clangd 映像只用 openkal-musl 与 libc++;os GitCode、mcpp-index 条目)→ C → L → I → xim-pkgindex PR。之后在干净的 xlings 环境中验证:裸 `xlings install` 解析到新版本; mcpp 新建、构建、运行、`--pkg-format polyglot`;mcppls 在四种编辑器入口中可用,`mcppls status` 的输出;mcpp-ide 的按项目版本 感知(锁定版本的项目与未锁定的项目)。 + +### 25.6 进度(rev 9) + +| 任务 | 状态 | +|---|---| +| M | 已合入并发布 2026.10.31.1 | +| K1–K4 | openkal 0.18.0、openkal-linux 0.19.0、openkal-macos 0.16.0、openkal-windows 0.14.0、openkal-image 0.1.5、openkal-musl 0.23.0 已发布;一致性套件的副本改在套件自身目录运行(wine 经 binfmt 以全路径启动时,副本按卷根运行,无法创建文件) | +| K5 | openkal-llvm-runtime 0.16.10、openkal-platform 0.19.0 已发布,mcpplibs.os 0.2.4 随索引发布进行。platform 0.19.0 的 CI 暴露 mcpp 2026.10.31.1 的缺陷:包的 Windows 启动器钉 `mcpplibs.os` 0.2.1(openkal 0.17.1)却建在项目所选的平台上,版本组指名 openkal 0.18.0 时图中有两个 openkal;M2 改为在 mcpp 的 openkal 行所钉的平台上构建启动器,platform 的 CI 以 M2 的分支在四个系统上通过 | +| C | B2、B3 定位到 LLVM 补丁(上表),补丁在 probe 分支验证 | +| L | `status` 的动作表扩充为 mcppls 的全部动作(S3-8-7) | +| M2 | 版本定为 2026.10.31.2:`test-launch`、构建契约的目标侧(C2 的前提)、Windows 启动器的平台 | +| I | 检查点 ①–⑤ 已推送:⑤ 为规则层移入后端(LSP,协议 2)、三个编辑器的接入、F15 的降级模式及其 CI 任务;⑥ CLion 原生界面在进行 | + diff --git a/CHANGELOG.md b/CHANGELOG.md index 24150e453..65b623740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.10.31.2] - 2026-10-31 + +### Added + +- **`mcpp test --print-launch`** (feature `test-launch`, docs/50 §9): + the one test the pattern names, built as `mcpp test` builds it and described + as `mcpp test` would start it -- its runner, the environment the run adds, the + arguments after `--` -- in the `mcpp.launch` envelope `mcpp run + --print-launch` prints, with `test` naming it. A debugger starts what it + describes (an IDE debugging a test or one of its cases, design 2026-10-30 + part 2 §7.4). A pattern several tests contain resolves to the test named + exactly; one naming several, `--list`, `--no-run`, `--archive`, + `--message-format json`, several legs and several members are refused. + +### Fixed + +- **The build's contract states the target side** (`${mcpp.contract}`, + docs/30). Its words held the configuration's own flags only: a foreign build + reading it for an ok image (`-openkal`) had neither the C library's and + the C++ runtime's headers nor `__OPENKAL__`, and could not compile + ``. They now end with what every unit receives from beneath it: the + platform's layers' include directories and flags, the engine's predefines + (`__MCPP_TARGET___`, `__OPENKAL__`) and the realised C environment. e2e 994 + compiles a C and a C++ unit for the target with the contract alone. +- **A package's Windows launcher builds whichever openkal-platform release the + project selects** (SPEC-014 §4.3). The launcher (`bin/.exe`) is mcpp's + program, and its `mcpplibs.os` pins one openkal release; it was built over the + project's platform, so a platform whose version group names a newer openkal + (openkal-platform 0.19.0) gave its graph two openkal releases and `mcpp pack` + stopped. It is now built over the platform mcpp's openkal rows pin, the one + its `mcpplibs.os` was released against. e2e 995 packs a project that selects + openkal-platform 0.19.0 with its Windows launcher. + ## [2026.10.31.1] - 2026-10-31 ### Added diff --git a/docs/08-testing.md b/docs/08-testing.md index 4e7f23ffa..c76813369 100644 --- a/docs/08-testing.md +++ b/docs/08-testing.md @@ -236,6 +236,14 @@ One NDJSON record per test, for a CI job or an editor. The schema and its version are [50 — Machine-Readable Output](50-machine-output.md); what belongs here is only that the flag exists and that the human format is the default. +```bash +mcpp test unit/math --print-launch -- --gtest_filter=Math.Adds +``` + +Builds the one test the pattern names and prints how `mcpp test` would start it +(the `mcpp.launch` envelope, mcpp 2026.10.31.2+), for a debugger to start: the +runner, the environment the run adds and the arguments after `--` are in it. + ## Current limitations - A test is one `.cpp` producing one program. mcpp does not discover cases diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 5175d24e9..b1bac4a50 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -818,7 +818,9 @@ own arguments, looking for the runner first in `libexec//-/` and then on `PATH`. The Windows launcher is the engine's program built for `x86_64-windows-gnu`: it finds the package from its own path, picks the image of the machine's architecture, and starts the runner the same way; no `.cmd` is -written. A machine of an architecture the package does not hold gets exit +written. Where the build's platform has a hosted arrangement for that row, the +launcher is built over the platform mcpp's openkal rows pin rather than the +project's, since the `mcpplibs.os` release it uses pins that platform's openkal. A machine of an architecture the package does not hold gets exit status 126 and a sentence naming both. Files `[runtime] deploy` places lie beside each leg's image. diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 35a3517e2..d137ae2a9 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -244,6 +244,7 @@ envelope says when either has changed since. |---|---|---|---| | `target-sets` | 2026.10.31.1 (meaning from 2026.10.30.1) | `--target openkal` and the other sets expand to their legs (SPEC-015 §1) | an IDE's target picker | | `pkg-format-list` | 2026.10.31.1 (meaning from 2026.10.30.1) | `--pkg-format tar,okb` writes each container of one tree (SPEC-014 §5.4) | an IDE's pack form | +| `test-launch` | 2026.10.31.2 | `mcpp test --print-launch` prints the `mcpp.launch` of one test (below) | an IDE debugging a test | ### How a tool runs mcpp for a project @@ -1001,6 +1002,18 @@ starting it. `data` is `program`, `argv`, `cwd`, `env` and `via`, as `launch` above. `--print-launch` takes no `--format`: on `mcpp run`, `--pkg-format` names a distributable. +``` +mcpp test --print-launch [-- ] +``` + +The same for one test (feature `test-launch`, 2026.10.31.2): the test `` names is +built as `mcpp test` builds it and described as `mcpp test` would start it -- +its runner, the environment the run adds, and the arguments after `--` -- with +`test` in `data` naming it. A name that several tests contain is resolved to +the one named exactly; a pattern that names several is refused, as are +`--list`, `--no-run`, `--archive`, `--message-format json`, several legs and +several members. A debugger starts what the envelope describes. + ## 10. Denying effects — `--deny` *(mcpp 2026.10.26.1+)* ``` diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md index 1207571e7..c842c522c 100644 --- a/docs/zh/08-testing.md +++ b/docs/zh/08-testing.md @@ -208,6 +208,13 @@ mcpp test --message-format json [50 —— 机器可读输出](50-machine-output.md);本章只交代这个旗标存在,以及人类可读 格式是默认值。 +```bash +mcpp test unit/math --print-launch -- --gtest_filter=Math.Adds +``` + +构建该模式指定的一个测试,并打印 `mcpp test` 启动它的方式(`mcpp.launch` 信封,mcpp 2026.10.31.2+), +供调试器启动:其中包括运行器、运行所加的环境与 `--` 之后的参数。 + ## 当前边界 - 一个测试是一个 `.cpp` 产出一个程序。mcpp 不发现文件内部的用例,因此框架的逐用例 diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 7415864d9..5c0237586 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -709,7 +709,8 @@ target/dist/hello-0.1.0-openkal/ 包的根,按 `uname -m`(`x86_64`、`amd64` → `x86_64`;`aarch64`、`arm64` → `aarch64`)选择架构目录,Git Bash、MSYS 与 Cygwin 的 shell 交给 `bin/.exe`,以该目录中的映像与自身的参数启动运行器,先在 `libexec//-/` 中、再在 `PATH` 上查找运行器。Windows 启动器是为 `x86_64-windows-gnu` 构建的引擎的程序:从自身路径找到包,选本机 -架构的映像,以同样的方式启动运行器;不生成 `.cmd`。包中没有本机架构时以状态 126 退出,写出两者。 +架构的映像,以同样的方式启动运行器;不生成 `.cmd`。构建的平台为该行提供宿主安排时,启动器在 mcpp 的 openkal 行固定的 +平台上构建,而不是项目选择的平台上,因为它使用的 `mcpplibs.os` 版本固定了该平台的 openkal。包中没有本机架构时以状态 126 退出,写出两者。 `[runtime] deploy` 放置的文件位于每条腿的映像旁边。 `--mode self-contained` 时运行器随包携带:对 `--systems` 的每个系统(`linux`、`macos`、`windows` 取腿的全部架构; diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 8c2bda996..44cbecd04 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -207,6 +207,7 @@ effects 采用具名的方式而不是一个 `destructive: true|false`,因为 |---|---|---|---| | `target-sets` | 2026.10.31.1(语义自 2026.10.30.1) | `--target openkal` 等集合展开为其腿(SPEC-015 §1) | IDE 的 target 选择 | | `pkg-format-list` | 2026.10.31.1(语义自 2026.10.30.1) | `--pkg-format tar,okb` 写出一棵树的各个容器(SPEC-014 §5.4) | IDE 的打包界面 | +| `test-launch` | 2026.10.31.2 | `mcpp test --print-launch` 打印一个测试的 `mcpp.launch`(见下) | 调试测试的 IDE | ### 工具为项目运行 mcpp 的方式 @@ -889,6 +890,15 @@ mcpp run [target] --print-launch `argv`、`cwd`、`env` 与 `via`,同上文的 `launch`。`--print-launch` 不接受 `--format`:在 `mcpp run` 上,`--pkg-format` 表示可分发物的格式。 +``` +mcpp test --print-launch [-- <参数>] +``` + +对一个测试作同样的描述(特性 `test-launch`,2026.10.31.2):`` 指定的测试按 `mcpp test` +的方式构建,并按 `mcpp test` 启动它的方式描述——它的运行器、运行所加的环境、`--` 之后的参数——`data` +另以 `test` 给出测试名。多个测试名包含该模式时取名字完全相同的那个;仍指向多个测试时拒绝,`--list`、 +`--no-run`、`--archive`、`--message-format json`、多条腿与多个成员同样拒绝。调试器启动信封描述的程序。 + ## 10. 拒绝副作用 —— `--deny` *(mcpp 2026.10.26.1+)* ``` diff --git a/mcpp.toml b/mcpp.toml index 2885341f9..9d3b5b1d0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.31.1" +version = "2026.10.31.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index e50972f22..115e2ff9b 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.10.31.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.31.2"; } // namespace mcpp diff --git a/src/build/compile_commands.cppm b/src/build/compile_commands.cppm index 6a3a5d759..d256bc0cb 100644 --- a/src/build/compile_commands.cppm +++ b/src/build/compile_commands.cppm @@ -871,13 +871,36 @@ std::expected write_build_contract(const BuildPlan& plan, con t["args"] = std::move(args); return t; }; + // The target side every unit receives from beneath it (the platform's + // layers' headers and flags, the engine's predefines, the realised C + // environment), after the configuration's own words, as on a unit's line. + auto target_side = [&](nlohmann::json& t, const std::vector& words, bool assembly) { + auto& args = t["args"]; + for (auto const& d : plan.contractTargetSide.includeDirs) { + args.push_back("-isystem"); + args.push_back(d.generic_string()); + } + for (auto const& d : plan.contractTargetSide.includeDirsAfter) { + args.push_back("-idirafter"); + args.push_back(d.generic_string()); + } + for (auto const& w : words) { + // An assembler unit takes the C words' -D, -U and -I only, as a unit's line does. + if (assembly && !(w.starts_with("-D") || w.starts_with("-U") || w.starts_with("-I"))) continue; + args.push_back(w); + } + }; nlohmann::json doc; doc["schema"] = 1; doc["target"] = plan.toolchain.targetTriple; doc["dialect"] = std::string(mcpp::toolchain::dialect_for(plan.toolchain).id); doc["compile"]["c"] = tool(flags.ccBinary, flags.cc, true); + target_side(doc["compile"]["c"], plan.contractTargetSide.cflags, false); doc["compile"]["cxx"] = tool(flags.cxxBinary, flags.cxx, true); + target_side(doc["compile"]["cxx"], plan.contractTargetSide.cxxflags, false); doc["compile"]["asm"] = tool(flags.ccBinary, flags.as, true); + target_side(doc["compile"]["asm"], plan.contractTargetSide.cflags, true); + for (auto const& w : plan.contractTargetSide.asmflags) doc["compile"]["asm"]["args"].push_back(w); auto link = tool(flags.ldDriver.empty() ? flags.cxxBinary : flags.ldDriver, flags.ld, false); link["archiver"] = flags.arBinary.generic_string(); doc["link"] = std::move(link); diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 68952b1a2..46d080ae5 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -4265,6 +4265,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, std::vector argv; std::vector> env; long long buildMs = 0; // this test's own edges in .ninja_log + std::filesystem::path program; // the test's own artifact }; std::vector runnable; @@ -4551,7 +4552,39 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, } runnable.push_back({lu.targetName, std::move(argv), std::move(childEnv), - buildMsOfTest}); + buildMsOfTest, exe}); + } + // `mcpp test --print-launch` (design 2026-10-30 part 2 §7.4): the one test the pattern + // names, built, described as `mcpp run --print-launch` describes a program -- the `mcpp.launch` + // envelope, composed here where the run composes it -- and not started. A debugger starts it. + if (print_launch_flag()) { + const Runnable* chosen = nullptr; + for (auto const& r : runnable) + if (r.name == testOpts.filter) chosen = &r; + if (!chosen && runnable.size() == 1) chosen = &runnable.front(); + if (!chosen) { + mcpp::ui::error(runnable.empty() + ? std::format("--print-launch: no test that built is named '{}'", testOpts.filter) + : std::format("--print-launch describes one test, and '{}' names {}: name one", testOpts.filter, runnable.size())); + return 2; + } + if (!invocationNotRunReason.empty()) { + mcpp::ui::error(invocationNotRunReason); + return 2; + } + ctx->buildLock.reset(); + mcpp::build::result::Launch launch{chosen->argv, chosen->env, + std::filesystem::current_path().generic_string(), + !tb.runUnderPrefix.empty() ? "run_under" + : !runnerTmpl.empty() ? "runner" : ""}; + mcpp::wire::Envelope env{.kind = "mcpp.launch", + .effects = {mcpp::wire::Effect::ReadProject, + mcpp::wire::Effect::WriteProject}}; + env.data = mcpp::build::result::to_json(launch); + env.data["program"] = chosen->program.generic_string(); + env.data["test"] = chosen->name; + mcpp::wire::emit(env); + return 0; } // Pass 2: run them. Concurrently unless there is exactly one — see // `runJobs` for why the single-test case is deliberately different. diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 3af12d2c7..9890abceb 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -435,6 +435,11 @@ struct BuildPlan { // `openkal-llvm` family name, which had to be invented to carry a fact // about the dependency graph on an object that describes a compiler. mcpp::targetside::TargetSide targetSide; + // What every unit of this build receives from beneath it -- the target + // side's include directories and flags, the engine's predefines, the + // realised C environment -- as the build's contract states it for a + // foreign build compiling for the target (`write_build_contract`). + mcpp::modgraph::UsageRequirements contractTargetSide; // THE C++ ISLAND OF THIS BUILD, when it has one (design 2026-10-26 §6.3): // a C++ runtime other than the build's own reaches the link -- declared by // packages' private `mcpp:c++-abi` requirements, named on the link line, diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index ac32ecc35..14975b910 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -510,6 +510,7 @@ static std::expected step13_make_plan(PrepareState& state, Bu // that function does depends on it: the flag assembly that does reads the // plan, and every reader of `compute_flags` runs after this line. ctx.plan.targetSide = state.resolvedTargetSide; + ctx.plan.contractTargetSide = state.contractTargetSide; // C5 / D5a-b (design 2026-09-26 §3.5): the standard-library units this // configuration's build compiles, when it imports `std`. Recovered here, diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 3ac8c8b15..d4ae73c97 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -858,6 +858,9 @@ struct PrepareState { std::optional cxxLayerProviderIndex; bool targetSideResolved = false; mcpp::modgraph::UsageRequirements targetSideUsage; + // The same, with the engine's predefines and the realised C environment: + // what the build's contract states (BuildPlan::contractTargetSide). + mcpp::modgraph::UsageRequirements contractTargetSide; // ── P9/P11: the module scan, its validation, and the fingerprint they // feed; read again by P13 (the plan, resolution.json) ─────────────────── diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index 639b61a7b..ba7174d2a 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -819,6 +819,26 @@ step9_target_side_include_broadcast(PrepareState& state, TargetSideGather& gathe } } + // THE CONTRACT'S TARGET SIDE: everything above that every unit receives, + // in one set, for `${mcpp.contract}` (a foreign build compiling for the + // target reads it there; docs/30). The C environment is the default + // one: `c-environment = "platform"` is a package's own exception. + state.contractTargetSide = state.targetSideUsage; + if (state.tc) { + std::string contractOs; + if (auto ttOs = mcpp::toolchain::triple::parse(state.tc->targetTriple)) + contractOs = ttOs->os; + const auto defines = mcpp::toolchain::predefines::define_tokens( + contractOs, state.tc->kernelAbiIsOpenkal); + for (auto* words : {&state.contractTargetSide.cflags, &state.contractTargetSide.cxxflags}) { + state.appendUniqueFlags(*words, defines); + state.appendUniqueFlags(*words, state.tc->cEnvTokens); + state.appendUniqueFlags(*words, state.tc->cEnvBuiltinsTokens); + } + state.appendUniqueFlags(state.contractTargetSide.asmflags, state.tc->cEnvTokens); + state.appendUniqueFlags(state.contractTargetSide.asmflags, state.tc->cEnvBuiltinsTokens); + } + if (state.tc && (state.tc->kernelAbiIsOpenkal || !state.tc->cEnvTokens.empty() || !state.tc->cEnvBuiltinsTokens.empty())) { for (auto& p : state.packages) { diff --git a/src/cli.cppm b/src/cli.cppm index 50467ce65..4ae83812e 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -828,6 +828,8 @@ int run(int argc, char** argv) { .help("List (filtered) tests without building or running them")) .option(cl::Option("no-runner") .help("Run test binaries directly, ignoring any [target.].runner (a host that runs them natively)")) + .option(cl::Option("print-launch") + .help("Build the one test PATTERN names, then print how it would be started (the mcpp.launch envelope, as `mcpp run --print-launch` prints) instead of running it; arguments after `--` are in its argv")) .option(cl::Option("no-run") .help("Build the tests for --target and stop; a build that succeeds is the result (for a target this host cannot execute)")) .option(cl::Option("archive").takes_value().value_name("FILE") @@ -1799,6 +1801,10 @@ int run(int argc, char** argv) { {"run --print-launch", {Effect::InitMcppHome, Effect::ReadProject, Effect::WriteProject, Effect::Network, Effect::WriteGlobalCache, Effect::ExecBuildScript}}, + // ...and of a test, the one it names. + {"test --print-launch", {Effect::InitMcppHome, Effect::ReadProject, + Effect::WriteProject, Effect::Network, + Effect::WriteGlobalCache, Effect::ExecBuildScript}}, // The index is refreshed when older than the search TTL. {"search", {Effect::InitMcppHome, Effect::Network}}, {"index list", {Effect::InitMcppHome, Effect::ReadProject}}, diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index 08b6d9a73..206166990 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -1235,6 +1235,20 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, ".tar.zst, .tar.xz)", to.archive.string())); return 2; } + // ONE TEST'S LAUNCH (`--print-launch`): the program a debugger starts, so one test of one + // package on one target, built and not run, and nothing else on stdout. + if (parsed.is_flag_set("print-launch")) { + std::string refused; + if (to.filter.empty()) refused = "names no test: --print-launch describes the one test PATTERN names"; + else if (to.list || to.noRun || !to.archive.empty()) refused = "describes a test to run; --list, --no-run and --archive do not run one"; + else if (to.format == mcpp::build::TestMessageFormat::Json) refused = "prints the mcpp.launch envelope; --message-format json prints records"; + else if (!testLegs.empty()) refused = "describes one test on one target, and --target names several legs"; + if (!refused.empty()) { + mcpp::ui::error(std::format("mcpp test --print-launch {}", refused)); + return 2; + } + mcpp::build::set_print_launch(true); + } if (runArchive) { if (!testLegs.empty() || !ov.target_triple.empty()) { mcpp::ui::error("mcpp test --archive runs the legs the archive holds; --target " @@ -1259,6 +1273,10 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, mcpp::ui::error("--archive holds the tests of one package: select one member (-p)"); return 2; } + if (*selection && ((*selection)->whole || (*selection)->members.size() > 1) && parsed.is_flag_set("print-launch")) { + mcpp::ui::error("mcpp test --print-launch describes one test of one package: select one member (-p)"); + return 2; + } if (*selection && ((*selection)->whole || (*selection)->members.size() > 1)) { // The members, tested on each leg in turn: a leg's members are planned // by its configuration groups and built once, as for one target. diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index 53c42d62b..871557396 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -1075,7 +1075,8 @@ std::string exe_launcher_manifest(std::string_view name, std::string_view versio "[package]\nname = \"{0}\"\nversion = \"{1}\"\n\n" "[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" "# 0.2.1: the release whose Windows half builds over a platform's hosted\n" - "# arrangement, which the launcher is built over.\n" + "# arrangement, which the launcher is built over; its openkal is the one\n" + "# the platform of mcpp's openkal rows names, the platform it is built over.\n" "[dependencies.mcpplibs]\nos = \"0.2.1\"\n", name, version.empty() ? std::string_view("0.0.0") : version); } diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 945bbd269..712b14351 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -2469,6 +2469,14 @@ std::string static_row_pin(const ProgramRow& row, std::string_view platformToolc return std::format("\n[target.{}]\ntoolchain = \"{}\"\n", row.triple, pin); } +// The platform the Windows launcher is built over: the one mcpp's openkal +// rows pin, which the launcher's `mcpplibs.os` release was released against. +std::string_view launcher_platform() { + auto t = mcpp::toolchain::triple::parse("x86_64-openkal"); + auto* k = t ? mcpp::toolchain::triple::find_known_target(*t) : nullptr; + return k ? std::string_view(k->platform) : std::string_view(); +} + // Whether this mcpp builds programs for `row`: a target its table does not // state (`aarch64-windows-gnu`), or states as planned (`x86_64-macos`, its // toolchain not published), is not built. @@ -2724,9 +2732,16 @@ run_loaded(const Plan& plan) // x86_64 Windows, which an arm64 Windows machine also runs. A host // that builds no program for that row leaves it out, says so, and // the package's `runs_on` names no Windows pair. - // Over the platform's hosted arrangement where it serves the row, as - // the runners are: the same program whichever machine packs it. - const auto row = program_row("windows", "x86_64", true, &plan.platform); + // Over a platform's hosted arrangement where the build's platform + // serves the row, as the runners are: the same program whichever + // machine packs it. The launcher is mcpp's program and not the + // project's: its `mcpplibs.os` pins one openkal release, which only the + // platform mcpp's own openkal rows name agrees with, so it is built + // over that platform whichever release the project selected (a newer + // version group named another openkal, and the launcher's graph held + // two; openkal-platform 0.19.0's CI, 2026-10-30). + auto row = program_row("windows", "x86_64", true, &plan.platform); + if (!row.platform.empty()) row.platform = std::string(launcher_platform()); if (!row_is_built(row)) { mcpp::ui::warning(std::format( "no Windows launcher in the package: this mcpp builds no program for {}; the package " diff --git a/src/wire.cppm b/src/wire.cppm index 862637098..b5e7c863a 100644 --- a/src/wire.cppm +++ b/src/wire.cppm @@ -264,9 +264,10 @@ inline nlohmann::json to_json(const Diagnostic& in) { // exist, which a client turns something on or off by. A name is added when a // client gates on it, is never renamed and never removed; docs/50 names the // release that added it and the client that reads it. -inline constexpr std::array kFeatures{{ +inline constexpr std::array kFeatures{{ "target-sets", // SPEC-015 §1: `--target openkal` is the set's legs (an IDE's target picker) "pkg-format-list", // SPEC-014 §5.4: `--pkg-format tar,okb` writes each container (an IDE's pack form) + "test-launch", // docs/50 §6: `mcpp test --print-launch` (an IDE debugging a test) }}; // The same list as JSON: `--protocol-version` and every envelope's `mcpp` field diff --git a/tests/e2e/993_a_test_describes_its_launch.sh b/tests/e2e/993_a_test_describes_its_launch.sh new file mode 100755 index 000000000..767be9dc8 --- /dev/null +++ b/tests/e2e/993_a_test_describes_its_launch.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# requires: python3 +# 993 -- `mcpp test --print-launch` (docs/50 §9, feature `test-launch`; +# design 2026-10-30 part 2 §7.4): one test, built and described as `mcpp test` +# would start it, for a debugger to start. +# +# Criteria: +# A. The `mcpp.launch` envelope names the test (`data.test`), its program +# (`argv[0]` is `program`, which exists), `env` and `cwd`, and ends its argv +# with the arguments after `--`; the test is not started. +# B. What the envelope describes runs: the argv with the environment exits 0 +# and the test's own output appears. +# C. A pattern several tests contain is resolved to the test named exactly. +# D. Refused, exit 2: no pattern; a pattern naming several tests; +# `--message-format json`; `--no-run`. +# E. `--protocol-version` states `test-launch`. +set -e +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } +PY=python3 + +cd "$TMP" +"$MCPP" new app > /dev/null +cd app +mkdir -p tests/unit +cat > tests/unit/math.cpp <<'EOF' +import std; +int main(int argc, char** argv) { + std::ofstream("started-math") << argc; + std::println("math ran with {} argument(s), last [{}]", argc - 1, argc > 1 ? argv[argc - 1] : ""); + return 0; +} +EOF +cat > tests/unit/math2.cpp <<'EOF' +import std; +int main() { std::ofstream("started-math2") << 1; return 0; } +EOF + +# A +"$MCPP" test unit/math --print-launch -- --flag x > a.out 2> a.err || fail "A: --print-launch failed" a.err +[ ! -e started-math ] && [ ! -e target/started-math ] || fail "A: the test was started" +$PY - a.out <<'EOF' || fail "A: not the test's mcpp.launch envelope" a.out +import json, sys, os +same = lambda a, b: os.path.normcase(os.path.normpath(a)) == os.path.normcase(os.path.normpath(b)) +d = json.load(open(sys.argv[1])) +assert d["kind"] == "mcpp.launch", d["kind"] +data = d["data"] +assert data["test"] == "unit/math", data +assert os.path.isfile(data["program"]), data["program"] +argv = data["argv"] +assert any(same(a, data["program"]) for a in argv), data +assert argv[-2:] == ["--flag", "x"], argv +assert "env" in data and "cwd" in data +EOF +echo "ok: the envelope describes the test" + +# B +$PY - a.out > b.out 2> b.err <<'EOF' || fail "B: what the envelope describes did not run" b.out b.err +import json, sys, os, subprocess +d = json.load(open(sys.argv[1]))["data"] +env = dict(os.environ) +env.update(d.get("env") or {}) +r = subprocess.run(d["argv"], cwd=d["cwd"], env=env, capture_output=True, text=True) +sys.stdout.write(r.stdout) +sys.exit(r.returncode) +EOF +grep -q "math ran with 2 argument(s), last \[x\]" b.out || fail "B: the test's output" b.out +echo "ok: the description runs" + +# C -- `unit/math` is contained in `unit/math2` too +grep -q '"unit/math"' a.out || fail "C: the exact name was not chosen" a.out +echo "ok: the exact name is chosen" + +# D +set +e +"$MCPP" test --print-launch > d1.out 2> d1.err; rc1=$? +"$MCPP" test unit/ --print-launch > d2.out 2> d2.err; rc2=$? +"$MCPP" test unit/math --print-launch --message-format json > d3.out 2> d3.err; rc3=$? +"$MCPP" test unit/math --print-launch --no-run > d4.out 2> d4.err; rc4=$? +set -e +[ "$rc1" = 2 ] && grep -q "names no test" d1.err || fail "D: no pattern ($rc1)" d1.err +[ "$rc2" = 2 ] && grep -q "describes one test" d2.err || fail "D: several tests ($rc2)" d2.err +[ "$rc3" = 2 ] && grep -q "message-format json" d3.err || fail "D: json ($rc3)" d3.err +[ "$rc4" = 2 ] && grep -q -- "--no-run" d4.err || fail "D: --no-run ($rc4)" d4.err +echo "ok: the refusals" + +# E +"$MCPP" --protocol-version > e.out 2>&1 || fail "E: --protocol-version" e.out +grep -q '"test-launch"' e.out || fail "E: test-launch is not stated" e.out +echo "ok: the feature is stated" + +echo "OK" diff --git a/tests/e2e/994_the_contract_names_the_target_side.sh b/tests/e2e/994_the_contract_names_the_target_side.sh new file mode 100755 index 000000000..d5d0313ad --- /dev/null +++ b/tests/e2e/994_the_contract_names_the_target_side.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# requires: llvm +# 994 -- the build's contract (`${mcpp.contract}`, docs/30) states the target +# side every unit receives: for an ok image (`-openkal`), the platform's +# layers' headers (the C library's, the C++ runtime's), the engine's +# predefines (`__OPENKAL__`) and the realised C environment. A foreign build +# compiles for the target with the contract alone (the clangd ok image's +# LLVM build, design 2026-10-30 part 2 §25.3 C2). +# +# Criteria: +# A. The contract's C++ words name `-D__OPENKAL__` and an `-isystem` +# directory holding the C library's . +# B. A C++ unit that includes and compiles with the +# contract's C++ program and words (and a standard of its own) alone. +# C. A C unit that includes compiles with the C program and words. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -m)" in + x86_64|amd64) ARCH=x86_64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac + +cd "$TMP" +mkdir -p app/src +printf '[package]\nname = "ct"\nversion = "0.1.0"\n' > app/mcpp.toml +printf 'int main() { return 0; }\n' > app/src/main.cpp +(cd app && "$MCPP" build --target "$ARCH-openkal") > b.log 2>&1 || fail "the build" b.log +contract=$(find app/target/"$ARCH"-openkal -maxdepth 2 -name mcpp-contract.json | head -1) +[ -n "$contract" ] || fail "no contract" b.log + +# A +python3 - "$contract" <<'EOF' || fail "A: the target side is not in the contract" "$contract" +import json, os, sys +d = json.load(open(sys.argv[1])) +cxx = d["compile"]["cxx"]["args"] +assert "-D__OPENKAL__" in cxx, cxx +dirs = [cxx[i + 1] for i, w in enumerate(cxx[:-1]) if w == "-isystem"] +assert any(os.path.isfile(os.path.join(p, "stdio.h")) for p in dirs), dirs +EOF +echo "ok: the contract names the target side" + +# B, C +printf '#include \n#include \nint f() { std::vector v{1, 2}; std::printf("%%zu", v.size()); return 0; }\n' > u.cpp +printf '#include \nint g(void) { return puts("x"); }\n' > u.c +python3 - "$contract" > run.log 2>&1 <<'EOF' || fail "B/C: a unit did not compile with the contract alone" run.log +import json, subprocess, sys +d = json.load(open(sys.argv[1])) +for lang, unit, std in (("cxx", "u.cpp", "-std=c++20"), ("c", "u.c", "-std=c11")): + t = d["compile"][lang] + argv = [t["program"], *t["args"], std, "-c", unit, "-o", unit + ".o"] + r = subprocess.run(argv, capture_output=True, text=True) + print(lang, r.returncode, r.stderr[-2000:]) + assert r.returncode == 0, lang +EOF +echo "ok: units compile with the contract alone" + +echo "OK" diff --git a/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh b/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh new file mode 100755 index 000000000..d9525df95 --- /dev/null +++ b/tests/e2e/995_the_windows_launcher_builds_over_any_platform_release.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# requires: llvm +# 995 -- the Windows launcher of a package is mcpp's program, built over the +# platform mcpp's own openkal rows pin, whichever openkal-platform release the +# project selects (SPEC-014 §4.3; docs/10). The launcher's mcpplibs.os pins one +# openkal release; built over the project's platform, a version group naming +# another openkal put two of them in its graph and `mcpp pack` stopped +# (openkal-platform 0.19.0's CI, 2026-10-30). +# +# Criteria: +# A. A project that selects openkal-platform 0.19.0 (openkal 0.18.0) packs +# `--mode self-contained --systems linux,windows`: the tree holds +# `bin/.exe` and the Windows runner. +# B. The image is the selected platform's: the pack names 0.19.0. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64|Linux-amd64) ;; + *) echo "SKIP: the Windows launcher is built here on Linux x86_64"; exit 0 ;; +esac + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "wl" +version = "0.1.0" + +[target.'cfg(os = "openkal")'] +platform = "mcpplibs.openkal-platform@0.19.0" +TOML +cat > src/main.cpp <<'CPP' +import std; +int main(int argc, char**) { std::println("wl ok: {} args", argc); } +CPP + +# A +"$MCPP" pack --target x86_64-openkal --mode self-contained --systems linux,windows \ + --pkg-format dir -o t > a.log 2>&1 || fail "A: pack" a.log +[ -f target/dist/t/bin/wl.exe ] || fail "A: no bin/wl.exe" a.log +[ -f target/dist/t/libexec/okrun/windows-x86_64/okrun.exe ] || fail "A: no Windows runner" a.log +echo "ok: the launcher builds whichever platform release the project selects" + +# B +grep -q "openkal-platform 0.19.0" a.log || fail "B: the selected platform is not the image's" a.log +echo "ok: the image is the selected platform's" + +echo "PASS: 995 the Windows launcher builds over any platform release" diff --git a/tests/e2e/timings/linux.tsv b/tests/e2e/timings/linux.tsv index f9d34787c..f1fb5d647 100644 --- a/tests/e2e/timings/linux.tsv +++ b/tests/e2e/timings/linux.tsv @@ -605,3 +605,6 @@ 990_a_lock_key_does_not_depend_on_the_selected_members.sh 20000 991_a_prebuilt_archive_in_ldflags_is_a_link_input.sh 30000 992_a_polyglot_file_runs_on_every_system_it_holds.sh 240000 +993_a_test_describes_its_launch.sh 30000 +994_the_contract_names_the_target_side.sh 60000 +995_the_windows_launcher_builds_over_any_platform_release.sh 240000 diff --git a/tests/unit/test_wire.cpp b/tests/unit/test_wire.cpp index 825179088..6a02dc8d0 100644 --- a/tests/unit/test_wire.cpp +++ b/tests/unit/test_wire.cpp @@ -148,6 +148,7 @@ TEST(WireProtocol, NamesTheFeaturesAClientGatesOn) { for (auto const& f : j["features"]) names.push_back(f.get()); EXPECT_NE(std::ranges::find(names, "target-sets"), names.end()); EXPECT_NE(std::ranges::find(names, "pkg-format-list"), names.end()); + EXPECT_NE(std::ranges::find(names, "test-launch"), names.end()); } TEST(WireProtocol, IsItselfSelfIdentifying) {