diff --git a/.agents/docs/2026-11-07-ecosystem-885-closeout-design.md b/.agents/docs/2026-11-07-ecosystem-885-closeout-design.md index c6f369bb3..9c46caa21 100644 --- a/.agents/docs/2026-11-07-ecosystem-885-closeout-design.md +++ b/.agents/docs/2026-11-07-ecosystem-885-closeout-design.md @@ -668,6 +668,99 @@ IDE 通过 mcppls 的载荷找到语言服务:后端 `languageservice.cppm` 6. 更新 #52、#75 的正文;在 #885 发布最终台账,逐项列出“实现提交 / 局部验证 / 最终 CI / 公开发布 / 镜像与索引”。 7. 流程:exact-head CI 被取消时不得合入(Q13);家族发布经 A6,自动列出未关闭缺陷。 +### 4.9 IDE 中的运行与构建反馈(D19) + +**现象(用户实测,CLion):** 右键 Run 时控制台先出现“mcpp: building…”,约 1 s 后才出现程序输出,代码没有改动时也是如此;构建时看不到 mcpp 的构建信息与进度;侧栏上没有 MC++ 图标。 + +**测量(Linux x86_64,无改动时):** + +| 命令 | 耗时 | +|---|---| +| `mcpp build`、`mcpp run` | 0.00–0.01 s(快速路径) | +| `mcpp run --print-launch`,单文件工程 | 0.08–0.10 s | +| `mcpp run --print-launch`,mcpp-ide 工程 | 0.30 s | +| 后端 `mcpp/availability` 与 `run/launch` | 0 ms 与上一行相同 | + +**原因:** + +1. `--print-launch` 不走快速路径(`try_fast_run` 以“the launch the full path composes”拒绝),每次都重新解析依赖、工具链并重做规划;`--message-format json` 下也只输出最后的 envelope,没有进度事件。 +2. 后端的 `run/launch` 是一次同步请求,期间既不转发 mcpp 的叙述,也不转发进度;只有失败时才把叙述交给编辑器。CLion 与 Neovim 因此先打印固定的“mcpp: building…”,再静默等待。VS Code 在终端里运行 `mcpp run`,看得到 mcpp 的原始输出。 +3. CLion 的 Build 作业转发了叙述(`job/output`),但没有处理 `job/progress`,看不到 `n/m` 进度;VS Code 与 Neovim 处理了这一通知。 +4. CLion 的工具窗口注册在右侧,新 UI 默认不把第三方工具窗口放上侧栏。 + +**设计:** + +1. **mcpp 2026.11.7.2。** + - **原先的限制与取舍。** 2026-10-26 控制契约设计(§5.3、B4)要求启动描述与 `mcpp run` 的启动逐项一致;完整路径 + 组合 runner 前缀、执行宿主、runner 携带的文件与运行契约环境,快速路径只组合程序、loader 环境与 subos 环境, + 而快速路径的正确性靠一份屡有漏项的拒绝清单(#544、#716、#755、riscv)。因此 `--print-launch` 只走完整路径。 + 这不是必要的限制:没有改动时 `mcpp run` 本身就由快速路径以它组合的 argv 与环境执行程序,缺的只是完整路径 + 才有的 `configuration` 与“两条路径组合相同”的保证。 + - **记录与比较。** 完整路径输出启动描述时,把它(不含本次的程序参数)记录在该配置的目录中 + (`launch/.json`)。快速路径照常经过 `mcpp run` 快速路径的全部准入与 ninja,然后把自己组合出的 + `argv`、`env`、`via`、`program` 与记录逐项比较:相同则输出记录并附上本次的程序参数;不同或没有记录则退回 + 完整路径,由它重写记录。输出因此在构造上就是完整路径的结果,不另设拒绝清单。 + - `--print-launch --message-format json` 输出 NDJSON:构建的 plan、waiting、progress、artifact 事件,最后一行是 + `mcpp.launch` 信封;特性名 `launch-stream`。不带 `--print-launch` 的 `run --message-format json` 被拒绝。不带 + `--message-format json` 时输出不变。 + - `mcpp test --print-launch` 不变:`mcpp test` 没有快速路径,每次都规划。 + - 回归 e2e 1024:两条路径的信封逐字段相同,且没有重新解析;改动源文件后重新编译并给出相同描述;被改动的记录 + 不被使用并被重写;流式输出逐行是 JSON、含 progress、末行为信封;`--protocol-version` 列出 `launch-stream`。 + 实测:单文件工程 0.09 s 降到 0.01 s,mcpp-ide 工程 0.30 s 降到 0.01 s。 +2. **mcpp-ide 1.2.0 后端。** 新增作业种类 `launch`:mcpp 具备 `launch-stream` 时以 `--print-launch --message-format json` 执行(否则以 `--print-launch` 执行,只转发叙述),构建期间发出 `job/output`(mcpp 的叙述)与 `job/progress`,`job/finished` 携带 launch。`run/launch` 与 `debug/configuration` 保留,供同系列的旧客户端使用。 +3. **CLion。** + - Run 与 Debug 以 `launch` 作业构建:控制台实时显示 mcpp 的叙述,不再打印固定文字;无改动时只有 mcpp 的 + “Finished … in 0.01s”。Debug 的构建在进度对话框中显示 mcpp 的最后一行叙述与步数比例,随后取调试配置(mcpp + 走快速路径)。 + - Build、Test 与 Run 的进度显示在状态栏 widget 上(`<作业> n/m`)。 + - 在 idea.log 记录各段耗时:作业受理、构建、程序启动。 + - 工具窗口改为左侧(`anchor="left"`),与 Project 并列;第一次打开 mcpp 项目时把它的按钮放上侧栏(新 UI + 默认把插件的工具窗口放在“更多工具窗口”中),之后保留用户的调整。项目判定不变:打开的目录有 `mcpp.toml`。 +4. **Neovim。** `:Mcpp run` 以 `launch` 作业构建,叙述写入 `mcpp://output`,进度进入状态行(`<作业> n/m`),与 Build 一致;冒烟测试覆盖。 +5. **VS Code。** Run 仍在终端中运行 `mcpp run`;调试前的构建改用 `launch` 作业,进度进入状态栏。 + +### 4.10 平台与 os 的版本语义(D20) + +**取舍。** 2026.11.7.1 的内置平台是系列 `^0.24`:没有锁文件时取索引中最新的补丁,结果写入 `mcpp.lock`。有锁文件时 +构建可以复现;没有锁文件的工程(新建的、不提交锁文件的)在不同时间首次解析会得到不同的补丁,而 mcpp 的 e2e 与发布 +canary 验证的是一个具体的平台版本。本轮的 0.24.1 带入 Windows 路径丢卷,由 0.24.2 修正,说明自动取用补丁也会把未经 +该版 mcpp 验证的改动带给用户。改为精确版本后: + +- 同一版 mcpp 在没有锁文件时也构建同一组版本,默认值就是该版 mcpp 验证过的组合; +- openkal 生态发布补丁或新系列都不要求发布 mcpp:开发者在 `[target.'cfg(os = "openkal")'] platform` 中选择,系列写法 + (`^0.24`)、版本组与锁文件照常生效; +- 代价是修正不会自动到达不写平台的工程,由失败提示弥补,严重问题以 mcpp 补丁版本推进默认值。 + +**设计(mcpp 2026.11.7.2):** + +1. **默认平台。** 两个内置 openkal 目标的默认平台为精确的 openkal-platform 0.24.3(带 openkal-image 0.5.2 的 Windows 主线程栈修正, + 4.11)。0.24.3 发布并进入索引之后,2026.11.7.2 才发布,已解析到 0.24.2 或 0.24.3 的工程不会被降级。锁文件中的平台不满足 + 精确请求时按请求重新解析,并给出一行说明(锁定版本与所选版本)。开发者写出的平台(精确或系列)照常生效。 +2. **失败提示。** openkal 目标的构建失败、平台来自内置默认值、且已同步的索引中有更新的版本时,输出一行提示:本次使用的 + 平台版本(某版 mcpp 的默认值)、索引中更新的补丁与系列,以及可直接复制的写法 + `[target.'cfg(os = "openkal")'] platform = "openkal-platform@<版本>"`。不推测失败原因;开发者写出平台时不提示。 +3. **mcpplibs.os。** mcpp 自身的 `mcpp.toml`、`modules/` 的成员与 `mcpp pack` 生成的启动器清单写 `os = "=0.3.1"`,打包产物在不同 + 时间可复现。os 对 openkal 实现的依赖保持兼容区间,避免过度约束下游。 +4. **回归。** e2e 1020/1021 改为验证开发者显式写出的系列(版本组选择、锁定与跳过更新的引擎);新增 e2e:默认平台是精确的 + 0.24.3,与锁文件冲突时重新解析并说明,构建失败且索引有更新版本时给出提示,显式写平台时不提示。`docs/35-platforms` + 中英文与 CHANGELOG 写明默认值与覆盖方式。 +5. **生态产品。** mcppls 与 mcpp-ide 的清单改为精确的 `openkal-platform@0.24.3`(取代本轮 A4 的系列写法),发布的载荷对应确定的 + 平台版本;mcpp-plugins 不指定平台,不受影响。 + +### 4.11 ok 映像在 Windows 上的主线程栈(openkal-image 0.5.2,平台 0.24.3) + +**现象。** clangd.okb 的 e2e(4.5 第 6 项)在 windows-2022 上,`compiler_extensions` 的 `windows-mode-off` 用例(`--target=x86_64-pc-windows-msvc +-fno-ms-extensions`,`--sync`)以 `0xC00000FD STATUS_STACK_OVERFLOW` 结束;原生 clangd 与 Linux 上的映像都通过。 + +**原因。** 映像在 okrun 的主线程上执行。Windows 主线程的栈是可执行文件的预留值,已发布 okrun.exe 的 PE 头为 1 MiB(链接器默认); +openkal-windows 给其他线程至少 8 MiB,Linux 与 macOS 的主线程为 8 MiB。clangd 带 `--sync` 时在主线程上为 MSVC 头文件建立标准库索引, +错误恢复的递归超出 1 MiB。所有 ok 映像在 Windows 上的主线程都受此限制。 + +**修正。** openkal-tools 0.5.2 在 Windows 上链接时预留 8 MiB + 256 KiB(openkal-windows 线程默认栈与其保护页余量),按链接器分别写成 +MSVC ABI 的 `/STACK` 与 GNU ABI 的 `--stack`。`examples/stack` 在主线程上递归 6 MiB,CI 在四个系统上运行。本地验证:交叉构建的 +okrun.exe 预留 8650752 字节;wine 下旧 okrun.exe 运行该示例无输出地结束,新 okrun.exe 正常输出。平台 0.24.3 的版本组只把 +openkal-image 改为 0.5.2,经一个索引变更发布(D12)。 + ## 5. 决定 | 编号 | 决定 | 状态 | @@ -690,6 +783,8 @@ IDE 通过 mcppls 的载荷找到语言服务:后端 `languageservice.cppm` | D16 | 运行入口与测试结果以同一份后端数据、各编辑器原生的图标呈现;测试输出逐项归属,状态与 mcpp 契约逐值一致;用例切分只在 IDE 后端 | 已批准 | | D17 | #896(同名不同命名空间的两个包只链接其一)与 #897(后到的边请求的新特性不被遍历)是 mcpp 的缺陷,并入 #889 修复(4.1.7) | 用户已决定 | | D18 | mcppls-clangd、mcppls、mcpp-ide 自本轮版本起只分发 ok 映像,测试也以 ok 映像进行;原生构建保留为测试工具与对比运行,PR、push 与发布的 CI 默认只在 linux-x64 构建原生程序,其余平台的原生构建与测试由手动触发(4.5 第 6 项) | 用户已决定 | +| D19 | IDE 中的运行与构建反馈与终端一致:`run --print-launch` 走快速路径并可流式报告;后端以作业执行启动构建;CLion 与 Neovim 显示 mcpp 的真实叙述与进度;CLion 工具窗口位于左侧并默认显示(4.9) | 用户已决定 | +| D20 | 内置 openkal platform 与 mcpplibs.os 用精确版本:mcpp 2026.11.7.2 的默认平台为 openkal-platform 0.24.3,mcpp 自身与它生成的启动器清单写 os `=0.3.1`;生态更新由开发者在自己的 `mcpp.toml` 中选择,构建失败而索引有更新版本时给出一行提示;mcppls 与 mcpp-ide 的平台写精确的 `@0.24.3`(4.10) | 用户已决定 | ## 6. 执行顺序 diff --git a/CHANGELOG.md b/CHANGELOG.md index f34768485..67ef965d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.7.2] - 2026-11-07 + +### Changed + +- `mcpp run --print-launch` takes the fast path when nothing changed, as `mcpp + run` does: 0.01 s in place of 0.1 to 0.3 s for each launch an editor asks for. + The full path records the launch it composes in the configuration's directory; + the fast path prints that record, with the request's program arguments, only + when the launch it composes itself is the recorded one, so a launch through a + runner, an execution host or carried files is still described by the full + path (e2e 1024). + +- The built-in platform of the two ok-image targets is one release, + openkal-platform 0.24.3, the one this mcpp was qualified with (2026.11.7.1 + selected the series `^0.24`): a project that states no platform builds the + same versions at any time, with or without `mcpp.lock`. A lock that records + another release does not decide, and the build says which it recorded. A + newer platform needs no new mcpp: a project selects it in its manifest, and a + build that fails over the default while the synced index holds a newer + release ends with a `tip:` naming it and the line to write (e2e 1025; + SPEC-013 1.5). +- mcpp, its modules and the launchers `mcpp pack` generates name mcpplibs.os + `=0.3.1`, so a pack is the same at any later time. + +### Added + +- `mcpp run --print-launch --message-format json` streams the build's events + (`plan`, `waiting`, `progress`, `artifact`) before the `mcpp.launch` envelope, + which is the last line; feature `launch-stream`. An editor shows the build of + a Run as it goes (e2e 1024). + +### Fixed + +- `mcpp test -p --no-run --archive` carries the files a workspace + member deploys from the member's product directory, where the build placed + them; 2026.11.7.1 named them where a plan of one package places them and + refused the archive: "cannot copy … into the test archive". A member's test + run through a runner names the same files (e2e 1026). + ## [2026.11.7.1] - 2026-11-07 ### Fixed diff --git a/docs/35-platforms.md b/docs/35-platforms.md index 7f75066be..3b535ae03 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -183,16 +183,29 @@ directory or a repository are not bound by the group. An mcpp before 2026.11.7.1 checks the group after resolution instead; a family released as one index change resolves the same either way. -## The built-in platform is a series - -The target table selects `openkal-platform@^0.24` for the two ok-image targets: -a series. A series takes its newest release whose `[package] mcpp` floor this -mcpp meets, naming each release it skips for a newer engine, and `mcpp.lock` -records it; the next build takes the recorded release while the series admits -it. `mcpp update` (or `mcpp update openkal-platform`) drops the record, and the -build after it takes the newest release again. A project selects a series of -its own the same way, `platform = "openkal-platform@^0.24"`, and an exact -release with a version. +## The built-in platform is one release + +The target table selects one openkal-platform release for the two ok-image +targets, the one this mcpp was qualified with: 0.24.3 for mcpp 2026.11.7.2 +(2026.11.7.1 selected the series `^0.24`). A project that states no platform +builds the same versions at any time, with or without `mcpp.lock`; when the lock +records another release, the build takes the default and says which release the +lock recorded. A newer platform needs no new mcpp: a project selects it in its +manifest, + +```toml +[target.'cfg(os = "openkal")'] +platform = "openkal-platform@0.24.4" # a release, or a series: "openkal-platform@^0.24" +``` + +and a build that fails while the default is in use and the synced index holds a +newer release ends with a `tip:` naming both and this line. + +A series takes its newest release whose `[package] mcpp` floor this mcpp meets, +naming each release it skips for a newer engine, and `mcpp.lock` records it; the +next build takes the recorded release while the series admits it. `mcpp update` +(or `mcpp update openkal-platform`) drops the record, and the build after it +takes the newest release again. ## Writing a platform package diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 9fd99ba49..8f66fbb0d 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -245,6 +245,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 | +| `launch-stream` | 2026.11.7.2 | `mcpp run --print-launch --message-format json` streams the build's events, then the `mcpp.launch` envelope (below) | an IDE's Run, showing the build as it goes | ### How a tool runs mcpp for a project @@ -527,7 +528,7 @@ installation, which is located on the machine rather than installed by mcpp. A target row is `{target, note, toolchain, pin, status, default}`, and `status` is one of `installed` / `available` / `via dependency graph` / `planned`. A target row also carries `platform` (2026.10.27.1+): the platform package the -target table selects for it (the series `mcpplibs.openkal-platform@^0.24` from 2026.11.7.1), or `null`. +target table selects for it (`mcpplibs.openkal-platform@0.24.3` from 2026.11.7.2; the series `^0.24` in 2026.11.7.1), or `null`. **`toolchain` and `pin` are not the same field twice.** `toolchain` is what the row is associated with — the installed payload on an installed row, the @@ -1002,6 +1003,19 @@ 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. +With `--message-format json` (2026.11.7.2+, feature `launch-stream`) standard +output is NDJSON: the build's events, as `mcpp build --message-format json` +streams them (`plan`, `waiting`, `progress`, `artifact`), and the `mcpp.launch` +envelope as the last line. Without `--print-launch`, `mcpp run --message-format +json` is refused (exit 2). + +With nothing changed the description takes the fast path, as `mcpp run` does +(2026.11.7.2+): the full path records the launch it composes in the +configuration's directory, and the fast path prints that record, with this +request's arguments, when the launch it composes itself is the recorded one. +Any other launch -- a runner, an execution host, the files a runner carries -- +is described by the full path. + `data.configuration` (2026.11.7.1+) contains the selected program's `triple`, `profile` and resolved `platform`, using the same fields as the build record's configuration. For a target set it describes the leg selected for this launch. diff --git a/docs/specs/README.md b/docs/specs/README.md index 1e7341480..1e7047588 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -42,7 +42,7 @@ | [SPEC-010](home-initialization.md) | home 选择、单跳入口、初始化、网络边界与单次运行的 `--home` | 草案 v0.2 | 2026-10-26 | mcpp >= 2026.10.19.1;网络能力逐条标注;§6 mcpp >= 2026.10.26.1、xlings >= 2026.10.26.1 | | [SPEC-011](concurrency.md) | 并发运行:三段、构建目录锁、原子发布、`mcpp.lock` 的乐观写入、取消 | 评审中 v1.0 | 2026-10-26 | mcpp >= 2026.10.26.1 | | [SPEC-012](human-output.md) | 面向人的输出:两条流、两种介质、行的种类、颜色 | 评审中 v1.0 | 2026-10-26 | mcpp >= 2026.10.26.1 | -| [SPEC-013](platforms.md) | 平台:`[platform]` 的七组键、版本组与 `hosted`,三种写法与层次,目标图与宿主图的选择,`cfg(platform)`,依赖图的补全,链接、命名与封装,运行器与载体,平台阶段与协议 17 | 评审中 v1.4 | 2026-11-07 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;§7.5 版本组参与解析与 §14 平台系列 mcpp >= 2026.11.7.1 | +| [SPEC-013](platforms.md) | 平台:`[platform]` 的七组键、版本组与 `hosted`,三种写法与层次,目标图与宿主图的选择,`cfg(platform)`,依赖图的补全,链接、命名与封装,运行器与载体,平台阶段与协议 17 | 评审中 v1.5 | 2026-11-07 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;§7.5 版本组参与解析与 §14 平台系列 mcpp >= 2026.11.7.1;§14 内置平台为确切版本 mcpp >= 2026.11.7.2 | | [SPEC-014](packaging.md) | 打包与分发:链接、闭包、容器三个维度与构建腿,腿的合并,暂存树与 `mcpp-package.json`,平台的树、运行覆盖、单文件、polyglot、容器列表、签名、压缩、合并与分发描述 | 评审中 v2.1 | 2026-10-31 | mcpp >= 2026.10.31.1;逐条标注 | | [SPEC-015](target-sets.md) | 目标集合与腿:集合的写法与展开,命令对多条腿的语义,串行规划与一起构建,测试归档,机器输出 | 评审中 v1.0 | 2026-10-30 | mcpp >= 2026.10.30.1 | diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md index 7dce02551..44877aca1 100644 --- a/docs/specs/platforms.md +++ b/docs/specs/platforms.md @@ -4,10 +4,10 @@ |---|---| | 规范编号 | SPEC-013 | | 标题 | 平台:一类目标的可复用配置,其声明、选择、合并、对依赖图的补全、链接与封装、运行器与载体 | -| 状态 | 评审中 v1.4 | -| 版本 | 1.4 | +| 状态 | 评审中 v1.5 | +| 版本 | 1.5 | | 最后修改 | 2026-11-07 | -| 对应实现 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;`runner.hosts`、载体列表、`descriptor_keys` 与 `universal` 的弃用 mcpp >= 2026.10.30.1;§7.5 版本组参与解析、§14 平台系列与 §15.1 系列的锁 mcpp >= 2026.11.7.1;逐条标注 | +| 对应实现 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;`runner.hosts`、载体列表、`descriptor_keys` 与 `universal` 的弃用 mcpp >= 2026.10.30.1;§7.5 版本组参与解析、§14 平台系列与 §15.1 系列的锁 mcpp >= 2026.11.7.1;§14 内置平台为确切版本与提示 mcpp >= 2026.11.7.2;逐条标注 | | 相关设计文档 | `.agents/docs/2026-10-28-platforms-design.md`、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md` | | 相关 issue | mcpp#846、mcpp#847、mcpp#848 | | 相关规范 | [SPEC-002](target-side.md)(目标侧的层)、[SPEC-004](manifest-semantics.md)(条件化形状)、[SPEC-006](toolchain-management.md)(工具链选择)、[SPEC-007](build-plugins.md)(构建程序与协议)、[SPEC-011](concurrency.md)(原子发布)、[SPEC-014](packaging.md)(打包) | @@ -391,8 +391,10 @@ run_under = ["qemu-aarch64"] ## 14. 内置平台与目标表 宿主目标(Linux、macOS、Windows、Android、iOS、Emscripten)的约定留在引擎中,无平台包。目标表有平台列: -`x86_64-openkal`、`aarch64-openkal` 的值为平台系列 `mcpplibs.openkal-platform@^0.24`,即这两个目标的内置支持; -选择来源报告为 `built-in`。项目可以以 `[target.] platform` 选择另一个版本或系列。**已实现** +`x86_64-openkal`、`aarch64-openkal` 的值为一个确切的平台版本 `mcpplibs.openkal-platform@0.24.3`,即当前引擎验证过的版本, +也是这两个目标的内置支持;选择来源报告为 `built-in`。项目可以以 `[target.] platform` 选择另一个版本或系列。 +锁中记录的版本不满足内置版本时不被采用,报告写出采用的版本与锁中记录的版本。使用内置版本的构建失败、且已同步的索引中 +有更新版本时,构建以一条 `tip` 写出两个版本与选择它的写法(mcpp >= 2026.11.7.2;2026.11.7.1 的值为系列 `^0.24`)。**已实现** **平台系列(mcpp >= 2026.11.7.1)。** 以范围选择的平台是一个系列: @@ -517,6 +519,8 @@ openkal-platform 的 `targets` 因此不含 WebAssembly。Web 经 `wasm32-emscri 13. 映像的单文件的载体是映像架构的程序;`--mode self-contained` 的单目标包是树,携带该架构的运行器(e2e 961)。 14. 以系列选择的平台取引擎可读的最新版本,跳过下限更高的版本并写出;锁记录的版本在系列内时被采用,`mcpp update` 之后 取更新的版本(e2e 1021)。 +15. 内置平台为确切版本:不写平台的工程构建该版本并锁定;锁中记录的其他版本不被采用并写出;工程写出的平台照常采用 + (e2e 1025;提示的条件与内容见单元测试 PlatformDefault)。 ## 变更记录 @@ -526,4 +530,5 @@ openkal-platform 的 `targets` 因此不含 WebAssembly。Web 经 `wasm32-emscri | 1.1 | 2026-10-28 | `[platform] universal`;载体的三种携带形式(运行器形式、映像、映像组的 zip)、`mcpp_carried_digest`、Mach-O 节名 `__carried`、按系统的行;平台阶段可声明包格式与 artifact action;`self env --host` 报告运行器与工具;判据 10、11(e2e 952、953)。 | | 1.2 | 2026-10-29 | mcpp 2026.10.29.1:§10 给出执行宿主时 `same-arch` 运行器的程序为产物架构的载体运行器形式,`mcpp test` 写出完整的拒绝原因一次;§11 单目标包与单文件的程序为产物架构的,不构建的行在生成工程之前被拒绝;§14 ok 映像与 Web;§16 `self env` 报告运行器程序的架构、执行宿主与拒绝,平台包的 `mcpp test` 运行给出执行宿主的目标;判据 7、12、13(e2e 960、961)。 | | 1.3 | 2026-10-30 | mcpp 2026.10.30.1(设计 2026-10-29 §4.3–§4.5、§13.3):§1.1 `universal` 弃用、不起作用;`runner.hosts`;`carrier` 接受列表,每项的 `hosts` 与 `build`;`descriptor_keys`;§10 执行宿主的运行器程序按 `hosts` 选择载体项;§11 携带映像的载体只属于单文件,树携带运行器形式;判据 13 改写(e2e 961)。 | +| 1.5 | 2026-11-07 | mcpp 2026.11.7.2(设计 2026-11-07 §4.10,D20):§14 目标表的平台列为确切版本 0.24.3,锁中不满足的记录不被采用并写出;使用内置版本的构建失败且索引有更新版本时给出提示;判据 15 新增(e2e 1025)。 | | 1.4 | 2026-11-07 | mcpp 2026.11.7.1(设计 2026-11-07 §4.1.4、§4.2.2 A2/A3):§7.5 版本组参与解析,解析后的检查保留为断言;§3.2 平台引用接受 SemVer 约束;§14 目标表的平台列为系列 `^0.24`,系列取引擎可读的最新版本;§15.1 系列从锁中读回,`mcpp update` 移动它;判据 5 改写、14 新增(e2e 1020、1021)。 | diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index 29146d3ad..e9fcce3ee 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -155,12 +155,22 @@ error: openkal-musl is requested as '^0.27.0' by app, and the platform openkal-p 只为本机构建的包(工具的)以及从目录或仓库取得的包不受版本组约束。早于 2026.11.7.1 的 mcpp 改为在解析之后检查 版本组;一个家族以一次索引改动整组发布时,两者结果相同。 -## 内置平台是一个系列 +## 内置平台是一个版本 -目标表为两个 ok 映像目标选择 `openkal-platform@^0.24`,即一个系列。系列取其中 `[package] mcpp` 下限当前 mcpp 满足的 -最新版本,并写出因需要更新的引擎而跳过的版本;`mcpp.lock` 记录结果,之后的构建在系列容许时采用记录的版本。 -`mcpp update`(或 `mcpp update openkal-platform`)删除这条记录,下一次构建重新取最新版本。工程以同样的写法选择自己的 -系列(`platform = "openkal-platform@^0.24"`),写版本则选择一个确切的版本。 +目标表为两个 ok 映像目标选择一个 openkal-platform 版本,即当前 mcpp 验证过的版本:mcpp 2026.11.7.2 为 0.24.3 +(2026.11.7.1 选择系列 `^0.24`)。不写平台的工程在任何时候、有无 `mcpp.lock` 都构建同一组版本;锁中记录了另一个版本时, +构建采用默认版本并写出锁中记录的版本。更新的平台不需要新的 mcpp:工程在自己的清单中选择, + +```toml +[target.'cfg(os = "openkal")'] +platform = "openkal-platform@0.24.4" # 一个版本,或一个系列:"openkal-platform@^0.24" +``` + +使用默认平台的构建失败、且已同步的索引中有更新版本时,最后输出一行 `tip:`,写出两个版本与上面这一行。 + +系列取其中 `[package] mcpp` 下限当前 mcpp 满足的最新版本,并写出因需要更新的引擎而跳过的版本;`mcpp.lock` 记录结果, +之后的构建在系列容许时采用记录的版本。`mcpp update`(或 `mcpp update openkal-platform`)删除这条记录,下一次构建重新 +取最新版本。 ## 编写平台包 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index c9e5fd224..1bab6f3e3 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -208,6 +208,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 | +| `launch-stream` | 2026.11.7.2 | `mcpp run --print-launch --message-format json` 先流式输出构建事件,再输出 `mcpp.launch` 信封(见下) | IDE 的运行,边构建边显示 | ### 工具为项目运行 mcpp 的方式 @@ -463,7 +464,7 @@ mcpp toolchain list --format json 的形状是 `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned` 之一。 目标行另有 `platform`(2026.10.27.1+):目标表为该目标选择的平台包 -(2026.11.7.1 起为系列 `mcpplibs.openkal-platform@^0.24`),或 `null`。 +(2026.11.7.2 起为 `mcpplibs.openkal-platform@0.24.3`;2026.11.7.1 为系列 `^0.24`),或 `null`。 **`toolchain` 与 `pin` 不是把同一个字段写了两遍。** `toolchain` 表示这一行 关联到什么 —— 已装的行是装好的载荷,词表行是它自己的约定。`pin` 只承载目标 @@ -890,6 +891,15 @@ mcpp run [target] --print-launch `argv`、`cwd`、`env` 与 `via`,同上文的 `launch`。`--print-launch` 不接受 `--format`:在 `mcpp run` 上,`--pkg-format` 表示可分发物的格式。 +带 `--message-format json`(2026.11.7.2+,特性 `launch-stream`)时,标准输出为 NDJSON:先是与 +`mcpp build --message-format json` 相同的构建事件(`plan`、`waiting`、`progress`、`artifact`), +最后一行是 `mcpp.launch` 信封。不带 `--print-launch` 的 `mcpp run --message-format json` 被拒绝 +(退出码 2)。 + +没有改动时,启动描述与 `mcpp run` 一样走快速路径(2026.11.7.2+):完整路径把它组合出的启动 +记录在该配置的目录中;快速路径自己组合出的启动与记录逐项相同时,输出这份记录,并附上本次的 +程序参数。其他启动(runner、执行宿主、runner 携带的文件)由完整路径描述。 + `data.configuration`(2026.11.7.1+)包含所选程序的 `triple`、`profile` 与解析后的 `platform`,字段与构建记录的 configuration 相同。目标集合中,它描述本次启动所选的 目标。`configuration.runners` 列出可用的具名运行器,包括 platform 为 null 时 diff --git a/mcpp.toml b/mcpp.toml index 89aa8b1ad..314bb184e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.7.1" +version = "2026.11.7.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] @@ -102,7 +102,7 @@ mcpplibs.cmdline = "0.0.1" # The operating system is mcpplibs.os's (design 2026-10-26 §25): mcpp keeps no # layer of its own. [dependencies.mcpplibs] -os = "^0.3.1" +os = "=0.3.1" [target.linux.build] diff --git a/modules/invocation/mcpp.toml b/modules/invocation/mcpp.toml index eb67a13b2..e36e88afc 100644 --- a/modules/invocation/mcpp.toml +++ b/modules/invocation/mcpp.toml @@ -13,4 +13,4 @@ description = "The environment of one invocation of mcpp: statements over the st license = "Apache-2.0" [dependencies.mcpplibs] -os = "^0.3.1" +os = "=0.3.1" diff --git a/modules/manifest/mcpp.toml b/modules/manifest/mcpp.toml index e65d883fc..6a04878cd 100644 --- a/modules/manifest/mcpp.toml +++ b/modules/manifest/mcpp.toml @@ -28,7 +28,7 @@ versioning = { path = "../versioning" } # state its contract in isolation; the root `tests/unit/` states the facts two # layers must agree on. [dependencies.mcpplibs] -os = "^0.3.1" +os = "=0.3.1" [dev-dependencies.compat] gtest = "1.15.2" diff --git a/modules/toolchain-model/mcpp.toml b/modules/toolchain-model/mcpp.toml index e487563cc..480996c7f 100644 --- a/modules/toolchain-model/mcpp.toml +++ b/modules/toolchain-model/mcpp.toml @@ -27,7 +27,7 @@ versioning = { path = "../versioning" } # state its contract in isolation; the root `tests/unit/` states the facts two # layers must agree on. [dependencies.mcpplibs] -os = "^0.3.1" +os = "=0.3.1" [dev-dependencies.compat] gtest = "1.15.2" diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 1845a83be..66ca3979d 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -906,9 +906,9 @@ inline constexpr TargetInfo kKnownTargets[] = { // `openkal-platform` (SPEC-013), pinned here. The two architectures the // loader relocates. { "x86_64-openkal", "preview", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@^0.24" }, + "mcpplibs.openkal-platform@0.24.3" }, { "aarch64-openkal", "preview", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@^0.24" }, + "mcpplibs.openkal-platform@0.24.3" }, }; inline std::span known_targets() { return kKnownTargets; } diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 4288a2395..e31e3b9a2 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.11.7.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.7.2"; } // namespace mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index bfb7bdb19..cf7f96e80 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -61,6 +61,7 @@ import mcpp.build.schedule.detach_codegen; import mcpp.fetcher.progress; import mcpp.project; import mcpp.ui; +import mcpp.ui.terminal; // a streamed launch envelope is the protocol stream's last line import mcpp.toolchain.msvc_coroutines; import mcpp.build.progress; @@ -2940,11 +2941,69 @@ bool& print_launch_flag() { } export void set_print_launch(bool on) { print_launch_flag() = on; } +// `--print-launch --message-format json` (design 2026-11-07 §4.9): the build's +// events stream before the envelope, so the envelope is the stream's last line. +bool& launch_stream_flag() { + static bool on = false; + return on; +} +export void set_launch_stream(bool on) { launch_stream_flag() = on; } + +void emit_launch(const mcpp::wire::Envelope& env) { + if (launch_stream_flag()) mcpp::ui::terminal::write_protocol_stdout(mcpp::wire::to_json(env).dump() + "\n"); + else mcpp::wire::emit(env); +} + +// THE LAUNCH THE FULL PATH COMPOSED, RECORDED FOR THE FAST PATH (design +// 2026-11-07 §4.9). A launch description is the run's own composition, and only +// the full path composes all of it: the runner, the execution host, the files +// a runner carries, the named runners of `configuration`. It is recorded in the +// configuration's directory, without this request's program arguments. The fast +// path describes the launch from the record only when the launch it composes +// itself -- the one `mcpp run` executes there -- is the recorded one, field for +// field; otherwise the full path describes it and records it again. The +// description is therefore the full path's by construction, and no list of the +// cases the fast path cannot compose is kept. +std::filesystem::path launch_record_path(const std::filesystem::path& outputDir, + const std::filesystem::path& exe) { + return outputDir / "launch" / (exe.filename().string() + ".json"); +} + +void record_launch(const std::filesystem::path& outputDir, const std::filesystem::path& exe, + nlohmann::json data, std::size_t arguments) { + auto& argv = data["argv"]; + for (std::size_t i = 0; i < arguments && !argv.empty(); ++i) argv.erase(argv.size() - 1); + data.erase("cwd"); + std::error_code ec; + const auto path = launch_record_path(outputDir, exe); + std::filesystem::create_directories(path.parent_path(), ec); + const auto staged = std::filesystem::path(path.string() + ".tmp"); + { + std::ofstream out(staged, std::ios::binary | std::ios::trunc); + out << data.dump(); + if (!out) return; // a description without a record: the next one takes the full path + } + std::filesystem::rename(staged, path, ec); +} + +std::expected +recorded_launch(const std::filesystem::path& outputDir, const std::filesystem::path& exe, + const nlohmann::json& composed) { + std::ifstream in(launch_record_path(outputDir, exe), std::ios::binary); + if (!in) return std::unexpected("no launch the full path composed is recorded"); + auto record = nlohmann::json::parse(in, nullptr, false); + if (record.is_discarded() || !record.is_object()) + return std::unexpected("the recorded launch cannot be read"); + for (const char* field : {"argv", "env", "via", "program"}) + if (record.value(field, nlohmann::json()) != composed.value(field, nlohmann::json())) + return std::unexpected(std::format("the recorded launch has another {}: {} where this path composes {}", + field, record.value(field, nlohmann::json()).dump(), composed.value(field, nlohmann::json()).dump())); + return record; +} + std::optional try_fast_run(const std::filesystem::path& projectRoot, const std::optional& targetName, std::span passthrough) { - if (print_launch_flag()) - return fast_path_declined("run", "--print-launch describes the launch the full path composes"); // Same reason as try_fast_build's: this path skips resolution, and // `--locked` is an assertion about resolution. if (mcpp::invocation::get("MCPP_LOCKED").value_or("") == "1") @@ -3059,16 +3118,11 @@ std::optional try_fast_run(const std::filesystem::path& projectRoot, fast_path_end(record, true); buildLock.reset(); - auto exe = outputDir / chosen->second; + // The cache records the program's path with `/`; the full path spells it + // natively (`bin\app.exe` on Windows), and so does this one, so that the + // two compose the same argv. + auto exe = outputDir / std::filesystem::path(chosen->second).make_preferred(); auto pathCtx = mcpp::fetcher::make_path_ctx(/*cfg=*/nullptr, projectRoot); - yield_terminal(); - mcpp::ui::status("Running", - std::format("`{}`", mcpp::ui::shorten_path(exe, pathCtx))); - run_separator(); - std::vector argv; - argv.push_back(exe.string()); - for (auto& a : passthrough) argv.push_back(a); - std::vector> childEnv; if (!match->runEnvKey.empty() && !match->runEnvValue.empty()) childEnv.emplace_back(match->runEnvKey, match->runEnvValue); @@ -3083,6 +3137,35 @@ std::optional try_fast_run(const std::filesystem::path& projectRoot, })) childEnv.push_back(std::move(kv)); + // `--print-launch`: the recorded launch, when it is the one this path executes. + if (print_launch_flag()) { + nlohmann::json composed = mcpp::build::result::to_json( + mcpp::build::result::Launch{{exe.string()}, childEnv, {}, ""}); + composed["program"] = exe.generic_string(); + auto described = recorded_launch(outputDir, exe, composed); + if (!described) return fast_path_declined("run", described.error()); + yield_terminal(); + mcpp::ui::status("Running", + std::format("`{}`", mcpp::ui::shorten_path(exe, pathCtx))); + run_separator(); + mcpp::wire::Envelope env{.kind = "mcpp.launch", + .effects = {mcpp::wire::Effect::ReadProject, + mcpp::wire::Effect::WriteProject}}; + env.data = std::move(*described); + for (auto& a : passthrough) env.data["argv"].push_back(a); + env.data["cwd"] = std::filesystem::current_path().generic_string(); + emit_launch(env); + return 0; + } + + yield_terminal(); + mcpp::ui::status("Running", + std::format("`{}`", mcpp::ui::shorten_path(exe, pathCtx))); + run_separator(); + std::vector argv; + argv.push_back(exe.string()); + for (auto& a : passthrough) argv.push_back(a); + // Same contract as the prepare path below: a refused spawn is reported and // exits in the 125-127 band, never folded into the artifact's own status. // No runner can be declared for an entry this path accepts (see the @@ -3334,7 +3417,8 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, env.data = mcpp::build::result::to_json(launch); env.data["program"] = exe.generic_string(); env.data["configuration"] = launch_configuration(ctx); - mcpp::wire::emit(env); + record_launch(ctx.outputDir, exe, env.data, passthrough.size()); + emit_launch(env); return 0; } int spawnErr = 0; @@ -4614,7 +4698,11 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // in the single-threaded pass, one list per test program. if (!runnerTmpl.empty() || !tb.runUnderPrefix.empty()) { std::vector> carried; - with_member(*ctx, owner, [&] { carried = runtime_files_for(*ctx, exe, owner); }); + // The member's files: a plan selected to one member (`-p`) still + // holds the member's link group, and its tests sit in the member's + // own product directory with that group's deploy set. + const std::string_view of = owner.empty() ? std::string_view(lu.memberOf) : owner; + with_member(*ctx, of, [&] { carried = runtime_files_for(*ctx, exe, of); }); if (auto listed = write_runtime_files_list(*ctx, exe, carried)) childEnv.emplace_back(std::string(kRuntimeFilesEnv), listed->string()); else if (invocationNotRunReason.empty()) @@ -4673,7 +4761,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, env.data["program"] = chosen->program.generic_string(); env.data["test"] = chosen->name; env.data["configuration"] = launch_configuration(*ctx); - mcpp::wire::emit(env); + emit_launch(env); return 0; } // Pass 2: run them. Concurrently unless there is exactly one — see @@ -4789,7 +4877,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // One leg's built tests, for the archive. struct ArchiveLeg { std::string triple; - const BuildContext* ctx = nullptr; + BuildContext* ctx = nullptr; const std::vector>* programEnv = nullptr; }; @@ -4821,7 +4909,10 @@ int write_test_archive(const TestOptions& o, std::span legs) { if (!fs::is_regular_file(exe, ec)) continue; ta::Test t{lu.targetName, ta::test_dest(l.triple, lu.targetName, exe.filename().string()), {}}; copies.push_back({exe, t.program}); - for (auto const& [dest, source] : runtime_files_for(*l.ctx, exe)) { + // The files of the test's member, placed in its product directory. + std::vector> carried; + with_member(*l.ctx, lu.memberOf, [&] { carried = runtime_files_for(*l.ctx, exe, lu.memberOf); }); + for (auto const& [dest, source] : carried) { // A file the program reads above its own directory goes beside // it under its own name: the archive keeps one directory per test. const bool above = dest.starts_with("../"); diff --git a/src/build/prepare/platform.cpp b/src/build/prepare/platform.cpp index 2c891f7c4..1d92e7fd0 100644 --- a/src/build/prepare/platform.cpp +++ b/src/build/prepare/platform.cpp @@ -243,20 +243,31 @@ void step4b_seed_platform(PrepareState& state) { item.requestedBy = state.m->package.name + " (platform)"; item.originalConstraint = item.spec.version; // A SERIES TAKES THE RELEASE THE LOCK RECORDS (SPEC-013 §14): a platform - // selected as `^0.24` (the target table's default) resolves to the newest - // release of the series once, and mcpp.lock keeps it there until - // `mcpp update` drops the entry; a recorded release outside the series is - // not taken. + // selected as `^0.24` resolves to the newest release of the series once, + // and mcpp.lock keeps it there until `mcpp update` drops the entry; a + // recorded release outside the request is not taken, and the build says + // which it takes (the target table's exact default after an mcpp upgrade, + // design 2026-11-07 §4.10). if (!item.spec.isPath() && !item.spec.isGit()) { const auto qualified = item.spec.namespace_ + "." + item.spec.shortName; if (auto anchor = state.platformLockAnchors.find(qualified); anchor != state.platformLockAnchors.end()) if (auto req = mcpp::version_req::parse_req(item.spec.version)) - if (auto v = mcpp::version_req::parse_version(anchor->second); - v && mcpp::version_req::matches(*req, *v) && item.spec.version != anchor->second) { - mcpp::ui::info("Platform", std::format("{} {} → v{} (mcpp.lock)", - item.spec.shortName, item.spec.version, anchor->second)); - item.spec.version = "=" + anchor->second; + if (auto v = mcpp::version_req::parse_version(anchor->second); v) { + if (mcpp::version_req::matches(*req, *v)) { + if (item.spec.version != anchor->second) { + mcpp::ui::info("Platform", std::format("{} {} → v{} (mcpp.lock)", + item.spec.shortName, item.spec.version, anchor->second)); + item.spec.version = "=" + anchor->second; + } + } else { + mcpp::ui::info("Platform", std::format("{} {} ({}; mcpp.lock recorded v{})", + item.spec.shortName, item.spec.version, + state.platform.selection.origin.tier == mcpp::manifest::platform::Tier::BuiltIn + ? std::format("mcpp {}'s default", mcpp::MCPP_VERSION) + : mcpp::platform::origin_text(state.platform.selection.origin), + anchor->second)); + } } } item.consumerDepIndex = kMainConsumer; @@ -494,8 +505,27 @@ void step4b_read_platform_group(PrepareState& state, const std::string& resolved return; state.platformGroupRead = true; state.platformGroup = state.dep_manifests.back()->platform.versions; - state.platformGroupFrom = std::format("{} {}", state.platformPackageShortName, - state.dep_manifests.back()->package.version); + const auto used = state.dep_manifests.back()->package.version; + state.platformGroupFrom = std::format("{} {}", state.platformPackageShortName, used); + // THE DEFAULT IS A RELEASE, AND A NEWER ONE IS THE PROJECT'S CHOICE (design + // 2026-11-07 §4.10). The target table names one platform release, the one + // this mcpp was qualified with; a project selects another in its manifest. + // When the default is in use and the synced index holds a newer release, a + // failed build says so once, with the line to write. It guesses no cause. + auto const& sel = state.platform.selection; + if (sel.origin.tier != mcpp::manifest::platform::Tier::BuiltIn || sel.ref.spec.isPath() + || sel.ref.spec.isGit() || used.empty()) + return; + auto cfg = state.get_cfg(true); + if (!cfg) return; + auto newer = mcpp::pm::resolve_semver(sel.ref.spec.namespace_.empty() ? std::string("mcpplibs") + : sel.ref.spec.namespace_, + state.platformPackageShortName, ">" + used, + state.index_route(*cfg), *state.targetPlatform); + if (!newer) return; + if (auto tip = mcpp::platform::newer_release_tip(sel.origin, state.platformPackageShortName, used, + *newer, mcpp::MCPP_VERSION)) + mcpp::ui::add_failure_notice("platform-newer", *tip); } // A SERIES TAKES ITS NEWEST RELEASE THIS ENGINE READS (SPEC-013 §14). A diff --git a/src/cli.cppm b/src/cli.cppm index e83bca65d..f43223c01 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -845,6 +845,8 @@ int run(int argc, char** argv) { .help("Play a game in the status row while it builds: --play-game=snake|stack|runner, or one at random")) .option(cl::Option("print-launch") .help("Build, then print how the program would be started (argv, the environment mcpp adds, cwd, runner) as the mcpp.launch envelope, instead of starting it")) + .option(cl::Option("message-format").takes_value().value_name("FMT") + .help("With --print-launch: human (default) | json (the build's events as NDJSON, then the envelope as the last line)")) .action(wrap_rc([&passthrough](const cl::ParsedArgs& p) { return cmd_run(p, std::span(passthrough)); }))) @@ -1949,7 +1951,7 @@ int run(int argc, char** argv) { app.run(*parsed); // The run's closing notices come last, after everything the command // printed, whatever its exit status (see mcpp::ui::add_closing_notice). - mcpp::ui::print_closing_notices(); + mcpp::ui::print_closing_notices(action_rc != 0); // A run that stopped before an effect its caller denied exits 5 // (SPEC-003, design 2026-10-26 D23): the caller's constraint, not a // failure of the project, and a script tells the two apart by the status. diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index 96ebdb7a4..a4e0a6a32 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -215,6 +215,26 @@ target_legs(const mcpplibs::cmdline::ParsedArgs& parsed, const mcpp::build::Buil int cmd_build_body(const mcpplibs::cmdline::ParsedArgs& parsed); +namespace { +// The build's events on standard output (design 2026-10-26 §5.1): `waiting` while +// another build holds the directory, `progress` with ninja's count, and what the +// record adds. `mcpp build --message-format json` and `mcpp run --print-launch +// --message-format json` stream them before their envelope. +void stream_build_events() { + namespace res = mcpp::build::result; + res::enable_events(true); + mcpp::build::dirlock::set_waiting_observer( + [](const mcpp::build::dirlock::Holder& h, std::chrono::milliseconds waited) { + res::emit("waiting", {{"holder", {{"pid", h.pid}, {"command", h.command}, + {"client", h.client}, {"startedAt", h.startedAt}}}, + {"waitedMs", waited.count()}}); + }); + mcpp::build::progress::set_step_observer([](std::size_t finished, std::size_t total) { + res::emit("progress", {{"finished", finished}, {"total", total}}); + }); +} +} // namespace + // `mcpp build --message-format json` (design 2026-10-26 §5.1). // // NDJSON ON STANDARD OUTPUT WHILE IT BUILDS, THEN ONE ENVELOPE. Events are @@ -234,16 +254,7 @@ export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { if (messageFormat == "human") return cmd_build_body(parsed); namespace res = mcpp::build::result; - res::enable_events(true); - mcpp::build::dirlock::set_waiting_observer( - [](const mcpp::build::dirlock::Holder& h, std::chrono::milliseconds waited) { - res::emit("waiting", {{"holder", {{"pid", h.pid}, {"command", h.command}, - {"client", h.client}, {"startedAt", h.startedAt}}}, - {"waitedMs", waited.count()}}); - }); - mcpp::build::progress::set_step_observer([](std::size_t finished, std::size_t total) { - res::emit("progress", {{"finished", finished}, {"total", total}}); - }); + stream_build_events(); // mcpp's own errors (a manifest's rejection, a refused requirement) are // the envelope's diagnostics, each located where its message says. std::vector errors; @@ -1145,6 +1156,19 @@ export int cmd_run(const mcpplibs::cmdline::ParsedArgs& parsed, return mcpp::build::list_runners(package_filter, cache_mode, no_cache, target_triple, features, profile, accel); mcpp::build::set_print_launch(parsed.is_flag_set("print-launch")); + // A launch asked for as JSON streams the build's events, and its envelope is + // the last line (design 2026-11-07 §4.9): an editor shows the build as it goes. + if (auto mf = parsed.value("message-format"); mf && *mf != "human" && *mf != "json") { + mcpp::ui::error(std::format("unknown --message-format '{}' (human|json)", *mf)); + return 2; + } else if (mf && *mf == "json" && !parsed.is_flag_set("print-launch")) { + mcpp::ui::error("mcpp run --message-format json describes a launch: add --print-launch"); + return 2; + } + if (parsed.is_flag_set("print-launch") && parsed.value("message-format").value_or("human") == "json") { + mcpp::build::set_launch_stream(true); + stream_build_events(); + } // Closed before the program starts: the program owns the terminal. mcpp::build::progress::open(mcpp::log::is_verbose()); return mcpp::build::build_run_target(targetName, passthrough, package_filter, diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index b02497dd5..f58189785 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -1074,9 +1074,9 @@ std::string exe_launcher_manifest(std::string_view name, std::string_view versio "# Generated by `mcpp pack`: the Windows launcher of {0} (design 2026-10-29 §4.3).\n" "[package]\nname = \"{0}\"\nversion = \"{1}\"\n\n" "[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" - "# The OS library as a compatible range: the build's graph, and its\n" - "# platform's version group, choose the release (design 2026-11-07, A3).\n" - "[dependencies.mcpplibs]\nos = \"^0.3.1\"\n", + "# The OS library as one release, the one this mcpp was qualified with, so a\n" + "# pack is the same at any later time (design 2026-11-07, D20).\n" + "[dependencies.mcpplibs]\nos = \"=0.3.1\"\n", name, version.empty() ? std::string_view("0.0.0") : version); } @@ -1422,7 +1422,7 @@ std::string launcher_manifest(std::string_view name, std::string_view version) { "[package]\nname = \"{0}\"\nversion = \"{1}\"\n\n" "[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" "[build]\nsources = [\"src/main.cpp\", \"src/carried.S\"]\n\n" - "[dependencies.mcpplibs]\nos = \"^0.3.1\"\n\n" + "[dependencies.mcpplibs]\nos = \"=0.3.1\"\n\n" "[dependencies]\n\"mcpplibs.mwrap.zstd\" = \"1.5.7\"\n", name, version.empty() ? std::string_view("0.0.0") : version); } diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm index d327199be..ff8319c01 100644 --- a/src/platform/platform.cppm +++ b/src/platform/platform.cppm @@ -160,6 +160,16 @@ struct Resolved { // `config.toml`, `mcpp.toml`, `env`, `flag`, `graph`, `build-program`. std::string origin_text(const mcpp::manifest::platform::Origin& o); +// THE DEFAULT IS A RELEASE, AND A NEWER ONE IS THE PROJECT'S CHOICE (design +// 2026-11-07 §4.10). The tip a failed build prints when the platform is the +// target table's default (`origin` is built-in) and the index holds a newer +// release: the release used, this mcpp's, the newer one and the line to write. +// None for a platform the project, the configuration or the command line chose, +// or when nothing newer is known. +std::optional newer_release_tip(const mcpp::manifest::platform::Origin& origin, + std::string_view name, std::string_view used, + std::string_view newer, std::string_view mcppVersion); + // The lines `Platform …` and its fields, as the target report prints them. std::vector> report_rows(const Resolved& r); @@ -502,6 +512,17 @@ std::string Resolved::display() const { return std::format("{} {}", dot == std::string::npos ? package : package.substr(dot + 1), version); } +std::optional newer_release_tip(const mp::Origin& origin, std::string_view name, + std::string_view used, std::string_view newer, + std::string_view mcppVersion) { + if (origin.tier != mp::Tier::BuiltIn || used.empty() || newer.empty() || newer == used) + return std::nullopt; + return std::format("this build used {} {}, mcpp {}'s default; the index has {} {}. A project " + "selects a release in its mcpp.toml: [target.'cfg(os = \"openkal\")'] " + "platform = \"{}@{}\"", + name, used, mcppVersion, name, newer, name, newer); +} + std::string origin_text(const mp::Origin& o) { switch (o.tier) { case mp::Tier::Env: diff --git a/src/ui.cppm b/src/ui.cppm index a53b2f6bd..2c7dd3afc 100644 --- a/src/ui.cppm +++ b/src/ui.cppm @@ -109,8 +109,12 @@ struct ClosingNotice { std::string message; // one line; no trailing newline }; void add_closing_notice(std::string code, std::string message); +// A notice that belongs to a failed run only: advice that would be noise after +// a success (design 2026-11-07 §4.10, a newer platform release in the index). +void add_failure_notice(std::string code, std::string message); std::vector take_closing_notices(); -void print_closing_notices(); +// `failed`: the run is failing, and its failure notices are printed as well. +void print_closing_notices(bool failed = false); // Multi-line Rust-style diagnostic (M4 #8.1). // Renders as: @@ -864,8 +868,19 @@ std::vector& closing_notices() { static std::vector notices; return notices; } +std::vector& failure_notices() { + static std::vector notices; + return notices; +} } // namespace +void add_failure_notice(std::string code, std::string message) { + auto& all = failure_notices(); + for (auto const& n : all) + if (n.message == message) return; + all.push_back({std::move(code), std::move(message)}); +} + void add_closing_notice(std::string code, std::string message) { auto& all = closing_notices(); for (auto const& n : all) @@ -877,8 +892,10 @@ std::vector take_closing_notices() { return std::exchange(closing_notices(), {}); } -void print_closing_notices() { +void print_closing_notices(bool failed) { auto notices = take_closing_notices(); + auto onFailure = std::exchange(failure_notices(), {}); + if (failed) notices.insert(notices.end(), onFailure.begin(), onFailure.end()); if (g_quiet) return; init(); for (auto const& n : notices) { diff --git a/src/wire.cppm b/src/wire.cppm index f85b59f3a..98886381f 100644 --- a/src/wire.cppm +++ b/src/wire.cppm @@ -260,10 +260,11 @@ 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) + "launch-stream", // docs/50 §6: `mcpp run --print-launch --message-format json` (an IDE's Run) }}; // The same list as JSON: `--protocol-version` and every envelope's `mcpp` field diff --git a/tests/e2e/1021_a_platform_series_is_locked_and_skips_newer_engines.sh b/tests/e2e/1021_a_platform_series_is_locked_and_skips_newer_engines.sh index 4cdfc0328..51f91c239 100755 --- a/tests/e2e/1021_a_platform_series_is_locked_and_skips_newer_engines.sh +++ b/tests/e2e/1021_a_platform_series_is_locked_and_skips_newer_engines.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # requires: unix-shell gcc fresh-sandbox -# A PLATFORM SELECTED AS A SERIES (SPEC-013 §4.6, 2026.11.7.1). The target -# table's default platform is a series (`openkal-platform@^0.24`), and a -# project may select one as well (`"@^0"`): +# A PLATFORM SELECTED AS A SERIES (SPEC-013 §4.6, 2026.11.7.1). A project +# selects one (`"@^0"`); the target table's default is one release +# from 2026.11.7.2 (design 2026-11-07 §4.10): # # A. The series takes its newest release whose `[package] mcpp` floor this # mcpp meets: a release made for a newer mcpp is skipped and named. diff --git a/tests/e2e/1024_a_launch_description_takes_the_fast_path.sh b/tests/e2e/1024_a_launch_description_takes_the_fast_path.sh new file mode 100755 index 000000000..ef85c2b95 --- /dev/null +++ b/tests/e2e/1024_a_launch_description_takes_the_fast_path.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# requires: python3 +# 1024 -- `mcpp run --print-launch` takes the fast path (design 2026-11-07 +# §4.9, D19). The full path records the launch it composes in the +# configuration's directory; with nothing changed, the fast path describes the +# launch from that record when the launch it composes itself is the recorded one. +# +# Criteria: +# A. Unchanged: the second description is the first, with this request's +# program arguments, and is made without resolving (no `Resolving` line). +# B. A changed source is compiled and the launch is described the same way. +# C. A record that is not the fast path's own composition is not used: the full +# path describes the launch and records it again. +# D. `--message-format json`: every line of standard output is a JSON document, +# a rebuild reports `progress`, and the last line is the `mcpp.launch` +# envelope. Without `--print-launch` it is refused, exit 2. +# E. `--protocol-version` states `launch-stream`. +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 +"$MCPP" build > build.log 2>&1 || fail "the project does not build" build.log + +# A. Where `mcpp run` itself takes the fast path here (some rows, a stated +# target base for one, always plan), so does the description. +"$MCPP" run --print-launch > full.json 2> full.err || fail "A: the first description failed" full.err +"$MCPP" run > plain.out 2> plain.err || fail "A: mcpp run failed" plain.err +fast_here=1 +grep -q 'Resolving' plain.err && fast_here=0 +"$MCPP" run --print-launch -- one two > fast.json 2> fast.err || fail "A: the second description failed" fast.err +if [[ $fast_here -eq 1 ]] && grep -q 'Resolving' fast.err; then + "$MCPP" run --print-launch --verbose > /dev/null 2> why.err || true + grep 'fast-path' why.err > fast.why || true + fail "A: the unchanged description resolved again (the full path) where mcpp run does not" fast.why fast.err plain.err +fi +$PY - full.json fast.json <<'PY' || fail "A: the fast description is not the full path's" full.json fast.json +import json, sys +full, fast = (json.load(open(p)) for p in sys.argv[1:3]) +assert fast["kind"] == "mcpp.launch", fast +f, g = full["data"], fast["data"] +assert g["argv"][-2:] == ["one", "two"], g["argv"] +g = dict(g, argv=g["argv"][:-2]) +assert f == g, (f, g) +PY + +# B +src=$(ls src/*.cpp | head -1) +printf '\n// changed\n' >> "$src" +"$MCPP" run --print-launch > changed.json 2> changed.err || fail "B: the changed description failed" changed.err +grep -q 'Compiling' changed.err || fail "B: the changed source was not compiled" changed.err +$PY - full.json changed.json <<'PY' || fail "B: the description after a change differs" full.json changed.json +import json, sys +full, changed = (json.load(open(p))["data"] for p in sys.argv[1:3]) +assert full == changed, (full, changed) +PY + +# C +record=$(find target -path '*/launch/*.json' | head -1) +[[ -n "$record" ]] || fail "C: no launch is recorded in the configuration's directory" +$PY - "$record" <<'PY' +import json, sys +r = json.load(open(sys.argv[1])); r["env"]["MCPP_E2E_FOREIGN"] = "1" +json.dump(r, open(sys.argv[1], "w")) +PY +"$MCPP" run --print-launch > again.json 2> again.err || fail "C: the description failed" again.err +[[ $fast_here -eq 0 ]] || grep -q 'Resolving' again.err || fail "C: a foreign record was used by the fast path" again.err +grep -q MCPP_E2E_FOREIGN again.json && fail "C: the foreign field reached the description" again.json +grep -q MCPP_E2E_FOREIGN "$record" && fail "C: the full path did not record the launch again" "$record" + +# D +printf '\n// changed again\n' >> "$src" +"$MCPP" run --print-launch --message-format json > stream.ndjson 2> stream.err || fail "D: the streamed description failed" stream.err +$PY - stream.ndjson <<'PY' || fail "D: the stream is not NDJSON ending in the envelope" stream.ndjson +import json, sys +lines = [json.loads(l) for l in open(sys.argv[1]) if l.strip()] +assert any(l.get("event") == "progress" for l in lines[:-1]), lines +assert lines[-1]["kind"] == "mcpp.launch" and lines[-1]["data"]["argv"], lines[-1] +PY +set +e +"$MCPP" run --message-format json > refused.out 2> refused.err +rc=$? +set -e +[[ $rc -eq 2 ]] || fail "D: --message-format json without --print-launch exited $rc, not 2" refused.err +grep -q 'print-launch' refused.err || fail "D: the refusal does not name --print-launch" refused.err + +# E +"$MCPP" --protocol-version > e.out 2>&1 || fail "E: --protocol-version failed" e.out +grep -q '"launch-stream"' e.out || fail "E: launch-stream is not stated" e.out + +echo "PASS: 1024 a launch description takes the fast path" diff --git a/tests/e2e/1025_the_default_platform_is_one_release.sh b/tests/e2e/1025_the_default_platform_is_one_release.sh new file mode 100755 index 000000000..48fc29638 --- /dev/null +++ b/tests/e2e/1025_the_default_platform_is_one_release.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# requires: llvm python3 +# 1025 -- THE DEFAULT PLATFORM IS ONE RELEASE (design 2026-11-07 §4.10, D20). +# The target table names the openkal platform release this mcpp was qualified +# with, so a project that states none builds the same versions at any time, +# with or without mcpp.lock; a project selects another release in its manifest. +# +# Criteria: +# A. A project that states no platform builds `x86_64-openkal` over +# openkal-platform 0.24.3, from the target table, and mcpp.lock records it. +# B. A lock that records another release of it does not decide: the build +# says which it takes and which the lock recorded, and records 0.24.3. +# C. A platform the project states is taken as it is (a series here). +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 2>&1 +cd app +locked() { $PY - <<'PY' +import re +text = open("mcpp.lock").read() +m = re.search(r'\[package\."openkal-platform"\][^\[]*?version\s*=\s*"([^"]+)"', text) +print(m.group(1) if m else "") +PY +} + +# A +"$MCPP" build --target x86_64-openkal > a.log 2>&1 || fail "A: the openkal build failed" a.log +grep -q 'Platform *openkal-platform 0.24.3' a.log || fail "A: the build does not use openkal-platform 0.24.3" a.log +[[ "$(locked)" == "0.24.3" ]] || fail "A: mcpp.lock does not record 0.24.3" mcpp.lock + +# B +$PY - <<'PY' +import re +text = open("mcpp.lock").read() +text = re.sub(r'(\[package\."openkal-platform"\][^\[]*?version\s*=\s*")0\.24\.3(")', r'\g<1>0.24.2\2', text, count=1) +open("mcpp.lock", "w").write(text) +PY +[[ "$(locked)" == "0.24.2" ]] || fail "B: the fixture could not edit mcpp.lock" mcpp.lock +"$MCPP" build --target x86_64-openkal > b.log 2>&1 || fail "B: the openkal build failed" b.log +grep -q "default; mcpp.lock recorded v0.24.2" b.log || fail "B: the build does not say which release it takes over the lock's" b.log +[[ "$(locked)" == "0.24.3" ]] || fail "B: mcpp.lock was not rewritten to 0.24.3" mcpp.lock + +# C +cat >> mcpp.toml <<'EOT' + +[target.'cfg(os = "openkal")'] +platform = "openkal-platform@^0.24" +EOT +"$MCPP" build --target x86_64-openkal > c.log 2>&1 || fail "C: the build with a stated series failed" c.log +grep -q 'Platform *openkal-platform 0.24' c.log || fail "C: no platform line" c.log +grep -q "'s default" c.log && fail "C: a stated platform was reported as the default" c.log + +echo "PASS: 1025 the default platform is one release" diff --git a/tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh b/tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh new file mode 100755 index 000000000..492cbfef2 --- /dev/null +++ b/tests/e2e/1026_an_archived_member_test_carries_its_members_files.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# requires: python3 +# 1026 -- an archived member test carries its member's files (SPEC-015 §4). +# +# In a workspace, a member's programs and tests are placed in the member's own +# product directory with the files its `[runtime] deploy` names. `mcpp test +# --no-run --archive` copies each test with the files beside it; on +# 2026.11.7.1 the copy named the files where a plan of one package places +# them (`bin/`), not the member's directory (`bin//`), and +# the archive of a member with a deploy set was refused: "cannot copy … into +# the test archive". +# +# Criteria: +# A. `mcpp test -p tool --no-run --archive` writes the archive, and the +# member's deployed file is beside its test there. +# B. In a checkout without target/, the archived test runs and finds the +# file beside it. +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; } +MCPP="${MCPP:-mcpp}" + +mkdir -p "$TMP/ws/app/src" "$TMP/ws/tool/src" "$TMP/ws/tool/tests" "$TMP/ws/tool/notices" +cd "$TMP/ws" +printf '[workspace]\nmembers = ["app", "tool"]\n' > mcpp.toml +printf '[package]\nname = "app"\nversion = "0.1.0"\n' > app/mcpp.toml +printf 'int main() { return 0; }\n' > app/src/main.cpp +cat > tool/mcpp.toml <<'TOML' +[package] +name = "tool" +version = "0.1.0" + +[runtime] +deploy = [ { from = "notices/LICENSE", to = "licenses/tool" } ] +TOML +printf 'tool license\n' > tool/notices/LICENSE +printf 'int main() { return 0; }\n' > tool/src/main.cpp +cat > tool/tests/notice.cpp <<'CPP' +import std; +int main(int, char** argv) { + auto here = std::filesystem::absolute(argv[0]).parent_path(); + std::ifstream in(here / "licenses" / "tool" / "LICENSE"); + std::string line; + std::getline(in, line); + return line == "tool license" ? 0 : 1; +} +CPP + +check_archive() { # $1 archive, $2 log + rm -rf "$TMP/x" && mkdir -p "$TMP/x" && tar -xzf "$1" -C "$TMP/x" + python3 - "$TMP/x" <<'PY' || fail "the archive does not carry the member's file" "$2" +import json, pathlib, sys +m = next(pathlib.Path(sys.argv[1]).glob("*/mcpp-tests.json")) +d = json.load(open(m)) +tests = [t for leg in d["legs"] for t in leg["tests"] if t["name"] == "notice"] +assert tests, d +t = tests[0] +files = [f for f in t["files"] if f.endswith("licenses/tool/LICENSE")] +assert files, t +assert (m.parent / files[0]).read_text() == "tool license\n", files +PY +} + +# A +"$MCPP" test -p tool --no-run --archive "$TMP/a.tar.gz" > a.log 2>&1 || fail "A: -p tool --no-run --archive" a.log +check_archive "$TMP/a.tar.gz" a.log +echo "ok: a member's archived test carries the member's file" + +# B +mkdir -p "$TMP/co" && cp -R "$TMP/ws/mcpp.toml" "$TMP/ws/app" "$TMP/ws/tool" "$TMP/co/" +rm -rf "$TMP/co/target" "$TMP/co/app/target" "$TMP/co/tool/target" +cd "$TMP/co" +"$MCPP" test -p tool --archive "$TMP/a.tar.gz" > b.log 2>&1 || fail "B: mcpp test --archive" b.log +grep -q "notice ... ok" b.log || fail "B: the archived test did not find its file" b.log +echo "ok: the archived test finds the file beside it" + +echo "PASS: 1026 an archived member test carries its member's files" diff --git a/tests/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp index 6d9a8a597..77e0b972e 100644 --- a/tests/unit/test_pack_distribution.cpp +++ b/tests/unit/test_pack_distribution.cpp @@ -261,7 +261,7 @@ TEST(PackDistribution, TheWindowsLauncherIsAProgram) { EXPECT_NE(src.find("(\"windows-\" + arch)"), std::string::npos) << src; EXPECT_NE(src.find("os::process::exec"), std::string::npos) << src; const auto manifest = d::exe_launcher_manifest("hello", "1.0.0"); - EXPECT_NE(manifest.find("os = \"^0.3.1\""), std::string::npos) << manifest; + EXPECT_NE(manifest.find("os = \"=0.3.1\""), std::string::npos) << manifest; EXPECT_NE(manifest.find("[targets.hello]"), std::string::npos) << manifest; } diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp index a3336c579..4bf6dcd0b 100644 --- a/tests/unit/test_platform.cpp +++ b/tests/unit/test_platform.cpp @@ -559,3 +559,21 @@ TEST(PlatformMerge, AnOverrideReplacesHostsCarriersAndDescriptorKeys) { EXPECT_EQ(merged.carriers[0].section, ".x"); EXPECT_EQ(merged.descriptorKeys.value, (std::vector{"openkal"})); } + +// D20 (design 2026-11-07 §4.10): the target table's default is one release; a +// failed build names a newer one in the index with the line to write, and says +// nothing when the project, the configuration or the command line chose. +TEST(PlatformDefault, ANewerReleaseIsNamedOnlyForTheDefault) { + namespace mp = mcpp::manifest::platform; + mp::Origin builtIn{}; builtIn.tier = mp::Tier::BuiltIn; + auto tip = mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "0.24.4", "2026.11.7.2"); + ASSERT_TRUE(tip.has_value()); + EXPECT_NE(tip->find("openkal-platform 0.24.3, mcpp 2026.11.7.2's default"), std::string::npos) << *tip; + EXPECT_NE(tip->find("platform = \"openkal-platform@0.24.4\""), std::string::npos) << *tip; + for (auto tier : {mp::Tier::Manifest, mp::Tier::Config, mp::Tier::Env, mp::Tier::Flag}) { + mp::Origin chosen{}; chosen.tier = tier; + EXPECT_FALSE(mcpp::platform::newer_release_tip(chosen, "openkal-platform", "0.24.3", "0.24.4", "x")); + } + EXPECT_FALSE(mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "0.24.3", "x")); + EXPECT_FALSE(mcpp::platform::newer_release_tip(builtIn, "openkal-platform", "0.24.3", "", "x")); +} diff --git a/tests/unit/test_wire.cpp b/tests/unit/test_wire.cpp index c52048d4e..dd3679976 100644 --- a/tests/unit/test_wire.cpp +++ b/tests/unit/test_wire.cpp @@ -149,6 +149,7 @@ TEST(WireProtocol, NamesTheFeaturesAClientGatesOn) { 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()); + EXPECT_NE(std::ranges::find(names, "launch-stream"), names.end()); } TEST(WireProtocol, IsItselfSelfIdentifying) {