diff --git a/.agents/docs/2026-10-28-platforms-design.md b/.agents/docs/2026-10-28-platforms-design.md new file mode 100644 index 000000000..5450b1f83 --- /dev/null +++ b/.agents/docs/2026-10-28-platforms-design.md @@ -0,0 +1,580 @@ +--- +subject: platform +status: active +--- + +# 平台:目标体系的通用扩展;openkal 作为第一个平台包;IDE 一线与 mcppls 的同批升级 + +日期:2026-10-28。前文:[part 1](2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md)(控制契约、IDE 生态规划 §13–§14)、 +[part 2](2026-10-26-ecosystem-part2-os-library-over-openkal-design.md)(ok 映像与 `-openkal`)。 +基线:mcpp `7d704820`(2026.10.26.2)+ release PR [#847](https://github.com/mcpp-community/mcpp/pull/847)(含 #846);openkal 0.17.1 轮 +(openkal-linux 0.18.2、openkal-macos 0.15.1、openkal-windows 0.13.3、openkal-musl 0.22.3、openkal-llvm-runtime 0.16.3、openkal-image 0.1.1); +mcpplibs.os 0.2.0;mcppls 0.0.18([#72](https://github.com/Sunrisepeak/mcpp-language-server/pull/72),未合入);mcpp-ide 本地仓库 `90ae383`; +mcpp-vscode 0.8.0;mcpp-plugins [#51](https://github.com/mcpp-community/mcpp-plugins/pull/51)(0.20.0,未合入)。 +相关规范:SPEC-002、004、006、007、011、012。 + +**修订记录** + +- rev 1(2026-10-27):openkal 内置目标栈(`[openkal]` 表、`src/openkal/`)。 +- rev 2(2026-10-28):通用平台机制与 SPEC-013;openkal 是第一个平台包;板级支持包与目标表一并迁移。 +- rev 3:第二轮评审——键表精简为七组(§4);宿主图与目标图分别选择平台,构建程序可以作为 ok 映像由 `okrun` 运行(§6); + mcppls(含语义高亮的时延,§11.1)、mcpp-ide、mcpp-clion、mcpp-vscode、mcpp-plugins 纳入同批,作为生态级方案(§11); + #846 并入 #847;每仓库一个 PR、commit 作检查点的交付计划(§13);多视角自审(§15)。 +- **rev 4(本稿)**:第三轮评审——§16 的 1–7 定案;mcpp-clion 以插件依赖的形式依赖 mcppls 的 CLion 插件,语言功能只由 mcppls 提供 + (§11.2);VS Code 与 JetBrains Marketplace 的发布由维护者上传,交付物为产物路径(§13.3)。§15 按定案重审,§16 改为本轮待确认项。 +- **rev 6**:补充设计 part 2(打包与分发、okb、`.okso` 本机绑定、体积、openkal 0.17.2 轮)定案,以 mcppls 与 mcpp-ide 交叉验证(§17)。 +- **rev 5**:§16 定案(VS Code 以 `extensionPack`、CLion 以 `` 引入 mcppls;上传交付物为本机验证后的产物目录);开始实施。 + 实施中发现两处缺口,列入同批:os 0.2.0 在宿主三元组上以 openkal-musl 为 C 库时不能编译(L9,os 0.2.1);M3 载体的链接取到宿主 GCC 的 + `crtbeginT.o`(L10,§4.7)。 + +## 0. 摘要 + +**平台**是一类目标的一份可复用、带版本、可发布的配置:工具链、目标侧各层的默认提供者、本机工具、编译与链接约定、 +产物命名与封装、运行器。平台包在自己的清单中以 `[platform]` 段声明;使用者在 `[target.]`(目标图)或 +`[host]`(宿主图)中以同名键覆盖;构建程序在平台阶段以 SPEC-007 指令陈述的内容与之同义。引擎只提供机制:选择、 +四层合并、把默认提供者放进依赖图、执行封装、报告来源。 + +openkal 由数据包 `openkal-platform` 描述;目标表为 `-openkal` 钉住它的一个版本,即"内置支持"。`mcpp build|run|test|pack +--target x86_64-openkal` 不写任何声明即可工作;引擎中不再有 openkal 专属代码。宿主图同样可以选择平台:选择 openkal 平台时, +构建程序编译为本机架构的 ok 映像,由 `okrun` 运行,在三个系统上行为与产物一致。 + +同批升级 IDE 一线:mcppls 修正大文件语义高亮远慢于补全的问题并迁移到平台;新仓库 mcpp-ide 交付后端、VS Code 适配层 +(沿用扩展 ID `mcpp-vscode`)与 JetBrains 适配层(`mcpp-clion`);mcpp-vscode 转为指向 mcpp-ide。 + +## 1. 问题(实测) + +| 编号 | 观察 | 出处 | +|---|---|---| +| P1 | 净室中 `mcpp new hello && mcpp run --target x86_64-openkal` 需手写 `openkal-tools` 与 `openkal-llvm-runtime` 四行。 | 2026-10-27 净室,2026.10.26.2 | +| P2 | 缺少 `okld` 的提示写死 `openkal-tools = "0.1.0"`。 | `src/okimage/okimage.cppm` | +| P3 | 两个 openkal 版本同图时,多版本改名只改 C++ 模块名,不改 C 头文件与链接符号,故障出现在无关位置。 | part 1 §25.8 | +| P4 | 精确版本在本地索引中缺失时安装失败不刷新,并尝试 `compat:` 拼写。 | 用户报告;#847 | +| P5 | os、mcppls 在清单中重复声明运行时与工具,CI 从源码自建 `okrun`。 | os、mcppls | +| P6 | 承载 openkal 行为的引擎代码有六处:`flags`、`ninja_backend`、`prepare/plan`、`execute`、`pack`(按包名找 `openkal-tools`)、`triple`。 | `grep` | +| P7 | 报告把目标默认的运行时标为 `graph`。 | targetside 报告 | +| P8 | 体系知识分布在五种互不相通的机制:目标表行、`sysroot` 覆盖、payload 默认运行器、板级支持包的构建程序指令、`[target.]` 键。 | docs/04、22、32、34、41 | +| P9 | 构建程序由宿主工具链按宿主约定编译:同一 `build.mcpp` 在 Windows 上由 MSVC、在 Linux 上由 GCC 编译,行为与诊断随系统不同,缓存不能跨系统复用。 | SPEC-007 §10 | +| P10 | mcppls 中,大文件的补全很快生效,语义高亮往往晚很多(§11.1 分析)。 | 用户报告 | +| P11 | IDE 一线(part 1 §14)尚未交付:mcpp-ide 只有本地后端,无远端仓库;VS Code 与 JetBrains 适配层为空目录;mcpp-vscode 仍是独立实现。 | mcpp-ide `90ae383` | + +## 2. 原则 + +1. **目标的属性归平台,平台归生态。** 引擎不持有体系知识。 +2. **引擎只提供机制与规范。** +3. **一套词汇,三种写法同义**:平台包声明约定,使用者声明覆盖,构建程序陈述需要计算的部分。先例:R9.9。 +4. **声明优先,代码兜底。** +5. **内置即钉住**:内置支持是目标表中的一个版本号。 +6. **宿主与目标对称**:两张图各自选择平台,机制相同;宿主图的平台默认是宿主约定。 +7. **一致、稳定、可观察**:同一份报告、同一个锁、同一组优先级;每个值可追溯来源;拒绝时给出键、来源与出路。 + +## 3. 词汇 + +| 词 | 含义 | +|---|---| +| 目标(target) | 产物运行的机器与格式,以三元组命名 | +| 平台(platform) | 一类目标的配置包(`openkal-platform`、一块开发板的支持包);区别于构建配置 `--profile` | +| 目标图 / 宿主图 | 为目标构建的包;为构建机器构建的程序(构建程序、工具) | +| 层(layer) | `kernel-abi`、`c-abi`、`c++-abi`、`compiler-runtime`(SPEC-002) | +| 工具(tool) | 在构建机器上运行的程序(`okld`);区别于随产物发布的 `artifacts`(R6.4) | +| 封装(seal) | 链接之后、发布之前对产物本身的处理与检查;区别于 `artifact` 角色(生成另一个文件) | +| 运行器(runner) | `mcpp run` / `mcpp test` 执行产物的方式;`runners.` 为命名的设备动作 | +| 载体(carrier) | 把依赖加载器的产物与加载器合成一个本机程序的宿主程序包(M3) | + +## 4. 平台包规范(SPEC-013 要点,精简后的七组键) + +```toml +[platform] +targets = ["", …] # 服务的目标 +toolchain = "" # 约定的工具链 +sysroot = … # 预构建的目标基(可选;同 [target].sysroot) +runner = ["", "{}"] # 或 { cmd = […], host = "same-arch" | "any", exclusive = bool } +runners = { = [ … ] } # 命名的设备动作(flash、monitor…) +carrier = { package = "", section = "<节名>", main = "<入口源码>" } # M3 载体(可选) +plugin = "build.mcpp" # 平台包的构建程序,在平台阶段运行(可选) + +[platform.layers] = <依赖规格> | false +[platform.tools] = <依赖规格> | { program = "<名字>" } +[platform.build] cflags / cxxflags / asmflags / ldflags = [ … ] +[platform.build.'cfg(<谓词>)'] 同上,条件化(沿用 cfg 语法,例如 arch = "aarch64") +[platform.link] driver = "compiler" | "lld"; flags = [ … ](driver = "lld" 时直接给 ld.lld 的参数) + start = "<符号>"; script = "<包内路径>" + inputs = [ { file = "<包内路径>" } | { from = [argv…], as = "<名字>" } ] + exe = "{name}."; shared = "lib{name}." + seal = [ [argv…], … ] +[platform.versions] <包> = "<版本>" # 一组相互一致的版本(§8.1) +[platform.hosted] targets、toolchain、layers # 宿主三元组选用本平台时适用的子集(§5.4);targets 限定可用的宿主三元组 +``` + +相对 rev 2 的精简:`platform-spec`、`requires-mcpp` 删去,改用 `[package] mcpp`(R9.8,平台规范版本随引擎版本);`round` / +`round-members` 合为 `versions`(平台包自身的版本就是版本组的名字);`artifact.*` 与 `seal` 并入 `link`("产物怎样生成"是一件事); +`build.` 改用已有的 cfg 子表;`run.host`、`run.exclusive` 并入 `runner` 的表形式;`pack.carrier` 改为顶层 `carrier`。 +功能不变:rev 2 的每个键都有对应。 + +约定沿用已有写法:argv 中 `{}` 为产物;程序以名字书写,按"图中为本机构建的程序 → 声明的载荷 → `PATH`"查找。 + +### 4.1 三种写法同义 + +平台包写约定(`[platform]`),使用者写覆盖(`[target.]`、`[host]`,键名相同),构建程序在平台阶段以指令陈述可变部分 +(新增指令 `link_driver`、`link_input`、`artifact_name`、`seal`、`run_host`,与已有的 `cxxflag`、`link_flag`、`link_start`、`link_script`、 +`runner`、`run_exclusive` 同列)。三者解析为同一结构 `PlatformStatement`,每个字段带来源。 + +### 4.2 声明与代码的分界 + +- **只能声明**:`targets`、`toolchain`、`sysroot`、`layers`、`tools`、`runner`、`runners`、`carrier`、`versions`、`hosted`——解析工具链与依赖图 + 时需要,那时没有构建程序可运行;`self env`、IDE、报告不经构建即可读取。 +- **首选声明**:`build`、`link`。 +- **插件代码**(`plugin`):依使用者的图、特性或机型而变的部分;在 `platform` 阶段运行(与 R9.9 的 `toolchain` 阶段并列),只能陈述 + `build`、`link`、`runner`、`runners` 的指令,其他指令被拒绝并点名;遵守 SPEC-007 的义务;结果按身份缓存。 + +### 4.3 层的默认提供者与让位 + +`layers.` 作为根的依赖边在依赖遍历前加入,来源 `platform`。图中已有包以 `provides` 提供该层时不加入;一个包提供多层只加入一次; +`false` 表示不提供。判断读 `provides`,不比较包名。 + +### 4.4 工具 + +依赖规格:为本机构建(等价于带 `tools = [...]` 的构建依赖,同包的工具只构建一次);`{ program }`:由载荷或 `PATH` 提供,引擎报告所用的 +程序(承担 R6.2 的报告义务)。图中已有为本机构建的同名程序时让位。 + +### 4.5 合并 + +标志按"平台 → 本机配置 → 项目"拼接,约定在前,使用者只能追加(需要不同约定时换平台);`link.seal`、`link.inputs` 整体替换;其余按字段替换。 + +### 4.6 封装 + +`link.seal` 的步骤在链接器写出的临时产物上依次执行(`{}` 为它);全部成功后原子发布(SPEC-011),任一步失败则不发布、保留上一次发布的 +产物,错误给出该步 argv 与输出;步骤属于链接边的命令行。 + +### 4.7 载体(M3) + +`mcpp pack --mode self-contained` 为本机构建 `carrier.package`,以 `.incbin` 把产物放入其 `section` 节(ELF、Mach-O、COFF 三种汇编由引擎生成), +并给出符号 `mcpp_carried_begin`、`mcpp_carried_end`、`mcpp_carried_name`;程序入口是平台包的 `carrier.main`(引擎不含任何加载器的调用)。 +取代 `pack.cppm` 中按包名找 `openkal-tools` 的代码。载体按宿主约定构建(自举边界):Linux 上为 `-linux-musl` 行并显式写出该行的工具链 +(载体的图提供 kernel-abi,否则会取消行的约定,L10)。 + +## 5. 目标图:选择与覆盖 + +### 5.1 选择 + +第一个给出答案的胜出:① `--platform <规格>` / `MCPP_TARGET__PLATFORM`(`none` 不用平台);② `[target.] platform`(工作空间根被成员继承); +③ `config.toml` 同键;④ 目标表的 `platform` 列;⑤ 宿主目标的内置约定。`platform` 不能写在 `cfg(platform = …)` 选择器下。 + +### 5.2 覆盖 + +```toml +[target.'cfg(platform = "openkal")'] # 两个架构共用一份 +layers.c-abi = { path = "../openkal-musl" } +tools.okld = { path = "../openkal-image/tools" } +runner = ["my-okrun", "{}"] +``` + +四层优先级与 `sysroot` 相同:`MCPP_TARGET__` > 项目 > 本机 > 平台包。 + +### 5.3 平台作为依赖 + +被选中的平台包是该配置下根的依赖:其库目标可被 `import`(板级支持包选为平台即可用其模块)。旧写法(板级支持包作为普通依赖、 +由构建程序发出 `runner`、`link_script`)继续有效,按 §4.1 合并。 + +### 5.4 宿主三元组选用平台 + +宿主三元组(`x86_64-linux`、`x86_64-windows-gnu`)只适用平台 `hosted` 子表中的 `toolchain` 与 `layers`;无 `hosted` 的平台被拒绝并说明它只服务其 +`targets`。docs/24 的 "openkal 交叉" 因此写作 `[target.'cfg(any(linux, macos, windows))'] platform = "openkal-platform@0.17.1"`;手写 +`openkal-llvm-runtime` 的旧写法由让位规则保持有效。 + +## 6. 宿主图:选择平台,构建程序作为 ok 映像 + +### 6.1 模型 + +一次构建有两张图:目标图为 `--target` 构建;宿主图为构建机器构建构建程序(`build.mcpp`、平台插件)与本机工具。两张图各自有一个平台, +机制相同(选择、合并、注入、报告): + +```toml +[host] # mcpp.toml 或 config.toml +platform = "openkal-platform@0.17.1" # 宿主图的程序编译为 <本机架构>-openkal,由 okrun 运行 +``` + +选择顺序:`--host-platform` / `MCPP_HOST_PLATFORM` > 项目 `[host]` > 本机 `config.toml [host]` > 默认(宿主约定,即今天的行为)。 + +### 6.2 自举边界 + +宿主图选用平台 P 时,P 自己的工具与运行器(`okld`、`okrun`)总是按宿主约定原生构建:运行器不能由它自己运行。规则:**一个平台的 +`tools` 与 `runner` 所需程序,在任何图中都按宿主约定构建**。没有循环:P 的工具属于原生宿主图,构建程序属于 P 的宿主图。 + +### 6.3 意义(为何做) + +- **一致**:构建程序在 Linux、macOS、Windows 上由同一工具链(llvm)以同一约定编译,读同一个 openkal,行为、诊断与产物字节一致(P9)。 + Windows 上编译构建程序不再需要 MSVC。 +- **缓存跨系统**:同一架构的构建程序映像与系统无关,身份不含宿主系统;全局缓存与 CI 缓存可在三个系统间共享。 +- **对称**:宿主图与目标图是同一机制的两个实例,没有为构建程序另设的通道。 + +### 6.4 约束与默认值 + +- 构建程序的协议(标准输出上的指令、`MCPP_*` 环境、文件读写、查询用的进程启动)全部经 openkal 提供,映像内可用;依赖宿主原生库的 + 构建程序不能编译为映像,此时报错点名该依赖与 `[host] platform` 键。 +- 本批交付机制、验收与测量:e2e 覆盖构建程序、`build.mcpp` 的库、平台插件三类宿主程序作为映像运行;mcpp-index 的全量构建以 + `MCPP_HOST_PLATFORM=openkal-platform@0.17.1` 运行一次并报告结果(§13 mcpp-index)。默认值保持宿主约定;当测量显示索引中的构建程序全部 + 通过时,默认值改为 openkal 平台,那是一行数据(目标表的宿主平台列),不需要新的设计。 + +## 7. 内置平台 + +- **宿主目标**(Linux、macOS、Windows、Android、iOS、Emscripten):约定留在引擎中(链接 ELF、Mach-O、PE 所必需),以平台的形状报告 + (`Platform host conventions (built-in)`),其 `pin`、`sysroot` 列即内置平台的 `toolchain`、`sysroot`,使用者以同名键覆盖。 +- **钉住的平台包**:目标表增加 `platform` 列,`-openkal` 写 `"mcpplibs.openkal-platform@0.17.1"`;增加宿主平台列,默认空(宿主约定)。 +- **获取**:平台包按普通包解析、下载、缓存、锁定,在工具链解析前读取清单;无 `plugin` 的平台包不编译。`mcpp toolchain install --target ` + (及 `--host`)预取一个配置的全部内容,之后可 `--offline` 构建。 + +## 8. 一致性与稳定性 + +### 8.1 版本组 + +图中属于平台 `versions` 的包被解析为不同版本时拒绝,消息列出双方请求链与两条出路:换平台版本,或固定造成冲突的包。 + +### 8.2 多版本改名的边界(一般规则) + +改名只改 C++ 模块名,只对接口全由具名模块构成的包成立。导出头文件目录的包、提供目标侧层的包及其依赖不改名,两版本并存时拒绝并列出请求者。 + +### 8.3 稳定性的其他保证 + +- 平台包的身份(名字、版本、来源)进入构建配置的指纹:换平台即换构建目录,不复用不相容的产物(与 2026.10.26.2 的暂存修正一起, + 杜绝升级后残留旧产物)。 +- 平台的约定只能追加,不能被使用者删除;需要不同约定时换平台,变化因此总是显式的、可在报告中看到的。 +- 锁记录平台与其注入的包(`origin = "platform"`);引擎升级不悄然替换已锁定的版本,`mcpp update` 才前移。 +- 平台包获取、工具构建、封装、平台插件的失败都在发生处报告,带键与来源;网络问题遵循 #847 的刷新策略。 + +## 9. 命令、输出与诊断(可观察性) + +- 报告(所有目标同构): + +``` + Target x86_64-openkal → x86_64-unknown-none-elf + Platform openkal-platform 0.17.1 (built-in) + kernel-abi openkal (the loader, okrun) + c-abi musl (openkal-musl@0.22.3, platform) + c++-abi libc++ (openkal-llvm-runtime@0.16.3, platform) + tools okld, okrun (openkal-tools@0.1.1, platform) + runner okrun (platform) +``` + + 宿主图选用了平台时多一行 `Host platform openkal-platform 0.17.1 (config.toml)`。来源取值:`built-in`、`platform`、`config.toml`、`mcpp.toml`、 + `MCPP_*`、`--platform`、`graph`。 +- `--message-format json`:`plan` 事件增加 `platform` 与 `hostPlatform` 对象(名字、版本、来源、`fields` 中每个字段的值与来源)。 +- `mcpp self env --target [--host] [--format json]`;`mcpp toolchain list` 增加平台列;`mcpp toolchain install --target `;`mcpp self doctor` + 检查平台就绪与版本组;`-v` 下每个字段的决定以 `decision` 记录(R6.5 的同一机制)。 +- 诊断:每条拒绝给出键、来源与出路,不写死版本号;新增错误码进入 `mcpp self explain`;格式遵守 SPEC-012。首次使用平台时的获取在 + 状态行中可见(`Fetching platform openkal-platform 0.17.1`)。 + +## 10. openkal-platform + +### 10.1 它做什么 + +一个**只含数据、不编译**的包,内容是 openkal 生态对 `-openkal` 的全部约定(映像的编译与链接标志、入口、注释节、`okld note/check`、 +`.oke`/`.okso`、`okrun`、M3 载体)与默认版本(运行时、工具、实现)。今天前者写在引擎里,后者要求每个项目手写。它跟随 openkal 轮次发布 +(0.17.1),只改约定时发 `0.17.1.1`;带验收测试(两个目标各构建一个最小程序,在本机架构运行)。mcpp 只钉住它的版本;项目也可以不等 +mcpp,自己写 `platform = "openkal-platform@0.18.0"`。 + +### 10.2 内容 + +```toml +[package] +namespace = "mcpplibs" +name = "openkal-platform" +version = "0.17.1" +mcpp = ">=<本批 mcpp 版本>" + +[platform] +targets = ["x86_64-openkal", "aarch64-openkal"] +toolchain = "llvm@23.1.3" +runner = { cmd = ["okrun", "{}"], host = "same-arch" } +carrier = { package = "openkal-run@0.1.1", section = ".oke", main = "carrier/main.cpp" } + +[platform.layers] +compiler-runtime = "openkal-llvm-runtime@0.16.3" +c++-abi = "openkal-llvm-runtime@0.16.3" +c-abi = { name = "openkal-musl", version = "0.22.3", default-features = false, features = ["posix"] } + +[platform.tools] +okld = "openkal-tools@0.1.1" +okrun = "openkal-tools@0.1.1" + +[platform.build] +cflags = ["-fPIC", "-femulated-tls", "-fvisibility=hidden", "-fasynchronous-unwind-tables", "-Xclang", "-fstack-clash-protection"] +cxxflags = ["-fPIC", "-femulated-tls", "-fvisibility=hidden", "-fasynchronous-unwind-tables", "-Xclang", "-fstack-clash-protection"] +[platform.build.'cfg(arch = "aarch64")'] +cflags = ["-ffixed-x18", "-mno-outline-atomics"] +cxxflags = ["-ffixed-x18", "-mno-outline-atomics"] + +[platform.link] +driver = "lld" +start = "ok_main" +flags = ["-shared", "-z", "max-page-size=65536", "-z", "now", "-z", "noexecstack", "--hash-style=both", "--gc-sections", "--eh-frame-hdr"] +inputs = [{ from = ["okld", "note-source"], as = "ok-notes.S" }] +exe = "{name}.oke" +shared = "lib{name}.okso" +seal = [["okld", "note", "{}"], ["okld", "check", "{}", "--with", "{needed}"]] + +[platform.hosted] +targets = ["x86_64-linux-gnu", "aarch64-linux-gnu", "x86_64-linux-musl", "aarch64-linux-musl", + "x86_64-macos", "aarch64-macos", "x86_64-windows-gnu", "x86_64-windows-musl"] +toolchain = "llvm@23.1.3" +[platform.hosted.layers] # 与 [platform.layers] 相同 + +[platform.versions] +openkal = "0.17.1" +openkal-linux = "0.18.2" +openkal-macos = "0.15.1" +openkal-windows = "0.13.3" +openkal-musl = "0.22.3" +openkal-llvm-runtime = "0.16.3" +openkal-ld = "0.1.1" +openkal-run = "0.1.1" +openkal-tools = "0.1.1" +``` + +`link.flags` 是 `build.ldflags` 之外、只给 `driver = "lld"` 的链接器参数(`ld.lld` 直接调用时不经编译器驱动的 `-Wl,` 转写)。每个值取自今天的引擎 +或已发布版本,无新约定。`-ffreestanding`(图中无 C 库)、`__OPENKAL__`(由 `kernel-abi` 取值定义)、无 C++ 运行时时拒绝 `import std`,本来就是按层 +判断的通用规则,留在引擎。 + +### 10.3 仓库与发布顺序 + +新仓库 `mcpplibs/openkal-platform`(GitCode 镜像 `mcpp-res/openkal-platform`)。它引用 openkal-llvm-runtime,后者经 openkal-musl 依赖 openkal-image +的 openkal-ld,同仓会形成发布环,因此独立,是一轮中最后发布的包:实现 → openkal-image → openkal-musl → openkal-llvm-runtime → openkal-platform → 索引 → +mcpp 前移钉住。 + +## 11. 生态(同批,每仓库一个 PR) + +### 11.1 mcppls:语义高亮的时延(P10)、平台迁移与 0.0.18 内容 + +**分析(代码事实)。** + +| 环节 | 补全(快) | 语义高亮(慢) | +|---|---|---| +| 是否等待打字停顿 | 否 | 是:`textDocument/semanticTokens/*` 在 `builds_main_file_` 列表中(`src/engine/clangd.cpp:2261`),打字期间等待停顿 `typing_quiet_`(上次构建时长 ×1.2,夹在 300–1000 ms) | +| clangd 所需 | 前导(preamble)加当前文本,不建主文件 AST | 主文件的完整 AST;大文件的 AST 构建是秒级 | +| mcppls 的快速路径 | 有:`answered_from_word`(同一词内复用上一答案并重定位)、`answered_late`(迟到答案在同词内立即给出,C-2) | 无:没有上一结果的缓存,打开文件或 AST 重建期间什么也不返回 | +| 增量 | 不适用 | 无:声明 `full: true`,不声明 `delta`;客户端发来的 `full/delta` 被改写成 `full`(`workspace.cpp:1102`),每次传输整份 token | +| mcppls 自身的处理 | 轻 | `remap_core_tokens` 与 `merge` 各做一次 decode → vector → nlohmann JSON 数组的 encode;`merge` 对每个原生 token 扫描全部 core token(O(N×M)) | + +结论:高亮慢于补全的主因是**高亮必须等主文件 AST**,而补全不需要;mcppls 又没有为高亮做补全那样的上一结果复用,也没有增量。约束:不动 clangd。 + +**方案(mcppls 内)。** + +- **H1 上一结果即时返回。** 每个打开文档保存最近一次 clangd 的 token(已映射到本服务器的图例)与它所对应的文本版本。请求到达而 AST 未就绪时, + 以上一结果立即答复,按两版本之间的编辑重定位(行列平移;被编辑的行内 token 丢弃),然后在 AST 就绪后发 `workspace/semanticTokens/refresh` + (引擎已有合并后的 refresh 机制,`engine.cppm:145`)。客户端不支持 refresh 时退回等待。 +- **H2 持久缓存。** 以(文档内容摘要、编译命令摘要、mcppls 与 clangd 的版本、模块 BMI 的身份)为键,把 token 写入工作区缓存目录(遵守 C-8 + 的预算);打开文件时键命中即立即答复,再走 H1 的刷新。键中任何一项不同即视为未命中,保证不显示错误高亮。 +- **H3 增量。** 声明 `full: { delta: true }`,为每次答复分配 `resultId`,对 `full/delta` 以上一结果计算编辑(LSP 的 SemanticTokensEdit),大文件 + 只传变化部分。 +- **H4 处理成本。** `merge` 改为两路有序合并 O(N+M);remap、merge、encode 在一次遍历中完成,直接写出整数数组,不经中间 JSON。 +- **H5 首屏。** 客户端发 `range` 请求时(VS Code 对可见区域先发),H1/H2 的结果按范围裁剪即时答复;mcppls 自身的模块语法 token(无需 AST) + 总是立即可用。 +- **测量与判据。** devtools bench 增加任务:一个 1 万行、导入多个模块的文件,记录 didOpen 到首个补全、首个高亮(H1/H2 前后)、编辑后高亮更新 + 的时延,以及每次答复的字节数;CI 的 bench-validate 校验基线与参考对。判据:打开已缓存文件时首个高亮不晚于首个补全;编辑后高亮答复的 + 字节数与编辑规模成比例(H3)。 + +**操作系统层只来自 mcpplibs.os。** 与 mcpp 2026.10.26.2 删除 `modules/platform` 相同,mcppls 删除自己的 `modules/os` 与 `modules/platform`,直接使用 +mcpplibs.os;只在确属 mcppls 自身需要处保留基于 mcpplibs.os 的最小封装。通用且适合进入 mcpplibs.os 的接口提交到 os 本批的 PR(mcpplibs/os#7,0.2.1), +带测试,供其他程序复用。 + +**同一 PR 的其他内容。** #72(0.0.18:服务器建于 mcpplibs.os、每架构一个 ok 映像、读取 mcpp 的结果记录)的内容;删除只为目标服务的 openkal +声明(运行时、工具、ld),映像以零声明的 `--target -openkal` 构建;CI 改用新 mcpp、`mcpp toolchain install --target`;CLion 插件 e2e 断言 +mcpp 工程不被 Compilation Database 接管(issue #71 的第三项)。版本 0.0.18,发布于 mcpp 之后。 + +### 11.2 mcpp-ide(新仓库 `mcpp-community/mcpp-ide`) + +按 part 1 §14 的结构与边界交付全部内容,并加入平台: + +| 部分 | 内容 | +|---|---| +| 后端(已有,补全) | 会话、监听(结果记录、manifest)、编排(同工程串行、取消、新取代旧)、视图模型、设置校验与迁移、自检、i18n、能力协商;新增:目标与平台视图(来自 `mcpp toolchain list --format json` 与 `plan` 事件的 `platform`),`self env --target` 定位运行器 | +| `protocol/` | JSON Schema 与取自本批 mcpp 真实输出的 golden fixture(含 `platform` 对象) | +| `spec/` | 命令表、设置、菜单、i18n;由 `build.mcpp` 生成 VS Code `package.json` 声明部分与 `plugin.xml`(不提交生成物) | +| `webui/` | 依赖图、库、缓存、设置、打包、目标与平台面板;VS Code webview 与 JetBrains JCEF 加载同一份 | +| `vscode/` | 由 mcpp-vscode 0.8.0 迁移:Task(PTY)、Testing API、调试(cppdbg / CodeLLDB / lldb-dap;ok 映像经 `okrun` 的 GDB JIT 接口调试)、诊断来自事件流、manifest 诊断来自 mcpp;以扩展 ID `mcpp-community.mcpp-vscode` 发布 1.0.0 | +| `jetbrains/` | Kotlin,CLion 2025.2+(`sinceBuild = 252`,与 mcppls 的 CLion 插件相同):mcpp 运行配置类型(PTY 控制台、`main()` 旁 ▶)、测试树、调试、console filter、面板;以 `mcpp-clion` 发布。**语言功能只由 mcppls 提供**:`plugin.xml` 以 `io.github.sunrisepeak.mcppls` 依赖 mcppls 的 CLion 插件(Marketplace 安装 mcpp-clion 时一并安装它),mcpp-clion 不注册任何语言服务、不启用 CLion 的 Compilation Database 接管 mcpp 工程;两个插件的分工:mcppls = 编辑(补全、高亮、诊断、导航),mcpp-clion = 工程(构建、运行、测试、调试、面板) | +| `agents/` | 面向 agent 的说明与 skill | +| 分发 | 后端以 `--target -openkal` 构建为 ok 映像,经 `mcpp pack --mode self-contained` 成为各系统程序(与 mcppls 0.0.18 同法),随 vsix 与 CLion 插件分发;同时以 xlings 包 `mcpp-ide` 发布。GitHub Release 附 `mcpp-vscode-.vsix` 与 `mcpp-clion-.zip`;两个 Marketplace 由维护者上传(§13.3) | +| CI | 后端三系统测试(先 `mcpp build` 再 `mcpp test`,或使用 L5 的 `MCPP_BIN_`);VS Code 扩展测试;JetBrains plugin verifier;README 与 README.zh-CN 按 mcppls 的结构 | + +VS Code 一端分工相同,依赖的强度不同:`vscode/` 的 `package.json` 以 `extensionPack: ["sunrisepeak.mcpp-language-server"]` 引入 mcppls 的 VS Code +扩展(`engines.vscode` 与之相同,`^1.91.0`)——一并安装,但用户可以卸载而保留 mcpp-vscode。理由:VS Code 用户可能已有自己的 C++ 语言服务,mcpp-vscode +0.7.0 起就提供"一键关闭其他 C++ 语言服务"而不是强制替换;CLion 的原生引擎不理解 mcpp 工程,那里没有可以保留的替代,因此用强依赖。两端都只经 mcppls +提供 mcpp 工程的语言功能,mcpp-ide 本身不注册语言服务。 + +### 11.3 mcpp-vscode + +mcpp-ide 首个发布之后,一个 PR:README 指向 mcpp-ide,说明扩展 ID 不变、用户无感升级;仓库随后归档(part 1 的 O9)。 + +### 11.4 mcpp-plugins + +#51(0.20.0:SYCL 一个 C++ 运行时)为本批的唯一 PR,追加:L2 通用库 `mcpp.plugins.platform`(平台插件构造平台陈述的函数,同 `mcpp.plugins.toolchain`), +docs/rules.md 增加平台插件一节;CI 使用本批 mcpp。 + +### 11.5 其他仓库 + +| 仓库 | PR 内容 | +|---|---| +| mcpplibs/openkal-platform(新) | §10;验收测试;CI 以 mcpp 的 release 分支构建(发布前)/ 发布版(发布后);发布 0.17.1 与 GitCode 镜像 | +| mcpplibs/mcpp-index | 收录 openkal-platform;测试成员 `openkal-platform`(零声明的 `--target x86_64-openkal` 构建与运行);全量构建以宿主平台 openkal 运行一次的测量(§6.4,报告进 PR) | +| mcpplibs/os | 0.2.1:L9;删除 `[target.'cfg(os = "openkal")']` 下的运行时与工具声明;CI 的映像任务改用 `mcpp toolchain install --target` 与 `self env --target` | +| mcpplibs/openkal-image | README:工具由平台提供;`[target.'cfg(platform = "openkal")'] tools.okld = { path }` 用于开发工具本身 | +| openxlings/xim-pkgindex | `mcpp-ide` 包(后端,预编译);mcpp 的版本前移由机器人 PR 完成 | + +## 12. mcpp 侧:迁移、遗留与代码组织 + +### 12.1 迁移 + +| 对象 | 变化 | +|---|---| +| 引擎的 openkal 代码 | 删除 `src/okimage/`;六处分支改为平台机制的通用调用 | +| 目标表 | 增加目标平台列与宿主平台列;`toolchain list` 显示 | +| 板级支持包 | docs/34 改写为"编写平台包"(板级支持包即平台包,指令写法同义保留);e2e 952 以一个与 openkal 无关的本地平台包覆盖标志、链接脚本、命名、封装与运行器;332、336、338(目标行的启动)与 333(指令写法)不变,作兼容判据 | +| 清单 | `[target.]` 增加 `platform`、`layers.*`、`tools.*`、`link.*`、`carrier`;`[host]`;`cfg(platform = …)`;SPEC-004 已知键表同步 | +| 引擎接口 | `mcpp.core` 协议 17:`platform` 阶段与 §4.1 的新指令 | +| 文档 | 新增 docs/35「平台」与 SPEC-013;改写 docs/24、34;增补 docs/04、22、30、41、50;中英文同步 | + +### 12.2 遗留问题 + +| 编号 | 问题 | 处理 | +|---|---|---| +| L1 | 精确版本缺失时安装失败不刷新(P4) | 已在 #847:`E_NOT_FOUND` 交给刷新策略后再请求一次;e2e 949 | +| L2 | 已读描述符的包仍尝试 `compat:` 拼写 | 已在 #847 | +| L3 | 多版本改名对带 C 接口的包静默生效(P3) | §8.2 | +| L4 | 安装失败的诊断转述 xlings 的 "the index is current",而 mcpplibs 索引是陈旧工件 | mcpp 自己陈述:哪个索引、哪个修订、多久前同步、刷新是否进行及原因;xlings 原文降为 `-v` | +| L5 | `mcpp test` 不构建包的可执行目标 | 先构建,测试环境给出 `MCPP_BIN_`(对标 Cargo `CARGO_BIN_EXE_`) | +| L6 | 工作空间中同一依赖的缓存行打印两次 | 每包每次构建报告一次 | +| L7 | os、mcppls 的 CI 自建 `okrun` | §11 | +| L8 | kernel-abi 包在宿主目标上不进入共享映像(#846) | 已并入 #847(a336ef01,e2e 950) | +| L9 | os 0.2.0 在宿主三元组上以 openkal-musl 与 openkal-llvm-runtime 为运行时(mcppls 的形态,即 §5.4 的 `hosted`)时不能编译:macOS 源文件包含 SDK 头(`sys/sysctl.h`、`mach/mach.h`),Windows 源文件把 `path::c_str()`(此时为 `char`)传给宽字符接口 | os 0.2.1:按 Darwin ABI 自行声明所用的 libSystem 接口,Windows 显式构造宽字符串;CI 增加三系统的该形态测试 | +| L10 | `mcpp pack --mode self-contained` 为 M3 构建载体时,静态链接取到宿主 GCC 的 `crtbeginT.o`,被封闭性检查拒绝 | `:carrier` 以平台工具链为本机构建载体,C 运行时对象来自工具链载荷;e2e 覆盖 | + +### 12.3 代码组织 + +`src/platform/`,模块 `mcpp.platform`:`:model`(`PlatformStatement`,纯数据)、`:select`(目标图与宿主图的选择)、`:merge`、`:inject`(层、工具、 +载体、让位)、`:link`(driver、inputs、命名、封装)、`:carrier`、`:versions`(版本组与改名边界)、`:report`。清单解析在 `modules/manifest`;新指令在 +`hostprogram.cppm` 与 `build_program`。`:model`、`:select`、`:merge` 为纯函数,以表格测试锁定。 + +## 13. 交付:每仓库一个 PR,commit 作检查点 + +### 13.1 顺序 + +``` +openkal-platform(新) ─┐ + ├─> mcpp-index ─> mcpp #847 CI 绿 ─> 合入/发布/索引前移 ─┬─> os、openkal-image、mcppls #72、mcpp-plugins #51 + │ └─> mcpp-ide 首发 ─> xim-pkgindex(mcpp-ide) ─> mcpp-vscode +``` + +- openkal-platform 先于 mcpp-index:索引收录需要它的发布。 +- mcpp-index 的成员需要本批 mcpp(零声明构建)。解决:该成员的 `[package] mcpp` 写本批版本;索引 CI 增加与 os 相同的 `MCPP_SOURCE_REF` + 能力(成员的下限高于 CI 所用 mcpp 时,以该 ref 构建 mcpp 运行它;无 ref 时报告为跳过并点名下限);收录条目在 mcpp 的最终 CI 之前合入,使 mcpp 的 e2e 读到索引中的平台包。 +- mcpp 的最终 commit 把目标表的钉住从开发期的 `{ git, tag }` 换成索引版本(与 os 0.2.0 的切换相同)。 +- 等待 CI 期间并行开发 mcppls、mcpp-ide、mcpp-plugins(不等 mcpp 发布即可开发,以 release 分支构建验证)。 + +### 13.2 各仓库的检查点 + +**mcpp-community/mcpp — #847(标题改为本批发布,版本号在发布当日确定)** + +1. L1/L2(已有);L8 #846(已有) +2. SPEC-013、docs/35 初稿 +3. 清单:`[platform]`、`[target]` 新键、`[host]`、`cfg(platform)`;单元测试 +4. `src/platform` 的 `:model :select :merge`;表格测试 +5. `:inject`、锁的 `origin = "platform"` +6. `:link`(driver、inputs、命名、封装)接入 flags / ninja / plan;`:carrier` 从 pack 移出 +7. 运行器表形式与 `host`;execute +8. `:report`、JSON、`self env --target/--host`、`toolchain install --target`、`toolchain list` 平台列、doctor、explain 码 +9. 删除 `src/okimage`;目标表平台列(开发期钉 git tag) +10. 宿主图平台与自举边界;构建程序作为映像的 e2e +11. `:versions` 与改名边界 +12. L4、L5、L6 +13. 板级支持包迁移(examples/13、docs/34、e2e 332/336/338) +14. 新 e2e;942、947 改为零声明 +15. 文档(04、22、24、30、34、35、41、50,中英)、CHANGELOG、specs README +16. 钉住切换为索引版本 + +**mcpplibs/openkal-platform(新)**:初始 commit(README、LICENSE)→ PR:平台清单 → 验收测试 → CI → 发布 0.17.1、GitCode 镜像。 + +**mcpplibs/mcpp-index**:收录描述符 → 测试成员 → 宿主平台测量(报告)→ CHANGELOG。 + +**Sunrisepeak/mcpp-language-server — #72**:bench 任务(先测基线)→ H4 → H1 → H2 → H3 → H5 → bench 判据 → 平台迁移 → CI 与 CLion 断言 → CHANGELOG 与版本。 + +**mcpp-community/mcpp-ide(新)**:初始 commit(现有后端)→ PR:protocol fixture 更新 → 后端补全与平台视图 → spec 生成器 → webui → vscode 适配层(迁移)→ +jetbrains 适配层 → agents → 分发与 CI → README 中英 → 发布 0.1.0。 + +**mcpp-community/mcpp-plugins — #51**:`mcpp.plugins.platform` → 文档 → CI 前移到本批 mcpp。 + +**mcpplibs/os、openkal-image、openxlings/xim-pkgindex、mcpp-vscode**:各一个 PR,内容见 §11。 + +### 13.3 维护者的上传 + +mcpp-ide 发布时,交付本机验证后的产物目录(含两份文件与 `SHA256SUMS`): + +- `mcpp-vscode-1.0.0.vsix`:上传到 VS Code Marketplace(publisher `mcpp-community`,扩展 ID `mcpp-community.mcpp-vscode`),可同时上传 Open VSX; +- `mcpp-clion-0.1.0.zip`:上传到 JetBrains Marketplace(插件 ID `io.github.mcpp-community.mcpp-clion`,依赖 `io.github.sunrisepeak.mcppls`)。 + +两份文件在交付前通过:vsix 的扩展测试与 `vsce ls` 清单检查;zip 的 plugin verifier(最低与当前 CLion)。mcpp-vscode 的归档在两份上传完成之后。 + +## 14. 测试 + +单元:`test_platform_merge`(四层、拼接、整体替换、继承、`cfg(platform)` 下写 `platform` 被拒);`test_platform_select`(目标图五级、宿主图四级); +`test_platform_inject`(让位、多层一次、`false`、工具让位、自举边界);`test_platform_manifest`(每个键与错误位置、`hosted` 的键限制); +`test_platform_versions`;`test_mangle_boundary`;锁的 `origin`。 + +e2e:零声明 openkal 的 build / run / test / pack(含 aarch64 构建);942、947 零声明后照常通过;覆盖(`layers.c-abi = false`、`tools.okld = { path }`、 +`config.toml`、`MCPP_TARGET_*_PLATFORM`、`--platform`);让位(声明 openkal-musl 时来源为 `graph`);版本组冲突;一个与 openkal 无关的本地平台包 +(标志、封装、命名、运行器);板级支持包两种写法;宿主三元组选用平台与被拒;宿主图平台(构建程序、库、平台插件三类作为映像运行;自举边界下 +`okrun` 原生构建);输出与 JSON;`toolchain install --target` 后 `--offline`;L4、L5、L6;950。 + +mcppls:bench 判据(§11.1);高亮缓存命中、未命中与键变化的单元测试;delta 的往返测试;VS Code e2e 断言首个高亮时延。 +mcpp-ide:后端单元测试与 golden fixture;两端适配层的冒烟测试。 + +## 15. 多视角自审 + +1. **兼容性。** 旧写法全部保留:手写 `openkal-tools` / `openkal-llvm-runtime`(让位)、板级支持包的指令写法(同义合并)、`[target]` 的已有键、旧锁(无 + `origin` 视为项目声明);宿主图默认不变。破坏性只有一处:`src/okimage` 的提示文字与 `.oke` 生成由平台提供,行为不变(942、947 判据)。mcppls 的 + 高亮缓存键包含版本与编译命令,升级后不显示陈旧结果。mcpp-vscode 的用户经同一扩展 ID 无感升级。 +2. **跨平台。** 平台机制与三系统无关;载体汇编覆盖 ELF、Mach-O、COFF;`runner.host = "same-arch"` 处理跨架构不可运行并建议 `run_under`;宿主图作为 + 映像使三系统的构建程序一致(§6.3)。所有新 e2e 在三系统 CI 运行,需要 llvm 的沿用 `requires: llvm`。 +3. **用户开发体验(应用开发者)。** 已知目标零配置;覆盖只在一处、键与平台相同;报告与 JSON 给出每个值的来源;首次获取可见;离线可预取。 +4. **mcpp 使用体验(平台作者、插件作者、IDE)。** 一个包、一段 `[platform]`、一份规范;本地 `path` 开发;与规则插件共用 `mcpp.core`;`mcpp test` 在平台包中 + 对每个目标构建并在可运行的目标上运行;IDE 通过 `toolchain list` 与 `plan` 事件取得目标与平台,不解析清单。 +5. **简洁优雅。** 键从约 27 个精简为七组(§4);不引入新模板语法;宿主图复用目标图的全部机制;openkal 在引擎中归零。风险:`[target.]` 的键增多—— + 新键全部是平台字段的同名覆盖,docs/35 以一张表列出。 +6. **稳定性。** 约定只能追加;平台身份进入指纹;版本组与改名边界在解析时拒绝而非在链接或运行时失败;锁防止静默前移;自举边界杜绝循环;宿主图 + 默认不变,映像化以测量为前提(§6.4)。mcppls 高亮缓存的每一项失配都视为未命中。 +7. **可观察性。** 报告行、JSON、`self env`、`doctor`、`-v` 的 `decision` 记录、`explain` 码;mcppls 的 bench 给出时延与字节数。 +8. **架构。** 引擎 = 机制 + 规范 + 宿主平台的官方约定;体系知识在平台包;IDE 逻辑在 mcpp-ide,mcpp 不含 IDE 概念;mcppls 不动 clangd。宿主图与目标图对称, + 为日后默认以映像运行构建程序留出的只是一行数据。 +9. **规模与风险。** 本批改动面大(mcpp、两个新仓库、六个生态仓库)。缓解:纯函数先以表格测试锁定;openkal 的现有 e2e 是行为不变的判据;与 openkal 无关的 + 平台 e2e 证明机制通用;每个仓库内以 commit 作检查点,CI 在每个检查点可运行;并行开发由 §13.1 的依赖图约束。 +10. **对外动作。** 新建公开仓库(openkal-platform、mcpp-ide 与其 GitCode 镜像)已确认;两个 Marketplace 由维护者上传,交付物为路径与校验和(§13.3); + mcpp-vscode 在上传完成后归档。 +11. **IDE 的分工(rev 4)。** 语言功能只有一个来源:mcppls。mcpp-clion 以强依赖(``)、VS Code 适配层以扩展包(`extensionPack`,可卸载)引入它,不注册语言服务,不让 CLion 的 + Compilation Database 接管 mcpp 工程;工程功能只有一个来源:mcpp-ide 后端,经 mcpp 的控制契约。两端的能力表相同(part 1 §14.3)。风险:mcppls 的 CLion + 插件未装或版本过旧——依赖声明使 Marketplace 一并安装;后端握手时读 mcppls 的版本(`mcppls --version`),低于要求时在面板中给出升级指引,而不是静默降级。 +12. **复审 rev 3 的定案。** 宿主图的平台(§6)与自举边界无循环;mcppls 的 H1–H5 只改 mcppls,判据先测基线再改;#72、#51、#847 各为本仓库唯一 PR; + 索引 CI 的 `MCPP_SOURCE_REF` 与 os 的做法相同。未发现需要改动的定案。 + +## 16. 定案(rev 5) + +1. mcpp-clion 以 `` 强依赖 mcppls 的 CLion 插件;VS Code 适配层以 `extensionPack` 引入 mcppls 的扩展(一并安装、可卸载);后端握手检查 mcppls 的版本。 +2. 两个 Marketplace 由维护者上传;交付物是本机验证后的产物目录;插件 ID `io.github.mcpp-community.mcpp-clion`。 +3. 按 §13 实施;L9、L10 同批解决。 + +## 17. 补充设计:part 2(打包与分发) + +第四、五轮讨论把打包与分发、`.okso` 的本机绑定、体积与新增的 openkal 0.17.2 轮定为本批内容,写在 +[part 2](2026-10-28-platforms-part2-packaging-and-distribution-design.md)。要点: + +- `mcpp pack` 的选择拆为链接(`--linkage`)、闭包(`--mode`)、容器(`--pkg-format`)三个正交维度与构建腿;`--mode static` 是简写; + `--pkg-format` 取代 pack、run 的 `--format`(旧写法提示)。 +- 平台以 `universal = true` 声明其目标默认打通用包:一个包含全部架构,解压即可在任何系统、任何平台架构上运行;`--systems`、`--merge` + 产出免安装的多系统包。 +- 单文件是通用容器(原生:启动桩加载荷;openkal:载体从内存加载);okb 是 openkal 的通用映像包,格式由 openkal-image 定义,由 + openkal-platform 的插件作为包格式提供。 +- 新规范 SPEC-014(打包与分发);体积在构建、剥离、容器、去重、共享五处优化并实测。 +- `.okso` 的本机绑定由 openkal 体系的构建插件 `openkal-bind` 完成;openkal 0.17.2 轮(openkal-image 0.1.2、openkal-musl 0.22.4、 + openkal-llvm-runtime 0.16.4、openkal-platform 0.17.2)在本批最后;APE 单文件为后续(mcpp#849)。 +- 定案(part 2 §14):`[platform] universal`;okb 由平台插件作为包格式提供;`tar` 默认 gzip、zstd 显式选择;openkal-platform 0.17.1 先行、 + 0.17.2 为最终钉住。 +- 交叉验证(part 2 §15):mcppls 与 mcpp-ide 逐项采用零声明目标、通用包、okb、单文件、xlings 单资产、宿主图平台与体积优化—— + 编辑器扩展只带一个 okb 与各系统 × 架构的 okrun,一个 vsix 服务全部平台;判据写入两个 CI。 diff --git a/.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md b/.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md new file mode 100644 index 000000000..0e55428b5 --- /dev/null +++ b/.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md @@ -0,0 +1,284 @@ +--- +subject: platform +status: active +--- + +# 平台 part 2:打包与分发——三维模型、通用包、单文件、okb、体积,以及 openkal 0.17.2 轮 + +日期:2026-10-28。前文:[platforms design](2026-10-28-platforms-design.md)(SPEC-013,平台机制)。本文是它的补充,覆盖第四、五轮讨论 +(2026-10-28)定下的打包与分发、`.okso` 的本机绑定、体积,以及因此新增的 openkal 0.17.2 轮。相关规范:SPEC-007(构建插件)、 +SPEC-012(输出)、SPEC-013(平台)、新增 SPEC-014(打包与分发)。 + +**修订记录** + +- rev 1:定案——三维模型与 `--pkg-format`;okb v1 由 openkal-image 定义;体积优化本批实施并实测;`.okso` 本机绑定由 + openkal 体系的构建插件完成;APE 单文件记为后续(mcpp#849)。 +- rev 2(本稿):§14 的四项定案;新增 §15,以 mcppls 与 mcpp-ide 交叉验证 openkal 体系的独有特性。 + +## 0. 摘要 + +`mcpp pack` 的选择拆成三个正交维度,各自回答一个问题:**链接**(`--linkage`,程序属性:库在程序里还是在旁边)、**闭包** +(`--mode`,打包属性:运行的机器要提供什么)、**容器**(`--pkg-format`,打包属性:交付成什么),外加**构建腿**(`--target`,可重复)。 +一个平台可以声明其目标的包默认是**通用**的:一个包含全部架构,解压即可在任何系统、任何该平台架构上运行。单文件是通用的容器 +(原生目标为启动桩加载荷,openkal 为载体从内存加载);okb 是 openkal 的通用映像包,由 openkal-image 定义格式、由 openkal-platform 的 +插件作为包格式提供,引擎不含 openkal 知识。体积在构建、剥离、容器、去重、共享五处优化,本批实测后写入。openkal 侧新增一轮 0.17.2: +okb 与 okrun 的 bundle 和库接口、`.okso` 本机绑定插件、openkal-musl 的 renameat2 修正。 + +## 1. 问题(讨论中确认) + +| 编号 | 观察 | +|---|---| +| Q1 | `--format` 在报告类命令里是输出编码(`self env --format json`),在 `pack`、`run` 里是包格式;同名异义。 | +| Q2 | `--mode static` 把"程序全静态"(链接)与"机器什么都不用提供"(闭包)合在一个词里;openkal 有静态(一个 `.oke`)与共享(`.oke` 加 `.okso`)之分,却被说成"不适用"。 | +| Q3 | openkal 的价值——一份二进制对应"架构"而不是"系统 × 架构"——在包里没有体现:M2 是单架构的目录,M3 是单系统单架构的程序。 | +| Q4 | 没有单文件交付;用户需要自己解压。 | +| Q5 | 打包已近九千行,有包格式提供者(构建插件)的机制,却没有一份规范说明模型、阶段、暂存树与接口。 | +| Q6 | 体积:`--gc-sections` 已有而没有 `-ffunction-sections/-fdata-sections`,容器只有 gzip,资源按腿重复。 | +| Q7 | 一个 C 库要为每个系统各编一份原生库;`.okso` 只能被 ok 映像使用。 | +| Q8 | openkal-musl 的 `renameat2` 忽略 `RENAME_EXCHANGE`,第二个条目被覆盖(数据丢失;mcpplibs.os 0.2.1 的审计发现)。 | + +## 2. 模型:三个维度与构建腿 + +| 维度 | 选项 | 归属 | 原生目标 | openkal 目标 | +|---|---|---|---|---| +| 链接 `--linkage` | `static` / `shared` | 程序属性;对应 `[build] dependency_linkage`,打包时按它重建 | Linux 上 `static` 还选 `-linux-musl` 行(全静态,今天的 `--mode static` 行为) | `static`:一个 `.oke`,无 `.okso`;`shared`:`.oke` 加 `.okso` | +| 闭包 `--mode` | `system` / `vendored` / `self-contained` | 打包属性:机器提供什么 | 系统库 / 加非系统库 / 加 C、C++ 运行时 | 机器有加载器与 `.okso` / 带 `.okso` / 再带加载器(载体) | +| 容器 `--pkg-format` | `dir` `tar` `zip` `single-file` 与提供者的格式(`okb`、`appimage`、`msi`…) | 打包属性:交付成什么 | `single-file`:启动桩加压缩载荷 | `single-file`:载体从内存加载;`okb`:通用映像包 | +| 腿 `--target`(可重复) | 三元组 | 一次打包覆盖哪些目标 | 每条一份产物,或合并为一棵树(已有:APK 的 universal) | 平台声明 `universal` 时默认覆盖平台的全部 `targets` | + +- `--mode static` 保留为简写:`--linkage static --mode self-contained`。对原生目标与今天的行为相同;对 openkal 目标是"一个静态 `.oke` 加载体"。 +- 组合由引擎在打包开始前检查,不可能的组合拒绝并给出出路(例如 `--mode system --pkg-format single-file`:单文件必须自带它所运行的 + 东西)。 +- **`--pkg-format`**:`pack` 与 `run` 的包格式选项改名为 `--pkg-format`;旧的 `--format` 继续接受并提示是旧写法。全局用法规则:构建类命令 + (build、test、run、pack)的机器输出是 `--message-format`;报告类命令的 `--format` 是输出编码;包格式只叫 `--pkg-format`。 +- `[pack]` 增加 `linkage`、`format`(包格式的项目默认)、`compress`;`default_mode` 照旧。 + +## 3. 通用包(openkal 的默认) + +平台在 `[platform]` 中写 `universal = true`(openkal-platform 写),其任一目标的 `mcpp pack` 默认覆盖平台的全部 `targets`;`--no-universal` +只打所选目标。布局(`dir`、`tar`、`zip` 相同): + +``` +hello-0.1.0-openkal/ 包名带平台名,不带系统与架构 + bin/hello POSIX 启动器 + bin/hello.cmd Windows 启动器 + lib/x86_64/hello.oke …okso 每个架构一组映像 + lib/aarch64/hello.oke …okso + libexec/okrun/-/okrun[.exe] 仅 --mode self-contained + share/… 资源只放一份 + mcpp-package.json SPEC-014 的包元数据 +``` + +- **启动器由引擎生成**,内容只来自平台的 `runner` 与腿:按 `uname -m` / `%PROCESSOR_ARCHITECTURE%` 选架构目录(映射是引擎的通用知识: + `x86_64|amd64|AMD64 → x86_64`,`aarch64|arm64|ARM64 → aarch64`),按 `runner.cmd` 起动;`self-contained` 时先找包内 + `libexec/okrun/-/`,再找 PATH。 +- **self-contained 的多系统加载器**:`--systems linux,macos,windows`(默认本机系统)为每个系统 × 架构交叉构建载体的加载器程序。缺少某个 + 系统的 SDK 载荷时拒绝并点名;CI 矩阵可各自产出后用 `mcpp pack --merge a.zip b.zip …` 按 `mcpp-package.json` 合并为一棵树(腿与系统取并集, + 同名映像的摘要必须相同)。 +- 结果:同一个解压目录在任何系统、任何平台架构上可运行(vendored 需要机器上有 okrun;self-contained 什么都不需要)。 + +## 4. 单文件(通用) + +| 组合 | 产物 | 运行 | +|---|---|---| +| 原生 `vendored`/`self-contained` + `single-file` | 引擎的启动桩 + zstd 压缩的暂存树 | 首次运行把载荷解到用户缓存(按载荷摘要寻址,原子改名),之后直接 exec;缓存根:`$XDG_CACHE_HOME`/`~/.cache`、`~/Library/Caches`、`%LOCALAPPDATA%` | +| 原生 `static` + `single-file`(Linux) | 静态启动桩 + 静态程序 | 同上,零依赖 | +| openkal `self-contained` + `single-file` | 平台的载体 + 本架构的映像组(okb 切片) | 载体经 openkal-run 的 bundle 接口从内存加载,不写盘;Linux 上载体静态,零依赖 | +| openkal `vendored` + `okb` | 通用映像包(§5) | `okrun app.okb`,或 xlings 的 shim;任何系统、任何架构 | + +- 启动桩的源码随引擎提供(同 engine package 的机制),打包时为目标构建,基于 mcpplibs.os;它只做"校验、解压、exec"。 +- 载体从内存加载需要 openkal-run 的 bundle 接口(§8);载体的入口源码是平台的(`carrier.main`),引擎只把字节放进节、给出 + `mcpp_carried_*` 符号。 +- 一个文件在所有系统、所有架构上运行(APE,Cosmopolitan)不在本批:需要基于 Cosmopolitan 的 openkal 实现;记于 mcpp#849。 + +## 5. okb v1(openkal-image 定义) + +格式规范写在 openkal-image 的 `docs/okb.md`,是 okrun 的输入格式,不进入 openkal 核心规范(SPEC 12),因此不引起实现与 musl 的级联。 + +- **容器**:标准 ZIP(允许 zip64),任何 zip 工具可查看。 +- **映像**:以 stored 存放,数据起点按 64 KiB 对齐(与映像的 `max-page-size=65536` 一致,用额外字段补齐,同 zipalign),okrun 直接 mmap 原文件 + 的段,不复制、可跨进程共享页。 +- **v1 只含映像与清单**。程序在运行时读取的数据文件随通用包交付;映像从 okb 内读取资源需要加载器提供的文件视图,属于格式的后续版本, + v1 的清单以 `okb` 版本号为将来留出位置。 +- **清单 `okb.json`**:`okb`(版本 1)、`name`、`version`、`entry`、每个架构的目录与入口映像、每个条目的 sha256。 +- **解析顺序**:本架构目录 → 包内 `lib/` → okrun 的搜索路径(含 xlings 的包存储,§7)。 +- **信任**:每个映像各自 `okld sign`;清单可签名,签名覆盖全部条目摘要;okrun 的 `require_signature` 对整个 okb 生效。 +- **工具**:`okld bundle …` 写一个 okb(格式的拥有者提供写入器),`okld bundle check` 校验对齐、摘要与签名。 +- **由平台作为包格式提供**:openkal-platform 的插件(`plugin = "build.mcpp"`,平台阶段)以已有的包格式机制(SPEC-007 `pack-format`、 + `role = "artifact"` 的 action,第二遍读暂存树)声明 `okb`,动作调用 `okld bundle`。引擎不认识 okb;平台阶段的指令表因此加入 + `pack-format` 与 artifact action。 + +## 6. SPEC-014:打包与分发 + +新规范,本批写定,`pack` 的实现与之对齐。 + +1. **模型**:§2 的三个维度与腿;组合的合法性表;`--mode static` 的定义。 +2. **管线**:构建各腿 → 按 `mode` 收集闭包 → 生成暂存树 → 变换(strip、relocate、平台的 `seal`、签名)→ 写容器 → 产物、校验和、 + `mcpp.pack` 信封。`--merge` 是"读入若干暂存树 → 合并 → 写容器"。 +3. **暂存树与元数据**:布局(§3);`mcpp-package.json` 的 schema(名称、版本、平台、腿、系统、每个文件的角色与摘要、启动方式)。这是 + 包格式提供者读的输入,也是 `--merge` 与 `mcpp emit xpkg` 读的输入。 +4. **接口**(面向构建插件、框架与系统): + - 包格式提供者:构建插件或平台插件以 `pack-format` 声明、以 artifact action 生成(已有,规范化输入输出契约:暂存树、 + `mcpp-package.json`、`MCPP_PACK_*` 环境,产物路径与种类)。 + - 启动器与载体:引擎的通用启动桩;平台的 `carrier`。 + - 签名器:`[pack] sign = [argv]`(`{}` 为文件)或插件;签名在写容器之前、对暂存树内的程序与映像。 + - 分发描述:`mcpp emit xpkg` 由 `mcpp-package.json` 生成索引条目——每个系统 × 架构一项,或"一个通用资产对应所有系统"一项。 +5. **机器输出**:`mcpp.pack` 信封增加 `linkage`、`mode`、`pkgFormat`、`legs`、`systems`,`artifacts[]` 带 sha256。 +6. **全局用法规则**:§2 的 `--format`、`--pkg-format`、`--message-format` 分工。 + +## 7. 分发:经 xlings 不改 xlings + +xim-pkgindex 的包描述可以表达 openkal 程序: + +- 一个资产(通用包或 okb)对应 linux、macosx、windows 三个块,URL 相同。 +- `deps` 依赖 okrun(openkal-tools);xvm 注册时把程序命令配成 `okrun <安装目录>/.okb`(或通用包内的启动器)。 +- 共享的 `.okso` 可作为独立的 xlings 包分发(每个架构一份,三系统通用),okrun 的搜索路径包含 xlings 的包存储,多个程序共用一份。 + +本批在 xim-pkgindex 的 PR 中加入一个 openkal 程序的示例与规则说明(同一 PR 还有 mcpp-ide 包)。 + +## 8. `.okso` 的本机绑定:openkal 体系的构建插件 + +- **openkal-run 增加库接口**:`okrun_open_library(path | bytes)`、`okrun_symbol(lib, name)`、`okrun_close(lib)`,以及 §4、§5 用到的 bundle 接口 + (一组内存中的映像按名字相互解析)。 +- **插件 `mcpplibs.openkal-bind`**(openkal-image 仓库,与 openkal-run 同版本发布;模块 `mcpp.mcpplibs.openkal_bind`,按 SPEC-007 R9.6):一个 + 以 `.okso` 加 C 头文件发布的库,在非 openkal 目标上以构建依赖引入它;库自己的 build.mcpp 调用插件,插件用工具链的 clang 读出头文件中 + 的函数原型(AST JSON),生成绑定源码——每个函数一个同名本机导出,首次调用时经 openkal-run 加载 `.okso`,经带 `sysv_abi` 的函数指针转发 + (调用约定由编译器转换)——作为本包的源文件编译。`.okso` 与 openkal-run 经 `[runtime] deploy` 放到程序旁边。消费方照常依赖并 + `#include`,无感。 +- **边界由插件检查并拒绝**:只有 C 接口;边界类型不得含 `long`、`unsigned long`、`wchar_t`、`long double`(LP64 与 LLP64 不同);资源归属 + 在一侧(接口成对创建、销毁);回调参数由插件生成反向 thunk,原型必须完整;异常与 C++ 类型不跨边界。 +- mcpp 引擎不需要新机制;若实现中发现缺口(例如部署 `.okso` 的条件化),在 #847 中补齐。 + +## 9. 体积 + +本批全部实施,实测见本节末表(hello 映像;mcppls 服务器与 mcpp-ide 后端的数字随其 PR 写入 §15)。 + +| 处 | 做法 | +|---|---| +| 构建 | openkal-platform 增加 `-ffunction-sections -fdata-sections`(`--gc-sections` 已有,回收粒度因此到函数与数据)与 `--icf=safe`;`release` 配置可选 `-Oz`;LTO 已有 | +| 剥离 | 默认 strip;`--debug-symbols DIR` 输出分离的调试信息(已有) | +| 容器 | `--compress zstd\|gzip\|xz\|none[:level]`,`tar` 默认仍为 gzip(兼容),`.tar.zst` 可选;单文件载荷用 zstd;okb 内映像不压缩(为 mmap) | +| 去重 | 通用包与 okb 中资源只放一份,只有映像按架构;分发矩阵从"系统 × 架构"降为"架构" | +| 共享 | 共享 `.okso` 作为独立 xlings 包,一个架构装一次,多个程序共用(§7) | + +**实测(2026-10-28,x86_64 Linux 主机,`import std` 的 hello,release 构建,已 strip)。** + +| 项 | 0.17.1 平台 | 0.17.2 平台 | 变化 | +|---|---|---|---| +| x86_64 映像 `hello.oke` | 1 149 944 B | 1 098 872 B | −4.4%(`-ffunction-sections -fdata-sections`、`--icf=safe`) | +| 同上,`[profile.release] opt = "z"` | — | 998 928 B | 再 −9.1% | + +| 容器(通用包:x86_64 与 aarch64 两组映像) | 大小 | 相对 `.tar.gz` | +|---|---|---| +| `tar`(gzip,缺省) | 787 266 B | — | +| `zip`(deflate) | 787 669 B | +0.05% | +| `tar --compress zstd`(19 级) | 594 457 B | −24.5% | +| `tar --compress xz`(6 级) | 532 196 B | −32.4% | +| `okb`(映像 stored、64 KiB 对齐,为原地映射) | 2 475 329 B | 未压缩:分发时由外层容器或传输压缩 | +| 单文件(openkal,Linux 静态载体加 x86_64 映像) | 1 164 664 B | 一个可执行文件 | + +结论:分发矩阵从"系统 × 架构"降为"架构"后,再以 zstd/xz 压缩容器得到最大收益;映像本身的收益来自段级回收与 `-Oz`。 +okb 不压缩是格式的选择(原地加载),其传输体积由外层(xlings 下载的压缩包或 HTTP 压缩)决定。 + +## 10. openkal 0.17.2 轮(本批最后) + +| 包 | 版本 | 内容 | +|---|---|---| +| openkal-image(openkal-ld、openkal-run、openkal-tools,同版本) | 0.1.2 | okb v1(`docs/okb.md`)、`okld bundle`/`bundle check`、okrun 读 okb、bundle 接口、库接口;新包 `openkal-bind`(同仓同版本) | +| openkal-musl | 0.22.4 | `renameat2` 的 `RENAME_EXCHANGE` 等标志按语义实现或拒绝(Q8);钉住 openkal-ld 0.1.2 | +| openkal-llvm-runtime | 0.16.4 | 钉住 musl 0.22.4 | +| openkal-platform | 0.17.2 | 版本组前移;体积标志;`universal = true`;okb 包格式插件;载体入口改用 bundle 接口 | +| openkal 与三个实现 | 不变 | — | + +顺序:openkal-image → openkal-musl → openkal-llvm-runtime → openkal-platform → 索引 → mcpp 目标表钉住 0.17.2(#847 的最终 commit)。 +openkal-platform 0.17.1 先行发布,用于 #847 开发期的 CI 与索引;0.17.2 是本批的最终钉住。os 0.2.1 的钉住不涉及 musl,dev 依赖的 +llvm-runtime 随平台迁移移除;mcppls 的平台迁移后不再自己声明运行时。 + +## 11. 交付(在 part 1 §13 的基础上追加) + +| 仓库 | PR | 追加内容 | +|---|---|---| +| mcpp | #847 | 三维模型、`--pkg-format`(旧写法提示)、通用包与启动器、`--systems`、`--merge`、单文件启动桩、通用载体字节嵌入、签名接口、压缩、`mcpp-package.json`、平台阶段的包格式指令、SPEC-014、docs/10 重写(中英)、e2e | +| mcpplibs/openkal-image | 新 PR(0.1.2) | §5、§8 | +| mcpplibs/openkal-musl | 新 PR(0.22.4) | Q8 | +| mcpplibs/openkal-llvm-runtime | 新 PR(0.16.4) | 钉住 | +| mcpplibs/openkal-platform | 首个 PR 发 0.17.1;第二个 PR 发 0.17.2 | §3 的 `universal`、§5 的插件、§9 的标志 | +| mcpplibs/mcpp-index | 同 part 1 的 PR | 0.17.2 轮条目 | +| openxlings/xim-pkgindex | 同 part 1 的 PR | §7 的示例与规则 | + +openkal-platform 有两个 PR:0.17.1 是平台机制的首发(#847 开发期需要它进入索引),0.17.2 随新一轮;二者内容不同,各自对应一个 +发布,不合并。 + +## 12. 测试 + +e2e:三维组合与拒绝;`--pkg-format` 与旧写法;通用包两架构布局与启动器(本机架构运行;另一架构在 qemu-user 可用时运行); +`--merge`;原生单文件(vendored 与 static)首次解压与复用;openkal 单文件从内存运行(不写盘:缓存目录无新文件);okb 的生成、 +`okld bundle check`、`okrun app.okb`;签名步骤;`mcpp emit xpkg` 的通用资产条目;体积标志生效(`--gc-sections` 后未引用函数消失、 +`--icf` 合并)。openkal-image:okb 读写、对齐、摘要、签名的单元测试;绑定插件对一个 C 库(含拒绝的类型)的 e2e,三系统 CI。 + +## 13. 自审 + +1. **一致性**:三个维度各回答一个问题,旧选项保留为简写或旧写法,用户的已有命令不变;`--pkg-format` 消除同名异义。 +2. **引擎边界**:okb、载体入口、绑定插件都在 openkal 体系;引擎只提供通用机制(启动桩、字节嵌入、包格式提供者、启动器生成)。 +3. **跨平台**:通用包与 okb 让 openkal 的产物数等于架构数;self-contained 的多系统加载器在一台机器上可产出(SDK 缺失时拒绝),或由 + `--merge` 合并 CI 矩阵的结果。 +4. **稳定性与安全**:okb 映像对齐后 mmap,对齐不足或摘要不符时拒绝而非退回复制;单文件解压先校验载荷摘要,解到同目录的临时名再原子改名, + 按摘要寻址,不复用不完整的缓存,缓存目录只对当前用户可写。 +5. **用户体验**:默认即通用;解压即用;单文件免解压;xlings 一条命令安装。 +6. **可观察性**:`mcpp-package.json` 与 `mcpp.pack` 信封记录腿、系统、每个文件的摘要。 +7. **规模与风险**:本批再增一轮 openkal 发布与 SPEC-014。缓解:okb 不进入核心规范,级联止于 image、musl、llvm-runtime、platform; + openkal-platform 0.17.1 先行使 #847 的 CI 不等待新一轮。 +8. **未纳入**:APE 单文件(mcpp#849)。 + +## 14. 定案 + +1. `[platform] universal` 声明其目标默认打通用包(openkal-platform 写 `true`),`--no-universal` 只打所选目标。 +2. okb 由 openkal-platform 的插件作为包格式提供,引擎不认识 okb;平台阶段的指令表加入 `pack-format` 与 artifact action。 +3. `tar` 默认仍为 gzip,zstd 经 `--compress zstd` 或 `[pack] compress` 选择。 +4. openkal-platform 0.17.1 先行发布,0.17.2 为本批最终钉住。 + +## 15. 交叉验证:mcppls 与 mcpp-ide 用上 openkal 的独有特性 + +两个 IDE 一线的程序是本批特性的第一批真实用户:它们的服务器与后端本来就以 ok 映像分发,因此逐项采用本批的机制, +既是功能交付,也是对平台、打包与 okb 的交叉验证。二者的 PR 在 mcpp 发布与 openkal 0.17.2 轮之后完成这部分,判据写入各自 CI。 + +| 特性 | mcppls(#72) | mcpp-ide(0.1.0) | 验证的是 | +|---|---|---|---| +| 零声明的 `--target -openkal`(SPEC-013) | 服务器删去运行时、工具、ld 的声明 | 后端同样零声明 | 平台的层、工具、链接、封装、运行器 | +| 通用包(`universal`) | 一份 `mcppls--openkal.zip` 代替每系统 × 架构的服务器包 | 一份后端包 | 布局、启动器、`--systems` 的多系统加载器、`--merge` | +| okb | 编辑器扩展(VS Code vsix、CLion 插件、Zed)只带一个 `mcppls.okb` 与各系统 × 架构的 okrun,一个 vsix 服务全部平台 | vsix 与 mcpp-clion 的 zip 同样只带一个 `mcpp-ide.okb`,各平台一个包而不是按平台分包 | okb v1 的生成、`okld bundle check`、okrun 读 okb、mmap 与签名 | +| 单文件(openkal) | 命令行用户的 `mcppls` 单文件(每系统 × 架构,免安装) | 无(由扩展启动) | 载体从内存加载、bundle 接口 | +| xlings 单资产分发 | xim-pkgindex 的 mcppls 包改为一个资产对应三系统,依赖 okrun | 新增 mcpp-ide 包同法 | §7 的写法 | +| 宿主图的平台(SPEC-013 §6) | CI 一条以 `--host-platform openkal-platform@…` 构建(devtools 的构建程序作为映像运行) | CI 同一条(`spec/` 生成器作为映像运行) | 构建程序、其库作为映像,自举边界 | +| 体积(§9) | 服务器映像的实测基线与优化后数字写入本方案 §9 | 后端同 | 体积标志、zstd、去重 | +| 运行时版本组 | 以 openkal-platform 0.17.2 的版本组构建 | 同 | §8.1 | + +判据:两个 CI 各有一条"扩展包内只有一个 okb、每个系统 × 架构的 okrun 都能启动它"的检查;vsix 与 CLion 插件 zip 的体积在本方案 +记录前后对比。交付给维护者上传的产物目录(part 1 §13.3)中的 vsix 与 zip 即为采用 okb 后的版本。 + +### 15.1 实施中的修正与实测 + +- **运行器在任何机器上以平台的托管安排构建。** 每个系统的运行器与载体在其行(`-linux-musl`、`-macos`、 + `-windows-gnu`)上以 openkal-platform 的托管安排构建,Linux 上一次打包即得四个 okrun(Linux 两个架构、macOS + arm64、Windows x64),Windows 的 okrun 不再带 VCRUNTIME140.dll。mcpp-ide 因此去掉了各系统分别打包与 `--merge` 的作业; + mcppls 保留 Linux 与 macOS 两份的合并,以继续交叉验证 `--merge`。 +- **Windows 主机上的映像字节不同(mcpp#850,上游)。** clang 在 Windows 主机上拼接包含路径时用 `\`,`-ffile-reproducible` + 对 POSIX 目标不改;libc++abi 的 `__FILE__` 因此不同,合并正确地拒绝。Linux 与 macOS 构建的映像逐字节相同,通用包在二者 + 之一上打包。 +- **macOS 托管安排的参数(openkal-musl 0.22.6)。** Mach-O 的构造函数顺序取决于链接载入对象的顺序;musl 的构造函数早于 + openkal-macos 记录参数时,`main` 得到 argc 0。mcppls 的 CI 测得;0.17.3 轮修复,平台的 hosted 检查断言参数。 +- **mcppls 的 xlings 包仍按系统。** 其载荷带每个系统的 clangd 与语义套件,载荷中的服务器为同一个 okb 与该系统的 okrun; + "一个资产对应三系统"适用于 mcpp-ide 的后端包与 okrun 之外的纯映像程序。 + +**mcpp-ide 实测(CI run 40640226458,openkal-platform 0.17.3)。** + +| 产物 | 采用 okb 之前 | 0.17.2 平台 | 0.17.3 平台(运行器以托管安排构建) | +|---|---|---|---| +| vsix | 4 686 866 B | 2 965 783 B | 3 217 042 B | +| CLion 插件 zip | 4 605 498 B | 2 924 975 B | 3 172 459 B | +| `mcpp-ide.okb`(两个架构) | — | 5 731 095 B | 5 731 231 B | +| okrun linux-x86_64 / linux-aarch64 | — | 682 944 / 655 096 B | 683 024 / 655 128 B | +| okrun macos-aarch64 | — | 1 398 448 B | 1 237 392 B | +| okrun windows-x86_64 | — | 462 336 B + VCRUNTIME140.dll | 828 416 B | + +扩展包由"每平台一个包"变为"一个包服务所有平台";0.17.3 的增长来自 Windows 的 okrun 改为静态的托管安排程序。 diff --git a/.agents/docs/README.md b/.agents/docs/README.md index e6d722b61..f2ac87124 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -338 records. +340 records. ## By subject @@ -91,6 +91,11 @@ Records that declare one. Everything else is listed by date below. - [openkal 生态:完整性收尾与验收方案](2026-09-21-openkal-ecosystem-completion-and-acceptance.md) — active - [C 环境生态方案:执行计划](2026-09-20-ecosystem-execution-plan.md) — active +### platform + +- [平台 part 2:打包与分发——三维模型、通用包、单文件、okb、体积,以及 openkal 0.17.2 轮](2026-10-28-platforms-part2-packaging-and-distribution-design.md) — active +- [平台:目标体系的通用扩展;openkal 作为第一个平台包;IDE 一线与 mcppls 的同批升级](2026-10-28-platforms-design.md) — active + ### plugins - [The category the plugin taxonomy does not name, and what a platform actually decomposes into](2026-09-11-distribution-plugins-and-platform-decomposition.md) — active @@ -144,6 +149,8 @@ Records that declare one. Everything else is listed by date below. ### 2026-10 +- [平台 part 2:打包与分发——三维模型、通用包、单文件、okb、体积,以及 openkal 0.17.2 轮](2026-10-28-platforms-part2-packaging-and-distribution-design.md) — active +- [平台:目标体系的通用扩展;openkal 作为第一个平台包;IDE 一线与 mcppls 的同批升级](2026-10-28-platforms-design.md) — active - [生态设计 part 2:openkal 的最小原子与二进制映像、其上的 `mcpplibs.os`,以及 part 1 的收尾与回归记录](2026-10-26-ecosystem-part2-os-library-over-openkal-design.md) — active - [控制契约、并发与性能、单一 C++ 运行时,以及 IDE 生态(生态级,#838、#839)](2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md) — active - [目标基座与执行宿主:sysroot 的通用模型、运行器契约、自备输入,以及同批的发布产物路径(#832)](2026-10-23-target-base-and-execution-host-design.md) — landed diff --git a/.agents/skills/mcpp-contributing/SKILL.md b/.agents/skills/mcpp-contributing/SKILL.md index d6ebec57b..019ac5d0c 100644 --- a/.agents/skills/mcpp-contributing/SKILL.md +++ b/.agents/skills/mcpp-contributing/SKILL.md @@ -121,6 +121,8 @@ git checkout -b / - 遵循现有代码风格(查看相邻代码) - 模块导入用 `import std;` 和 `import mcpp.xxx;` - 只改需要改的,不顺手重构不相关代码 +- **循环嵌套不超过两层**:第三层改为具名的辅助函数、一次扁平的遍历(例如带深度的 + `recursive_directory_iterator`)或先收集再处理;确有必要保留三层时,在该处注释写明理由 - 平台相关代码放在 `src/platform/` 目录下 **构建验证** diff --git a/.github/tools/upgrade_lane.sh b/.github/tools/upgrade_lane.sh index be23c19da..e2a0bb811 100644 --- a/.github/tools/upgrade_lane.sh +++ b/.github/tools/upgrade_lane.sh @@ -218,12 +218,21 @@ if [ "$VARIANT" = snapshot ]; then || fail "toolchain list does not offer the newer packaging" fi if [ "$VARIANT" = modules ]; then - # A BMI of the previous spelling is not read: the dependency is rebuilt - # once (its cache key carries the spelling), then served from the cache. + # A BMI of another spelling is not read: the dependency is rebuilt once (its + # cache key carries the spelling), then served from the cache. The spelling + # moved in 2026.10.26.2 (the module map's `$root`); a previous release from + # then on spells as this one, and its entry is served at once. + old_version=$("$OLD" --version | grep -Eo '[0-9]{4}\.[0-9]+\.[0-9]+\.[0-9]+' | head -1) + same_spelling=$(printf '%s\n2026.10.26.2\n' "$old_version" | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | head -1) modules_project m-new1 run new-modules1 "$W/m-new1" "$NEW" build -v || fail "the module project does not build over the previous release's cache" - grep -q "STAGE gcm.cache/mcpplibs.openkal/" "$W/new-modules1.log" \ - && fail "a dependency BMI of the previous spelling was taken from the cache" + if [ "$same_spelling" = 2026.10.26.2 ]; then + grep -q "STAGE gcm.cache/mcpplibs.openkal/" "$W/new-modules1.log" \ + || fail "a dependency BMI of the same spelling ($old_version) was not served from the cache" + else + grep -q "STAGE gcm.cache/mcpplibs.openkal/" "$W/new-modules1.log" \ + && fail "a dependency BMI of the previous spelling ($old_version) was taken from the cache" + fi modules_project m-new2 run new-modules2 "$W/m-new2" "$NEW" build -v || fail "the second module project failed" grep -q "STAGE gcm.cache/mcpplibs.openkal/" "$W/new-modules2.log" \ diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml index 90da06984..9625fc3ad 100644 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ b/.github/workflows/ci-aarch64-fresh-install.yml @@ -168,7 +168,11 @@ jobs: # leg was written separately and never got it. rm -f .xlings.json mcpp self config --mirror GLOBAL 2>/dev/null || true - mcpp build --target aarch64-linux-musl + # Two compiles at a time: the runner (4 vCPU, 16 GB) received a + # shutdown signal twice at 656/689, with four of the engine's largest + # module units compiling at once with debug information + # (2026-10-28). The build is the same; only its width is not. + MCPP_JOBS=2 mcpp build --target aarch64-linux-musl # Absolute: it is used again after `cd /tmp/xlings-src` below. m=$(find "$PWD/target/aarch64-linux-musl" -type f -path '*/bin/mcpp' | head -1) file "$m" | grep -q "ARM aarch64" || { echo "expected aarch64 mcpp"; exit 1; } @@ -239,7 +243,8 @@ jobs: # selects one that does not exist. Same removal as the self-host step # above, same reason as install_released_mcpp.sh point 1. rm -f .xlings.json - mcpp build --target aarch64-linux-musl + # Two compiles at a time, for the reason of the self-host step above. + MCPP_JOBS=2 mcpp build --target aarch64-linux-musl self=$(find target/aarch64-linux-musl -type f -path '*/bin/mcpp' | head -1) test -x "$self" self=$(realpath "$self") diff --git a/CHANGELOG.md b/CHANGELOG.md index cc7425fd7..0f9d2df60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,178 @@ > 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.27.1] - 2026-10-27 + +### Added + +- **Platforms: the configuration of a class of targets as one versioned package** + (SPEC-013; docs/35). A package's `[platform]` section states the targets it + serves, their toolchain and target base, the package for each target-side + layer (`[platform.layers]`, or `false`), the tools built for this machine + (`[platform.tools]`), the compile flags (`[platform.build]` and its + `cfg(...)` tables), the link (`[platform.link]`: `driver`, `flags`, `start`, + `script`, `inputs`, the names `exe` and `shared`, the `seal` steps), the + runner (`{ cmd, host = "same-arch" | "any", exclusive }`) and named runners, + the carrier of a self-contained program, a plugin run in the platform phase, + a version group (`[platform.versions]`) and the part a host triple takes + (`[platform.hosted]`). The engine selects a platform for the target graph + (`--platform`, `MCPP_TARGET__PLATFORM`, `[target.] platform`, + `config.toml`, the target table) and for the host graph (`--host-platform`, + `MCPP_HOST_PLATFORM`, `[host] platform`), merges the overrides a project and + this machine write under the same key names, adds the layers and tools the + graph lacks (a layer a package of the graph provides is left to it), links, + seals and names the artifact, runs it, and reports every value with its + source. `cfg(platform = "")` selects by platform. +- **`x86_64-openkal` and `aarch64-openkal` need no declaration.** The target + table selects `mcpplibs.openkal-platform` for both rows, and the platform + states what a project wrote by hand in 2026.10.26.2: `llvm@23.1.3`, + openkal-musl, openkal-llvm-runtime, `okld` and `okrun`, the image's flags, + its link and seal, `{name}.oke` and `lib{name}.okso`, the runner and the M3 + carrier. A project that names only its sources builds, runs, tests and + packs an ok image. The engine holds no openkal code (`src/okimage` is + removed). +- **The host graph's platform.** With `[host] platform` build programs and + their libraries are compiled as ok images of this machine's architecture, + with one toolchain on Linux, macOS and Windows, and run by the platform's + runner; a platform's own tools are always built natively. e2e 953; a + platform package unrelated to openkal is e2e 952. +- **Protocol 17** (SPEC-007 R9.10): `link_driver`, `link_input`, + `link_input_from`, `artifact_name`, `seal` and `run_host`, stated by a + platform's plugin in the platform phase (`mcpp::phase()` is `"platform"`) + or by any build program in its build phase. +- **One configuration, without a build.** `mcpp self env --target ` + (`--host`, `--format json`) reports the toolchain, the platform with each + field's source, the tools, the runner and its program; `mcpp toolchain + install --target ` fetches the target's platform and builds its + tools; `mcpp toolchain list` has a PLATFORM column. +- **`mcpp test` builds the package's programs first** and gives each test + `MCPP_BIN_`, the path of the program ``. In a platform package + with no `--target`, it builds the tests for each of the platform's targets + and runs those of this machine's architecture. +- **Packaging along three axes** (SPEC-014; docs/10): `--linkage + static|shared` (the program's dependency linkage; on Linux `static` is the + musl row), `--mode system|vendored|self-contained` (what the machine + provides; `--mode static` is `--linkage static --mode self-contained`), + `--pkg-format tar|zip|dir|single-file|` (what is delivered; a + format left unstated is a zip for a Windows program and a tar elsewhere) and + the legs (`--target`, repeatable). Impossible combinations are refused before + the build. `[pack] linkage`, `format`, `compress`, `sign` and `universal` are + the project's defaults. e2e 954. +- **Universal packages.** A platform with `universal = true` (openkal-platform) + packs every one of its targets into one package, `--`, + with `lib//` per leg and generated launchers (`bin/`, + `bin/.cmd`) that pick the machine's architecture and start the + platform's runner; `--mode self-contained` adds the runner of each system of + `--systems` and each architecture under `libexec/`, built over the platform's + hosted arrangement on any machine (a macOS or Windows runner is packed on + Linux with no SDK and no C run-time of that system); `--no-universal` packs + the selected target alone. e2e 955. +- **Single files.** `--pkg-format single-file` writes one executable: for a + native program the engine's launcher carrying the staged tree as a zstd + frame, extracted once into the user's cache under the payload's sha256 and + run from there; for an ok image the platform's carrier running the image + from memory, one file per system of `--systems`. e2e 956. +- **Compression in mcpp's own codecs.** `--compress gzip|zstd|xz|none[:level]` + (`.tar.gz`, `.tar.zst`, `.tar.xz`, `.tar`) with zlib, zstd and liblzma as + dependencies of mcpp: no compressor is looked for on `PATH`, and an archive + is the same bytes on every host. A zip's entries are deflated (`gzip`) or + stored (`none`). +- **`mcpp-package.json` and what reads it.** Every staged tree describes its + package: platform, the three choices, legs, systems, how it starts, and each + file with its role, leg and sha256. `mcpp pack --merge a --merge b` joins + packages written on several machines (legs and systems united, one digest + per path); `mcpp emit xpkg --package --url ` writes the + xim-pkgindex entry, one asset for every system for a universal package with + its runner as a dependency. `[pack] sign = [argv]` signs each program, image, + library and loader before the container, and each single file. e2e 957. +- The `mcpp.pack` envelope gains `linkage`, `mode`, `pkgFormat`, `legs`, + `systems` and each file artifact's `sha256`. A platform's plugin may provide + package formats (`pack-format`, `artifact` actions) in the platform phase. +- `mcpp.lock` records the packages a platform added, and those only they + reach, with `origin = "platform"`, and names each entry's configurations in + `targets` (absent for an entry of this machine's native configuration alone, + so a lock of a natively built project is unchanged): a build for this machine + and a build or pack for an ok image leave each other's entries as they were. + The `plan` event and `result.json` carry `configuration.platform` and + `hostPlatform`. + +### Changed + +- **`--pkg-format` is the package format of `mcpp pack` and `mcpp run`.** + `--format` there is the earlier spelling and prints a warning; on every other + command `--format` is the output encoding. +- The reason `image-tool-absent` is `platform-tool-absent`; new reasons + `platform-not-a-platform`, `platform-target-unserved`, + `platform-version-group` and `platform-phase-directive`. +- A layer the target's platform added is reported `platform`, and the + platform follows on a `Platform` line (`Host platform` for the host graph). +- A version of a dependency the index does not have is stated by mcpp: which + index, its revision and age, and whether it was refreshed now and why not; + the provider's own report is printed under `-v`. +- A package shared by several configurations of one command is announced once. +- **With `trim_paths`, clang is given `-ffile-reproducible`** (clang 17 and + later): `__FILE__` takes the target's separator where clang applies it. An ok + image built on Linux and one built on macOS are the same bytes; one built on + a Windows host is not, because clang keeps the `\` it joined an included + path with for a POSIX target (#850, upstream), and `mcpp pack --merge` + refuses its images beside the others. +- **`--pkg-format tar` writes `.tar.gz` on every host, Windows included**, with + mcpp's own zlib, for a program's package and a library's alike; no `tar` or + `gzip` program is run. `--compress none` writes `.tar`. +- `x86_64-openkal` and `aarch64-openkal` select openkal-platform 0.17.3 + (openkal-musl 0.22.6, openkal-llvm-runtime 0.16.6): the C library takes a + program's arguments when it is entered, which a hosted program on macOS did + not receive. +- A platform's link with `ld.lld` itself finds the linker as `ld.lld`, `lld` + or `lld-link` beside the compiler (a Windows payload ships `lld-link`, run + with `-flavor gnu`); `mcpp explain ` explains each `platform-*` + reason. A build program's cached run is replayed only while what it named + below its output directory is there (e2e 958). + +### Fixed + +- **A version the local index lacks is refreshed for when its install is + refused.** The walk refreshes the index when a descriptor is missing, and not + when a descriptor is present without the version asked for (version tables + are per system, so that absence is not proof). The provider's "not found" for + the version is, and it now reaches the refresh policy (offline, + `[index] auto_refresh`, the debounce, one sync per run); after a refresh the + install is asked once more. A dependency on a version published after the + index was last synced (`openkal-llvm-runtime = "0.16.3"`) was refused until + `mcpp index update`. e2e 949. +- **A kernel-abi package over the system's C library is placed into the shared + image that reaches it.** Every package providing an `mcpp:` layer was a + boundary of static placement and went into programs only, which is right for + a C library or a C++ runtime. On a hosted target openkal-linux (-macos, + -windows) sits on the system's C library and is an ordinary static library + under `mcpplibs.os`; a shared image reaching it was left with `kal_*` + undefined, and CPython could not load an extension built on `mcpplibs.os` + (`undefined symbol: kal_process_close`). A `kernel-abi` provider is now a + boundary only when a C library of the graph sits on it. e2e 950. +- **A kernel-abi package over the system's C library leaves a hosted row's + toolchain in place.** The musl and windows-gnu rows name the payload that + supplies the target's C library, and a graph that supplies a C library + replaces it; a kernel-abi package counted as one. On linux-aarch64 a project + reaching openkal-linux through `mcpplibs.os` then compiled `--linkage + static` (the musl row) with the machine's llvm against glibc's headers. A + kernel-abi package now supplies the system only on a target that has none of + its own (a freestanding or a loader-supplied row). e2e 292. +- **A package whose descriptor was read is the only one asked for.** A refused + install of a namespace-omitted dependency was tried again as + `compat:` and `compat.`, shown as downloading, and reported with + three addresses, although the descriptor had named the package in + `mcpplibs`. The compat spellings are tried only when no descriptor could be + read. +- **Two versions of a package that exports a header directory or provides a + target-side layer are refused**, naming both requests. A renamed copy kept + its C names and link symbols, and the two met in one link or one include + path at a place that named neither. +- **A version group is checked before the platform completes the graph.** A + family package the project pinned outside the group is refused as the + group's conflict, with both remedies. +- **The std module of a macOS build whose C library is the graph's takes the + deployment floor** every unit takes; it took the running system's version. + ## [2026.10.26.2] - 2026-10-26 ### Added diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index 9351f82a0..7fed0511b 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -249,9 +249,9 @@ platform's contract, not a name mcpp assigns. `exports` (above) applies to an `windows_entry` (below) accept `app` exactly as they accept `bin`. `mcpp run` of an `app` target, on a row where its form is a shared library, -refuses without `--format`, naming the flag and the formats the resolved -graph provides. `mcpp pack --format apk` stages the library where the -closure already places a shared object. See `mcpp run --format` in +refuses without `--pkg-format`, naming the flag and the formats the resolved +graph provides. `mcpp pack --pkg-format apk` stages the library where the +closure already places a shared object. See `mcpp run --pkg-format` in [10 — Pack and Release](10-pack-and-release.md). An engine older than 2026.9.12.3 does not know the value and refuses it, @@ -1382,11 +1382,10 @@ different argv (`-bios default` for an OpenSBI boot, `-bios none -semihosting` for a picolibc image) — and an engine that guesses one is an engine the other board has to fight. A board-support package normally supplies it. -The one target whose runner is a property of the target rather than of a -board is `-openkal` (2026.10.26.2): an ok image is run by `okrun` on every -system, so `okrun` is that target's runner when the project declares none. It is -found as a declared runner's program is, with the programs the dependency graph -built for this machine (`tools = [...]`) searched first ([24](24-openkal-cross.md)). +A target configured by a platform takes the platform's runner when the project +declares none: `-openkal` is run by `okrun` on every system (2026.10.27.1+, +[35](35-platforms.md)). It is found as a declared runner's program is, with the +programs the dependency graph built for this machine searched first. ### 2.7.3 `runner` on a hosted target (2026.9.2.1+) @@ -1490,6 +1489,36 @@ when a base is stated, `MCPP_SYSROOT`. Its exit status is the program's. An Apple artifact on a host that is not macOS, with no `run_under`, is refused before anything is spawned, naming this key. +#### 2.7.3.3 The platform keys (2026.10.27.1+) + +`[target.]` also selects and overrides the target's **platform**, with +the platform's own key names ([35 — Platforms](35-platforms.md)): + +```toml +[target.'cfg(os = "openkal")'] +platform = "openkal-platform@0.17.3" # or { path = "…" }, or "none" + +[target.'cfg(platform = "openkal")'.layers] +c-abi = { path = "../openkal-musl" } + +[target.'cfg(platform = "openkal")'] +runner = { cmd = ["okrun", "{}"], host = "same-arch", exclusive = false } +``` + +| Key | Value | +|---|---| +| `platform` | the platform package of the selected targets, `none`, or a table with `path` / `git` | +| `layers.` | the package that provides `c-abi`, `c++-abi`, `compiler-runtime` or `kernel-abi`, or `false` | +| `tools.` | the package that builds a tool for this machine, or `{ program = "" }` | +| `link` | `driver`, `flags`, `start`, `script`, `inputs`, `exe`, `shared`, `seal` | +| `carrier` | `{ package, section, main }`: the program `--mode self-contained` builds | +| `runner` | an argv, or `{ cmd, host, exclusive }`: `host = "same-arch"` runs an artifact only on a machine of its architecture | + +`cfg(platform = "")` selects by platform; `platform` itself is not +accepted under such a selector. The same keys are read from +`~/.mcpp/config.toml`. A package that is a platform states them under +`[platform]`, and `[host] platform` selects the platform of the build programs. + ### 2.8 `[features]` — Features Moved to [06 — Features and Capabilities](06-features-and-capabilities.md), diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md index eaf4fa245..95c0af0dc 100644 --- a/docs/05-dependencies.md +++ b/docs/05-dependencies.md @@ -362,9 +362,12 @@ an error, never a directory-order choice. See the normative template rows in `mcpp build` / `run` / `test` refresh the package index **only when a dependency cannot be resolved from the local copy** — never merely because time has passed. Concretely, a refresh happens when there is no local index at all, when a -dependency's descriptor is missing from it, or when a SemVer constraint matches -none of the versions it knows. A build whose dependencies all resolve locally -makes no network request, however old the local index is. +dependency's descriptor is missing from it, when a SemVer constraint matches +none of the versions it knows, or when the install of an exact version is +refused because the index does not carry it (a version published after the +last sync); after that refresh the install is asked once more. A build whose +dependencies all resolve locally makes no network request, however old the +local index is. The consequence worth knowing: a constraint like `^1.2` resolves against the **the versions the local index knows**. A `1.3.0` published upstream after the last refresh diff --git a/docs/08-testing.md b/docs/08-testing.md index e6f93e32c..ffbd62f2c 100644 --- a/docs/08-testing.md +++ b/docs/08-testing.md @@ -107,6 +107,18 @@ test's verdict, the output of a test that failed, the `test result` line and, fo collects the report, and `mcpp test 2>&1 | tee test.log` collects both ([09 — Commands by scenario](09-commands-by-scenario.md#output-streams)). +**A test finds the package's programs by name** (2026.10.27.1+). `mcpp test` builds +the package's programs first, as `mcpp build` does, and gives each test +`MCPP_BIN_`: the path of program ``, upper-cased, every character +other than a letter or a digit written `_` (`MCPP_BIN_MY_TOOL` for `my-tool`). +A test that runs the program it tests reads it from there. + +**A platform package is tested on its targets** (2026.10.27.1+). `mcpp test` in a +package with a `[platform]` section, with no `--target`, builds the tests for +each of the platform's `targets` with the package itself as the platform. The +targets its runner can run on this machine are run; the others are built and +reported as not run ([35](35-platforms.md)). + ### In a workspace ```bash diff --git a/docs/09-commands-by-scenario.md b/docs/09-commands-by-scenario.md index e0eae8f68..9e10d95d7 100644 --- a/docs/09-commands-by-scenario.md +++ b/docs/09-commands-by-scenario.md @@ -115,7 +115,7 @@ which selects the compiler for that invocation and writes nothing: ```bash mcpp test --toolchain llvm@23.1.3 mcpp run --toolchain gcc@16.1.0 -mcpp pack --toolchain llvm@23.1.3 --format dir +mcpp pack --toolchain llvm@23.1.3 --pkg-format dir ``` For that invocation the option takes the place of `[toolchain] default` in @@ -130,6 +130,12 @@ executable has moved or been removed, the next `mcpp build` plans the graph again and writes the record anew; nothing under `target/` needs to be deleted by hand. +`--platform ` and `--host-platform ` select the target's platform +and the build programs' platform for one invocation in the same way +(2026.10.27.1+, [35](35-platforms.md)); `--platform none` builds with the +target's conventions alone, and `mcpp self env --target ` reports the +configuration a build would use. + ## Explaining a resolution `mcpp why` reports what a build would resolve, and builds nothing: diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index f9d667aa3..99deef270 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -55,21 +55,37 @@ when glibc is actually required. Read the `/proc/self/exe` note below before choosing it: launching through a bundled loader changes what the program sees about itself. -## Two axes: target (libc) × mode (bundling depth) +## Three axes and legs -Distribution is two orthogonal choices: +A pack is decided by three independent choices and a set of legs (2026.10.27.1+): -- **libc / static** — a *build-target* property: `--target …-linux-gnu` (glibc) - vs `--target …-linux-musl` (musl, static). `--target …-musl` implies `static`. -- **bundling depth** — a *pack* property: how much of the shared-lib closure - travels with the artifact. This is what `--mode` selects. +| Axis | Option | Question | Values | +|---|---|---|---| +| linkage | `--linkage` | are the libraries inside the program or beside it | `static`, `shared` | +| closure | `--mode` | what the machine that runs the package provides | `system`, `vendored` (default), `self-contained` | +| container | `--pkg-format` | what is delivered | `tar`, `zip`, `dir`, `single-file`, a format a package provides; unstated: `zip` for a Windows program, `tar` elsewhere | +| legs | `--target`, repeatable | which targets one package covers | triples | + +- **Linkage** is a property of the program. `--linkage static|shared` (or + `[pack] linkage`) is the build's dependency linkage for the pack, over + `[build] dependency_linkage` and the profile's; unstated, each library links as + it declares itself. On Linux `--linkage static` selects the `-linux-musl` + row (a `-linux-gnu` target becomes its musl row) and packs the fully static + program whole, whatever `--mode` says; `--target …-linux-musl` implies it. On + an ok image it is one `.oke` with no `.okso`. +- **Closure** is a property of the package: how much of the shared-library + closure travels with the program. `--mode static` is shorthand for + `--linkage static --mode self-contained`. +- **Container** is the shape of the delivery, described below. +- **Legs**: each `--target` is one build, staged into the same tree. A platform + that declares `universal = true` covers all of its targets by default. | Mode | Host must provide | Size | Use case | |---|---|---|---| | `system` | every `.so` (incl. third-party) | smallest | `.deb`/`.rpm`, same-distro fleet (pkg manager declares deps) | | `vendored` (default) | libc / libstdc++ / loader | +a few MB | Mainstream distros (Ubuntu 22+, Debian 12+, RHEL 9+) | | `self-contained` | nothing | +30–50 MB | Any Linux incl. older glibc; bundles closure + `run.sh` wrapper | -| `static` | nothing (single file) | +5–10 MB | musl; matching Linux x86_64 or aarch64 host, Docker scratch, Alpine | +| `static` (`--linkage static --mode self-contained`) | nothing (single file) | +5–10 MB | musl; matching Linux x86_64 or aarch64 host, Docker scratch, Alpine | How to choose: @@ -88,6 +104,27 @@ it is "the target provides everything", which is a statement about the target and cannot be spelled with this machine's absolute paths. e2e 215 sweeps every ELF in the bundle for anything under `$MCPP_HOME` and fails on a hit. +A combination that cannot hold is refused before anything compiles, naming both +choices: + +| Combination | Remedy | +|---|---| +| `--pkg-format single-file` with `--mode system` | `--mode vendored` or `self-contained`: a single file carries what it runs | +| `--compress` with `--pkg-format dir` or a provided format | `--pkg-format tar`, `zip` or `single-file`: only a container is compressed | +| `--compress zstd` or `xz` with `--pkg-format zip` | `--compress gzip` (DEFLATE) or `none`, or `--pkg-format tar` | +| `--compress gzip` or `xz` with `--pkg-format single-file` | `--compress zstd[:]` or `none`: the launcher decompresses zstd | +| several `--target` for an executable outside a universal package | one pack per target | +| `--systems` on anything but a self-contained universal package or a single file of an image | `--mode self-contained` over a platform with `universal = true`, or `--pkg-format single-file` | +| an image's single file for a system whose row this mcpp builds no program for (`x86_64-macos`, `aarch64-windows-gnu`) | `--systems` naming the systems that are built | +| an image's single file for several `--systems` with `-o` | leave `-o` out: one file per system | +| a universal package of a platform that states no `runner` | `--no-universal` | +| `--target` outside the platform's `targets` in a universal package | a target the platform serves | +| a dispatched format for a `lib` or `shared` target | the library route, [12](12-binary-distribution.md) | +| `--pkg-format single-file` for a `lib` or `shared` target | `tar`, `zip` or `dir` | + +A `[pack] compress` that does not fit the container (a directory, `zstd` for a +zip) is left aside for that pack; only the flag is refused. + ### A program that needs the HOST to provide something "Self-contained" has a floor. Some libraries can only come from the target @@ -144,32 +181,43 @@ follow the rename: `vendored` → no suffix, `self-contained` → `-bundle-all`, ## Commands ```bash -mcpp pack # vendored by default +mcpp pack # vendored by default mcpp pack --mode system mcpp pack --mode static -mcpp pack --mode self-contained # alias: --mode bundle-all -mcpp pack --target x86_64-linux-musl # equivalent to --mode static -mcpp pack --target aarch64-linux-musl # ARM64 equivalent -mcpp pack --format dir # output as a directory, no tarball -mcpp pack --format appimage # a format a package in the graph provides -mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz -mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path -mcpp pack --profile dev # build with a different profile (default: release) -mcpp pack --dev # the same, as `build` and `run` spell it; --profile wins over it -mcpp pack --message-format json # one mcpp.pack envelope on stdout (mcpp 2026.9.16.1+) -mcpp pack --no-strip # ship the artifacts as built -mcpp pack -p app --format release # a workspace member, as if run in its directory -mcpp pack --workspace --format release # every member with a program, planned and built once -mcpp pack -p cli -p gui -o dist/ # two members, each archive below dist/ -mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/ -mcpp pack --format msi --features installer # activate root-package features for the pack +mcpp pack --mode self-contained # alias: --mode bundle-all +mcpp pack --target x86_64-linux-musl # equivalent to --mode static +mcpp pack --target aarch64-linux-musl # ARM64 equivalent +mcpp pack --pkg-format dir # output as a directory, no tarball +mcpp pack --pkg-format appimage # a format a package in the graph provides +mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz +mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path +mcpp pack --profile dev # build with a different profile (default: release) +mcpp pack --dev # the same, as `build` and `run` spell it; --profile wins over it +mcpp pack --message-format json # one mcpp.pack envelope on stdout (mcpp 2026.9.16.1+) +mcpp pack --no-strip # ship the artifacts as built +mcpp pack -p app --pkg-format release # a workspace member, as if run in its directory +mcpp pack --workspace --pkg-format release # every member with a program, planned and built once +mcpp pack -p cli -p gui -o dist/ # two members, each archive below dist/ +mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/ +mcpp pack --pkg-format msi --features installer # activate root-package features for the pack +mcpp pack --linkage static # the libraries inside the program +mcpp pack --pkg-format single-file # one executable file +mcpp pack --pkg-format tar --compress zstd # .tar.zst +mcpp pack --target x86_64-openkal # a universal package: every architecture of the platform +mcpp pack --target x86_64-openkal --mode self-contained --systems linux,macos,windows +mcpp pack --merge a.zip --merge b.zip -o all.zip # one package from packages built on several machines +mcpp emit xpkg --package all.zip --url https://example.org/v1.0.0 # its xim-pkgindex entry ``` +`--format` is accepted on `pack` and `run` as the earlier spelling of +`--pkg-format` and prints a warning (2026.10.27.1+). On every other command +`--format` is the output encoding (`--format json`). + `--features ` (mcpp 2026.9.15.2+) activates root-package features for every build pass the pack performs: each `--target` leg, and both passes of a dispatched format. It is the value `mcpp build --features` takes, so a host tool needed only for one distribution is declared under `[feature-deps.]` with `tools = [...]` and built -only by the pack that names ``. `mcpp run --format --features ` hands +only by the pack that names ``. `mcpp run --pkg-format --features ` hands the same features to the pack it performs. `-p ` (mcpp 2026.9.28.3+) packs a workspace member from the workspace @@ -185,7 +233,7 @@ A pack states the build it performs as `mcpp build` states one (mcpp 2026.10.1.2+): a line for each package that does work, the status row while it runs, and `Finished` before the first `Packing` line. A pack over several configurations builds every configuration first and writes one `Finished`. The -second pass of a dispatched `--format` is stated in the same way, and its +second pass of a dispatched `--pkg-format` is stated in the same way, and its `Distributing` and `Packed` lines follow it. The build runs `[build] jobs` compiles at once, as every command that compiles does. Before 2026.10.1.2 the build of a pack wrote none of this, and a pack that recompiled for minutes @@ -199,9 +247,9 @@ planned as one selection and built once, and each is then staged, and packed, in a tree of its own: ```bash -mcpp pack --workspace --format release # every member with a program +mcpp pack --workspace --pkg-format release # every member with a program mcpp pack --workspace --exclude updater # every such member but one -mcpp pack -p cli -p gui --format release +mcpp pack -p cli -p gui --pkg-format release ``` Packing each member with its own `mcpp pack -p ` plans the graph, runs @@ -218,7 +266,7 @@ What each member receives is what `mcpp pack -p ` gives it: packed alone, below its own `target/dist/`. With `--output ` it is written below the directory, which is created when it is missing, under the name it has by default. -- **A staged tree of its own.** With a dispatched `--format`, the pack prepares +- **A staged tree of its own.** With a dispatched `--pkg-format`, the pack prepares twice, as above, and the second pass tells each build program the staged tree of the member it acts for. `mcpp::pack_stage_dir()` and `${mcpp.stage_dir}` answer per member. A program acts for the member it belongs to; a package that @@ -250,7 +298,7 @@ packs them all. | more than one `--target` | a program is built for one target, and the several-target Android pack stages the legs of one member | | `--output` that exists as a file | each member's archive or tree is written below the directory | | a member named with `-p` that has no program target | there is nothing to pack for it | -| with a dispatched `--format`, a member no provider acts for | the format is provided by a package the member does not reach, or by a package several members reach | +| with a dispatched `--pkg-format`, a member no provider acts for | the format is provided by a package the member does not reach, or by a package several members reach | | two members that would write one archive or tree | `--output` names one directory, and two packages of one name, version and target share an archive name | A package with a build program that provides a format for several members cannot @@ -261,39 +309,38 @@ one at a time. `--message-format json` (mcpp 2026.9.16.1+) prints one `mcpp.pack` envelope on stdout after the command finishes and sends every human line to stderr. Its `data.artifacts` holds each produced file or directory with its absolute path, -its `type` (`file` or `directory`), the `--format` value and the triples of its +its `type` (`file` or `directory`), the `--pkg-format` value and the triples of its legs; `data.stage` holds the staged tree, its manifest and whether the closure was walked ([50 — Machine Output](50-machine-output.md)). A pack of several members lists every member's artifacts, each naming its `member`, leaves `data.stage` null and names one tree per member in `data.stages` -(2026.10.1.1+). `--format` names the +(2026.10.1.1+). `--pkg-format` names the package format on this command, so machine output is asked for the way `mcpp test` asks for it. -### `--format` owns one axis, and the engine owns two of its values +### `--pkg-format` owns one axis, and the engine owns four of its values -`tar` and `dir` answer the same question `msi` and `appimage` answer — what -shape does the output take — so they are values of one flag rather than the -beginning of a second one. The split between what the engine holds and what a -package holds is: +`tar`, `dir`, `zip` and `single-file` answer the same question `msi` and +`appimage` answer — what shape the output takes — so they are values of one +flag. The split between what the engine holds and what a package holds is: -> **`mcpp pack` owns the mechanism and the one universal format. Every other -> format lives in a package, and `mcpp pack` dispatches to it.** +> **`mcpp pack` owns the mechanism and the formats that need no other release: +> a tree, an archive and a single file. Every other format lives in a package, +> and `mcpp pack` dispatches to it.** -The universal format is what it already produces: an archive that extracts and -runs. It is universal in the only sense that matters here — it needs no -knowledge of anyone else's release. Everything past it does. dpkg's control +Those four extract, or start, and run, and need no knowledge of anyone else's +release. Everything past it does. dpkg's control fields, AppImage's runtime, WiX's schema, Apple's notarisation, Android's signing scheme: each one bound into the engine would couple an mcpp release to a release mcpp does not control. The same argument the project already made for languages, where Slang is supported without being named in the engine. -So the value set is open (mcpp 2026.9.11.1+). `--format ` finds the +So the value set is open (mcpp 2026.9.11.1+). `--pkg-format ` finds the package in the resolved graph that declares `` and hands it the staged tree; an unknown value names what *is* available rather than a fixed list: ``` -error: unknown --format 'bogus'. +error: unknown --pkg-format 'bogus'. available in this build: tar, dir, appimage A format past `tar` and `dir` comes from a package in the resolved graph, which declares it with `mcpp::provides_pack_format("")` in its build program. Add the package @@ -311,7 +358,7 @@ A dispatched format applies to a **program** target — `kind = "app"` included, on every row, whatever file that row links it to (see [04 §2.2](04-mcpp-toml.md)). A library package ships an interface plus prebuilt binaries per triple and has no single staged tree, so -`mcpp pack --format ` is refused rather than ignored. +`mcpp pack --pkg-format ` is refused rather than ignored. **A `kind = "app"` target whose artifact is a shared object accepts more than one `--target`** (mcpp 2026.9.13.2+): on every Android row an application @@ -334,18 +381,18 @@ when it includes a directory (relative or absolute), the literal path is used. For the full set of options, see `mcpp pack --help`. -### `mcpp run --format ` (mcpp 2026.9.12.3+) +### `mcpp run --pkg-format ` (mcpp 2026.9.12.3+) ```bash -mcpp run --target x86_64-linux-android --format apk -mcpp run --target aarch64-ios-sim --format app +mcpp run --target x86_64-linux-android --pkg-format apk +mcpp run --target aarch64-ios-sim --pkg-format app ``` -`--format` is the same flag `mcpp pack` takes, and reused here for the case a +`--pkg-format` is the same flag `mcpp pack` takes, and reused here for the case a plain `mcpp run` cannot reach: an Android application is a `.apk` and an installed iOS application a `.app`, and neither is the link output `mcpp run` -executes by default. `mcpp run --format ` packs the target for `` -— the same two passes and staged tree as `mcpp pack --format ` — and +executes by default. `mcpp run --pkg-format ` packs the target for `` +— the same two passes and staged tree as `mcpp pack --pkg-format ` — and then runs the artifact the pack reported, through the runner resolved for a program: the project's `[target.] runner`, then a dependency's `mcpp::runner(...)`, then the payload descriptor's. @@ -355,7 +402,7 @@ The artifact a pack reports is the request's **terminal** one: among the action consumes. A provider is often a chain (`dist-apk`: link, add the libraries, align, sign), and every output in it is verified to exist, but only the last is the distributable and only it is printed as `Packed`. A format -whose chain ends in two files is refused by `mcpp run --format`, naming both, +whose chain ends in two files is refused by `mcpp run --pkg-format`, naming both, because a runner takes one operand. A format may report many outputs, one per file of a distribution tree. Up to @@ -365,10 +412,10 @@ files)`), and `--verbose` prints every one (2026.9.29.5+). `--message-format json` lists every output in either case. An unknown `` is refused naming the format set the resolved graph -provides, the same set `mcpp pack --format bogus` reports. `--format` together +provides, the same set `mcpp pack --pkg-format bogus` reports. `--pkg-format` together with `--no-runner` is refused — an `.apk` or an installed `.app` cannot be executed directly. Running a `kind = "app"` target on a row where its form is -a library, with no `--format`, is refused naming the flag and that same +a library, with no `--pkg-format`, is refused naming the flag and that same format set. `mcpp test` is unaffected: a test binary is a program on every row. @@ -448,7 +495,7 @@ path. every library name the closure resolved: the staged path of a library the tree carries, `platform` for one the target provides, and `unresolved` for one found nowhere (the line format is in [50](50-machine-output.md#the-stage-manifest)). -When a name is unresolved, `--format tar` and `--format dir` fail naming it, +When a name is unresolved, `--pkg-format tar` and `--pkg-format dir` fail naming it, because a tree without that library does not start where it is installed; a dispatched format still receives the tree, with `closure = not-walked`. A library the target provides and this machine does not have is named in @@ -627,9 +674,10 @@ for the same reason — there is no zip tool present on every host. Two consequences worth knowing: -- Entries are **stored, not deflated**, so a Windows package is roughly the - size of its contents. Compression is a size optimization, not a correctness - one, and it is not implemented yet. +- Entries are **deflated** by zlib when that makes them smaller, and stored + otherwise; `--compress gzip:` sets the level, `--compress none` stores + every entry. A format left unstated is a zip for a Windows program; + `--pkg-format tar` stated writes a tar. - The archive is **deterministic**: no timestamps are read, so two packs of the same tree are byte-identical and a published checksum means something. @@ -675,22 +723,24 @@ unaffected. ### ok images (`-openkal`): the image, or a program that carries it An ok image runs through `okrun` on every system of its architecture (see -[24](24-openkal-cross.md)). The mode keeps its meaning — what the machine that +[24](24-openkal-cross.md)), and its platform states how it is packed +([35](35-platforms.md)). The mode keeps its meaning — what the machine that runs the package must provide: | Mode | The package holds | The machine provides | |---|---|---| | `system` | `bin/.oke` | `okrun` and every image it names | | `vendored` (default) | `bin/.oke` and the `.okso` images it names | `okrun` | -| `self-contained` | `bin/` — a program of this machine that carries the image as a section (`.oke`, `__OKE,__image`) and runs it through libokrun — and the `.okso` images beside it | nothing | -| `static` | refused | — | +| `self-contained` | `bin/` — a program of this machine that carries the image in the platform's carrier section (`.oke` on ELF and COFF, `__OKE,__carried` on Mach-O) and runs it through libokrun — and the `.okso` images beside it | nothing | +| `static` (`--linkage static --mode self-contained`) | `bin/`, a program of this machine that carries one static `.oke` | nothing | -The image is not recompiled for `self-contained`: mcpp generates the entry and -the section, depends on `openkal-run` from the source of the project's -`openkal-tools`, and packs that program as it packs any other — static (musl) -on Linux, with only the system's libraries on macOS and Windows. The bytes in -the section are the `.oke`'s. Another system's program is packed on that -system. +These are the packages of one target (`--no-universal`, or a platform that is +not universal). `--linkage static` builds the image with no `.okso`. The image +is not recompiled for `self-contained`: mcpp builds the platform's carrier +(`[platform] carrier`, a package and its entry source) as a generated project +under `target/carrier-pack/--/`, places the image's bytes in +the carrier's section, and packs that program as it packs any other — static +(musl) on Linux, with only the system's libraries on macOS and Windows. ### Android: the application object and its closure under `lib/` @@ -718,6 +768,139 @@ own closure. `--mode` does not apply on this row: the device provides exactly the platform libraries, so there is no host set to leave out and no loader to carry. +### A universal package (`[platform] universal`) + +A platform that declares `universal = true` (openkal-platform does) makes a +pack of any of its targets cover all of them; `--no-universal` packs the +selected target alone. One tree serves every system and every architecture of +the platform: + +``` +target/dist/hello-0.1.0-openkal/ +├── bin/hello ← POSIX launcher +├── bin/hello.cmd ← Windows launcher +├── lib/x86_64/hello.oke ← one set of images per architecture +├── lib/aarch64/hello.oke +├── libexec/okrun/-/ ← only with --mode self-contained +├── share/… ← resources, once +└── mcpp-package.json +``` + +The other legs are built during the pack (`Packed leg `). The +launchers are generated from the platform's `runner`: each resolves the package +as the parent of its own directory, picks the architecture directory from +`uname -m` (`x86_64`, `amd64` → `x86_64`; `aarch64`, `arm64` → `aarch64`) or +from `%PROCESSOR_ARCHITECTURE%` / `%PROCESSOR_ARCHITEW6432%`, and starts the +runner on the image there with its own arguments, looking for the runner first +in `libexec//-/` and then on `PATH`. 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. +`[pack] universal = false` is `--no-universal`; a platform that states no +`runner` cannot make a universal package, and the pack is refused with +`--no-universal` as the remedy. + +Under `--mode self-contained` the runner travels: for each system of +`--systems linux,macos,windows` (default: this machine's) and each architecture +of the package, mcpp builds the platform's carrier with nothing carried — a +program that runs the image its first argument names, as `okrun` does — into +`libexec//-/`. Each runner is built on its row +(`-linux-musl`, `-macos`, `-windows-gnu`) over the platform's +hosted arrangement, so the same program results whichever machine packs, with +no SDK or C run-time of that system; a Linux runner is static. A row this mcpp +builds no program for (`x86_64-macos`, planned; `aarch64-windows-gnu`, not in +its table) is left out with a warning, and the launcher there looks for the +runner on `PATH`. An image built on a Windows host is not the bytes of one +built elsewhere (#850): pack universal packages on Linux or macOS. + +Packages built on several machines are joined with +`mcpp pack --merge a.zip --merge b.zip` (also `--merge a.zip,b.zip`). The inputs +are package directories or archives (`.zip`, `.tar`, `.tar.gz`, `.tgz`, +`.tar.zst`, `.tar.xz`) this command wrote; nothing is built. Legs and systems +are united, each file is taken once, and the result is described anew. Inputs +of another name, version, platform, linkage or mode are refused, and so is one +path with two digests, naming the path, both digests and both inputs. The +container is `--pkg-format`, else the extension of `-o`, else the first +input's. + +### A single-file package + +`--pkg-format single-file` delivers one executable file: + +| Target and mode | The file | On start | +|---|---|---| +| native, `vendored` or `self-contained` | the engine's launcher carrying the staged tree as one zstd frame | the first run unpacks the tree into the user's cache, and later runs start it from there | +| native Linux, `--linkage static` | a static launcher and the static program | as above, with no system library | +| ok image | the platform's carrier and the image, or a zip of all its images | the carrier loads the images from memory and writes nothing to disk | + +A native program's file is `target/dist/--` (`.exe` on +Windows); its tree is staged under `target/single-file/stage/`. The launcher is +a small program whose source mcpp writes with each pack (under +`target/single-file//`, over mcpplibs.os and zstd) and builds for the +target: static (musl) on Linux, with the system's libraries only on macOS and +Windows. The payload is one zstd frame (`--compress zstd[:]`, the +default, or `none`) of the tree. The first run unpacks it into +`/mcpp/single-file//`, where `` is +`$XDG_CACHE_HOME` (else `~/.cache`) on Linux, `~/Library/Caches` on macOS and +`%LOCALAPPDATA%` on Windows: into a name of its own beside it, renamed into +place without replacing when every file is written (`.mcpp-complete` marks a +complete tree; the directory is the user's alone on Linux and macOS). The frame +carries its content's checksum, and a payload that does not match it is never +unpacked. The launcher then becomes the program — `bin/`, the wrapper at +the root of a self-contained Linux tree, or `.exe` at the root of a +Windows tree — with its arguments, and its exit status is the program's. + +An ok image's file is `target/dist/---` (`.exe` on +Windows), one per system of `--systems` (default: this machine's). The carrier +carries the image alone when it names no other image, and otherwise a stored +zip of all the images, each aligned to 64 KiB, which the carrier's entry reads +through openkal-run's bundle interface (openkal-platform 0.17.2 and later). + +### `mcpp-package.json` + +Every staged tree carries `mcpp-package.json`: the package, its platform, its +three choices, its legs and systems, how it starts, and each file with its role +and sha256. A provided format, `--merge` and `mcpp emit xpkg` read it +([SPEC-014](specs/packaging.md) §4.5): + +```json +{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.17.3", + "linkage": "shared", "mode": "vendored", "legs": ["x86_64-openkal", "aarch64-openkal"], "systems": [], + "launch": {"runner": ["okrun", "{}"], "entry": {"x86_64": "lib/x86_64/hello.oke", "aarch64": "lib/aarch64/hello.oke"}}, + "files": [{"path": "lib/x86_64/hello.oke", "role": "image", "leg": "x86_64-openkal", "sha256": "…"}]} +``` + +The file is written for every pack, a provided format's included. Each file's +`role` is `program`, `image`, `library`, `loader`, `launcher`, `resource` or +`metadata`; in a universal package every file of `lib//` is an image of +that leg. Symbolic links are not listed. `linkage` is `static`, `shared` or +`declared` (each library as it declares itself); `launch` is `program` for one +native leg and `runner` with an `entry` per architecture for a universal +package. + +`mcpp emit xpkg --package [--package …] --url [-o ]` +writes the xim-pkgindex entry (spec 2) of packages this command wrote, from +their `mcpp-package.json`. A universal package is one asset that `linux`, +`macosx` and `windows` install alike, with the runner as a dependency +(`deps = { "okrun" }`) unless the package carries it; packages of one system +and architecture each make that system's entry, one URL with `${arch}` and a +sha256 per architecture. The install moves the archive's top directory into +place and registers the program with xvm (the `.cmd` launcher on Windows). + +### Compression and signing + +`--compress zstd|gzip|xz|none[:level]` (or `[pack] compress`) chooses the +compression of `tar` (`.tar.gz`, the default, `.tar.zst`, `.tar.xz`, `.tar`). +`tar` writes the archive and mcpp compresses it with its own codecs (zlib, zstd +and liblzma), so no compressor is looked for on `PATH` and an archive is the +same bytes on every host. A `zip` deflates its entries (`gzip[:level]`) or +stores them (`none`); a single file's payload is `zstd` or `none`; the images +inside an okb are stored so that the loader maps them in place. + +`[pack] sign = ["", …, "{}"]` runs once on each program, image, library +and loader of the staged tree before the container is written, and on each +single file; `{}` is the file. A failure stops the pack, naming the argv and its +output, and `mcpp-package.json` states the digests of the signed files. + ## Configuration Packaging behavior is configured via the `[pack]` section in `mcpp.toml`. The @@ -726,9 +909,14 @@ common fields are: ```toml [pack] default_mode = "static" # override the normal vendored default for bare `mcpp pack` +linkage = "shared" # the default of --linkage +format = "tar" # the default of --pkg-format +compress = "zstd" # the default of --compress +sign = ["signtool", "sign", "{}"] # run on each program, image, library and loader before the container +universal = false # = --no-universal strip = true # default. false ships the artifacts as built debug_symbols = "dist/debug" # separate the debug info here instead of discarding it -include = ["share/**", "config/*.toml"] # extra files to bundle +include = ["share/**", "config/*.toml"] # project files staged at the same path, once exclude = ["debug/**"] # Fine-tune the vendored filtering policy. The configuration key keeps its @@ -747,15 +935,13 @@ The `static` mode additionally requires a musl toolchain configured under `[target.]`; for the full setup, see the `mcpp.toml` in [`examples/03-pack-static`](../examples/03-pack-static/). -## Planned Support - -Windows DLL bundling beyond the current `.zip` is on the roadmap. - -Distribution formats such as `.deb`, `.rpm`, AppImage and `.msi` are **not** on -this list, and that is a decision rather than an omission: they live in -packages and reach the user through `--format `, for the reason the -section above gives. Nothing further needs to join `[pack]`'s built-in modes. +## Current limitations -This document evolves alongside the `mcpp pack` implementation; for the latest -options, refer to `mcpp pack --help`. +- Distribution formats such as `.deb`, `.rpm`, AppImage and `.msi` come from + packages through `--pkg-format `; the engine provides `tar`, `dir`, `zip` + and `single-file`. +- One file that runs on several systems is not produced; a single-file package + serves one system and one architecture. +- An ELF program's closure is resolved by running its dynamic linker, so a Linux + program is not packed from Windows. diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index adc35e630..7c9a7f85c 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -539,7 +539,7 @@ installed by mcpp · `SDK` the platform's own · `—` unreachable from this hos | `x86_64-windows-musl` | Windows by payload; anywhere by graph | no gcc emits PE+musl, and LLVM cannot spell the triple | | `aarch64-macos` | macOS | the SDK is the machine's | | `*-none-elf` | every host | clang and lld are cross-compilers by construction | -| `*-openkal` | every host | the same; the image is compiled for no operating system and runs on every system through `okrun` ([24](24-openkal-cross.md)) | +| `*-openkal` | every host | the same; the image is compiled for no operating system and runs on every system through `okrun` ([24](24-openkal-cross.md)); the target table selects `openkal-platform` for both rows ([35](35-platforms.md)) | | `wasm32-emscripten`, `*-linux-android` | every host | the SDK ships its own sysroot and upstream publishes it per host; one archive serves every guest arch | **A `—` is about payloads, not about possibility.** `host_can_serve` answers diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 52525b8e9..aa521815e 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -211,6 +211,10 @@ Interface and implementation are separate columns. `openkal` is an interface and `openkal-windows` an implementation of it; collapsing the two would conceal why one source reaches several machines. +The source column reads `graph` for a package the project's graph supplied and +`platform` for one the target's platform added (2026.10.27.1+, +[35](35-platforms.md)); a platform's report follows on a `Platform` line. + ## Closure Visibility The five-layer report above answers "where does each layer come from"; it diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index 7db7130ea..4bc07a540 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -110,15 +110,16 @@ practice. ## What A Project Writes ```toml -[dependencies] -openkal-llvm-runtime = "0.1.1" - -[toolchain] -default = "llvm@23.1.3" +[target.'cfg(any(linux, macos, windows))'] +platform = "openkal-platform@0.17.3" ``` -Two lines. The first selects three layers of the target side; the second names -a compiler and says nothing about where anything else comes from. +One table (2026.10.27.1+). It selects the openkal platform for the host triples, +whose hosted part names the compiler and the packages of three layers of the +target side; the system's openkal implementation provides the fourth +([35 — Platforms](35-platforms.md)). Naming `openkal-llvm-runtime` as a +dependency and `llvm@23.1.3` as `[toolchain] default` states the same by hand, +and remains valid: the platform leaves a layer to the package that provides it. Targets are given on the command line: @@ -145,7 +146,7 @@ preprocessor directive is required in the source. A worked example is | `openkal-musl` | `c-abi` | musl redirected onto openkal, ported once | | `openkal-llvm-runtime` | `compiler-runtime`, `c++-abi` | compiler-rt builtins, libunwind, libc++abi and libc++, configured for openkal-musl | -A project names the last of these. The others follow from its dependencies. +The platform names the last two, or a project names the last; the others follow from their dependencies. ## The Reason The Compiler Must Be LLVM @@ -396,8 +397,9 @@ loader runs on every system of that architecture. `okrun` is that loader on Linux, macOS and Windows. ```toml -[build-dependencies] -openkal-tools = { version = "0.1.0", tools = ["okld", "okrun"] } +[package] +name = "app" +version = "0.1.0" ``` ```sh @@ -405,25 +407,30 @@ mcpp build --target x86_64-openkal # bin/app.oke mcpp run --target x86_64-openkal # okrun bin/app.oke ``` -| | the engine's action | the reason | +Nothing else is declared (2026.10.27.1+): the target table selects +`openkal-platform` for both rows, and the platform states the toolchain, the +layers, the tools, the conventions below and the runner ([35](35-platforms.md)). + +| | the platform's statement | the reason | |---|---|---| | compile | `--target=-unknown-none-elf -fPIC -femulated-tls -fvisibility=hidden -fasynchronous-unwind-tables -Xclang -fstack-clash-protection`; on AArch64 also `-ffixed-x18 -mno-outline-atomics`; `-ffreestanding` when the graph supplies no C library | an image is told no operating system; it does not address the thread pointer and does not use the platform register, which two systems reserve; it exports what its source marks `visibility("default")` | | link | `ld.lld -shared -z max-page-size=65536 -z now --gc-sections --eh-frame-hdr`, entry `ok_main`; an image another loads (`kind = "shared"`) is `lib.okso` with its name | ET_DYN, every segment aligned to the largest protection granularity among the systems; what nothing reaches is discarded, so the requirements note names only the interfaces the program uses | | finish | `okld note` writes the interfaces the image's strong imports require; `okld check` checks every clause against the image and the `.okso` it needs; only then is `bin/.oke` published | a loader refuses an image that requires an interface the implementation lacks, naming it, and an image that breaks the convention is never published | | run | `okrun` is the target's default runner, found among the programs the graph built for this machine, then in declared payloads, then on PATH | the loader holds the openkal implementation; the image carries none | -**The kernel interface is fixed; every layer above it is the graph's.** The -report reads `kernel-abi openkal (the loader)`. The C library and the C++ -run-time are packages that provide `mcpp:c-abi` and `mcpp:c++-abi` for this -target (`openkal-musl`, `openkal-llvm-runtime`), and an image with neither is -legal: it defines `ok_main` itself and supplies `__emutls_get_address`. The +**The kernel interface is fixed; every layer above it is a package.** The C +library and the C++ run-time are the packages the platform names +(`openkal-musl`, `openkal-llvm-runtime`), reported `platform`; a package of the +graph that provides the layer takes the platform's place. An image with neither +is legal: `[target.'cfg(platform = "openkal")'.layers]` switches them off, and +the image defines `ok_main` itself and supplies `__emutls_get_address`. The payload's libc++ is not offered, because it was configured for the payload's own system; `import std;` without a C++ run-time in the graph is refused before -anything compiles, naming the packages. +anything compiles, saying where the layer was switched off. **Missing `okld` is a refusal, not a warning.** An image linked without its -requirements note would be refused by every loader; the engine names the -dependency to add instead. +requirements note would be refused by every loader; the refusal +(`platform-tool-absent`) names the package that builds it. ## Bare Metal diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index 29f13264a..bd8d87a14 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -181,11 +181,16 @@ int main() { | `mcpp::runtime_search_dir(dir)` *(protocol 12)* | `mcpp:runtime-search-dir=` — see below | | `mcpp::link_script(p)` *(2026.8.19+)* | `mcpp:link-script=` | | `mcpp::link_start(symbol)` *(protocol 16)* | `mcpp:link-start=` | +| `mcpp::link_driver(d)` *(protocol 17)* | `mcpp:link-driver=` — `"lld"` (`ld.lld` itself links) or `"compiler"`; a platform's `link.driver` ([35](35-platforms.md)) | +| `mcpp::link_input(path)` / `mcpp::link_input_from(as, token)` *(protocol 17)* | `mcpp:link-input=` — a file the link takes, or the output of a program saved as `as` (one token per call); `link.inputs` | +| `mcpp::artifact_name(kind, pattern)` *(protocol 17)* | `mcpp:artifact-name=` — `"exe"` or `"shared"` and a pattern with `{name}`; `link.exe`, `link.shared` | +| `mcpp::seal(step, token)` *(protocol 17)* | `mcpp:seal=` — one token of seal step `step`, run on the linked file (`{}`) before it is published; `link.seal` | +| `mcpp::run_host(h)` *(protocol 17)* | `mcpp:run-host=` — `"same-arch"` or `"any"`; `runner.host` | | `mcpp::runner(tok)` *(2026.8.19.2+)* | `mcpp:runner=` — see below | | `mcpp::xpkg_dir(ns, name)` / `mcpp::xpkg_dir(name)` *(2026.8.19+)* | the payload directory of a package declared in `[xlings.workspace]` — by this manifest, or by a dependency compiled into this build program *(2026.9.6.6+)*; `""` when it was not declared or is not installed (see below) | | `mcpp::xpkg_source(ns, name)` / `mcpp::xpkg_program(ns, name)` *(protocol 15)* | where that payload comes from — `payload`, `override`, `pending`, or `""` — and the program an override named | | `mcpp::xpkg_request(ns, name)` / `mcpp::xpkg_pending()` *(protocol 15)* | the directory of a payload declared `provision = "on-request"`, asking for it when it is not installed yet; `xpkg_pending()` is then true and the program should return (see below) | -| `mcpp::phase()` *(protocol 15)* | `"toolchain"` while a root program states the build toolchain, `"build"` otherwise | +| `mcpp::phase()` *(protocol 15)* | `"toolchain"` while a root program states the build toolchain, `"platform"` while a platform's `plugin` runs (protocol 17), `"build"` otherwise | | `mcpp::decision(subject, from, value, file, line, payload)` *(protocol 15)* | `mcpp:decision=` — record where a tool came from. `mcpp.plugins.tool` states it for every member | | `mcpp::toolchain(key, value)` *(protocol 15)* | `mcpp:toolchain=` — state the build toolchain in the toolchain phase. `mcpp.plugins.toolchain` builds the statement | | `mcpp::warning(text)` *(2026.8.21.2+)* | `mcpp:warning=` — see below | @@ -300,7 +305,7 @@ neither one's. whichever library of the program makes the call. A program that chooses between two runners chooses before it emits either. A runner named after a pack format reaches that format's distributable under `mcpp run ---format ` ([41 — Reaching a Device](41-devices.md)). +--pkg-format ` ([41 — Reaching a Device](41-devices.md)). ### Asking instead of declaring: `toolchain_dir` / `sysroot_dir` (2026.8.19.4+) @@ -980,7 +985,7 @@ usual work and all of them are `artifact`: each consumes **link outputs** and produces something a user installs. What the engine adds for them is a mechanism and no format at all. -`mcpp pack --format ` resolves `` through the resolved graph, the +`mcpp pack --pkg-format ` resolves `` through the resolved graph, the same way `--target` reaches a triple the engine did not have to know individually. `tar` and `dir` remain the archive shapes `mcpp pack` owns; everything past them comes from a package. @@ -1012,7 +1017,7 @@ int main() { ``` **Declare unconditionally, submit conditionally.** The declaration is what lets -the engine answer a question the requesting build cannot: `--format bogus` +the engine answer a question the requesting build cannot: `--pkg-format bogus` names what *is* available, and `--help` says "any format the resolved graph provides". Both read the set collected from a pass that asked for nothing. A member that declared only when asked still works for its author — they always @@ -1020,7 +1025,7 @@ pass their own format — and makes the set unknowable for everyone else. mcpp refuses a format nothing submitted for, rather than reporting a pack that produced no package. -**`mcpp pack --format ` prepares twice.** An `artifact` action is a ninja +**`mcpp pack --pkg-format ` prepares twice.** An `artifact` action is a ninja edge and the staged tree is produced by mcpp *after* the link, so the tree cannot be an input of the pass that built it. The first pass collects the declarations and refuses an unknown format before anything is compiled; the @@ -1036,7 +1041,7 @@ strip policy, the debug-symbol split and `include`/`exclude`. An AppImage, a (`.deb`, `.rpm`) owns the re-layout, because which directory a file belongs in is that format's knowledge and not the engine's. -`mcpp pack --format dir` writes the same tree to a path and stops, which is how +`mcpp pack --pkg-format dir` writes the same tree to a path and stops, which is how a person inspects what a member will be handed. **`mcpp pack` reports the artifact actions the request introduced.** An action @@ -1061,7 +1066,7 @@ reported as up to date. files before it resolves the artifact's dependency closure, so the tree can exist without a complete one: a closure with a name that resolves to no file the tree can carry (2026.9.14.2+), or a non-PE, non-Mach-O artifact packed from -a Windows host. `--format tar` and `--format dir` fail the command in that +a Windows host. `--pkg-format tar` and `--pkg-format dir` fail the command in that case, since the archive IS the closure; a dispatched format receives the tree regardless, with a second manifest line, `reason = `. The `needs` lines that follow the header state each needed name and what satisfies it @@ -1085,7 +1090,7 @@ none to rely on), and the only interpolations are a closed set: | `${mcpp.bin_dir}` | where produced binaries land: `bin/`, or, for an action a workspace member's build program declares, that member's product directory (2026.9.29.4+) | | `${mcpp.compile_db}` | path to `compile_commands.json` (what clang-tidy's `-p` wants) | | `${mcpp.target_file:}` | the built file of target `` | -| `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --format ` | +| `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --pkg-format ` | | `${mcpp.self}` *(2026.9.13.1+)* | the engine's own executable, absolute. An action's command is an argv with no shell, so a build program has no portable way to copy a file; the engine is present wherever a build runs, and `${mcpp.self} stage --verify content --output ` copies one file, creates the destination's parent, and writes only when the bytes differ. That argument shape is a contract from 2026.9.13.1 on; a build program that names it declares that release as its floor | Every path a `${mcpp.*}` token expands to is spelled with **forward slashes**. @@ -1346,7 +1351,7 @@ The running program receives the build context as `MCPP_*` variables | `MCPP_PKG_AUTHORS` *(2026.9.11.1+)* | `mcpp::package_authors()` | The `[package] authors`, joined with `;`. Not `,`: an author entry is conventionally `Name ` and a name may carry a comma, so a comma-joined list cannot be split back into the entries it was made from | | `MCPP_PKG_REPO` *(2026.9.11.1+)* | `mcpp::package_repo()` | The `[package] repo` | | `MCPP_TARGET_MIN_PLATFORM_VERSION` *(2026.9.12.3+)* | `mcpp::min_platform_version()` | The project's floor for this triple, in the platform's own words: on macOS, `[build] macos_deployment_target` or the engine's own default `14.0`; on iOS, `[build] ios_deployment_target` verbatim, empty when the project states none; on `*-linux-android`, `[target.] min_api_level`, or a fallback the resolved NDK payload states; empty on every other row. The value the effective triple carries, and part of the re-run key | -| `MCPP_PACK_FORMAT` *(2026.9.11.1+)* | `mcpp::pack_format()` | The `--format` value of the `mcpp pack` pass this program is part of; empty for every ordinary build. The empty value is the one that carries the meaning — a member gates its submission on this, so `mcpp build` has the graph it always had | +| `MCPP_PACK_FORMAT` *(2026.9.11.1+)* | `mcpp::pack_format()` | The `--pkg-format` value of the `mcpp pack` pass this program is part of; empty for every ordinary build. The empty value is the one that carries the meaning — a member gates its submission on this, so `mcpp build` has the graph it always had | | `MCPP_PACK_STAGE_DIR` *(2026.9.11.1+)* | `mcpp::pack_stage_dir()` | Where `mcpp pack` has already staged the closure, absolute; empty when this build is not packaging. Read it to decide the shape of the work; write `${mcpp.stage_dir}` into the action, so the path in the graph and the path the program read cannot disagree | | `MCPP_PACK_STRIP` *(2026.9.16.1+)* | `mcpp::pack_strip()` | `1` when the `mcpp pack` pass this program is part of strips, `0` under `--no-strip`; empty for every ordinary build. A member that stages libraries of its own follows it, so one switch governs every file in the package | | `MCPP_PACK_DEBUG_SYMBOLS_DIR` *(2026.9.16.1+)* | `mcpp::pack_debug_symbols_dir()` | Where `--debug-symbols` sends the separated `*.debug` files, absolute; empty when they are discarded or the build is not packing | @@ -1501,7 +1506,7 @@ the content is unchanged, so calling it unconditionally costs no rebuild. A `dist-*` member fits neither definition. It does not compile a translation unit and it does not run while the build program runs: it consumes link outputs and produces something a user installs, through an `artifact` action and -`mcpp pack --format `. The prefix matters because the taxonomy is +`mcpp pack --pkg-format `. The prefix matters because the taxonomy is load-bearing — a consumer reading `rules-wix` would expect a compiler and a translation unit, and there is neither. diff --git a/docs/31-authoring-a-rule-package.md b/docs/31-authoring-a-rule-package.md index 646513fd4..3db8d3b43 100644 --- a/docs/31-authoring-a-rule-package.md +++ b/docs/31-authoring-a-rule-package.md @@ -346,7 +346,7 @@ A `dist-*` member is neither a rule nor a tool. It does not compile a translation unit and it does not do its work while the build program runs: it consumes **link outputs** and produces something a user installs — an `.msi`, a `.deb`, an AppImage, a signed `.app`. The mechanism is an `artifact` action and -`mcpp pack --format `, documented in +`mcpp pack --pkg-format `, documented in [30 — Producing a distributable](30-build-mcpp.md#producing-a-distributable-pack_format--stage_dir-20269111). Everything in this chapter applies unchanged. Six things bind harder here, and diff --git a/docs/34-authoring-a-bsp.md b/docs/34-authoring-a-bsp.md index 3bd11f6df..f6cb5a942 100644 --- a/docs/34-authoring-a-bsp.md +++ b/docs/34-authoring-a-bsp.md @@ -125,6 +125,31 @@ something else, and neither has to be known to mcpp. cannot overlap, and `mcpp test` then serialises them. A project never has to remember `-j1`. +## The board as a platform package + +The fields a board's build program states — the link script, the entry, the +runner and the named runners — are platform fields (2026.10.27.1+, +[35 — Platforms](35-platforms.md)). A BSP may state the fixed ones in its +manifest instead and keep the build program for what depends on the selected +feature: + +```toml +[platform] +targets = ["thumbv7em-none-eabihf"] +runner = ["qemu-system-arm", "-machine", "mps2-an386", "-nographic", "-kernel", "{}"] +runners = { flash = ["probe-rs", "download", "--chip", "STM32L475VG", "{}"] } + +[platform.link] +script = "cortex-m.ld" +start = "Reset_Handler" +``` + +A project then selects the board as its target's platform +(`[target.thumbv7em-none-eabihf] platform = "@"`) rather than as +a dependency. Both forms merge into one statement: a value the build program +states replaces the manifest's, and a project's `[target.]` keys replace +both. + ## Current limitations - A BSP declares its emulator and its probe driver as payloads, so a board diff --git a/docs/35-platforms.md b/docs/35-platforms.md new file mode 100644 index 000000000..40246b781 --- /dev/null +++ b/docs/35-platforms.md @@ -0,0 +1,280 @@ +# 35 — Platforms + +**Reader:** someone building for a target a platform package configures (an ok +image, `-openkal`), changing part of that configuration, or writing a +platform package for a class of targets. + +**The question this chapter answers:** how a class of targets is configured as +one package, how a build selects it, and how a project overrides or replaces it. + +**Not here:** the target-side layers a platform names, which are +[22 — The Target Side](22-target-side.md); openkal's model and its hosted +cross-compilation, which are [24 — Cross-Compilation Over openkal](24-openkal-cross.md); +packaging a program, which is [10 — Packaging an Application for Release](10-pack-and-release.md); +the exact rules, which are [SPEC-013](specs/platforms.md). + +Before: [34 — Authoring a Board-Support Package](34-authoring-a-bsp.md). +After: [40 — Bare-Metal and Freestanding Targets](40-baremetal.md). + +A **platform** is one versioned package that states everything a class of +targets needs: the toolchain, the package that provides each target-side layer, +the tools the build runs on this machine, the compile and link conventions, the +name and seal of an artifact, and the runner. A project states none of it. The +target table selects `mcpplibs.openkal-platform` for `x86_64-openkal` and +`aarch64-openkal` (2026.10.27.1+), which is what built-in support for those two +targets is. + +## The shortest run + +A project that names only its sources: + +```toml +[package] +name = "app" +version = "0.1.0" +``` + +```sh +mcpp run --target x86_64-openkal +``` + +``` + Resolved llvm@23.1.3 → x86_64-openkal → …/xim-x-llvm/23.1.3/bin/clang++ + the platform's (openkal-platform 0.17.3), replacing your gcc@16.1.0 — override with `[target.x86_64-openkal] toolchain` + Target x86_64-openkal → x86_64-unknown-none-elf + compiler-runtime compiler-rt (openkal-llvm-runtime@0.16.6, platform) + c-abi musl (openkal-musl@0.22.6, platform) + c++-abi libc++ (openkal-llvm-runtime@0.16.6, platform) + Platform openkal-platform 0.17.3 (built-in) + tools okld, okrun (openkal-tools@0.1.3, platform) + runner okrun (platform) + link lld, start ok_main (platform) + Compiling app v0.1.0 (.) + Running `okrun … target/x86_64-openkal/…/bin/app.oke` +``` + +Every value is reported with its source. `mcpp test`, `mcpp pack` and +`--target aarch64-openkal` need nothing more. `mcpp.lock` records the packages +the platform added with `origin = "platform"`, and each entry's configurations +in `targets` (absent for this machine's native configuration alone), so a build +for this machine and one for an ok image leave each other's entries in place. + +## What a platform states + +| Group | Keys | Effect | +|---|---|---| +| the targets | `targets`, `toolchain`, `sysroot` | which triples it serves, the toolchain they use, a prebuilt target base | +| the layers | `[platform.layers]` | the package for `c-abi`, `c++-abi`, `compiler-runtime`, `kernel-abi`, or `false` | +| the tools | `[platform.tools]` | programs built for this machine, or found (`{ program = "" }`) | +| the compile | `[platform.build]`, `[platform.build.'cfg(…)']` | flags for every unit of the target | +| the link | `[platform.link]` | `driver`, `flags`, `start`, `script`, `inputs`, the names `exe` and `shared`, the `seal` steps | +| the run | `runner`, `runners` | how `mcpp run` and `mcpp test` start an artifact, and named device actions | +| the carrier | `carrier` | the program `mcpp pack` builds of a loaded artifact for a system: the self-contained program, a universal package's runner, a single file | +| the package | `universal` | a pack of any target covers all of them, in one package ([10](10-pack-and-release.md)) | +| the code | `plugin` | a build program run in the platform phase, for what depends on the graph | +| the group | `[platform.versions]` | one version of each package of a family | +| the host part | `[platform.hosted]` | what a host triple takes when it selects the platform | + +## Overriding part of a platform + +A project overrides a field under `[target.]` with the platform's own +key name. `cfg(platform = "openkal")` matches the platform whatever the +architecture: + +```toml +[target.'cfg(platform = "openkal")'.layers] +c-abi = { path = "../openkal-musl" } # a working tree in place of the release + +[target.'cfg(platform = "openkal")'.tools] +okld = { path = "../openkal-image/tools" } + +[target.'cfg(platform = "openkal")'] +runner = ["my-okrun", "{}"] +``` + +| Field | Rule | +|---|---| +| compile flags, `link.flags` | appended after the platform's; a convention cannot be removed | +| `link.inputs`, `link.seal` | replaced whole | +| `layers`, `tools`, `runners` | replaced key by key; ` = false` leaves the layer out | +| `runner` | the argv is replaced; `host` and `exclusive` are kept unless stated | +| every other field | replaced | + +The same tables are accepted in `~/.mcpp/config.toml` for one machine. A layer +a package of the graph already provides is left to that package and reported +`graph`. The package of the C++ run-time also provides the compiler run-time, so +`"c++-abi" = false` takes effect only with `compiler-runtime = false` beside it. + +## Selecting a platform, or none + +The first of these that answers selects the target's platform: + +| Source | Spelling | +|---|---| +| the command line | `--platform openkal-platform@0.17.3`, `--platform path:../openkal-platform`, `--platform none` | +| the environment | `MCPP_TARGET_X86_64_OPENKAL_PLATFORM=…` | +| the project | `[target.] platform = "openkal-platform@0.17.3"`, or `{ path = "…" }` | +| this machine | the same key in `~/.mcpp/config.toml` | +| the target table | the platform column (`mcpp toolchain list`) | + +`platform` is not accepted under a `cfg(platform = …)` selector. `none` builds +with the engine's conventions for the target and nothing else. + +## The host graph's platform + +Build programs and the tools the build runs are compiled for this machine by +its own conventions. `[host] platform` selects a platform for them instead: + +```toml +[host] +platform = "openkal-platform@0.17.3" +``` + +``` + Host platform openkal-platform 0.17.3 (mcpp.toml:2): build programs are x86_64-openkal images run by okrun +``` + +Every build program then compiles as an ok image of this machine's +architecture, with one toolchain on Linux, macOS and Windows, and runs through +the platform's runner. The platform's own tools are still built natively. +`--host-platform` and `MCPP_HOST_PLATFORM` select it for one run, and +`[host]` in `~/.mcpp/config.toml` for one machine. + +## The hosted arrangement + +A host triple that selects a platform takes only the platform's +`[platform.hosted]` part: its toolchain and its layers. With openkal-platform +that is the family's C library and C++ run-time over the system's own openkal +implementation: + +```toml +[target.'cfg(any(linux, macos, windows))'] +platform = "openkal-platform@0.17.3" +``` + +On Windows the arrangement is the GNU ABI's row (`--target x86_64-windows-gnu`); +a triple outside `[platform.hosted] targets` is refused, naming the row of the +same system and architecture that it serves. + +## The version group + +`[platform.versions]` names one version of each package of a family whose +packages pin one another exactly. A graph that holds another version of one of +them is refused before anything compiles: + +``` +error: openkal-musl 0.22.2 is in this build, and the platform openkal-platform 0.17.3 is a version group that names openkal-musl 0.22.6. + requested by: own + The family's packages pin one another exactly, so two versions of one of + them cannot be linked into one program. Either select the platform release + of that version ([target.] platform = "openkal-platform@"), or write + openkal-musl = "0.22.6" where it is requested. +``` + +A package taken from a directory or a repository is not bound by the group. + +## Writing a platform package + +A platform package is a package with a `[platform]` section. openkal-platform, +abridged: + +```toml +[package] +namespace = "mcpplibs" +name = "openkal-platform" +version = "0.17.3" +mcpp = ">=2026.10.27.1" + +[platform] +targets = ["x86_64-openkal", "aarch64-openkal"] +toolchain = "llvm@23.1.3" +runner = { cmd = ["okrun", "{}"], host = "same-arch" } +carrier = { package = "openkal-run@0.1.3", section = ".oke", main = "carrier/main.cpp" } +universal = true +plugin = "build.mcpp" + +[platform.layers] +compiler-runtime = "openkal-llvm-runtime@0.16.6" +"c++-abi" = "openkal-llvm-runtime@0.16.6" +c-abi = { name = "openkal-musl", version = "0.22.6", default-features = false, features = ["posix"] } + +[platform.tools] +okld = "openkal-tools@0.1.3" +okrun = "openkal-tools@0.1.3" + +[platform.build] +cxxflags = ["-fPIC", "-femulated-tls", "-fvisibility=hidden", "-fasynchronous-unwind-tables"] + +[platform.build.'cfg(arch = "aarch64")'] +cxxflags = ["-ffixed-x18", "-mno-outline-atomics"] + +[platform.link] +driver = "lld" +start = "ok_main" +flags = ["-shared", "-z", "max-page-size=65536", "-z", "now", "--gc-sections", "--icf=safe", "--eh-frame-hdr"] +inputs = [{ from = ["okld", "note-source"], as = "ok-notes.S" }] +exe = "{name}.oke" +shared = "lib{name}.okso" +seal = [["okld", "note", "{}"], ["okld", "check", "{}", "--with", "{needed}"]] +``` + +- **Programs are named, not located.** `okld` in `inputs` and `seal` is found among + the programs the graph built for this machine, then in declared payloads, then + on `PATH`. +- **`{}` is the artifact.** In a seal step `{needed}` expands to the shared images + the artifact needs; a preceding `--with` is repeated before each. +- **A seal step that fails publishes nothing.** The artifact published by the + last successful build stays in place. +- **What depends on the graph is code.** `plugin = "build.mcpp"` runs in the + platform phase (`mcpp::phase()` is `"platform"`) and may state flags, link + fields and runners (`mcpp::link_driver`, `mcpp::link_input_from`, + `mcpp::artifact_name`, `mcpp::seal`, `mcpp::run_host`); any other directive is + refused, except the package formats the platform provides (`pack-format` and + `artifact` actions, which make e.g. an `okb` from the staged tree). A + board-support package's build program states the same fields in its build + phase ([34](34-authoring-a-bsp.md)). +- **The carrier's entry reads what it carries.** mcpp places the carried bytes + between `mcpp_carried_begin` and `mcpp_carried_end` (the entry's name at + `mcpp_carried_name`): nothing, when the program is a universal package's + runner and runs the image its first argument names; the image itself; or a + stored zip of the images (`PK\x03\x04`, each aligned to 64 KiB) for a + single file of an image that names others ([SPEC-013](specs/platforms.md) + §11). + +During development a project selects the working tree with +`--platform path:` or `platform = { path = "" }`. `mcpp test` in the +platform package, with no `--target`, builds its tests for each of its +`targets` with the package as the platform, runs those of this machine's +architecture, and reports the others as built and not run. + +## Inspecting a configuration + +```sh +mcpp self env --target x86_64-openkal # toolchain, platform, tools, runner +mcpp self env --target x86_64-openkal --format json +mcpp self env --host # the host graph, its runner and tools once its build programs were built +mcpp toolchain list # the PLATFORM column +mcpp toolchain install --target x86_64-openkal # fetch and build what the target needs +``` + +`--message-format json` carries `platform` and `hostPlatform` in the `plan` +event and in `result.json` ([50](50-machine-output.md)). + +## Refusals + +| Reason | Cause | Remedy | +|---|---|---| +| `platform-not-a-platform` | the selected package has no `[platform]` | select a platform package, or `none` | +| `platform-target-unserved` | the platform does not serve the triple | build for one of its `targets`, or for the host row it names | +| `platform-tool-absent` | a program of the link or the seal is neither built nor found | name its package under `[target.'cfg(platform = "…")'.tools]` | +| `platform-version-group` | another version of a family package is in the graph | select the platform release of that version, or pin the group's version | +| `platform-phase-directive` | the plugin stated a directive the platform phase does not take | state it from the package's own build program | +| `lld-required-absent` | the platform links with `ld.lld` and the toolchain has none | use the platform's toolchain | + +## Current limitations + +- A platform states one value per field; a field that differs per architecture + is written under `[platform.build.'cfg(arch = "…")']` for flags only. +- The host graph's platform is selected per run or per project; its default is + the host conventions. +- `[host]` accepts `platform` only. diff --git a/docs/41-devices.md b/docs/41-devices.md index 9328f7c85..4dcbb2672 100644 --- a/docs/41-devices.md +++ b/docs/41-devices.md @@ -94,6 +94,22 @@ a dependency supplied, and the override is reported rather than applied in silence. Exactly one dependency may supply a given name; a second is an error naming both packages. +A runner may also be a table (2026.10.27.1+), which states where the artifact +runs and whether runs overlap: + +```toml +[target.'cfg(platform = "openkal")'] +runner = { cmd = ["okrun", "{}"], host = "same-arch", exclusive = false } +``` + +`host = "same-arch"` runs an artifact only on a machine of its architecture, and +an artifact of another architecture is refused naming `run_under`; `"any"` is a +runner that reaches the artifact from any machine. A target's platform supplies +its default runner ([35](35-platforms.md)); a dependency's build program, this +machine's `config.toml`, the project and `MCPP_TARGET__RUNNER` each +replace it in that order, and an argv alone keeps the platform's `host` and +`exclusive`. + ## What a runner receives A runner receives an argv — its own tokens, with the artifact's path appended @@ -115,7 +131,7 @@ The first field is the destination relative to the artifact's directory, with `/` separators; it begins with `../` for a test program discovered in a subdirectory. The second is the absolute path of the file in the output tree. A TAB separates them. The file exists for every runner invocation and is empty -when there is nothing to carry; a distributable run with `--format` receives +when there is nothing to carry; a distributable run with `--pkg-format` receives an empty file, because the distributable holds its own files. A runner that executes the artifact where it lies ignores the variable. A @@ -124,7 +140,7 @@ copies each listed file to the destination beside the moved artifact. ## Running a distributable -`mcpp run --format ` packs the distributable `` and runs it. Without +`mcpp run --pkg-format ` packs the distributable `` and runs it. Without `--runner`, it is reached by the runner named `` when the graph or the manifest supplies one, and by the default runner otherwise: @@ -133,12 +149,12 @@ mcpp::runner("app", ""); // a package names a runner after its fo ``` ```bash -mcpp run --format app # through the runner named `app` -mcpp run --format app --runner other # a typed --runner still wins +mcpp run --pkg-format app # through the runner named `app` +mcpp run --pkg-format app --runner other # a typed --runner still wins ``` A distributable that is a directory, such as an application bundle, is not -executed directly. When no runner reaches it, `mcpp run --format ` refuses +executed directly. When no runner reaches it, `mcpp run --pkg-format ` refuses before starting anything, with exit status `126`, and names the runner `` and the manifest table that would declare it. diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 19d9c4e4e..e988d6cc4 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -102,10 +102,11 @@ mcpp --format json streaming case and is **not** accepted — asking for it is an error, not a silent fallback. -A command whose `--format` already names its **product** asks for machine -output with `--message-format json` instead, as `mcpp test` does. `mcpp pack ---format` names the package format (`tar`, `dir`, `msi`), so its report is -`mcpp pack --message-format json` (2026.9.16.1+). The shape follows the kind: +A command that builds asks for machine output with `--message-format json` +instead, as `mcpp test` does: `build`, `test`, `run` and `pack`. The package +format of `mcpp pack` and `mcpp run` is `--pkg-format` (`tar`, `zip`, `dir`, `single-file`, `msi`; +2026.10.27.1+; `--format` there is the earlier spelling and warns), so the +report of a pack is `mcpp pack --message-format json` (2026.9.16.1+). The shape follows the kind: `mcpp test` streams a record per test because tests finish over time, and `mcpp pack` prints one envelope because a pack has one result. `mcpp build --message-format json` (2026.10.26.1+) streams events while it builds and ends @@ -253,8 +254,8 @@ and only the first belongs to the program: |---|---| | `0`–`124` | the program ran; this is its own status, passed through unchanged. The exception is `101`, below | | `101` | `mcpp run` could not build the program: its planning or its build failed, and nothing was started. This is the status Cargo gives a failed `cargo run` build | -| `125`–`127` | the spawn was attempted and refused — `127` not found, `126` found but not executable, `125` anything else; `126` also answers `mcpp run --format ` for a distributable that is a directory and meets no runner, refused before the spawn with the same meaning (2026.9.14.2+) | -| `2` | mcpp refused the request before building anything, or after building it and before starting anything: a usage error, `--format` with `--no-runner`, a program or a runner that the project does not declare | +| `125`–`127` | the spawn was attempted and refused — `127` not found, `126` found but not executable, `125` anything else; `126` also answers `mcpp run --pkg-format ` for a distributable that is a directory and meets no runner, refused before the spawn with the same meaning (2026.9.14.2+) | +| `2` | mcpp refused the request before building anything, or after building it and before starting anything: a usage error, `--pkg-format` with `--no-runner`, a program or a runner that the project does not declare | Until 2026.9.30.2 a failed build exited `1` and a failed planning `2`, so that a compile error and a program that returns `1` could not be told apart by a @@ -337,6 +338,36 @@ That is why this exists at all: without it a client has to reimplement mcpp's home resolution, including the part where the `mcpp` on `PATH` may be an xlings shim rather than the real binary. +**One configuration** *(2026.10.27.1+)*. With `--target ` (or `--host` +for the host graph) `self env` prepares that configuration, without building, +and reports it; in a project it is the project's configuration, elsewhere an +empty program's under the home's cache. `--platform` and `--host-platform` +select as they do for a build ([35](35-platforms.md)). + +```jsonc +{ + "triple": "x86_64-openkal", + "graph": "target", // or "host" + "toolchain": {"compiler": "/…/xim-x-llvm/23.1.3/bin/clang++", "origin": "platform"}, + "platform": {"name": "mcpplibs.openkal-platform", "version": "0.17.3", "source": "built-in", + "hosted": false, "fields": {"runner": {"value": ["okrun", "{}"], "source": "platform"}}}, + "tools": {"okld": "/…/okld", "okrun": "/…/okrun"}, + "runner": {"argv": ["okrun", "{}"], "host": "same-arch", "exclusive": false, + "program": "/…/okrun", "source": "platform"}, + "runUnder": [] +} +``` + +`platform` is `null` when no platform is selected; its shape is the `plan` +event's (§9). `runner.program` is the platform's own tool when the runner names +one, else the program found on this machine. For `--host` the host platform's +`runner` and `tools` are reported once the project's build programs have been +built under it. A reason the project's sources would be refused for (an `import +std` with no C++ run-time in the graph) is a `warning` diagnostic with the code +`MCPP_CONFIGURATION_SOURCES_REFUSED`, and the configuration is still reported. +A configuration that cannot be prepared carries `MCPP_CONFIGURATION_FAILED` and +no `data`. + `defaultToolchain` is the toolchain a build with nothing configured resolves on this host: the one answer the first run installs, and the value the tables of docs/01 and docs/20 are checked against on each CI host. On Windows it depends @@ -454,6 +485,8 @@ mcpp toolchain list --format json 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 (`mcpplibs.openkal-platform@0.17.3`), 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 @@ -569,7 +602,11 @@ a program classifying the outcome reads `reason`: | `std-module-precompile` | the standard library's module could not be precompiled for this configuration | | `msvc-redist-unavailable` | an explicit `cxx_runtime = "toolchain-coupled"` on an MSVC-ABI row whose toolset has no redistributable directory to stage *(2026.9.28.1+)* | | `crt-declared-under-host-coupled` | a file of the MSVC C++ runtime is declared beside the program while its contract is host-coupled, under which the system's runtime serves it and no copy is placed *(2026.9.28.2+)* | -| `image-tool-absent` | a build of `-openkal` links an image and finds no `okld`, the program of the graph that notes and checks an ok image *(2026.10.26.2+)* | +| `platform-not-a-platform` | the package selected as the target's platform has no `[platform]` section *(2026.10.27.1+)* | +| `platform-target-unserved` | the selected platform does not serve the target: the triple is not among its `targets`, and a host triple is not among its `[platform.hosted] targets` *(2026.10.27.1+)* | +| `platform-tool-absent` | a program the platform's link or seal runs (`okld` for an ok image) is neither built by the graph nor found on this machine *(2026.10.27.1+; `image-tool-absent` in 2026.10.26.2)* | +| `platform-version-group` | a package of the platform's version group is in the graph at another version *(2026.10.27.1+)* | +| `platform-phase-directive` | a platform's plugin stated a directive its phase does not take *(2026.10.27.1+)* | | `other` | a refusal whose branch has not been given a token yet | **One token is also printed by `mcpp build` itself.** @@ -672,7 +709,7 @@ fails there still fails the build. ### `mcpp.pack` — the products of a pack *(mcpp 2026.9.16.1+)* ``` -mcpp pack [target] [--format ] [--target ...] --message-format json +mcpp pack [target] [--pkg-format ] [--target ...] --message-format json ``` The envelope is printed once, after the command finishes; every human line goes @@ -681,8 +718,10 @@ to stderr, including what the build programs and tools the pack starts print. | field | | |---|---| -| `artifacts` | one record per produced artifact: `path` (absolute), `type` (`file` or `directory`), `format` (the `--format` value, `tar` when omitted) and `targets` (the canonical triple of each leg that went into it). A dispatched format reports the terminal outputs of the actions the request introduced; a several-`--target` Android pack reports one artifact whose `targets` lists every leg | +| `artifacts` | one record per produced artifact: `path` (absolute), `type` (`file` or `directory`), `format` (the container: the `--pkg-format` value; when none is given, `zip` for a Windows program and `tar` otherwise), `targets` (the canonical triple of each leg that went into it) and `sha256` (a file's digest; `null` for a directory; 2026.10.27.1+). A dispatched format reports the terminal outputs of the actions the request introduced; a several-`--target` Android pack reports one artifact whose `targets` lists every leg | | `stage` | the tree the artifact was made from: `dir`, `manifest` (the stage manifest below) and `closure` (`walked` or `not-walked`); `null` for a library package, when no tree was staged, and for a pack of several members | +| `linkage`, `mode`, `pkgFormat` *(2026.10.27.1+)* | the three choices of the pack ([10](10-pack-and-release.md)): `linkage` is `static`, `shared` or `declared`; `pkgFormat` is the `format` of the artifacts (`tar`, `zip`, `dir`, `single-file`, or a provided format) | +| `legs`, `systems` *(2026.10.27.1+)* | the canonical triple of each leg (a universal package lists every target of its platform), and the `-` of each runner a self-contained universal package carries | | `stages` *(2026.10.1.1+)* | present only for a pack of several members (`--workspace`, or `-p` repeated): one record per member in `[workspace] members` order, with `member` (the qualified package name) and the `dir`, `manifest` and `closure` of `stage` | For a pack of several members `artifacts` lists every member's artifacts, in the @@ -842,7 +881,7 @@ Standard output is NDJSON. Each line but the last is an event: | event | carries | |---|---| -| `plan` | `configuration`: the configuration whose build directory the build has entered | +| `plan` | `configuration`: the configuration whose build directory the build has entered; `platform` and `hostPlatform` (2026.10.27.1+) are the platforms of the target and host graphs, or `null` | | `waiting` | `holder` (`pid`, `command`, `client`, `startedAt`) and `waitedMs`: another build holds this build directory, and this one waits for it (§11) | | `progress` | `finished`, `total`: ninja's count of steps; a build may run more than one ninja pass, and each restarts the count | | `diagnostic` | `diagnostic`: one diagnostic of a compiler, from the output of a failed step, in the shape of `diagnostics[]` of the record below | @@ -870,7 +909,7 @@ terminal is therefore visible to an IDE that reads the record, and the reverse. | `pid` | the process that wrote the record | | `startedAt`, `finishedAt` | milliseconds since the epoch; `finishedAt` is `null` while `running` and after `cancelled` | | `status` | `running`, `ok`, `failed`, or `cancelled` | -| `configuration` | `triple`, `profile`, `fingerprint`, `outputDir` | +| `configuration` | `triple`, `profile`, `fingerprint`, `outputDir`, `platform`, `hostPlatform` (2026.10.27.1+) | | `compileDatabase` | the configuration's `compile_commands.json`, or `null` | | `artifacts` | `name`, `kind` (`bin`, `test`, `shared`, `static`), `path`, for a runnable product `launch`, and `entry` when the program's beginning or its entry symbol was stated | | `diagnostics` | the compilers' diagnostics of a failed build: `severity`, `message`, `path`, `range` | @@ -907,7 +946,7 @@ mcpp run [target] --print-launch Builds the program as `mcpp run` does and prints the envelope instead of starting it. `data` is `program`, `argv`, `cwd`, `env` and `via`, as -`launch` above. `--print-launch` takes no `--format`: on `mcpp run`, `--format` +`launch` above. `--print-launch` takes no `--format`: on `mcpp run`, `--pkg-format` names a distributable. ## 10. Denying effects — `--deny` *(mcpp 2026.10.26.1+)* @@ -981,13 +1020,13 @@ mcpp index list --format json project's `[indices]` entries, when run in a project). The packages are `mcpp search`'s answer. -### `mcpp.pack.formats` — the formats `mcpp pack --format` accepts +### `mcpp.pack.formats` — the formats `mcpp pack --pkg-format` accepts ``` mcpp pack --list-formats ``` -`data.formats` lists the engine's own formats (`tar`, `dir`; `builtin: true`) +`data.formats` lists the engine's own formats (`tar`, `zip`, `dir`, `single-file`; `builtin: true`) and each format a package of the resolved graph provides (`builtin: false`, with `providers`). The graph is planned in a directory of the mcpp home, so nothing is written into the project. diff --git a/docs/90-build-from-source.md b/docs/90-build-from-source.md index 9af55bba1..4892ab520 100644 --- a/docs/90-build-from-source.md +++ b/docs/90-build-from-source.md @@ -81,19 +81,19 @@ src/ └── scaffold/ `mcpp new` and templates tests/ -├── unit/ 154 C++ tests, discovered by `mcpp test` -└── e2e/ 635 shell scripts against a real binary +├── unit/ 156 C++ tests, discovered by `mcpp test` +└── e2e/ 645 shell scripts against a real binary ``` ## Test Organization Two layers, and they answer different questions. -**Unit tests** (`tests/unit/`, 154 files) are C++ programs `mcpp test` +**Unit tests** (`tests/unit/`, 156 files) are C++ programs `mcpp test` discovers. They exercise a module's contract with no binary and no filesystem state. -**End-to-end tests** (`tests/e2e/NN_.sh`, 635 files) run a real `mcpp` +**End-to-end tests** (`tests/e2e/NN_.sh`, 645 files) run a real `mcpp` binary against a real project. `run_all.sh` is the CI entry point. `mcpp test` does **not** run them. diff --git a/docs/README.md b/docs/README.md index 6c74499d3..54396ef5c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,11 +28,11 @@ anything a reader needs is written here or in a specification instead. | write a program | [01](01-getting-started.md), [04](04-mcpp-toml.md) §1 | [`01-hello`](../examples/01-hello/), [`02-with-deps`](../examples/02-with-deps/) | | write a library others import | [11](11-publishing-a-library.md), [06](06-features-and-capabilities.md), [04](04-mcpp-toml.md) §2.4 | [`04-workspace`](../examples/04-workspace/), [`11-features`](../examples/11-features/) | | publish it | [10](10-pack-and-release.md), [11](11-publishing-a-library.md), [12](12-binary-distribution.md) | [`03-pack-static`](../examples/03-pack-static/), [`05-lib-distribution`](../examples/05-lib-distribution/) | -| build for another machine | [21](21-the-target-triple.md), [24](24-openkal-cross.md), [40](40-baremetal.md) | [`06-openkal-cross`](../examples/06-openkal-cross/), `mcpp new … --template riscv-virt-rt` | +| build for another machine | [21](21-the-target-triple.md), [24](24-openkal-cross.md), [35](35-platforms.md), [40](40-baremetal.md) | [`06-openkal-cross`](../examples/06-openkal-cross/), `mcpp new … --template riscv-virt-rt` | | use a GPU or an accelerator | [42](42-heterogeneous-builds.md), [41](41-devices.md) | [`09-heterogeneous`](../examples/09-heterogeneous/), starting at [`boundary/`](../examples/09-heterogeneous/boundary/) | | add a rule, a language or a generator | [31](31-authoring-a-rule-package.md), [30](30-build-mcpp.md) | [`08-build-rules`](../examples/08-build-rules/), [`12-a-new-device-language`](../examples/12-a-new-device-language/) | | add a package to the index | [11](11-publishing-a-library.md), [SPEC-001](specs/package-identity.md) | [09](09-commands-by-scenario.md) — the publishing scenarios | -| package a tool, a driver or a board for others | [32](32-authoring-a-payload.md), [33](33-authoring-an-adapter.md), [34](34-authoring-a-bsp.md) | the descriptors in `xim-pkgindex` and `mcpp-index` | +| package a tool, a driver, a board or a platform for others | [32](32-authoring-a-payload.md), [33](33-authoring-an-adapter.md), [34](34-authoring-a-bsp.md), [35](35-platforms.md) | the descriptors in `xim-pkgindex` and `mcpp-index` | | change mcpp itself | [90](90-build-from-source.md), [92](92-release.md), [51](51-supported-versions.md) | — | Lessons also arrive as project templates, which a package ships and `mcpp new @@ -91,6 +91,7 @@ Within a part the order is a reading order, not an alphabet. - [32 — Authoring a Payload](32-authoring-a-payload.md) — a tool or a prebuilt library mcpp installs - [33 — Authoring a Runtime Adapter](33-authoring-an-adapter.md) — reaching a library the host supplies - [34 — Authoring a Board-Support Package](34-authoring-a-bsp.md) — a board, and the way in +- [35 — Platforms](35-platforms.md) — the configuration of a class of targets, as a package ### 4x — Devices and accelerators @@ -131,6 +132,7 @@ token in front of a reader to the chapter that owns it. | `min_api_level` | [04](04-mcpp-toml.md) | `macos_deployment_target` | [04](04-mcpp-toml.md) | | `[target..targets.] kind` | [22](22-target-side.md) | `[index.repos.]` in `config.toml` | [32](32-authoring-a-payload.md) | | `[test] discover` | [04](04-mcpp-toml.md), [08](08-testing.md) | `MCPP_RUNTIME_FILES` | [41](41-devices.md) | +| `[platform]`, `[host]`, `cfg(platform = …)` | [35](35-platforms.md) | `[target.] platform`, `layers`, `tools`, `link`, `carrier` | [35](35-platforms.md) | **Commands** @@ -140,6 +142,7 @@ token in front of a reader to the chapter that owns it. | `new`, `new --template` | [01](01-getting-started.md) | `add`, `update`, `why` | [05](05-dependencies.md) | | `pack` | [10](10-pack-and-release.md) | `publish`, `emit xpkg`, `xpkg parse` | [11](11-publishing-a-library.md) | | `toolchain` | [20](20-toolchains.md) | `clean`, `cache`, `index`, `self …` | [09](09-commands-by-scenario.md) | +| `--platform`, `--host-platform`, `self env --target` | [35](35-platforms.md) | `pack --pkg-format`, `--linkage`, `--merge` | [10](10-pack-and-release.md) | **Concepts** @@ -151,6 +154,7 @@ token in front of a reader to the chapter that owns it. | an island, a seam, `accel`, `MCPP_EXPORT_C` | [42](42-heterogeneous-builds.md) | a descriptor, an index | [11](11-publishing-a-library.md) | | an ABI tag, a prebuilt artifact | [12](12-binary-distribution.md) | exit codes, JSON output | [50](50-machine-output.md) | | an `xim:` payload, `[xlings.workspace]` | [32](32-authoring-a-payload.md), [23](23-the-project-environment.md) | a `compat:` adapter, `runtime.library_dirs` | [33](33-authoring-an-adapter.md) | +| a platform, a platform package, a version group | [35](35-platforms.md) | a universal package, a single-file package, `mcpp-package.json` | [10](10-pack-and-release.md) | ## Specifications @@ -171,3 +175,5 @@ downstream tooling. - [SPEC-010 — Home selection and initialization](specs/home-initialization.md) - [SPEC-011 — Concurrent runs: the build directory lock, whole outputs, optimistic writes, cancellation](specs/concurrency.md) - [SPEC-012 — Output for people: two streams, two media, the kinds of line, colour](specs/human-output.md) + - [SPEC-013 — Platforms: the configuration of a class of targets, its selection, merge, graph completion, link, seal, runner and carrier](specs/platforms.md) + - [SPEC-014 — Packaging and distribution: link, closure and container, legs, the staged tree, universal and single-file packages](specs/packaging.md) diff --git a/docs/specs/README.md b/docs/specs/README.md index fb202045d..a5f49f390 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -42,6 +42,8 @@ | [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.0 | 2026-10-28 | mcpp >= 2026.10.27.1 | +| [SPEC-014](packaging.md) | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与 `mcpp-package.json`,通用包、单文件、签名、压缩、合并与分发描述 | 评审中 v1.0 | 2026-10-28 | mcpp >= 2026.10.27.1;逐条标注 | ## 文档约定 diff --git a/docs/specs/build-plugins.md b/docs/specs/build-plugins.md index 538e15430..d71cb18ab 100644 --- a/docs/specs/build-plugins.md +++ b/docs/specs/build-plugins.md @@ -5,9 +5,9 @@ | 规范编号 | SPEC-007 | | 标题 | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | | 状态 | 草案 v0.9 | -| 版本 | 0.10 | -| 最后修改 | 2026-10-27 | -| 对应实现 | 逐条标注。未注明版本的「已实现」条款对应 mcpp >= 2026.9.26.1;注明 mcpp#702 的条款对应 mcpp >= 2026.9.26.2;注明 mcpp#707、#708、#709、#711 的条款对应 mcpp >= 2026.9.27.1;注明 mcpp#723 的条款对应 mcpp >= 2026.9.28.1;注明 mcpp 2026.9.28.2 的条款对应该版本;§9 与注明 mcpp#734 的条款对应 mcpp >= 2026.9.28.3;注明 mcpp#778 的条款对应 mcpp >= 2026.10.10.1;§10 对应 mcpp >= 2026.10.11.1 | +| 版本 | 0.11 | +| 最后修改 | 2026-10-28 | +| 对应实现 | 逐条标注。未注明版本的「已实现」条款对应 mcpp >= 2026.9.26.1;注明 mcpp#702 的条款对应 mcpp >= 2026.9.26.2;注明 mcpp#707、#708、#709、#711 的条款对应 mcpp >= 2026.9.27.1;注明 mcpp#723 的条款对应 mcpp >= 2026.9.28.1;注明 mcpp 2026.9.28.2 的条款对应该版本;§9 与注明 mcpp#734 的条款对应 mcpp >= 2026.9.28.3;注明 mcpp#778 的条款对应 mcpp >= 2026.10.10.1;§10 对应 mcpp >= 2026.10.11.1;R9.10 与协议 17 对应 mcpp >= 2026.10.27.1 | | 相关设计文档 | `.agents/docs/2026-09-26-compile-database-and-issue-699-design.md`(§5)、`.agents/docs/2026-09-28-ecosystem-design-and-optimisation-plan.md`(WS1、WS3)、`.agents/docs/2026-09-28-build-cost-foreign-toolsets-and-library-surface-design.md`(§3、§4)、`.agents/docs/2026-10-10-one-source-one-meaning-design.md`(§4)、`.agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md` | | 相关 issue | mcpp#699、mcpp#701、mcpp#702、mcpp#703、mcpp#707、mcpp#708、mcpp#709、mcpp#711、mcpp#734、mcpp#755、mcpp#778、mcpp#797 | | 使用文档 | [docs/30 - build.mcpp](../30-build-mcpp.md)、[docs/31 - 编写规则包](../31-authoring-a-rule-package.md) | @@ -258,6 +258,7 @@ | 14 | 2026.9.28.3 | `mcpp.core`、构建信息(R9.3)、`mcpp::report`(R9.4) | | 15 | 2026.10.1.3 | 来源(R6.5、R6.6、R9.9):`xpkg_source`、`xpkg_program`、`xpkg_request`、`xpkg_pending`、`phase`、`decision`、`toolchain` | | 16 | 2026.10.26.2 | 镜像入口:`link_start`(`mcpp:link-start=`,docs/04 `start`) | + | 17 | 2026.10.27.1 | 平台的链接与运行器字段(SPEC-013 §12):`link_driver`、`link_input`、`link_input_from`、`artifact_name`、`seal`、`run_host`;平台阶段(`mcpp::phase()` 为 `"platform"`) | - **R9.3** 构建信息以事实陈述解析出的工具链,不针对任何外部构建系统:`tool(role)`(这一行的 工具)、`abi_tool(role)`(目标 ABI 的原生工具,MSVC ABI 上为工具集的 `cl`、`link`、`lib`、 @@ -289,6 +290,13 @@ 提供构造这些陈述的函数。编译并运行构建程序的是 bootstrap 工具链,因此一个无法使用的 自定义工具链**必须**仍能让构建程序运行并报告原因。(**已实现**,mcpp 2026.10.1.3) +- **R9.10** 平台包的 `[platform] plugin` 在平台阶段运行(`mcpp::phase()` 为 `"platform"`,`MCPP_PHASE=platform`)。 + 该阶段只能陈述编译与链接标志、`link_script`、`link_start`、协议 17 的指令、`runner` 与命名运行器、 + `run_exclusive`、平台提供的包格式(`pack-format` 与 `role = "artifact"` 的 action,SPEC-014 §6),以及重跑 + 条件、警告与诊断;其他指令(含其他角色的 action)**必须**被拒绝(拒绝码 `platform-phase-directive`), + 消息点名第一条不属于该阶段的指令。陈述的内容在 SPEC-013 的 Program 层次合并;协议 17 的指令也可以由 + 任何构建程序在构建阶段发出。(**已实现**,mcpp 2026.10.27.1) + ## 10. 构建程序的库与构建插件(mcpp#797) - **R10.1** 构建程序的依赖是其包 `[build-dependencies]` 中请求库的条目(SPEC-004 §12 第 5 条), @@ -330,3 +338,4 @@ | 0.2 | 2026-09-26 | 随 mcpp 2026.9.26.2 落地:R1.3 的警告、R2.1 的 `runtime_search_dir`、R2.4、R3.3 的 `prepare`(目录须含文件;链接边等待所有 `prepare`)、R3.5、R3.6、R4.1、R4.3、R5.2、R5.3 标为已实现。 | | 0.9 | 2026-10-11 | 随 mcpp 2026.10.11.1(mcpp#797):新增 §10 构建程序的库与构建插件(R10.1–R10.6,P1–P5);变更记录移为 §11。 | | 0.10 | 2026-10-27 | 随 mcpp 2026.10.26.2(mcpp#845):R9.2 的协议表增加协议 16(`link_start`,镜像入口)。 | +| 0.11 | 2026-10-28 | 随 mcpp 2026.10.27.1(mcpp#847):R9.2 的协议表增加协议 17(平台的链接与运行器字段);新增 R9.10 平台阶段(SPEC-013),平台阶段可声明包格式与 artifact action(SPEC-014 §6)。 | diff --git a/docs/specs/manifest-semantics.md b/docs/specs/manifest-semantics.md index 2d2e4fe43..df9fc4415 100644 --- a/docs/specs/manifest-semantics.md +++ b/docs/specs/manifest-semantics.md @@ -5,8 +5,8 @@ | **规范编号** | SPEC-004 | | **标题** | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | | **状态** | **草案(Draft)** | -| **版本** | 1.18 | -| **最后修改** | 2026-10-27 | +| **版本** | 1.19 | +| **最后修改** | 2026-10-28 | | **最低实现版本** | 条件化形状:mcpp **2026.8.29.1**(`[target..build-dependencies]` 起齐备);目标轴:mcpp **2026.9.6.4** | | **作者/维护** | mcpp-community | | **相关设计文档** | `.agents/docs/2026-09-07-mcpp-toml-unified-semantics-design.md`
`.agents/docs/2026-06-04-manifest-schema-ownership.md`
`.agents/docs/2026-09-03-xlings-workspace-as-the-one-table.md`
`.agents/docs/2026-09-25-issue-690-workspace-build-inheritance-consistency.md`
`.agents/docs/2026-09-27-eight-reports-by-home-and-one-optimisation-plan.md`
`.agents/docs/2026-10-05-std-module-pair-msvc-lto-and-export-discovery-design.md`
`.agents/docs/2026-10-06-windows-x86-arch-vocabulary-and-rc-follow-ups-design.md`
`.agents/docs/2026-10-10-one-source-one-meaning-design.md`
`.agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md`
`.agents/docs/2026-10-23-target-base-and-execution-host-design.md`
`.agents/docs/2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md`(§24) | @@ -54,6 +54,7 @@ | 条件 | `[target..
]` | 在哪个目标上要 | | 产物元数据 | `[runtime]`、`[resources]` | 产出的东西是什么 | | 生命周期 | `[hooks]` | 构建前后跑什么 | +| 平台 | `[platform]`、`[host]` | 一类目标的配置;宿主图的平台(SPEC-013) | **状态:已实现。** @@ -66,8 +67,9 @@ **值的层次。** 同一个键的值按四档取第一个:覆盖(命令行与环境变量)、工程(`mcpp.toml`, 含工作空间层)、机器默认(`~/.mcpp/config.toml`,其次系统级 `config.toml`)、引擎默认。 -`[target.<三元组>].sysroot` 与 `[target.<三元组>].run_under` 两个键**可以**同时出现在工程与 -机器默认中;其余键只属于工程。环境变量中空值视为未设置,`none` 表示显式清除。 +`[target.<三元组>].sysroot`、`[target.<三元组>].run_under`、`[target.]` 的平台键(`platform`、 +`layers`、`tools`、`link`、`carrier`、`runner`、`runners`)与 `[host] platform` **可以**同时出现在工程与机器默认中; +其余键只属于工程。平台键的层次另见 SPEC-013 §2。环境变量中空值视为未设置,`none` 表示显式清除。 机器默认**禁止**放宽工程的约束:`requires` 与版本下限照样核对。 **库依赖与工具是两个平面,不是一个。** 库有模块与 ABI,参与解析与链接;载荷是可执行 @@ -196,6 +198,8 @@ feature-deps feature-xlings ← 限定词是门 | `[target..xlings…]` | 目标 | **已实现**(2026.9.6.4) | | `[target.].sysroot` | 目标 | **已实现** | | `[target.].run_under` | 目标(执行宿主) | **已实现**(2026.10.24.1) | +| `[target.]` 的 `platform`、`layers`、`tools`、`link`、`carrier` | 目标 | **已实现**(2026.10.27.1,SPEC-013) | +| `[host] platform` | 宿主 | **已实现**(2026.10.27.1,SPEC-013 §4) | 顶层平台键对象**不是**遗留拼法,它是宿主轴**正确**的写法:工具必须能在这台机器上 执行。`[target.].sysroot` 是目标轴 xpkg 引用的既有先例。 @@ -787,3 +791,4 @@ mcpp 2026.10.10.1,#785、#786;第 14 至 16 条与第 13 条的依赖位置为 m | 1.16 | 2026-10-21 | 新增 §13,明确依赖 EXE、DLL 的资源只进入其所属包的 image,且按计划内目标校验 manifest 冲突(#826)。 | | 1.17 | 2026-10-23 | mcpp 2026.10.24.1:§2 陈述值的四档层次,`sysroot` 与 `run_under` 可出现在工程与机器默认中;§4.2 增加 `run_under`,陈述 `sysroot` 的表写法。 | | 1.18 | 2026-10-27 | mcpp 2026.10.26.2(mcpp#845):新增 §14 程序起点与镜像入口(`kind = "entry"`、`main = { from }`、`start`、`[test] main` 与 `start`、`mcpp:link-start`);§3.1.1 的按行形式可陈述起点。 | +| 1.19 | 2026-10-28 | mcpp 2026.10.27.1(mcpp#847):§2 增加平台平面(`[platform]`、`[host]`),平台键可出现在工程与机器默认中;§4.2 增加 `[target.]` 的平台键与 `[host] platform`(SPEC-013)。 | diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md new file mode 100644 index 000000000..d016911e8 --- /dev/null +++ b/docs/specs/packaging.md @@ -0,0 +1,339 @@ +# SPEC-014:打包与分发 + +| 项 | 值 | +|---|---| +| 规范编号 | SPEC-014 | +| 标题 | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与包元数据,通用包、单文件、签名、压缩、合并与分发描述 | +| 状态 | 评审中 v1.1 | +| 版本 | 1.1 | +| 最后修改 | 2026-10-28 | +| 对应实现 | mcpp >= 2026.10.27.1 | +| 相关设计文档 | `.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`(§2–§7、§14)、`.agents/docs/2026-10-28-platforms-design.md`(§4.7) | +| 相关 issue | mcpp#847、mcpp#849 | +| 相关规范 | [SPEC-007](build-plugins.md)(包格式提供者、artifact action)、[SPEC-011](concurrency.md)(原子发布)、[SPEC-013](platforms.md)(平台、载体) | +| 使用文档 | [10 —— 发布打包](../zh/10-pack-and-release.md) | + +用语按 RFC 2119:**必须 / 禁止**为强制,**应当**为建议,**可以**为可选。 + +## 0. 范围 + +本规范规定 `mcpp pack` 为一个**程序**目标产出可分发物的模型、管线、暂存树、包元数据、容器与接口,以及 +`mcpp run --pkg-format` 与 `mcpp emit xpkg` 读取的部分。库的预编译包(docs/12)不在本规范内。 + +## 1. 模型:三个维度与构建腿 + +一次打包的选择由三个正交维度与一组构建腿组成,每个维度回答一个问题: + +| 维度 | 选项 | 回答的问题 | 归属 | +|---|---|---|---| +| 链接 `--linkage` | `static`、`shared` | 库在程序里,还是在程序旁边 | 程序属性 | +| 闭包 `--mode` | `system`、`vendored`、`self-contained` | 运行的机器要提供什么 | 打包属性 | +| 容器 `--pkg-format` | `dir`、`tar`、`zip`、`single-file`,以及提供者的格式 | 交付成什么 | 打包属性 | +| 腿 `--target`(可重复) | 三元组 | 一次打包覆盖哪些目标 | 打包属性 | + +### 1.1 链接 + +`--linkage`(未给出时取 `[pack] linkage`)是这次打包的构建的依赖链接,优先于 `[build] dependency_linkage` +与构建配置中的值;都未给出时每个库按它自己的声明链接,包元数据记为 `declared`。原生 Linux 目标上 `static` +选择 `-linux-musl` 行(`-linux-gnu` 目标换成对应的 musl 行),全静态程序不论 `--mode` 为何都整体打包; +openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` 加其所需的 `.okso`。 + +### 1.2 闭包 + +| 模式 | 原生目标:包携带 | openkal 目标:包携带 | 运行的机器提供 | +|---|---|---|---| +| `system` | 程序 | 映像 | 全部共享库;openkal 上为加载器与映像所需的 `.okso` | +| `vendored`(缺省) | 程序与非系统共享库 | 映像与其 `.okso` | C 库、C++ 运行时与加载器;openkal 上为加载器 | +| `self-contained` | 程序、全部共享库、C 与 C++ 运行时、加载器 | 映像、`.okso` 与本机程序形式的加载器(载体,SPEC-013 §11) | 无 | + +`bundle-project` 与 `bundle-all` 是 `vendored` 与 `self-contained` 的永久别名。 + +`--mode static` 是 `--linkage static --mode self-contained` 的简写:原生目标上为全静态程序;openkal 目标上为一个静态 +`.oke` 加载体。 + +### 1.3 容器 + +| 值 | 产物 | +|---|---| +| `dir` | 暂存树本身 | +| `tar` | 一个归档,单一顶层目录与归档同名;缺省压缩为 gzip(§5.1) | +| `zip` | 一个 ZIP 归档,单一顶层目录;条目以 DEFLATE 压缩,`--compress none` 时存储(§5.1) | +| `single-file` | 一个可执行文件(§5.2) | +| 其他名字 | 依赖图中的提供者以 `pack-format` 声明的格式(§6) | + +未给出 `--pkg-format` 与 `[pack] format` 时,Windows(PE)程序的容器为 `zip`,其余为 `tar`;显式给出的 `tar` +在任何目标上都写 tar。 + +### 1.4 构建腿 + +`--target` 可以重复。每条腿是一次构建;各腿暂存进同一棵树(§4)。平台以 `[platform] universal = true` 声明其目标 +默认打**通用包**:对该平台任一目标的打包覆盖平台的全部 `targets`,其余的腿在打包过程中构建;`--no-universal` 或 +`[pack] universal = false` 只打所选目标;单文件不是通用包(§5.2)。一个产物为可执行文件的目标,只有在通用包中或在 +产物为共享对象的行(Android 的 `kind = "app"`)上接受多条腿。 + +### 1.5 组合的合法性 + +组合在任何构建开始之前检查;不可能的组合**必须**被拒绝,消息写出冲突的两个选择与出路。 + +| 组合 | 结果 | +|---|---| +| `--pkg-format single-file` 与 `--mode system` | 拒绝:单文件携带它所运行的东西;出路 `--mode vendored` 或 `self-contained` | +| `--compress` 与 `--pkg-format dir` 或提供者的格式 | 拒绝:只有容器被压缩;出路 `--pkg-format tar`、`zip` 或 `single-file` | +| `--compress zstd` 或 `xz` 与 `--pkg-format zip` | 拒绝:zip 的条目为 DEFLATE 或存储;出路 `--compress gzip`、`none`,或 `--pkg-format tar` | +| `--compress gzip` 或 `xz` 与 `--pkg-format single-file` | 拒绝:启动器解压 zstd;出路 `--compress zstd[:]` 或 `none` | +| 需要宿主提供能力的程序(docs/04 §2.11)与 `--mode self-contained` 或 `static` | 拒绝:携带自己 C 库的包不能使用宿主提供的库;出路 `--mode vendored` | +| `cxx_runtime = "toolchain-coupled"` 与 `--mode system` | 拒绝 | +| 产物为可执行文件、非通用包,且多条 `--target` | 拒绝:一个可执行文件属于一个目标 | +| `--systems` 而非 `--mode self-contained` 的通用包或映像的单文件 | 拒绝:只有携带加载器的包与映像的单文件按系统区分 | +| 映像的单文件,`--systems` 中某系统在映像架构上的行为本 mcpp 不构建的行(目标表标为 planned,如 `x86_64-macos`;或未列出,如 `aarch64-windows-gnu`) | 拒绝,写出该系统与行;出路为其余系统组成的 `--systems` | +| 映像的单文件,多个 `--systems` 与 `-o` | 拒绝:每个系统一个文件,`-o` 只能命名一个 | +| 平台未声明 `runner` 时打通用包 | 拒绝:启动器经平台的运行器启动映像;出路 `--no-universal` | +| `--merge` 的输入缺少 `mcpp-package.json`,或名字、版本、平台、链接、模式不同 | 拒绝,写出不一致的字段 | +| `--merge` 的两个输入在同一路径上有摘要不同的文件 | 拒绝,写出该路径、两个摘要与两个输入 | +| 库目标(`lib`、`shared`)与提供者格式 | 拒绝:库包没有单一暂存树 | + +`[pack] compress` 是项目的缺省值:与所选容器不相容时在这次打包中被搁置,只有 `--compress` 选项被拒绝。 + +## 2. 选项与配置 + +### 2.1 选项的全局用法 + +| 选项 | 命令 | 含义 | +|---|---|---| +| `--message-format json` | 构建类命令:`build`、`test`、`run`、`pack` | 机器输出 | +| `--format <编码>` | 报告类命令:`self env`、`toolchain list`、`emit build-database` 等 | 输出编码 | +| `--pkg-format <格式>` | `pack`、`run` | 包格式 | + +`pack` 与 `run` 继续接受 `--format <格式>`,行为与 `--pkg-format` 相同,并以一条警告写出它是旧写法。 + +`pack` 的其余选项:`--linkage static|shared`、`--compress <算法>[:<级别>]`、`--no-universal`、 +`--systems [,...]`、`--merge <包>`(可重复,也可逗号分隔)。 + +### 2.2 `[pack]` + +| 键 | 语义 | +|---|---| +| `default_mode` | 未给 `--mode` 时的闭包模式 | +| `linkage` | 未给 `--linkage` 时的链接(`static`、`shared`) | +| `format` | 未给 `--pkg-format` 时的容器 | +| `compress` | 未给 `--compress` 时的压缩(§5.1) | +| `sign` | 签名步骤的 argv,`{}` 为文件(§7) | +| `universal` | `false` 时等同 `--no-universal` | +| `include`、`exclude` | 工程中按 glob 选出的文件(相对工程根),放在暂存树中同样的相对路径下,一个包只放一份(通用包亦然);`exclude` 去掉其中匹配的文件;`mcpp-package.json` 中角色为 `resource` | +| `strip`、`debug_symbols`、`[pack.bundle-project]` | 既有键 | + +## 3. 管线 + +一次打包依次经过: + +1. **构建各腿**:按维度与腿构建,`release` 为缺省构建配置;通用包的其余腿在暂存之前构建。 +2. **收集闭包**:按 `--mode` 决定每条腿携带的库;闭包记录在暂存清单(docs/50)中,未解析的名字使 `dir` 与 `tar` + 拒绝。 +3. **生成暂存树**:§4 的布局;通用包生成启动器;`self-contained` 加入加载器或载体。 +4. **变换**:strip、重定位、平台的封装(SPEC-013 §9.3)、签名(§7),然后写出 `mcpp-package.json`(§4.5)。 +5. **写容器**:§5;提供者格式在第二遍准备中读取暂存树(§6)。 +6. **报告**:产物、每个文件产物的 sha256、`mcpp.pack` 信封(§9)。 + +`--merge` 是"读入若干包 → 合并 → 描述 → 写容器",不经第 1、2、3 步(§4.6)。 + +## 4. 暂存树与包元数据 + +### 4.1 单腿布局 + +单腿包的布局由目标的二进制格式决定(ELF 的 `bin/` 与 `lib/`,PE 的扁平目录,Mach-O 的 `bin/`,Android 的 `lib/`, +WebAssembly 的同名文件族,ok 映像的 `bin/`),规定见 docs/10。 + +### 4.2 通用包布局 + +``` +--/ + bin/ POSIX 启动器 + bin/.cmd Windows 启动器 + lib//.oke, *.okso 每个架构一组映像 + libexec//-/... 仅 --mode self-contained + mcpp-package.json +``` + +包名带平台名(平台包名去掉 `-platform`),不带系统与架构。`[runtime] deploy` 放置的文件位于每条腿的映像旁边。 + +### 4.3 启动器 + +启动器由引擎生成,内容只来自平台的 `runner` 与腿: + +- 包的根为启动器所在目录的上一级。 +- 架构目录按 `uname -m`(POSIX)或 `%PROCESSOR_ARCHITEW6432%`、`%PROCESSOR_ARCHITECTURE%`(Windows)选择;映射为 + `x86_64`、`amd64`、`AMD64` → `x86_64`,`aarch64`、`arm64`、`ARM64` → `aarch64`;没有对应目录时以状态 126 退出, + 写出本机架构与包含的架构。 +- 运行 `runner.cmd`,与 `mcpp run` 的展开相同:含 `{}` 的词中 `{}` 替换为所选架构目录中的入口映像,没有词含 `{}` + 时映像追加在末尾;其余词原样传递,随后是启动器自己的参数;运行器先在 `libexec//-/` 中找, + 再找 `PATH`。 +- POSIX 启动器把每个词写成单引号字符串,沿符号链接找到自身的位置(不依赖 `readlink -f`);`.cmd` 启动器中 `%` + 写成 `%%`。`.cmd` 的词不能含双引号与换行:含有它们的运行器在打包时拒绝。 + +### 4.4 `--systems` + +`--systems [,...]`(`linux`、`macos`、`windows`;缺省为本机系统)对 `self-contained` 的通用包,为每个系统 × +包的每个架构构建平台的载体(不携带任何内容,即运行器形式,SPEC-013 §11)并放入 +`libexec//-/`;对映像的单文件,为每个系统写一个文件(§5.2)。每个系统的程序在其行 +(`-linux-musl`、`-macos`、`-windows-gnu`)上构建;平台的托管安排(SPEC-013 §5.4)服务该行时以它 +构建,于是同一程序在任何打包的机器上相同,不需要该系统的 SDK 与 C 运行时;Linux 的程序为静态。托管安排不服务该行时 +按惯常行构建,与本机系统和架构相同时取本机的行。本 mcpp 不构建程序的行(目标表标为 planned 或未列出)不放入包,并 +写出该系统与行。 + +### 4.5 `mcpp-package.json` + +暂存树根的 `mcpp-package.json` 是包格式提供者、`--merge` 与 `mcpp emit xpkg` 的输入,每次打包都写出(提供者的 +格式也不例外): + +```json +{ + "schema": 1, + "name": "hello", + "version": "0.1.0", + "platform": "openkal-platform@0.17.3", + "linkage": "declared", + "mode": "vendored", + "legs": ["x86_64-openkal", "aarch64-openkal"], + "systems": [], + "launch": {"runner": ["okrun", "{}"], "entry": {"x86_64": "lib/x86_64/hello.oke", "aarch64": "lib/aarch64/hello.oke"}}, + "files": [{"path": "lib/x86_64/hello.oke", "role": "image", "leg": "x86_64-openkal", "sha256": "..."}] +} +``` + +| 字段 | 语义 | +|---|---| +| `schema` | 本 schema 的版本,当前为 1;读取者拒绝其他版本 | +| `name`、`version` | 包的名字与版本 | +| `platform` | 平台包与版本(`<包>@<版本>`);无平台时为 `null` | +| `linkage` | `static`、`shared`,或 `declared`(每个库按自己的声明) | +| `mode` | §1.2 的模式:`system`、`vendored`、`self-contained`;`--mode static` 的原生包为 `static`,映像为 `self-contained` | +| `legs` | 腿的规范三元组 | +| `systems` | `libexec//` 中加载器的 `-`;无加载器时为空 | +| `launch` | 通用包为 `runner`(平台的 argv)与每个架构的 `entry`;单腿原生包为 `program`(程序的相对路径) | +| `files` | 树中每个文件(符号链接除外):`path`(相对于树根,`/` 分隔)、`role`、`leg`(所属的腿,共用文件为 `null`)、`sha256` | + +`role` 取 `program`、`image`、`library`、`loader`、`launcher`、`resource`、`metadata` 之一:通用包中 `lib//` +下的文件为该腿的 `image`,生成的启动器为 `launcher`;其余按位置与名字判定 —— `libexec/` 下为 `loader`; +`README*`、`LICENSE*`、`HOST-REQUIREMENTS`、`share/licenses/` 为 `metadata`;`.so` 与 `.so.<版本>`、`.dylib`、`.dll`、`.okso` 为 +`library`;`.oke` 为 `image`;`bin/` 下的可执行文件为 `program`;`.cmd`、`.sh` 与树根的可执行文件为 `launcher`; +其他为 `resource`。 + +### 4.6 合并 + +`mcpp pack --merge --merge ...`(也可 `--merge ,`)读入至少两个本命令写出的包(目录,或 `.zip`、 +`.tar`、`.tar.gz`、`.tgz`、`.tar.zst`、`.tar.xz` 归档),按 `mcpp-package.json` 合并为一棵树:名字、版本、平台、 +链接与模式**必须**相同;腿与系统取并集(同一映像在 Linux 与 macOS 上构建出相同字节;Windows 主机上 clang 在 +`__FILE__` 中保留它拼接包含路径时用的 `\`,其映像不同,合并时被拒绝,mcpp#850);每个文件在复制前计算摘要并与记录的摘要比较,不符时拒绝并写出文件、输入 +与两个摘要;同一路径的文件摘要**必须**相同,每个文件只取一份;合并后的树重新描述。`mcpp-package.json` 中的每个 +路径**必须**是包内的规范相对路径。输出**必须**是新的路径,或一个已有的、不是输入之一的本命令写出的归档;目录输出 +只写新目录。 +容器取 `--pkg-format`,否则取 `-o` 的扩展名,否则取第一个输入的容器;`-o` 缺省为当前目录下与树同名的文件。 +不进行任何构建。 + +## 5. 容器 + +### 5.1 压缩 + +`--compress [:]` 与 `[pack] compress` 选择 `tar` 的压缩:mcpp 自己写出 ustar 归档(按 +路径排序;长名与链接目标用 pax 头;属主为 0,时间为 0,模式 0755、0644、0777),以自带的编解码器(zlib、zstd、 +liblzma)压缩,不调用 `tar` 等外部程序;产物扩展名为 `.tar.gz`(缺省)、`.tar.zst`、`.tar.xz` 或 `.tar`。级别 +范围:gzip 1–9,zstd 1–22,xz 0–9;写出的级别照用(含 `xz:0`、`zstd:1`),未写时为 gzip 的缺省(6)、zstd 19、xz 6。`zip` 的条目在变小时以 DEFLATE 压缩(`gzip[:]` 设定级别),否则存储; +`none` 时全部存储。单文件的载荷总是一个 zstd 帧(启动器只解 zstd),`none` 取级别 1。okb 中的映像不压缩(SPEC-013 平台插件的格式规定)。同一棵树的 +归档在每个宿主上字节相同:gzip 成员不带名字与时间、系统字节为 255,zip 条目使用固定日期。 + +**读取归档**(`--merge`、`mcpp emit xpkg --package`)在进程内完成,读取 ustar、pax 与 GNU 长名及首项为 `./` 的 +归档,zip 的每个条目校验 CRC-32。写出任何文件之前拒绝:绝对路径或含 `..` 的条目、位于链接条目之下的条目、重复的 +条目、指向树外的链接(链接目标逐段解析);链接在全部文件之后创建;目标目录必须不存在或为空。 + +### 5.2 单文件 + +| 组合 | 产物 | 运行 | +|---|---|---| +| 原生目标,`vendored` 或 `self-contained` | 引擎的启动器,携带暂存树的一个 zstd 帧 | 首次运行把树解到用户缓存,之后直接执行 | +| 原生 Linux 目标,`--linkage static` | 静态启动器加静态程序 | 同上,不依赖任何系统库 | +| openkal 目标 | 平台的载体,携带映像,或全部映像的一个 zip | 载体经加载器库从内存加载,不写盘 | + +原生目标的启动器: + +1. 源码随引擎提供,每次打包写入 `target/single-file//`(依赖 mcpplibs.os 与 zstd)并为目标构建:Linux 上为 + 静态(musl),macOS 上为 `vendored`,Windows 上为 `system`;它只做校验、解压与执行。树暂存在 + `target/single-file/stage/` 下;产物为 `target/dist/--`(Windows 上带 `.exe`)。 +2. 载荷是暂存树的记录流(`MCPPTREE1\n`,每项为类型字节 `d`/`f`/`x`/`l`、32 位路径长度、路径、64 位数据长度、数据; + 小端,按路径排序)的一个 zstd 帧,帧写明内容大小与内容校验和。 +3. 缓存根:`$XDG_CACHE_HOME`,未设置时 `~/.cache`(Linux);`~/Library/Caches`(macOS);`%LOCALAPPDATA%` + (Windows);其下为 `mcpp/single-file/<载荷的 sha256>/`,该摘要由 mcpp 打包时写入启动器。 +4. 解压时**必须**校验帧的内容校验和,不符时不解开;解到同一目录下的临时名(`.<摘要>-<进程号>`),全部文件写完后 + 写入 `.mcpp-complete` 并以不替换的方式改名到位;**禁止**使用没有 `.mcpp-complete` 的目录;另一次运行先改名到位时 + 使用它的结果;缓存目录在 POSIX 上只对当前用户可写,`mcpp/`、`single-file/` 或摘要目录为符号链接或属于其他 + 用户时启动器拒绝运行。 +5. 以原有的参数执行树中的程序(`bin/`;self-contained 的 Linux 树为根部的包装脚本;Windows 树为根部的 + `.exe`),退出状态即程序的退出状态。 + +openkal 目标的单文件:载体携带映像本身(映像不引用其他映像时),否则携带全部映像的一个 stored zip,各映像数据按 +64 KiB 对齐,`mcpp_carried_name` 为入口映像(SPEC-013 §11);`--systems` 的每个系统(缺省为本机系统)各写一个文件 +`target/dist/---`(Windows 上带 `.exe`)。运行时不写盘。 + +### 5.3 一个文件在多个系统上运行 + +一个文件同时在多个系统与架构上运行(APE)不在本规范内(mcpp#849)。 + +## 6. 包格式提供者 + +一个包以构建程序的 `mcpp::provides_pack_format("")` 声明一个格式,以 `role = "artifact"` 的 action 生成产物 +(SPEC-007)。`--pkg-format ` 找到图中声明该名字的包,在第二遍准备中把暂存树交给它:暂存树目录 +(`mcpp::pack_stage_dir()`、`${mcpp.stage_dir}`)、其中的 `mcpp-package.json`、`MCPP_PACK_*` 环境;它报告的终端 +输出即产物。未知的名字被拒绝,消息列出图中可用的格式。 + +平台插件(SPEC-013 §12)**可以**在平台阶段声明包格式与 artifact action;平台阶段的指令表因此包含 `pack-format` +与 `role = "artifact"` 的 action(其他角色的 action 仍被拒绝)。该平台的目标打包时,这些格式与引擎的格式并列。 + +## 7. 签名 + +`[pack] sign = ["", ..., "{}"]` 在写容器之前对暂存树中角色为 `program`、`image`、`library`、`loader` 的每个 +文件运行一次,也对每个单文件运行一次,`{}` 为文件路径;一个包格式提供者或构建插件**可以**代替它签名。任一次失败 +使打包失败,写出 argv 与输出。签名后 `mcpp-package.json` 重新计算摘要。 + +## 8. 分发描述 + +`mcpp emit xpkg --package <归档> [--package ...] --url <前缀> [-o <文件>]` 由本命令写出的包的 `mcpp-package.json` +生成 xim-pkgindex 条目(spec 2),资产地址为 `<前缀>/<文件名>`: + +- 通用包是一个资产,`linux`、`macosx`、`windows` 三项的 URL 与 sha256 相同;包不携带运行器(非 `self-contained`) + 时三项各以运行器为依赖(`deps = { "" }`,`` 为平台运行器 argv 首项的程序名,即 xim-pkgindex + 中同名的包;openkal 为 `okrun`)。 +- 各系统各架构一个的包:每个系统一项,一个以 `${arch}` 代替架构的 URL 与每个架构的 sha256;文件名除架构外不同时 + 拒绝。 +- 安装把归档的唯一顶层目录移到安装目录;配置以 xvm 注册程序(通用包为 `bin/`,Windows 上为 + `bin/.cmd`;单腿原生包为程序所在的目录)。 + +不带 `--package` 时 `mcpp emit xpkg` 照旧生成源码包的描述。 + +## 9. 机器输出 + +`mcpp pack --message-format json` 打印一个 `mcpp.pack` 信封(docs/50)。`data` 增加: + +| 字段 | 语义 | +|---|---| +| `linkage`、`mode`、`pkgFormat` | 三个维度的取值 | +| `legs` | 腿的规范三元组 | +| `systems` | 加载器所属的 `-` | +| `artifacts[].sha256` | 每个文件产物的 sha256;目录产物为 `null` | + +`artifacts[].format` 保留,与 `pkgFormat` 相同。库目标的打包不带这些字段。 + +## 10. 判据 + +1. 每个不合法的组合在任何编译之前被拒绝,消息写出冲突的选择与出路(e2e 954)。 +2. `--format` 在 `pack` 与 `run` 上与 `--pkg-format` 等价,并写出警告(e2e 954)。 +3. 通用包在本机架构上经启动器运行;`self-contained` 的通用包在 `PATH` 上没有运行器时运行(e2e 955)。 +4. `--merge` 的结果含两个输入的全部腿、系统与文件,摘要一致;冲突与不同版本被拒绝(e2e 957)。 +5. 单文件首次运行解压一次,再次运行不解压;静态单文件不带解释器;映像的单文件运行时缓存目录中无新文件(e2e 956)。 +6. `mcpp-package.json` 的 `files` 覆盖树中每个文件,摘要与文件一致(e2e 954、957)。 +7. 通用包的 `mcpp emit xpkg` 条目在三个系统上为同一资产,并以运行器为依赖(e2e 957)。 + +## 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| 1.0 | 2026-10-28 | 首版(mcpp#847):三个维度与构建腿、组合的合法性、选项的全局用法与 `--pkg-format`、`[pack]` 的新键、管线、单腿与通用包的布局、启动器、`--systems`、`mcpp-package.json`、`--merge`、压缩、单文件、包格式提供者与平台插件、签名、分发描述、`mcpp.pack` 的新字段。 | +| 1.1 | 2026-10-28 | 与实现对齐:zip 的条目以 DEFLATE 压缩;`[pack] universal`;压缩的拒绝与项目缺省值的搁置;`--systems` 也用于映像的单文件;启动器的根与退出状态;单文件的载荷格式、校验与执行;映像单文件的载体内容;`mcpp-package.json` 的 `linkage` 取值与角色判定;`--merge` 的输入、选项写法与容器;签名的范围;`mcpp emit xpkg --package`;判据对应 e2e 954–957。 | diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md new file mode 100644 index 000000000..74a67fde0 --- /dev/null +++ b/docs/specs/platforms.md @@ -0,0 +1,458 @@ +# SPEC-013:平台 + +| 项 | 值 | +|---|---| +| 规范编号 | SPEC-013 | +| 标题 | 平台:一类目标的可复用配置,其声明、选择、合并、对依赖图的补全、链接与封装、运行器与载体 | +| 状态 | 评审中 v1.1 | +| 版本 | 1.1 | +| 最后修改 | 2026-10-28 | +| 对应实现 | mcpp >= 2026.10.27.1;逐条标注 | +| 相关设计文档 | `.agents/docs/2026-10-28-platforms-design.md`、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-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)(打包) | +| 使用文档 | [35 —— 平台](../zh/35-platforms.md) | + +用语按 RFC 2119:**必须 / 禁止**为强制,**应当**为建议,**可以**为可选。 + +## 0. 范围与词汇 + +本规范规定**平台**:一类目标的一份可复用、带版本、可发布的配置。平台由一个包在自己的清单中以 +`[platform]` 声明,由引擎选择并合并,其内容进入工具链的决定、依赖图、编译与链接的命令、产物的 +命名与封装、运行器与打包。引擎不持有任何一个平台的知识;它提供本规范规定的机制。 + +| 词 | 含义 | +|---|---| +| 平台包 | 清单含 `[platform]` 段的包 | +| 目标图 | 为 `--target` 构建的包 | +| 宿主图 | 为构建机器构建的程序:构建程序(`build.mcpp`、平台插件)与本机工具 | +| 层 | `kernel-abi`、`c-abi`、`c++-abi`、`compiler-runtime`(SPEC-002) | +| 工具 | 在构建机器上运行的程序(例:`okld`) | +| 封装(seal) | 链接之后、发布之前对产物本身执行的步骤 | +| 运行器 | `mcpp run`、`mcpp test` 执行产物的方式;`runners.` 为命名的设备动作 | +| 载体 | 把依赖加载器的产物与加载器合成为一个本机程序的宿主程序包(M3) | +| 平台陈述 | 一份平台数据(`Statement`):本规范 §1 的全部字段,每个值带来源 | + +## 1. `[platform]` 段 + +一个包的清单含 `[platform]` 段时,该包是平台包。`[platform]` **必须**含 `targets`;否则清单被拒绝。 +**已实现** + +```toml +[platform] +targets = ["", ...] +toolchain = "" +sysroot = "" | { path = "", provides = [...] } +runner = ["", "{}"] | { cmd = [...], host = "same-arch" | "any", exclusive = } +runners = { = ["", ...] } +carrier = { package = "", section = "", main = "" } +plugin = "build.mcpp" + +[platform.layers] = | false +[platform.tools] = | { program = "" } +[platform.build] cflags / cxxflags / asmflags / ldflags = [...] +[platform.build.'cfg()'] 同上 +[platform.link] driver, flags, start, script, inputs, exe, shared, seal +[platform.versions] = "" +[platform.hosted] targets, toolchain, layers +``` + +### 1.1 键 + +| 键 | 类型 | 语义 | +|---|---|---| +| `targets` | 非空字符串数组 | 平台服务的目标三元组 | +| `toolchain` | 字符串 | 约定的工具链规格(SPEC-006),例 `llvm@23.1.3` | +| `sysroot` | 字符串或 `{ path, provides }` | 预构建的目标基,写法同 `[target.].sysroot`;`path` 相对于平台包 | +| `runner` | 字符串数组或表 | §10 | +| `runners` | ` = [argv]` | 命名的设备动作,每个 argv 非空 | +| `carrier` | 表 | §11;`package`、`section`、`main` 三键都**必须**给出 | +| `plugin` | 字符串 | 只能为 `"build.mcpp"`:平台包自身的构建程序(SPEC-007)在平台阶段运行(§12);其他值拒绝 | +| `universal` | 布尔 | `true` 时本平台任一目标的打包缺省为覆盖全部 `targets` 的通用包(SPEC-014 §1.4、§4.2) | +| `layers` | 表 | §7.2;键只能是 §0 的四个层名 | +| `tools` | 表 | §7.3 | +| `build` | 表 | 四个标志列表,以及键形如 `cfg(...)` 的子表(同四个键) | +| `link` | 表 | §9 | +| `versions` | ` = ""` | 版本组(§7.5) | +| `hosted` | 表 | 宿主三元组选用本平台时适用的部分(§3.4);键只能是 `targets`、`toolchain`、`layers` | + +**包引用。** `layers`、`tools`、`carrier.package` 与选择平台的值都是包引用:字符串 +`"[.]@"`,或表。表的键为 `name`、`version`、`path`、`git` 与 +`rev` / `tag` / `branch` 之一、`features`、`default-features`;**必须**恰好给出 `version`、`path`、 +`git` 之一;`version` 与 `name` 同时出现;`path` 相对于写它的文件;`git` 的引用同时给出 `name`;`path` 的引用**可以** +省略 `name`,此时由该目录中包自己的清单命名。`false` 只在 `layers` 中有效;`{ program = "" }` 只在 +`tools` 中有效,且不带其他键。**已实现** + +**argv 约定。** argv 中的 `{}` 为产物;程序以名字书写,按"图中为本机构建的程序 → 声明的载荷 → +`PATH`"查找。**已实现** + +**错误。** 类型错误是拒绝,消息写出键;`[platform]` 与 `[platform.hosted]`、`[platform.link]` 等子表中 +引擎不认识的键记为警告(`--strict` 下拒绝),使为更新的引擎写的平台包仍能被读取;平台包以 +`[package] mcpp = ">="` 声明下限(SPEC-007 R9.8)。**已实现** + +### 1.2 只能声明的键与可由代码陈述的键 + +`targets`、`toolchain`、`sysroot`、`layers`、`tools`、`carrier`、`versions`、`hosted` 在解析工具链与依赖图 +时需要,那时没有构建程序可以运行,因此**只能**在清单中声明。`build`、`link`、`runner`、`runners` **可以** +由平台插件或构建程序陈述(§12)。**已实现** + +## 2. 三种写法与层次 + +同一组字段有三种同义的写法,解析为同一结构,每个值带来源: + +| 写法 | 位置 | 层次 | +|---|---|---| +| 约定 | 平台包的 `[platform]` | Platform | +| 陈述 | 平台插件与构建程序的协议 17 指令(§12) | Program | +| 覆盖 | `config.toml` 的 `[target.]`、`[host]` | Config | +| 覆盖 | 工程 `mcpp.toml` 的 `[target.]`、`[host]` | Manifest | +| 覆盖 | 环境变量 `MCPP_TARGET__PLATFORM`、`MCPP_TARGET__RUNNER`、`MCPP_HOST_PLATFORM` | Env | +| 覆盖 | `--platform`、`--host-platform` | Flag | + +层次按 Platform < Program < Config < Manifest < Env < Flag 排列,后者按字段替换前者(§6)。另有两个 +来源:`built-in`(目标表或宿主约定)与 `graph`(依赖图中的包已经提供,§7.2)。**已实现** + +`[target.]` 中与平台对应的键为 `platform`、`layers`、`tools`、`carrier`、`link`,以及表原有的 +`runner`、`runners`、`toolchain`、`sysroot`;`[host]` 只接受 `platform`。覆盖的键名与 `[platform]` 相同。 +`` 为三元组大写、非字母数字字符改为 `_` 的形式(例:`MCPP_TARGET_X86_64_OPENKAL_PLATFORM`)。**已实现** + +## 3. 目标图的选择 + +### 3.1 顺序 + +目标图的平台由第一个给出答案的来源决定: + +1. `--platform <值>`,即本次调用的 `MCPP_PLATFORM`; +2. `MCPP_TARGET__PLATFORM`; +3. 工程的 `[target.] platform`(工作空间层先于根包,§13); +4. `config.toml` 的 `[target.] platform`; +5. 目标表的平台列(§14); +6. 无:宿主目标的内置约定,或无平台。 + +第 3、4 步在命中的表中取具体程度最高者(SPEC-004 §3.1.1)。**已实现** + +### 3.2 值 + +| 值 | 含义 | +|---|---| +| `none` | 不使用平台 | +| `"@"`、包引用表 | 一个平台包,按普通依赖解析、获取与锁定 | +| `path:`,或写成路径的值(以 `/`、`./`、`../`、`~` 开头,Windows 盘符,或含路径分隔符且不含 `@`) | 一个目录中的平台包;命令行与环境变量中的相对路径相对于 mcpp 的启动目录 | + +一个目录的清单不可读时,选择**必须**以该清单的错误失败。**已实现** + +### 3.3 `platform` 的位置限制 + +`platform` **禁止**写在询问平台的选择器下(`[target.'cfg(platform = "...")'] platform`),因为该选择器的 +答案由 `platform` 决定;清单被拒绝,消息给出可用的选择器(三元组,或 `os`、`arch`、`env` 的 `cfg`)。 +**已实现** + +### 3.4 宿主三元组选用平台 + +被选中的平台不服务所求目标时: + +- 目标在 `targets` 中:适用整个平台。 +- 目标是宿主三元组,平台有 `[platform.hosted]`,且 `hosted.targets` 未写或含该三元组:只适用 + `hosted` 的 `toolchain` 与 `layers`,以及 `versions`;报告与 JSON 标为 `hosted`。 +- `hosted.targets` 不含该三元组:拒绝(`platform-target-unserved`),消息列出 `hosted.targets`,并在其中 + 有同系统同架构的三元组时给出改用它的命令。 +- 平台无 `[platform.hosted]`:拒绝(`platform-target-unserved`),消息列出 `targets`。 + +**已实现** + +## 4. 宿主图的平台 + +### 4.1 选择 + +宿主图的平台由第一个给出答案的来源决定:`--host-platform` / `MCPP_HOST_PLATFORM`;工程的 `[host] platform`; +`config.toml` 的 `[host] platform`;否则宿主约定。选择只在最外层的准备中进行。**已实现** + +### 4.2 模型 + +宿主图选用平台 P 时,构建程序与其库编译为 `<本机架构>-

` 的产物(P 的 `targets` 中与本机架构 +相同的那个三元组),由 P 的运行器运行;构建程序的身份含 P 的身份。报告多一行 +`Host platform (<来源>): build programs are images run by `。**已实现** + +### 4.3 自举边界 + +一个平台的 `tools` 与运行器所需的程序,在任何图中都**必须**按宿主约定构建:运行器不能由它自己运行。 +嵌套的准备(工具的子构建、宿主目标的子构建、构建程序图)不选择平台,除非调用者为它指定。因此不形成 +循环:P 的工具属于原生宿主图,构建程序属于 P 的宿主图。**已实现** + +## 5. `cfg(platform = ...)` + +`cfg()` 谓词增加键 `platform`。它匹配所选平台的名字(平台包短名去掉结尾的 `-platform`,`openkal-platform` +的名字是 `openkal`)或平台包的完整短名。它在具体程度中计一个分量,与 `arch`、`env` 相同,低于 `os`(计 +`os` 与族两个分量)与三元组;`all(...)` 中的分量相加(SPEC-004 §3.1.1)。无平台时,`cfg(platform = ...)` +不匹配。**已实现** + +## 6. 合并 + +一个图的平台陈述按"平台 → 平台插件与构建程序 → `config.toml` → 工程 → 环境变量"的顺序合并;每一层只改 +它写出的字段: + +| 字段 | 规则 | +|---|---| +| `build` 的四个列表、`link.flags` | 拼接,平台的在前;使用者只能追加,不能删除平台的约定 | +| `build` 的 `cfg(...)` 子表 | 追加 | +| `link.inputs`、`link.seal` | 整体替换 | +| `layers`、`tools`、`runners`、`versions` | 按键替换 | +| `runner` | `cmd` 替换;`host`、`exclusive` 各自独立替换,只写 argv 的覆盖保留平台的 `host` 与 `exclusive` | +| 其余 | 按字段替换 | + +`MCPP_TARGET__RUNNER` 以空白分隔的 argv 替换 `runner.cmd`;值为 `none` 时清除运行器。**已实现** + +## 7. 依赖图的补全 + +### 7.1 平台包是根的依赖 + +被选中的平台包作为根的第一个依赖进入依赖图,按普通包解析、获取、缓存与锁定;其库目标可被 `import`。 +无 `plugin` 且无源文件的平台包不编译。被选中的包没有 `[platform]` 段时拒绝(`platform-not-a-platform`)。 +**已实现** + +### 7.2 层 + +第一轮遍历之后,对 `layers` 中的每一层: + +1. 值为 `false`:平台不为该层提供包;报告为 `none`。 +2. 图中已有包以 `provides` 提供该层:平台让位;该包由平台加入时报告为 `platform`,否则为 `graph`。 + 判断读 `provides`,不比较包名。 +3. 否则:该包作为根的依赖加入(来源 `the platform's `),一个包提供的多层只加入一次。 + +加入的包再次遍历;补全最多进行三轮。一个包提供多个层时,关闭其中一层不移除该包:例如 `c++-abi = false` +而 `compiler-runtime` 仍由同一包提供时,该包的 `provides` 使 `c++-abi` 由图提供。**已实现** + +### 7.3 工具 + +`tools` 中的每个包引用作为根的构建依赖加入,带 `tools = []`,为本机构建;同一包的多个工具只构建一次。 +根已经以构建依赖请求同名工具时让位;`{ program = "" }` 不构建,在声明的载荷与 `PATH` 中查找。 +**已实现** + +### 7.4 工作空间成员 + +工作空间的计划中,平台加入的层的包同时成为每个被选中成员的依赖边,使成员的链接闭包含有这些层。**已实现** + +### 7.5 版本组 + +`versions` 中的包以不同于组中版本的版本出现在图中时拒绝(`platform-version-group`),消息写出该包、两个版本、 +请求链与两条出路:选择携带所需版本的平台版本,或在请求处把该包写成组中的版本。只为本机构建的包(工具) +与以 `path`、`git` 取得的包不受版本组约束。检查在每一轮补全之前与最后一轮之后进行,使工程固定的一个版本以 +版本组的冲突报告,而不是以平台加入的包遇到第二个版本报告。**已实现** + +### 7.6 多版本改名的边界 + +两个版本的同一个包以改名并存只改 C++ 模块名。导出头文件目录的包与以 `provides` 提供 `mcpp:` 层的包 +**禁止**以改名并存:两个版本同时被请求时拒绝,消息列出两个请求者,有平台时另给出换平台版本的出路。 +**已实现** + +## 8. 工具链 + +平台的 `toolchain` 位于目标表钉住的工具链的位置:它替换默认值,**禁止**替换工程或本机为该目标写出的工具链 +(`[target.] toolchain`、`[toolchain]`、`--toolchain`)。图中提供 `kernel-abi` 的包不取消平台的 +工具链。工具链报告行说明它是平台的,以及它替换了哪个默认值与覆盖它的键。**已实现** + +平台写 `sysroot` 时,目标基在依赖图存在之前决定;平台包不在本地时,第一遍准备获取它,第二遍以它的 +`sysroot` 重新决定目标基。**已实现** + +## 9. 编译、链接、命名与封装 + +### 9.1 编译 + +平台陈述的 `build` 标志进入目标图每个翻译单元的命令行(`cflags` 给 C,`cxxflags` 给 C++,`asmflags` 给汇编, +`ldflags` 给链接,`driver = "lld"` 时位于 `link.flags` 之后、直接给 `ld.lld`),`cfg(...)` 子表按目标求值后追加在无条件的标志之后。**已实现** + +### 9.2 链接 + +| 键 | 语义 | +|---|---| +| `driver` | `"compiler"`(编译器驱动链接,缺省)或 `"lld"`(`ld.lld` 直接链接) | +| `flags` | `driver = "lld"` 时给 `ld.lld` 的参数,位于其他链接参数之前 | +| `start` | 入口符号(SPEC-004 §14);入口未定义是链接错误 | +| `script` | 平台包中的链接脚本,以 `-T` 给出 | +| `inputs` | `{ file = "<包内路径>" }`,或 `{ from = [argv], as = "<文件名>" }`:运行程序,把其标准输出保存为 `as`;以 `.S`、`.s` 结尾时先汇编,否则原样参与链接 | +| `exe` | 可执行产物的名字模式,**必须**以 `{name}` 开头 | +| `shared` | 共享产物的名字模式,`{name}` 恰好一次,不含目录 | +| `seal` | 封装步骤,每步一个 argv(§9.3) | + +`driver = "lld"` 时引擎在编译器旁依次查找 `ld.lld`、`lld`、`lld-link`(Windows 载荷只带 `lld-link`);都没有时拒绝 +(`lld-required-absent`),消息写出平台的工具链。程序名不是 `ld.lld` 时引擎加 `-flavor gnu`。链接或封装命名的程序既非图中为本机构建、也不在本机可找到时拒绝 +(`platform-tool-absent`),消息写出负责构建它的包,或写出 `[target.'cfg(platform = "")'.tools]` 的写法。 +**已实现** + +### 9.3 封装 + +`seal` 的步骤在链接器写出的临时产物上依次执行,`{}` 为该临时产物;参数 `{needed}` 展开为该产物所需的共享产物, +每个一个参数,当它前面的参数是取值的选项(以 `-` 开头、不含 `=` 与 `{}`)时,该选项在每个产物前重复, +不需要任何共享产物时二者都省略。全部步骤成功后产物原子发布(SPEC-011 §3);任一步失败则不发布,保留上一次 +发布的产物,错误给出该步的 argv 与输出。步骤属于链接边的命令行,改变步骤即重新链接。**已实现** + +## 10. 运行器 + +`runner` 的数组形式为 argv;表形式: + +| 键 | 语义 | +|---|---| +| `cmd` | argv,非空;`{}` 为产物 | +| `host` | `"same-arch"`:产物只在其架构的机器上运行;`"any"`:运行器可以从任何机器到达产物(模拟器、探针) | +| `exclusive` | 为真时该目标的运行不重叠,`mcpp test` 串行运行 | + +`host = "same-arch"` 而产物的架构不是本机架构时,`mcpp run` 与 `mcpp test` 拒绝运行,消息给出 +`[target.] run_under` 的写法;`run_under` 存在或给出 `--no-runner` 时不拒绝。运行器的合并顺序与 §6 +相同;`mcpp run` 与 `mcpp test` 读合并后的值。**已实现** + +## 11. 载体(M3) + +对一个依赖加载器的产物,`mcpp pack` 在三处生成一个系统程序的工程(`target/carrier-pack/--/`): +`--mode self-contained` 的单目标包(该程序携带产物)、`self-contained` 通用包的运行器(不携带任何内容,SPEC-014 +§4.4)、映像的单文件(SPEC-014 §5.2)。工程依赖 `carrier.package`;入口是平台包中的 `carrier.main`;一个汇编文件 +以 `.incbin` 把携带的字节放进节 `carrier.section`(ELF 写作该名并带 `"a"` 标志,COFF 写作该名并带 `"dr"` 标志, +Mach-O 写作 `__<大写名>,__carried`),给出符号 `mcpp_carried_begin`、`mcpp_carried_end`、`mcpp_carried_name` +(入口的名字)与 `mcpp_carried_digest`(字符串)。 + +携带的字节**必须**是以下之一,载体的入口按内容区分: + +| 携带 | 判别 | 载体的行为 | +|---|---|---| +| 无(`mcpp_carried_begin == mcpp_carried_end`) | 长度为 0 | 运行器形式:运行第一个参数所指的映像,其余参数传给它,如同平台的运行器程序 | +| 一个映像 | 产物本身 | 从内存运行该映像,映像以程序自己的名字为名 | +| 一组映像 | 以 `PK\x03\x04` 开头的 stored zip,各条目数据按 64 KiB 对齐 | 从内存运行名为 `mcpp_carried_name` 的映像,映像之间按名字相互解析 | + +程序按其系统的约定构建(自举边界),不使用平台:Linux 上为 `-linux-musl` 行的静态程序,该行的工具链被 +显式写出;macOS 上为 `-macos`、Windows 上为 `-windows-gnu` 行,系统与架构同本机时取本机的行,只依赖 +系统库(映像的单文件在 Windows 上以 `system` 模式打包)。引擎不含任何加载器的调用。平台未声明 `carrier` 时, +需要载体的打包被拒绝并写出该键。 + +## 12. 平台阶段与协议 17 + +### 12.1 平台阶段 + +平台包的 `plugin` 在**平台阶段**运行:`MCPP_PHASE` 为 `platform`,`mcpp::phase()` 返回 `"platform"`。该阶段只能 +陈述编译与链接标志、`link_script`、`link_start` 与协议 17 的指令、`runner` 与命名运行器、`run_exclusive`、平台 +提供的包格式(`pack-format` 与 `role = "artifact"` 的 action,SPEC-014 §6),以及重跑条件、警告与诊断;其他指令 +(含其他角色的 action)被拒绝(`platform-phase-directive`),消息点名第一条不属于该阶段的指令。陈述工具链在平台 +阶段被拒绝。**已实现** + +### 12.2 协议 17 的指令 + +| `mcpp.core` 函数 | 指令 | 字段 | +|---|---|---| +| `link_driver(d)` | `mcpp:link-driver=` | `link.driver` | +| `link_input(path)` | `mcpp:link-input=file:` | `link.inputs` 的 `file` | +| `link_input_from(as, token)` | `mcpp:link-input=from::` | `link.inputs` 的 `from` 与 `as`,同一 `as` 的 token 依次组成 argv | +| `artifact_name(kind, pattern)` | `mcpp:artifact-name=:` | `link.exe`、`link.shared` | +| `seal(step, token)` | `mcpp:seal=:` | `link.seal`,按步号分组 | +| `run_host(h)` | `mcpp:run-host=` | `runner.host` | + +协议 17 的指令也可以由任何构建程序在构建阶段发出(板级支持包的写法),在 Program 层次合并。格式错误的 +指令被拒绝,消息写出合法形式。**已实现** + +## 13. 工作空间 + +`[target.]` 的平台键与 `[host]` 是根位置的键,从计划的根读取。工作空间层的 +`[workspace.target.]` 先于成员自己的表合并:同一选择器的两张表,成员的字段胜出;不同选择器之间 +按具体度,与 `[target.]` 其他键的合并相同。`[host]` 不是工作空间层的表。虚拟根携带第一个被选中 +成员的平台键与 `[host]`;根位置的比较键包含适用的平台表与 `[host]`,使二者不同的成员不共用一个计划。 +**已实现** + +## 14. 内置平台与目标表 + +宿主目标(Linux、macOS、Windows、Android、iOS、Emscripten)的约定留在引擎中,无平台包。目标表有平台列: +`x86_64-openkal`、`aarch64-openkal` 的值为 `mcpplibs.openkal-platform@`,即这两个目标的内置支持; +选择来源报告为 `built-in`。项目可以不等引擎更新,以 `[target.] platform` 选择另一个版本。 +**已实现** + +## 15. 锁、身份与报告 + +### 15.1 锁 + +平台加入的包,以及只经由它们到达的包(层的实现、工具与插件的库),在 `mcpp.lock` 中带 `origin = "platform"`(当解析 +它的每个配置都这样看时);无 `origin` 的条目属于工程。每个条目以 `targets` 写出解析它的配置(目标三元组,排序);原生 +配置——不带 `--target` 的构建解析到的目标,即 `[build] target`、本机的缺省目标或本机三元组——独有的条目不写 +`targets`,读取时无 `targets` 的条目属于写锁的机器的原生配置。一次配置把自己的目标加入它解析到的条目、从没有解析到的 +条目中去掉,不再被任何配置解析的条目被删除;一个条目的版本改变时其 `targets` 只剩这次的配置;条目按身份排序。为本机的 +构建与为 `x86_64-openkal` 的构建、打包因此不互相改写锁。**已实现** + +### 15.2 身份 + +平台包与它加入的包是依赖图的成员,进入配置指纹的依赖部分;换平台或平台版本即换构建目录。平台的身份为 +`@`,`path`、`git` 来源另带来源;宿主图平台的身份进入构建程序的身份。**已实现** + +### 15.3 报告 + +目标报告中,平台加入的层的来源写作 `platform`,图已提供的写作 `graph`。层之后为平台块: + +``` + Platform openkal-platform 0.17.3 (built-in) + tools okld, okrun (openkal-tools@0.1.3, platform) + runner okrun (platform) + link lld, start ok_main (platform) +``` + +来源取值:`built-in`、`platform`、`build-program`、`config.toml:<行>`、`mcpp.toml:<行>`、环境变量名、 +`--platform`、`graph`。宿主三元组选用平台时来源后加 `, hosted`。**已实现** + +### 15.4 机器输出 + +`--message-format json` 的 `plan` 事件与 `result.json` 的 `configuration` 含 `platform` 与 `hostPlatform`,无平台时为 +`null`;对象形式: + +```json +{"name": "mcpplibs.openkal-platform", "version": "0.17.3", "source": "built-in", "hosted": false, + "fields": {"toolchain": {"value": "llvm@23.1.3", "source": "platform"}, + "layers.c-abi": {"value": "openkal-musl@0.22.6", "source": "platform"}}} +``` + +`fields` 的键为 `toolchain`、`runner`、`runners.`、`layers.`、`tools.`(值为所用程序的路径)、 +`link.driver`、`link.start`、`link.exe`、`link.shared`、`carrier`。**已实现** + +## 16. 命令 + +| 命令 | 规定 | +|---|---| +| `--platform <值>` | `build`、`run`、`test`、`why`、`pack`、`emit build-database`、`self env`、`toolchain install`;即 `MCPP_PLATFORM` | +| `--host-platform <值>` | `build`、`run`、`test`、`why`、`pack`、`emit build-database`、`self env`;即 `MCPP_HOST_PLATFORM` | +| `mcpp self env --target [--format json]` | 准备并报告一个配置:三元组、编译器与其来源、平台、工具、运行器与其程序、`run_under`;在工程中为该工程的配置,工程外为家目录缓存中的空程序;工程源码会被拒绝的原因(例:无 C++ 运行时而 `import std`)作为警告报告,配置照常报告 | +| `mcpp self env --host` | 宿主图的配置;其构建程序已构建过时,报告宿主平台的运行器与其程序、工具 | +| `mcpp toolchain install --target `(不指定编译器) | 安装该目标的工具链后准备该配置:获取平台包,构建其工具 | +| `mcpp toolchain list` | 目标行有 PLATFORM 列,JSON 有 `platform` 字段 | +| `mcpp test`(在平台包中,未给 `--target`) | 对 `targets` 中的每个目标以本包为平台构建测试;`host = "same-arch"` 时只运行本机架构的目标,其余报告为已构建未运行 | + +**已实现** + +## 17. 拒绝码 + +机器可读的拒绝码出现在结果记录的 `reason` 中(docs/50)。 + +| 码 | 条件 | +|---|---| +| `platform-not-a-platform` | 被选中的包没有 `[platform]` | +| `platform-target-unserved` | 平台不服务该目标(§3.4) | +| `platform-tool-absent` | 链接或封装命名的程序既非图中构建、也未找到(§9.2) | +| `platform-version-group` | 图中的包与版本组冲突(§7.5) | +| `platform-phase-directive` | 平台阶段陈述了不属于该阶段的指令(§12.1) | +| `lld-required-absent` | `driver = "lld"` 而工具链无 `ld.lld` | + +`platform-tool-absent` 取代 2026.10.26.2 的 `image-tool-absent`。**已实现** + +## 18. 判据 + +1. `[platform]` 及其子表、`[host]` 与包引用中每个键的类型错误在读清单时拒绝,消息写出键与文件;未知键为警告并被忽略。 +2. 选择按 §3.1、§4.1 的顺序;`none` 不使用平台;目录形式读取目录中的清单。 +3. `cfg(platform = ...)` 下写 `platform` 被拒绝。 +4. 图已提供某层时平台不加入该层的包,报告为 `graph`;`false` 不加入。 +5. 版本组冲突在任何链接之前拒绝,消息给出两条出路。 +6. 封装失败不发布产物,保留上一次的产物。 +7. `same-arch` 的运行器不运行其他架构的产物,并给出 `run_under` 的写法。 +8. 平台的工具在任何图中按宿主约定构建。 +9. 锁中平台加入的包带 `origin = "platform"`;报告与 JSON 给出每个字段的来源。 +10. 一个与 openkal 无关的平台包的编译标志、命名、封装与运行器到达程序,`cfg(platform)` 下的覆盖取代对应字段, + `--platform none` 不使用平台,没有 `[platform]` 的包被拒绝为平台(e2e 952)。 +11. 宿主图选用平台时构建程序与其库作为映像构建并经平台的运行器运行,平台的工具按宿主约定构建, + `--host-platform none` 时构建程序为本机程序(e2e 953)。 + +## 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| 1.0 | 2026-10-28 | 首版(mcpp 2026.10.27.1,mcpp#847):`[platform]` 的七组键与 `versions`、`hosted`;三种写法与层次;目标图与宿主图的选择;`cfg(platform)`;合并;依赖图的补全、版本组与改名边界;工具链;链接、命名与封装;运行器的表形式;载体;平台阶段与协议 17;工作空间;目标表的平台列;锁、报告、机器输出、命令与拒绝码。 | +| 1.1 | 2026-10-28 | `[platform] universal`;载体的三种携带形式(运行器形式、映像、映像组的 zip)、`mcpp_carried_digest`、Mach-O 节名 `__carried`、按系统的行;平台阶段可声明包格式与 artifact action;`self env --host` 报告运行器与工具;判据 10、11(e2e 952、953)。 | diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index d6848ee66..fc18d6cbb 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -251,9 +251,9 @@ main = "src/main.cpp" 接受 `app` 的方式也与接受 `bin` 完全相同。 在其形式为共享库的那一行上,`mcpp run` 一个 `app` 目标,若不带 -`--format` 会拒绝执行,并点名这个旗标与所解析出的图提供的那些格式。 -`mcpp pack --format apk` 会把这个库放到闭包已经放置共享对象的位置。见 -[10 —— 打包与发布](10-pack-and-release.md)中的 `mcpp run --format`。 +`--pkg-format` 会拒绝执行,并点名这个旗标与所解析出的图提供的那些格式。 +`mcpp pack --pkg-format apk` 会把这个库放到闭包已经放置共享对象的位置。见 +[10 —— 打包与发布](10-pack-and-release.md)中的 `mcpp run --pkg-format`。 早于 2026.9.12.3 的引擎不认识这个取值,会拒绝并点名它认识的三种: @@ -1279,10 +1279,8 @@ argv(OpenSBI 启动用 `-bios default`,picolibc 镜像用 `-bios none -semihosting`)——一个替某一块板子猜一个值的引擎,就是另一块板子必须 与之对抗的引擎。一个板级支持包通常会提供它。 -唯一一个 runner 属于 target 而非板子的 target 是 `-openkal` -(2026.10.26.2):ok 映像在每个系统上都由 `okrun` 运行,因此工程未声明 runner -时,`okrun` 就是该 target 的 runner。它的查找方式与声明的 runner 程序相同,并 -优先查找依赖图为本机构建的程序(`tools = [...]`)([24](24-openkal-cross.md))。 +由平台配置的目标在工程未声明 runner 时使用平台的 runner:`-openkal` 在每个系统上都由 `okrun` 运行 +(2026.10.27.1+,[35](35-platforms.md))。它的查找方式与声明的 runner 程序相同,并优先查找依赖图为本机构建的程序。 ### 2.7.3 hosted 目标上的 `runner`(2026.9.2.1+) @@ -1376,6 +1374,33 @@ runner 与 `run_under`。 以及陈述了基座时的 `MCPP_SYSROOT`;其退出码即程序的退出码。非 macOS 宿主上的 Apple 产物在没有 `run_under` 时于执行前被拒绝,拒绝信息点名本键。 +#### 2.7.3.3 平台键(2026.10.27.1+) + +`[target.]` 还以平台自己的键名选择并覆盖目标的**平台**([35 —— 平台](35-platforms.md)): + +```toml +[target.'cfg(os = "openkal")'] +platform = "openkal-platform@0.17.3" # or { path = "…" }, or "none" + +[target.'cfg(platform = "openkal")'.layers] +c-abi = { path = "../openkal-musl" } + +[target.'cfg(platform = "openkal")'] +runner = { cmd = ["okrun", "{}"], host = "same-arch", exclusive = false } +``` + +| 键 | 取值 | +|---|---| +| `platform` | 所选目标的平台包、`none`,或带 `path` / `git` 的表 | +| `layers.` | 提供 `c-abi`、`c++-abi`、`compiler-runtime` 或 `kernel-abi` 的包,或 `false` | +| `tools.` | 为本机构建一个工具的包,或 `{ program = "" }` | +| `link` | `driver`、`flags`、`start`、`script`、`inputs`、`exe`、`shared`、`seal` | +| `carrier` | `{ package, section, main }`:`--mode self-contained` 构建的程序 | +| `runner` | 一个 argv,或 `{ cmd, host, exclusive }`:`host = "same-arch"` 只在产物架构的机器上运行它 | + +`cfg(platform = "")` 按平台选择;`platform` 本身不能写在这样的选择器下。同样的键也从 +`~/.mcpp/config.toml` 读取。平台包在 `[platform]` 下陈述它们,`[host] platform` 选择构建程序的平台。 + ### 2.8 `[features]` —— Feature 已移至 [06 —— Feature 与能力](06-features-and-capabilities.md),连同 diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md index 9c4225b54..18d5a4e84 100644 --- a/docs/zh/05-dependencies.md +++ b/docs/zh/05-dependencies.md @@ -325,7 +325,8 @@ package = { `mcpp build` / `run` / `test` **只在依赖无法用本地副本解析时**刷新包索引, 绝不会仅仅因为时间流逝就刷新。具体地说:本地根本没有索引、依赖的描述符不在 -其中,或一条 SemVer 约束在本地已知版本里无解,这三种情况会触发刷新。只要 +其中、一条 SemVer 约束在本地已知版本里无解,或某个确切版本因索引尚未收录 +(上次同步之后发布)而安装被拒,这四种情况会触发刷新;刷新之后安装再请求一次。只要 所有依赖都能在本地解析出来,不论本地索引多旧,构建都不会发出任何网络请求。 由此带来一个值得知道的推论:`^1.2` 这类约束,是对**本地索引已知的那些版本** diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md index e309fdd3a..1ea4a9e18 100644 --- a/docs/zh/08-testing.md +++ b/docs/zh/08-testing.md @@ -99,6 +99,14 @@ mcpp test -- --verbose # everything after `--` goes to each test binary `mcpp test 2>&1 | tee test.log` 两者都收集 (见 [09 —— 按场景选命令](09-commands-by-scenario.md#输出流))。 +**测试按名字找到包的程序**(2026.10.27.1+)。`mcpp test` 先按 `mcpp build` 的方式构建包的程序,并给每个测试 +`MCPP_BIN_`:程序 `` 的路径,名字转为大写,字母与数字之外的字符写作 `_`(`my-tool` 为 +`MCPP_BIN_MY_TOOL`)。运行被测程序的测试从这里读取它。 + +**平台包在它的目标上测试**(2026.10.27.1+)。在带 `[platform]` 段的包中执行 `mcpp test` 而不给 `--target` 时, +以该包本身为平台,为平台的每个 `targets` 构建测试。其 runner 能在本机运行的目标被运行;其余已构建、报告为未运行 +([35](35-platforms.md))。 + ### 在工作空间中 ```bash diff --git a/docs/zh/09-commands-by-scenario.md b/docs/zh/09-commands-by-scenario.md index 88e8df6ae..b1322706e 100644 --- a/docs/zh/09-commands-by-scenario.md +++ b/docs/zh/09-commands-by-scenario.md @@ -105,7 +105,7 @@ $ mcpp search imgui ```bash mcpp test --toolchain llvm@23.1.3 mcpp run --toolchain gcc@16.1.0 -mcpp pack --toolchain llvm@23.1.3 --format dir +mcpp pack --toolchain llvm@23.1.3 --pkg-format dir ``` 对这一次调用,这个选项取代 `mcpp.toml` 中的 `[toolchain] default`,优先级 @@ -113,6 +113,10 @@ mcpp pack --toolchain llvm@23.1.3 --format dir 每个工具链构建到它自己的输出目录,一次记录下来的构建只为记录它的那个工具链 请求重放。 +`--platform ` 与 `--host-platform ` 以同样方式为一次调用选择目标的平台与构建程序的平台 +(2026.10.27.1+,[35](35-platforms.md));`--platform none` 只以目标的约定构建, +`mcpp self env --target ` 报告一次构建将使用的配置。 + ## 解释一次解析 `mcpp why` 报告一次构建会解析出什么,并且不构建任何东西: diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 94919095e..088ad8c4d 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -49,21 +49,33 @@ glibc 专有行为(NSS 查询、`dlopen` 宿主插件)时的首选。 它之前先读下面 `/proc/self/exe` 一节:经由内置 loader 启动会改变程序对「自己 在哪」的认知。 -## 两条轴:target(libc)× mode(打包深度) +## 三个维度与构建腿 -分发有两项正交的选择: +一次打包由三个相互独立的选择与一组构建腿决定(2026.10.27.1+): -- **libc / static** 是一项*构建 target* 属性:`--target …-linux-gnu`(glibc) - 与 `--target …-linux-musl`(musl,静态)。`--target …-musl` 隐含 `static`。 -- **打包深度** 是一项*pack* 属性:产物随身携带多少共享库闭包,由 `--mode` - 选择。 +| 维度 | 选项 | 回答的问题 | 取值 | +|---|---|---|---| +| 链接 | `--linkage` | 库在程序里,还是在程序旁边 | `static`、`shared` | +| 闭包 | `--mode` | 运行该包的机器提供什么 | `system`、`vendored`(默认)、`self-contained` | +| 容器 | `--pkg-format` | 交付成什么 | `tar`、`zip`、`dir`、`single-file`、某个包提供的格式;未给出时 Windows 程序为 `zip`,其余为 `tar` | +| 腿 | `--target`,可重复 | 一个包覆盖哪些目标 | 三元组 | + +- **链接**是程序的属性。`--linkage static|shared`(或 `[pack] linkage`)是这次打包的构建的依赖链接, + 优先于 `[build] dependency_linkage` 与构建配置中的值;未给出时每个库按它自己的声明链接。Linux 上 + `--linkage static` 选择 `-linux-musl` 行(`-linux-gnu` 目标换成对应的 musl 行),全静态程序 + 不论 `--mode` 为何都整体打包;`--target …-linux-musl` 隐含它。ok 映像上它是一个 `.oke`、没有 `.okso`。 +- **闭包**是包的属性:共享库闭包中有多少随程序携带。`--mode static` 是 + `--linkage static --mode self-contained` 的简写。 +- **容器**是交付的形状,见下文。 +- **腿**:每个 `--target` 是一次构建,暂存进同一棵树。声明了 `universal = true` 的平台 + 缺省覆盖它的全部目标。 | Mode | 宿主必须提供 | 体积 | 使用场景 | |---|---|---|---| | `system` | 每一个 `.so`(含第三方) | 最小 | `.deb`/`.rpm`,同发行版机群(由包管理器声明依赖) | | `vendored`(默认) | libc / libstdc++ / loader | +几 MB | 主流发行版(Ubuntu 22+、Debian 12+、RHEL 9+) | | `self-contained` | 无 | +30–50 MB | 任意 Linux,含较旧的 glibc;携带闭包与 `run.sh` wrapper | -| `static` | 无(单文件) | +5–10 MB | musl;架构匹配的 Linux x86_64 或 aarch64,Docker scratch,Alpine | +| `static`(`--linkage static --mode self-contained`) | 无(单文件) | +5–10 MB | musl;架构匹配的 Linux x86_64 或 aarch64,Docker scratch,Alpine | 如何选择: @@ -81,6 +93,25 @@ glibc 专有行为(NSS 查询、`dlopen` 宿主插件)时的首选。 来。e2e 215 会扫描 bundle 里每一个 ELF,查找任何落在 `$MCPP_HOME` 之下的路径, 命中即失败。 +不能成立的组合在任何编译之前被拒绝,消息写出两个选择: + +| 组合 | 出路 | +|---|---| +| `--pkg-format single-file` 与 `--mode system` | `--mode vendored` 或 `self-contained`:单文件携带它所运行的东西 | +| `--compress` 与 `--pkg-format dir` 或提供者的格式 | `--pkg-format tar`、`zip` 或 `single-file`:只有容器被压缩 | +| `--compress zstd` 或 `xz` 与 `--pkg-format zip` | `--compress gzip`(DEFLATE)或 `none`,或 `--pkg-format tar` | +| `--compress gzip` 或 `xz` 与 `--pkg-format single-file` | `--compress zstd[:]` 或 `none`:启动器解压 zstd | +| 通用包之外、对一个可执行文件给出多个 `--target` | 每个目标打一次 | +| 自带加载器的通用包与映像的单文件之外使用 `--systems` | 在声明 `universal = true` 的平台上用 `--mode self-contained`,或 `--pkg-format single-file` | +| 映像的单文件,`--systems` 中某系统的行本 mcpp 不构建程序(`x86_64-macos`、`aarch64-windows-gnu`) | 只列出会构建的系统的 `--systems` | +| 映像的单文件,多个 `--systems` 与 `-o` 同用 | 去掉 `-o`:每个系统一个文件 | +| 平台未声明 `runner` 时打通用包 | `--no-universal` | +| 通用包中给出平台 `targets` 之外的 `--target` | 平台服务的目标 | +| 对 `lib` 或 `shared` 目标使用被分派的格式 | 库的路线,[12](12-binary-distribution.md) | +| 对 `lib` 或 `shared` 目标使用 `--pkg-format single-file` | `tar`、`zip` 或 `dir` | + +与容器不相容的 `[pack] compress`(目录,或 zip 的 `zstd`)在这次打包中被搁置;只有选项被拒绝。 + ### 需要宿主提供某种能力的程序 「自包含」有一个下限。有些库只能来自目标机器:图形驱动的用户态部分与正在 @@ -128,32 +159,42 @@ wire 格式(由 `install.sh` 消费),**不**跟随改名:`vendored` 无 ## 命令 ```bash -mcpp pack # vendored by default +mcpp pack # vendored by default mcpp pack --mode system mcpp pack --mode static -mcpp pack --mode self-contained # alias: --mode bundle-all -mcpp pack --target x86_64-linux-musl # equivalent to --mode static -mcpp pack --target aarch64-linux-musl # ARM64 equivalent -mcpp pack --format dir # output as a directory, no tarball -mcpp pack --format appimage # a format a package in the graph provides -mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz -mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path -mcpp pack --profile dev # build with a different profile (default: release) -mcpp pack --dev # the same, as `build` and `run` spell it; --profile wins over it -mcpp pack --message-format json # one mcpp.pack envelope on stdout (mcpp 2026.9.16.1+) -mcpp pack --no-strip # ship the artifacts as built -mcpp pack -p app --format release # a workspace member, as if run in its directory -mcpp pack --workspace --format release # every member with a program, planned and built once -mcpp pack -p cli -p gui -o dist/ # two members, each archive below dist/ -mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/ -mcpp pack --format msi --features installer # activate root-package features for the pack +mcpp pack --mode self-contained # alias: --mode bundle-all +mcpp pack --target x86_64-linux-musl # equivalent to --mode static +mcpp pack --target aarch64-linux-musl # ARM64 equivalent +mcpp pack --pkg-format dir # output as a directory, no tarball +mcpp pack --pkg-format appimage # a format a package in the graph provides +mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz +mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path +mcpp pack --profile dev # build with a different profile (default: release) +mcpp pack --dev # the same, as `build` and `run` spell it; --profile wins over it +mcpp pack --message-format json # one mcpp.pack envelope on stdout (mcpp 2026.9.16.1+) +mcpp pack --no-strip # ship the artifacts as built +mcpp pack -p app --pkg-format release # a workspace member, as if run in its directory +mcpp pack --workspace --pkg-format release # every member with a program, planned and built once +mcpp pack -p cli -p gui -o dist/ # two members, each archive below dist/ +mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/ +mcpp pack --pkg-format msi --features installer # activate root-package features for the pack +mcpp pack --linkage static # the libraries inside the program +mcpp pack --pkg-format single-file # one executable file +mcpp pack --pkg-format tar --compress zstd # .tar.zst +mcpp pack --target x86_64-openkal # a universal package: every architecture of the platform +mcpp pack --target x86_64-openkal --mode self-contained --systems linux,macos,windows +mcpp pack --merge a.zip --merge b.zip -o all.zip # one package from packages built on several machines +mcpp emit xpkg --package all.zip --url https://example.org/v1.0.0 # its xim-pkgindex entry ``` +`pack` 与 `run` 接受 `--format` 作为 `--pkg-format` 的旧写法,并打印一条警告(2026.10.27.1+)。 +其他命令上的 `--format` 是输出编码(`--format json`)。 + `--features `(mcpp 2026.9.15.2+)为打包所执行的每一次构建启用根包 feature:每一条 `--target` 腿,以及被分派格式的两遍构建。它接受的取值与 `mcpp build --features` 相同,因此只为某一种发布才需要的宿主工具,可以声明在 带 `tools = [...]` 的 `[feature-deps.]` 下,只由点名 `` 的那次打包构建 -出来。`mcpp run --format --features ` 把同样的 feature 交给它 +出来。`mcpp run --pkg-format --features ` 把同样的 feature 交给它 执行的那次打包。 `-p `(mcpp 2026.9.28.3+)在工作区根目录打包某个成员:成员的解析方式与其他 `-p` @@ -165,7 +206,7 @@ feature:每一条 `--target` 腿,以及被分派格式的两遍构建。它 打包对它所执行的构建的陈述方式与 `mcpp build` 相同(mcpp 2026.10.1.2+):每个有工作的包 一行,运行期间显示状态行,并在第一行 `Packing` 之前写出 `Finished`。跨多个配置的打包先构建 -全部配置,只写一次 `Finished`。由包提供的 `--format` 的第二遍以同样方式陈述,其后是 +全部配置,只写一次 `Finished`。由包提供的 `--pkg-format` 的第二遍以同样方式陈述,其后是 `Distributing` 与 `Packed` 行。构建同时运行 `[build] jobs` 个编译,与每一条会编译的命令相同。 2026.10.1.2 之前,打包中的构建不写出上述任何内容,重新编译数分钟的打包只显示 `Planning`。 @@ -176,9 +217,9 @@ feature:每一条 `--target` 腿,以及被分派格式的两遍构建。它 然后每个成员在各自的暂存树中暂存并打包: ```bash -mcpp pack --workspace --format release # 每个有程序的成员 +mcpp pack --workspace --pkg-format release # 每个有程序的成员 mcpp pack --workspace --exclude updater # 除一个之外的每个这样的成员 -mcpp pack -p cli -p gui --format release +mcpp pack -p cli -p gui --pkg-format release ``` 对每个成员分别运行 `mcpp pack -p `,每次都会重新规划图、重新运行构建程序、 @@ -190,7 +231,7 @@ mcpp pack -p cli -p gui --format release - **产物。** 成员的归档或目录,就是它单独打包时得到的那一份,位于它自己的 `target/dist/` 之下。给出 `--output

` 时,它写在该目录之下(目录不存在时会创建),使用默认的名字。 -- **自己的暂存树。** 使用被分派的 `--format` 时,打包如上所述准备两次,第二遍告诉每个 +- **自己的暂存树。** 使用被分派的 `--pkg-format` 时,打包如上所述准备两次,第二遍告诉每个 构建程序它所代表的成员的暂存树。`mcpp::pack_stage_dir()` 与 `${mcpp.stage_dir}` 按成员回答。一个程序代表它所属的成员;只有一个被打包成员依赖到的包(成员所依赖的 提供者)代表那个成员;被多个被打包成员依赖到的包不代表其中任何一个,它既不会被告知 @@ -214,7 +255,7 @@ mcpp pack -p cli -p gui --format release | 多于一个 `--target` | 一个程序只为一个 target 构建,而多 target 的 Android 打包暂存的是一个成员的各条腿 | | 已存在且是文件的 `--output` | 每个成员的归档或目录写在该目录之下 | | 用 `-p` 点名、却没有程序 target 的成员 | 它没有可打包的东西 | -| 使用被分派的 `--format` 时,没有任何提供者代表它的成员 | 该格式由成员依赖不到的包提供,或由被多个成员依赖到的包提供 | +| 使用被分派的 `--pkg-format` 时,没有任何提供者代表它的成员 | 该格式由成员依赖不到的包提供,或由被多个成员依赖到的包提供 | | 两个会写同一个归档或目录的成员 | `--output` 只点名一个目录,而名字、版本与 target 相同的两个包共用同一个归档名 | 一个为多个成员提供格式、带构建程序的包,无法同时服务它们:提供者只会得到一棵暂存树。 @@ -223,35 +264,33 @@ mcpp pack -p cli -p gui --format release `--message-format json`(mcpp 2026.9.16.1+)在命令结束后于 stdout 上输出一个 `mcpp.pack` 信封,所有给人看的行都改走 stderr。它的 `data.artifacts` 列出 产出的每一个文件或目录,带绝对路径、`type`(`file` 或 `directory`)、 -`--format` 取值以及各条腿的三元组;`data.stage` 给出暂存树、它的 manifest, +`--pkg-format` 取值以及各条腿的三元组;`data.stage` 给出暂存树、它的 manifest, 以及闭包是否已经走通(见 [50 —— 机器输出](50-machine-output.md))。对多个成员的 打包列出每个成员的产物,每一条点名它的 `member`,`data.stage` 为 null,并在 `data.stages` 中为每个成员给出一棵树(2026.10.1.1+)。这条命令 -上的 `--format` 表示的是包格式,因此机器输出改用 `mcpp test` 请求它的那种 +上的 `--pkg-format` 表示的是包格式,因此机器输出改用 `mcpp test` 请求它的那种 方式。 -### `--format` 只是一根轴,引擎只拥有它的两个取值 +### `--pkg-format` 只是一根轴,引擎拥有它的四个取值 -`tar` 与 `dir` 回答的问题,和 `msi` 与 `appimage` 回答的问题是同一个 —— -输出取什么形状 —— 所以它们是同一个 flag 的不同取值,而不是第二个 flag 的开端。 -引擎持有什么、包持有什么,分界如下: +`tar`、`dir`、`zip` 与 `single-file` 回答的问题,和 `msi` 与 `appimage` 回答的问题是同一个 —— +输出取什么形状 —— 所以它们是同一个 flag 的不同取值。引擎持有什么、包持有什么,分界如下: -> **`mcpp pack` 拥有机制,以及那一种通用格式。其余每一种格式都住在某个包里, -> 由 `mcpp pack` 分派过去。** +> **`mcpp pack` 拥有机制,以及不依赖任何其他发布的格式:一棵树、一个归档、一个文件。 +> 其余每一种格式都住在某个包里,由 `mcpp pack` 分派过去。** -那种通用格式就是它已经在产出的东西:一个解开即可运行的归档。它「通用」只在这 -里唯一要紧的那个意义上成立 —— 它不需要了解任何别人的发布方式。此外的一切都 +这四种解开或启动即可运行,不需要了解任何别人的发布方式。此外的一切都 需要。dpkg 的 control 字段、AppImage 的 runtime、WiX 的 schema、Apple 的 公证、Android 的签名方案:其中任何一个被绑进引擎,都会把一次 mcpp 的发布耦合 到一次 mcpp 并不控制的发布上。这与本项目已经为语言做过的论证是同一个 —— Slang 被支持,而引擎里没有它的名字。 -所以取值集合是开放的(mcpp 2026.9.11.1+)。`--format ` 在解析后的图里 +所以取值集合是开放的(mcpp 2026.9.11.1+)。`--pkg-format ` 在解析后的图里 找到声明了 `` 的那个包,并把暂存树交给它;一个未知的取值点名的是 **当下确实可用**的那些,而不是一份固定清单: ``` -error: unknown --format 'bogus'. +error: unknown --pkg-format 'bogus'. available in this build: tar, dir, appimage A format past `tar` and `dir` comes from a package in the resolved graph, which declares it with `mcpp::provides_pack_format("")` in its build program. Add the package @@ -267,7 +306,7 @@ error: unknown --format 'bogus'. 被分派的格式作用于一个**程序** target —— `kind = "app"` 也不例外,无论这一行 把它链接成什么文件(见 [04 §2.2](04-mcpp-toml.md))。库包发布的是一份接口 加上按三元组给出的预构建产物,没有单独一棵暂存树,所以 -`mcpp pack <库> --format ` 会被拒绝,而不是被忽略。 +`mcpp pack <库> --pkg-format ` 会被拒绝,而不是被忽略。 **产物是共享目标文件的 `kind = "app"` target 可以接受一个以上的 `--target`** (mcpp 2026.9.13.2+):在每一行 Android 上,应用*就是*平台加载的那个共享库, @@ -287,17 +326,17 @@ target,第二个 `--target` 依旧被拒绝:为多个三元组打包同一 完整选项参见 `mcpp pack --help`。 -### `mcpp run --format `(mcpp 2026.9.12.3+) +### `mcpp run --pkg-format `(mcpp 2026.9.12.3+) ```bash -mcpp run --target x86_64-linux-android --format apk -mcpp run --target aarch64-ios-sim --format app +mcpp run --target x86_64-linux-android --pkg-format apk +mcpp run --target aarch64-ios-sim --pkg-format app ``` -`--format` 与 `mcpp pack` 用的是同一个 flag,这里复用它是为了覆盖一种普通 +`--pkg-format` 与 `mcpp pack` 用的是同一个 flag,这里复用它是为了覆盖一种普通 `mcpp run` 够不到的情形:一个 Android 应用程序是一个 `.apk`,一个已安装的 iOS 应用程序是一个 `.app`,两者都不是 `mcpp run` 默认执行的链接产物。 -`mcpp run --format ` 先为 `` 打包 —— 与 `mcpp pack --format +`mcpp run --pkg-format ` 先为 `` 打包 —— 与 `mcpp pack --pkg-format ` 相同的两遍构建与暂存树 —— 再经由为一个程序解析出的 runner 运行打包 报出的那个产物:先看项目的 `[target.] runner`,其次看依赖的 `mcpp::runner(...)`,再次看载荷描述文件的。 @@ -306,16 +345,16 @@ mcpp run --target aarch64-ios-sim --format app 没有被任何其他被引入的动作当作输入的那一个输出。一个提供者常常是一条链 (`dist-apk`:链接、加库、对齐、签名),链上每个输出都会被核验存在,但只有 最后一个是发布物,也只有它以 `Packed` 报出。链的末端产出两个文件的格式会被 -`mcpp run --format` 拒绝并点名两者,因为一个 runner 只接受一个操作数。 +`mcpp run --pkg-format` 拒绝并点名两者,因为一个 runner 只接受一个操作数。 一个格式可能报出很多输出,发布目录中的每个文件各一个。不超过八个时逐行打印;更多时按它们 在共同目录之下所处的条目打印,并附数量(`Packed Release/app (1309 files)`),`--verbose` 打印全部(2026.9.29.5+)。`--message-format json` 在两种情况下都列出全部输出。 未知的 `` 会被拒绝,点名已解析图提供的格式集合,与 -`mcpp pack --format bogus` 报出的是同一个集合。`--format` 与 `--no-runner` +`mcpp pack --pkg-format bogus` 报出的是同一个集合。`--pkg-format` 与 `--no-runner` 同时出现会被拒绝 —— 一个 `.apk` 或一个已安装的 `.app` 无法被直接执行。在 -`kind = "app"` 的形态是一个库的那一行上、不带 `--format` 运行它,同样会被 +`kind = "app"` 的形态是一个库的那一行上、不带 `--pkg-format` 运行它,同样会被 拒绝,点名该 flag 与同一个格式集合。`mcpp test` 不受影响:测试二进制在每一行 上都是一个程序。 @@ -384,7 +423,7 @@ tarball 的内容包在一个顶层目录里,该目录的名字与 tarball 文 暂存树旁边的 `.stage-manifest` 列出闭包解析过的每一个库名:树里携带的 库给出其暂存路径,目标机自己提供的库记为 `platform`,哪里都找不到的记为 `unresolved`(行格式见 [50](50-machine-output.md#暂存清单))。只要有一个名字 -是 unresolved,`--format tar` 与 `--format dir` 就会失败并点名它,因为一棵 +是 unresolved,`--pkg-format tar` 与 `--pkg-format dir` 就会失败并点名它,因为一棵 缺了这个库的树在安装处无法启动;被分派的格式仍然会收到这棵树,并带上 `closure = not-walked`。目标机提供、而本机没有的库,写进 `[pack.bundle-project] also_skip`。不打包任何东西的 mode(`system`、 @@ -549,8 +588,9 @@ mcpp pack --target x86_64-windows-gnu # from a Linux host 有两点值得知道: -- 条目是**存储而非压缩**的,因此 Windows 包的体积大致等于其内容之和。压缩是 - 体积上的优化,不是正确性问题,目前尚未实现。 +- 条目在变小时由 zlib **DEFLATE 压缩**,否则存储;`--compress gzip:` 设定级别, + `--compress none` 存储全部条目。未给出格式时 Windows 程序写 zip;显式给出 `--pkg-format tar` + 则写 tar。 - 压缩包是**确定性的**:不读取任何时间戳,因此同一棵树打两次包字节完全一致, 公布出去的校验和才有意义。 @@ -591,20 +631,20 @@ dylib 的消费者,其链接所带的 `@loader_path` rpath 本就会查找的 ### ok 映像(`-openkal`):映像本身,或携带它的程序 -ok 映像经 `okrun` 在其架构的每个系统上运行(见 [24](24-openkal-cross.md))。mode -的含义不变 —— 运行该包的机器必须提供什么: +ok 映像经 `okrun` 在其架构的每个系统上运行(见 [24](24-openkal-cross.md)),怎样打包由它的平台陈述 +([35](35-platforms.md))。mode 的含义不变 —— 运行该包的机器必须提供什么: | Mode | 包内内容 | 机器需提供 | |---|---|---| | `system` | `bin/.oke` | `okrun` 与它所需的每个映像 | | `vendored`(默认) | `bin/.oke` 与它所需的 `.okso` 映像 | `okrun` | -| `self-contained` | `bin/`:本机的程序,以段(`.oke`、`__OKE,__image`)携带映像并经 libokrun 运行,旁边是所需的 `.okso` 映像 | 无 | -| `static` | 拒绝 | — | +| `self-contained` | `bin/`:本机的程序,以平台载体的段(ELF 与 COFF 上为 `.oke`,Mach-O 上为 `__OKE,__carried`)携带映像并经 libokrun 运行,旁边是所需的 `.okso` 映像 | 无 | +| `static`(`--linkage static --mode self-contained`) | `bin/`:本机的程序,携带一个静态 `.oke` | 无 | -`self-contained` 不重新编译映像:mcpp 生成入口与段,依赖与工程的 `openkal-tools` -同源的 `openkal-run`,再按任何程序的方式打包该程序 —— Linux 上为静态(musl), -macOS 与 Windows 上只引用系统自带的库。段中的字节即 `.oke` 的字节。其他系统的 -程序在该系统上打包。 +以上是单个目标的包(`--no-universal`,或平台不声明通用包)。`--linkage static` 构建不带 `.okso` 的映像。 +`self-contained` 不重新编译映像:mcpp 把平台的载体(`[platform] carrier`,一个包与它的入口源码)生成为 +`target/carrier-pack/--/` 下的工程,把映像的字节放进载体的段,再按任何程序的方式打包该程序 —— +Linux 上为静态(musl),macOS 与 Windows 上只引用系统自带的库。 ### Android:应用目标文件与它的闭包在 `lib/` 下 @@ -630,6 +670,98 @@ target/dist/myapp-0.1.0-x86_64-linux-android/ 闭包。`--mode` 不适用于这一行:设备恰好提供了平台库,因此既没有可以留给宿主 的库集合,也没有需要携带的 loader。 +### 通用包(`[platform] universal`) + +声明了 `universal = true` 的平台(openkal-platform 即是)使它任一目标的打包覆盖其全部目标;`--no-universal` +只打所选的目标。一棵树服务该平台的每个系统与每个架构: + +``` +target/dist/hello-0.1.0-openkal/ +├── bin/hello ← POSIX launcher +├── bin/hello.cmd ← Windows launcher +├── lib/x86_64/hello.oke ← one set of images per architecture +├── lib/aarch64/hello.oke +├── libexec/okrun/-/ ← only with --mode self-contained +├── share/… ← resources, once +└── mcpp-package.json +``` + +其余的腿在打包过程中构建(`Packed leg `)。启动器由平台的 `runner` 生成:以自身目录的上一级为包的 +根,按 `uname -m`(`x86_64`、`amd64` → `x86_64`;`aarch64`、`arm64` → `aarch64`)或 +`%PROCESSOR_ARCHITECTURE%` / `%PROCESSOR_ARCHITEW6432%` 选择架构目录,以该目录中的映像与自身的参数启动运行器, +先在 `libexec//-/` 中、再在 `PATH` 上查找运行器。包中没有本机架构时以状态 126 退出,写出两者。 +`[runtime] deploy` 放置的文件位于每条腿的映像旁边。`[pack] universal = false` 等同 `--no-universal`;平台未声明 +`runner` 时不能打通用包,打包被拒绝,出路为 `--no-universal`。 + +`--mode self-contained` 时运行器随包携带:对 `--systems linux,macos,windows`(缺省为本机系统)的每个系统与包的 +每个架构,mcpp 构建不携带任何内容的平台载体 —— 一个像 `okrun` 一样运行其第一个参数所指映像的程序 —— 放入 +`libexec//-/`。每个运行器在其行(`-linux-musl`、`-macos`、`-windows-gnu`)上 +以平台的托管安排构建,因此无论在哪台机器上打包结果都是同一个程序,不需要该系统的 SDK 与 C 运行时;Linux 的运行器 +为静态。本 mcpp 不构建程序的行(`x86_64-macos` 为 planned;`aarch64-windows-gnu` 不在目标表中)不放入包并给出警告, +启动器在那里从 `PATH` 查找运行器。在 Windows 主机上构建的映像与其他主机上的字节不同(#850):通用包在 Linux 或 +macOS 上打包。 + +多台机器打出的包以 `mcpp pack --merge a.zip --merge b.zip`(也可 `--merge a.zip,b.zip`)合并。输入是本命令写出的 +包目录或归档(`.zip`、`.tar`、`.tar.gz`、`.tgz`、`.tar.zst`、`.tar.xz`),不进行任何构建。腿与系统取并集,每个 +文件只取一份,结果重新描述。名字、版本、平台、链接或模式不同的输入被拒绝;同一路径有两个摘要时也被拒绝, +写出该路径、两个摘要与两个输入。容器取 `--pkg-format`,否则取 `-o` 的扩展名,否则取第一个输入的容器。 + +### 单文件包 + +`--pkg-format single-file` 交付一个可执行文件: + +| 目标与模式 | 文件 | 启动时 | +|---|---|---| +| 原生,`vendored` 或 `self-contained` | 引擎的启动器,以一个 zstd 帧携带暂存树 | 首次运行把树解到用户缓存,之后从那里启动 | +| 原生 Linux,`--linkage static` | 静态启动器加静态程序 | 同上,不依赖任何系统库 | +| ok 映像 | 平台的载体加映像,或全部映像的一个 zip | 载体从内存加载映像,不写盘 | + +原生程序的文件为 `target/dist/--`(Windows 上带 `.exe`),其树暂存在 +`target/single-file/stage/` 下。启动器是一个小程序,mcpp 每次打包写出它的源码(`target/single-file//`, +基于 mcpplibs.os 与 zstd)并为目标构建:Linux 上为静态(musl),macOS 与 Windows 上只引用系统自带的库。载荷是 +树的一个 zstd 帧(`--compress zstd[:]`,缺省,或 `none`)。首次运行把它解到 +`/mcpp/single-file/<载荷的 sha256>/`,`` 在 Linux 上为 `$XDG_CACHE_HOME`(否则 `~/.cache`), +macOS 上为 `~/Library/Caches`,Windows 上为 `%LOCALAPPDATA%`:先解到旁边一个自己的名字,全部文件写完后以不替换 +的方式改名到位(`.mcpp-complete` 标记完整的树;Linux 与 macOS 上该目录只对当前用户可写)。帧携带内容的校验和, +不符的载荷从不解开。随后启动器成为程序 —— `bin/`、self-contained 的 Linux 树根部的包装脚本,或 Windows +树根部的 `.exe` —— 带着它的参数,退出状态即程序的退出状态。 + +ok 映像的文件为 `target/dist/---`(Windows 上带 `.exe`),`--systems` 的每个系统 +(缺省为本机系统)各一个。映像不引用其他映像时载体只携带它,否则携带全部映像的一个 stored zip(每个映像对齐到 +64 KiB),载体的入口经 openkal-run 的 bundle 接口读取(openkal-platform 0.17.2 起)。 + +### `mcpp-package.json` + +每棵暂存树都带 `mcpp-package.json`:包、它的平台、三个选择、腿与系统、启动方式,以及每个文件的角色与 sha256。 +提供的格式、`--merge` 与 `mcpp emit xpkg` 读取它([SPEC-014](../specs/packaging.md) §4.5): + +```json +{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.17.3", + "linkage": "shared", "mode": "vendored", "legs": ["x86_64-openkal", "aarch64-openkal"], "systems": [], + "launch": {"runner": ["okrun", "{}"], "entry": {"x86_64": "lib/x86_64/hello.oke", "aarch64": "lib/aarch64/hello.oke"}}, + "files": [{"path": "lib/x86_64/hello.oke", "role": "image", "leg": "x86_64-openkal", "sha256": "…"}]} +``` + +每次打包都写出该文件,提供者的格式也不例外。每个文件的 `role` 为 `program`、`image`、`library`、`loader`、 +`launcher`、`resource` 或 `metadata`;通用包中 `lib//` 下的每个文件都是该腿的映像。符号链接不列出。 +`linkage` 为 `static`、`shared` 或 `declared`(每个库按它自己的声明);`launch` 对单个原生腿为 `program`, +对通用包为 `runner` 加每个架构的 `entry`。 + +`mcpp emit xpkg --package <归档> [--package …] --url <前缀> [-o <文件>]` 由本命令写出的包的 +`mcpp-package.json` 生成 xim-pkgindex 条目(spec 2)。通用包是 `linux`、`macosx`、`windows` 同样安装的一个资产, +包不携带运行器时以运行器为依赖(`deps = { "okrun" }`);各系统各架构一个的包,每个系统一项,一个带 `${arch}` 的 +URL 与每个架构的 sha256。安装把归档的顶层目录移到位,并向 xvm 注册程序(Windows 上为 `.cmd` 启动器)。 + +### 压缩与签名 + +`--compress zstd|gzip|xz|none[:level]`(或 `[pack] compress`)选择 `tar` 的压缩(`.tar.gz` 为缺省,另有 +`.tar.zst`、`.tar.xz`、`.tar`)。`tar` 写出归档,mcpp 用自己的编解码器(zlib、zstd、liblzma)压缩,因此不在 +`PATH` 上查找压缩程序,同一归档在每个宿主上字节相同。`zip` 的条目以 DEFLATE 压缩(`gzip[:level]`)或存储 +(`none`);单文件的载荷为 `zstd` 或 `none`;okb 中的映像以 stored 存放,使加载器原地映射。 + +`[pack] sign = ["", …, "{}"]` 在写容器之前对暂存树中的每个程序、映像、库与加载器各运行一次,也对每个单文件 +运行一次;`{}` 为该文件。失败使打包停止,写出 argv 与其输出;`mcpp-package.json` 写出签名后文件的摘要。 + ## 配置项 打包行为通过 `mcpp.toml` 中的 `[pack]` 节配置,常用字段如下: @@ -637,9 +769,14 @@ target/dist/myapp-0.1.0-x86_64-linux-android/ ```toml [pack] default_mode = "static" # override the normal vendored default for bare `mcpp pack` +linkage = "shared" # the default of --linkage +format = "tar" # the default of --pkg-format +compress = "zstd" # the default of --compress +sign = ["signtool", "sign", "{}"] # run on each program, image, library and loader before the container +universal = false # = --no-universal strip = true # default. false ships the artifacts as built debug_symbols = "dist/debug" # separate the debug info here instead of discarding it -include = ["share/**", "config/*.toml"] # extra files to bundle +include = ["share/**", "config/*.toml"] # 工程文件,放在包中同样的相对路径下,只放一份 exclude = ["debug/**"] # Fine-tune the vendored filtering policy. The configuration key keeps its @@ -658,12 +795,9 @@ force_bundle = ["libfoo.so"] # bundle even if matched by the PEP 600 list 参见 [`examples/03-pack-static`](../../examples/03-pack-static/) 中的 `mcpp.toml`。 -## 待支持 - -当前 `.zip` 之外的 Windows DLL 分发方式在规划中。 - -`.deb`、`.rpm`、AppImage、`.msi` 这些分发格式**不在**这份清单上,而这是一个 -决定而不是一处遗漏:它们住在各自的包里,经由 `--format ` 到达用户, -理由见上一节。`[pack]` 内建的这几种 mode 不需要再添任何新成员。 +## 当前边界 -本文档随 `mcpp pack` 的实现演进;最新选项以 `mcpp pack --help` 为准。 +- `.deb`、`.rpm`、AppImage、`.msi` 等分发格式经 `--pkg-format ` 由包提供;引擎提供 `tar`、`dir`、 + `zip` 与 `single-file`。 +- 不产出在多个系统上运行的单个文件;一个单文件包服务一个系统与一个架构。 +- ELF 程序的闭包由其动态链接器解析,因此不能在 Windows 上打包 Linux 程序。 diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 62b5334eb..891b687f7 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -507,7 +507,7 @@ system · `系统` 在机器上被找到,不是 mcpp 安装的 · `SDK` 平台 | `x86_64-windows-musl` | payload 只在 Windows 上;图则任意宿主 | 没有 gcc 能发出 PE + musl,且 LLVM 拼不出这个三元组 | | `aarch64-macos` | macOS | SDK 是那台机器自己的 | | `*-none-elf` | 每一台宿主 | clang 与 lld 按构造就是交叉编译器 | -| `*-openkal` | 每一台宿主 | 同上;映像为“无操作系统”编译,经 `okrun` 在每个系统上运行([24](24-openkal-cross.md)) | +| `*-openkal` | 每一台宿主 | 同上;映像为“无操作系统”编译,经 `okrun` 在每个系统上运行([24](24-openkal-cross.md));目标表为两行选择 `openkal-platform`([35](35-platforms.md)) | | `wasm32-emscripten`、`*-linux-android` | 每一台宿主 | SDK 自带 sysroot,且上游按宿主分别发布;一份归档服务每个 guest 架构 | **一个 `—` 讲的是 payload,不是可能性。** `host_can_serve` 回答的是 diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index b4337f3b4..9b48cd2ce 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -178,6 +178,9 @@ openkal-llvm-runtime = "0.1" 接口与实现是两列分开写的。`openkal` 是一个接口,`openkal-windows` 是它的一个 实现;把二者合并会掩盖一份源码为何能够到达多台机器。 +来源一列对工程依赖图提供的包写作 `graph`,对目标的平台加入的包写作 `platform`(2026.10.27.1+, +[35](35-platforms.md));平台的报告随后在 `Platform` 一行给出。 + ## 闭包可见性 上面这份五层报告回答的是「每一层来自哪里」,不回答图中「还有哪些别的包」—— diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 97498cdd8..9f1f3de4e 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -92,15 +92,13 @@ SDK,或者区分 `linux`/`windows`/`macos`。那些是 C 环境层或平台层 ## 工程要写的内容 ```toml -[dependencies] -openkal-llvm-runtime = "0.1.1" - -[toolchain] -default = "llvm@23.1.3" +[target.'cfg(any(linux, macos, windows))'] +platform = "openkal-platform@0.17.3" ``` -两行。第一行选定目标侧的三个层;第二行命名一个编译器,并且对其余一切来自 -何处只字未提。 +一张表(2026.10.27.1+)。它为宿主三元组选择 openkal 平台,平台的宿主部分命名编译器与目标侧三个层的包; +第四层由系统自己的 openkal 实现提供([35 —— 平台](35-platforms.md))。把 `openkal-llvm-runtime` 写成依赖、 +把 `llvm@23.1.3` 写成 `[toolchain] default` 是同一内容的手写形式,仍然有效:平台把一个层让给提供它的包。 target 在命令行上给出: @@ -126,7 +124,7 @@ mcpp build --target x86_64-windows-gnu | `openkal-musl` | `c-abi` | 被重定向到 openkal 上的 musl,只移植过一次 | | `openkal-llvm-runtime` | `compiler-runtime`、`c++-abi` | compiler-rt 内建函数、libunwind、libc++abi 与 libc++,为 openkal-musl 配置 | -一个工程命名其中最后一个,其余的由它的依赖推出。 +平台命名其中最后两个,或者工程命名最后一个;其余的由它们的依赖推出。 ## 编译器必须是 LLVM 的原因 @@ -350,8 +348,9 @@ wasm-ld: error: obj/main.o: undefined symbol: kal_process_spawn 由加载器在该架构的每个系统上运行。`okrun` 是 Linux、macOS 与 Windows 上的加载器。 ```toml -[build-dependencies] -openkal-tools = { version = "0.1.0", tools = ["okld", "okrun"] } +[package] +name = "app" +version = "0.1.0" ``` ```sh @@ -359,22 +358,24 @@ mcpp build --target x86_64-openkal # bin/app.oke mcpp run --target x86_64-openkal # okrun bin/app.oke ``` -| | mcpp 的处理 | 原因 | +无需其他声明(2026.10.27.1+):目标表为两行选择 `openkal-platform`,由平台陈述工具链、各层、工具、下表的约定 +与 runner([35](35-platforms.md))。 + +| | 平台的陈述 | 原因 | |---|---|---| | 编译 | `--target=-unknown-none-elf -fPIC -femulated-tls -fvisibility=hidden -fasynchronous-unwind-tables -Xclang -fstack-clash-protection`;AArch64 另加 `-ffixed-x18 -mno-outline-atomics`;依赖图不提供 C 库时加 `-ffreestanding` | 映像按“无操作系统”编译;不访问线程指针,不使用两个系统保留的平台寄存器;只导出源码标注 `visibility("default")` 的名字 | | 链接 | `ld.lld -shared -z max-page-size=65536 -z now --gc-sections --eh-frame-hdr`,入口 `ok_main`;被其他映像加载的映像(`kind = "shared"`)为带名字的 `lib.okso` | ET_DYN,每个段按各系统中最大的保护粒度对齐;丢弃不可达的代码,需求 note 只列出程序实际使用的接口 | | 收尾 | `okld note` 由映像的强导入写入所需接口;`okld check` 对映像及其所需的 `.okso` 检查每一条约定;通过后才发布 `bin/.oke` | 加载器拒绝需要实现所缺接口的映像并指名该接口;违反约定的映像不会被发布 | | 运行 | `okrun` 是该 target 的默认 runner,依次在依赖图为本机构建的程序、声明的载荷、PATH 中查找 | 加载器持有 openkal 实现,映像不携带实现 | -**内核接口固定,其上每一层来自依赖图。** 报告显示 `kernel-abi openkal (the -loader)`。C 库与 C++ 运行时是为该 target 提供 `mcpp:c-abi`、`mcpp:c++-abi` 的包 -(`openkal-musl`、`openkal-llvm-runtime`);两者都没有的映像也合法:它自行定义 -`ok_main` 并提供 `__emutls_get_address`。载荷的 libc++ 为载荷自身的系统配置,不 -提供给映像;依赖图中没有 C++ 运行时时,`import std;` 在编译之前被拒绝,并指名 -提供它的包。 +**内核接口固定,其上每一层都是一个包。** C 库与 C++ 运行时是平台命名的包(`openkal-musl`、 +`openkal-llvm-runtime`),报告为 `platform`;依赖图中提供该层的包取代平台的包。两者都没有的映像也合法: +`[target.'cfg(platform = "openkal")'.layers]` 关闭它们,映像自行定义 `ok_main` 并提供 +`__emutls_get_address`。载荷的 libc++ 为载荷自身的系统配置,不提供给映像;依赖图中没有 C++ 运行时时, +`import std;` 在编译之前被拒绝,并指出该层在何处被关闭。 -**缺少 `okld` 是拒绝而非警告。** 没有需求 note 的映像会被每个加载器拒绝;引擎 -转而指出应添加的依赖。 +**缺少 `okld` 是拒绝而非警告。** 没有需求 note 的映像会被每个加载器拒绝;拒绝(`platform-tool-absent`) +指名构建它的包。 ## 裸机 diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index 62e950cc7..b6057e40d 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -165,11 +165,16 @@ int main() { | `mcpp::runtime_search_dir(dir)` *(protocol 12)* | `mcpp:runtime-search-dir=` —— 见下 | | `mcpp::link_script(p)` *(2026.8.19+)* | `mcpp:link-script=` | | `mcpp::link_start(symbol)` *(协议 16)* | `mcpp:link-start=` | +| `mcpp::link_driver(d)` *(协议 17)* | `mcpp:link-driver=` —— `"lld"`(由 `ld.lld` 本身链接)或 `"compiler"`;即平台的 `link.driver`([35](35-platforms.md)) | +| `mcpp::link_input(path)` / `mcpp::link_input_from(as, token)` *(协议 17)* | `mcpp:link-input=` —— 链接所取的文件,或一个程序的输出、保存为 `as`(每次调用一个 token);即 `link.inputs` | +| `mcpp::artifact_name(kind, pattern)` *(协议 17)* | `mcpp:artifact-name=` —— `"exe"` 或 `"shared"` 与带 `{name}` 的模式;即 `link.exe`、`link.shared` | +| `mcpp::seal(step, token)` *(协议 17)* | `mcpp:seal=` —— 封装步骤 `step` 的一个 token,在链接出的文件(`{}`)发布之前运行;即 `link.seal` | +| `mcpp::run_host(h)` *(协议 17)* | `mcpp:run-host=` —— `"same-arch"` 或 `"any"`;即 `runner.host` | | `mcpp::runner(tok)` *(2026.8.19.2+)* | `mcpp:runner=` —— 见下 | | `mcpp::xpkg_dir(ns, name)` / `mcpp::xpkg_dir(name)` *(2026.8.19+)* | `[xlings.workspace]` 里声明的包的载荷目录 —— 本 manifest 声明的,或编进本构建程序的某个依赖声明的(2026.9.6.6+);没声明或没安装时返回 `""`(见下) | | `mcpp::xpkg_source(ns, name)` / `mcpp::xpkg_program(ns, name)` *(协议 15)* | 该载荷从哪里来 —— `payload`、`override`、`pending` 或 `""` —— 以及一条覆盖点名的程序 | | `mcpp::xpkg_request(ns, name)` / `mcpp::xpkg_pending()` *(协议 15)* | 一个声明了 `provision = "on-request"` 的载荷的目录;尚未安装时请求它,此时 `xpkg_pending()` 为真,程序应当返回(见下) | -| `mcpp::phase()` *(协议 15)* | 根程序陈述构建工具链时为 `"toolchain"`,其余为 `"build"` | +| `mcpp::phase()` *(协议 15)* | 根程序陈述构建工具链时为 `"toolchain"`,平台的 `plugin` 运行时为 `"platform"`(协议 17),其余为 `"build"` | | `mcpp::decision(subject, from, value, file, line, payload)` *(协议 15)* | `mcpp:decision=` —— 记录一个工具从哪里来。`mcpp.plugins.tool` 为每个成员陈述它 | | `mcpp::toolchain(key, value)` *(协议 15)* | `mcpp:toolchain=` —— 在工具链阶段陈述构建工具链。`mcpp.plugins.toolchain` 构造该陈述 | | `mcpp::warning(text)` *(2026.8.21.2+)* | `mcpp:warning=` —— 见下 | @@ -263,7 +268,7 @@ for (auto a : {"-machine","virt","-nographic","-no-reboot","-kernel"}) 调用追加到默认 runner,每次 `mcpp::runner(name, tok)` 调用追加到名为 `name` 的 runner,按发出顺序,无论调用来自这个程序的哪个库。要在两个 runner 之间选择的程序,在发出任何一个之前完成选择。以打包格式命名的 runner 在 -`mcpp run --format ` 下抵达该格式的可分发物([41 —— 抵达一台设备](41-devices.md))。 +`mcpp run --pkg-format ` 下抵达该格式的可分发物([41 —— 抵达一台设备](41-devices.md))。 ### 问,而不是声明:`toolchain_dir` / `sysroot_dir`(2026.8.19.4+) @@ -832,7 +837,7 @@ int main() { 惯常活计,而它们全都是 `artifact`:每一个都消费**链接产物**,产出用户去安装的东西。 引擎为它们加的是一套机制,而不是任何一种格式。 -`mcpp pack --format ` 把 `` 交给解析后的图去解决,方式与 `--target` +`mcpp pack --pkg-format ` 把 `` 交给解析后的图去解决,方式与 `--target` 够到一个引擎不必逐个认识的三元组相同。`tar` 与 `dir` 仍然是 `mcpp pack` 自己拥有的 归档形状;此外的一切都来自某个包。 @@ -863,13 +868,13 @@ int main() { ``` **无条件声明,有条件提交。** 声明是让引擎能回答一个发起请求的那次构建自己回答不了的 -问题:`--format bogus` 要点名**当下确实可用**的那些格式,`--help` 要说「解析后的图 +问题:`--pkg-format bogus` 要点名**当下确实可用**的那些格式,`--help` 要说「解析后的图 提供的任何格式」。两者读的都是一次「什么格式都没要」的 pass 收集到的集合。一个只在被 问到时才声明的成员,对它的作者仍然照常工作 —— 作者永远传的是自己那个格式 —— 而对其他 所有人,这个集合变成不可知的。对一个谁都没为之提交的格式,mcpp 会拒绝,而不是报告一次 「什么包都没产出」的成功打包。 -**`mcpp pack --format ` 会 prepare 两次。** 一条 `artifact` action 是一条 +**`mcpp pack --pkg-format ` 会 prepare 两次。** 一条 `artifact` action 是一条 ninja 边,而那棵暂存树是 mcpp 在链接**之后**产出的,所以这棵树不可能成为构建出它自己 那一次 pass 的输入。第一次 pass 收集声明,并在任何东西被编译之前拒绝未知的格式;随后 才是构建与暂存;第二次 pass 设上 `pack_format` 与 `pack_stage_dir`,并构建提供方提交 @@ -882,7 +887,7 @@ pass 和那次暂存已经回答过的。 而要一棵 FHS 树的格式(`.deb`、`.rpm`)自己负责重排布局,因为一个文件该落在哪个目录是 那个格式的知识,不是引擎的。 -`mcpp pack --format dir` 把同一棵树写到一个路径上就停下,这是人去查看一个成员将会拿到 +`mcpp pack --pkg-format dir` 把同一棵树写到一个路径上就停下,这是人去查看一个成员将会拿到 什么的方式。 **`mcpp pack` 报告的是这次请求**引入**的那些 artifact action。** 一条无论有没有人 @@ -901,8 +906,8 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不 **这份 manifest 的第一行是 `closure = walked` 或 `closure = not-walked`。** `mcpp pack` 会先暂存程序本身与它声明过的运行期文件,再解析该产物的依赖闭包 —— 所以这棵 树可以在闭包不完整的情况下存在:闭包里有一个名字解析不到树能携带的文件(2026.9.14.2+), -或者在 Windows 宿主上打包一个既非 PE 也非 Mach-O 的产物。`--format tar` 与 -`--format dir` 在这种情况下让命令失败,因为归档本身就是闭包;一个被分发出去的格式无论如何 +或者在 Windows 宿主上打包一个既非 PE 也非 Mach-O 的产物。`--pkg-format tar` 与 +`--pkg-format dir` 在这种情况下让命令失败,因为归档本身就是闭包;一个被分发出去的格式无论如何 都会拿到这棵树,manifest 上多出第二行 `reason = <原因>`。头部之后的 `needs` 行给出每个 被需要的名字以及满足它的东西([50 —— 机器输出](50-machine-output.md)),自行放置库的 提供方读这些行,而不是从 `lib/` 里推断闭包。 @@ -921,7 +926,7 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不 | `${mcpp.bin_dir}` | 产出的二进制所在目录:`bin/`;工作区成员的构建程序声明的 action 中为该成员的产物目录(2026.9.29.4+) | | `${mcpp.compile_db}` | `compile_commands.json` 的路径(clang-tidy 的 `-p` 要的就是它) | | `${mcpp.target_file:}` | target `` 构建出的文件 | -| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format ` 下可用 | +| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --pkg-format ` 下可用 | | `${mcpp.self}` *(2026.9.13.1+)* | 引擎自己的可执行文件,绝对路径。action 的命令是没有 shell 的 argv,构建程序因此没有可移植的拷贝手段;引擎在构建运行的每台机器上都在,`${mcpp.self} stage --verify content --output ` 拷贝一个文件、创建目标的父目录、只在字节不同时写入。这个参数形状自 2026.9.13.1 起是契约;写下它的构建程序即以该版本为下限 | `${mcpp.*}` 展开出的每个路径都用**正斜杠**拼写。token 会被拼进构建程序写下的 @@ -1148,7 +1153,7 @@ mcpp 会把它自己构建时用的**同一份** std 模块暂存过来,缓存 | `MCPP_PKG_AUTHORS` *(2026.9.11.1+)* | `mcpp::package_authors()` | `[package] authors`,以 `;` 连接。不用 `,`:一条 author 的惯例写法是 `Name `,名字里可能带逗号,以逗号连接的列表无法再切回原来的条目 | | `MCPP_PKG_REPO` *(2026.9.11.1+)* | `mcpp::package_repo()` | `[package] repo` | | `MCPP_TARGET_MIN_PLATFORM_VERSION` *(2026.9.12.3+)* | `mcpp::min_platform_version()` | 项目对这个三元组的下限,用平台自己的措辞:macOS 上是 `[build] macos_deployment_target` 或引擎自带的默认值 `14.0`;iOS 上是 `[build] ios_deployment_target` 原样给出,项目没写就是空;`*-linux-android` 上是 `[target.] min_api_level`,或已解析 NDK 载荷给出的回落值;其余每一行都是空。取的是有效三元组携带的那个值,并进入重跑键 | -| `MCPP_PACK_FORMAT` *(2026.9.11.1+)* | `mcpp::pack_format()` | 本程序所处的这次 `mcpp pack` 的 `--format` 取值;任何普通构建下都为空。承载含义的正是这个空值 —— 成员据此为自己的提交加闸,于是 `mcpp build` 拿到的还是它一直以来的那张图 | +| `MCPP_PACK_FORMAT` *(2026.9.11.1+)* | `mcpp::pack_format()` | 本程序所处的这次 `mcpp pack` 的 `--pkg-format` 取值;任何普通构建下都为空。承载含义的正是这个空值 —— 成员据此为自己的提交加闸,于是 `mcpp build` 拿到的还是它一直以来的那张图 | | `MCPP_PACK_STAGE_DIR` *(2026.9.11.1+)* | `mcpp::pack_stage_dir()` | `mcpp pack` 已经把闭包暂存到的位置,绝对路径;本次构建不在打包时为空。读它来判断这次要干的活是什么形状,而把 `${mcpp.stage_dir}` 写进 action —— 这样图里的路径与程序读到的路径不可能不一致 | | `MCPP_PACK_STRIP` *(2026.9.16.1+)* | `mcpp::pack_strip()` | 本程序所处的这次 `mcpp pack` 剥离时为 `1`,`--no-strip` 下为 `0`;任何普通构建下都为空。自己暂存库的成员跟随它,于是一个开关管住包里的每个文件 | | `MCPP_PACK_DEBUG_SYMBOLS_DIR` *(2026.9.16.1+)* | `mcpp::pack_debug_symbols_dir()` | `--debug-symbols` 放置分离出的 `*.debug` 文件的位置,绝对路径;丢弃调试信息或本次构建不在打包时为空 | diff --git a/docs/zh/31-authoring-a-rule-package.md b/docs/zh/31-authoring-a-rule-package.md index f6a673e58..641a05079 100644 --- a/docs/zh/31-authoring-a-rule-package.md +++ b/docs/zh/31-authoring-a-rule-package.md @@ -313,7 +313,7 @@ mcpp::floor("cuda.driver", runtime_needs); 一个 `dist-*` 成员既不是规则也不是工具。它不编译翻译单元,也不在构建程序 运行期间把活干完:它消费**链接产物**,产出用户要去安装的东西 —— 一个 `.msi`、一个 `.deb`、一个 AppImage、一个签过名的 `.app`。机制是一条 -`artifact` action 加上 `mcpp pack --format `,见 +`artifact` action 加上 `mcpp pack --pkg-format `,见 [30 —— 产出可分发物](30-build-mcpp.md#产出可分发物pack_format-与-stage_dir20269111)。 本章其余内容原样适用。有六条在这里绑得更紧,每一条都是这个类别会犯、而 diff --git a/docs/zh/34-authoring-a-bsp.md b/docs/zh/34-authoring-a-bsp.md index 762c98ce2..f4620cad1 100644 --- a/docs/zh/34-authoring-a-bsp.md +++ b/docs/zh/34-authoring-a-bsp.md @@ -112,6 +112,25 @@ int main() { **一个探针,一个使用者。** `mcpp::run_exclusive()` 声明这个目标的运行不可 重叠,`mcpp test` 随后把它们串行化。工程方永远不需要记得加 `-j1`。 +## 作为平台包的板子 + +板子的构建程序陈述的字段——链接脚本、入口、runner 与具名 runner——都是平台的字段(2026.10.27.1+, +[35 —— 平台](35-platforms.md))。BSP 可以改在清单中陈述其中固定的部分,构建程序只保留随所选 feature 变化的部分: + +```toml +[platform] +targets = ["thumbv7em-none-eabihf"] +runner = ["qemu-system-arm", "-machine", "mps2-an386", "-nographic", "-kernel", "{}"] +runners = { flash = ["probe-rs", "download", "--chip", "STM32L475VG", "{}"] } + +[platform.link] +script = "cortex-m.ld" +start = "Reset_Handler" +``` + +工程随后把这块板子选为目标的平台(`[target.thumbv7em-none-eabihf] platform = "@"`),而不是作为依赖。 +两种写法合并为一份陈述:构建程序陈述的值替换清单中的值,工程的 `[target.]` 键替换二者。 + ## 当前边界 - BSP 把它的模拟器与探针驱动声明为载荷,因此工具链未在某个平台发布的板子, diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md new file mode 100644 index 000000000..2a0b2c5a4 --- /dev/null +++ b/docs/zh/35-platforms.md @@ -0,0 +1,243 @@ +# 35 —— 平台 + +**读者:** 为一个由平台包配置的目标构建的人(ok 映像,`-openkal`)、要改动该配置一部分的人, +以及为一类目标编写平台包的人。 + +**本章回答的那一个问题:** 一类目标怎样作为一个包来配置,构建怎样选择它,工程怎样覆盖或替换它。 + +**不在这里:** 平台所命名的目标侧各层,见 [22 —— 目标侧](22-target-side.md);openkal 的模型与宿主上的 +交叉构建,见 [24 —— 基于 openkal 的交叉构建](24-openkal-cross.md);打包一个程序,见 +[10 —— 发布打包](10-pack-and-release.md);精确规则,见 [SPEC-013](../specs/platforms.md)。 + +在此之前:[34 —— 编写板级支持包](34-authoring-a-bsp.md)。 +在此之后:[40 —— 裸机与 freestanding 目标](40-baremetal.md)。 + +**平台**是一个带版本的包,陈述一类目标所需的全部内容:工具链、目标侧各层的提供包、构建在本机运行的 +工具、编译与链接约定、产物的命名与封装,以及运行器。工程一项都不必写。目标表为 `x86_64-openkal` 与 +`aarch64-openkal` 选择 `mcpplibs.openkal-platform`(2026.10.27.1+),这两个目标的内置支持即是如此。 + +## 最短可跑 + +一个只写出源码的工程: + +```toml +[package] +name = "app" +version = "0.1.0" +``` + +```sh +mcpp run --target x86_64-openkal +``` + +``` + Resolved llvm@23.1.3 → x86_64-openkal → …/xim-x-llvm/23.1.3/bin/clang++ + the platform's (openkal-platform 0.17.3), replacing your gcc@16.1.0 — override with `[target.x86_64-openkal] toolchain` + Target x86_64-openkal → x86_64-unknown-none-elf + compiler-runtime compiler-rt (openkal-llvm-runtime@0.16.6, platform) + c-abi musl (openkal-musl@0.22.6, platform) + c++-abi libc++ (openkal-llvm-runtime@0.16.6, platform) + Platform openkal-platform 0.17.3 (built-in) + tools okld, okrun (openkal-tools@0.1.3, platform) + runner okrun (platform) + link lld, start ok_main (platform) + Compiling app v0.1.0 (.) + Running `okrun … target/x86_64-openkal/…/bin/app.oke` +``` + +每个值都带来源报告。`mcpp test`、`mcpp pack` 与 `--target aarch64-openkal` 无需更多声明。`mcpp.lock` 以 +`origin = "platform"` 记录平台加入的包,并以 `targets` 写出每个条目的配置(只属于本机原生配置的条目不写), +因此为本机的构建与为 ok 映像的构建不互相改写对方的条目。 + +## 平台陈述的内容 + +| 组 | 键 | 作用 | +|---|---|---| +| 目标 | `targets`、`toolchain`、`sysroot` | 服务哪些三元组、它们使用的工具链、预构建的目标基 | +| 层 | `[platform.layers]` | `c-abi`、`c++-abi`、`compiler-runtime`、`kernel-abi` 的提供包,或 `false` | +| 工具 | `[platform.tools]` | 为本机构建的程序,或查找到的程序(`{ program = "" }`) | +| 编译 | `[platform.build]`、`[platform.build.'cfg(…)']` | 目标每个翻译单元的标志 | +| 链接 | `[platform.link]` | `driver`、`flags`、`start`、`script`、`inputs`、名字 `exe` 与 `shared`、封装步骤 `seal` | +| 运行 | `runner`、`runners` | `mcpp run` 与 `mcpp test` 怎样启动产物,以及具名的设备动作 | +| 载体 | `carrier` | `mcpp pack` 为依赖加载器的产物按系统构建的程序:self-contained 的程序、通用包的运行器、单文件 | +| 包 | `universal` | 任一目标的打包覆盖全部目标,成为一个包([10](10-pack-and-release.md)) | +| 代码 | `plugin` | 在平台阶段运行的构建程序,陈述随依赖图而变的部分 | +| 版本组 | `[platform.versions]` | 一个家族中每个包的一个版本 | +| 宿主部分 | `[platform.hosted]` | 宿主三元组选用该平台时适用的部分 | + +## 覆盖平台的一部分 + +工程在 `[target.]` 下以平台自己的键名覆盖一个字段。`cfg(platform = "openkal")` 不论架构都匹配该平台: + +```toml +[target.'cfg(platform = "openkal")'.layers] +c-abi = { path = "../openkal-musl" } # a working tree in place of the release + +[target.'cfg(platform = "openkal")'.tools] +okld = { path = "../openkal-image/tools" } + +[target.'cfg(platform = "openkal")'] +runner = ["my-okrun", "{}"] +``` + +| 字段 | 规则 | +|---|---| +| 编译标志、`link.flags` | 追加在平台的之后;约定不能删除 | +| `link.inputs`、`link.seal` | 整体替换 | +| `layers`、`tools`、`runners` | 按键替换;` = false` 不提供该层 | +| `runner` | argv 替换;未写出时保留 `host` 与 `exclusive` | +| 其余字段 | 替换 | + +同样的表也可以写在 `~/.mcpp/config.toml` 中,对一台机器生效。图中已有包提供的层交给该包,报告为 `graph`。 +C++ 运行时的包同时提供编译器运行时,因此 `"c++-abi" = false` 只有与 `compiler-runtime = false` 同时写出才生效。 + +## 选择平台,或不用平台 + +第一个给出答案的来源决定目标的平台: + +| 来源 | 写法 | +|---|---| +| 命令行 | `--platform openkal-platform@0.17.3`、`--platform path:../openkal-platform`、`--platform none` | +| 环境变量 | `MCPP_TARGET_X86_64_OPENKAL_PLATFORM=…` | +| 工程 | `[target.] platform = "openkal-platform@0.17.3"`,或 `{ path = "…" }` | +| 本机 | `~/.mcpp/config.toml` 中的同一个键 | +| 目标表 | 平台列(`mcpp toolchain list`) | + +`platform` 不能写在 `cfg(platform = …)` 选择器下。`none` 只以引擎对该目标的约定构建。 + +## 宿主图的平台 + +构建程序与构建运行的工具按本机的约定为本机编译。`[host] platform` 为它们选择一个平台: + +```toml +[host] +platform = "openkal-platform@0.17.3" +``` + +``` + Host platform openkal-platform 0.17.3 (mcpp.toml:2): build programs are x86_64-openkal images run by okrun +``` + +此后每个构建程序都编译为本机架构的 ok 映像,在 Linux、macOS 与 Windows 上使用同一个工具链,经平台的运行器 +运行。平台自己的工具仍以原生方式构建。`--host-platform` 与 `MCPP_HOST_PLATFORM` 对一次运行选择它, +`~/.mcpp/config.toml` 中的 `[host]` 对一台机器选择它。 + +## 宿主上的安排 + +宿主三元组选用一个平台时,只适用平台的 `[platform.hosted]` 部分:它的工具链与它的层。对 openkal-platform 而言, +即家族的 C 库与 C++ 运行时,位于系统自己的 openkal 实现之上: + +```toml +[target.'cfg(any(linux, macos, windows))'] +platform = "openkal-platform@0.17.3" +``` + +Windows 上这一安排对应 GNU ABI 的行(`--target x86_64-windows-gnu`);`[platform.hosted] targets` 之外的 +三元组被拒绝,消息给出它所服务的同系统同架构的行。 + +## 版本组 + +`[platform.versions]` 为一个包与包之间精确钉住的家族列出每个包的一个版本。依赖图中出现其中某个包的 +另一个版本时,在任何编译之前拒绝: + +``` +error: openkal-musl 0.22.2 is in this build, and the platform openkal-platform 0.17.3 is a version group that names openkal-musl 0.22.6. + requested by: own + The family's packages pin one another exactly, so two versions of one of + them cannot be linked into one program. Either select the platform release + of that version ([target.] platform = "openkal-platform@"), or write + openkal-musl = "0.22.6" where it is requested. +``` + +从目录或仓库取得的包不受版本组约束。 + +## 编写平台包 + +平台包是带 `[platform]` 段的包。openkal-platform 的节选: + +```toml +[package] +namespace = "mcpplibs" +name = "openkal-platform" +version = "0.17.3" +mcpp = ">=2026.10.27.1" + +[platform] +targets = ["x86_64-openkal", "aarch64-openkal"] +toolchain = "llvm@23.1.3" +runner = { cmd = ["okrun", "{}"], host = "same-arch" } +carrier = { package = "openkal-run@0.1.3", section = ".oke", main = "carrier/main.cpp" } +universal = true +plugin = "build.mcpp" + +[platform.layers] +compiler-runtime = "openkal-llvm-runtime@0.16.6" +"c++-abi" = "openkal-llvm-runtime@0.16.6" +c-abi = { name = "openkal-musl", version = "0.22.6", default-features = false, features = ["posix"] } + +[platform.tools] +okld = "openkal-tools@0.1.3" +okrun = "openkal-tools@0.1.3" + +[platform.build] +cxxflags = ["-fPIC", "-femulated-tls", "-fvisibility=hidden", "-fasynchronous-unwind-tables"] + +[platform.build.'cfg(arch = "aarch64")'] +cxxflags = ["-ffixed-x18", "-mno-outline-atomics"] + +[platform.link] +driver = "lld" +start = "ok_main" +flags = ["-shared", "-z", "max-page-size=65536", "-z", "now", "--gc-sections", "--icf=safe", "--eh-frame-hdr"] +inputs = [{ from = ["okld", "note-source"], as = "ok-notes.S" }] +exe = "{name}.oke" +shared = "lib{name}.okso" +seal = [["okld", "note", "{}"], ["okld", "check", "{}", "--with", "{needed}"]] +``` + +- **程序写名字,不写位置。** `inputs` 与 `seal` 中的 `okld` 依次在图为本机构建的程序、声明的载荷、`PATH` 中查找。 +- **`{}` 是产物。** 封装步骤中的 `{needed}` 展开为产物所需的共享映像;它前面的 `--with` 在每个映像前重复。 +- **封装步骤失败时不发布。** 上一次成功构建发布的产物保留原位。 +- **随依赖图而变的部分写成代码。** `plugin = "build.mcpp"` 在平台阶段运行(`mcpp::phase()` 为 `"platform"`), + 可以陈述标志、链接字段与运行器(`mcpp::link_driver`、`mcpp::link_input_from`、`mcpp::artifact_name`、 + `mcpp::seal`、`mcpp::run_host`);其他指令被拒绝,平台提供的包格式除外(`pack-format` 与 `artifact` + action,例如由暂存树生成 `okb`)。板级支持包的构建程序在构建阶段陈述同样的字段 + ([34](34-authoring-a-bsp.md))。 +- **载体的入口读取它携带的内容。** mcpp 把携带的字节放在 `mcpp_carried_begin` 与 `mcpp_carried_end` 之间 + (入口的名字在 `mcpp_carried_name`):程序为通用包的运行器时不携带任何内容,运行其第一个参数所指的映像; + 或映像本身;或映像引用其他映像时的单文件所携带的全部映像的一个 stored zip(`PK\x03\x04`,每个映像对齐到 + 64 KiB)([SPEC-013](../specs/platforms.md) §11)。 + +开发期间,工程以 `--platform path:` 或 `platform = { path = "" }` 选择工作树。在平台包中执行 +`mcpp test` 而不给 `--target` 时,以该包为平台为其每个 `targets` 构建测试,运行本机架构的那些,其余报告为 +已构建、未运行。 + +## 查看一个配置 + +```sh +mcpp self env --target x86_64-openkal # toolchain, platform, tools, runner +mcpp self env --target x86_64-openkal --format json +mcpp self env --host # the host graph, its runner and tools once its build programs were built +mcpp toolchain list # the PLATFORM column +mcpp toolchain install --target x86_64-openkal # fetch and build what the target needs +``` + +`--message-format json` 在 `plan` 事件与 `result.json` 中给出 `platform` 与 `hostPlatform` +([50](50-machine-output.md))。 + +## 拒绝 + +| 原因 | 条件 | 出路 | +|---|---|---| +| `platform-not-a-platform` | 所选的包没有 `[platform]` | 选择一个平台包,或 `none` | +| `platform-target-unserved` | 平台不服务该三元组 | 为它的某个 `targets` 构建,或为消息给出的宿主行构建 | +| `platform-tool-absent` | 链接或封装要运行的程序既未构建、也未找到 | 在 `[target.'cfg(platform = "…")'.tools]` 下写出构建它的包 | +| `platform-version-group` | 家族中某个包的另一个版本在图中 | 选择携带该版本的平台版本,或钉住版本组中的版本 | +| `platform-phase-directive` | 插件陈述了平台阶段不接受的指令 | 改由包自己的构建程序陈述 | +| `lld-required-absent` | 平台以 `ld.lld` 链接,而工具链没有它 | 使用平台的工具链 | + +## 当前边界 + +- 一个平台的每个字段只有一个值;随架构不同的字段只有标志可以写在 `[platform.build.'cfg(arch = "…")']` 下。 +- 宿主图的平台按一次运行或一个工程选择;缺省为宿主约定。 +- `[host]` 只接受 `platform`。 diff --git a/docs/zh/41-devices.md b/docs/zh/41-devices.md index f940db252..b6a29a4c5 100644 --- a/docs/zh/41-devices.md +++ b/docs/zh/41-devices.md @@ -84,6 +84,17 @@ monitor = ["probe-rs", "attach", "--chip", "STM32L475VG"] 被默默应用。同一个名字只允许一个依赖提供,出现第二个是一个点名两个包的 错误。 +runner 也可以是一张表(2026.10.27.1+),陈述产物在哪里运行、运行能否重叠: + +```toml +[target.'cfg(platform = "openkal")'] +runner = { cmd = ["okrun", "{}"], host = "same-arch", exclusive = false } +``` + +`host = "same-arch"` 只在产物架构的机器上运行它,其他架构的产物被拒绝并点名 `run_under`;`"any"` 表示 +runner 能从任何机器抵达产物。目标的平台提供它的缺省 runner([35](35-platforms.md));依赖的构建程序、本机的 +`config.toml`、工程与 `MCPP_TARGET__RUNNER` 依次替换它,只写 argv 时保留平台的 `host` 与 `exclusive`。 + ## runner 收到的内容 runner 收到一段 argv——它自己的 token,产物路径被追加或替换 `{}`—— @@ -103,7 +114,7 @@ libfw.so/abs/project/target///bin/libfw.so 第一个字段是相对于产物所在目录的目的路径,分隔符为 `/`;对于在子目录中 发现的测试程序,它以 `../` 开头。第二个字段是该文件在输出树中的绝对 路径。两者以 TAB 分隔。每次启动 runner 时这个文件都存在,没有需要携带 -的东西时为空;以 `--format` 运行的可分发物收到的是空文件,因为可分发物 +的东西时为空;以 `--pkg-format` 运行的可分发物收到的是空文件,因为可分发物 自带其文件。 就地执行产物的 runner 会忽略这个变量。把产物移走的 runner——移到设备、 @@ -112,7 +123,7 @@ libfw.so/abs/project/target///bin/libfw.so ## 运行一个可分发物 -`mcpp run --format ` 打包可分发物 `` 并运行它。不带 `--runner` +`mcpp run --pkg-format ` 打包可分发物 `` 并运行它。不带 `--runner` 时,若图或清单提供了名为 `` 的 runner,就由它抵达,否则由默认 runner 抵达: @@ -121,12 +132,12 @@ mcpp::runner("app", ""); // a package names a runner after its fo ``` ```bash -mcpp run --format app # through the runner named `app` -mcpp run --format app --runner other # a typed --runner still wins +mcpp run --pkg-format app # through the runner named `app` +mcpp run --pkg-format app --runner other # a typed --runner still wins ``` 目录形式的可分发物(例如应用包)不会被直接执行。没有 runner 能抵达它 -时,`mcpp run --format ` 会在启动任何东西之前拒绝,退出状态为 +时,`mcpp run --pkg-format ` 会在启动任何东西之前拒绝,退出状态为 `126`,并点名能抵达它的 runner `` 以及本该声明它的清单表。 ## 终止由声明决定,不由推断 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index bcd88fbf5..c7c0acb35 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -91,10 +91,10 @@ mcpp --format json 目前只支持 `json`。`ndjson` 为将来真正需要流式的场景保留,**现在不被接受** —— 请求它是一个错误,而不是静默地回落到别的格式。 -`--format` 已经用来表示某种**产物**的命令,改用 `--message-format json` 来 -请求机器输出,`mcpp test` 就是这样做的。`mcpp pack --format` 表示的是包格式 -(`tar`、`dir`、`msi`),所以它的报告方式是 -`mcpp pack --message-format json`(2026.9.16.1+)。具体形状随 kind 而定: +构建类命令改用 `--message-format json` 来请求机器输出,`mcpp test` 就是这样做的: +`build`、`test`、`run` 与 `pack`。`mcpp pack` 与 `mcpp run` 的包格式是 `--pkg-format` +(`tar`、`zip`、`dir`、`single-file`、`msi`;2026.10.27.1+;在这两个命令上 `--format` 是旧写法,会给出警告),所以打包的 +报告方式是 `mcpp pack --message-format json`(2026.9.16.1+)。具体形状随 kind 而定: `mcpp test` 的测试是随时间陆续完成的,所以它按测试逐条输出流式记录;一次 `mcpp pack` 只有一个结果,所以它输出一个信封。`mcpp build --message-format json` (2026.10.26.1+)在构建过程中输出事件流,以一个信封结束(§9)。 @@ -219,8 +219,8 @@ mcpp cache list --json -> {"root": …, "entries": [ … ]} |---|---| | `0`–`124` | 程序运行过了;这是它自己的状态,原样透传。例外是 `101`,见下一行 | | `101` | `mcpp run` 无法构建程序:规划或构建失败,没有启动任何东西。这是 `cargo run` 构建失败时 Cargo 给出的状态 | -| `125`–`127` | 尝试过启动但被拒绝 —— `127` 是找不到,`126` 是找到了但不可执行,`125` 是其他原因;对 `mcpp run --format ` 而言,一个是目录、且没有任何 runner 能到达的分发物同样得到 `126`,它在启动之前就被拒绝,含义相同(2026.9.14.2+) | -| `2` | mcpp 在构建任何东西之前拒绝了请求,或在构建之后、启动任何东西之前拒绝了请求:用法错误、`--format` 与 `--no-runner` 同用、项目没有声明的程序或 runner | +| `125`–`127` | 尝试过启动但被拒绝 —— `127` 是找不到,`126` 是找到了但不可执行,`125` 是其他原因;对 `mcpp run --pkg-format ` 而言,一个是目录、且没有任何 runner 能到达的分发物同样得到 `126`,它在启动之前就被拒绝,含义相同(2026.9.14.2+) | +| `2` | mcpp 在构建任何东西之前拒绝了请求,或在构建之后、启动任何东西之前拒绝了请求:用法错误、`--pkg-format` 与 `--no-runner` 同用、项目没有声明的程序或 runner | 在 2026.9.30.2 之前,构建失败退出 `1`,规划失败退出 `2`,脚本因此无法区分 一次编译错误和一个返回 `1` 的程序。现在构建失败是 `101`;`mcpp build`、 @@ -298,6 +298,30 @@ mcpp self env --format json 解析逻辑 —— 包括「PATH 上的 `mcpp` 可能是一个 xlings shim 而不是真正的 二进制」这一部分。 +**一个配置** *(2026.10.27.1+)*。带 `--target `(宿主图用 `--host`)时,`self env` +准备该配置而不构建,并报告它;在工程中是该工程的配置,工程之外是 home 缓存中一个空程序的配置。 +`--platform` 与 `--host-platform` 的选择与构建相同([35](35-platforms.md))。 + +```jsonc +{ + "triple": "x86_64-openkal", + "graph": "target", // or "host" + "toolchain": {"compiler": "/…/xim-x-llvm/23.1.3/bin/clang++", "origin": "platform"}, + "platform": {"name": "mcpplibs.openkal-platform", "version": "0.17.3", "source": "built-in", + "hosted": false, "fields": {"runner": {"value": ["okrun", "{}"], "source": "platform"}}}, + "tools": {"okld": "/…/okld", "okrun": "/…/okrun"}, + "runner": {"argv": ["okrun", "{}"], "host": "same-arch", "exclusive": false, + "program": "/…/okrun", "source": "platform"}, + "runUnder": [] +} +``` + +未选择平台时 `platform` 为 `null`;其形状与 `plan` 事件的相同(§9)。运行器指名平台自己的工具时 +`runner.program` 为该工具,否则为本机找到的程序。`--host` 时,工程的构建程序已在宿主平台下构建过,才报告宿主平台 +的 `runner` 与 `tools`。工程源码会被拒绝的原因 +(图中没有 C++ 运行时而 `import std`)是一条代码为 `MCPP_CONFIGURATION_SOURCES_REFUSED` 的 +`warning` 诊断,配置照常报告。无法准备的配置带 `MCPP_CONFIGURATION_FAILED`,没有 `data`。 + `defaultToolchain` 是在这台宿主上、什么都没有配置时一次构建所解析的工具链:首次运行 安装的正是它,各 CI 宿主也以它核对 docs/01 与 docs/20 的表格。在 Windows 上,它取决于 是否存在可用的 MSVC(来自 Visual Studio 或受管的工具集)。 @@ -408,6 +432,8 @@ mcpp toolchain list --format json `source: "system"` —— 它是在机器上被定位到的,不是由 mcpp 安装的。一行目标 的形状是 `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned` 之一。 +目标行另有 `platform`(2026.10.27.1+):目标表为该目标选择的平台包 +(`mcpplibs.openkal-platform@0.17.3`),或 `null`。 **`toolchain` 与 `pin` 不是把同一个字段写了两遍。** `toolchain` 表示这一行 关联到什么 —— 已装的行是装好的载荷,词表行是它自己的约定。`pin` 只承载目标 @@ -516,7 +542,11 @@ replaced}` —— `origin` 与构建的状态行使用的是同一句话 | `std-module-precompile` | 标准库的模块在这个配置下无法被预编译 | | `msvc-redist-unavailable` | 在 MSVC ABI 的行上显式写了 `cxx_runtime = "toolchain-coupled"`,而该行的工具集没有可放置的 redistributable 目录 *(2026.9.28.1+)* | | `crt-declared-under-host-coupled` | 程序的契约是 host-coupled(由系统的运行时服务、不放置任何副本),而程序旁声明了 MSVC C++ 运行时的文件 *(2026.9.28.2+)* | -| `image-tool-absent` | 一个 `-openkal` 的构建要链接一个映像,而找不到 `okld` —— 为 ok 映像写需求注记并检查约定的、图中的程序 *(2026.10.26.2+)* | +| `platform-not-a-platform` | 被选为该目标平台的包没有 `[platform]` 段 *(2026.10.27.1+)* | +| `platform-target-unserved` | 所选平台不服务该目标:三元组不在其 `targets` 中,宿主三元组也不在其 `[platform.hosted] targets` 中 *(2026.10.27.1+)* | +| `platform-tool-absent` | 平台的链接或封装要运行的程序(ok 映像的 `okld`)既不由图构建,也不在本机 *(2026.10.27.1+;2026.10.26.2 中为 `image-tool-absent`)* | +| `platform-version-group` | 平台版本组中的一个包以另一个版本出现在图中 *(2026.10.27.1+)* | +| `platform-phase-directive` | 平台插件陈述了其阶段不接受的指令 *(2026.10.27.1+)* | | `other` | 一个尚未被赋予记号的拒绝分支 | **其中一个记号也由 `mcpp build` 自己打印。** `interface-not-provided` 会 @@ -605,7 +635,7 @@ mcpp emit build-database [--spec s1|compile-commands] --format json ### `mcpp.pack` —— 一次打包的产物 *(mcpp 2026.9.16.1+)* ``` -mcpp pack [target] [--format ] [--target ...] --message-format json +mcpp pack [target] [--pkg-format ] [--target ...] --message-format json ``` 信封在命令结束后输出一次;所有给人看的行都改走 stderr,包括打包过程启动的 @@ -613,8 +643,10 @@ mcpp pack [target] [--format ] [--target ...] --message-format json | 字段 | | |---|---| -| `artifacts` | 每个产出的产物一条记录:`path`(绝对路径)、`type`(`file` 或 `directory`)、`format`(`--format` 的取值,省略时为 `tar`),以及 `targets`(进入该产物的每条腿的规范三元组)。被分派的格式报告本次请求引入的那些 action 的终端输出;一个多 `--target` 的 Android 打包报告一个产物,其 `targets` 列出每一条腿 | +| `artifacts` | 每个产出的产物一条记录:`path`(绝对路径)、`type`(`file` 或 `directory`)、`format`(容器:`--pkg-format` 的取值;未给出时 Windows 程序为 `zip`,其余为 `tar`)、`targets`(进入该产物的每条腿的规范三元组),以及 `sha256`(文件的摘要;目录为 `null`;2026.10.27.1+)。被分派的格式报告本次请求引入的那些 action 的终端输出;一个多 `--target` 的 Android 打包报告一个产物,其 `targets` 列出每一条腿 | | `stage` | 该产物所来自的那棵树:`dir`、`manifest`(即下文的暂存清单)与 `closure`(`walked` 或 `not-walked`);库包、没有暂存任何树的情形,以及对多个成员的打包,此字段为 `null` | +| `linkage`、`mode`、`pkgFormat` *(2026.10.27.1+)* | 本次打包的三个选择([10](10-pack-and-release.md)):`linkage` 为 `static`、`shared` 或 `declared`;`pkgFormat` 为产物的 `format`(`tar`、`zip`、`dir`、`single-file` 或提供者的格式) | +| `legs`、`systems` *(2026.10.27.1+)* | 每条腿的规范三元组(通用包列出其平台的每个目标),以及自带加载器的通用包所携带的每个运行器的 `-` | | `stages` *(2026.10.1.1+)* | 只在对多个成员的打包(`--workspace`,或重复的 `-p`)中出现:每个成员一条记录,按 `[workspace] members` 的顺序,带 `member`(限定的包名),以及与 `stage` 相同的 `dir`、`manifest`、`closure` | 对多个成员的打包,`artifacts` 按同样的成员顺序列出每个成员的产物,每条记录多出 @@ -757,7 +789,7 @@ mcpp build [target] [-p ] [--release] --message-format json | 事件 | 携带 | |---|---| -| `plan` | `configuration`:构建已进入其构建目录的那个配置 | +| `plan` | `configuration`:构建已进入其构建目录的那个配置;`platform` 与 `hostPlatform`(2026.10.27.1+)是目标图与宿主图的平台,或 `null` | | `waiting` | `holder`(`pid`、`command`、`client`、`startedAt`)与 `waitedMs`:另一次构建持有这个构建目录,本次在等待(§11) | | `progress` | `finished`、`total`:ninja 的步骤计数;一次构建可以运行不止一轮 ninja,每轮重新计数 | | `diagnostic` | `diagnostic`:失败步骤输出中的一条编译器诊断,形状同下文记录的 `diagnostics[]` | @@ -783,7 +815,7 @@ mcpp build [target] [-p ] [--release] --message-format json | `pid` | 写下记录的进程 | | `startedAt`、`finishedAt` | 自纪元起的毫秒数;`running` 期间与 `cancelled` 之后 `finishedAt` 为 `null` | | `status` | `running`、`ok`、`failed` 或 `cancelled` | -| `configuration` | `triple`、`profile`、`fingerprint`、`outputDir` | +| `configuration` | `triple`、`profile`、`fingerprint`、`outputDir`、`platform`、`hostPlatform`(2026.10.27.1+) | | `compileDatabase` | 该配置的 `compile_commands.json`,或 `null` | | `artifacts` | `name`、`kind`(`bin`、`test`、`shared`、`static`)、`path`;可运行的产物另有 `launch`;陈述了程序起点或入口符号时另有 `entry` | | `diagnostics` | 失败构建的编译器诊断:`severity`、`message`、`path`、`range` | @@ -817,7 +849,7 @@ mcpp run [target] --print-launch 按 `mcpp run` 的方式构建程序,然后打印信封而不启动它。`data` 为 `program`、 `argv`、`cwd`、`env` 与 `via`,同上文的 `launch`。`--print-launch` 不接受 -`--format`:在 `mcpp run` 上,`--format` 表示可分发物的格式。 +`--format`:在 `mcpp run` 上,`--pkg-format` 表示可分发物的格式。 ## 10. 拒绝副作用 —— `--deny` *(mcpp 2026.10.26.1+)* @@ -879,13 +911,13 @@ mcpp index list --format json `data` 为 `registries`(`name`、`url`、`default`)与 `indices`(在工程中运行时, 工程的 `[indices]` 条目)。包的目录由 `mcpp search` 回答。 -### `mcpp.pack.formats` —— `mcpp pack --format` 接受的格式 +### `mcpp.pack.formats` —— `mcpp pack --pkg-format` 接受的格式 ``` mcpp pack --list-formats ``` -`data.formats` 列出引擎自己的格式(`tar`、`dir`;`builtin: true`)以及已解析依赖 +`data.formats` 列出引擎自己的格式(`tar`、`zip`、`dir`、`single-file`;`builtin: true`)以及已解析依赖 图中某个包提供的每种格式(`builtin: false`,带 `providers`)。依赖图在 mcpp home 的 一个目录中规划,因此不向工程写入任何东西。 diff --git a/docs/zh/90-build-from-source.md b/docs/zh/90-build-from-source.md index cbce28e3e..7d6c0097e 100644 --- a/docs/zh/90-build-from-source.md +++ b/docs/zh/90-build-from-source.md @@ -83,18 +83,18 @@ src/ └── scaffold/ `mcpp new` and templates tests/ -├── unit/ 154 C++ tests, discovered by `mcpp test` -└── e2e/ 635 shell scripts against a real binary +├── unit/ 156 C++ tests, discovered by `mcpp test` +└── e2e/ 645 shell scripts against a real binary ``` ## 测试组织 两层,回答的是不同的问题。 -**单元测试**(`tests/unit/`,154 个文件)是 `mcpp test` 发现的 C++ +**单元测试**(`tests/unit/`,156 个文件)是 `mcpp test` 发现的 C++ 程序。它们检验一个模块的契约,不需要二进制、也不需要文件系统状态。 -**端到端测试**(`tests/e2e/NN_.sh`,635 个文件)拿真实的 `mcpp` +**端到端测试**(`tests/e2e/NN_.sh`,645 个文件)拿真实的 `mcpp` 二进制跑真实工程。`run_all.sh` 是 CI 的入口。`mcpp test` **不**运行它们。 ```bash diff --git a/docs/zh/README.md b/docs/zh/README.md index 5a300e6aa..6ebd79aa2 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -25,11 +25,11 @@ | 写一个程序 | [01](01-getting-started.md)、[04](04-mcpp-toml.md) §1 | [`01-hello`](../../examples/01-hello/)、[`02-with-deps`](../../examples/02-with-deps/) | | 写一个供他人 import 的库 | [11](11-publishing-a-library.md)、[06](06-features-and-capabilities.md)、[04](04-mcpp-toml.md) §2.4 | [`04-workspace`](../../examples/04-workspace/)、[`11-features`](../../examples/11-features/) | | 发布它 | [10](10-pack-and-release.md)、[11](11-publishing-a-library.md)、[12](12-binary-distribution.md) | [`03-pack-static`](../../examples/03-pack-static/)、[`05-lib-distribution`](../../examples/05-lib-distribution/) | -| 为另一台机器构建 | [21](21-the-target-triple.md)、[24](24-openkal-cross.md)、[40](40-baremetal.md) | [`06-openkal-cross`](../../examples/06-openkal-cross/)、`mcpp new … --template riscv-virt-rt` | +| 为另一台机器构建 | [21](21-the-target-triple.md)、[24](24-openkal-cross.md)、[35](35-platforms.md)、[40](40-baremetal.md) | [`06-openkal-cross`](../../examples/06-openkal-cross/)、`mcpp new … --template riscv-virt-rt` | | 使用 GPU 或加速器 | [42](42-heterogeneous-builds.md)、[41](41-devices.md) | [`09-heterogeneous`](../../examples/09-heterogeneous/),从 [`boundary/`](../../examples/09-heterogeneous/boundary/) 开始 | | 增加一条规则、一种语言或一个生成器 | [31](31-authoring-a-rule-package.md)、[30](30-build-mcpp.md) | [`08-build-rules`](../../examples/08-build-rules/)、[`12-a-new-device-language`](../../examples/12-a-new-device-language/) | | 向索引添加一个包 | [11](11-publishing-a-library.md)、[SPEC-001](../specs/package-identity.md) | [09](09-commands-by-scenario.md)——发布相关场景 | -| 为别人打包一个工具、一个驱动或一块板子 | [32](32-authoring-a-payload.md)、[33](33-authoring-an-adapter.md)、[34](34-authoring-a-bsp.md) | `xim-pkgindex` 与 `mcpp-index` 里的描述符 | +| 为别人打包一个工具、一个驱动、一块板子或一个平台 | [32](32-authoring-a-payload.md)、[33](33-authoring-an-adapter.md)、[34](34-authoring-a-bsp.md)、[35](35-platforms.md) | `xim-pkgindex` 与 `mcpp-index` 里的描述符 | | 修改 mcpp 本身 | [90](90-build-from-source.md)、[92](92-release.md)、[51](51-supported-versions.md) | — | 课程也可以以**项目模板**的形式到达:模板由包提供,`mcpp new --template` @@ -87,6 +87,7 @@ - [32 —— 编写一个载荷](32-authoring-a-payload.md)——由 mcpp 安装的工具或预编译库 - [33 —— 编写运行时适配包](33-authoring-an-adapter.md)——够到宿主提供的库 - [34 —— 编写板级支持包](34-authoring-a-bsp.md)——一块板子,以及抵达它的方式 +- [35 —— 平台](35-platforms.md)——一类目标的配置,作为一个包 ### 4x —— 设备与加速器 @@ -127,6 +128,7 @@ | `min_api_level` | [04](04-mcpp-toml.md) | `macos_deployment_target` | [04](04-mcpp-toml.md) | | `[target..targets.] kind` | [22](22-target-side.md) | `config.toml` 中的 `[index.repos.]` | [32](32-authoring-a-payload.md) | | `[test] discover` | [04](04-mcpp-toml.md)、[08](08-testing.md) | `MCPP_RUNTIME_FILES` | [41](41-devices.md) | +| `[platform]`、`[host]`、`cfg(platform = …)` | [35](35-platforms.md) | `[target.] platform`、`layers`、`tools`、`link`、`carrier` | [35](35-platforms.md) | **命令** @@ -136,6 +138,7 @@ | `new`、`new --template` | [01](01-getting-started.md) | `add`、`update`、`why` | [05](05-dependencies.md) | | `pack` | [10](10-pack-and-release.md) | `publish`、`emit xpkg`、`xpkg parse` | [11](11-publishing-a-library.md) | | `toolchain` | [20](20-toolchains.md) | `clean`、`cache`、`index`、`self …` | [09](09-commands-by-scenario.md) | +| `--platform`、`--host-platform`、`self env --target` | [35](35-platforms.md) | `pack --pkg-format`、`--linkage`、`--merge` | [10](10-pack-and-release.md) | **概念** @@ -147,6 +150,7 @@ | 岛、接缝、`accel`、`MCPP_EXPORT_C` | [42](42-heterogeneous-builds.md) | 描述符、索引 | [11](11-publishing-a-library.md) | | ABI tag、预建产物 | [12](12-binary-distribution.md) | 退出码、JSON 输出 | [50](50-machine-output.md) | | `xim:` 载荷、`[xlings.workspace]` | [32](32-authoring-a-payload.md)、[23](23-the-project-environment.md) | `compat:` 适配包、`runtime.library_dirs` | [33](33-authoring-an-adapter.md) | +| 平台、平台包、版本组 | [35](35-platforms.md) | 通用包、单文件包、`mcpp-package.json` | [10](10-pack-and-release.md) | ## 规范文档 @@ -166,3 +170,5 @@ - [SPEC-010 —— home 选择与初始化](../specs/home-initialization.md) - [SPEC-011 —— 并发运行:构建目录锁、整体发布、乐观写入与取消](../specs/concurrency.md) - [SPEC-012 —— 面向人的输出:两条流、两种介质、行的种类与颜色](../specs/human-output.md) + - [SPEC-013 —— 平台:一类目标的配置,其选择、合并、对依赖图的补全、链接、封装、运行器与载体](../specs/platforms.md) + - [SPEC-014 —— 打包与分发:链接、闭包与容器,构建腿,暂存树,通用包与单文件包](../specs/packaging.md) diff --git a/mcpp.toml b/mcpp.toml index a863fe0dd..8afd1bfe8 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.26.2" +version = "2026.10.27.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] @@ -93,6 +93,14 @@ mcpplibs.cmdline = "0.0.1" [dependencies.mcpplibs] os = "0.2.0" +# The codecs of a package's container (SPEC-014 §5): gzip and raw DEFLATE, +# zstd, xz. The reference libraries, so an archive is written the same way on +# every host and nothing is looked for on PATH. +[dependencies.compat] +zlib = "1.3.2" +zstd = "1.5.7" +xz = "5.8.3" + [target.linux.build] sources = ["!src/native/windows/**"] diff --git a/modules/buildmcpp/src/directives.cppm b/modules/buildmcpp/src/directives.cppm index fea054399..ad4df83e7 100644 --- a/modules/buildmcpp/src/directives.cppm +++ b/modules/buildmcpp/src/directives.cppm @@ -201,6 +201,11 @@ enum class Slot : std::size_t { // starter. Not an ldflag: the engine renders it per linker and refuses // two statements that disagree, which a flag would make two `-e` words. LinkStart, + LinkDriver, + LinkInputs, + ArtifactNames, + Seal, + RunHost, Count }; inline constexpr std::size_t kSlotCount = static_cast(Slot::Count); @@ -299,7 +304,7 @@ struct Def { int sinceProtocol; }; -inline constexpr std::array kTable{{ +inline constexpr std::array kTable{{ // wire tag slot scope transform must missingPrefix missingSuffix since {"cxxflag", "cxxflag", Slot::CxxFlags, Scope::PackagePrivate, Transform::Verbatim, false, "", "", 1}, {"cflag", "cflag", Slot::CFlags, Scope::PackagePrivate, Transform::Verbatim, false, "", "", 1}, @@ -492,6 +497,16 @@ inline constexpr std::array kTable{{ // as `link-script` is: it is a fact of every image linked over the board. // kCacheEpoch is not bumped, for the reason `runtime-search-dir` states. {"link-start", "link-start", Slot::LinkStart, Scope::LinkGlobal, Transform::Verbatim, false, "", "", 16}, + // v17 (SPEC-013 §4.1): the platform's link and runner fields, stated by a + // program -- a board's or a platform's plugin. LinkGlobal and RunGlobal as + // `link-script` and `runner` are; they become the build's platform + // statement at the build-program tier. kCacheEpoch is not bumped, for the + // reason `runtime-search-dir` states. + {"link-driver", "link-driver", Slot::LinkDriver, Scope::LinkGlobal, Transform::Verbatim, false, "", "", 17}, + {"link-input", "link-input", Slot::LinkInputs, Scope::LinkGlobal, Transform::Verbatim, false, "", "", 17}, + {"artifact-name", "artifact-name", Slot::ArtifactNames, Scope::LinkGlobal, Transform::Verbatim, false, "", "", 17}, + {"seal", "seal", Slot::Seal, Scope::LinkGlobal, Transform::Verbatim, false, "", "", 17}, + {"run-host", "run-host", Slot::RunHost, Scope::RunGlobal, Transform::Verbatim, false, "", "", 17}, }}; // ── Collected output of one run ──────────────────────────────────────────── @@ -640,6 +655,13 @@ bool glob_leaves_package(std::string_view pattern); // place that knows which manifest channel each slot feeds. void apply(mcpp::manifest::Manifest& m, const Directives& d); +// The platform's fields a program stated (protocol 17: `link-driver`, +// `link-input`, `artifact-name`, `seal`, `run-host`), as a platform statement +// at the build-program tier (SPEC-013 §4.1). `apply` calls it; the platform +// phase reads it for the platform's own program. +void apply_platform(mcpp::manifest::platform::Statement& s, const Directives& d, + const std::filesystem::path& root = {}); + // Decode one `mcpp:action=` JSON payload. nullopt = malformed. std::optional decode_action(std::string_view payload); @@ -647,6 +669,9 @@ std::optional decode_action(std::string_view payloa // caller can refuse BEFORE applying anything — a half-applied action set is // worse than none. std::string action_error(const Directives& d); +// The role an `mcpp:action=` payload names ("source" when absent), or nothing +// for a payload that is not JSON. +std::optional action_role_of(std::string_view payload); // Non-empty when a `windows-subsystem` or `windows-entry` directive names // something `apply` cannot honour: a value that is not `:`, a @@ -1188,6 +1213,10 @@ void apply(mcpp::manifest::Manifest& m, const Directives& d) { // program that states two different symbols, so the last line is the one. for (auto const& symbol : d.at(Slot::LinkStart)) m.runtimeConfig.linkIntent.start = symbol; + + // The platform's fields (protocol 17), at the build-program tier; + // `platform_directive_error` has refused a malformed line. + apply_platform(m.buildConfig.programPlatform, d, m.sourcePath.parent_path()); } // The five roles `mcpp::action::role` may name, in the order @@ -1339,6 +1368,58 @@ static std::string named_target_error(const mcpp::manifest::Manifest& m, return {}; } +void apply_platform(mcpp::manifest::platform::Statement& s, const Directives& d, + const std::filesystem::path& root) { + namespace mp = mcpp::manifest::platform; + auto origin = [](std::string_view wire) { + return mp::Origin{ mp::Tier::Program, {}, 0, std::format("mcpp:{}", wire) }; + }; + for (auto const& v : d.at(Slot::LinkDriver)) + s.link.driver = mp::Text{ v, origin("link-driver"), true }; + if (!d.at(Slot::LinkInputs).empty()) { + std::vector inputs; + for (auto const& v : d.at(Slot::LinkInputs)) { + if (v.starts_with("file:")) { + // A relative file is the stating package's own, as in the + // TOML form (`[platform.link] inputs`). + inputs.push_back({ .file = root.empty() ? v.substr(5) : abs_against(root, v.substr(5)) }); + continue; + } + const auto colon = v.find(':', 5); + const auto as = v.substr(5, colon - 5); + const auto token = v.substr(colon + 1); + auto it = std::ranges::find_if(inputs, [&](auto const& in) { return in.as == as; }); + if (it == inputs.end()) inputs.push_back({ .from = { token }, .as = as }); + else it->from.push_back(token); + } + s.link.inputs = std::move(inputs); + s.link.inputsOrigin = origin("link-input"); + s.link.inputsSet = true; + } + for (auto const& v : d.at(Slot::ArtifactNames)) { + const auto colon = v.find(':'); + auto& field = v.substr(0, colon) == "exe" ? s.link.exe : s.link.shared; + field = mp::Text{ v.substr(colon + 1), origin("artifact-name"), true }; + } + if (!d.at(Slot::Seal).empty()) { + std::map> steps; + for (auto const& v : d.at(Slot::Seal)) { + const auto colon = v.find(':'); + int index = 0; + std::from_chars(v.data(), v.data() + colon, index); + steps[index].push_back(v.substr(colon + 1)); + } + s.link.seal.clear(); + for (auto& [i, step] : steps) s.link.seal.push_back(std::move(step)); + s.link.sealOrigin = origin("seal"); + s.link.sealSet = true; + } + for (auto const& v : d.at(Slot::RunHost)) { + s.runner.host = v; + s.runner.origin = origin("run-host"); + } +} + std::string target_directive_error(const mcpp::manifest::Manifest& m, const Directives& d) { if (auto e = named_target_error(m, d.at(Slot::WindowsSubsystem), "windows-subsystem", "windows_subsystem", true); @@ -1348,6 +1429,52 @@ std::string target_directive_error(const mcpp::manifest::Manifest& m, const Dire "windows-entry", "windows_entry", false); !e.empty()) return e; + // The platform's fields (protocol 17). + for (auto const& v : d.at(Slot::LinkDriver)) + if (v != "lld" && v != "compiler") + return std::format("build.mcpp emitted `mcpp:link-driver={}`; the drivers are " + "\"lld\" and \"compiler\"", v); + for (auto const& v : d.at(Slot::LinkInputs)) { + const bool file = v.starts_with("file:") && v.size() > 5; + const auto colon = v.find(':', 5); + const bool from = v.starts_with("from:") && colon != std::string::npos && colon > 5 + && colon + 1 < v.size(); + if (!file && !from) + return std::format("build.mcpp emitted `mcpp:link-input={}`; an input is " + "`file:` or `from::`", v); + if (from && v.substr(5, colon - 5).find_first_of("/\\") != std::string::npos) + return std::format("build.mcpp emitted `mcpp:link-input={}`; `` names the file " + "the output is saved as, without a directory", v); + } + for (auto const& v : d.at(Slot::ArtifactNames)) { + const auto colon = v.find(':'); + const auto kind = v.substr(0, colon); + const auto pattern = colon == std::string::npos ? std::string() : v.substr(colon + 1); + if ((kind != "exe" && kind != "shared") || pattern.empty()) + return std::format("build.mcpp emitted `mcpp:artifact-name={}`; write " + "`exe:` or `shared:`", v); + const auto at = pattern.find("{name}"); + if (at == std::string::npos || pattern.find("{name}", at + 1) != std::string::npos + || (kind == "exe" && at != 0)) + return std::format("build.mcpp emitted `mcpp:artifact-name={}`; the pattern names " + "the artifact as {{name}} once{}", v, + kind == "exe" ? ", at its start" : ""); + if (pattern.find_first_of("/\\") != std::string::npos) + return std::format("build.mcpp emitted `mcpp:artifact-name={}`; the pattern is a file " + "name, without a directory", v); + } + for (auto const& v : d.at(Slot::Seal)) { + const auto colon = v.find(':'); + const auto index = v.substr(0, colon); + if (colon == std::string::npos || index.empty() + || !std::ranges::all_of(index, [](char c) { return c >= '0' && c <= '9'; }) + || colon + 1 >= v.size()) + return std::format("build.mcpp emitted `mcpp:seal={}`; write `:`", v); + } + for (auto const& v : d.at(Slot::RunHost)) + if (v != "same-arch" && v != "any") + return std::format("build.mcpp emitted `mcpp:run-host={}`; it is \"same-arch\" " + "or \"any\"", v); // `link-start` (protocol 16): a symbol, and one per program. auto const& starts = d.at(Slot::LinkStart); for (auto const& symbol : starts) { diff --git a/modules/buildmcpp/src/program_protocol.cppm b/modules/buildmcpp/src/program_protocol.cppm index b4b8c59e2..bf92a4ce6 100644 --- a/modules/buildmcpp/src/program_protocol.cppm +++ b/modules/buildmcpp/src/program_protocol.cppm @@ -122,7 +122,11 @@ export namespace mcpp::build::program_protocol { // v16 (design 2026-10-26 §24.3): `mcpp:link-start=` -- the symbol every image // linked over the package is entered at, the build-program form of a target's // `start`. No directive of v15 changes spelling. -inline constexpr int kProtocolVersion = 16; +// v17 (SPEC-013 §4.1): the platform's fields a program states -- `link-driver`, +// `link-input`, `artifact-name`, `seal`, `run-host` -- and `mcpp::phase()` +// answering "platform" for a platform's plugin. No directive of v16 changes +// spelling. +inline constexpr int kProtocolVersion = 17; // ── Cache-format epoch ───────────────────────────────────────────────────── // diff --git a/modules/manifest/src/cfg_selector.cppm b/modules/manifest/src/cfg_selector.cppm index 955e0278e..cd210192b 100644 --- a/modules/manifest/src/cfg_selector.cppm +++ b/modules/manifest/src/cfg_selector.cppm @@ -40,8 +40,11 @@ export namespace mcpp::manifest::cfg { // target-side layer and are answerable only after the graph is resolved (see // the note on `kCfgLayerKeys` in mcpp.build.prepare_inputs); `accelerator` is // an input, known before resolution. +// `platform` is answered by the platform selection (SPEC-013 §3), which is +// made from the triple and the configuration before resolution, so it is read +// in the same pass as the triple's own keys. inline constexpr std::string_view kCfgTripleKeys[] = { - "arch", "env", "family", "os", + "arch", "env", "family", "os", "platform", }; inline constexpr std::string_view kCfgEarlyLayerKeys[] = { "accelerator", @@ -72,13 +75,16 @@ namespace mcpp::manifest::cfg { namespace { -constexpr unsigned kArch = 1, kOs = 2, kEnv = 4, kFamily = 8; +constexpr unsigned kArch = 1, kOs = 2, kEnv = 4, kFamily = 8, kPlatform = 16; unsigned component_of_key(std::string_view key) { if (key == "arch") return kArch; if (key == "os") return kOs | kFamily; if (key == "env") return kEnv; if (key == "family") return kFamily; + // A platform names a class of targets: one component, less specific than + // an operating system (two) and than a triple. + if (key == "platform") return kPlatform; return 0; // a layer key, `accelerator`, or an unknown key } diff --git a/modules/manifest/src/key_registry.cppm b/modules/manifest/src/key_registry.cppm index df89c41e1..d5f33a4ef 100644 --- a/modules/manifest/src/key_registry.cppm +++ b/modules/manifest/src/key_registry.cppm @@ -84,6 +84,8 @@ inline constexpr KeyClass kKeyRegistry[] = { {"target.", "cxx_runtime", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "linkage", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "min_api_level", KeyScope::Configuration, KeyMerge::Scalar, true, true }, + {"target.", "platform", KeyScope::Configuration, KeyMerge::Scalar, true, true }, + {"target.", "carrier", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "run_under", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "runner", KeyScope::Configuration, KeyMerge::Scalar, true, true }, {"target.", "sysroot", KeyScope::Configuration, KeyMerge::Scalar, true, true }, @@ -94,6 +96,9 @@ inline constexpr KeyClass kKeyRegistry[] = { {"target..xlings", "", KeyScope::Configuration, KeyMerge::Table, true, true }, {"target..targets", "", KeyScope::Package, KeyMerge::Table, false, true }, {"target..dependencies", "", KeyScope::Package, KeyMerge::Table, false, true }, + {"target..layers", "", KeyScope::Configuration, KeyMerge::Table, true, true }, + {"target..tools", "", KeyScope::Configuration, KeyMerge::Table, true, true }, + {"target..link", "", KeyScope::Configuration, KeyMerge::Table, true, true }, // whole tables {"toolchain", "", KeyScope::Configuration, KeyMerge::Table, true, false}, {"indices", "", KeyScope::Configuration, KeyMerge::Table, true, false}, @@ -109,6 +114,8 @@ inline constexpr KeyClass kKeyRegistry[] = { {"test", "", KeyScope::Package, KeyMerge::Table, false, false}, {"hooks", "", KeyScope::Package, KeyMerge::Table, false, false}, {"runtime", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"platform", "", KeyScope::Package, KeyMerge::Table, false, false}, + {"host", "", KeyScope::Configuration, KeyMerge::Table, false, false}, }; // The keys of `table`, in registry order. diff --git a/modules/manifest/src/manifest.cppm b/modules/manifest/src/manifest.cppm index 877c23727..874bb0153 100644 --- a/modules/manifest/src/manifest.cppm +++ b/modules/manifest/src/manifest.cppm @@ -11,6 +11,7 @@ // mcpp.manifest.toml mcpp.toml parsing (projects / packages on disk) // mcpp.manifest.xpkg xpkg .lua `mcpp = {}` segment (index descriptors) // mcpp.manifest.flag_words the words an element of a compile-flag list stands for +// mcpp.manifest.platform what a platform states (SPEC-013), and its TOML reader // mcpp.pm.compat.workspace_position COMPAT(workspace-position): a workspace // root's own tables given to members by position @@ -21,6 +22,8 @@ export import mcpp.manifest.toml; export import mcpp.manifest.xpkg; export import mcpp.manifest.flag_words; export import mcpp.manifest.cfg_selector; +export import mcpp.manifest.platform; +export import mcpp.manifest.platform_toml; export import mcpp.manifest.key_registry; export import mcpp.pm.compat.workspace_position; export import mcpp.pm.compat.registry; diff --git a/modules/manifest/src/platform.cppm b/modules/manifest/src/platform.cppm new file mode 100644 index 000000000..53fa649de --- /dev/null +++ b/modules/manifest/src/platform.cppm @@ -0,0 +1,350 @@ +// mcpp.manifest.platform -- the data a platform states (SPEC-013; design +// 2026-10-28-platforms-design.md §4). +// +// A PLATFORM is one reusable, versioned configuration of a class of targets: +// the toolchain, the default provider of each target-side layer, the tools the +// build runs on this machine, the compile and link conventions, the artifact's +// naming and sealing, and the runner. A platform package states it in its own +// manifest under `[platform]`; a project overrides it field by field under +// `[target.]` (and the host graph's under `[host]`) with the same key +// names; a build program states the parts that must be computed with +// directives of the same meaning. All three read into `Statement`, every field +// carrying the place it was stated, so the engine merges one structure and +// reports one source per value. +// +// This module is pure data. The parser is mcpp.manifest.toml's (it owns the +// TOML document and the dependency grammar); selection, merging, injection and +// reporting are the engine's (`src/platform`). + +export module mcpp.manifest.platform; + +import std; +import mcpp.pm.dep_spec; + +export namespace mcpp::manifest::platform { + +// The tier a value was stated at. Ordered by precedence where tiers compete: +// a later tier replaces an earlier one field by field (§4.5). +enum class Tier : std::uint8_t { + BuiltIn, // the engine's own conventions for a host target + Platform, // the platform package's `[platform]` section + Program, // the platform's plugin or a build program (directives) + Config, // config.toml: `[target.]`, `[host]` + Manifest, // the project's mcpp.toml: `[target.]`, `[host]` + Env, // MCPP_TARGET__, MCPP_HOST_PLATFORM + Flag, // --platform, --host-platform + Graph, // a package of the dependency graph provides it (§4.3) +}; + +constexpr std::string_view tier_name(Tier t) { + switch (t) { + case Tier::BuiltIn: return "built-in"; + case Tier::Platform: return "platform"; + case Tier::Program: return "build-program"; + case Tier::Config: return "config.toml"; + case Tier::Manifest: return "mcpp.toml"; + case Tier::Env: return "env"; + case Tier::Flag: return "flag"; + case Tier::Graph: return "graph"; + } + return "platform"; +} + +// Where one value was stated: the tier, the file and line when there is one, +// and the key as written (`[target.'cfg(platform = "openkal")'].runner`, +// `MCPP_TARGET_X86_64_OPENKAL_PLATFORM`). +struct Origin { + Tier tier = Tier::Platform; + std::string file; + int line = 0; + std::string key; +}; + +// A package a platform names: the platform itself, a layer's provider, the +// package of a tool, or the carrier. +// +// String form `"[.]@"`; table form the dependency +// grammar (`version`, `path`, `git` with `rev`/`tag`/`branch`, `features`, +// `default-features`) plus `name`, which the string form carries in front of +// the `@`. A path or git source may leave `name` out: the package's own +// manifest then names it. +struct PackageRef { + std::string name; // "[ns.]name"; empty: the source names it + mcpp::pm::DependencySpec spec; // the source and the requests on the edge + // `tools. = { program = "" }`: a program found in a + // declared payload or on PATH, not built. + std::string program; + // `layers. = false`: the platform provides no package for the layer. + bool off = false; + + bool empty() const { + return name.empty() && program.empty() && !off && spec.path.empty() + && spec.git.empty() && spec.version.empty(); + } +}; + +// A scalar or an argv with its origin. `set` distinguishes "absent" from +// "present and empty"; the struct carries no std::optional because an +// optional of a string as a member of an exported struct instantiates its +// special members in every importer, which MSVC's standard library under +// clang refuses (see TargetEntry::sysroot in mcpp.manifest.types). +struct Text { + std::string value; + Origin origin; + bool set = false; +}; + +struct Words { + std::vector value; + Origin origin; + bool set = false; +}; + +struct Flag { + bool value = false; + Origin origin; + bool set = false; +}; + +// `runner = ["okrun", "{}"]` or `runner = { cmd = [...], host = "same-arch", +// exclusive = true }`. `host`: "" (unstated), "same-arch" (the artifact runs +// on a machine of its architecture), "any" (the runner reaches it from any +// machine, an emulator or a probe). +struct Runner { + std::vector cmd; + std::string host; + bool exclusive = false; + bool exclusiveSet = false; + Origin origin; + bool set = false; +}; + +// The four flag lists of `[platform.build]` and of each of its +// `[platform.build.'cfg(...)']` tables. +struct Flags { + std::vector cflags, cxxflags, asmflags, ldflags; + bool empty() const { + return cflags.empty() && cxxflags.empty() && asmflags.empty() && ldflags.empty(); + } +}; + +struct CfgFlags { + std::string predicate; // the selector, `cfg(arch = "aarch64")` + Flags flags; + Origin origin; +}; + +// One input of the link besides the objects: a file of the platform package, +// or the standard output of a program saved as `as` (assembled when it ends +// in `.S` or `.s`, linked as it is otherwise). +struct LinkInput { + std::string file; // absolute, inside the package that stated it + std::vector from; // argv; `from[0]` is a program name + std::string as; // the file name the output is saved under +}; + +// `[platform.link]`: how the artifact is produced (§4). +struct Link { + Text driver; // "compiler" | "lld" + Words flags; // given to ld.lld itself when driver = "lld" + Text start; // the entry symbol + Text script; // a linker script, absolute, inside the stating package + std::vector inputs; + Origin inputsOrigin; + bool inputsSet = false; + Text exe; // "{name}." + Text shared; // "lib{name}." + // Post-link steps, each an argv run on the linked file (`{}`) before it + // is published (§4.6). + std::vector> seal; + Origin sealOrigin; + bool sealSet = false; +}; + +// `carrier = { package = "", section = "", main = "" }` (M3, +// §4.7): the package a self-contained program of this machine is built over, +// the section the carried artifact is placed in, and the source of the +// program's entry, a file of the stating package that runs the artifact it +// finds between `mcpp_carried_begin` and `mcpp_carried_end`. +struct Carrier { + PackageRef package; + std::string section; + std::string main; // absolute + Origin origin; + bool set = false; +}; + +struct LayerEntry { + PackageRef ref; + Origin origin; +}; + +struct ToolEntry { + PackageRef ref; + Origin origin; +}; + +// `[platform.hosted]`: the part that applies when a HOST triple selects this +// platform (§5.4). +struct Hosted { + // The host triples the arrangement is for (`x86_64-windows-gnu` and not + // `-msvc`: a C library of the graph needs the GNU ABI on Windows). Unset: + // every host triple. + Words targets; + Text toolchain; + std::map layers; + bool set = false; +}; + +// Everything one source states about a platform, in the seven groups of §4. +struct Statement { + // [platform] + Words targets; + Text toolchain; + Text sysroot; + std::vector sysrootProvides; + Runner runner; + std::map runners; + Carrier carrier; + Text plugin; + // `universal = true`: `mcpp pack` of any of the targets packs all of them + // into one package (SPEC-014 §3). + Flag universal; + // [platform.layers], [platform.tools] + std::map layers; + std::map tools; + // [platform.build] and its cfg tables + Flags build; + Origin buildOrigin; + std::vector buildCfg; + // [platform.link] + Link link; + // [platform.versions] + std::map versions; + Origin versionsOrigin; + // [platform.hosted] + Hosted hosted; + + bool empty() const { + return !targets.set && !toolchain.set && !sysroot.set && !runner.set + && runners.empty() && !carrier.set && !plugin.set && !universal.set && layers.empty() + && tools.empty() && build.empty() && buildCfg.empty() + && !link.driver.set && !link.flags.set && !link.start.set && !link.script.set + && !link.inputsSet && !link.exe.set && !link.shared.set && !link.sealSet + && versions.empty() && !hosted.set; + } +}; + +// The platform part of one `[target.]` table, or of `[host]` +// (selector empty): which platform, and the fields it overrides. +struct Override { + std::string selector; + PackageRef platform; // `platform = ""` or a table + Origin platformOrigin; + bool platformSet = false; + bool platformNone = false; // `platform = "none"` + Statement fields; +}; + +// The four target-side layers a platform may provide a package for (SPEC-002). +inline constexpr std::string_view kLayers[] = { + "kernel-abi", "c-abi", "c++-abi", "compiler-runtime", +}; + +constexpr bool is_layer(std::string_view k) { + for (auto l : kLayers) if (l == k) return true; + return false; +} + +// The keys a `[platform]` section may carry, and those of its sub-tables. One +// list per table, read by the parser's unknown-key sweep and by the tests. +inline constexpr std::string_view kPlatformKeys[] = { + "targets", "toolchain", "sysroot", "runner", "runners", "carrier", "plugin", + "universal", "layers", "tools", "build", "link", "versions", "hosted", +}; +inline constexpr std::string_view kBuildKeys[] = { "cflags", "cxxflags", "asmflags", "ldflags" }; +inline constexpr std::string_view kLinkKeys[] = { + "driver", "flags", "start", "script", "inputs", "exe", "shared", "seal", +}; +inline constexpr std::string_view kHostedKeys[] = { "targets", "toolchain", "layers" }; + +// The `[target.]` keys a project or config.toml uses to select and +// override a platform. `runner`, `runners`, `toolchain` and `sysroot` are the +// table's existing keys and keep their parsers. +inline constexpr std::string_view kOverrideKeys[] = { + "platform", "layers", "tools", "carrier", "link", +}; + +// `"[.]@"` -> the ref; the error names what is +// missing. `none` is not a ref; callers test for it first. +inline std::expected parse_ref_text(std::string_view text) { + PackageRef r; + const auto at = text.rfind('@'); + if (at == std::string_view::npos || at == 0 || at + 1 == text.size()) + return std::unexpected(std::format( + "'{}' is not a package reference; write \"@\", for example " + "\"openkal-platform@0.17.1\", or a table such as {{ path = \"\" }}", text)); + r.name = std::string(text.substr(0, at)); + r.spec.version = std::string(text.substr(at + 1)); + const auto dot = r.name.find('.'); + if (dot == std::string::npos) { + r.spec.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + r.spec.shortName = r.name; + r.spec.namespaceOmitted = true; + } else { + r.spec.namespace_ = r.name.substr(0, dot); + r.spec.shortName = r.name.substr(dot + 1); + } + return r; +} + +// The name a reference is shown under: `openkal-platform@0.17.1`, +// `openkal-musl (path ../openkal-musl)`, `okrun (program)`. +inline std::string display(const PackageRef& r) { + if (r.off) return "false"; + if (!r.program.empty()) return std::format("{} (program)", r.program); + std::string n = r.spec.shortName.empty() ? r.name : r.spec.shortName; + if (!r.spec.path.empty()) + return n.empty() ? std::format("path {}", r.spec.path) + : std::format("{} (path {})", n, r.spec.path); + if (!r.spec.git.empty()) + return std::format("{}{}{} ({} {})", n, n.empty() ? "" : " ", r.spec.git, + r.spec.gitRefKind, r.spec.gitRev); + return std::format("{}@{}", n, r.spec.version); +} + +// What a comparison of root positions keys an override on: its selector and +// the places its entries were written. One declaration is one key, so two +// workspace members whose plans an override would tell apart do not share one. +inline std::string identity(const Override& o) { + std::string s = o.selector; + auto add = [&](const Origin& g) { s += std::format("|{}:{}:{}", g.file, g.line, g.key); }; + if (o.platformSet) { + s += o.platformNone ? std::string("|none") : "|" + display(o.platform); + add(o.platformOrigin); + } + auto const& f = o.fields; + for (auto const& [k, e] : f.layers) add(e.origin); + for (auto const& [k, e] : f.tools) add(e.origin); + for (auto const& [k, w] : f.runners) add(w.origin); + if (f.carrier.set) add(f.carrier.origin); + if (f.runner.set) add(f.runner.origin); + for (auto const* t : { &f.link.driver, &f.link.start, &f.link.script, &f.link.exe, &f.link.shared }) + if (t->set) add(t->origin); + if (f.link.flags.set) add(f.link.flags.origin); + if (f.link.inputsSet) add(f.link.inputsOrigin); + if (f.link.sealSet) add(f.link.sealOrigin); + return s; +} + +// The name `cfg(platform = "")` matches: the package name with a +// trailing `-platform` removed, so `openkal-platform` is the platform +// `openkal`. A selector may also name the package in full. +inline std::string platform_name(std::string_view packageShortName) { + constexpr std::string_view kSuffix = "-platform"; + if (packageShortName.size() > kSuffix.size() && packageShortName.ends_with(kSuffix)) + return std::string(packageShortName.substr(0, packageShortName.size() - kSuffix.size())); + return std::string(packageShortName); +} + +} // namespace mcpp::manifest::platform diff --git a/modules/manifest/src/platform_toml.cppm b/modules/manifest/src/platform_toml.cppm new file mode 100644 index 000000000..6e821e9c0 --- /dev/null +++ b/modules/manifest/src/platform_toml.cppm @@ -0,0 +1,668 @@ +// mcpp.manifest.platform_toml -- the TOML reading of a platform statement +// (SPEC-013 §2; design 2026-10-28 §4, §5.2, §6.1). +// +// One reader for the three places the same keys are written: a platform +// package's `[platform]` section, a `[target.]` table of a project or +// of config.toml, and `[host]`. The tier and the file travel with every value, +// so a merged statement reports where each of its fields came from. +// +// Type errors are refusals naming the key. A key this engine does not know is +// recorded as a warning (`--strict` refuses it), as `[target.]` +// does: a package written for a newer engine still loads, and states its floor +// with `[package] mcpp` (R9.8). + +export module mcpp.manifest.platform_toml; + +import std; +import mcpp.libs.toml; +import mcpp.manifest.platform; +import mcpp.pm.dep_spec; + +export namespace mcpp::manifest::platform { + +struct Reader { + std::filesystem::path file; // the file the values are written in + Tier tier = Tier::Platform; + std::vector* warnings = nullptr; + + Origin at(const mcpp::libs::toml::Value& v, std::string key) const; + std::filesystem::path absolute(const std::string& written) const; + + std::expected, std::string> + strings(const mcpp::libs::toml::Value& v, std::string_view where) const; + + // A package: `"@"`, a dependency table with `name`, or — + // where allowed — `false` (a layer the platform does not provide) and + // `{ program = "" }` (a tool found rather than built). + std::expected + ref(const mcpp::libs::toml::Value& v, std::string_view where, bool allowOff, bool allowProgram) const; + + std::expected runner(const mcpp::libs::toml::Value& v, std::string_view where) const; + + std::expected + flags(const mcpp::libs::toml::Table& t, std::string_view where, Flags& out, + std::vector* cfg) const; + + std::expected + link(const mcpp::libs::toml::Table& t, std::string_view where, Link& out) const; + + // One entry of `link.inputs`: `{ file = "..." }` or `{ from = [...], as = "..." }`. + std::expected + link_input(const mcpp::libs::toml::Value& e, std::string_view key) const; + + std::expected + layers(const mcpp::libs::toml::Value& v, std::string_view where, + std::map& out) const; + + std::expected + tools(const mcpp::libs::toml::Value& v, std::string_view where, + std::map& out) const; + + std::expected carrier(const mcpp::libs::toml::Value& v, std::string_view where) const; + + // `[platform]` of a platform package. + std::expected statement(const mcpp::libs::toml::Table& t) const; + + // The platform part of one `[target.]` table (the keys of + // `kOverrideKeys`, plus `runner` and `runners`, which the table already + // had) or of `[host]` (selector empty: `platform` only). + std::expected + override_(const mcpp::libs::toml::Table& t, std::string_view selector) const; + + void warn(std::string text) const { if (warnings) warnings->push_back(std::move(text)); } +}; + +} // namespace mcpp::manifest::platform + +// ── implementation ────────────────────────────────────────────────────────── + +namespace mcpp::manifest::platform { + +namespace { + +namespace toml = mcpp::libs::toml; + +std::string list_of(std::span keys) { + std::string out; + for (auto k : keys) { if (!out.empty()) out += ", "; out += k; } + return out; +} + +bool contains(std::span keys, std::string_view k) { + return std::ranges::find(keys, k) != keys.end(); +} + +void split_name(PackageRef& r) { + if (r.name.empty()) return; + const auto dot = r.name.find('.'); + if (dot == std::string::npos) { + r.spec.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + r.spec.shortName = r.name; + r.spec.namespaceOmitted = true; + } else { + r.spec.namespace_ = r.name.substr(0, dot); + r.spec.shortName = r.name.substr(dot + 1); + r.spec.namespaceOmitted = false; + } +} + +} // namespace + +Origin Reader::at(const toml::Value& v, std::string key) const { + return Origin{ tier, file.generic_string(), static_cast(v.position.line), std::move(key) }; +} + +std::filesystem::path Reader::absolute(const std::string& written) const { + std::filesystem::path p(written); + if (p.is_relative()) { + std::error_code ec; + auto dir = file.parent_path(); + if (dir.is_relative()) dir = std::filesystem::absolute(dir, ec); + p = dir / p; + } + return p.lexically_normal(); +} + +std::expected, std::string> +Reader::strings(const toml::Value& v, std::string_view where) const { + if (!v.is_array()) + return std::unexpected(std::format("{} must be an array of strings", where)); + std::vector out; + for (auto const& e : v.as_array()) { + if (!e.is_string()) + return std::unexpected(std::format("{} must contain only strings", where)); + out.push_back(e.as_string()); + } + return out; +} + +std::expected +Reader::ref(const toml::Value& v, std::string_view where, bool allowOff, bool allowProgram) const { + PackageRef r; + if (v.is_bool()) { + if (allowOff && !v.as_bool()) { r.off = true; return r; } + return std::unexpected(std::format( + "{} must be a package reference{}", where, allowOff ? " or false" : "")); + } + if (v.is_string()) { + auto parsed = parse_ref_text(v.as_string()); + if (!parsed) return std::unexpected(std::format("{}: {}", where, parsed.error())); + return std::move(*parsed); + } + if (!v.is_table()) + return std::unexpected(std::format( + "{} must be \"@\" or a table such as {{ path = \"\" }}", where)); + auto const& t = v.as_table(); + if (auto it = t.find("program"); it != t.end()) { + if (!allowProgram) + return std::unexpected(std::format( + "{}: `program` names a program found on this machine and is accepted " + "for a tool only", where)); + if (!it->second.is_string() || it->second.as_string().empty()) + return std::unexpected(std::format("{}.program must be a program name", where)); + if (t.size() != 1) + return std::unexpected(std::format( + "{}: {{ program = \"...\" }} takes no other key", where)); + r.program = it->second.as_string(); + return r; + } + for (auto const& [k, val] : t) { + auto key = std::format("{}.{}", where, k); + auto str = [&]() -> std::expected { + if (!val.is_string() || val.as_string().empty()) + return std::unexpected(std::format("{} must be a non-empty string", key)); + return val.as_string(); + }; + if (k == "name") { + auto s = str(); if (!s) return std::unexpected(s.error()); + r.name = *s; + } else if (k == "version") { + auto s = str(); if (!s) return std::unexpected(s.error()); + r.spec.version = *s; + } else if (k == "path") { + auto s = str(); if (!s) return std::unexpected(s.error()); + r.spec.path = absolute(*s).generic_string(); + } else if (k == "git") { + auto s = str(); if (!s) return std::unexpected(s.error()); + r.spec.git = *s; + } else if (k == "rev" || k == "tag" || k == "branch") { + auto s = str(); if (!s) return std::unexpected(s.error()); + if (!r.spec.gitRefKind.empty()) + return std::unexpected(std::format( + "{}: one of rev / tag / branch, not both {} and {}", where, r.spec.gitRefKind, k)); + r.spec.gitRev = *s; + r.spec.gitRefKind = k; + } else if (k == "features") { + auto s = strings(val, key); if (!s) return std::unexpected(s.error()); + r.spec.features = std::move(*s); + } else if (k == "default-features") { + if (!val.is_bool()) + return std::unexpected(std::format("{} must be true or false", key)); + r.spec.defaultFeatures = val.as_bool(); + r.spec.defaultFeaturesDeclared = true; + } else { + warn(std::format( + "{}: '{}' is not a key of a package reference (ignored); the keys are name, version, " + "path, git, rev, tag, branch, features, default-features{}", + where, k, allowProgram ? ", or program alone" : "")); + } + } + const int sources = int(!r.spec.version.empty()) + int(!r.spec.path.empty()) + + int(!r.spec.git.empty()); + if (sources != 1) + return std::unexpected(std::format( + "{} must name exactly one source: version, path or git", where)); + if (!r.spec.git.empty() && r.spec.gitRev.empty()) + return std::unexpected(std::format("{}: a git source needs one of rev / tag / branch", where)); + if (r.spec.git.empty() && !r.spec.gitRev.empty()) + return std::unexpected(std::format("{}: {} applies to a git source", where, r.spec.gitRefKind)); + if (!r.spec.version.empty() && r.name.empty()) + return std::unexpected(std::format( + "{}: a version names the package too: {{ name = \"\", version = \"\" }}, " + "or \"@\"", where)); + // A repository is not read before its package is fetched, and its URL does + // not say the package's name; a git reference names it. + if (!r.spec.git.empty() && r.name.empty()) + return std::unexpected(std::format( + "{}: a git source names the package too: {{ name = \"\", git = \"\", ... }}", + where)); + split_name(r); + return r; +} + +std::expected Reader::runner(const toml::Value& v, std::string_view where) const { + Runner r; + r.origin = at(v, std::string(where)); + r.set = true; + if (v.is_array()) { + auto c = strings(v, where); + if (!c) return std::unexpected(c.error()); + r.cmd = std::move(*c); + } else if (v.is_table()) { + for (auto const& [k, val] : v.as_table()) { + auto key = std::format("{}.{}", where, k); + if (k == "cmd") { + auto c = strings(val, key); + if (!c) return std::unexpected(c.error()); + r.cmd = std::move(*c); + } else if (k == "host") { + if (!val.is_string() || (val.as_string() != "same-arch" && val.as_string() != "any")) + return std::unexpected(std::format( + "{} must be \"same-arch\" (the artifact runs on a machine of its " + "architecture) or \"any\" (the runner reaches it from any machine)", key)); + r.host = val.as_string(); + } else if (k == "exclusive") { + if (!val.is_bool()) return std::unexpected(std::format("{} must be true or false", key)); + r.exclusive = val.as_bool(); + r.exclusiveSet = true; + } else { + warn(std::format( + "{}: '{}' is not a key of a runner (ignored); the keys are cmd, host, exclusive", + where, k)); + } + } + } else { + return std::unexpected(std::format( + "{} must be an array of strings or {{ cmd = [...], host = \"same-arch\" }}", where)); + } + if (r.cmd.empty()) + return std::unexpected(std::format( + "{} has an empty command; an empty template would run nothing and report success", where)); + return r; +} + +std::expected +Reader::flags(const toml::Table& t, std::string_view where, Flags& out, + std::vector* cfg) const { + for (auto const& [k, v] : t) { + auto key = std::format("{}.{}", where, k); + if (cfg && v.is_table() && k.starts_with("cfg(") && k.ends_with(")")) { + CfgFlags c; + c.predicate = k; + c.origin = at(v, std::format("{}.'{}'", where, k)); + if (auto r = flags(v.as_table(), std::format("{}.'{}'", where, k), c.flags, nullptr); !r) + return r; + cfg->push_back(std::move(c)); + continue; + } + std::vector* dst = + k == "cflags" ? &out.cflags + : k == "cxxflags" ? &out.cxxflags + : k == "asmflags" ? &out.asmflags + : k == "ldflags" ? &out.ldflags + : nullptr; + if (!dst) { + warn(std::format( + "{} has unsupported key '{}' (ignored); the keys are {}{}", where, k, list_of(kBuildKeys), + cfg ? ", and 'cfg(...)' tables of the same keys" : "")); + continue; + } + auto s = strings(v, key); + if (!s) return std::unexpected(s.error()); + dst->insert(dst->end(), s->begin(), s->end()); + } + return {}; +} + +std::expected +Reader::link_input(const toml::Value& e, std::string_view key) const { + if (!e.is_table()) + return std::unexpected(std::format( + "{} must contain only tables: {{ file = \"...\" }} or " + "{{ from = [...], as = \"...\" }}", key)); + LinkInput in; + for (auto const& [ik, iv] : e.as_table()) { + auto ikey = std::format("{}.{}", key, ik); + if (ik == "file") { + if (!iv.is_string() || iv.as_string().empty()) + return std::unexpected(std::format("{} must be a path", ikey)); + in.file = absolute(iv.as_string()).generic_string(); + } else if (ik == "from") { + auto s = strings(iv, ikey); + if (!s) return std::unexpected(s.error()); + if (s->empty()) + return std::unexpected(std::format("{} is empty; it names a program", ikey)); + in.from = std::move(*s); + } else if (ik == "as") { + if (!iv.is_string() || iv.as_string().empty() + || iv.as_string().find_first_of("/\\") != std::string::npos) + return std::unexpected(std::format("{} must be a file name", ikey)); + in.as = iv.as_string(); + } else { + warn(std::format( + "{}: '{}' is not a key of a link input (ignored); the keys are file, " + "or from and as", key, ik)); + } + } + if (in.file.empty() == in.from.empty()) + return std::unexpected(std::format( + "{}: an input is {{ file = \"...\" }} or {{ from = [...], as = \"...\" }}", key)); + if (!in.from.empty() && in.as.empty()) + return std::unexpected(std::format( + "{}: an input made by a program names the file it is saved as (`as`)", key)); + return in; +} + +std::expected +Reader::link(const toml::Table& t, std::string_view where, Link& out) const { + for (auto const& [k, v] : t) { + auto key = std::format("{}.{}", where, k); + auto text = [&](Text& dst, bool needsName) -> std::expected { + if (!v.is_string() || v.as_string().empty()) + return std::unexpected(std::format("{} must be a non-empty string", key)); + // A name pattern holds `{name}` exactly once and names no directory + // (the rule `mcpp::platform::split_name_pattern` applies). + const std::string_view s = v.as_string(); + const auto first = s.find("{name}"); + if (needsName && (first == std::string_view::npos + || s.find("{name}", first + 1) != std::string_view::npos)) + return std::unexpected(std::format( + "{} = \"{}\" names the artifact as {{name}} exactly once; " + "for example \"{{name}}.oke\"", key, s)); + if (needsName && s.find_first_of("/\\") != std::string_view::npos) + return std::unexpected(std::format( + "{} = \"{}\" is a file name pattern; it names no directory", key, s)); + dst = Text{ v.as_string(), at(v, key), true }; + return {}; + }; + if (k == "driver") { + if (!v.is_string() || (v.as_string() != "compiler" && v.as_string() != "lld")) + return std::unexpected(std::format( + "{} must be \"compiler\" (the compiler driver links) or \"lld\" " + "(ld.lld itself links, with `flags`)", key)); + out.driver = Text{ v.as_string(), at(v, key), true }; + } else if (k == "flags") { + auto s = strings(v, key); + if (!s) return std::unexpected(s.error()); + out.flags = Words{ std::move(*s), at(v, key), true }; + } else if (k == "start") { + if (auto r = text(out.start, false); !r) return r; + } else if (k == "script") { + if (auto r = text(out.script, false); !r) return r; + out.script.value = absolute(out.script.value).generic_string(); + } else if (k == "exe") { + if (auto r = text(out.exe, true); !r) return r; + if (!out.exe.value.starts_with("{name}")) + return std::unexpected(std::format( + "{} = \"{}\": an executable's name begins with the target's name; " + "write \"{{name}}\", for example \"{{name}}.oke\"", key, out.exe.value)); + } else if (k == "shared") { + if (auto r = text(out.shared, true); !r) return r; + } else if (k == "inputs") { + if (!v.is_array()) + return std::unexpected(std::format( + "{} must be an array of {{ file = \"...\" }} or {{ from = [...], as = \"...\" }}", key)); + out.inputs.clear(); + for (auto const& e : v.as_array()) { + auto in = link_input(e, key); + if (!in) return std::unexpected(in.error()); + out.inputs.push_back(std::move(*in)); + } + out.inputsOrigin = at(v, key); + out.inputsSet = true; + } else if (k == "seal") { + if (!v.is_array()) + return std::unexpected(std::format( + "{} must be an array of commands, for example [[\"okld\", \"check\", \"{{}}\"]]", key)); + out.seal.clear(); + for (auto const& e : v.as_array()) { + auto s = strings(e, key + "[]"); + if (!s) return std::unexpected(std::format( + "{} must be an array of commands, each an array of strings", key)); + if (s->empty()) + return std::unexpected(std::format("{} has an empty command", key)); + out.seal.push_back(std::move(*s)); + } + out.sealOrigin = at(v, key); + out.sealSet = true; + } else { + warn(std::format( + "{} has unsupported key '{}' (ignored); the keys are {}", where, k, list_of(kLinkKeys))); + } + } + return {}; +} + +std::expected +Reader::layers(const toml::Value& v, std::string_view where, + std::map& out) const { + if (!v.is_table()) + return std::unexpected(std::format( + "{} must be a table of = | false, for example " + "c-abi = \"openkal-musl@0.22.6\"", where)); + for (auto const& [k, val] : v.as_table()) { + auto key = std::format("{}.{}", where, k); + if (!is_layer(k)) + return std::unexpected(std::format( + "{}: '{}' is not a target-side layer; the layers are {}", where, k, list_of(kLayers))); + auto r = ref(val, key, /*allowOff=*/true, /*allowProgram=*/false); + if (!r) return std::unexpected(r.error()); + out[k] = LayerEntry{ std::move(*r), at(val, key) }; + } + return {}; +} + +std::expected +Reader::tools(const toml::Value& v, std::string_view where, + std::map& out) const { + if (!v.is_table()) + return std::unexpected(std::format( + "{} must be a table of = | {{ program = \"\" }}", where)); + for (auto const& [k, val] : v.as_table()) { + auto key = std::format("{}.{}", where, k); + auto r = ref(val, key, /*allowOff=*/false, /*allowProgram=*/true); + if (!r) return std::unexpected(r.error()); + out[k] = ToolEntry{ std::move(*r), at(val, key) }; + } + return {}; +} + +std::expected Reader::carrier(const toml::Value& v, std::string_view where) const { + if (!v.is_table()) + return std::unexpected(std::format( + "{} must be {{ package = \"@\", section = \"\" }}", where)); + Carrier c; + c.origin = at(v, std::string(where)); + c.set = true; + for (auto const& [k, val] : v.as_table()) { + auto key = std::format("{}.{}", where, k); + if (k == "package") { + auto r = ref(val, key, false, false); + if (!r) return std::unexpected(r.error()); + c.package = std::move(*r); + } else if (k == "section") { + if (!val.is_string() || val.as_string().empty()) + return std::unexpected(std::format("{} must be a section name", key)); + c.section = val.as_string(); + } else if (k == "main") { + if (!val.is_string() || val.as_string().empty()) + return std::unexpected(std::format("{} must be a source file of the package", key)); + c.main = absolute(val.as_string()).generic_string(); + } else { + warn(std::format( + "{}: '{}' is not a key of a carrier (ignored); the keys are package, section, main", + where, k)); + } + } + if (c.package.empty() || c.section.empty() || c.main.empty()) + return std::unexpected(std::format( + "{} names `package`, `section` and `main` (the program's entry source)", where)); + return c; +} + +std::expected Reader::statement(const toml::Table& t) const { + Statement s; + for (auto const& [k, v] : t) { + auto key = std::format("[platform].{}", k); + if (k == "targets") { + auto w = strings(v, key); + if (!w) return std::unexpected(w.error()); + if (w->empty()) return std::unexpected(std::format("{} names no target", key)); + s.targets = Words{ std::move(*w), at(v, key), true }; + } else if (k == "toolchain") { + if (!v.is_string() || v.as_string().empty()) + return std::unexpected(std::format( + "{} must be a toolchain, for example \"llvm@23.1.3\"", key)); + s.toolchain = Text{ v.as_string(), at(v, key), true }; + } else if (k == "sysroot") { + if (v.is_string()) { + s.sysroot = Text{ v.as_string(), at(v, key), true }; + } else if (v.is_table()) { + std::string path; + for (auto const& [sk, sv] : v.as_table()) { + if (sk == "path" && sv.is_string()) path = sv.as_string(); + else if (sk == "provides") { + auto p = strings(sv, key + ".provides"); + if (!p) return std::unexpected(p.error()); + s.sysrootProvides = std::move(*p); + } else warn(std::format( + "{}: '{}' is not a key of a sysroot table (ignored); the keys are path, provides", + key, sk)); + } + if (path.empty()) + return std::unexpected(std::format("{} is a table without `path`", key)); + s.sysroot = Text{ "path:" + absolute(path).generic_string(), at(v, key), true }; + } else { + return std::unexpected(std::format("{} must be a string or {{ path = \"\" }}", key)); + } + } else if (k == "runner") { + auto r = runner(v, key); + if (!r) return std::unexpected(r.error()); + s.runner = std::move(*r); + } else if (k == "runners") { + if (!v.is_table()) + return std::unexpected(std::format("{} must be a table of = [argv]", key)); + for (auto const& [rk, rv] : v.as_table()) { + auto rkey = std::format("{}.{}", key, rk); + auto w = strings(rv, rkey); + if (!w) return std::unexpected(w.error()); + if (w->empty()) return std::unexpected(std::format("{} is empty", rkey)); + s.runners[rk] = Words{ std::move(*w), at(rv, rkey), true }; + } + } else if (k == "carrier") { + auto c = carrier(v, key); + if (!c) return std::unexpected(c.error()); + s.carrier = std::move(*c); + } else if (k == "plugin") { + // The platform's program is its package's build program, which a + // package names `build.mcpp` (SPEC-007); no other file is run. + if (!v.is_string() || v.as_string() != "build.mcpp") + return std::unexpected(std::format( + "{} must be \"build.mcpp\": the platform's program is its package's build program", key)); + s.plugin = Text{ absolute(v.as_string()).generic_string(), at(v, key), true }; + } else if (k == "universal") { + if (!v.is_bool()) + return std::unexpected(std::format("{} must be true or false", key)); + s.universal = Flag{ v.as_bool(), at(v, key), true }; + } else if (k == "layers") { + if (auto r = layers(v, "[platform.layers]", s.layers); !r) return std::unexpected(r.error()); + } else if (k == "tools") { + if (auto r = tools(v, "[platform.tools]", s.tools); !r) return std::unexpected(r.error()); + } else if (k == "build") { + if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); + s.buildOrigin = at(v, "[platform.build]"); + if (auto r = flags(v.as_table(), "[platform.build]", s.build, &s.buildCfg); !r) + return std::unexpected(r.error()); + } else if (k == "link") { + if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); + if (auto r = link(v.as_table(), "[platform.link]", s.link); !r) + return std::unexpected(r.error()); + } else if (k == "versions") { + if (!v.is_table()) + return std::unexpected(std::format("{} must be a table of = \"\"", key)); + for (auto const& [vk, vv] : v.as_table()) { + if (!vv.is_string() || vv.as_string().empty()) + return std::unexpected(std::format("[platform.versions].{} must be a version", vk)); + s.versions[vk] = vv.as_string(); + } + s.versionsOrigin = at(v, "[platform.versions]"); + } else if (k == "hosted") { + if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); + s.hosted.set = true; + for (auto const& [hk, hv] : v.as_table()) { + auto hkey = std::format("[platform.hosted].{}", hk); + if (hk == "targets") { + auto w = strings(hv, hkey); + if (!w) return std::unexpected(w.error()); + s.hosted.targets = Words{ std::move(*w), at(hv, hkey), true }; + } else if (hk == "toolchain") { + if (!hv.is_string() || hv.as_string().empty()) + return std::unexpected(std::format("{} must be a toolchain", hkey)); + s.hosted.toolchain = Text{ hv.as_string(), at(hv, hkey), true }; + } else if (hk == "layers") { + if (auto r = layers(hv, "[platform.hosted.layers]", s.hosted.layers); !r) + return std::unexpected(r.error()); + } else { + warn(std::format( + "[platform.hosted] has unsupported key '{}' (ignored); a host triple takes " + "the platform's {} only", hk, list_of(kHostedKeys))); + } + } + } else { + warn(std::format("[platform] has unsupported key '{}' (ignored). The keys are: {}.", + k, list_of(kPlatformKeys))); + } + } + return s; +} + +std::expected +Reader::override_(const toml::Table& t, std::string_view selector) const { + Override o; + o.selector = std::string(selector); + const std::string where = selector.empty() + ? std::string("[host]") : std::format("[target.{}]", selector); + for (auto const& [k, v] : t) { + auto key = std::format("{}.{}", where, k); + if (k == "platform") { + if (v.is_string() && v.as_string() == "none") { + o.platformNone = true; + } else { + auto r = ref(v, key, false, false); + if (!r) return std::unexpected(r.error()); + o.platform = std::move(*r); + } + o.platformOrigin = at(v, key); + o.platformSet = true; + } else if (selector.empty()) { + // `[host]` selects the host graph's platform and nothing else. + warn(std::format( + "[host] has unsupported key '{}' (ignored); it holds `platform`, the host graph's platform", + k)); + } else if (k == "layers") { + if (auto r = layers(v, key, o.fields.layers); !r) return std::unexpected(r.error()); + } else if (k == "tools") { + if (auto r = tools(v, key, o.fields.tools); !r) return std::unexpected(r.error()); + } else if (k == "carrier") { + auto c = carrier(v, key); + if (!c) return std::unexpected(c.error()); + o.fields.carrier = std::move(*c); + } else if (k == "link") { + if (!v.is_table()) return std::unexpected(std::format("{} must be a table", key)); + if (auto r = link(v.as_table(), key, o.fields.link); !r) return std::unexpected(r.error()); + } else if (k == "runner") { + auto r = runner(v, key); + if (!r) return std::unexpected(r.error()); + o.fields.runner = std::move(*r); + } else if (k == "runners") { + if (!v.is_table()) continue; // the table's own reader refuses the shape + for (auto const& [rk, rv] : v.as_table()) { + auto rkey = std::format("{}.{}", key, rk); + auto w = strings(rv, rkey); + if (!w) continue; // ditto + o.fields.runners[rk] = Words{ std::move(*w), at(rv, rkey), true }; + } + } + // Every other key belongs to the table's other readers. + } + // `platform` decides what `cfg(platform = ...)` means, so it cannot be + // stated under a selector that asks about it (§5.1). + if (o.platformSet && selector.find("platform") != std::string_view::npos + && selector.starts_with("cfg(")) + return std::unexpected(std::format( + "{}.platform: a platform cannot be selected under a selector that asks which " + "platform is selected; write it under a triple or a cfg() of os/arch/env", where)); + return o; +} + +} // namespace mcpp::manifest::platform diff --git a/modules/manifest/src/targetside_model.cppm b/modules/manifest/src/targetside_model.cppm index ad46b4e60..304b59366 100644 --- a/modules/manifest/src/targetside_model.cppm +++ b/modules/manifest/src/targetside_model.cppm @@ -245,6 +245,9 @@ struct Layer { std::string interfaceName; // openkal / linux / win32 / darwin / musl / glibc / libc++ std::string impl; // package@version, or an xpkg reference bool subset = false; // C++ layer: a freestanding subset, not the whole library + // The package was added to the graph by the target's platform (SPEC-013 + // §4.3): still a graph package, reported as the platform's. + bool byPlatform = false; bool absent() const { return origin == Origin::None; } bool prebuilt() const { return origin == Origin::Payload || origin == Origin::Xpkg; } @@ -566,10 +569,11 @@ struct Inputs { std::string targetOs; // mcpp's own OS field, for the payload interface name std::string targetEnv; // mcpp's own ENV field ("musl", "gnu", …) bool freestandingTarget = false; - // An ok image (`-openkal`): its kernel interface is openkal's, held - // by the loader that runs it rather than by a package or a payload, and + // A target whose loader supplies the system (an ok image, `-openkal`): + // its kernel interface is the one the triple names (`targetOs`), held by + // the loader that runs it rather than by a package or a payload, and // every layer above it is the graph's or absent. - bool openkalTarget = false; + bool loaderTarget = false; // The compiler, which is always a payload and never a package (see // `layer_is_suppliable_by_package`). Present here so that the layering rule @@ -721,10 +725,10 @@ inline TargetSide resolve(const Inputs& in) { ts.compilerRuntime = { Origin::Graph, in.compilerRuntime->display_interface(), in.compilerRuntime->id(), false }; - else if (in.payloadCompilerRuntimeAbsent || in.openkalTarget) - // An ok image's runtime routines are the image's own (a C++ - // run-time package, or the program): the payload's are built for an - // operating system the image does not have. + else if (in.payloadCompilerRuntimeAbsent || in.loaderTarget) + // The runtime routines of a program its loader runs are its own (a + // C++ run-time package, or the program): the payload's are built for + // an operating system the program does not have. ts.compilerRuntime = { Origin::None, {}, {}, false }; else if (!in.compilerFamily.empty()) ts.compilerRuntime = { Origin::Payload, in.compilerFamily, {}, false }; @@ -733,10 +737,10 @@ inline TargetSide resolve(const Inputs& in) { if (in.kernelAbi) ts.kernelAbi = { Origin::Graph, in.kernelAbi->display_interface(), in.kernelAbi->id(), false }; - else if (in.openkalTarget) + else if (in.loaderTarget) // Fixed by the target: an ok image calls `kal_*` and nothing else, - // and the loader binds them to its implementation (SPEC 12.3). - ts.kernelAbi = { Origin::Payload, "openkal", "the loader (okrun)", false }; + // and the loader binds them to its implementation (openkal SPEC 12.3). + ts.kernelAbi = { Origin::Payload, in.targetOs, "the loader", false }; else if (in.freestandingTarget) // Not a gap. A bare machine has no kernel, and saying so is the // information: the same target reads `openkal` when an implementation @@ -757,7 +761,7 @@ inline TargetSide resolve(const Inputs& in) { ts.cAbi = { Origin::Xpkg, in.sysrootLibc.empty() ? xpkg_interface(in.sysrootXpkg) : in.sysrootLibc, in.sysrootXpkg, false }; - else if (in.freestandingTarget || in.openkalTarget) + else if (in.freestandingTarget || in.loaderTarget) ts.cAbi = { Origin::None, {}, {}, false }; else ts.cAbi = { Origin::Payload, payload_libc_name(in.targetOs, in.targetEnv), @@ -1081,13 +1085,14 @@ inline std::string format_layers(const TargetSide& ts, bool verbose) { std::string suffix = l.subset ? ", subset" : ""; // One column layout whether or not an implementation is named, so the // rows read as a table rather than as a list of sentences. + const std::string_view from = l.byPlatform ? std::string_view("platform") + : origin_name(l.origin); if (l.impl.empty()) out += std::format("{}{:<17} {:<14} ({}{})\n", kIndent, label, - l.interfaceName, origin_name(l.origin), suffix); + l.interfaceName, from, suffix); else out += std::format("{}{:<17} {:<14} ({}, {}{})\n", kIndent, label, - l.interfaceName, l.impl, origin_name(l.origin), - suffix); + l.interfaceName, l.impl, from, suffix); } return out; } diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 5f4ae7f97..605be26c4 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -5,6 +5,8 @@ export module mcpp.manifest.toml; import mcpp.manifest.types; import mcpp.manifest.key_registry; import mcpp.manifest.cfg_selector; +import mcpp.manifest.platform; +import mcpp.manifest.platform_toml; import mcpp.targetside; import std; import mcpp.invocation; @@ -1043,13 +1045,18 @@ std::expected parse_document(const t::Document& documen // every entry. Mixed arrays (strings and tables) never reach here. "package.requires", "target.*.requires", + // `inputs = [{ file = "..." }, { from = [...], as = "..." }]` (SPEC-013 + // §2.5): the platform reader type-checks every entry. + "platform.link.inputs", + "target.*.link.inputs", }; if (auto badPath = find_disallowed_array_of_tables(doc->root(), "", kAllowedArraysOfTables)) { return std::unexpected(error(origin, std::format( "[[{}]] (array-of-tables) is not allowed for section '{}'; " "array-of-tables syntax is only supported for [[build.flags]], " "[[features..flags]], [[runtime.requirements]], " - "[[runtime.artifacts]], [[runtime.deploy]], and [xlings] deps entries", + "[[runtime.artifacts]], [[runtime.deploy]], [[platform.link.inputs]], " + "and [xlings] deps entries", *badPath, *badPath))); } @@ -3735,6 +3742,29 @@ std::expected parse_document(const t::Document& documen } } + // [platform] — the section that makes a package a PLATFORM (SPEC-013; + // design 2026-10-28 §4): the configuration of a class of targets, read + // by the engine before the toolchain is decided. And [host] — the + // platform of the host graph (§6.1). + if (auto* pt = doc->get_table("platform")) { + mcpp::manifest::platform::Reader rd{origin, mcpp::manifest::platform::Tier::Platform, + &m.schemaWarnings}; + auto st = rd.statement(*pt); + if (!st) return std::unexpected(error(origin, st.error())); + if (!st->targets.set) + return std::unexpected(error(origin, + "[platform] names the targets it serves: targets = [\"\", ...]")); + m.platform = std::move(*st); + m.platformDeclared = true; + } + if (auto* ht = doc->get_table("host")) { + mcpp::manifest::platform::Reader rd{origin, mcpp::manifest::platform::Tier::Manifest, + &m.schemaWarnings}; + auto ov = rd.override_(*ht, ""); + if (!ov) return std::unexpected(error(origin, ov.error())); + m.hostPlatform = std::move(*ov); + } + // [lib] — library root convention (cargo-style). if (auto v = doc->get_string("lib.path")) { m.lib.path = *v; @@ -3774,6 +3804,24 @@ std::expected parse_document(const t::Document& documen m.packConfig.alsoSkip = *v; if (auto v = doc->get_string_array("pack.bundle-project.force_bundle")) m.packConfig.forceBundle = *v; + // The three axes' defaults, the signer and `universal` (SPEC-014). The + // values are checked where the command reads them, with the flags that + // take the same words. + if (auto v = doc->get_string("pack.linkage")) { + if (*v != "static" && *v != "shared") + return std::unexpected(error(origin, std::format( + "[pack].linkage = '{}' invalid; expected 'static' | 'shared'", *v))); + m.packConfig.linkage = *v; + } + if (auto v = doc->get_string("pack.format")) m.packConfig.format = *v; + if (auto v = doc->get_string("pack.compress")) m.packConfig.compress = *v; + if (auto v = doc->get_string_array("pack.sign")) { + if (v->empty()) + return std::unexpected(error(origin, std::string( + "[pack].sign is an argv, `{}` the file signed; it is empty"))); + m.packConfig.sign = *v; + } + if (auto v = doc->get_bool("pack.universal")) m.packConfig.universal = *v; // [target.] — per-target overrides. We accept both GCC // (x86_64-linux-musl) and Rust-style (x86_64-unknown-linux-musl) @@ -3927,13 +3975,22 @@ std::expected parse_document(const t::Document& documen // scalar (the unknown-key sweep below skips it by type) nor part // of the conditional sub-table channel. if (auto it = body.find("runner"); it != body.end()) { - if (!it->second.is_array()) { + // The table form, `{ cmd = [...], host = "same-arch", exclusive = + // true }`, is the platform reader's (SPEC-013 §2.3); its command + // is the template here as well. + if (!it->second.is_array() && it->second.is_table()) { + mcpp::manifest::platform::Reader rd{origin, mcpp::manifest::platform::Tier::Manifest, + &m.schemaWarnings}; + auto r = rd.runner(it->second, std::format("[target.{}].runner", triple)); + if (!r) return std::unexpected(error(origin, r.error())); + e.runner = r->cmd; + } else if (!it->second.is_array()) { return std::unexpected(error(origin, std::format( - "[target.{}].runner must be an array of strings, " - "e.g. runner = [\"qemu-system-riscv64\", \"-kernel\"]", + "[target.{}].runner must be an array of strings or {{ cmd = [...], " + "host = \"same-arch\" }}, e.g. runner = [\"qemu-system-riscv64\", \"-kernel\"]", triple))); } - for (auto& el : it->second.as_array()) { + if (it->second.is_array()) for (auto& el : it->second.as_array()) { if (!el.is_string()) { return std::unexpected(error(origin, std::format( "[target.{}].runner must contain only strings", triple))); @@ -3986,6 +4043,52 @@ std::expected parse_document(const t::Document& documen } } + // THE PLATFORM KEYS (SPEC-013 §2.3; design 2026-10-28 §5): which + // platform this selector's targets use (`platform`), and the fields + // of it this table overrides with the platform's own key names + // (`layers`, `tools`, `carrier`, `link`, plus `runner` and + // `runners` above). One reader, mcpp.manifest.platform_toml, reads + // them here, in a platform package's `[platform]` and in `[host]`. + bool platformKeys = false; + if (auto it = body.find("platform"); it != body.end()) { + if (!it->second.is_string() && !it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[target.{}].platform must be \"@\", \"none\", or a table " + "such as {{ path = \"\" }}", triple))); + platformKeys = true; + } + if (auto it = body.find("layers"); it != body.end()) { + if (!it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[target.{}].layers must be a table, e.g. [target.{}.layers] c-abi = \"@\"", triple, triple))); + platformKeys = true; + } + if (auto it = body.find("tools"); it != body.end()) { + if (!it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[target.{}].tools must be a table, e.g. [target.{}.tools] okld = {{ path = \"\" }}", triple, triple))); + platformKeys = true; + } + if (auto it = body.find("carrier"); it != body.end()) { + if (!it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[target.{}].carrier must be a table, e.g. carrier = {{ package = \"@\", section = \"\" }}", triple))); + platformKeys = true; + } + if (auto it = body.find("link"); it != body.end()) { + if (!it->second.is_table()) + return std::unexpected(error(origin, std::format( + "[target.{}].link must be a table, e.g. [target.{}.link] start = \"\"", triple, triple))); + platformKeys = true; + } + if (platformKeys || body.contains("runner") || body.contains("runners")) { + mcpp::manifest::platform::Reader rd{origin, mcpp::manifest::platform::Tier::Manifest, + &m.schemaWarnings}; + auto ov = rd.override_(body, canon_triple(triple)); + if (!ov) return std::unexpected(error(origin, ov.error())); + m.platformOverrides.push_back(std::move(*ov)); + } + // Unsupported scalar and array keys are REPORTED, not dropped. // `[targets.]` has done this since #249; this table did not, // so a key that looks plausible — `cxx_runtime_tests` was the real @@ -4024,25 +4127,28 @@ std::expected parse_document(const t::Document& documen // arrays, so a sixth key added to the parser without a matching // entry here fails that test instead of shipping silently again. static constexpr std::string_view kKnownTargetScalars[] = { - "cxx_runtime", "linkage", "min_api_level", "sysroot", "toolchain", + "cxx_runtime", "linkage", "min_api_level", "platform", "sysroot", "toolchain", }; static constexpr std::string_view kKnownTargetArrays[] = { "requires", "run_under", "runner" }; static constexpr std::string_view kKnownTargetTables[] = { - "abi", "build", "build-dependencies", "dependencies", + "abi", "build", "build-dependencies", "carrier", "dependencies", "dev-dependencies", "feature-deps", "feature-requires-abi", - "feature-xlings", "requires_abi", "runners", "runtime", - "targets", "xlings", + "feature-xlings", "layers", "link", "requires_abi", "runners", "runtime", + "targets", "tools", "xlings", }; for (auto& [key, value] : body) { if (value.is_table()) { if (std::ranges::find(kKnownTargetTables, key) != std::end(kKnownTargetTables)) continue; - // A scalar key whose value may also be written as a table - // (`sysroot = { path = ... }`) is a value, not a section; - // it was parsed, or refused, above. + // A scalar or an array key whose value may also be written + // as a table (`sysroot = { path = ... }`, `runner = { cmd = + // [...] }`) is a value, not a section; it was parsed, or + // refused, above. if (std::ranges::find(kKnownTargetScalars, key) - != std::end(kKnownTargetScalars)) + != std::end(kKnownTargetScalars) + || std::ranges::find(kKnownTargetArrays, key) + != std::end(kKnownTargetArrays)) continue; std::string tables; for (auto name : kKnownTargetTables) { @@ -4061,8 +4167,9 @@ std::expected parse_document(const t::Document& documen if (std::ranges::find(known, key) != known.end()) continue; m.schemaWarnings.push_back(std::format( "[target.{}] has unsupported key '{}' (ignored). Supported keys: " - "cxx_runtime, linkage, min_api_level, run_under, runner, sysroot, toolchain, " - "plus the [target..runners] table for named runners. " + "cxx_runtime, linkage, min_api_level, platform, run_under, runner, sysroot, " + "toolchain, plus the [target..runners] table for named runners and " + "the platform's layers, tools, carrier and link tables. " "Per-role contracts go in [build].cxx_runtime's table form.", triple, key)); } diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index 5e720e176..84a2a5541 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -11,6 +11,7 @@ import mcpp.pm.compat; // Legacy dependency-key compatibility helpers import mcpp.pm.index_spec; // IndexSpec for [indices] section import mcpp.manifest.flag_words; import mcpp.targetside; +import mcpp.manifest.platform; // SPEC-013: [platform], the platform keys of [target.], [host] export namespace mcpp::manifest { @@ -875,6 +876,11 @@ struct BuildConfig : BuildInputs { // configuration error, not something to merge: appending would produce an // argv that is neither one's, and it would fail at exec time with no // indication of which package contributed which token. + // The platform's fields a build program stated (protocol 17; SPEC-013 + // §4.1): its link driver, link inputs, artifact names, seal steps and + // runner host, at the build-program tier. Filled by `apply`; prepare + // merges every program's into the build's platform statement. + mcpp::manifest::platform::Statement programPlatform; std::vector runner; // NAMED WAYS OF REACHING THE ARTEFACT, AND THE ENGINE KNOWS NONE OF // THEIR NAMES. @@ -1855,6 +1861,20 @@ struct PackConfig { // assumption ("modern desktop Linux"). std::vector alsoSkip; // libs to ALSO skip on top of PEP 600 std::vector forceBundle; // libs to bundle even if PEP 600 says skip + // The three axes' project defaults (SPEC-014 §1): `linkage` ("static" | + // "shared"), `format` (the package format, as `--pkg-format` names it), + // `compress` (`[:]`, as `--compress` takes it). Empty: + // unstated. + std::string linkage; + std::string format; + std::string compress; + // The signer (SPEC-014 §4): an argv run on each program, image and library + // of the staged tree, and on a single file, before the container is + // written; `{}` is the file. + std::vector sign; + // `false`: a pack of a platform's target packs that target alone, where + // the platform states `universal = true` (SPEC-014 §3). + std::optional universal; }; // `[workspace]` — multi-package workspace support (0.0.11+). @@ -2441,6 +2461,17 @@ struct Manifest { // [target.] tables — empty if user didn't declare any. std::map targetOverrides; + // PLATFORMS (SPEC-013; design 2026-10-28). `platform` is a platform + // package's own `[platform]` section, `platformDeclared` whether it has + // one. `platformOverrides` holds the platform part of every + // `[target.]` table, in file order (selection and per-field + // overrides; the engine orders matching selectors by specificity), and + // `hostPlatform` the `[host]` table: the platform of the host graph. + mcpp::manifest::platform::Statement platform; + bool platformDeclared = false; + std::vector platformOverrides; + mcpp::manifest::platform::Override hostPlatform; + // [pack] — `mcpp pack` config (see docs/10-pack-and-release.md). PackConfig packConfig; diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 8807e4a28..8457fac74 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -323,6 +323,13 @@ struct Triple { // `cfg(windows)` are both false, and the system it runs on is the // loader's, known at run time (`os::host()`). bool is_openkal() const { return os == "openkal"; } + // THE LOADER SUPPLIES THE SYSTEM. The compiler is told no operating + // system, and what lies beneath the program is bound when it is loaded. + // The layer rules of such a target are the engine's (a C++ header set + // only from the graph, a freestanding C environment when the graph has no + // C library); everything else about it -- flags, link, naming, runner -- + // is its platform's (SPEC-013). + bool loader_supplies_os() const { return is_openkal(); } // WHETHER THIS ROW'S TOOLCHAIN PIN IS A CAPABILITY RATHER THAN A // CONVENTION — the distinction that decides whether an author may override @@ -476,6 +483,13 @@ struct TargetInfo { // subset alike to one libc, one ISA and one version. std::string_view sysroot; bool defaultStatic; // target's default linkage is static + // THE PLATFORM a build of this target uses when nothing selects one + // (SPEC-013 §7): a pinned version of a platform package, which is what + // "built-in support" for a class of targets is. Empty = the engine's own + // conventions for the target (every hosted row). A project selects + // another with `[target.] platform`, a machine in config.toml, + // one invocation with `--platform`. + std::string_view platform = {}; }; // (note deliberately excludes "static" — the display layer derives that tag @@ -886,11 +900,15 @@ inline constexpr TargetInfo kKnownTargets[] = { { "wasm32-emscripten", "verified", "wasm","emsdk@6.0.9","", true }, // OK IMAGES (openkal SPEC clause 12; design 2026-10-26 part 2, §8.5). One // artifact per architecture, run by `okrun` on Linux, macOS and Windows of - // that architecture. clang emits it from one binary for either row; the C - // library and the C++ run-time are the graph's (`provides`), and an image - // with neither is legal. The two architectures the loader relocates. - { "x86_64-openkal", "preview", "ok", "llvm@23.1.3","", true }, - { "aarch64-openkal", "preview", "ok", "llvm@23.1.3","", true }, + // that architecture. clang emits it from one binary for either row. What + // makes it an ok image -- its flags, its link, its note and check, its + // name, its runner, its C library and C++ run-time -- is the platform's, + // `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.17.3" }, + { "aarch64-openkal", "preview", "ok", "llvm@23.1.3","", true, + "mcpplibs.openkal-platform@0.17.3" }, }; inline std::span known_targets() { return kKnownTargets; } @@ -1176,6 +1194,11 @@ struct ArtifactNaming { // produced artifact. Shared libraries have never been verified end-to-end // on PE or Mach-O — every shared-library e2e declares `# requires: elf`. bool sharedNeedsImportLib; + // A shared library's own prefix where a platform names it + // (`[platform.link] shared = "lib{name}.okso"`); the static library keeps + // `libPrefix`. + std::string_view sharedLibPrefix = {}; + bool sharedLibPrefixSet = false; }; // Naming for an explicit target triple. An EMPTY triple means "build for this diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 77cb6edf1..eb50e20fb 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.26.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.27.1"; } // namespace mcpp diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index a95553768..a8b0e068b 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -376,6 +376,10 @@ struct BuildProgramEnv { std::string programIdentity; std::chrono::milliseconds programCompile{0}; std::string programError; + // THE ARGV THAT RUNS IT, `{}` the program: the host graph's platform's + // runner when the program is one of its images (SPEC-013 §6, `okrun {}`); + // empty: the program is run directly. + std::vector programRunner; }; // The env-var name `hostprogram::xpkg_dir` reads back. One spelling of the @@ -961,10 +965,18 @@ CacheRecord read_cache(const fs::path& bdir) { return r; } +// The first value `vanished` answers for. +template +std::optional first_vanished(const std::vector& values, F&& vanished) { + for (auto const& v : values) + if (auto path = vanished(v)) return path; + return std::nullopt; +} + // Why the cached run is not valid, for the log; empty when it is. std::string cache_stale_reason(const fs::path& root, const fs::path& bdir, const CacheRecord& c, const std::string& programHash, const std::string& compilerHash, - const std::string& ctxHash) { + const std::string& ctxHash, const fs::path& outDir = {}) { if (!c.loaded) return "no record"; if (c.epoch != dirs::kCacheEpoch) return "record epoch"; // pre-epoch entries rerun once if (c.unknownRecord) return "unknown record line"; @@ -991,6 +1003,36 @@ std::string cache_stale_reason(const fs::path& root, const fs::path& bdir, const for (auto const& p : c.directives.at(def.slot)) if (!fs::exists(abs_against_root(root, p))) return "output " + p; } + // WHAT THE PROGRAM WROTE INTO ITS OUTPUT DIRECTORY AND NAMED: an include + // directory, a library directory, a file to deploy. A directive that names + // a path below `MCPP_OUT_DIR` which is no longer there was written by a + // run whose output was deleted since (`rm -rf target/.build-mcpp/out`), + // and replaying it hands the build a directory with nothing in it. + if (!outDir.empty()) { + const std::array spellings{ outDir.generic_string(), outDir.string() }; + // The path a value names below the output directory, when it is gone. + auto vanished = [&](const std::string& v) -> std::optional { + std::error_code ec; + for (auto const& text : spellings) { + const auto at = v.find(text); + if (at == std::string::npos) continue; + const auto end = v.find_first_of("\t\"", at); + auto path = v.substr(at, end == std::string::npos ? std::string::npos : end - at); + if (!fs::exists(path, ec)) return path; + } + return std::nullopt; + }; + // Only what reaches a compile, a link or a run: a warning or a claim + // that mentions a path is text, and an action's outputs are the + // build's products, made again by the build whenever they are missing. + for (auto const& def : dirs::kTable) { + if (def.scope == dirs::Scope::Advisory || def.scope == dirs::Scope::Claim + || def.scope == dirs::Scope::RerunKey || def.scope == dirs::Scope::GraphNode) + continue; + if (auto path = first_vanished(c.directives.at(def.slot), vanished)) + return "output " + *path; + } + } return {}; } @@ -1104,7 +1146,8 @@ std::expected run_build_program_impl( // Fast path: declared inputs + contract unchanged → reapply cached // directives, no run. CacheRecord cache = read_cache(bdir); - const auto stale = cache_stale_reason(root, bdir, cache, programHash, compilerHash, ctxHash); + const auto stale = cache_stale_reason(root, bdir, cache, programHash, compilerHash, ctxHash, + outDir); if (!stale.empty()) { std::string keys; if (stale == "environment contract") keys = changed_contract_keys(bdir, contractEnv); @@ -1201,8 +1244,18 @@ std::expected run_build_program_impl( // the one who knows how long it takes. const auto deadline = dirs::run_timeout_for(m.buildConfig.buildProgramTimeoutSecs); + std::vector argv{bin.string()}; + if (!env.programRunner.empty()) { + argv.clear(); + bool placed = false; + for (auto const& a : env.programRunner) { + if (a == "{}") { argv.push_back(bin.string()); placed = true; } + else argv.push_back(a); + } + if (!placed) argv.push_back(bin.string()); + } auto rres = mcpp::run::capture_exec_deadline( - {bin.string()}, childEnv, deadline, &timedOut, root.string()); + argv, childEnv, deadline, &timedOut, root.string()); if (timedOut) { // Name the manifest to edit. Without this the user edits their OWN // mcpp.toml when a dependency's build program is what timed out, and @@ -1305,6 +1358,46 @@ std::expected run_build_program_impl( " `mcpp::plugins::toolchain::configure` returns true.", def.wire)); } + } else if (env.phase == "platform") { + // THE PLATFORM PHASE STATES THE PLATFORM'S BUILD, LINK AND RUNNER + // FIELDS, AND THE PACKAGE FORMATS IT PROVIDES (SPEC-013 §4.2, SPEC-014 + // §4): it describes a class of targets, not a package of sources, and + // what it states reaches every unit and every link of the target. A + // format is provided as any package provides one -- `pack-format` and + // the `artifact` actions that produce it from the staged tree. Refused + // rather than dropped, naming the first directive that does not belong. + for (auto const& def : dirs::kTable) { + switch (def.slot) { + case Slot::CxxFlags: case Slot::CFlags: case Slot::LdFlags: + case Slot::Runner: case Slot::NamedRunner: case Slot::RunnerLongLived: + case Slot::RunExclusive: case Slot::LinkStart: case Slot::LinkDriver: + case Slot::LinkInputs: case Slot::ArtifactNames: case Slot::Seal: + case Slot::RunHost: case Slot::RerunFiles: case Slot::RerunEnv: + case Slot::RerunGlobs: case Slot::Warnings: case Slot::Diagnostics: + case Slot::PackFormats: + continue; + default: break; + } + if (d.at(def.slot).empty()) continue; + if (def.slot == Slot::Actions + && std::ranges::all_of(d.at(def.slot), [](std::string const& payload) { + return dirs::action_role_of(payload).value_or("") == "artifact"; + })) + continue; + mcpp::build::refusal::record(mcpp::build::refusal::Code::PlatformPhaseDirective); + return std::unexpected(std::format( + "build.mcpp stated `mcpp:{}=` in its platform phase.\n" + " A platform's plugin states the platform's build, link and runner\n" + " fields (compile and link flags, the link driver, inputs, entry,\n" + " artifact names, seal steps, runners) and the package formats it\n" + " provides (`pack-format`, `artifact` actions); `mcpp::phase()` is\n" + " \"platform\" there (SPEC-013 §4.2).", + def.wire)); + } + if (!d.at(Slot::ToolchainStatement).empty()) + return std::unexpected(std::string( + "build.mcpp stated `mcpp:toolchain=` in its platform phase; a platform's " + "toolchain is its `[platform] toolchain`")); } else if (!d.at(Slot::ToolchainStatement).empty()) { return std::unexpected(std::string( "build.mcpp stated `mcpp:toolchain=` outside the toolchain phase.\n" diff --git a/src/build/execute.cppm b/src/build/execute.cppm index dc4f143ed..c0dcb70cd 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -21,7 +21,8 @@ import mcpp.diag; import mcpp.build.plan; import mcpp.toolchain.triple; import mcpp.freestanding.runner; -import mcpp.okimage; +import mcpp.platform; +import mcpp.libs.json; // the plan event's platform objects import mcpp.build.runner_lookup; // #544: where the runner's program is import mcpp.home; // config.toml, for the machine's default toolchain import mcpp.libs.toml; @@ -50,6 +51,7 @@ import mcpp.provider.select; import mcpp.runtime.binding; import mcpp.log; import mcpplibs.os.platform; +import mcpp.toolchain.platform; import mcpp.invocation; import mcpp.effects; import mcpp.runtime.launch_env; @@ -158,7 +160,8 @@ std::string toolchain_request_identity() { const auto configFile = mcpp::home::root() / "config.toml"; auto target_table = [&](const std::filesystem::path& file) { auto doc = mcpp::libs::toml::parse_file(file); - if (!doc || !doc->get_table("target")) return; + // `[host]` too: the host graph's platform (SPEC-013 §6.1). + if (!doc || (!doc->get_table("target") && !doc->get_table("host"))) return; std::ifstream is(file, std::ios::binary); std::string text((std::istreambuf_iterator(is)), std::istreambuf_iterator()); std::uint64_t h = 1469598103934665603ull; @@ -174,9 +177,14 @@ std::string toolchain_request_identity() { target_table(mcpp::home::system_config_file()); std::string overrides; for (auto const& e : mcpp::invocation::with_prefix("MCPP_TARGET_")) - if (e.find("_SYSROOT=") != std::string::npos || e.find("_RUN_UNDER=") != std::string::npos) + if (e.find("_SYSROOT=") != std::string::npos || e.find("_RUN_UNDER=") != std::string::npos + || e.find("_PLATFORM=") != std::string::npos || e.find("_RUNNER=") != std::string::npos) overrides += e + ";"; if (auto s = mcpp::invocation::get("MCPP_SYSROOT")) overrides += std::format("MCPP_SYSROOT={};", *s); + // The platforms a flag or the environment selects (SPEC-013 §5.1, §6.1). + if (auto s = mcpp::invocation::get("MCPP_PLATFORM")) overrides += std::format("MCPP_PLATFORM={};", *s); + if (auto s = mcpp::invocation::get("MCPP_HOST_PLATFORM")) + overrides += std::format("MCPP_HOST_PLATFORM={};", *s); if (overrides.empty() && targets.empty()) return std::format("cli={};default={}", cli, machineDefault); return std::format("cli={};default={};base={}{}", cli, machineDefault, overrides, targets); @@ -1065,6 +1073,16 @@ struct ExecutionHost { std::string error; }; +// The platform's own tool by the name a runner gives it (SPEC-013 §10): the +// program the build located for it, before any search. +std::optional platform_tool(const mcpp::build::BuildContext& ctx, + std::string_view name) { + auto const& tools = ctx.plan.platform.tools; + if (auto it = tools.find(std::string(name)); it != tools.end() && !it->second.empty()) + return std::filesystem::path(it->second); + return std::nullopt; +} + ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string_view tripleKey) { ExecutionHost h; if (ctx.runUnder.empty()) return h; @@ -1098,8 +1116,24 @@ ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string // here, and is believed. std::optional unreachable_execution(const mcpp::build::BuildContext& ctx, std::string_view tripleKey, bool noRunner) { - if (noRunner || !ctx.runUnder.empty() || mcpplibs::os::is_macos) return std::nullopt; + if (noRunner || !ctx.runUnder.empty()) return std::nullopt; auto t = mcpp::toolchain::triple::parse(tripleKey); + // A RUNNER THAT RUNS THE ARTIFACT ON A MACHINE OF ITS ARCHITECTURE + // (`runner = { ..., host = "same-arch" }`, SPEC-013): an ok image's loader + // is a program of this machine, and an image of another architecture is + // not one it can map. + if (auto const& r = ctx.plan.platform.fields.runner; t && r.set && r.host == "same-arch" + && t->arch != mcpp::toolchain::platform::host_arch) + return std::format( + "cannot run {} artifacts on this {} host: the runner `{}` ({}) runs an artifact on a " + "machine of its architecture.\n" + " Name an execution host of that architecture: [target.{}] run_under = " + "[\"remote-run\", \"\", \"--\"]\n" + " (in mcpp.toml for the team, or ~/.mcpp/config.toml for this machine).", + tripleKey, mcpp::toolchain::platform::host_arch, + r.cmd.empty() ? std::string() : r.cmd.front(), + mcpp::platform::origin_text(r.origin), tripleKey); + if (mcpplibs::os::is_macos) return std::nullopt; if (!t || !t->is_apple()) return std::nullopt; return std::format( "cannot run {} artifacts on this host: {} runs only on macOS.\n" @@ -1175,50 +1209,34 @@ RunnerChoice choose_device_action(const BuildContext& ctx, const auto ft = mcpp::toolchain::triple::parse(ctx.tc.targetTriple); if (ft) c.freestanding = ft->is_freestanding(); c.tripleKey = ft ? ft->str() : ctx.tc.targetTriple; - // The graph's answer: the default runner, or a named one a package supplied. - if (isDefault) { - c.tmpl = ctx.manifest.buildConfig.runner; - } else if (auto it = ctx.manifest.buildConfig.namedRunners.find(std::string(which)); - it != ctx.manifest.buildConfig.namedRunners.end()) { - c.tmpl = it->second.argv; - c.longLived = it->second.longLived; - } - // The row is found as every other `[target.]` reader finds it - // (`find_target_entry`), whichever spelling the section and the toolchain - // use. The toolchain's own `targetTriple` is what the driver reported, - // which on macOS is `arm64-apple-darwin24.6.0`; an exact lookup of it - // matched on Linux and never on macOS (measured on CI, 2026-09-02), and an - // exact lookup of the canonical spelling missed a section written - // `[target.x86-windows-msvc]`. The raw form is kept as a fallback for a - // triple the parser does not know. - auto with_slot = [&](const mcpp::manifest::TargetEntry* e) { - const mcpp::manifest::TargetEntry* none = nullptr; - if (!e) return none; - if (isDefault) return e->runner.empty() ? none : e; - auto nr = e->namedRunners.find(std::string(which)); - return (nr != e->namedRunners.end() && !nr->second.empty()) ? e : none; + // THE STATEMENT PREPARE MERGED (SPEC-013 §4.5): the platform's runner, a + // build program's (`mcpp::runner`), this machine's, the project's + // `[target.]` (a cfg selector as well as a triple, whichever + // spelling), the environment's -- the later replacing the earlier. The + // project overriding what a dependency supplied is reported. + auto const& pf = ctx.plan.platform.fields; + auto overrides_program = [&](const mcpp::manifest::platform::Origin& o, bool programHad) { + using T = mcpp::manifest::platform::Tier; + return programHad && (o.tier == T::Manifest || o.tier == T::Config || o.tier == T::Env); }; - const mcpp::manifest::TargetEntry* entry = nullptr; - if (ft) { - entry = with_slot(mcpp::build::find_target_entry(ctx.manifest, *ft)); - } else if (auto it = ctx.manifest.targetOverrides.find(ctx.tc.targetTriple); - it != ctx.manifest.targetOverrides.end()) { - entry = with_slot(&it->second); - } - if (entry) { - if (isDefault) { - c.fromManifest = !ctx.manifest.buildConfig.runner.empty(); - c.tmpl = entry->runner; - } else { - c.fromManifest = ctx.manifest.buildConfig.namedRunners.contains( - std::string(which)); - c.tmpl = entry->namedRunners.at(std::string(which)); + if (isDefault) { + if (pf.runner.set) { + c.tmpl = pf.runner.cmd; + c.fromManifest = overrides_program(pf.runner.origin, + !ctx.manifest.buildConfig.runner.empty()); } + } else if (auto it = pf.runners.find(std::string(which)); it != pf.runners.end()) { + c.tmpl = it->second.value; + auto nr = ctx.manifest.buildConfig.namedRunners.find(std::string(which)); + c.fromManifest = overrides_program(it->second.origin, + nr != ctx.manifest.buildConfig.namedRunners.end()); + if (nr != ctx.manifest.buildConfig.namedRunners.end() && !c.fromManifest) + c.longLived = nr->second.longLived; } - // AND THE TOOLCHAIN'S OWN ANSWER, WHEN THE PROJECT AND ITS GRAPH GAVE NONE. + // AND THE TOOLCHAIN'S OWN ANSWER, WHEN NOTHING ABOVE GAVE ONE. // - // The third source and the last. A project's `[target.] runner` - // and a package's `mcpp::runner(...)` both outrank it, because they are + // The last source. A project's `[target.] runner`, a platform's + // and a package's `mcpp::runner(...)` all outrank it, because they are // statements about THIS program; the payload's is a statement about // everything its compiler produces. Measured 2026-09-12 in a sandbox with // no `node` on PATH: an Emscripten artefact built correctly and then @@ -1231,12 +1249,6 @@ RunnerChoice choose_device_action(const BuildContext& ctx, c.tmpl = mcpp::toolchain::payload_default_runner(ctx.tc.binaryPath); c.fromPayload = !c.tmpl.empty(); } - // AND THE TARGET'S OWN, FOR AN OK IMAGE: the loader is what runs one, on - // every system (SPEC 12). `okrun` is found as any runner's program is -- - // first among the programs the graph built for this machine - // (`openkal-tools`), then in declared payloads, then on PATH. - if (isDefault && c.tmpl.empty() && ft && ft->is_openkal()) - c.tmpl = { std::string(mcpp::okimage::kRunner) }; // `--no-runner` is the operator on THIS host stating a host fact the // manifest cannot carry: the triple is native here. On a freestanding // target that leaves nothing to execute, and the caller's existing @@ -1342,10 +1354,14 @@ mcpp::build::result::Record start_record(const BuildContext& ctx) { r.pid = mcpp::build::dirlock::process_id(); r.triple = ctx.tc.targetTriple; r.profile = ctx.profile; + r.platform = mcpp::platform::to_json(ctx.plan.platform); + r.hostPlatform = mcpp::platform::to_json(ctx.hostPlatform); mcpp::build::result::write(r); mcpp::build::result::emit("plan", {{"configuration", { {"triple", r.triple}, {"profile", r.profile}, {"fingerprint", r.fingerprint}, - {"outputDir", r.outputDir.generic_string()}}}}); + {"outputDir", r.outputDir.generic_string()}, + {"platform", nlohmann::json::parse(r.platform, nullptr, false)}, + {"hostPlatform", nlohmann::json::parse(r.hostPlatform, nullptr, false)}}}}); return r; } @@ -1386,12 +1402,16 @@ FastRecord fast_path_begin(const std::filesystem::path& projectRoot, if (previous) { f.record.triple = previous->triple; f.record.profile = previous->profile; + f.record.platform = previous->platform; + f.record.hostPlatform = previous->hostPlatform; f.artifacts = std::move(previous->artifacts); } res::write(f.record); res::emit("plan", {{"configuration", { {"triple", f.record.triple}, {"profile", f.record.profile}, - {"fingerprint", f.record.fingerprint}, {"outputDir", f.record.outputDir.generic_string()}}}}); + {"fingerprint", f.record.fingerprint}, {"outputDir", f.record.outputDir.generic_string()}, + {"platform", nlohmann::json::parse(f.record.platform, nullptr, false)}, + {"hostPlatform", nlohmann::json::parse(f.record.hostPlatform, nullptr, false)}}}}); return f; } void fast_path_end(FastRecord& f, bool ok) { @@ -2027,22 +2047,28 @@ export int list_runners(const std::string& package_filter, if (!ctx) { mcpp::ui::error(ctx.error()); return 2; } const auto& bc = ctx->manifest.buildConfig; + // The runners prepare merged: the platform's, a build program's, this + // machine's and the project's, each with where it was stated. + const auto& pf = ctx->plan.platform.fields; const auto ft = mcpp::toolchain::triple::parse(ctx->tc.targetTriple); const std::string key = ft ? ft->str() : ctx->tc.targetTriple; std::println("Target {}", key); - if (bc.runner.empty() && bc.namedRunners.empty()) { + if (!pf.runner.set && pf.runners.empty()) { std::println(" (none — this project reaches its artifact by executing it)"); return 0; } - if (!bc.runner.empty()) - std::println(" {:<12} {}", "(default)", bc.runner.front()); - for (auto const& [name, nr] : bc.namedRunners) { - std::println(" {:<12} {}{}", name, - nr.argv.empty() ? std::string("(no argv)") : nr.argv.front(), - nr.longLived ? " [long-lived]" : ""); + if (pf.runner.set) + std::println(" {:<12} {} ({})", "(default)", pf.runner.cmd.front(), + mcpp::platform::origin_text(pf.runner.origin)); + for (auto const& [name, w] : pf.runners) { + auto nr = bc.namedRunners.find(name); + const bool longLived = nr != bc.namedRunners.end() && nr->second.longLived; + std::println(" {:<12} {} ({}){}", name, + w.value.empty() ? std::string("(no argv)") : w.value.front(), + mcpp::platform::origin_text(w.origin), longLived ? " [long-lived]" : ""); } - if (bc.runExclusive) + if (bc.runExclusive || pf.runner.exclusive) std::println(" note: this target's runs cannot overlap; `mcpp test` " "serialises them"); return 0; @@ -2807,7 +2833,7 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, if (!format_name.empty() && choice.tmpl.empty() && std::filesystem::is_directory(exe, dirEc)) { std::println(stderr, - "error: --format {} produced a directory, '{}', and no runner reaches it.\n" + "error: --pkg-format {} produced a directory, '{}', and no runner reaches it.\n" " A directory is not executed directly: a runner named '{}' runs\n" " it. A package supplies one with `mcpp::runner(\"{}\", …)`, or the\n" " project declares it:\n" @@ -2847,6 +2873,7 @@ int run_artifact_via_runner(mcpp::build::BuildContext& ctx, auto tmpl = choice.tmpl; auto found = mcpp::build::runner_lookup::locate( tmpl.front(), ctx.xlingsDepBinDirs, mcpp::invocation::get("PATH").value_or("")); + if (auto tool = platform_tool(ctx, tmpl.front())) found.program = *tool; if (!found.program) { std::println(stderr, "error: {}", mcpp::build::runner_lookup::not_found_message( @@ -3007,7 +3034,7 @@ export int build_run_target(const std::optional& targetName, // no reading for a file this host was never going to execute on its own. if (!format.empty() && no_runner) { std::println(stderr, - "error: --format and --no-runner cannot be combined: a " + "error: --pkg-format and --no-runner cannot be combined: a " "distributable is not something this host executes directly."); return 2; } @@ -3083,7 +3110,7 @@ export int build_run_target(const std::optional& targetName, // shape that reports zero artifacts fails LOUDLY here instead of // dereferencing past the end of an empty vector below. std::println(stderr, - "error: --format {} reported success and named no artifact to run", format); + "error: --pkg-format {} reported success and named no artifact to run", format); return 1; } // THE RUNNER RESOLUTION NEEDS A BuildContext, AND build_and_pack's OWN @@ -3116,7 +3143,7 @@ export int build_run_target(const std::optional& targetName, names += a.string(); } std::println(stderr, - "error: --format {} produced {} distributables ({}); mcpp run needs " + "error: --pkg-format {} produced {} distributables ({}); mcpp run needs " "exactly one to hand to the runner", format, outcome.artifacts.size(), names); return 1; } @@ -3212,7 +3239,7 @@ export int build_run_target(const std::optional& targetName, std::println(stderr, "error: '{}' is an application, and on {} an application is " "a shared library that a package installs. Run it through a " - "distributable: mcpp run --format , where is " + "distributable: mcpp run --pkg-format , where is " "one of: {}", t.name, ctx->tc.targetTriple, formats.empty() ? std::string("none declared") : formats); @@ -3457,6 +3484,9 @@ struct TestBuild { bool bulkBuiltEverything = false; std::filesystem::path ninjaLog; std::uintmax_t logFrom = 0; + // MCPP_BIN_ for each program of the package, built before the + // tests (L5). + std::vector> programEnv; // How the test binaries are executed, resolved once for the build // (#544): every test of one build shares a target. @@ -3683,6 +3713,7 @@ static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool if (!runnerTmpl.empty()) { auto found = mcpp::build::runner_lookup::locate( runnerTmpl.front(), ctx->xlingsDepBinDirs, mcpp::invocation::get("PATH").value_or("")); + if (auto tool = platform_tool(*ctx, runnerTmpl.front())) found.program = *tool; if (found.program) runnerTmpl.front() = found.program->string(); else tb.invocationNotRunReason = mcpp::build::runner_lookup::not_found_message( runnerChoice.tripleKey, runnerTmpl.front(), found.searched); @@ -3819,7 +3850,8 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // Clamped rather than made an error: an exclusive target with one test // is an ordinary run, and refusing it would turn a correct // configuration into a failure. - const bool exclusiveDevice = ctx->manifest.buildConfig.runExclusive + const bool exclusiveDevice = (ctx->manifest.buildConfig.runExclusive + || ctx->plan.platform.fields.runner.exclusive) && !runnerChoice.tmpl.empty(); const int runJobsHere = exclusiveDevice ? 1 : runJobs; if (exclusiveDevice && runJobs > 1 && list.size() > 1) { @@ -4039,6 +4071,8 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // A GL test that cannot find a driver fails the same way a GL program // does, so it must be told the same things. for (auto& kv : subosEnv) childEnv.push_back(kv); + // ...and where the package's own programs are (L5). + for (auto& kv : tb.programEnv) childEnv.push_back(kv); // ...and, through a runner, the files the test carries with it, as // `mcpp run` hands them over (see `runtime_files_for`). Written here, // in the single-threaded pass, one list per test program. @@ -4258,6 +4292,45 @@ export int run_tests(std::span passthrough, } return 0; } + // THE PACKAGE'S PROGRAMS, BUILT BEFORE ITS TESTS AND NAMED TO THEM (L5). + // A test that runs the program it tests finds it as MCPP_BIN_ (the + // target's name, upper-cased, every other character `_`), the way a Cargo + // integration test finds CARGO_BIN_EXE_. Built by the pass `mcpp + // build` runs -- its fast path when nothing changed -- so a program is + // never linked against the tests' dev-dependencies. Before this a package + // whose tests ran its own executable had to be built first by hand. + std::vector> programEnv; + { + bool hasPrograms = false; + if (auto m = mcpp::manifest::load(testRoot / "mcpp.toml")) { + hasPrograms = std::ranges::any_of(m->targets, [](auto const& t) { + return t.kind == mcpp::manifest::Target::Binary + || t.kind == mcpp::manifest::Target::Application; + }); + std::error_code ec; + if (m->targets.empty() && std::filesystem::exists(testRoot / "src" / "main.cpp", ec)) + hasPrograms = true; + } + if (hasPrograms) { + auto binCtx = prepare_build(/*print_fp=*/false, /*includeDevDeps=*/false, {}, overrides); + if (!binCtx) { mcpp::ui::error(binCtx.error()); return 2; } + if (std::ranges::any_of(binCtx->plan.linkUnits, + [](auto const& lu) { return lu.kind == LinkUnit::Binary; })) { + if (int rc = run_build_plan(*binCtx, mcpp::log::is_verbose(), false); rc != 0) + return rc; + for (auto const& lu : binCtx->plan.linkUnits) { + if (lu.kind != LinkUnit::Binary) continue; + std::string key = "MCPP_BIN_"; + for (char c : lu.targetName) + key += (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A') + : ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_'; + programEnv.emplace_back(std::move(key), + (binCtx->outputDir / lu.output).lexically_normal().string()); + } + } + } + } + // 3. prepare_build with dev-deps enabled + synthetic targets. // A test binary is executed, so the run tier applies here exactly as it // does to `mcpp run` — `[xlings.workspace]` has no separate `test` tier @@ -4269,6 +4342,7 @@ export int run_tests(std::span passthrough, std::move(overrides)); if (!prepared) { mcpp::ui::error(prepared.error()); return 2; } TestBuild tb; + tb.programEnv = std::move(programEnv); tb.ctx.emplace(std::move(*prepared)); tb.backend = mcpp::build::make_ninja_backend(); tb.ninjaLog = tb.ctx->outputDir / ".ninja_log"; diff --git a/src/build/flags.cppm b/src/build/flags.cppm index bf2acdd6c..1dc88e531 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -20,7 +20,7 @@ import mcpp.build.refusal; import mcpp.diag; import mcpp.freestanding.target; import mcpp.freestanding.linkline; -import mcpp.okimage; +import mcpp.platform; import mcpp.manifest.types; import mcpp.manifest.flag_words; import mcpp.modgraph.scanner; @@ -1194,10 +1194,22 @@ CompileFlags compute_flags(const BuildPlan& plan) { // the flags every unit shares made a dependency's command line // depend on the members the plan builds, and switching members // recompiled it. A unit's map is its package's (`packageTrimMaps`). + // + // AND THE SEPARATOR IS THE TARGET'S (clang 17 and later, + // `-ffile-reproducible`). What follows a mapped prefix is the path + // the preprocessor joined, with the host's separator: an ok image + // built on Windows named `…/libcxxabi/src\demangle/ItaniumDemangle.h` + // and was not the bytes the same image built on Linux is. + int major = 0; + const auto& v = plan.toolchain.version; + std::from_chars(v.data(), v.data() + v.size(), major); + const char* reproducible = + mcpp::toolchain::is_clang(plan.toolchain) && major >= 17 ? " -ffile-reproducible" : ""; for (auto const& [package, map] : plan.packageTrimMaps) { std::string rendered; for (auto const& [from, to] : map) rendered += " -ffile-prefix-map=" + escape_path(from) + "=" + to; + rendered += reproducible; f.trimByPackage.emplace(package, std::move(rendered)); } } @@ -2467,40 +2479,73 @@ CompileFlags compute_flags(const BuildPlan& plan) { } } - // ── An ok image (`-openkal`): the convention's flags, and the link - // by `ld.lld` itself ── + // ── A target whose loader supplies the system: the layer rules ── // - // Applied by replacement, after every hosted decision, for the reason the - // freestanding block gives: an image has no loader path, no crt files and - // no system library, and what it holds above `kal_*` is the graph's, - // reached through the graph's own `-L`/`-l`. The compile keeps the hosted - // header model of a graph-supplied C library (`-nostdlibinc`, above); the - // C++ headers are withheld unless the graph supplies them too, because the - // payload's were configured for its own operating system. + // The compile keeps the hosted header model of a graph-supplied C library + // (`-nostdlibinc`, above); the C++ headers are withheld unless the graph + // supplies them too, because the payload's were configured for its own + // operating system. if (const auto ot = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - ot && ot->is_openkal()) { - std::string image; - for (auto const& w : mcpp::okimage::compile_flags(ot->arch)) image += " " + w; - if (!plan.targetSide.cxx.fromGraph()) image += " -nostdinc++"; - // AN IMAGE WITH NO C LIBRARY IS A FREESTANDING C ENVIRONMENT, in the + ot && ot->loader_supplies_os()) { + std::string w; + if (!plan.targetSide.cxx.fromGraph()) w += " -nostdinc++"; + // A PROGRAM WITH NO C LIBRARY IS A FREESTANDING C ENVIRONMENT, in the // standard's sense: nothing defines `strlen`, so the compiler must not // turn a loop into a call to it. Measured: `--release` of an image // that defines only `memcpy` and `memset` was refused by `okld check` // for importing `strlen`, which `-O2` had synthesised. - if (plan.targetSide.cAbi.absent()) image += " -ffreestanding"; - f.cxx += image; - f.cc += image; - f.as += image; + if (plan.targetSide.cAbi.absent()) w += " -ffreestanding"; + f.cxx += w; + f.cc += w; + f.as += w; + } - f.ldDriver = mcpp::freestanding::resolve_lld(plan.toolchain.binaryPath); - f.ld = mcpp::okimage::link_flags() + user_ldflags; - f.ldC = f.ld; - f.sysroot.clear(); - f.bFlag.clear(); - f.ldRuntimeFallback.clear(); - f.linkage = "static"; - for (auto& r : f.ldStdlibByRole) r.clear(); - for (auto& r : f.ldStdlibCByRole) r.clear(); + // ── THE PLATFORM'S CONVENTIONS (SPEC-013 §2.5, §4.5) ── + // + // Its flags come before every unit's own, so a package adds to a + // convention and cannot remove one. A platform that links with `ld.lld` + // itself (`[platform.link] driver = "lld"`, an ok image) gets the link it + // states, by replacement, after every hosted decision, for the reason the + // freestanding block gives: the artifact has no loader path, no crt files + // and no system library of the toolchain's, and what it holds is the + // graph's, reached through the graph's own `-L`/`-l`. + if (auto const& p = plan.platform; p.active) { + // Each element one word, quoted for the shell and `$` doubled for + // ninja, as every other word of a command is. + auto words = [](const std::vector& v) { + std::string out; + for (auto const& w : v) out += " " + ninja_command_word(w); + return out; + }; + f.cc += words(p.flags.cflags); + f.cxx += words(p.flags.cxxflags); + f.as += words(p.flags.asmflags); + if (p.fields.link.driver.value == "lld") { + f.ldDriver = mcpp::freestanding::resolve_lld_any(plan.toolchain.binaryPath); + f.ld = words(p.fields.link.flags.value) + words(p.flags.ldflags) + user_ldflags; + // `lld` and `lld-link` are the same program, which chooses its + // flavour from its name or from `-flavor`; a payload that ships + // one of them and not `ld.lld` is asked for the ELF linker + // explicitly. + if (f.ldDriver.stem() != "ld.lld") f.ld = " -flavor gnu" + f.ld; + f.ldC = f.ld; + f.sysroot.clear(); + f.bFlag.clear(); + f.ldRuntimeFallback.clear(); + f.linkage = "static"; + for (auto& r : f.ldStdlibByRole) r.clear(); + for (auto& r : f.ldStdlibCByRole) r.clear(); + } else { + f.ld += words(p.flags.ldflags); + f.ldC += words(p.flags.ldflags); + } + // A linker script the platform states: `-T` is the linker's own flag, + // and both drivers pass it through. + if (p.fields.link.script.set) { + const auto t = " -T " + escape_path(p.fields.link.script.value); + f.ld += t; + f.ldC += t; + } } // A LINKER STATED BY ROLE (mcpp#755, `tools = { ld = ... }`), AFTER EVERY diff --git a/src/build/hostprogram.cppm b/src/build/hostprogram.cppm index 18509cc30..1bd400cf6 100644 --- a/src/build/hostprogram.cppm +++ b/src/build/hostprogram.cppm @@ -191,6 +191,34 @@ inline void link_flag(const char* flag) { std::printf("mcpp:link-flag= // `link_script` does, and is checked there against the symbol the program or // its entry states; an image is entered at one symbol. inline void link_start(const char* symbol) { std::printf("mcpp:link-start=%s\n", symbol); } +// THE PLATFORM'S LINK, STATED BY A PROGRAM (protocol 17, SPEC-013 §4.1): the +// same fields as `[platform.link]`, for the part a board or a platform plugin +// computes. Each reaches the consumer's link, as `link_script` does. +// +// `link_driver("lld")`: `ld.lld` itself links, with the flags `link_flag` +// states; "compiler" (the default) leaves the link to the compiler driver. +inline void link_driver(const char* driver) { std::printf("mcpp:link-driver=%s\n", driver); } +// A file linked into every executable and shared library (assembled first when +// it is a `.S`/`.s`). +inline void link_input(const char* path) { std::printf("mcpp:link-input=file:%s\n", path); } +// The standard output of a program, saved as `as` and linked in the same way; +// one token of its argv per call, in order (`mcpp::link_input_from("notes.S", +// "okld")`, then `"note-source"`). +inline void link_input_from(const char* as, const char* token) { + std::printf("mcpp:link-input=from:%s:%s\n", as, token); +} +// How an artifact is named: `kind` is "exe" or "shared", `pattern` names it +// with `{name}` ("{name}.oke", "lib{name}.okso"). +inline void artifact_name(const char* kind, const char* pattern) { + std::printf("mcpp:artifact-name=%s:%s\n", kind, pattern); +} +// One token of seal step `step` (0, 1, ...): steps run in order on the linked +// file (`{}`) before it is published, `{needed}` naming the shared artifacts +// it needs. `mcpp::seal(0, "okld"); mcpp::seal(0, "check"); mcpp::seal(0, "{}");` +inline void seal(int step, const char* token) { std::printf("mcpp:seal=%d:%s\n", step, token); } +// Where the runner runs the artifact: "same-arch" (a machine of its +// architecture) or "any" (an emulator or a probe reaches it from any machine). +inline void run_host(const char* where) { std::printf("mcpp:run-host=%s\n", where); } // The PE subsystem ("console" | "windows") and the entry function ("main" | // "wmain" | "WinMain" | "wWinMain") of an executable target of THIS package, // named by `target` (#618). The same fields as `[targets.] diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 71d597016..1f32a5a40 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -44,7 +44,7 @@ import mcpp.toolchain.dialect; import mcpp.toolchain.provider; import mcpp.toolchain.registry; import mcpp.toolchain.triple; // shared_soname_flag decides by TARGET, not host -import mcpp.okimage; +import mcpp.toolchain.fingerprint; // hash_string: a platform seal's digest import mcpp.toolchain.msvc_coroutines; import mcpp.provider; import mcpp.provider.select; @@ -590,6 +590,12 @@ std::string pe_link_flag(const BuildPlan& plan, bool sep, return sep ? flag : "-Wl," + flag; } +// The platform links with `ld.lld` itself (`[platform.link] driver = "lld"`): +// a flag is the linker's own spelling, not the driver's `-Wl,` form. +bool links_with_lld(const BuildPlan& plan) { + return plan.platform.active && plan.platform.fields.link.driver.value == "lld"; +} + std::string shared_soname_flag(const LinkUnit& lu, const BuildPlan& plan) { if (lu.kind != LinkUnit::SharedLibrary) return ""; const auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); @@ -605,8 +611,8 @@ std::string shared_soname_flag(const LinkUnit& lu, const BuildPlan& plan) { const std::string name = lu.soname.empty() ? lu.output.filename().string() : lu.soname; if (macho) return "-Wl,-install_name,@rpath/" + name; - // An ok image is linked by `ld.lld` itself, which takes the bare flag. - if (t && t->is_openkal()) return lu.soname.empty() ? "" : "-soname " + lu.soname; + // A platform that links with `ld.lld` itself takes the bare flag. + if (links_with_lld(plan)) return lu.soname.empty() ? "" : "-soname " + lu.soname; return lu.soname.empty() ? "" : "-Wl,-soname," + lu.soname; } @@ -620,7 +626,7 @@ std::string shared_soname_default(const LinkUnit& lu, const BuildPlan& plan) { const bool pe = t ? t->is_pe() : bool(mcpplibs::os::is_windows); const bool macho = t ? t->is_mach_o() : bool(mcpplibs::os::is_macos); if (pe || macho) return ""; - if (t && t->is_openkal()) return "-soname " + lu.output.filename().string(); + if (links_with_lld(plan)) return "-soname " + lu.output.filename().string(); return "-Wl,-soname," + lu.output.filename().string(); } @@ -1528,34 +1534,95 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, placedFiles.push_back({{pl.source}, pl.dest}); } - // ── An ok image: its notes, and the step that finishes it ────────────── + // ── A platform's link inputs and seal (SPEC-013 §4, §4.6) ────────────── // - // The notes are one assembly source the engine writes (SPEC 12.4) and one - // object every image links. `okimage_finish` runs `okld note` and `okld - // check` on the linked image and only then publishes it under its name, - // so an image that breaks the convention never reaches `bin/`. - const bool okImageTarget = [&] { - const auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - return t && t->is_openkal(); - }(); - const auto okNoteSource = std::filesystem::path("obj") / "okimage" / "note.S"; - const auto okNoteObject = std::filesystem::path("obj") / "okimage" / "note.o"; - if (okImageTarget) { - std::error_code mkec; - std::filesystem::create_directories((plan.outputDir / okNoteSource).parent_path(), mkec); - write_file(plan.outputDir / okNoteSource, mcpp::okimage::note_source()); + // An input the platform names is linked into every executable and shared + // library: a file of the platform package, or the standard output of a + // program saved under `obj/platform/` by its own edge; an assembly + // source is assembled for the target first, by the compiler and nothing + // of the project's flags (the notes of an ok image are the case). The + // seal steps run on the linked file, written beneath `obj/platform/`, and + // only a file every step accepted is published under the artifact's name. + auto const& pf = plan.platform; + const bool platformLld = pf.active && pf.fields.link.driver.value == "lld"; + const bool platformSeal = pf.active && !pf.fields.link.seal.empty(); + // A program a platform names, as the platform resolved it, or its name for + // PATH at build time. + auto platform_program = [&](const std::string& name) { + auto it = pf.tools.find(name); + return it == pf.tools.end() ? name : it->second; + }; + std::vector platformInputs; + if (pf.active && !pf.fields.link.inputs.empty()) { const auto tt = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - append("rule okimage_note\n"); - append(std::format(" command = $cc --target={} --no-default-config -c $in -o $out\n", - tt->llvm_triple())); - append(" description = NOTE $out\n\n"); - append(std::format("build {} : okimage_note {}\n\n", - escape_ninja_path(okNoteObject), escape_ninja_path(okNoteSource))); - append(std::format("okld = {}\n", ninja_tool_word(plan.okldPath))); - append("okimage_with =\n"); - append("rule okimage_finish\n"); - append(" command = $mcpp okimage-finish --okld $okld -- $in $out $okimage_with\n"); - append(" description = IMAGE $out\n\n"); + const bool clangDriver = plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang; + append("rule platform_capture\n"); + append(" command = $mcpp platform-step capture --out $out -- $argv\n"); + append(" description = GEN $out\n\n"); + append("rule platform_asm\n"); + append(std::format(" command = $cc {}-c $in -o $out\n", + clangDriver && tt ? std::format("--target={} --no-default-config ", + tt->llvm_triple()) + : std::string{})); + append(" description = AS $out\n\n"); + for (auto const& in : pf.fields.link.inputs) { + std::filesystem::path src; + if (!in.from.empty()) { + src = std::filesystem::path("obj") / "platform" / in.as; + std::string argv, implicitIn; + for (std::size_t i = 0; i < in.from.size(); ++i) { + const auto word = i == 0 ? platform_program(in.from[0]) : in.from[i]; + argv += " " + ninja_command_word(word); + } + if (auto it = pf.tools.find(in.from[0]); it != pf.tools.end()) + implicitIn = " | " + escape_ninja_path(std::filesystem::path(it->second)); + append(std::format("build {} : platform_capture{}\n", escape_ninja_path(src), implicitIn)); + append(" argv =" + argv + "\n\n"); + } else { + src = std::filesystem::path(in.file); + } + const auto ext = src.extension().string(); + if (ext == ".S" || ext == ".s") { + auto obj = std::filesystem::path("obj") / "platform" / src.filename(); + obj.replace_extension(".o"); + append(std::format("build {} : platform_asm {}\n\n", + escape_ninja_path(obj), escape_ninja_path(src))); + platformInputs.push_back(std::move(obj)); + } else { + platformInputs.push_back(std::move(src)); + } + } + } + // The shared artifacts of this plan, which a seal step's `{needed}` names. + std::set sharedOutputs; + for (auto const& lu : plan.linkUnits) + if (lu.kind == LinkUnit::SharedLibrary) sharedOutputs.insert(lu.output); + if (platformSeal) { + // The steps, one per line, words separated by a tab, beside the + // configuration; their digest is part of the command, so a changed + // step reseals. + std::string steps; + for (auto const& step : pf.fields.link.seal) { + std::string line; + for (std::size_t i = 0; i < step.size(); ++i) { + if (i) line += '\t'; + line += i == 0 ? platform_program(step[0]) : step[i]; + } + steps += line + "\n"; + } + // IN THE COMMAND, HEX-ENCODED, NOT IN A FILE BESIDE IT: a file under + // `obj/` is gone after `rm -rf obj`, and a build that does not plan + // again (its graph is current) then ran the seal with no steps and + // published the artifact unsealed. The command is the steps, so a + // changed step reseals by the command's own change. + static constexpr char kHex[] = "0123456789abcdef"; + std::string hex; + for (unsigned char ch : steps) { hex += kHex[ch >> 4]; hex += kHex[ch & 15]; } + append("seal_with =\n"); + append("rule platform_seal\n"); + append(std::format(" command = $mcpp platform-step seal --steps-hex {} -- $in $out $seal_with\n", + hex)); + append(" description = SEAL $out\n\n"); } // ── The raw image a flasher takes ────────────────────────────────────── @@ -2327,18 +2394,18 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, "$ld /nologo /DLL /OUT:$out $implib_flag $def_flag " "$in $ldflags $unit_ldflags", "SHARED"); - } else if (okImageTarget) { - // AN OK IMAGE, LINKED BY `ld.lld` ITSELF (SPEC 12.2). An executable - // image is ET_DYN with the entry `ok_main`; an image another loads - // has its name and no entry. Each edge writes - // `obj/okimage/.unchecked`; the `okimage_finish` edge below - // writes the requirements note, - // checks the convention and publishes ``. - const std::string entry = std::format("-e {}", mcpp::okimage::kEntry); + } else if (platformLld) { + // A PLATFORM THAT LINKS WITH `ld.lld` ITSELF (an ok image, SPEC 12.2): + // its flags are the whole line, an executable is entered at the + // symbol the platform states, and an artifact another loads has + // its name and no entry. When the platform seals, each edge writes + // beneath `obj/platform/` and the seal edge below publishes. + const std::string entry = pf.fields.link.start.set + ? std::format(" -e {}", pf.fields.link.start.value) : std::string{}; link_rule("cxx_link", - "$ld_driver $ldflags $unit_ldflags " + entry + " -o $out $in", "LINK"); + "$ld_driver $ldflags $unit_ldflags" + entry + " -o $out $in", "LINK"); link_rule("c_link", - "$ld_driver $c_ldflags $unit_ldflags " + entry + " -o $out $in", "LINK"); + "$ld_driver $c_ldflags $unit_ldflags" + entry + " -o $out $in", "LINK"); link_rule("cxx_shared", "$ld_driver $ldflags $soname_default $soname_flag $unit_ldflags -o $out $in", "SHARED"); @@ -3348,14 +3415,16 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, for (auto& o : lu.objects) { ins += " " + escape_ninja_path(o); } - // An ok image's notes, in every image this plan links; the image is - // written beside its name and finished by its own edge (above). - const bool okImageUnit = okImageTarget && lu.kind != LinkUnit::StaticLibrary; - if (okImageUnit) ins += " " + escape_ninja_path(okNoteObject); - // Beneath obj/, not beside the name: what bin/ holds is what was - // published, and an unchecked image is not. - auto okUnchecked = std::filesystem::path("obj/okimage") / lu.output.relative_path(); - okUnchecked += ".unchecked"; + // The platform's link inputs, in every executable and shared library + // this plan links; with seal steps, the artifact is written beneath + // obj/ and published by its own edge (above). Not beside the name: + // what bin/ holds is what was published, and an unsealed file is not. + const bool platformUnit = pf.active && lu.kind != LinkUnit::StaticLibrary; + if (platformUnit) + for (auto const& in : platformInputs) ins += " " + escape_ninja_path(in); + const bool sealUnit = platformSeal && lu.kind != LinkUnit::StaticLibrary; + auto unsealed = std::filesystem::path("obj/platform") / lu.output.relative_path(); + unsealed += ".unsealed"; // mcpp#426. `separateLinker` (link.exe) takes no driver at all, so the // question does not arise there and the msvc rules are untouched. @@ -3509,7 +3578,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // order-only list of the edge. if (lu.kind != LinkUnit::StaticLibrary) orderOnly += linkOrderOnly; std::string out_line = std::format("build {}{} : {}{}{}{}\n", - escape_ninja_path(okImageUnit ? okUnchecked : lu.output), implicitOut, rule, ins, + escape_ninja_path(sealUnit ? unsealed : lu.output), implicitOut, rule, ins, implicit.empty() ? std::string{} : " |" + implicit, orderOnly.empty() ? std::string{} : " ||" + orderOnly); if (auto flag = shared_soname_flag(lu, plan); !flag.empty()) @@ -3633,21 +3702,21 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, } append(std::move(out_line)); - // The finished image: its requirements written and the convention - // checked against the images it needs, then published (SPEC 12.4). - if (okImageUnit) { + // The sealed artifact: every step of the platform's seal run on it, + // the shared artifacts it needs named to each, then published (§4.6). + if (sealUnit) { std::string with, withInputs; auto add_needed = [&](const std::filesystem::path& p) { - if (p.extension() != ".okso") return; + if (!sharedOutputs.contains(p)) return; with += " " + ninja_command_word(p.generic_string()); withInputs += " " + escape_ninja_path(p); }; for (auto const& o : lu.objects) add_needed(o); for (auto const& o : lu.implicitInputs) add_needed(o); - append(std::format("build {} : okimage_finish {}{}\n", - escape_ninja_path(lu.output), escape_ninja_path(okUnchecked), + append(std::format("build {} : platform_seal {}{}\n", + escape_ninja_path(lu.output), escape_ninja_path(unsealed), withInputs.empty() ? std::string{} : " |" + withInputs)); - if (!with.empty()) append(" okimage_with =" + with + "\n"); + if (!with.empty()) append(" seal_with =" + with + "\n"); append("\n"); } @@ -4541,13 +4610,12 @@ std::expected NinjaBackend::build(const BuildPlan& plan // which names an internal rule to somebody who wrote `kind = "shared"` in a // manifest. Defining the rule would be worse: `ld.lld -shared` succeeds and // produces an object nothing on that machine can load. - // An ok image is the exception: an image another loads (`.okso`) is - // placed by the loader (DT_NEEDED) or by `dlopen`, and has its rule. - const bool okImageShared = [&] { - const auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); - return t && t->is_openkal(); - }(); - if (!flags.ldDriver.empty() && !okImageShared) { + // A platform that links with ld.lld itself and names its shared + // artifacts is the exception (an ok image's `.okso`, placed by the loader + // through DT_NEEDED or by `dlopen`): it has the rule. + const bool platformShared = plan.platform.active + && plan.platform.fields.link.driver.value == "lld" && plan.platform.fields.link.shared.set; + if (!flags.ldDriver.empty() && !platformShared) { for (auto const& lu : plan.linkUnits) { if (lu.kind != LinkUnit::SharedLibrary) continue; return std::unexpected(BuildError{std::format( diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 161f0c187..3af12d2c7 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -28,6 +28,7 @@ import mcpp.runtime.binding; import mcpp.provider; import mcpp.provider.select; import mcpp.home; +import mcpp.platform; export namespace mcpp::build { @@ -539,10 +540,11 @@ struct BuildPlan { std::map> moduleScopes; std::filesystem::path stdCompatObjectPath; // absolute path to prebuilt std.compat.o std::filesystem::path scanDepsPath; // clang-scan-deps binary (Clang only) - // `okld` for an ok-image target (`-openkal`): the program of the graph - // (or of this machine) that writes each image's requirements note and - // checks the convention after the link (mcpp.okimage). Empty elsewhere. - std::filesystem::path okldPath; + // THE TARGET'S PLATFORM (SPEC-013): the merged statement, its flags for + // this target and the programs its link and seal run. `active` is false + // for a target that uses none; the runner fields are filled either way, + // because a build program and the project state them for any target. + mcpp::platform::Resolved platform; // NASM assembly (.asm sources). Both resolved in prepare AFTER the plan // exists — only when the plan actually contains .asm units (lazy, hard // failure when unavailable; never a silent skip). @@ -769,7 +771,10 @@ make_plan(const mcpp::manifest::Manifest& manifest, // cache address of a dependency source that lives outside its own // package root (a build.mcpp OUT_DIR product). Empty is legal and // simply makes those units uncacheable. - const std::vector& storeRoots = {}); + const std::vector& storeRoots = {}, + // The target's platform (SPEC-013): its artifact names, when it + // states them, replace the target row's. + const mcpp::platform::Resolved* platform = nullptr); // Expand one manifest `include_dirs` entry against the project root — the // #249 consistency join + the expand_dir_glob the dep path uses. Exported @@ -1006,7 +1011,8 @@ std::filesystem::path target_output(const mcpp::manifest::Target& t, if (t.kind == mcpp::manifest::Target::SharedLibrary || (t.kind == mcpp::manifest::Target::Application && asSharedObject)) { return std::filesystem::path("bin") / - std::format("{}{}{}", n.libPrefix, t.name, n.sharedLibExt); + std::format("{}{}{}", n.sharedLibPrefixSet ? n.sharedLibPrefix : n.libPrefix, + t.name, n.sharedLibExt); } return std::filesystem::path("bin") / std::format("{}{}", t.name, n.exeSuffix); @@ -1639,7 +1645,8 @@ make_plan(const mcpp::manifest::Manifest& manifest, const std::filesystem::path& outputDir, const std::filesystem::path& stdBmiPath, const std::filesystem::path& stdObjectPath, - const std::vector& storeRoots) + const std::vector& storeRoots, + const mcpp::platform::Resolved* platform) { BuildPlan plan; plan.manifest = manifest; @@ -1664,7 +1671,27 @@ make_plan(const mcpp::manifest::Manifest& manifest, auto t = mcpp::toolchain::triple::parse(tc.targetTriple); return t ? *t : mcpp::toolchain::triple::Triple{}; }(); - const auto naming = naming_for(tc); + auto naming = naming_for(tc); + // A PLATFORM NAMES ITS ARTIFACTS (`[platform.link] exe`, `shared`): an + // ok image is `.oke`, an image another loads `lib.okso`. + // The patterns were checked where they were read. + // `ArtifactNaming` holds views; these are views into the platform's own + // strings, which outlive the plan's construction. + if (platform && platform->active) { + constexpr std::string_view kName = "{name}"; + auto const& l = platform->fields.link; + if (l.exe.set && mcpp::platform::split_name_pattern(l.exe.value)) { + const std::string_view v = l.exe.value; + if (v.starts_with(kName)) naming.exeSuffix = v.substr(kName.size()); + } + if (l.shared.set && mcpp::platform::split_name_pattern(l.shared.value)) { + const std::string_view v = l.shared.value; + const auto at = v.find(kName); + naming.sharedLibPrefix = v.substr(0, at); + naming.sharedLibPrefixSet = true; + naming.sharedLibExt = v.substr(at + kName.size()); + } + } // The loader-tag contract exists only where DT_RPATH/DT_RUNPATH do. // Mach-O and PE have neither, so they get no flag rather than a branch in @@ -2536,10 +2563,28 @@ make_plan(const mcpp::manifest::Manifest& manifest, // runtime: `provides = ["mcpp:..."]`) is beneath every image, and // where its objects go is the runtime contract's decision, which // #641 made (`cxx_runtime = { shared = "self-contained" }`). + // + // A KERNEL-ABI PACKAGE UNDER NO C LIBRARY OF THE GRAPH IS NOT A LAYER. + // When the C library is the platform's (glibc, libSystem, the Windows + // CRT), openkal-linux (-macos, -windows) is a static library over it + // that a package (`mcpplibs.os`) depends on like any other. Treated as + // a layer, it went into the programs only, and a shared image that + // reached it was left with `kal_*` undefined: ELF linked it, and a + // host that had not linked the package -- CPython importing an + // extension module -- refused to load it (`undefined symbol: + // kal_process_close`); Mach-O and PE refuse the link. It is beneath + // every image when a C library of the graph (`mcpp:c-abi`, openkal- + // musl) sits on it, and placed like any static package otherwise. + const bool graphHasCLibrary = std::ranges::any_of(packages, [](const auto& pkg) { + return std::ranges::any_of(pkg.manifest.provides, [](const std::string& p) { + return p.starts_with("mcpp:c-abi"); + }); + }); std::set boundaries; for (std::size_t i = 1; i < packages.size(); ++i) { auto const& pm = packages[i].manifest; - const bool layer = std::ranges::any_of(pm.provides, [](const std::string& p) { + const bool layer = std::ranges::any_of(pm.provides, [&](const std::string& p) { + if (p.starts_with("mcpp:kernel-abi")) return graphHasCLibrary; return p.starts_with("mcpp:") || p == "hosted-standard-library"; }); if (layer || mcpp::pack::is_distribution_package(pm)) boundaries.insert(i); @@ -3067,8 +3112,9 @@ make_plan(const mcpp::manifest::Manifest& manifest, t.name, all)); } if (!targetTriple.is_freestanding()) { - const std::string owner = targetTriple.is_openkal() - ? std::string("okrun, at `ok_main`, which the image's C library defines") + const std::string owner = targetTriple.loader_supplies_os() + ? std::string("its loader, at the entry its platform states " + "(`[platform.link] start`), which the image's C library defines") : targetTriple.is_android() ? std::string("the Android runtime, through the exports the platform names") : std::string("the C runtime (`_start`, `mainCRTStartup`), which initialises " diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 186b999e1..47e68d2af 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -87,6 +87,7 @@ import mcpp.toolchain.post_install; import mcpp.toolchain.abi; import mcpp.toolchain.triple; import mcpp.build.plan; +import mcpp.platform; import mcpp.build.flags; // compute_flags — the per-role contracts (#418) import mcpp.build.graph_shape; // #407: the graph says which mode wrote it import mcpp.build.build_program; @@ -524,6 +525,17 @@ export struct BuildContext { // where it was stated, for the line that announces it. std::vector runUnder; std::string runUnderOrigin; + // THE HOST GRAPH'S PLATFORM (SPEC-013 §6): what the build programs and the + // tools of this build are built for. Inactive = the host conventions. + // The target's is `plan.platform`. + mcpp::platform::Resolved hostPlatform; + // The host graph's triple and compiler, prepared for `mcpp self env --host` + // only: under a host platform, the platform's target of this machine's + // architecture and the platform's toolchain; else the toolchain the build + // programs are compiled with. + std::string hostTriple; + std::string hostCompiler; + std::string hostCompilerOrigin; // The payload directory of every `[xlings]` address resolved above, for // the fast path's presence check (#716). See BuildCacheEntry::xlingsPayloads. std::vector xlingsPayloads; @@ -803,6 +815,11 @@ export struct BuildOverrides { // project root, as the resolution input it is, while the lock this planning // produces is written under `work_dir`. Build programs still run. bool plan_only = false; + // THE CONFIGURATION, NOT THE SOURCES (`prepare_configuration`, SPEC-013 + // §9): what the project's sources would be refused for -- `import std` + // with no C++ run-time in the graph -- is a note of the report, and the + // configuration is still reported. + bool configuration_only = false; // #355 tool provisioning re-enters prepare_build for the tool package. A // tool package's own build.mcpp may legitimately want another tool (gRPC's // wants protoc), so the depth cannot be 1 — but an unbounded chain is a @@ -869,6 +886,10 @@ export struct BuildOverrides { // same relationship --target has with [toolchain]. std::string accel; bool force_static = false; // --static (or implied by musl target) + // `mcpp pack --linkage` (SPEC-014 §1): the whole graph's dependency + // linkage for this build, over `[build] dependency_linkage` and the + // profile's. Empty: as the project states it. + std::string dependency_linkage; std::string package_filter; // -p : only build this workspace member // The workspace members this plan builds, as written in `[workspace] // members` ("." is a rooted workspace's own package), all of one @@ -971,6 +992,16 @@ export struct BuildOverrides { // `prepare_build` itself, never by a caller. std::vector toolchain_statement; std::string bootstrap_spec; + // THE PLATFORM PACKAGE'S DIRECTORY, when this prepare is the second pass + // of a build whose platform states a target base (`[platform] sysroot`): + // the base is read before the graph exists, so the first pass fetched the + // package and the second reads its `[platform]` from here at once + // (SPEC-013 §7). Set by `prepare_build` itself, never by a caller. + std::filesystem::path platform_root; + // THE PLATFORM THIS PREPARE'S TARGET USES, stated by the caller: the + // host graph's platform, given to the build programs' sub-build (SPEC-013 + // §6). Read as the flag tier; empty on every other prepare. + std::string platform_choice; }; // ── git dependency helpers ────────────────────────────────────────────────── @@ -1034,6 +1065,16 @@ prepare_build(bool print_fingerprint, bool includeDevDeps = false, std::vector extraTargets = {}, BuildOverrides overrides = {}); +// ONE CONFIGURATION, PREPARED WITHOUT A BUILD (SPEC-013 §9): what `mcpp self +// env --target` reports and what `mcpp toolchain install --target` fetches -- +// the toolchain, the platform package, the packages it adds and the tools it +// builds for this machine -- so a later build of that target works offline. +// Inside a project the project's own configuration is prepared (its +// `[target.]` tables apply); elsewhere an empty program in the +// home's cache stands for one. Nothing is compiled but the tools. +export std::expected +prepare_configuration(std::string_view targetTriple); + // The PlanNotes a failed call recorded before the phase that failed it. // // On success, `prepare_build` copies `PrepareState::planNotes` into diff --git a/src/build/prepare/base.cpp b/src/build/prepare/base.cpp index 3750bd914..4b6923886 100644 --- a/src/build/prepare/base.cpp +++ b/src/build/prepare/base.cpp @@ -181,7 +181,15 @@ base_statement(PrepareState& state, const mcpp::toolchain::triple::Triple& t) { return settle(std::move(s)); } } - // 4. The engine's: the row, read where it always was (`effective_sysroot`). + // 4. The engine's: the platform's `sysroot` (SPEC-013 §2), then the row, + // read where it always was (`effective_sysroot`). + if (state.platformEarly && state.platformData.sysroot.set) { + auto const& o = state.platformData.sysroot.origin; + auto s = stamped(tb::parse_text(state.platformData.sysroot.value), "platform", o.file, + o.line, "[platform].sysroot"); + if (s) s->provides = state.platformData.sysrootProvides; + return settle(std::move(s)); + } return settle(tb::Statement{}); } diff --git a/src/build/prepare/configuration.cpp b/src/build/prepare/configuration.cpp new file mode 100644 index 000000000..ca23b6f3d --- /dev/null +++ b/src/build/prepare/configuration.cpp @@ -0,0 +1,58 @@ +// configuration.cpp -- one configuration prepared without a build (SPEC-013 +// §9): `mcpp self env --target` and `mcpp toolchain install --target`. +// Declared in prepare.cppm. + +module mcpp.build.prepare; +import :state; + +import std; +import mcpp.home; +import mcpp.project; +import mcpp.toolchain.triple; + +namespace mcpp::build { + +namespace fs = std::filesystem; + +std::expected prepare_configuration(std::string_view targetTriple) { + BuildOverrides ov; + ov.target_triple = std::string(targetTriple); + ov.configuration_only = true; + std::error_code ec; + if (auto root = mcpp::project::find_manifest_root(fs::current_path(ec))) { + ov.project_root = *root; + return prepare_build(/*print_fingerprint=*/false, /*includeDevDeps=*/false, {}, ov); + } + // No project here: an empty program in the home's cache stands for one, + // one directory per target, so its lock and its build directory persist + // and a second run finds everything on disk. + // The directory is named after the triple, which is checked first: the + // text names a directory under the home. + std::string name = "host"; + if (!targetTriple.empty()) { + auto t = mcpp::toolchain::triple::parse(targetTriple); + if (!t) return std::unexpected(std::format( + "'{}' is not a target triple (-[-], for example x86_64-openkal)", + targetTriple)); + name = t->str(); + } + const auto dir = mcpp::home::root() / "cache" / "configurations" / name; + fs::create_directories(dir / "src", ec); + if (ec) return std::unexpected(std::format("cannot create {}: {}", dir.string(), ec.message())); + auto write_if_absent = [&](const fs::path& p, std::string_view text) { + if (fs::exists(p, ec)) return true; + std::ofstream out(p, std::ios::binary); + out << text; + return bool(out); + }; + if (!write_if_absent(dir / "mcpp.toml", + "# The configuration `mcpp self env --target` and `mcpp toolchain install\n" + "# --target` prepare when they are run outside a project (written by mcpp).\n" + "[package]\nname = \"configuration\"\nversion = \"0.0.0\"\n") + || !write_if_absent(dir / "src" / "main.cpp", "int main() { return 0; }\n")) + return std::unexpected(std::format("cannot write the configuration under {}", dir.string())); + ov.project_root = dir; + return prepare_build(/*print_fingerprint=*/false, /*includeDevDeps=*/false, {}, ov); +} + +} // namespace mcpp::build diff --git a/src/build/prepare/driver.cpp b/src/build/prepare/driver.cpp index 0e0ec7bee..c3213ce92 100644 --- a/src/build/prepare/driver.cpp +++ b/src/build/prepare/driver.cpp @@ -90,7 +90,14 @@ prepare_build(bool print_fingerprint, return prepare_build_pass(print_fingerprint, includeDevDeps, std::move(extraTargets), std::move(overrides)); auto first = prepare_build_pass(print_fingerprint, includeDevDeps, extraTargets, overrides); - if (first) { (void)take_toolchain_restart(); return first; } + if (first) { (void)take_toolchain_restart(); (void)take_platform_restart(); return first; } + // A platform that states a target base, fetched by this pass: the next + // reads its `[platform]` before the base is decided (SPEC-013 §7). + if (auto root = take_platform_restart(); root && overrides.platform_root.empty()) { + overrides.platform_root = std::move(*root); + return prepare_build(print_fingerprint, includeDevDeps, std::move(extraTargets), + std::move(overrides)); + } auto restart = take_toolchain_restart(); if (!restart) return first; overrides.toolchain_statement = std::move(restart->first); @@ -122,6 +129,13 @@ void read_build_programs_marker(BuildOverrides& ov) { if (eq == std::string::npos) continue; const auto key = line.substr(0, eq), value = line.substr(eq + 1); if (key == "toolchain" && ov.toolchain.empty()) ov.toolchain = value; + // A host graph with a platform (SPEC-013 §6): the programs are built + // for its target, by its choice. + else if (key == "platform" && ov.platform_choice.empty()) ov.platform_choice = value; + else if (key == "target" && ov.target_triple.empty()) { + ov.target_triple = value; + ov.host_target = false; + } else if (key == "profile" && ov.profile.empty()) ov.profile = value; else if (key == "index-store" && ov.project_index_root.empty()) ov.project_index_root = value; else if (key == "pin") @@ -136,8 +150,14 @@ prepare_build_pass(bool print_fingerprint, std::vector extraTargets, BuildOverrides overrides) { read_build_programs_marker(overrides); - PrepareState state(print_fingerprint, includeDevDeps, - std::move(extraTargets), std::move(overrides)); + // ON THE HEAP. The state is over 100 KiB, and a prepare nests: a host + // tool's sub-build, then its build programs' graph, each its own prepare. + // Three of them on a Windows main thread's 1 MiB stack overflowed it + // (measured: openkal-platform's tests on windows-2022 ended with exit 127 + // and no word, in the host graph of `okld`'s build). + auto statePtr = std::make_unique(print_fingerprint, includeDevDeps, + std::move(extraTargets), std::move(overrides)); + PrepareState& state = *statePtr; pending_flag_words_notes().clear(); g_notesOnFailure.clear(); diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 8bd7e9c60..f0eb91442 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -7,6 +7,7 @@ import :state; import mcpp.build.prepare_inputs; import std; +import mcpp.platform; import mcpp.diag; import mcpp.log; import mcpp.build.refusal; @@ -181,10 +182,23 @@ static void step6_compat_library_edges(PrepareState& state) { // Nothing is cleared from a manifest: `compilesHere` reads the result, and the // link flags a package without a target instance contributed during // resolution are withdrawn from the root's link line. -static void step6_instances(PrepareState& state) { +void merge_program_platform(PrepareState& state, mcpp::manifest::platform::Statement stated, + std::string_view who) { + for (auto* o : { &stated.link.driver.origin, &stated.link.inputsOrigin, + &stated.link.exe.origin, &stated.link.shared.origin, + &stated.link.sealOrigin, &stated.runner.origin }) + if (o->tier == mcpp::manifest::platform::Tier::Program) + o->key = std::format("{} ({})", who, o->key); + if (!stated.empty() || !stated.runner.host.empty()) + mcpp::platform::merge(state.programPlatform, stated); +} + +PackageInstances package_instances(const PrepareState& state) { const auto n = state.packages.size(); - if (n == 0) return; - std::vector target(n, false), host(n, false); + PackageInstances out{ std::vector(n, false), std::vector(n, false) }; + if (n == 0) return out; + auto& target = out.target; + auto& host = out.host; std::vector stack{0}; target[0] = true; while (!stack.empty()) { @@ -214,6 +228,13 @@ static void step6_instances(PrepareState& state) { stack.push_back(d); } } + return out; +} + +static void step6_instances(PrepareState& state) { + const auto n = state.packages.size(); + if (n == 0) return; + const auto [target, host] = package_instances(state); for (std::size_t d = 1; d < n; ++d) { state.packages[d].hostInstance = host[d]; // A package no edge mentions is a bookkeeping gap, not a build @@ -1788,6 +1809,13 @@ static std::expected step6_dependency_build_programs(PrepareS break; } } + // THE PLATFORM'S PLUGIN (SPEC-013 §4.2): the platform package's + // program runs in the `platform` phase, and states only the + // platform's build, link and runner fields. + const bool platformPhase = state.platform.active && state.platformData.plugin.set + && std::filesystem::weakly_canonical(pkg.root) + == std::filesystem::weakly_canonical(state.platform.root); + if (platformPhase) bpEnv.phase = "platform"; bpEnv.languageModules = pkg.manifest.language.modules; bpEnv.ruleModules = pkg.manifest.buildConfig.ruleModules; if (auto dit = state.deviceSourcesByPackage.find(pkg.root.string()); dit != state.deviceSourcesByPackage.end()) @@ -1814,12 +1842,15 @@ static std::expected step6_dependency_build_programs(PrepareS if (!program && !state.overrides.plan_only) return std::unexpected(program.error()); if (program) { bpEnv.programBinary = program->path; + bpEnv.programRunner = program->runner; bpEnv.programIdentity = program->identity; bpEnv.programCompile = program->compile; } else { bpEnv.programError = program.error(); } auto& bcDep = pkg.manifest.buildConfig; + const auto cxxN = bcDep.cxxflags.size(); + const auto cN = bcDep.cflags.size(); const auto mark = state.markDirectiveTail(pkg.manifest); const auto ldN = bcDep.ldflags.size(); const auto actN = bcDep.actions.size(); @@ -1935,6 +1966,20 @@ static std::expected step6_dependency_build_programs(PrepareS state.m->buildConfig.runExclusive = true; state.m->buildConfig.ldflags.insert(state.m->buildConfig.ldflags.end(), bcDep.ldflags.begin() + ldN, bcDep.ldflags.end()); + // THE PLATFORM'S FIELDS A PROGRAM STATED (protocol 17), at the + // build-program tier of the build's platform statement, named + // after the package (SPEC-013 §4.1). A plugin's compile flags are + // the platform's, for every unit of the target, not its own. + { + auto stated = std::exchange(bcDep.programPlatform, {}); + if (platformPhase) { + stated.build.cxxflags.assign(bcDep.cxxflags.begin() + static_cast(cxxN), + bcDep.cxxflags.end()); + stated.build.cflags.assign(bcDep.cflags.begin() + static_cast(cN), + bcDep.cflags.end()); + } + merge_program_platform(state, std::move(stated), pkg.manifest.package.name); + } } // apply() may have added interface defines to packages' publicUsage diff --git a/src/build/prepare/graph.cpp b/src/build/prepare/graph.cpp index 771d6774f..26926c464 100644 --- a/src/build/prepare/graph.cpp +++ b/src/build/prepare/graph.cpp @@ -988,6 +988,36 @@ step4b_identity_version_merge(PrepareState& state, WorklistItemCtx& ctx, spec.version)); } + // THE RENAME'S BOUNDARY (design 2026-10-28 §8.2): it renames + // C++ modules and nothing else, so it separates two versions + // only when their whole interface is named modules. A package + // that exports a header directory, or provides a target-side + // layer, has C names and link symbols the rename cannot touch: + // two of them meet in one link, or in one include path, at a + // place that names neither (measured with two openkal + // releases in one graph). Refused here, naming both requests. + { + const bool headers = !secondaryManifest.buildConfig.includeDirs.empty(); + const bool layer = std::ranges::any_of(secondaryManifest.provides, + [](const std::string& c) { return c.starts_with("mcpp:"); }); + if (headers || layer) + return std::unexpected(std::format( + "dependency '{}{}{}' has irreconcilable versions:\n" + " '{}' requested by '{}'\n" + " '{}' requested by '{}'\n" + "Two versions cannot share one build: the package {}, and a " + "renamed copy keeps those names, so they would collide in the " + "link or in the include path. Align the requests on one version " + "(pin it in your mcpp.toml){}.", + key.ns, key.ns.empty() ? "" : ".", key.shortName, + it->second.version, it->second.requestedBy, + spec.version, item.requestedBy, + layer ? "provides a target-side layer" : "exports a header directory", + state.platform.active + ? std::format(", or select the release of {} that carries the " + "version you need", state.platform.display()) + : std::string())); + } // Module names are authored API and are not required to // mirror package identity. Discover every provided module // root from the secondary's source text, then rewrite the @@ -2364,6 +2394,10 @@ std::expected phase4b_graph_worklist(PrepareState& state) { childSpec.features.push_back(depFeat); }; + // The platform package first: its `[platform]` decides what the graph + // is completed with once the root's own requests are walked (SPEC-013). + step4b_seed_platform(state); + // Seed the worklist from the main manifest. Dev-deps only when the // caller wants them; they're never propagated transitively. const std::string mainPkgLabel = state.m->package.name; @@ -2398,25 +2432,46 @@ std::expected phase4b_graph_worklist(PrepareState& state) { } - while (!state.worklist.empty()) { - WorklistItemCtx ctx; - ctx.item = std::move(state.worklist.front()); - state.worklist.pop_front(); + auto drain = [&]() -> std::expected { + while (!state.worklist.empty()) { + WorklistItemCtx ctx; + ctx.item = std::move(state.worklist.front()); + state.worklist.pop_front(); - if (auto r = step4b_resolve_identity(state, ctx); !r) - return std::unexpected(r.error()); + if (auto r = step4b_resolve_identity(state, ctx); !r) + return std::unexpected(r.error()); + + if (auto it = state.resolved.find(ctx.key); it != state.resolved.end()) { + if (auto r = step4b_handle_already_resolved(state, ctx, it); !r) + return std::unexpected(r.error()); + continue; + } - if (auto it = state.resolved.find(ctx.key); it != state.resolved.end()) { - if (auto r = step4b_handle_already_resolved(state, ctx, it); !r) + if (auto r = step4b_acquire_dependency_source(state, ctx); !r) + return std::unexpected(r.error()); + if (auto r = step4b_finalize_dependency(state, ctx); !r) return std::unexpected(r.error()); - continue; } - - if (auto r = step4b_acquire_dependency_source(state, ctx); !r) - return std::unexpected(r.error()); - if (auto r = step4b_finalize_dependency(state, ctx); !r) - return std::unexpected(r.error()); + return {}; + }; + if (auto r = drain(); !r) return r; + // THE PLATFORM COMPLETES THE GRAPH (SPEC-013 §4.3, §4.4): the layers no + // package of the graph provides and the tools it does not build are added + // as the platform's, and walked like any request. A few rounds at most: the + // packages one round adds may provide a layer the next then leaves. + // The version group is checked before each round's walk as well as after + // the last: a package of the family the project pinned outside the group + // is refused as such, before the platform's own packages would meet it as + // a second version of themselves. + for (int round = 0; round < 3; ++round) { + auto pushed = step4b_apply_platform(state); + if (!pushed) return std::unexpected(pushed.error()); + if (auto r = step4b_check_platform_versions(state); !r) return r; + if (!*pushed) break; + if (auto r = drain(); !r) return r; } + if (auto r = step4b_check_platform_versions(state); !r) return r; + step4b_platform_member_edges(state); if (auto r = step4b_cycle_check(state); !r) return std::unexpected(r.error()); diff --git a/src/build/prepare/graph_load.cpp b/src/build/prepare/graph_load.cpp index ee28f0e9c..13818af10 100644 --- a/src/build/prepare/graph_load.cpp +++ b/src/build/prepare/graph_load.cpp @@ -682,8 +682,14 @@ static void step4a_define_candidate_selection_closures(PrepareState& state) { } // Offline with no local copy of the index at all, the miss says // nothing about the package: nothing has been downloaded to look - // in. That is a download the run needs, not a wrong selector. - if (mcpp::effects::offline_mode()) { + // in. That is a download the run needs, not a wrong selector -- + // unless every candidate's namespace is served by an index of + // the project's own (`[indices]`), which is on disk and answered. + const bool servedByProjectIndex = std::ranges::all_of(candidates, + [&](const mcpp::pm::DependencyCoordinate& c) { + return state.index_route(nullptr).find_for_ns(c.namespace_) != nullptr; + }); + if (mcpp::effects::offline_mode() && !servedByProjectIndex) { if (auto cfgO = state.get_cfg(true); cfgO && !mcpp::provider::open(mcpp::config::make_xlings_env(**cfgO))->default_index_status(0).present) { hint += "\n offline: the package index has never been fetched; " @@ -1058,6 +1064,38 @@ step4a_load_version_dep_fetch(PrepareState& state, LoadVersionDepCtx& ctx) { mcpp::invocation::ScopedEnv hookVar4(hookVars.at(4).first, hookVars.at(4).second); mcpp::invocation::ScopedEnv hookVar5(hookVars.at(5).first, hookVars.at(5).second); auto r = install_one(target); + // A MISS AT INSTALL REFRESHES THE INDEX, THEN ASKS ONCE MORE. The + // walk refreshes when a descriptor is missing, and deliberately not + // when a descriptor is present without the version asked for: + // version tables are per system, so that absence is not proof. The + // provider's "not found" for that version is, and it is the only + // place the stale case shows (measured: `openkal-llvm-runtime = + // "0.16.3"` against an index synced before 0.16.3, refused until + // `mcpp index update`). The policy decides (offline, `auto_refresh`, + // the debounce, one sync per run). A project's own index is read as + // it is, so it is not refreshed here. + // What mcpp did about the miss, for the message if it stays one + // (L4): the refresh, and why it ran or did not. + std::string refreshNote; + const bool missedVersion = r && r->exitCode != 0 + && mcpp::pm::install_failure_policy(r->error ? r->error->code : std::string{}).refreshIndex; + if (missedVersion && !ctx.useProjectEnv) { + const auto env = mcpp::config::make_xlings_env(**cfg); + auto decision = mcpp::pm::decide_for_miss( + mcpp::pm::policy_for(**cfg), env, target, mcpp::pm::RefreshReason::InstallMiss); + if (auto applied = mcpp::pm::apply(decision, env); !applied) { + mcpp::ui::warning(applied.error()); + refreshNote = std::format("refreshing it failed: {}", applied.error()); + } else if (decision.shouldRefresh) { + r = install_one(target); + refreshNote = "refreshed it now, and the version is still not in it"; + } else { + refreshNote = std::format("did not refresh it: {}", + mcpp::pm::reason_text(decision.reason)); + } + } else if (missedVersion) { + refreshNote = "did not refresh it: a project's own index is read as it is"; + } // The first address's failure is the one about the package; a // compat spelling tried after it fails as "not found" whatever // the cause was. @@ -1067,11 +1105,14 @@ step4a_load_version_dep_fetch(PrepareState& state, LoadVersionDepCtx& ctx) { if (r->error) firstError += (firstError.empty() ? "" : "; ") + r->error->message; } - if (r && r->exitCode != 0 && + if (r && r->exitCode != 0 && !ctx.luaContent && (ns.empty() || ns == mcpp::pm::kDefaultNamespace)) { // Compat retry for a bare/default-namespace request whose // descriptor could not be read (no `wireAddr` to trust): the - // package may still be a `compat` one. Try BOTH spellings — a + // package may still be a `compat` one. A descriptor that was + // read names the package, and its failure is that package's: + // another namespace is not tried and not shown as downloading. + // Try BOTH spellings — a // SPEC-001 index keys it `compat:`, a pre-SPEC-001 index // keys it by the literal `compat.`. Sending only the // latter is what left the retry pointing at a name the migrated @@ -1113,13 +1154,38 @@ step4a_load_version_dep_fetch(PrepareState& state, LoadVersionDepCtx& ctx) { childErr += r->error->message; } auto target = std::format("{}@{}", depName, version); - auto diag = mcpp::pm::format_install_failure_diagnostic( - target, r->exitCode, indexRepos, childErr); std::string tried; for (auto& a : attempted) { if (!tried.empty()) tried += ", "; tried += a; } + // A VERSION THE INDEX DOES NOT HAVE IS SAID BY MCPP (L4): which + // index, at which revision and how old, and what was done about + // it. The provider's own sentence ("the index is current") is + // about its index, not mcpp's, and reads as the opposite of the + // cause; it stays available with -v. + if (missedVersion) { + std::string diag = std::format( + "the package index has no {} to install\n" + " index: {}\n" + " mcpp {}\n" + " if the version was published since, `mcpp index update` fetches it; " + "otherwise the version is not in the index", + target, ctx.useProjectEnv ? std::string("the project's own index") + : mcpp::pm::staleness_note(mcpp::config::make_xlings_env(**cfg)), + refreshNote.empty() ? std::string("did not refresh it") : refreshNote); + if (auto hint = mcpp::pm::unusable_index_hint(); !hint.empty()) diag += "\n" + hint; + if (mcpp::log::is_verbose()) + diag += "\n " + mcpp::pm::format_install_failure_diagnostic( + target, r->exitCode, indexRepos, childErr); + else if (!childErr.empty()) + diag += "\n (the provider's own report: run with -v)"; + diag += std::format("\n wire address{} tried: {}", + attempted.size() == 1 ? "" : "es", tried); + return std::unexpected(std::move(diag)); + } + auto diag = mcpp::pm::format_install_failure_diagnostic( + target, r->exitCode, indexRepos, childErr); diag += std::format("\n wire address{} tried: {}", attempted.size() == 1 ? "" : "es", tried); return std::unexpected(std::move(diag)); diff --git a/src/build/prepare/host_graph.cpp b/src/build/prepare/host_graph.cpp index 86056eeeb..ff5813f08 100644 --- a/src/build/prepare/host_graph.cpp +++ b/src/build/prepare/host_graph.cpp @@ -210,12 +210,30 @@ std::string features_of(const PrepareState& state, std::size_t d) { struct HostToolchain { std::string spec; std::string identity; + // The host graph's platform (SPEC-013 §6): the spelling the sub-build is + // told it by and the target of this machine's architecture it serves. + // Empty: the build programs are this machine's programs. + std::string platform; + std::string platformTarget; }; std::expected host_toolchain(PrepareState& state) { + HostToolchain t; + // UNDER A HOST PLATFORM the build programs are images of this machine's + // architecture: the platform's toolchain compiles them and its runner + // runs them, the same on every system (§6.3). Their identity is the + // platform's, with no trace of this machine's compiler. + if (!state.hostPlatformSel.none) { + if (auto r = load_host_platform(state); !r) return std::unexpected(r.error()); + t.platform = host_platform_choice(state); + t.platformTarget = state.hostPlatformTarget; + t.spec = state.hostPlatform.fields.toolchain.value; + t.identity = std::format("platform {}|{}|{}", state.hostPlatform.identity(), + t.platformTarget, t.spec); + return t; + } auto host = state.host_tc_for_build_program(); if (!host) return std::unexpected(host.error()); - HostToolchain t; t.spec = state.host_spec_for_build_program(); t.identity = std::format("{}|{}|{}|{}", t.spec, path_text(host->first), host->second.version, host->second.targetTriple); @@ -606,6 +624,11 @@ void write_group(PrepareState& state, const fs::path& dir, const std::vector sub.target_triple = ""; sub.host_target = true; sub.toolchain = tc.spec; + // Under a host platform: the platform's target of this architecture, and + // its toolchain by the platform's own pin. + if (!tc.platform.empty()) { + sub.target_triple = tc.platformTarget; + sub.host_target = false; + sub.toolchain = ""; + sub.platform_choice = tc.platform; + } sub.profile = "build-program"; sub.cache_mode = state.overrides.cache_mode; sub.strict = state.overrides.strict; @@ -749,6 +780,24 @@ build_group(PrepareState& state, const fs::path& dir, const std::vector describe(*ctx); if (goals.empty()) return std::map{}; + // THE RUNNER, under a host platform: the platform's, with its program + // as that build located it (§6.1). A platform with none cannot run what + // it built, which is said here rather than at the first program's run. + if (!tc.platform.empty()) { + auto const& pr = ctx->plan.platform.fields.runner; + if (!pr.set || pr.cmd.empty()) + return std::unexpected(std::format( + "the host graph's platform {} states no runner, so the build programs it " + "builds cannot be run", ctx->plan.platform.display())); + std::vector argv = pr.cmd; + if (auto it = ctx->plan.platform.tools.find(argv.front()); it != ctx->plan.platform.tools.end()) + argv.front() = it->second; + state.hostPlatformRunner = std::move(argv); + // The tools the host graph's build located, for the report of the + // host configuration (`self env --host`). + state.hostPlatform.tools = ctx->plan.platform.tools; + state.hostPlatform.fields.runner = pr; + } std::map binaries; std::vector ninjaGoals; for (auto const& lu : ctx->plan.linkUnits) { @@ -980,8 +1029,18 @@ ensure_build_programs(PrepareState& state, const std::vector& packa && fs::exists(it->value("binary", std::string{}), ec); if (current) { place_program_copy(state, p.index, fs::path(it->value("binary", std::string{}))); + std::vector runner; + if (it->contains("runner") && (*it)["runner"].is_array()) + runner = (*it)["runner"].get>(); + // A program built under the host platform by an earlier run: + // its runner as that run located it, for the report. + if (!runner.empty() && state.hostPlatformRunner.empty()) { + state.hostPlatformRunner = runner; + auto const& cmd = state.hostPlatform.fields.runner.cmd; + if (!cmd.empty()) state.hostPlatform.tools[cmd.front()] = runner.front(); + } state.buildProgramBinaries[p.index] = {fs::path(it->value("binary", std::string{})), - p.identity}; + p.identity, {}, std::move(runner)}; mcpp::log::verbose("build-program", std::format( "{}: identity {} current", qualified(manifest_of(state, p.index)), p.identity)); continue; @@ -1022,11 +1081,12 @@ ensure_build_programs(PrepareState& state, const std::vector& packa std::chrono::steady_clock::now() - began); for (auto const* p : stale) { const auto bin = built->at(p->member); - state.buildProgramBinaries[p->index] = {bin, p->identity, took}; + state.buildProgramBinaries[p->index] = {bin, p->identity, took, state.hostPlatformRunner}; place_program_copy(state, p->index, bin); nlohmann::json row; row["identity"] = p->identity; row["binary"] = path_text(bin); + if (!state.hostPlatformRunner.empty()) row["runner"] = state.hostPlatformRunner; row["package"] = qualified(manifest_of(state, p->index)); row["source"] = p->source.file.empty() ? std::string("synthesised") : path_text(p->source.file); diff --git a/src/build/prepare/local_toolchain.cpp b/src/build/prepare/local_toolchain.cpp index 4623c720d..fc9c9fd41 100644 --- a/src/build/prepare/local_toolchain.cpp +++ b/src/build/prepare/local_toolchain.cpp @@ -388,6 +388,7 @@ std::expected step6_toolchain_phase(PrepareState& state) { if (!program) return std::unexpected(std::format("the root build program's toolchain phase: {}", program.error())); bpEnv.programBinary = program->path; + bpEnv.programRunner = program->runner; bpEnv.programIdentity = program->identity; bpEnv.programCompile = program->compile; bpEnv.phase = "toolchain"; diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 7dce6c016..2b71e9912 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -3,7 +3,6 @@ // resolution.json are written by records.cpp. module mcpp.build.prepare; -import mcpp.okimage; import :state; import mcpp.build.prepare_inputs; @@ -448,9 +447,12 @@ static std::expected step13_make_plan(PrepareState& state, Bu for (std::size_t i = 1; i < state.packages.size(); ++i) state.packages[i].linkUsage.ldflags = normalized_dependency_ldflags( state.packages[i].root, state.packages[i].manifest.buildConfig.ldflags); + // The platform's statement, final: every build program has run. + step13_platform_statement(state); auto planResult = mcpp::build::make_plan(*state.m, *state.tc, state.fp, state.scan.graph, state.report.topoOrder, state.packages, *state.root, ctx.outputDir, - state.stdBmiPath, state.stdObjectPath, state.storeRoots); + state.stdBmiPath, state.stdObjectPath, state.storeRoots, + &state.platform); if (!planResult) return std::unexpected(planResult.error()); ctx.plan = std::move(*planResult); // `trim_paths` per package (mcpp#844, design 2026-10-26 part 2 §10.4): a @@ -1482,36 +1484,11 @@ static std::expected step13_build_graph_actions(PrepareState& } static std::expected step13_assembly_units(PrepareState& state, BuildContext& ctx) { - // AN OK IMAGE IS FINISHED BY `okld` (mcpp.okimage): the graph's, a - // declared payload's, or this machine's, in that order. Its absence is a - // refusal before anything compiles, naming the dependency that supplies - // it -- not a link that succeeds and an image no loader accepts. - // After `make plan`, which builds the plan these fields belong to. - if (auto tt = mcpp::toolchain::triple::parse(state.tc->targetTriple); tt && tt->is_openkal()) { - const std::string tool(mcpp::okimage::kTool); - if (auto it = state.rootTools.find(tool); it != state.rootTools.end()) { - ctx.plan.okldPath = it->second; - } else { - for (auto const& dir : ctx.xlingsDepBinDirs) { - for (auto const& name : { tool, tool + ".exe" }) { - std::error_code ec; - if (std::filesystem::is_regular_file(dir / name, ec)) { ctx.plan.okldPath = dir / name; break; } - } - if (!ctx.plan.okldPath.empty()) break; - } - if (ctx.plan.okldPath.empty()) - if (auto found = mcpp::invocation::which(tool)) ctx.plan.okldPath = *found; - } - // Only a build that links an image needs it: a library's archive is - // objects, and the image that takes them is checked where it is linked. - const bool linksAnImage = std::ranges::any_of(ctx.plan.linkUnits, [](auto const& lu) { - return lu.kind != mcpp::build::LinkUnit::StaticLibrary; - }); - if (ctx.plan.okldPath.empty() && linksAnImage && !state.overrides.plan_only) { - mcpp::build::refusal::record(mcpp::build::refusal::Code::ImageToolAbsent); - return std::unexpected(mcpp::okimage::no_tool_message(tt->str(), tool)); - } - } + // THE PLATFORM, FINISHED (SPEC-013): the statement every reader uses, + // with the build programs' directives in it, its flags evaluated for the + // target, and the programs its link and seal run located. After `make + // plan`, which builds the plan this belongs to. + if (auto r = step13_finish_platform(state, ctx); !r) return std::unexpected(r.error()); // Clang: discover clang-scan-deps for P1689 dyndep scanning. if (mcpp::toolchain::is_clang(*state.tc)) { if (auto sd = mcpp::toolchain::clang::find_scan_deps(*state.tc)) { @@ -2140,7 +2117,9 @@ std::expected phase13_finish(PrepareState& state) { ctx.profile = state.effectiveProfile; ctx.activeFeatureRequest = state.workspacePlan() ? state.requestedFeatures : state.overrides.features; - ctx.compilerChoice = { std::string(tc_origin_name(state.tcOrigin)), + ctx.compilerChoice = { state.platformToolchainPin && state.tcOrigin == TcOrigin::TargetPin + ? std::string("platform") + : std::string(tc_origin_name(state.tcOrigin)), state.graphCompilerRequiredBy, state.graphCompilerReplaced.empty() ? state.pinReplacedDefault : state.graphCompilerReplaced }; diff --git a/src/build/prepare/platform.cpp b/src/build/prepare/platform.cpp new file mode 100644 index 000000000..ea080a5f6 --- /dev/null +++ b/src/build/prepare/platform.cpp @@ -0,0 +1,733 @@ +// platform.cpp -- the platform of the target graph through the prepare phases +// (SPEC-013; design 2026-10-28 §5, §7-§9). Declared in `:state`. +// +// P1 which platform (`step1_select_platform`): before the conditional +// tables merge, so `cfg(platform = ...)` is answered with the rest of +// the target's keys. +// P4b the platform package is the root's first dependency +// (`step4b_seed_platform`); once the graph has been walked, its +// `[platform]` is read and merged with this machine's and the +// project's overrides, and the layers and tools the graph lacks are +// added to it (`step4b_apply_platform`, walked again by the caller); +// a version group the graph contradicts is refused +// (`step4b_check_platform_versions`). +// P5 its toolchain (`step5_platform_toolchain`). +// P13 the statement every reader uses (`step13_finish_platform`): +// directives merged in, flags evaluated for the target, tools located. + +module mcpp.build.prepare; +import :state; + +import std; +import mcpp.build.prepare_inputs; +import mcpp.build.refusal; +import mcpp.config; +import mcpp.invocation; +import mcpp.manifest; +import mcpp.platform; +import mcpp.pm.compat; +import mcpp.targetside; +import mcpp.toolchain.triple; +import mcpp.toolchain.platform; +import mcpp.ui; +import mcpp.freestanding.linkline; + +namespace mcpp::build { + +namespace fs = std::filesystem; +namespace mp = mcpp::manifest::platform; + +namespace { + +mcpp::toolchain::triple::Triple target_of(const PrepareState& state) { + if (!state.overrides.target_triple.empty()) + if (auto t = mcpp::toolchain::triple::parse(state.overrides.target_triple)) return *t; + return mcpp::toolchain::triple::host_triple(); +} + +// The package short name a reference names, before the package is fetched: +// the reference's own name (a git reference always has one), or the +// `[package]` of a directory. +std::string known_short_name(const mp::PackageRef& r) { + if (!r.spec.shortName.empty()) return r.spec.shortName; + if (!r.spec.path.empty()) { + if (auto m = mcpp::manifest::load(fs::path(r.spec.path) / "mcpp.toml")) + return mcpp::pm::compat::resolve_package_name(m->package.name, m->package.namespace_) + .shortName; + return {}; + } + return {}; +} + +// Whether this invocation's command line stated a variable (`--platform` is +// `MCPP_PLATFORM` for the invocation), rather than the environment. +bool stated_on_command_line(std::string_view name) { + return std::ranges::any_of(mcpp::invocation::overrides(), [&](auto const& o) { + return o.first == name && o.second.has_value(); + }); +} + +// The project's override tables: the plan root's, which hold the workspace +// layer's first (`merge_layer_root_position`). +std::vector project_tables(const mcpp::manifest::Manifest& m) { + return m.platformOverrides; +} + +// Which layers each package of the graph provides, by layer. +std::map provided_layers(const PrepareState& state) { + std::map out; + for (std::size_t i = 1; i < state.packages.size(); ++i) { + auto const& pm = state.packages[i].manifest; + for (auto const& entry : pm.provides) { + auto cap = mcpp::targetside::parse_capability(entry); + if (!cap || !*cap) continue; + auto const& c = **cap; + std::string layer(mcpp::targetside::cap_layer_name(c.layer)); + if (!mp::is_layer(layer)) continue; + out.try_emplace(layer, pm.package.version.empty() + ? pm.package.name + : std::format("{}@{}", pm.package.name, pm.package.version)); + } + } + return out; +} + +std::string qualified(const mcpp::manifest::Manifest& m) { + auto n = mcpp::pm::compat::resolve_package_name(m.package.name, m.package.namespace_); + return mcpp::pm::compat::qualified_name(n.namespace_, n.shortName); +} + +// A package the platform added, as `platformInjected` records it: qualified, +// the default namespace spelled out (the lock asks in that spelling). +std::string injected_name(const mcpp::manifest::DependencySpec& spec) { + return mcpp::pm::compat::qualified_name( + spec.namespace_.empty() ? std::string(mcpp::pm::kDefaultNamespace) : spec.namespace_, spec.shortName); +} + +// The key a worklist item for a reference is written under: its name, or +// the short name its source gives. +std::string item_key(const mp::PackageRef& r) { + if (!r.name.empty()) return r.name; + return known_short_name(r); +} + +// The selector evaluator the platform module asks: this graph's context. +mcpp::platform::Matches matcher(cfgpred::Ctx c) { + return [c = std::move(c)](std::string_view sel) { return cfgpred::matches(std::string(sel), c); }; +} + +} // namespace + +cfgpred::Ctx platform_cfg_context(const PrepareState& state) { + return cfgpred::context_for(target_of(state).str(), state.platformPackageShortName); +} + +std::expected step1_select_platform(PrepareState& state) { + // A host sub-build (build programs, tools) is the host graph: its platform + // is the one its caller states (the build programs' graph under a host + // platform), or the host conventions -- and a platform's own tools are + // always built so (the bootstrap boundary, §6.2). + const bool nested = state.overrides.host_target || state.overrides.tool_depth > 0 + || state.overrides.build_program_graph; + if (nested && state.overrides.platform_choice.empty()) return {}; + // THE HOST GRAPH'S PLATFORM, chosen by the outermost prepare (§6.1). + if (!nested) { + mp::Override config; + if (auto cfg = state.get_cfg(false); cfg) config = (*cfg)->hostPlatform; + const auto hostFlag = mcpp::invocation::get("MCPP_HOST_PLATFORM").value_or(""); + mp::Override project = state.m->hostPlatform; + auto hs = mcpp::platform::select_host(hostFlag, stated_on_command_line("MCPP_HOST_PLATFORM"), + project, config); + if (!hs) return std::unexpected(hs.error()); + state.hostPlatformSel = std::move(*hs); + } + const auto t = target_of(state); + const auto flag = !state.overrides.platform_choice.empty() + ? state.overrides.platform_choice + : mcpp::invocation::get("MCPP_PLATFORM").value_or(""); + const auto envKey = target_env_name(t, "PLATFORM"); + const auto env = mcpp::invocation::get(envKey).value_or(""); + auto project = project_tables(*state.m); + std::vector config; + if (auto cfg = state.get_cfg(false); cfg) config = (*cfg)->platformOverrides; + std::string column; + if (auto* row = mcpp::toolchain::triple::find_known_target(t)) column = std::string(row->platform); + mcpp::platform::ChoiceInputs in{ flag, env, envKey, project, config, column }; + if (state.overrides.platform_choice.empty() && !stated_on_command_line("MCPP_PLATFORM")) + in.flagKey = "MCPP_PLATFORM"; + auto sel = mcpp::platform::select_target(in, matcher(cfgpred::context_for(t.str()))); + if (!sel) return std::unexpected(sel.error()); + state.platform = mcpp::platform::Resolved{}; + state.platform.selection = std::move(*sel); + if (!state.platform.selection.none) { + auto const& ref = state.platform.selection.ref; + // A directory is read now, and its manifest's own error is the answer + // when it cannot be. + if (!ref.spec.path.empty() && ref.name.empty()) { + auto m = mcpp::manifest::load(fs::path(ref.spec.path) / "mcpp.toml"); + if (!m) + return std::unexpected(std::format("{}: the platform {}: {}", + mcpp::platform::origin_text(state.platform.selection.origin), + mp::display(ref), m.error().format())); + } + state.platformPackageShortName = known_short_name(ref); + if (state.platformPackageShortName.empty()) + return std::unexpected(std::format( + "{}: the platform {} names no package; write {{ name = \"\", ... }}", + mcpp::platform::origin_text(state.platform.selection.origin), + mp::display(state.platform.selection.ref))); + // READ NOW WHEN IT IS ON DISK: a directory, or the package a first + // pass fetched. The target base is decided before the graph exists, + // and a platform may state one (§7). + fs::path early = state.overrides.platform_root; + if (early.empty() && !state.platform.selection.ref.spec.path.empty()) + early = state.platform.selection.ref.spec.path; + if (!early.empty()) + if (auto m = mcpp::manifest::load(early / "mcpp.toml"); m && m->platformDeclared) { + state.platformData = m->platform; + state.platformEarly = true; + } + } + return {}; +} + +void step4b_seed_platform(PrepareState& state) { + if (state.platform.selection.none) return; + auto const& ref = state.platform.selection.ref; + WorkItem item; + item.name = item_key(ref); + item.spec = ref.spec; + if (item.spec.shortName.empty()) item.spec.shortName = state.platformPackageShortName; + if (item.spec.namespace_.empty()) { + item.spec.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + item.spec.namespaceOmitted = true; + } + if (item.spec.candidates.empty()) + item.spec.candidates = {{ .namespace_ = item.spec.namespace_, .shortName = item.spec.shortName }}; + item.spec.declaredIn = "the platform"; + item.requestedBy = state.m->package.name + " (platform)"; + item.originalConstraint = item.spec.version; + item.consumerDepIndex = kMainConsumer; + state.worklist.push_front(std::move(item)); +} + +namespace { +thread_local std::optional g_platformRestart; +} // namespace + +std::optional take_platform_restart() { + auto r = std::move(g_platformRestart); + g_platformRestart.reset(); + return r; +} + +std::expected step4b_apply_platform(PrepareState& state) { + auto& P = state.platform; + if (P.selection.none) return false; + const auto t = target_of(state); + + // ── The package, once: find it, read it, merge what overrides it ────── + if (!P.active) { + const mcpp::modgraph::PackageRoot* found = nullptr; + const auto& ref = P.selection.ref; + for (std::size_t i = 1; i < state.packages.size(); ++i) { + auto const& pr = state.packages[i]; + auto n = mcpp::pm::compat::resolve_package_name(pr.manifest.package.name, + pr.manifest.package.namespace_); + const bool byPath = !ref.spec.path.empty() + && fs::weakly_canonical(pr.root) == fs::weakly_canonical(fs::path(ref.spec.path)); + if (byPath || n.shortName == state.platformPackageShortName) { found = ≺ break; } + } + if (!found) + return std::unexpected(std::format( + "the platform {} ({}) was not resolved into the graph", + mp::display(ref), mcpp::platform::origin_text(P.selection.origin))); + if (!found->manifest.platformDeclared) { + refusal::record(refusal::Code::PlatformNotAPlatform); + return std::unexpected(std::format( + "{} is selected as the platform for {} ({}), and its mcpp.toml has no " + "[platform] section.\n" + " A platform package states the configuration of its targets there " + "(SPEC-013 §2); select a platform package, or none (--platform none).", + qualified(found->manifest), t.str(), + mcpp::platform::origin_text(P.selection.origin))); + } + P.package = qualified(found->manifest); + P.version = found->manifest.package.version; + P.root = found->root; + auto hosted = mcpp::platform::serves(found->manifest.platform, P.package, t); + if (!hosted) { + refusal::record(refusal::Code::PlatformTargetUnserved); + return std::unexpected(std::format("{} ({})", hosted.error(), + mcpp::platform::origin_text(P.selection.origin))); + } + P.selection.hosted = *hosted; + state.platformData = *hosted ? mcpp::platform::hosted_view(found->manifest.platform) + : found->manifest.platform; + P.fields = state.platformData; + const auto ctx = platform_cfg_context(state); + std::vector config; + if (auto cfg = state.get_cfg(false); cfg) config = (*cfg)->platformOverrides; + for (auto const* o : mcpp::platform::matching(config, matcher(ctx))) + mcpp::platform::merge(P.fields, o->fields); + auto project = project_tables(*state.m); + for (auto const* o : mcpp::platform::matching(project, matcher(ctx))) + mcpp::platform::merge(P.fields, o->fields); + P.active = true; + state.platformInjected.insert(P.package); + // A base the platform states, which this pass decided without: the + // next pass reads the package before deciding it (§7). + if (P.fields.sysroot.set && !state.platformEarly && P.fields.sysroot.origin.tier == mp::Tier::Platform) { + g_platformRestart = P.root; + return std::unexpected(std::string("mcpp:platform-restart")); + } + } + + // ── The layers the graph lacks (§4.3) ───────────────────────────────── + bool pushed = false; + const auto provided = provided_layers(state); + std::map> byKey; // item key -> layers + std::map entryOf; + for (auto layer : mp::kLayers) { + auto it = P.fields.layers.find(std::string(layer)); + if (it == P.fields.layers.end()) continue; + auto const& e = it->second; + if (e.ref.off) { + P.layers[std::string(layer)] = { "none", e.origin.tier }; + continue; + } + if (auto pv = provided.find(std::string(layer)); pv != provided.end()) { + // Whose: the platform's when the provider is a package the + // platform added, the graph's otherwise. + const auto providerName = pv->second.substr(0, pv->second.find('@')); + bool ours = false; + for (auto const& q : state.platformInjected) + if (q == providerName || q.ends_with("." + providerName) + || providerName.ends_with("." + q.substr(q.find('.') + 1))) + ours = true; + P.layers[std::string(layer)] = { pv->second, ours ? e.origin.tier : mp::Tier::Graph }; + continue; + } + const auto key = item_key(e.ref); + if (key.empty()) + return std::unexpected(std::format( + "{}: the package for {} names no package; write {{ name = \"\", ... }}", + mcpp::platform::origin_text(e.origin), layer)); + if (state.platformInjected.contains("layer:" + key)) continue; // already walked + byKey[key].push_back(std::string(layer)); + entryOf[key] = &e; + } + for (auto const& [key, layers] : byKey) { + auto const& e = *entryOf.at(key); + WorkItem item; + item.name = key; + item.spec = e.ref.spec; + if (item.spec.shortName.empty()) item.spec.shortName = known_short_name(e.ref); + if (item.spec.namespace_.empty()) { + item.spec.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + item.spec.namespaceOmitted = true; + } + if (item.spec.candidates.empty()) + item.spec.candidates = {{ .namespace_ = item.spec.namespace_, + .shortName = item.spec.shortName }}; + std::string names; + for (auto const& l : layers) { if (!names.empty()) names += ", "; names += l; } + item.spec.declaredIn = std::format("the platform's {}", names); + item.requestedBy = std::format("{} (platform: {})", state.m->package.name, names); + item.originalConstraint = item.spec.version; + item.consumerDepIndex = kMainConsumer; + // The root declares it, as it would by hand: every reader of the + // root's edges (the link's closure among them) sees it. + state.m->dependencies[key] = item.spec; + state.platformLayerSpecs[key] = item.spec; + state.worklist.push_back(std::move(item)); + state.platformInjected.insert("layer:" + key); + state.platformInjected.insert(injected_name(state.worklist.back().spec)); + pushed = true; + } + + // ── The tools the graph does not build (§4.4) ───────────────────────── + std::set requested; // tools the root already asks the graph for + for (auto const& [k, spec] : state.m->buildDependencies) + for (auto const& tool : spec.tools) requested.insert(tool); + std::map> toolsByKey; + std::map toolEntryOf; + for (auto const& [name, e] : P.fields.tools) { + if (!e.ref.program.empty()) continue; // found, not built + if (requested.contains(name)) continue; // the graph builds it already + const auto key = item_key(e.ref); + if (key.empty()) + return std::unexpected(std::format( + "{}: the package for the tool {} names no package; write {{ name = \"\", ... }}", + mcpp::platform::origin_text(e.origin), name)); + if (state.platformInjected.contains("tool:" + name)) continue; + toolsByKey[key].push_back(name); + toolEntryOf[key] = &e; + } + for (auto const& [key, tools] : toolsByKey) { + auto const& e = *toolEntryOf.at(key); + WorkItem item; + item.name = key; + item.spec = e.ref.spec; + if (item.spec.shortName.empty()) item.spec.shortName = known_short_name(e.ref); + if (item.spec.namespace_.empty()) { + item.spec.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + item.spec.namespaceOmitted = true; + } + if (item.spec.candidates.empty()) + item.spec.candidates = {{ .namespace_ = item.spec.namespace_, + .shortName = item.spec.shortName }}; + item.spec.tools = tools; + std::string names; + for (auto const& n : tools) { if (!names.empty()) names += ", "; names += n; } + item.spec.declaredIn = std::format("the platform's tools ({})", names); + item.requestedBy = std::format("{} (platform tools: {})", state.m->package.name, names); + item.originalConstraint = item.spec.version; + item.consumerDepIndex = kMainConsumer; + item.buildOnly = true; + item.fromBuildTable = true; + // The root asks for them as it would for a declared build dependency, + // so the tool provisioning pass builds them for this machine. + state.m->buildDependencies[key] = item.spec; + for (auto const& n : tools) state.platformInjected.insert("tool:" + n); + state.platformInjected.insert(injected_name(item.spec)); + state.worklist.push_back(std::move(item)); + pushed = true; + } + return pushed; +} + +std::expected step4b_check_platform_versions(PrepareState& state) { + auto const& P = state.platform; + if (!P.active || P.fields.versions.empty()) return {}; + // Asked of the edges as they stand: the check runs before each round's + // drain, before `buildTimeOnly` is decided. + const auto instances = package_instances(state); + for (std::size_t i = 1; i < state.packages.size(); ++i) { + // A package built for this machine only (a tool's) is not linked with + // the target's, and a group's exact pins are about one link. + if (!instances.target[i] && instances.host[i]) continue; + auto const& pm = state.packages[i].manifest; + auto n = mcpp::pm::compat::resolve_package_name(pm.package.name, pm.package.namespace_); + auto it = P.fields.versions.find(n.shortName); + if (it == P.fields.versions.end()) continue; + if (pm.package.version.empty() || pm.package.version == it->second) continue; + // A package taken from a directory or a repository is a development + // tree, which the version group does not bind. + if (auto r = state.resolved.find({n.namespace_, n.shortName}); + r != state.resolved.end() && r->second.source != "version") + continue; + std::string by; + if (auto r = state.resolved.find({n.namespace_, n.shortName}); r != state.resolved.end()) + by = r->second.requestedBy; + refusal::record(refusal::Code::PlatformVersionGroup); + return std::unexpected(std::format( + "{} {} is in this build, and the platform {} is a version group that names {} {}.\n" + " requested by: {}\n" + " The family's packages pin one another exactly, so two versions of one of\n" + " them cannot be linked into one program. Either select the platform release\n" + " of that version ([target.] platform = \"{}@\"), or write\n" + " {} = \"{}\" where it is requested.", + n.shortName, pm.package.version, P.display(), n.shortName, it->second, + by.empty() ? std::string("the graph") : by, + P.package.substr(P.package.find('.') + 1), n.shortName, it->second)); + } + return {}; +} + +// A WORKSPACE'S MEMBERS STAND ON THE PLATFORM'S LAYERS AS A ROOT DOES (§4.3). +// The packages the platform added are the virtual root's dependencies, and a +// member links its own closure; so each member of the plan is given an edge to +// each layer provider the platform added, as if it had declared it. (Measured: +// a build programs' graph under a host platform linked its programs without +// the C library and the C++ run-time, and `okld check` refused `free`.) +void step4b_platform_member_edges(PrepareState& state) { + if (!state.platform.active || !state.workspacePlan() || state.platformLayerSpecs.empty()) return; + // The packages of the plan by short name, so a member's edge to a layer + // provider is a lookup. + std::map, std::less<>> byName; + for (std::size_t l = 1; l < state.packages.size(); ++l) { + auto const& pm = state.packages[l].manifest; + byName[mcpp::pm::compat::resolve_package_name(pm.package.name, pm.package.namespace_) + .shortName].push_back(l); + } + auto give = [&](std::size_t member, const std::string& key, const mcpp::manifest::DependencySpec& spec) { + auto& deps = state.packages[member].manifest.dependencies; + if (deps.contains(key)) return; + deps[key] = spec; + auto it = byName.find(spec.shortName); + if (it == byName.end()) return; + for (auto const l : it->second) { + DependencyEdge e; + e.consumerPackageIndex = member; + e.dependencyPackageIndex = l; + e.requestedFeatures = spec.features; + e.defaultFeatures = spec.defaultFeatures; + state.dependencyEdges.push_back(std::move(e)); + } + }; + for (std::size_t m = 1; m < state.packages.size(); ++m) { + if (!state.packages[m].selectedMember) continue; + for (auto const& [key, spec] : state.platformLayerSpecs) give(m, key, spec); + } +} + +void step5_platform_toolchain(PrepareState& state) { + auto const& P = state.platform; + if (!P.active || !P.fields.toolchain.set) return; + // The platform's toolchain stands where the target row's pin stands: it + // replaces a default, never a toolchain the project or this machine named + // for the target (§4.5), and the graph supplying the system does not + // cancel it -- the platform is what supplies it. + if (tc_origin_is_user_explicit(state.tcOrigin)) return; + state.targetPinCandidate = P.fields.toolchain.value; + state.targetPinIsCapability = true; + state.platformToolchainPin = true; +} + +} // namespace mcpp::build + +namespace mcpp::build { + +namespace { + +// A program a platform names, as a runner's program is found: the programs +// the graph built for this machine, then the declared payloads, then PATH. +std::optional locate_program(const PrepareState& state, const BuildContext& ctx, + const std::string& name) { + if (auto it = state.rootTools.find(name); it != state.rootTools.end()) return it->second; + for (auto const& dir : ctx.xlingsDepBinDirs) + for (auto const& n : { name, name + ".exe" }) { + std::error_code ec; + if (fs::is_regular_file(dir / n, ec)) return dir / n; + } + return mcpp::invocation::which(name); +} + +std::vector split_words(std::string_view v) { + std::vector out; + std::string cur; + for (char c : v) { + if (c == ' ' || c == '\t') { if (!cur.empty()) out.push_back(std::exchange(cur, {})); } + else cur += c; + } + if (!cur.empty()) out.push_back(cur); + return out; +} + +} // namespace + +void step13_platform_statement(PrepareState& state) { + auto& P = state.platform; + const auto t = target_of(state); + const auto cfgCtx = platform_cfg_context(state); + + // ── The statement every reader uses: platform → build programs → this + // machine → the project → the environment (§4.5) ────────────────────── + mp::Statement s = P.active ? state.platformData : mp::Statement{}; + mp::Statement program = state.programPlatform; + // The root's own program states them as well (applied into its manifest); + // it is the last program to run, and it speaks for the project. + mcpp::platform::merge(program, state.m->buildConfig.programPlatform); + // A build program's runner and named runners (`mcpp::runner`), the BSP + // spelling of the same fields. + if (!state.m->buildConfig.runner.empty()) { + program.runner.cmd = state.m->buildConfig.runner; + program.runner.set = true; + program.runner.origin = mp::Origin{ mp::Tier::Program, {}, 0, + state.runnerProvider.empty() ? state.m->package.name : state.runnerProvider }; + if (state.m->buildConfig.runExclusive) { + program.runner.exclusive = true; + program.runner.exclusiveSet = true; + } + } + for (auto const& [name, nr] : state.m->buildConfig.namedRunners) { + auto it = state.namedRunnerProvider.find(name); + program.runners[name] = mp::Words{ nr.argv, mp::Origin{ mp::Tier::Program, {}, 0, + it == state.namedRunnerProvider.end() ? state.m->package.name : it->second }, true }; + } + mcpp::platform::merge(s, program); + std::vector config; + if (auto cfg = state.get_cfg(false); cfg) config = (*cfg)->platformOverrides; + for (auto const* o : mcpp::platform::matching(config, matcher(cfgCtx))) mcpp::platform::merge(s, o->fields); + auto project = project_tables(*state.m); + for (auto const* o : mcpp::platform::matching(project, matcher(cfgCtx))) mcpp::platform::merge(s, o->fields); + const auto envKey = target_env_name(t, "RUNNER"); + if (auto e = mcpp::invocation::get(envKey); e && !e->empty()) { + if (*e == "none") { + s.runner = mp::Runner{}; + } else { + s.runner.cmd = split_words(*e); + s.runner.origin = mp::Origin{ mp::Tier::Env, {}, 0, envKey }; + s.runner.set = !s.runner.cmd.empty(); + } + } + P.fields = std::move(s); + P.flags = mcpp::platform::evaluate_flags(P.fields, matcher(cfgCtx)); +} + +std::expected step13_finish_platform(PrepareState& state, BuildContext& ctx) { + auto P = state.platform; + const auto t = target_of(state); + + // ── The programs the link and the seal run (§4.4, §4.6) ─────────────── + if (P.active) { + std::set needed; + for (auto const& in : P.fields.link.inputs) + if (!in.from.empty()) needed.insert(in.from.front()); + for (auto const& step : P.fields.link.seal) + if (!step.empty()) needed.insert(step.front()); + for (auto const& [name, e] : P.fields.tools) + if (auto found = locate_program(state, ctx, e.ref.program.empty() ? name : e.ref.program)) + P.tools[name] = found->generic_string(); + const bool linksSomething = std::ranges::any_of(ctx.plan.linkUnits, [](auto const& lu) { + return lu.kind != mcpp::build::LinkUnit::StaticLibrary; + }); + for (auto const& name : needed) { + if (P.tools.contains(name)) continue; + if (auto found = locate_program(state, ctx, name)) { + P.tools[name] = found->generic_string(); + continue; + } + if (!linksSomething || state.overrides.plan_only) continue; + refusal::record(refusal::Code::PlatformToolAbsent); + auto it = P.fields.tools.find(name); + const std::string from = it == P.fields.tools.end() + ? std::string("the platform names no package that builds it") + : std::format("{} builds it ({})", mp::display(it->second.ref), + mcpp::platform::origin_text(it->second.origin)); + return std::unexpected(std::format( + "the platform {} links {} with `{}`, and it is neither built by the graph nor found " + "on this machine; {}.\n" + " Name the package that builds it for this machine:\n" + "\n" + " [target.'cfg(platform = \"{}\")'.tools]\n" + " {} = \"@\" # or {{ path = \"\" }}, {{ program = \"\" }}", + P.display(), t.str(), name, from, mp::platform_name(state.platformPackageShortName), + name)); + } + } + // A platform that links with ld.lld needs one beside the compiler; its + // absence is said here, not as a link line that starts with a flag. + if (P.active && P.fields.link.driver.value == "lld" && !state.overrides.plan_only + && std::ranges::any_of(ctx.plan.linkUnits, [](auto const& lu) { + return lu.kind != mcpp::build::LinkUnit::StaticLibrary; }) + && mcpp::freestanding::resolve_lld_any(state.tc->binaryPath).empty()) { + refusal::record(refusal::Code::LldRequiredAbsent); + return std::unexpected(std::format( + "the platform {} links {} with ld.lld itself ({}), and the toolchain has no ld.lld " + "beside {}.\n" + " Use a toolchain whose payload carries it -- the platform's own is {} -- with " + "`[target.] toolchain`, or remove the line that names this one.", + P.display(), t.str(), mcpp::platform::origin_text(P.fields.link.driver.origin), + state.tc->binaryPath.generic_string(), + P.fields.toolchain.set ? P.fields.toolchain.value : std::string("an llvm toolchain"))); + } + // THE PLATFORM, REPORTED (SPEC-013 §9): which one and why, then what it + // contributes beyond the layers the Target lines already showed. + if (P.active) { + mcpp::ui::info("Platform", std::format("{} ({}{})", P.display(), + mcpp::platform::origin_text(P.selection.origin), + P.selection.hosted ? ", hosted" : "")); + for (auto const& [label, value] : mcpp::platform::report_rows(P)) + if (!mp::is_layer(label)) + mcpp::ui::info("", std::format("{:<17} {}", label, value)); + } + ctx.plan.platform = std::move(P); + // THE HOST GRAPH'S, when the build programs were built under it (§6.1). + ctx.hostPlatform = state.hostPlatform; + if (ctx.hostPlatform.active) + mcpp::ui::info("Host", std::format("platform {} ({}): build programs are {} images run by {}", + ctx.hostPlatform.display(), mcpp::platform::origin_text(ctx.hostPlatform.selection.origin), + state.hostPlatformTarget, + state.hostPlatformRunner.empty() ? std::string("its runner") + : std::filesystem::path(state.hostPlatformRunner.front()).filename().string())); + if (state.overrides.configuration_only) { + if (ctx.hostPlatform.active) { + ctx.hostTriple = state.hostPlatformTarget; + ctx.hostCompiler = ctx.hostPlatform.fields.toolchain.value; + ctx.hostCompilerOrigin = "platform"; + } else if (auto h = state.host_tc_for_build_program()) { + ctx.hostTriple = h->second.targetTriple; + ctx.hostCompiler = h->second.binaryPath.generic_string(); + ctx.hostCompilerOrigin = "host"; + } else { + return std::unexpected(h.error()); + } + } + return {}; +} + +} // namespace mcpp::build + +namespace mcpp::build { + +// THE HOST GRAPH'S PLATFORM, LOADED (SPEC-013 §6): the package, and the +// target among its own that has this machine's architecture -- what the build +// programs are compiled for and run as. Read when the build programs are +// built; the package is fetched as any dependency is. +std::expected load_host_platform(PrepareState& state) { + auto& H = state.hostPlatform; + auto const& sel = state.hostPlatformSel; + if (sel.none || H.active) return {}; + const auto& ref = sel.ref; + fs::path root; + std::optional m; + if (!ref.spec.path.empty()) { + root = ref.spec.path; + auto r = mcpp::manifest::load(root / "mcpp.toml"); + if (!r) return std::unexpected(std::format("{}: the host platform {}: {}", + mcpp::platform::origin_text(sel.origin), mp::display(ref), r.error().format())); + m = std::move(*r); + } else if (!ref.spec.version.empty()) { + if (!state.loadVersionDep) + return std::unexpected("the host platform is read once the graph's loader exists"); + auto r = state.loadVersionDep(ref.name, ref.spec.namespace_, ref.spec.shortName, ref.spec.version); + if (!r) return std::unexpected(std::format("{}: the host platform {}: {}", + mcpp::platform::origin_text(sel.origin), mp::display(ref), r.error())); + root = r->first; + m = std::move(r->second); + } else { + return std::unexpected(std::format( + "{}: the host platform {} is named by a version or a directory", + mcpp::platform::origin_text(sel.origin), mp::display(ref))); + } + if (!m->platformDeclared) { + refusal::record(refusal::Code::PlatformNotAPlatform); + return std::unexpected(std::format( + "{} is selected as the host graph's platform ({}), and its mcpp.toml has no " + "[platform] section", qualified(*m), mcpp::platform::origin_text(sel.origin))); + } + const std::string arch(mcpp::toolchain::platform::host_arch); + for (auto const& w : m->platform.targets.value) { + auto t = mcpp::toolchain::triple::parse(w); + if (t && t->arch == arch) { state.hostPlatformTarget = t->str(); break; } + } + if (state.hostPlatformTarget.empty()) { + refusal::record(refusal::Code::PlatformTargetUnserved); + return std::unexpected(std::format( + "the host graph's platform {} ({}) serves no {} target, so this machine's build " + "programs cannot be built for it; its targets are {}", + qualified(*m), mcpp::platform::origin_text(sel.origin), arch, + [&] { std::string l; for (auto const& w : m->platform.targets.value) + l += (l.empty() ? "" : ", ") + w; return l; }())); + } + H.active = true; + H.selection = sel; + H.package = qualified(*m); + H.version = m->package.version; + H.root = root; + H.fields = m->platform; + return {}; +} + +// The spelling a sub-build is told the host platform by (`platform_choice`). +std::string host_platform_choice(const PrepareState& state) { + auto const& r = state.hostPlatformSel.ref; + if (!r.spec.path.empty()) return "path:" + r.spec.path; + return std::format("{}.{}@{}", r.spec.namespace_.empty() ? std::string(mcpp::pm::kDefaultNamespace) + : r.spec.namespace_, + r.spec.shortName, r.spec.version); +} + +} // namespace mcpp::build diff --git a/src/build/prepare/records.cpp b/src/build/prepare/records.cpp index 7830d89e5..7c0e24c9b 100644 --- a/src/build/prepare/records.cpp +++ b/src/build/prepare/records.cpp @@ -207,6 +207,38 @@ std::expected step13_lockfile(PrepareState& state, BuildConte lock.packages.push_back(std::move(lp)); } + // WHAT THE PROJECT'S OWN GRAPH REACHES (SPEC-013 §15.1): from the + // root and the selected members along the edges, never entering a + // package the platform added. A package the platform added, and one + // only those reach (its layers' implementations, its tools' and its + // plugin's libraries), is the platform's. + auto qualified_of = [](const mcpp::manifest::Manifest& m) { + auto n = mcpp::pm::compat::resolve_package_name(m.package.name, m.package.namespace_); + return mcpp::pm::compat::qualified_name( + n.namespace_.empty() ? std::string(mcpp::pm::kDefaultNamespace) : n.namespace_, n.shortName); + }; + std::set projectReaches; + { + const auto n = state.packages.size(); + std::vector own(n, false); + std::vector stack; + for (std::size_t i = 0; i < n; ++i) + if (i == 0 || state.packages[i].selectedMember) { own[i] = true; stack.push_back(i); } + while (!stack.empty()) { + const auto p = stack.back(); + stack.pop_back(); + for (auto const& e : state.dependencyEdges) { + const auto d = e.dependencyPackageIndex; + if (e.consumerPackageIndex != p || d >= n || own[d]) continue; + if (state.platformInjected.contains(qualified_of(state.packages[d].manifest))) continue; + own[d] = true; + stack.push_back(d); + } + } + for (std::size_t i = 0; i < n; ++i) + if (own[i]) projectReaches.insert(qualified_of(state.packages[i].manifest)); + } + // Version deps: the whole resolved graph, at the versions actually // chosen. `resolved` is an ordered map, so the file is deterministic. std::set versionLocked; @@ -237,6 +269,12 @@ std::expected step13_lockfile(PrepareState& state, BuildConte // prefix claimed otherwise. `index_package_digest` is FNV-1a on // every host. lp.hash = mcpp::pm::index_package_digest(sourceIndex, lp.name, lp.version); + // The platform's packages say so (SPEC-013 §15.1). + const auto qualified = mcpp::pm::compat::qualified_name( + key.ns.empty() ? std::string(mcpp::pm::kDefaultNamespace) : key.ns, key.shortName); + if (state.platform.active + && (state.platformInjected.contains(qualified) || !projectReaches.contains(qualified))) + lp.origin = "platform"; // One entry per identity: a dependency declared by the root and by // a member under different spellings is one package. if (!versionLocked.insert(lock_identity(lp)).second) continue; @@ -256,6 +294,63 @@ std::expected step13_lockfile(PrepareState& state, BuildConte for (auto const& mp : state.overrides.workspace_request) planned.insert(mp); partialWorkspace = !std::ranges::includes(planned, all); } + // EACH ENTRY NAMES THE CONFIGURATIONS THAT RESOLVE IT (SPEC-013 + // §15.1): this one's target is added to what it resolved, and taken + // from what it did not; an entry no configuration resolves any more is + // dropped. A build for this machine and a pack for an ok image then + // leave each other's entries as they were. An entry of this machine's + // own configuration alone says nothing (`targets` absent), so the lock + // of a project built only natively reads as it always has. (A partial + // workspace plan keeps the entries it did not resolve whole, below.) + { + // The native configuration is the one a build without --target + // resolves to: the project's `[build] target`, this machine's + // default target, or this machine's triple. + const auto canonical = [](const std::string& text) -> std::optional { + auto t = mcpp::toolchain::triple::parse(text); + if (text.empty() || !t) return std::nullopt; + return t->str(); + }; + const auto native = [&] { + if (auto t = canonical(state.m->buildConfig.target)) return *t; + if (auto cfg = state.get_cfg(false); cfg) + if (auto t = canonical((*cfg)->defaultTarget)) return *t; + return mcpp::toolchain::triple::host_triple().str(); + }(); + const auto here = canonical(state.overrides.target_triple).value_or(native); + auto targets_of = [&](const mcpp::lockfile::LockedPackage& p) { + return p.targets.empty() ? std::vector{ native } : p.targets; + }; + std::map was; + if (auto prior = mcpp::lockfile::load(state.workRoot / "mcpp.lock")) + for (auto const& p : prior->packages) was.emplace(lock_identity(p), p); + std::set listed; + for (auto& p : lock.packages) { + const auto id = lock_identity(p); + listed.insert(id); + auto it = was.find(id); + if (it == was.end() || it->second.version != p.version) { p.targets = { here }; continue; } + p.targets = targets_of(it->second); + // The platform's only where every configuration that resolves + // it says so: one whose project reaches it (`os` on Linux + // reaches openkal) keeps it the project's. + const bool others = std::ranges::any_of(p.targets, [&](auto const& t) { return t != here; }); + if (others && it->second.origin != "platform") p.origin.clear(); + if (std::ranges::find(p.targets, here) == p.targets.end()) p.targets.push_back(here); + } + for (auto& [id, p] : was) { + if (partialWorkspace || listed.contains(id)) continue; + auto kept = p; + kept.targets = targets_of(p); + std::erase(kept.targets, here); + if (!kept.targets.empty()) lock.packages.push_back(std::move(kept)); + } + for (auto& p : lock.packages) { + std::ranges::sort(p.targets); + if (p.targets == std::vector{ native }) p.targets.clear(); + } + + } if (partialWorkspace) { if (auto prior = mcpp::lockfile::load(state.workRoot / "mcpp.lock")) { // By identity, not by spelling: a prior entry that names a @@ -274,6 +369,8 @@ std::expected step13_lockfile(PrepareState& state, BuildConte if (!indicesHere.contains(i.name)) lock.indices.push_back(i); } } + // One order whichever configuration wrote the file last: by identity. + std::ranges::stable_sort(lock.packages, {}, [](auto const& p) { return lock_identity(p); }); if (!lock.packages.empty() || !lock.indices.empty()) { auto lockPath = state.workRoot / "mcpp.lock"; // `--locked` ASSERTS THAT THIS RESOLUTION IS THE RECORDED ONE. diff --git a/src/build/prepare/retarget.cpp b/src/build/prepare/retarget.cpp new file mode 100644 index 000000000..e1ea16711 --- /dev/null +++ b/src/build/prepare/retarget.cpp @@ -0,0 +1,411 @@ +// retarget.cpp -- telling a retargetable driver what it targets (phase 2 of +// prepare_build): the target triple, the sysroot and the multilib directory a +// clang driver is given when the toolchain resolved for a cross target. Moved +// out of toolchain.cpp whole; declared in :state. + +module mcpp.build.prepare; +import :state; +import mcpp.build.prepare_inputs; +import std; +import mcpp.build.engine_package; +import mcpp.diag; +import mcpp.build.refusal; +import mcpp.build.version_floor; +import mcpplibs.os.platform; +import mcpp.toolchain.platform; +import mcpp.toolchain.apple_sdk; +import mcpp.effects; +import mcpp.build.command_line; +import mcpp.manifest; +import mcpp.source_kind; +import mcpp.toolchain.hostflags; // the compile-token producer the package std module reuses +import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; +import mcpp.toolchain.fingerprint; +import mcpp.toolchain.msvc; +import mcpp.toolchain.registry; +import mcpp.toolchain.linkmodel; +import mcpp.toolchain.lifecycle; +import mcpp.toolchain.stdmod; +import mcpp.freestanding.target; // the target sysroot layout (libdir) +import mcpp.freestanding.linkline; // the ISA profile, for the std module command +import mcpp.toolchain.post_install; +import mcpp.toolchain.abi; +import mcpp.toolchain.triple; +import mcpp.toolchain.base; +import mcpp.toolchain.base_probe; +import mcpp.home; +import mcpp.build.plan; +import mcpp.build.flags; // compute_flags — the per-role contracts (#418) +import mcpp.build.graph_shape; // #407: the graph says which mode wrote it +import mcpp.pack.abi_tag; // the tag a prebuilt dependency is checked against +import mcpp.pack.prebuilt; // …and the check itself +import mcpp.pack.stage_tree; // where `${mcpp.stage_dir}` points, and its manifest +import mcpp.build.build_program; +import mcpp.build.backend; // BuildOptions for the tool sub-build +import mcpp.build.ninja; // make_ninja_backend — driving that sub-build +import mcpp.config; +import mcpp.provider; +import mcpp.provider.select; +import mcpp.provider.runtime_selection; +import mcpp.runtime.binding; +import mcpp.toolchain.post_install; +import mcpp.fetcher; +import mcpp.fetcher.progress; +import mcpp.ui; +import mcpp.invocation; + +namespace mcpp::build { + +std::expected +step2_retarget_for_retargetable_driver(PrepareState& state) { + // ── A retargetable driver has to be TOLD what it is targeting ──────── + // + // `tc.targetTriple` comes from `-dumpmachine`, and for every cross target + // that worked before this it was right for a reason that does not + // generalise: those targets use a DISTINCT compiler binary + // (`x86_64-w64-mingw32-g++`, `aarch64-linux-musl-g++`), whose own + // -dumpmachine reports the cross triple. Clang is ONE binary that emits + // every target it was built with, so -dumpmachine always answers with the + // host — and nothing downstream ever learns otherwise. + // + // Measured before this line existed: + // + // $ mcpp build --target riscv64-none-elf + // Resolved llvm@22.1.8 → riscv64-none-elf → …/bin/clang++ + // Finished dev [unoptimized + debuginfo] in 0.47s + // $ ls target/ + // x86_64-linux-gnu/ ← an ELF for the host, reported as riscv64 + // + // That is E1: success reported, host artifact produced. The output + // directory, the fingerprint, the cache key and the flag layer all read + // `tc.targetTriple`, so correcting it here corrects all of them at once — + // which is the point of there being one field rather than five answers. + // + // THIS USED TO BE SCOPED TO FREESTANDING, WITH THIS REASON: + // + // The hosted cross targets already resolve a per-target binary, and + // overwriting their probed triple would replace a measured fact with + // an assumed one for no gain. + // + // That was true while every hosted cross was served by a payload. It + // stops being true when the TARGET SIDE comes from the dependency graph: + // the C library, the C++ runtime and the platform's own implementation are + // then packages built from source, and the compiler is an ordinary clang — + // whose `-dumpmachine` answers the host, exactly as the paragraph above + // describes for freestanding. + // + // Measured 2026-08-23, with an explicit `[target.aarch64-macos] + // toolchain = "llvm@…"`. The manifest's cfg evaluation used the REQUESTED + // target, so the C library's aarch64 headers were on the command line; the + // toolchain's own triple was still the host's, so code generation was + // x86_64. Two answers to one question, in one command: + // + // okm_float_assert.c: the C library and the compiler disagree about + // LDBL_DIG ('33 == 18') 33 = aarch64 binary128, 18 = x87 + // + // ⇒ The condition is now the property the first paragraph of this comment + // already names: a RETARGETABLE driver has to be told. gcc is not one — a + // gcc payload IS its target — so the mingw and musl-gcc crosses keep + // answering from `-dumpmachine`, which for them remains a measured fact. + if (!state.overrides.target_triple.empty()) { + if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); + want && (want->is_freestanding() + || state.tc->compiler == mcpp::toolchain::CompilerId::Clang)) + { + state.tc->targetTriple = want->str(); + + // AND THE GATE THAT ALREADY EXISTS FOR THIS, APPLIED WHERE THE + // ANSWER IS KNOWN. + // + // `discover_link_runtime_dirs` refuses to report these + // directories for a target that carries its own sysroot, and the + // refusal never fired: that function runs during DETECTION, + // before this line, when `targetTriple` is still the HOST's. The + // gate read a host triple and answered correctly about it. + // + // The artefact is what showed it. An Android link line carried + // + // -L /toolchains/llvm/prebuilt/linux-x86_64/lib/ + // x86_64-unknown-linux-gnu + // + // whose last component is this machine's triple, produced by + // `root / "lib" / targetTriple` -- so the string names the + // question that was asked. Those are the compiler's own host + // runtime directories; an Android artefact must resolve libc++, + // the crt objects and the loader from the NDK's sysroot, and the + // hermetic check reported exactly that failure with six host + // objects. + // + // Cleared rather than re-derived. Re-running the discovery with + // the final triple would also change what every OTHER clang cross + // target gets, and those are measured as they stand; the claim + // being made here is only the one the gate already states. + if (want->has_own_sysroot()) state.tc->linkRuntimeDirs.clear(); + + // And the flag that says it to the driver — for a HOSTED target + // only. Freestanding already emits its own `--target`, together + // with the ISA flags that must accompany it + // (freestanding/target.cppm); a second one here would be the same + // decision in two places. + if (!want->is_freestanding() + && state.tc->compiler == mcpp::toolchain::CompilerId::Clang) { + state.tc->crossTargetFlag = + "--target=" + want->llvm_triple( + min_platform_version(*state.m, *want, state.tc->binaryPath)); + + // AND THE SAME FLAG ON THE std MODULE'S OWN COMMANDS, FOR A + // PAYLOAD THAT SERVES MORE THAN ONE TARGET. + // + // The std module is built by its own command assembly + // (clang.cppm), not by the compile flags, so a decision made + // only here reaches every translation unit and not that. For + // most toolchains the omission cannot be seen: a payload + // whose compiler IS its target finds its own headers, and a + // package-provided module carries the target inside + // `stdModuleFlags`. + // + // ONE NDK SERVES BOTH ANDROID ARCHES, which is the property + // that makes this necessary and is stated in the row's own + // pin: `android-ndk@` names no arch, so `--target` is the + // only thing that says which. Without it the precompile + // resolved libc++'s `#include <__config>` against the + // building machine and stopped there. + // + // NOT `has_own_sysroot()`, though both rows that answer true + // to it are SDKs with their own sysroot. Emscripten's `em++` + // serves exactly one target and needs no flag -- the verified + // wasm loop is measured without it -- so widening the gate to + // the predicate would add a flag to a command that does not + // want one. The property here is "one payload, several + // targets", and Android is the only row that has it; a future + // row brings its own measurement. + if (want->is_android()) { + state.tc->stdModuleTargetFlags = " " + state.tc->crossTargetFlag; + // BIONIC'S ctype HEADER AND A MODULE'S EXPORT RULES. + // + // bionic declares `isalnum` and its neighbours + // `static inline`, and libc++'s module surface exports + // them with `using std::isalnum`. A using-declaration + // cannot export a name with internal linkage, so the + // precompile fails on 14 names at once. Defining the + // macro empty makes those declarations extern, which is + // what every other C library this engine compiles + // against already does. + // + // Scoped to the std module and not to every unit: the + // rule being satisfied is about exporting from a module, + // and a translation unit that includes + // directly is entitled to bionic's inline definitions. + // `xim:android-ndk`'s own install-time self-test reaches + // the identical conclusion from the other direction. + // + // AND THE PAYLOAD MAY SAY SO ITSELF. The recipe applies + // this same define in that self-test, so it is a fact + // the payload already holds; `std_module_defines` in + // `.mcpp-toolchain.json` is the channel for it, and the + // define below is what a payload that ships no + // descriptor still gets. The two are not added + // together: a descriptor that names defines is the + // payload's complete answer for this channel, and + // appending to it would mean a payload could not + // withdraw a define this engine once needed. + auto stdDefines = [&]() -> std::vector { + auto desc = + mcpp::toolchain::payload_descriptor_for_compiler( + state.tc->binaryPath); + if (desc && *desc && !(*desc)->stdModuleDefines.empty()) + return (*desc)->stdModuleDefines; + return { "__BIONIC_CTYPE_INLINE=" }; + }(); + for (auto const& def : stdDefines) + state.tc->stdModuleTargetFlags += " -D" + def; + } + + // ── iOS: THE COMPILER IS OURS, THE SDK IS THE MACHINE'S ── + // + // The three iOS rows pin `llvm@23.1.3` -- any sufficiently + // new clang emits arm64 Mach-O for an iOS deployment target + // -- and take their headers and stub libraries from the + // machine's Xcode, which is where the whole item shrinks to + // a located sysroot. `aarch64-macos` is verified on exactly + // this split and is the precedent. + // + // LOCATED HERE, ONCE. Three later sites need the answer (the + // compile flags, the link line, and the std module's own + // command), and a function that probes the machine is the + // wrong thing to call three times: `xcrun` shells out, and + // three answers can differ if the developer directory + // changes mid-build. + // + // AND ITS ABSENCE IS A REFUSAL THAT NAMES THE SDK. The + // recorded host-surface rule is that a host dependency must + // be minimal, named, and never a fallthrough; the iOS SDK + // and `simctl` are the two this platform adds, both in the + // "proprietary runtime that exists only on its own OS" + // category. A build that continued without the SDK would + // fail in the driver's header search, naming a file rather + // than the thing that is missing. + if (want->is_ios()) { + // READ, NOT RE-DERIVED. The refusal above located it + // before any payload was resolved, and that is the one + // `xcrun` call this build makes. + // + // An empty answer here cannot happen through the + // `--target` path, and a line that prints when it does + // is cheaper than a branch that pretends it cannot: the + // row could be reached one day by a route that skipped + // the gate, and an iOS build with no `-isysroot` is a + // macOS artefact with an iOS triple on it. + if (!state.appleSdkLocated) { + return std::unexpected(std::format( + "internal: target {} reached toolchain " + "resolution without its SDK being located; the " + "gate that locates it did not run for this " + "request", want->str())); + } + state.tc->base.apple.root = *state.appleSdkLocated; + if (state.appleSdkFound) { + state.tc->base.apple.name = state.appleSdkFound->name; + state.tc->base.apple.version = state.appleSdkFound->version; + } + auto sdk = state.appleSdkLocated; + // AND THE std MODULE'S OWN COMMAND, WHICH IS A SEPARATE + // CHANNEL. Same reason the Android rows set it: the + // module is precompiled by `clang.cppm`'s own assembly + // rather than by the compile flags, so a decision made + // only in the flag builder reaches every translation + // unit and not the module they all import. Without the + // SDK here the precompile resolves libc++'s + // `#include <__config>` against the macOS SDK and the + // module is built for the wrong platform. + // + // QUOTED, as every path this string carries is (see the + // package-provided producer, which uses `shq` for each + // `-isystem`). The string is spliced into a shell + // command, and an Xcode installed as `Xcode 16.app` is + // a path with a space in it. + state.tc->stdModuleTargetFlags = + " " + state.tc->crossTargetFlag + + " -isysroot " + mcpp::build::command_line::quote(sdk->string()); + } + } + } + if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); + want && want->is_freestanding()) + { + // `import std` is structurally hosted, and turning it off is the + // SAME fact as the line above, not a second policy: libc++'s + // std.cppm is one module over the whole library, including the + // parts that are threads, filesystem and iostreams. There is no + // subset of it to precompile. + // + // Left on, the failure is neither early nor legible — measured: + // + // error: std module precompile failed (rc=1): + // .../include/c++/v1/__config:13:10: fatal error: + // '__config_site' file not found + // + // which reads as a broken toolchain payload and says nothing about + // the target. The freestanding std subset a user actually wants is + // an ordinary package (`mcpplibs.std.freestanding`), so mcpp's job + // here is to stop pretending the hosted one exists and to say + // where the other one is. + state.tc->clear_std_modules(); + + // ── The target's C library, resolved like its compiler ───────── + // + // The row in kKnownTargets names it, exactly as it names the + // toolchain pin, and it is installed through the same channel a + // project's `[xlings] deps` use (see the materialization above). + // Resolved HERE because the config is already open; the flag + // builder only reads the result. + // + // Absent is not an error at this point: the install happens + // earlier in this function and may legitimately not have run yet + // on a first pass. What follows would then simply not add the + // paths, and the link fails naming the missing libc — which is the + // truthful message either way. + const std::string want_sysroot = + mcpp::toolchain::triple::effective_sysroot( + *want, base_statement_text(state, *want)); + // A C library named by path (design 2026-10-23 §3.1): the same + // `/{include,lib}/` layout, nothing to install. + if (want_sysroot.starts_with("path:")) { + const std::filesystem::path dir = want_sysroot.substr(5); + std::error_code ec2; + if (!std::filesystem::is_directory(dir, ec2)) { + refusal::record(refusal::Code::TargetBase); + return std::unexpected(std::format( + "[target.{}].sysroot = {{ path = \"{}\" }} does not exist", want->str(), + dir.generic_string())); + } + if (auto spec = mcpp::freestanding::resolve(*want)) { + state.tc->base.multilibRoot = dir; + state.tc->base.package = dir.filename().string(); + const auto inc = dir / "include" / std::string(spec->libdir); + const auto lib = dir / "lib" / std::string(spec->libdir); + if (std::filesystem::is_directory(inc, ec2)) state.tc->base.multilibInclude = inc; + if (std::filesystem::is_directory(lib, ec2)) state.tc->base.multilibLib = lib; + } + } else if (!want_sysroot.empty()) { + if (auto cfg3 = state.get_cfg(true); cfg3) { + auto ref = mcpp::provider::parse_ref(want_sysroot); + auto xl = mcpp::config::make_xlings_env(**cfg3); + // INSTALLED, NOT MERELY LOOKED UP — THE SAME CHANNEL + // THE ROW'S TOOLCHAIN PIN GOES THROUGH. + // + // The row names two things and only one of them used to + // be made to exist: `pin` went through + // `resolve_xpkg_path(…, PayloadIntent::Use, …)` while + // `sysroot` was a pure lookup that returned nullopt and + // let the whole block below be skipped without a word. + // + // Measured 2026-08-26 in a clean environment (an empty + // home, so mcpp's registry starts fresh): + // + // Target riscv64-none-elf + // c-abi picolibc-riscv (…, prebuilt) + // error: 'stdio.h' file not found + // + // The report named the C library and the build could not + // find its headers. mcpp's own bare-metal CI installs it + // by hand, which is why no test ever saw this — every + // bare-metal e2e runs on a machine where the gap has + // already been papered over. + // + // OFFLINE AND `MCPP_NO_AUTO_INSTALL` ARE THE FETCHER'S + // DECISION, not re-derived here. One question, one place + // that answers it — asking it twice is the shape this + // whole release exists to remove. + mcpp::fetcher::Fetcher srFetcher(**cfg3); + mcpp::fetcher::InstallProgressHandler srProgress; + std::optional dir; + if (auto p = srFetcher.resolve_xpkg_path( + want_sysroot, mcpp::fetcher::PayloadIntent::Use, &srProgress)) + dir = p->root; + else + dir = mcpp::provider::payload(*mcpp::provider::open(xl), ref); + if (dir) { + if (auto spec = mcpp::freestanding::resolve(*want)) { + const auto inc = + *dir / "include" / std::string(spec->libdir); + const auto lib = + *dir / "lib" / std::string(spec->libdir); + std::error_code ec2; + state.tc->base.multilibRoot = *dir; + state.tc->base.package = ref.name; + if (std::filesystem::is_directory(inc, ec2)) + state.tc->base.multilibInclude = inc; + if (std::filesystem::is_directory(lib, ec2)) + state.tc->base.multilibLib = lib; + } + } + } + } + } + } + return {}; +} + +} // namespace mcpp::build diff --git a/src/build/prepare/scan.cpp b/src/build/prepare/scan.cpp index 53e4081a9..c52e2d8db 100644 --- a/src/build/prepare/scan.cpp +++ b/src/build/prepare/scan.cpp @@ -398,11 +398,11 @@ step11_package_std_module_source(PrepareState& state) { // Both are read only from a package that ALSO provides the capability // below. A package that named a std module without supplying the // library would be describing something it does not have. - // AN OK IMAGE'S STANDARD LIBRARY IS THE GRAPH'S OR NONE. The payload's - // libc++ was configured for the payload's own operating system; inside an - // image it would be a second answer to a question the image's C++ - // run-time package answers. - if (auto ft = mcpp::toolchain::triple::parse(state.tc->targetTriple); ft && ft->is_openkal()) + // THE STANDARD LIBRARY OF A TARGET WHOSE LOADER SUPPLIES THE SYSTEM (an + // ok image) IS THE GRAPH'S OR NONE. The payload's libc++ was configured + // for the payload's own operating system; inside such a program it would + // be a second answer to a question its C++ run-time package answers. + if (auto ft = mcpp::toolchain::triple::parse(state.tc->targetTriple); ft && ft->loader_supplies_os()) state.tc->clear_std_modules(); for (auto& pkg : state.packages) { if (pkg.manifest.stdModule.empty()) continue; @@ -584,6 +584,24 @@ step11_package_std_module_source(PrepareState& state) { if (flags.find(q) == std::string::npos) flags += q; } } + // THE DEPLOYMENT FLOOR WHEN THE C LIBRARY IS THE GRAPH'S. Above, it + // comes with the prebuilt C library's tokens; a graph C library (the + // hosted arrangement: openkal-musl on macOS, SPEC-013 §5.4) has none, + // and the precompile then took the driver's default -- the running + // system's version -- while every unit was built for the floor + // (measured by mcpplibs.os: std.pcm for 15.0, the units for 14.0, and + // each import refused as a configuration mismatch). The value is the + // one every unit reads (flags.cppm), and a cross target carries it in + // its triple instead. + if (!state.tc->cAbiPrebuilt && state.tc->crossTargetFlag.empty()) { + auto tt = mcpp::toolchain::triple::parse(state.tc->targetTriple); + const auto floor = mcpp::toolchain::apple::deployment_target( + tt && tt->os == "macos", state.m->buildConfig.macosDeploymentTarget); + if (tt && tt->os == "macos" && !floor.empty()) { + const auto q = " -mmacosx-version-min=" + floor; + if (flags.find(q) == std::string::npos) flags += q; + } + } // The same words the package's own units receive from this list // (mcpp.manifest.flag_words), one quoted word each. for (auto& w : mcpp::manifest::flag_words(state.targetSideUsage.cxxflags)) @@ -719,19 +737,39 @@ step11_std_module_availability_gate(PrepareState& state, bool needsStdModule) { state.tc->targetTriple)); } if (auto ft = mcpp::toolchain::triple::parse(state.tc->targetTriple); - ft && ft->is_openkal()) + ft && ft->loader_supplies_os()) { + // Which package the platform names for the layer, and where that + // was said, or that no platform names one (SPEC-013 §4.3). + std::string why = "No platform names one for it"; + if (state.platform.active) { + auto it = state.platform.fields.layers.find("c++-abi"); + why = it == state.platform.fields.layers.end() + ? std::format("The platform {} names none", state.platform.display()) + : it->second.ref.off + ? std::format("The platform's c++-abi is switched off ({})", + mcpp::platform::origin_text(it->second.origin)) + : std::format("The platform {} names {} ({})", state.platform.display(), + mcpp::manifest::platform::display(it->second.ref), + mcpp::platform::origin_text(it->second.origin)); + } + // The remedy in the form that applies: the platform's layer when + // a platform is selected, a dependency of the target otherwise. + const std::string remedy = state.platform.active + ? std::format(" [target.'cfg(platform = \"{}\")'.layers]\n" + " \"c++-abi\" = \"@\"", + mcpp::manifest::platform::platform_name(state.platformPackageShortName)) + : std::format(" [target.{}.dependencies]\n" + " = \"\"", + state.tc->targetTriple); return std::unexpected(std::format( - "`import std;` on '{}' needs a C++ run-time inside the image, and " + "`import std;` on '{}' needs a C++ run-time inside the program, and " "the graph supplies none.\n" - " An ok image holds its own C library and C++ run-time, " - "above `kal_*`. Depend on\n" - " the packages that provide them for this target:\n" + " {}. Name the package that provides `mcpp:c++-abi` for this " + "target:\n" "\n" - " [target.'cfg(os = \"openkal\")'.dependencies]\n" - " openkal-musl = \"0.22.0\" # mcpp:c-abi=musl\n" - " openkal-llvm-runtime = \"0.16.0\" # mcpp:c++-abi=libc++, the std module", - state.tc->targetTriple)); + "{}", + state.tc->targetTriple, why, remedy)); } return std::unexpected(std::format( "source imports std but toolchain '{}' provides no std module source", @@ -1039,8 +1077,15 @@ std::expected phase11_scan(PrepareState& state) { if (auto r = step11_apple_sdk_cxx_runtime(state, *needsStdModule); !r) return std::unexpected(r.error()); - if (auto r = step11_std_module_availability_gate(state, *needsStdModule); !r) - return std::unexpected(r.error()); + if (auto r = step11_std_module_availability_gate(state, *needsStdModule); !r) { + if (!state.overrides.configuration_only) return std::unexpected(r.error()); + // The configuration is reported; the sources' refusal travels with it. + PlanNote note; // a warning: the document is complete without the sources + note.code = "MCPP_CONFIGURATION_SOURCES_REFUSED"; + note.message = r.error(); + state.planNotes.push_back(std::move(note)); + *needsStdModule = false; + } step11_compute_fingerprint(state); diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index b36b70f3a..3ac8c8b15 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -22,6 +22,7 @@ import mcpp.toolchain.platform; import mcpp.run; import mcpp.libs.json; import mcpp.manifest; +import mcpp.platform; import mcpp.source_kind; import mcpp.modgraph.glob; import mcpp.modgraph.graph; @@ -252,6 +253,9 @@ struct BuildProgramBinary { std::filesystem::path path; std::string identity; std::chrono::milliseconds compile{0}; + // The argv that runs it, `{}` the program, when the host graph has a + // platform (an ok image run by okrun, SPEC-013 §6); empty: run directly. + std::vector runner; }; // PrepareState carries prepare_build's working state across the phases it @@ -784,6 +788,45 @@ struct PrepareState { // (`tools = [...]`), by program name. A runner's program and an ok image's // `okld` are looked up here first. std::map rootTools; + + // ── PLATFORMS (SPEC-013; design 2026-10-28) ──────────────────────────── + // The target graph's platform: selected in P1, loaded, merged and applied + // to the graph in P4b, its toolchain read in P5, finished in P13 + // (prepare/platform.cpp). + mcpp::platform::Resolved platform; + // The platform package's own `[platform]` (or its hosted view), before + // any override. + mcpp::manifest::platform::Statement platformData; + // The package name `cfg(platform = ...)` is answered from, known in P1. + std::string platformPackageShortName; + // What the platform added to the graph: qualified package names, and + // `layer:` / `tool:` markers. A package listed here is locked + // with `origin = "platform"` and reported as the platform's. + std::set platformInjected; + // The layer providers the platform added, by the key the root declares + // them under: a workspace's members are given the same declarations. + std::map platformLayerSpecs; + // The platform's toolchain is the pin of this build (P5), for the line + // that announces the compiler. + bool platformToolchainPin = false; + // `platformData` was read in P1 (a directory, or a second pass), so the + // target base reads the platform's `sysroot` (base.cpp, tier four). + bool platformEarly = false; + // What build programs stated with the platform's directives (Tier::Program): + // a dependency's, the root's, and the platform's plugin. + mcpp::manifest::platform::Statement programPlatform; + // THE HOST GRAPH'S PLATFORM (SPEC-013 §6): selected in P1 of the + // outermost prepare only -- a nested one (tools, build programs) builds + // with the host conventions, which is the bootstrap boundary: a + // platform's own tools and runner are never built by the platform. + mcpp::platform::Selection hostPlatformSel; + // Loaded when the build programs are built: the package, its version and + // the target of this machine's architecture it serves. + mcpp::platform::Resolved hostPlatform; + std::string hostPlatformTarget; + // The runner the host graph's platform runs a build program with, its + // program located by the build programs' sub-build. + std::vector hostPlatformRunner; std::function foldDirectiveTailIntoPrivateBuild; @@ -1055,6 +1098,34 @@ std::expected step2_use_local_toolchain(PrepareState& state, const mcpp::toolchain::ToolchainSpec& spec); std::expected step2_apply_local_toolchain(PrepareState& state); std::expected step6_toolchain_phase(PrepareState& state); +// Which instances each package has, by index (features.cpp): a target instance +// where ordinary edges reach it from the root, a build-program instance where +// a `[build-dependencies]` edge reaches it. +struct PackageInstances { + std::vector target; + std::vector host; +}; +PackageInstances package_instances(const PrepareState& state); +// The platform's fields a package's build program stated (protocol 17), at the +// build-program tier of the build's platform statement, named after `who`. +void merge_program_platform(PrepareState& state, mcpp::manifest::platform::Statement stated, + std::string_view who); +// The platform through the phases (prepare/platform.cpp). +mcpp::build::cfgpred::Ctx platform_cfg_context(const PrepareState& state); +std::expected step1_select_platform(PrepareState& state); +void step4b_seed_platform(PrepareState& state); +std::expected step4b_apply_platform(PrepareState& state); +std::expected step4b_check_platform_versions(PrepareState& state); +void step4b_platform_member_edges(PrepareState& state); +void step5_platform_toolchain(PrepareState& state); +void step13_platform_statement(PrepareState& state); +std::expected step13_finish_platform(PrepareState& state, BuildContext& ctx); +// retarget.cpp: a retargetable driver told its target, sysroot and multilib. +std::expected step2_retarget_for_retargetable_driver(PrepareState& state); +// The platform root a first pass asks the second to read in P1, once. +std::optional take_platform_restart(); +std::expected load_host_platform(PrepareState& state); +std::string host_platform_choice(const PrepareState& state); // The statement and bootstrap spec of a toolchain phase that asked prepare to // start again, taken once. std::optional, std::string>> take_toolchain_restart(); diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index 380c4cc78..639b61a7b 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -260,7 +260,7 @@ step9_gather_target_side_candidates(PrepareState& state) { in.targetOs = tt->os; in.targetEnv = tt->env; in.freestandingTarget = tt->is_freestanding(); - in.openkalTarget = tt->is_openkal(); + in.loaderTarget = tt->loader_supplies_os(); // NOT `tt->envExplicit`. By this line the triple has been // canonicalised, and the canonical form of `x86_64-linux` is // `x86_64-linux-gnu` — re-parsing it reports a segment the @@ -463,9 +463,9 @@ step9_resolve_and_realise_cabi(PrepareState& state, TargetSideGather& gather) { auto realised = mcpp::toolchain::cenv::realise( *state.resolvedTargetSide.cAbiDecl, tt ? tt->os : std::string{}, tt ? tt->arch : std::string{}, - // An ok image is told no operating system, as a bare machine - // is: the C library's identity macros are the only ones. - tt && (tt->is_freestanding() || tt->is_openkal())); + // A program its loader runs is told no operating system, as a + // bare machine is: the C library's identity macros are the only ones. + tt && (tt->is_freestanding() || tt->loader_supplies_os())); if (!realised) { refusal::record(refusal::Code::CEnvUnrealisable); return std::unexpected(realised.error()); @@ -626,12 +626,12 @@ step9_resolve_and_realise_cabi(PrepareState& state, TargetSideGather& gather) { // availability check links and runs without the archive; refusing it // would trade a diagnosed hazard for a regression. The degradation // names the platform, the file and the package that supplies it. - // An ok image's runtime routines are the image's own by design - // (mcpp.okimage), so its absence there is not a gap to report. + // The runtime routines of a program its loader runs are its own by + // design, so their absence there is not a gap to report. if (auto okt = state.tc ? mcpp::toolchain::triple::parse(state.tc->targetTriple) : std::optional{}; state.resolvedTargetSide.compilerRuntime.absent() && state.tc - && !(okt && okt->is_openkal())) { + && !(okt && okt->loader_supplies_os())) { auto tt = mcpp::toolchain::triple::parse(state.tc->targetTriple); mcpp::diag::degraded("target/compiler-runtime", std::format( "the toolchain payload carries no compiler runtime for {} " @@ -1086,9 +1086,12 @@ step9_pin_and_linkage_diagnostics(PrepareState& state) { auto tt = mcpp::toolchain::triple::parse(state.resolvedTargetCanonical); return tt && tt->pin_is_capability(); }(); + // AND NOT WHEN THE PIN IS THE PLATFORM'S (SPEC-013 §4.5): the platform + // is what supplies the system here, and its toolchain is part of that + // statement, not a row's guess made before the graph was known. if (!state.pinReplacedDefault.empty() && state.resolvedTargetSide.system_from_graph() - && !pinIsCapability) { + && !pinIsCapability && !state.platformToolchainPin) { mcpp::diag::warning("toolchain", std::format( "this project's target side comes from its dependency graph, so " "{} would have served {}.\n" @@ -1202,9 +1205,9 @@ step9_same_os_check_and_report(PrepareState& state) { "same-OS check: '{}'(os={}) vs '{}'(os={})", reportedTargetName, want ? want->os : "", state.resolvedTargetSide.llvmTriple, got ? got->os : "")); - // An ok image is compiled for no operating system by design - // (`-unknown-none-elf`): the loader is its environment. - const bool okImage = want && got && want->is_openkal() && got->is_freestanding(); + // A program its loader runs is compiled for no operating system + // by design (`-unknown-none-elf`): the loader is its environment. + const bool okImage = want && got && want->loader_supplies_os() && got->is_freestanding(); if (want && got && !want->os.empty() && !got->os.empty() && want->os != got->os && !okImage) { refusal::record(refusal::Code::OsMismatch); @@ -1222,6 +1225,22 @@ step9_same_os_check_and_report(PrepareState& state) { reportedTargetName, reportedTargetName)); } } + // A layer whose package the platform added is the platform's in the + // report (SPEC-013 §9), whatever else the graph holds. + if (state.platform.active) { + auto injected = [&](std::string_view impl) { + const auto name = impl.substr(0, impl.find('@')); + for (auto const& q : state.platformInjected) { + const std::string_view qs = q; + const auto shortName = qs.substr(qs.find('.') == std::string_view::npos ? 0 : qs.find('.') + 1); + if (name == qs || name == shortName) return true; + } + return false; + }; + for (auto* l : { &state.resolvedTargetSide.compilerRuntime, &state.resolvedTargetSide.kernelAbi, + &state.resolvedTargetSide.cAbi, &state.resolvedTargetSide.cxx }) + if (l->fromGraph() && injected(l->impl)) l->byPlatform = true; + } mcpp::ui::info("Target", tsd::format_report( state.resolvedTargetSide, reportedTargetName, mcpp::log::is_verbose())); return {}; @@ -1724,6 +1743,7 @@ static std::expected step9_root_build_program(PrepareState& s // The program, as the build programs' graph compiled it (mcpp#797). if (auto program = build_program_binary(state, 0); program) { bpEnv.programBinary = program->path; + bpEnv.programRunner = program->runner; bpEnv.programIdentity = program->identity; bpEnv.programCompile = program->compile; } else if (!state.overrides.plan_only) { @@ -2121,6 +2141,7 @@ static std::expected step9_member_build_programs(PrepareState // The program, as the build programs' graph compiled it (mcpp#797). if (auto program = build_program_binary(state, i); program) { bpEnv.programBinary = program->path; + bpEnv.programRunner = program->runner; bpEnv.programIdentity = program->identity; bpEnv.programCompile = program->compile; } else { @@ -2201,6 +2222,10 @@ static std::expected step9_member_build_programs(PrepareState who = pkg.manifest.package.name; } if (bc.runExclusive && !exclusiveBefore) rootBc.runExclusive = true; + // The platform's fields the member's program stated, as a dependency's + // program's are (SPEC-013 §4.1). + merge_program_platform(state, std::exchange(bc.programPlatform, {}), + pkg.manifest.package.name); } if (auto err = state.checkVersionFloors(); err) return std::unexpected(*err); // What the programs published (an include directory, an interface diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 13a46e644..1ed8c41e7 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -257,6 +257,10 @@ static std::expected step1_define_early_toolchain_closures(Pr // with the declared profile. if (pr.dependencyLinkageDeclared) state.m->buildConfig.dependencyLinkage = pr.dependencyLinkage; + // `mcpp pack --linkage`: the package's program property, stated for + // this build over the project's (SPEC-014 §1). + if (!state.overrides.dependency_linkage.empty()) + state.m->buildConfig.dependencyLinkage = state.overrides.dependency_linkage; state.m->buildConfig.optLevel = pr.optLevel; state.m->buildConfig.debug = pr.debug; state.m->buildConfig.lto = pr.lto; @@ -422,6 +426,17 @@ static std::expected step1_define_early_toolchain_closures(Pr // every later reader parses: a managed spec, or `path:`. if (auto r = step1_local_toolchain(state); !r) return std::unexpected(r.error()); + // `[target.]`'s build-shaping keys, applied the same way whichever + // path found the row (#704). The section's toolchain is a statement about + // this row the author wrote down, so it replaces `[toolchain]` and the + // global default; `--toolchain` and a consumer's decision for a host tool + // are statements about THIS invocation and keep precedence over it. + return {}; +} + +// The closures that answer "is MSVC usable here" and "what would a native +// build pick", and the Windows first run that seeds the GNU target from them. +static void step1_define_host_default_closures(PrepareState& state) { // ─── Windows first run without Visual Studio ──────────────────────── // The host triple on Windows is MSVC-ABI, so the historical default // (llvm) resolves to clang targeting MSVC — which uses the MSVC STL and @@ -478,13 +493,6 @@ static std::expected step1_define_early_toolchain_closures(Pr } } } - - // `[target.]`'s build-shaping keys, applied the same way whichever - // path found the row (#704). The section's toolchain is a statement about - // this row the author wrote down, so it replaces `[toolchain]` and the - // global default; `--toolchain` and a consumer's decision for a host tool - // are statements about THIS invocation and keep precedence over it. - return {}; } // A phase-local struct passed by reference to the steps that resolve one @@ -1064,10 +1072,11 @@ step1_target_row_pin_and_capability_check(PrepareState& state, TargetOverrideCtx ? "A freestanding target has no per-host cross payload: " "clang and lld are\n" " cross-compilers by construction and gcc is not." - : ctx.parsed->is_openkal() - ? "An ok image is emitted by clang and lld for every host: " - "the compiler is told\n" - " no operating system, and gcc has no such target." + : ctx.parsed->loader_supplies_os() + ? "A target whose loader supplies the system is emitted by " + "clang and lld for\n" + " every host: the compiler is told no operating system, " + "and gcc has no such target." : ctx.parsed->is_wasm() ? "Nothing but Emscripten emits WebAssembly: `em++` is a " "clang whose target,\n" @@ -1202,7 +1211,8 @@ static std::expected step1_device_axis_and_layer_merge(Prepar // over these (prepare_inputs::Ctx::layer_matches); before this the field // was declared, documented, and never written, so the key matched nothing. state.cfgCtx = [&]() { - auto c = cfgpred::context_for(state.overrides.target_triple); + auto c = cfgpred::context_for(state.overrides.target_triple, + state.platformPackageShortName); for (auto const& set : mcpp::pack::parse_accel(state.resolvedAccel())) c.accelerators.push_back(set.backend); return c; @@ -1335,7 +1345,11 @@ std::expected phase1_toolchain_spec_and_axes(PrepareState& st !r) return std::unexpected(r.error()); if (auto r = step1_define_early_toolchain_closures(state); !r) return std::unexpected(r.error()); + step1_define_host_default_closures(state); if (auto r = step1_target_and_static_overrides(state); !r) return std::unexpected(r.error()); + // The platform, once the target is known and before the conditional + // tables merge: `cfg(platform = ...)` is one of their keys (SPEC-013 §3). + if (auto r = step1_select_platform(state); !r) return std::unexpected(r.error()); if (auto r = step1_device_axis_and_layer_merge(state); !r) return std::unexpected(r.error()); return {}; @@ -1567,12 +1581,22 @@ step2_resolve_explicit_spec(PrepareState& state, ToolchainResolveCtx& ctx) { ? std::string{} : std::format(", not your {} — this project only", state.graphCompilerReplaced)); + else if (!state.pinReplacedDefault.empty() && state.platformToolchainPin) + chosenBy = std::format( + "\n the platform's ({}), replacing your " + "{} — override with `[target.{}] toolchain`", + state.platform.display(), state.pinReplacedDefault, + state.overrides.target_triple.empty() + ? mcpp::toolchain::triple::host_triple().str() + : state.overrides.target_triple); else if (!state.pinReplacedDefault.empty()) chosenBy = std::format( "\n target default for {}, replacing your " "{} — override with `[target.{}] toolchain`", state.overrides.target_triple, state.pinReplacedDefault, state.overrides.target_triple); + else if (state.platformToolchainPin) + chosenBy = std::format(" (the platform's, {})", state.platform.display()); else if (state.tcOrigin == TcOrigin::TargetPin || state.tcOrigin == TcOrigin::FirstRun) chosenBy = std::format(" ({})", tc_origin_name(state.tcOrigin)); @@ -1877,356 +1901,6 @@ step2_detect_toolchain(PrepareState& state) { return {}; } -static std::expected -step2_retarget_for_retargetable_driver(PrepareState& state) { - // ── A retargetable driver has to be TOLD what it is targeting ──────── - // - // `tc.targetTriple` comes from `-dumpmachine`, and for every cross target - // that worked before this it was right for a reason that does not - // generalise: those targets use a DISTINCT compiler binary - // (`x86_64-w64-mingw32-g++`, `aarch64-linux-musl-g++`), whose own - // -dumpmachine reports the cross triple. Clang is ONE binary that emits - // every target it was built with, so -dumpmachine always answers with the - // host — and nothing downstream ever learns otherwise. - // - // Measured before this line existed: - // - // $ mcpp build --target riscv64-none-elf - // Resolved llvm@22.1.8 → riscv64-none-elf → …/bin/clang++ - // Finished dev [unoptimized + debuginfo] in 0.47s - // $ ls target/ - // x86_64-linux-gnu/ ← an ELF for the host, reported as riscv64 - // - // That is E1: success reported, host artifact produced. The output - // directory, the fingerprint, the cache key and the flag layer all read - // `tc.targetTriple`, so correcting it here corrects all of them at once — - // which is the point of there being one field rather than five answers. - // - // THIS USED TO BE SCOPED TO FREESTANDING, WITH THIS REASON: - // - // The hosted cross targets already resolve a per-target binary, and - // overwriting their probed triple would replace a measured fact with - // an assumed one for no gain. - // - // That was true while every hosted cross was served by a payload. It - // stops being true when the TARGET SIDE comes from the dependency graph: - // the C library, the C++ runtime and the platform's own implementation are - // then packages built from source, and the compiler is an ordinary clang — - // whose `-dumpmachine` answers the host, exactly as the paragraph above - // describes for freestanding. - // - // Measured 2026-08-23, with an explicit `[target.aarch64-macos] - // toolchain = "llvm@…"`. The manifest's cfg evaluation used the REQUESTED - // target, so the C library's aarch64 headers were on the command line; the - // toolchain's own triple was still the host's, so code generation was - // x86_64. Two answers to one question, in one command: - // - // okm_float_assert.c: the C library and the compiler disagree about - // LDBL_DIG ('33 == 18') 33 = aarch64 binary128, 18 = x87 - // - // ⇒ The condition is now the property the first paragraph of this comment - // already names: a RETARGETABLE driver has to be told. gcc is not one — a - // gcc payload IS its target — so the mingw and musl-gcc crosses keep - // answering from `-dumpmachine`, which for them remains a measured fact. - if (!state.overrides.target_triple.empty()) { - if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); - want && (want->is_freestanding() - || state.tc->compiler == mcpp::toolchain::CompilerId::Clang)) - { - state.tc->targetTriple = want->str(); - - // AND THE GATE THAT ALREADY EXISTS FOR THIS, APPLIED WHERE THE - // ANSWER IS KNOWN. - // - // `discover_link_runtime_dirs` refuses to report these - // directories for a target that carries its own sysroot, and the - // refusal never fired: that function runs during DETECTION, - // before this line, when `targetTriple` is still the HOST's. The - // gate read a host triple and answered correctly about it. - // - // The artefact is what showed it. An Android link line carried - // - // -L /toolchains/llvm/prebuilt/linux-x86_64/lib/ - // x86_64-unknown-linux-gnu - // - // whose last component is this machine's triple, produced by - // `root / "lib" / targetTriple` -- so the string names the - // question that was asked. Those are the compiler's own host - // runtime directories; an Android artefact must resolve libc++, - // the crt objects and the loader from the NDK's sysroot, and the - // hermetic check reported exactly that failure with six host - // objects. - // - // Cleared rather than re-derived. Re-running the discovery with - // the final triple would also change what every OTHER clang cross - // target gets, and those are measured as they stand; the claim - // being made here is only the one the gate already states. - if (want->has_own_sysroot()) state.tc->linkRuntimeDirs.clear(); - - // And the flag that says it to the driver — for a HOSTED target - // only. Freestanding already emits its own `--target`, together - // with the ISA flags that must accompany it - // (freestanding/target.cppm); a second one here would be the same - // decision in two places. - if (!want->is_freestanding() - && state.tc->compiler == mcpp::toolchain::CompilerId::Clang) { - state.tc->crossTargetFlag = - "--target=" + want->llvm_triple( - min_platform_version(*state.m, *want, state.tc->binaryPath)); - - // AND THE SAME FLAG ON THE std MODULE'S OWN COMMANDS, FOR A - // PAYLOAD THAT SERVES MORE THAN ONE TARGET. - // - // The std module is built by its own command assembly - // (clang.cppm), not by the compile flags, so a decision made - // only here reaches every translation unit and not that. For - // most toolchains the omission cannot be seen: a payload - // whose compiler IS its target finds its own headers, and a - // package-provided module carries the target inside - // `stdModuleFlags`. - // - // ONE NDK SERVES BOTH ANDROID ARCHES, which is the property - // that makes this necessary and is stated in the row's own - // pin: `android-ndk@` names no arch, so `--target` is the - // only thing that says which. Without it the precompile - // resolved libc++'s `#include <__config>` against the - // building machine and stopped there. - // - // NOT `has_own_sysroot()`, though both rows that answer true - // to it are SDKs with their own sysroot. Emscripten's `em++` - // serves exactly one target and needs no flag -- the verified - // wasm loop is measured without it -- so widening the gate to - // the predicate would add a flag to a command that does not - // want one. The property here is "one payload, several - // targets", and Android is the only row that has it; a future - // row brings its own measurement. - if (want->is_android()) { - state.tc->stdModuleTargetFlags = " " + state.tc->crossTargetFlag; - // BIONIC'S ctype HEADER AND A MODULE'S EXPORT RULES. - // - // bionic declares `isalnum` and its neighbours - // `static inline`, and libc++'s module surface exports - // them with `using std::isalnum`. A using-declaration - // cannot export a name with internal linkage, so the - // precompile fails on 14 names at once. Defining the - // macro empty makes those declarations extern, which is - // what every other C library this engine compiles - // against already does. - // - // Scoped to the std module and not to every unit: the - // rule being satisfied is about exporting from a module, - // and a translation unit that includes - // directly is entitled to bionic's inline definitions. - // `xim:android-ndk`'s own install-time self-test reaches - // the identical conclusion from the other direction. - // - // AND THE PAYLOAD MAY SAY SO ITSELF. The recipe applies - // this same define in that self-test, so it is a fact - // the payload already holds; `std_module_defines` in - // `.mcpp-toolchain.json` is the channel for it, and the - // define below is what a payload that ships no - // descriptor still gets. The two are not added - // together: a descriptor that names defines is the - // payload's complete answer for this channel, and - // appending to it would mean a payload could not - // withdraw a define this engine once needed. - auto stdDefines = [&]() -> std::vector { - auto desc = - mcpp::toolchain::payload_descriptor_for_compiler( - state.tc->binaryPath); - if (desc && *desc && !(*desc)->stdModuleDefines.empty()) - return (*desc)->stdModuleDefines; - return { "__BIONIC_CTYPE_INLINE=" }; - }(); - for (auto const& def : stdDefines) - state.tc->stdModuleTargetFlags += " -D" + def; - } - - // ── iOS: THE COMPILER IS OURS, THE SDK IS THE MACHINE'S ── - // - // The three iOS rows pin `llvm@23.1.3` -- any sufficiently - // new clang emits arm64 Mach-O for an iOS deployment target - // -- and take their headers and stub libraries from the - // machine's Xcode, which is where the whole item shrinks to - // a located sysroot. `aarch64-macos` is verified on exactly - // this split and is the precedent. - // - // LOCATED HERE, ONCE. Three later sites need the answer (the - // compile flags, the link line, and the std module's own - // command), and a function that probes the machine is the - // wrong thing to call three times: `xcrun` shells out, and - // three answers can differ if the developer directory - // changes mid-build. - // - // AND ITS ABSENCE IS A REFUSAL THAT NAMES THE SDK. The - // recorded host-surface rule is that a host dependency must - // be minimal, named, and never a fallthrough; the iOS SDK - // and `simctl` are the two this platform adds, both in the - // "proprietary runtime that exists only on its own OS" - // category. A build that continued without the SDK would - // fail in the driver's header search, naming a file rather - // than the thing that is missing. - if (want->is_ios()) { - // READ, NOT RE-DERIVED. The refusal above located it - // before any payload was resolved, and that is the one - // `xcrun` call this build makes. - // - // An empty answer here cannot happen through the - // `--target` path, and a line that prints when it does - // is cheaper than a branch that pretends it cannot: the - // row could be reached one day by a route that skipped - // the gate, and an iOS build with no `-isysroot` is a - // macOS artefact with an iOS triple on it. - if (!state.appleSdkLocated) { - return std::unexpected(std::format( - "internal: target {} reached toolchain " - "resolution without its SDK being located; the " - "gate that locates it did not run for this " - "request", want->str())); - } - state.tc->base.apple.root = *state.appleSdkLocated; - if (state.appleSdkFound) { - state.tc->base.apple.name = state.appleSdkFound->name; - state.tc->base.apple.version = state.appleSdkFound->version; - } - auto sdk = state.appleSdkLocated; - // AND THE std MODULE'S OWN COMMAND, WHICH IS A SEPARATE - // CHANNEL. Same reason the Android rows set it: the - // module is precompiled by `clang.cppm`'s own assembly - // rather than by the compile flags, so a decision made - // only in the flag builder reaches every translation - // unit and not the module they all import. Without the - // SDK here the precompile resolves libc++'s - // `#include <__config>` against the macOS SDK and the - // module is built for the wrong platform. - // - // QUOTED, as every path this string carries is (see the - // package-provided producer, which uses `shq` for each - // `-isystem`). The string is spliced into a shell - // command, and an Xcode installed as `Xcode 16.app` is - // a path with a space in it. - state.tc->stdModuleTargetFlags = - " " + state.tc->crossTargetFlag - + " -isysroot " + mcpp::build::command_line::quote(sdk->string()); - } - } - } - if (auto want = mcpp::toolchain::triple::parse(state.overrides.target_triple); - want && want->is_freestanding()) - { - // `import std` is structurally hosted, and turning it off is the - // SAME fact as the line above, not a second policy: libc++'s - // std.cppm is one module over the whole library, including the - // parts that are threads, filesystem and iostreams. There is no - // subset of it to precompile. - // - // Left on, the failure is neither early nor legible — measured: - // - // error: std module precompile failed (rc=1): - // .../include/c++/v1/__config:13:10: fatal error: - // '__config_site' file not found - // - // which reads as a broken toolchain payload and says nothing about - // the target. The freestanding std subset a user actually wants is - // an ordinary package (`mcpplibs.std.freestanding`), so mcpp's job - // here is to stop pretending the hosted one exists and to say - // where the other one is. - state.tc->clear_std_modules(); - - // ── The target's C library, resolved like its compiler ───────── - // - // The row in kKnownTargets names it, exactly as it names the - // toolchain pin, and it is installed through the same channel a - // project's `[xlings] deps` use (see the materialization above). - // Resolved HERE because the config is already open; the flag - // builder only reads the result. - // - // Absent is not an error at this point: the install happens - // earlier in this function and may legitimately not have run yet - // on a first pass. What follows would then simply not add the - // paths, and the link fails naming the missing libc — which is the - // truthful message either way. - const std::string want_sysroot = - mcpp::toolchain::triple::effective_sysroot( - *want, base_statement_text(state, *want)); - // A C library named by path (design 2026-10-23 §3.1): the same - // `/{include,lib}/` layout, nothing to install. - if (want_sysroot.starts_with("path:")) { - const std::filesystem::path dir = want_sysroot.substr(5); - std::error_code ec2; - if (!std::filesystem::is_directory(dir, ec2)) { - refusal::record(refusal::Code::TargetBase); - return std::unexpected(std::format( - "[target.{}].sysroot = {{ path = \"{}\" }} does not exist", want->str(), - dir.generic_string())); - } - if (auto spec = mcpp::freestanding::resolve(*want)) { - state.tc->base.multilibRoot = dir; - state.tc->base.package = dir.filename().string(); - const auto inc = dir / "include" / std::string(spec->libdir); - const auto lib = dir / "lib" / std::string(spec->libdir); - if (std::filesystem::is_directory(inc, ec2)) state.tc->base.multilibInclude = inc; - if (std::filesystem::is_directory(lib, ec2)) state.tc->base.multilibLib = lib; - } - } else if (!want_sysroot.empty()) { - if (auto cfg3 = state.get_cfg(true); cfg3) { - auto ref = mcpp::provider::parse_ref(want_sysroot); - auto xl = mcpp::config::make_xlings_env(**cfg3); - // INSTALLED, NOT MERELY LOOKED UP — THE SAME CHANNEL - // THE ROW'S TOOLCHAIN PIN GOES THROUGH. - // - // The row names two things and only one of them used to - // be made to exist: `pin` went through - // `resolve_xpkg_path(…, PayloadIntent::Use, …)` while - // `sysroot` was a pure lookup that returned nullopt and - // let the whole block below be skipped without a word. - // - // Measured 2026-08-26 in a clean environment (an empty - // home, so mcpp's registry starts fresh): - // - // Target riscv64-none-elf - // c-abi picolibc-riscv (…, prebuilt) - // error: 'stdio.h' file not found - // - // The report named the C library and the build could not - // find its headers. mcpp's own bare-metal CI installs it - // by hand, which is why no test ever saw this — every - // bare-metal e2e runs on a machine where the gap has - // already been papered over. - // - // OFFLINE AND `MCPP_NO_AUTO_INSTALL` ARE THE FETCHER'S - // DECISION, not re-derived here. One question, one place - // that answers it — asking it twice is the shape this - // whole release exists to remove. - mcpp::fetcher::Fetcher srFetcher(**cfg3); - mcpp::fetcher::InstallProgressHandler srProgress; - std::optional dir; - if (auto p = srFetcher.resolve_xpkg_path( - want_sysroot, mcpp::fetcher::PayloadIntent::Use, &srProgress)) - dir = p->root; - else - dir = mcpp::provider::payload(*mcpp::provider::open(xl), ref); - if (dir) { - if (auto spec = mcpp::freestanding::resolve(*want)) { - const auto inc = - *dir / "include" / std::string(spec->libdir); - const auto lib = - *dir / "lib" / std::string(spec->libdir); - std::error_code ec2; - state.tc->base.multilibRoot = *dir; - state.tc->base.package = ref.name; - if (std::filesystem::is_directory(inc, ec2)) - state.tc->base.multilibInclude = inc; - if (std::filesystem::is_directory(lib, ec2)) - state.tc->base.multilibLib = lib; - } - } - } - } - } - } - return {}; -} static std::expected step2_bind_msvc_toolset(PrepareState& state) { diff --git a/src/build/prepare/toolchain_decision.cpp b/src/build/prepare/toolchain_decision.cpp index 7981be6dd..9d399c73e 100644 --- a/src/build/prepare/toolchain_decision.cpp +++ b/src/build/prepare/toolchain_decision.cpp @@ -19,6 +19,7 @@ import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.registry; import mcpp.toolchain.linkmodel; +import mcpp.toolchain.triple; // For `resolve_version_match` / `list_installed_versions`: a bare compiler // family named by the dependency graph resolves to a concrete version through // exactly the path `mcpp toolchain default ` uses. @@ -36,6 +37,35 @@ import mcpp.toolchain.post_install; namespace mcpp::build { +namespace { + +// WHAT "THE GRAPH SUPPLIES THE SYSTEM" MEANS FOR THIS TARGET. A C library of +// the graph does, on any target. A kernel-abi package does only where the +// target has no system of its own (a freestanding or a loader-supplied row): +// on a hosted target it sits over the system's C library, which the row's +// payload still supplies. Measured on linux-aarch64: openkal-tools reaches +// openkal-linux (kernel-abi over the system's C library), the musl row's +// convention was cancelled by it, and `mcpp pack --linkage static` compiled the +// musl target with the machine's llvm against glibc's headers. +bool graph_supplies_system(const PrepareState& state) { + const auto targetTriple = mcpp::toolchain::triple::parse( + state.overrides.target_triple.empty() ? mcpp::toolchain::triple::host_triple().str() + : state.overrides.target_triple); + const bool targetHasNoSystem = targetTriple + && (targetTriple->is_freestanding() || targetTriple->loader_supplies_os()); + auto supplies_system = [&](const std::string& entry) { + auto cap = mcpp::targetside::parse_capability(entry); + if (!cap || !*cap) return false; + return (*cap)->layer == mcpp::targetside::CapLayer::CAbi + || ((*cap)->layer == mcpp::targetside::CapLayer::KernelAbi && targetHasNoSystem); + }; + return std::ranges::any_of(state.packages, [&](auto const& pkg) { + return std::ranges::any_of(pkg.manifest.provides, supplies_system); + }); +} + +} // namespace + std::expected phase5_toolchain_after_graph(PrepareState& state) { // ─── The toolchain, resolved now that the graph exists ────────────────── @@ -160,15 +190,8 @@ std::expected phase5_toolchain_after_graph(PrepareState& stat reqCompilerBy, family, family, family)); }; + graphSuppliesSystem = graph_supplies_system(state); for (auto const& pkg : state.packages) { - for (auto const& entry : pkg.manifest.provides) { - auto cap = mcpp::targetside::parse_capability(entry); - if (!cap || !*cap) continue; - if ((*cap)->layer == mcpp::targetside::CapLayer::KernelAbi - || (*cap)->layer == mcpp::targetside::CapLayer::CAbi) { - graphSuppliesSystem = true; - } - } for (auto const& entry : pkg.manifest.requires_) { auto cap = mcpp::targetside::parse_capability(entry); if (!cap || !*cap) continue; @@ -205,9 +228,10 @@ std::expected phase5_toolchain_after_graph(PrepareState& stat } } } - // AND A FREESTANDING PIN SURVIVES IT. `graphSuppliesSystem` spans - // kernel-abi and c-abi, and it correctly cancels a HOSTED row's - // convention — that row names the payload the graph is replacing. + // AND A FREESTANDING PIN SURVIVES IT. `graphSuppliesSystem` (a C + // library of the graph, or a kernel-abi package where the target has no + // system) correctly cancels a HOSTED row's convention — that row names + // the payload the graph is replacing. // A bare-metal row names the only compiler that emits the target. // // Measured 2026-08-25, on a three-line manifest: @@ -223,6 +247,9 @@ std::expected phase5_toolchain_after_graph(PrepareState& stat // chosen for a target it cannot produce. Same shape as the four // defects 2026.8.25.1 fixed: a predicate spanning two layers deciding // something that does not depend on either of them. + // The platform's toolchain, when one is selected, stands where the + // row's pin stands (SPEC-013 §4.5). + step5_platform_toolchain(state); if (!state.targetPinCandidate.empty() && (!graphSuppliesSystem || state.targetPinIsCapability)) { if (state.tcOrigin == TcOrigin::GlobalDefault && state.tcSpec.has_value() diff --git a/src/build/prepare_inputs.cppm b/src/build/prepare_inputs.cppm index 059856118..81fa0396b 100644 --- a/src/build/prepare_inputs.cppm +++ b/src/build/prepare_inputs.cppm @@ -65,6 +65,10 @@ namespace cfgpred { // `requires = ["mcpp:compiler=llvm"]` permanently unsatisfiable. struct Ctx { std::string os, arch, family, env, triple; + // The selected platform (SPEC-013 §3): the platform's name (`openkal`) + // and its package's (`openkal-platform`); `cfg(platform = ...)` matches + // either. Empty when the target uses none. + std::string platform, platformPackage; bool layersKnown = false; std::string compiler, compilerRuntime, kernelAbi, cAbi, cxxAbi; // The first MULTI-VALUED layer. One build can enable several accelerator @@ -132,9 +136,14 @@ struct Ctx { // triple parser — so the cfg vocabulary IS the canonical triple vocabulary // (os: linux|macos|windows, arch: GNU spellings, env: gnu|musl|msvc), and // alias spellings ("x86_64-w64-mingw32") evaluate identically to canonical. -inline Ctx context_for(std::string_view targetTriple) { +inline Ctx context_for(std::string_view targetTriple, + std::string_view platformPackage = {}) { namespace triple = mcpp::toolchain::triple; Ctx c; + if (!platformPackage.empty()) { + c.platformPackage = std::string(platformPackage); + c.platform = mcpp::manifest::platform::platform_name(platformPackage); + } auto t = targetTriple.empty() ? std::optional(triple::host_triple()) : triple::parse(targetTriple); @@ -271,6 +280,8 @@ struct Parser { if (k == "arch") return c.arch == v; if (k == "family") return c.family == v; if (k == "env") return c.env == v; + if (k == "platform") + return !c.platform.empty() && (c.platform == v || c.platformPackage == v); // `accelerator` is answerable whenever the context carries the build's // accel, which is from the first pass onward -- see kCfgEarlyLayerKeys. if (is_cfg_early_layer_key(k)) return c.layer_matches(k, v); diff --git a/src/build/progress.cppm b/src/build/progress.cppm index 77b637dd5..03e429f98 100644 --- a/src/build/progress.cppm +++ b/src/build/progress.cppm @@ -769,6 +769,10 @@ struct Report { std::vector started; // packages announced since the last frame long long lastFrame = 0; // ms since command start std::size_t lastDone = 0; + // The lines a package was announced with in this command (L6): a + // workspace builds several configurations, and a dependency they share + // was written once per configuration with the same words. + std::set announcedLines; }; Report& report() { @@ -888,7 +892,11 @@ void announce(Report& r, Build::Impl& b, std::size_t i, std::vector const auto& p = b.record->packages[i]; if (s.announced || p.name == "std" || p.name.empty()) return; s.announced = true; - out.push_back(package_line(r, b, i)); + auto line = package_line(r, b, i); + // One line per package and per command: the same package, with the same + // words, in a second configuration says nothing new. + if (!r.announcedLines.insert(line).second) return; + out.push_back(std::move(line)); r.started.push_back(p.cachedUnits > 0 ? screen::Source::Cache : source_of(p.source)); } diff --git a/src/build/refusal.cppm b/src/build/refusal.cppm index d384f06ae..6c9bcb07d 100644 --- a/src/build/refusal.cppm +++ b/src/build/refusal.cppm @@ -185,13 +185,24 @@ enum class Code { // discover them from (2026.10.5.2). Distinct from every other code: both // statements are the author's, and one of them has to change. LtoExportDiscovery, - // A build of `-openkal` that links an image finds no `okld` -- the - // format's tool, which writes the image's requirements note and checks the - // convention before the image is published (design 2026-10-26 part 2, - // §8.5). Distinct from LldRequiredAbsent, which is about the toolchain's - // payload: `okld` is a program of the dependency graph, and declaring - // openkal-tools under `[build-dependencies]` supplies it. - ImageToolAbsent, + // PLATFORMS (SPEC-013). A program a platform's link or seal names -- + // `okld` for an ok image -- is neither built by the graph nor found on + // this machine. Distinct from LldRequiredAbsent, which is about the + // toolchain's payload: the tool is the platform's, and its `[platform.tools]` + // (or `[target..tools]`) says which package builds it. Named + // `image-tool-absent` in 2026.10.26.2, before ok images were a platform. + PlatformToolAbsent, + // The package selected as the platform has no `[platform]` section. + PlatformNotAPlatform, + // The platform does not serve the target: the triple is not among its + // `targets`, and it has no `[platform.hosted]` for a host triple. + PlatformTargetUnserved, + // A package of the platform's version group is in the graph at another + // version (§8.1). + PlatformVersionGroup, + // A platform's build program stated a directive outside its phase's + // vocabulary (build, link, runner, runners). + PlatformPhaseDirective, Other, // a refusal that has not been given a code yet }; @@ -246,7 +257,13 @@ constexpr std::string_view name(Code c) { case Code::LocalToolchain: return "local-toolchain"; case Code::TargetBase: return "target-base"; case Code::LtoExportDiscovery: return "lto-export-discovery"; - case Code::ImageToolAbsent: return "image-tool-absent"; + case Code::PlatformToolAbsent: return "platform-tool-absent"; + case Code::PlatformNotAPlatform: return "platform-not-a-platform"; + case Code::PlatformTargetUnserved: + return "platform-target-unserved"; + case Code::PlatformVersionGroup: return "platform-version-group"; + case Code::PlatformPhaseDirective: + return "platform-phase-directive"; case Code::Other: return "other"; } return "other"; diff --git a/src/build/result.cppm b/src/build/result.cppm index bb8f5f838..3c272b5c7 100644 --- a/src/build/result.cppm +++ b/src/build/result.cppm @@ -79,6 +79,10 @@ struct Record { std::string triple; std::string profile; std::string fingerprint; + // The configuration's platforms (SPEC-013 §9): the target graph's and the + // host graph's, as `mcpp::platform::to_json` writes them; "null" for none. + std::string platform = "null"; + std::string hostPlatform = "null"; std::filesystem::path outputDir; std::filesystem::path projectRoot; std::filesystem::path compileDatabase; @@ -182,6 +186,8 @@ nlohmann::json to_json(const Record& r) { {"profile", r.profile}, {"fingerprint", r.fingerprint}, {"outputDir", r.outputDir.generic_string()}, + {"platform", nlohmann::json::parse(r.platform, nullptr, false)}, + {"hostPlatform", nlohmann::json::parse(r.hostPlatform, nullptr, false)}, }}, {"compileDatabase", r.compileDatabase.empty() ? nlohmann::json(nullptr) : nlohmann::json(r.compileDatabase.generic_string())}, @@ -208,6 +214,8 @@ std::optional read(const std::filesystem::path& outputDir) { r.profile = c.value("profile", ""); r.fingerprint = c.value("fingerprint", ""); r.outputDir = c.value("outputDir", ""); + if (c.contains("platform")) r.platform = c["platform"].dump(); + if (c.contains("hostPlatform")) r.hostPlatform = c["hostPlatform"].dump(); } if (d.contains("compileDatabase") && d["compileDatabase"].is_string()) r.compileDatabase = d["compileDatabase"].get(); diff --git a/src/build/runner_lookup.cppm b/src/build/runner_lookup.cppm index 7b9b0587b..f0bb3ac39 100644 --- a/src/build/runner_lookup.cppm +++ b/src/build/runner_lookup.cppm @@ -88,9 +88,20 @@ inline Lookup locate(std::string_view argv0, out.searched.push_back(a0.parent_path()); return out; } + // A bare name on Windows is a program with the system's suffix, as the + // shell finds it: `okrun` is `okrun.exe`. + auto in_dir = [&](const std::filesystem::path& d) -> std::optional { + if (auto c = d / a0; detail::executable_file(c)) return c; + if constexpr (mcpplibs::os::is_windows) { + if (!a0.has_extension()) + for (auto ext : { ".exe", ".cmd", ".bat" }) + if (auto c = d / (a0.string() + ext); detail::executable_file(c)) return c; + } + return std::nullopt; + }; for (auto const& d : depBinDirs) { out.searched.push_back(d); - if (auto c = d / a0; detail::executable_file(c)) { out.program = c; return out; } + if (auto c = in_dir(d)) { out.program = *c; return out; } } constexpr char sep = mcpplibs::os::is_windows ? ';' : ':'; for (auto part : std::views::split(pathEnv, sep)) { @@ -98,7 +109,7 @@ inline Lookup locate(std::string_view argv0, if (sv.empty()) continue; std::filesystem::path d(sv); out.searched.push_back(d); - if (auto c = d / a0; detail::executable_file(c)) { out.program = c; return out; } + if (auto c = in_dir(d)) { out.program = *c; return out; } } return out; } diff --git a/src/cli.cppm b/src/cli.cppm index e9a7fb9fb..7c366fb5d 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -208,6 +208,24 @@ int enter_system_home(int argc, char** argv) { return 0; } +// `mcpp self env` with `--target` or `--host` reports one configuration +// (SPEC-013 §9), prepared by the build commands' module. +int cmd_env_any(const mcpplibs::cmdline::ParsedArgs& p) { + if (p.value("target") || p.is_flag_set("host")) return cmd_env_configuration(p); + return cmd_env(p); +} + +// `mcpp toolchain install --target ` with no compiler named installs the +// target's whole configuration once its toolchain is in (SPEC-013 §7). +int cmd_toolchain_any(const mcpplibs::cmdline::ParsedArgs& p) { + const int rc = cmd_toolchain(p); + if (rc != 0 || p.subcommand_name() != "install" || !p.subcommand_matches) return rc; + auto const& sub = *p.subcommand_matches; + auto target = sub.value("target"); + if (!target || target->empty() || !sub.positional(0).empty()) return rc; + return cmd_install_configuration(*target); +} + int run(int argc, char** argv) { namespace cl = mcpplibs::cmdline; ReportUnnarrowablePaths reportUnnarrowable_; @@ -382,6 +400,19 @@ int run(int argc, char** argv) { if (i + 1 < argc) mcpp::invocation::set("MCPP_SYSROOT", argv[++i]); } else if (a.starts_with("--sysroot=")) mcpp::invocation::set("MCPP_SYSROOT", std::string(a.substr(10))); + // --platform and --host-platform are `MCPP_PLATFORM` and + // `MCPP_HOST_PLATFORM` for this invocation (SPEC-013 §3, §6.1), for + // the reason --sysroot is: the selection is made deep in prepare, and + // the fast paths read the variables to know the configuration moved. + else if (a == "--platform") { + if (i + 1 < argc) mcpp::invocation::set("MCPP_PLATFORM", argv[++i]); + } + else if (a.starts_with("--platform=")) mcpp::invocation::set("MCPP_PLATFORM", std::string(a.substr(11))); + else if (a == "--host-platform") { + if (i + 1 < argc) mcpp::invocation::set("MCPP_HOST_PLATFORM", argv[++i]); + } + else if (a.starts_with("--host-platform=")) + mcpp::invocation::set("MCPP_HOST_PLATFORM", std::string(a.substr(16))); else if (a.starts_with("-j") && a.size() > 2) mcpp::invocation::set("MCPP_JOBS", std::string(a.substr(2))); // --play-game rides the same channel: its value is optional @@ -583,6 +614,10 @@ int run(int argc, char** argv) { .help("Build with this toolchain for one build, e.g. llvm@22.1.8")) .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("no-cache") .help("Deprecated alias for --cache=off (also clears the build dir)")) .option(cl::Option("target").takes_value().help( @@ -655,6 +690,10 @@ int run(int argc, char** argv) { .help("Build with this toolchain for one invocation, e.g. llvm@22.1.8")) .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("cache").takes_value().value_name("MODE") .help("Global dependency cache: global (default) | local | off")) .option(cl::Option("no-cache") @@ -706,10 +745,12 @@ int run(int argc, char** argv) { // given a run-specific spelling, because it is the same request: // "produce this distributable", asked from a command that also // executes it afterward. - .option(cl::Option("format").takes_value() - .help("Run the distributable a `--format ` pack would produce " - "(same values as `mcpp pack --format`); refused together with " + .option(cl::Option("pkg-format").takes_value().value_name("FORMAT") + .help("Run the distributable a `--pkg-format ` pack would produce " + "(same values as `mcpp pack --pkg-format`); refused together with " "--no-runner")) + .option(cl::Option("format").takes_value() + .help("Earlier spelling of --pkg-format")) .option(cl::Option("play-game") .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") @@ -755,6 +796,10 @@ int run(int argc, char** argv) { .help("Build the tests with this toolchain for one invocation, e.g. llvm@22.1.8")) .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("cache").takes_value().value_name("MODE") .help("Global dependency cache: global (default) | local | off")) .option(cl::Option("no-cache") @@ -790,6 +835,10 @@ int run(int argc, char** argv) { .help("Ask about this toolchain, e.g. llvm@22.1.8")) .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("features").takes_value().value_name("LIST") .help("Resolve with these features, as `build --features` does (root features and /)")) .option(cl::Option("format").takes_value().value_name("json") @@ -848,17 +897,36 @@ int run(int argc, char** argv) { // package. Omit it and mcpp picks the only packable target. .arg(cl::Arg("target").help("Target name from [targets.*] (optional)")) .option(cl::Option("mode").takes_value() - .help("system | vendored (default) | self-contained | static")) + .help("What the machine that runs it provides: system | vendored (default) | " + "self-contained | static (= --linkage static --mode self-contained)")) + .option(cl::Option("linkage").takes_value().value_name("static|shared") + .help("The program's libraries in it (static) or beside it (shared); rebuilds " + "with [build] dependency_linkage (default: as the project builds)")) .option(cl::Option("target").takes_value().multiple() .help("Triple, e.g. x86_64-linux-musl (repeatable: one leg per triple)")) + .option(cl::Option("no-universal") + .help("Pack the selected target alone where its platform packs all its targets " + "into one universal package")) + .option(cl::Option("systems").takes_value().value_name("LIST") + .help("Self-contained universal package: the systems whose loaders it carries " + "(linux,macos,windows; default: this machine's)")) + .option(cl::Option("compress").takes_value().value_name("ALG[:LEVEL]") + .help("The container's compression: gzip (tar default) | zstd | xz | none; " + "a zip deflates unless none")) + .option(cl::Option("merge").takes_value().multiple().value_name("PACKAGE") + .help("Merge packages this command wrote (universal legs, systems) into one; " + "repeatable, nothing is built")) // "plus any format the resolved graph provides", because the // values are no longer a fixed list. A package declares one with // `mcpp::provides_pack_format` and `--format ` dispatches to // it; an unknown value names what IS available rather than a // constant, so the help text does not have to enumerate them. + .option(cl::Option("pkg-format").takes_value().value_name("FORMAT") + .help("The package format: tar | zip | dir | single-file | any format the " + "resolved graph provides (e.g. appimage, msi, okb); default: zip for a " + "Windows program, tar elsewhere")) .option(cl::Option("format").takes_value() - .help("tar (default; .zip for a Windows target) | dir | any " - "format the resolved graph provides (e.g. appimage, msi)")) + .help("Earlier spelling of --pkg-format")) .option(cl::Option("output").short_name('o').takes_value() .help("Override output path; with several members, the directory each archive or tree is written below")) // Packaging builds RELEASE by default — the artifact leaves this @@ -874,6 +942,10 @@ int run(int argc, char** argv) { .help("Build with this toolchain for one invocation, e.g. llvm@22.1.8")) .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("features").takes_value().value_name("LIST") .help("Activate root-package features, and a dependency's as /, for every build pass of the pack (comma-separated)")) // `--format` names the PACKAGE format here, so machine output is @@ -906,7 +978,12 @@ int run(int argc, char** argv) { .option(cl::Option("namespace").takes_value().value_name("NS") .help("Package namespace for the emitted descriptor " "(overrides [package] namespace). Emits both " - "`namespace` and the fully-qualified `name`"))) + "`namespace` and the fully-qualified `name`")) + .option(cl::Option("package").takes_value().multiple().value_name("ARCHIVE") + .help("Describe packages `mcpp pack` wrote (repeatable): an xim-pkgindex entry " + "of the program, from their mcpp-package.json")) + .option(cl::Option("url").takes_value().value_name("PREFIX") + .help("With --package: where the packages are published (/)"))) // `sbom` belongs HERE rather than at the top level: `emit` // already means "generate a document describing this project" and // already carries `-o`. A separate `mcpp sbom` would be a second @@ -932,6 +1009,10 @@ int run(int argc, char** argv) { .help("Describe the build with this toolchain, e.g. llvm@22.1.8")) .option(cl::Option("sysroot").takes_value().value_name("SYSROOT") .help("The target's base for this invocation (= MCPP_SYSROOT): a package, glibc@system, path:, none")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("accel").takes_value().value_name("SPEC") .help("Device backends and architectures (same axis as `mcpp build --accel`)")) .option(cl::Option("no-accel") @@ -1000,7 +1081,9 @@ int run(int argc, char** argv) { .arg(cl::Arg("compiler").help("gcc | llvm | msvc | emsdk | android-ndk (or gcc@16.1.0; legacy aliases accepted)")) .arg(cl::Arg("version").help("e.g. 16.1.0, 15, 15.1")) .option(cl::Option("target").takes_value().help( - "Install the toolchain payload for (e.g. x86_64-windows-gnu)"))) + "Install the toolchain payload for (e.g. x86_64-windows-gnu)")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("With --target: the target's platform for this invocation (= MCPP_PLATFORM)"))) .subcommand(cl::App("default") .description("Set the default toolchain (and optionally the default target)") // Same dual-form as `install`: `gcc@16.1.0` or `gcc 16.1.0`, @@ -1021,7 +1104,7 @@ int run(int argc, char** argv) { .arg(cl::Arg("version").help("(optional, alternative to @-form)")) .option(cl::Option("target").takes_value().help( "Remove the payload for instead of the host one"))) - .action(wrap_rc(cmd_toolchain))) + .action(wrap_rc(cmd_toolchain_any))) .subcommand(cl::App("cache") .description("Inspect and manage the global build cache") .subcommand(cl::App("dir") @@ -1124,7 +1207,15 @@ int run(int argc, char** argv) { .subcommand(cl::App("doctor") .description("Diagnose mcpp environment health")) .subcommand(cl::App("env") - .description("Print mcpp paths and configuration") + .description("Print mcpp paths and configuration; with --target, one configuration: toolchain, platform, tools, runner") + .option(cl::Option("target").takes_value().value_name("TRIPLE") + .help("Report this target's configuration (prepared, not built; the project's when run in one)")) + .option(cl::Option("host") + .help("Report the host graph's configuration: what build programs and tools are built for")) + .option(cl::Option("platform").takes_value().value_name("SPEC") + .help("The target's platform for this invocation (= MCPP_PLATFORM): @, path:, none")) + .option(cl::Option("host-platform").takes_value().value_name("SPEC") + .help("The host graph's platform for this invocation (= MCPP_HOST_PLATFORM)")) .option(cl::Option("format").takes_value().value_name("json") .help("Machine-readable output (enveloped; see docs/50-machine-output.md)"))) .subcommand(cl::App("config") @@ -1135,7 +1226,7 @@ int run(int argc, char** argv) { .description("Show mcpp version")) .subcommand(cl::App("explain") .description("Show extended description for an error code") - .arg(cl::Arg("code").help("Error code such as E0001").required())) + .arg(cl::Arg("code").help("Error code such as E0001, or a refusal's reason token such as platform-version-group").required())) .subcommand(cl::App("store") .description("Compare this home's payloads with its upstreams") .subcommand(cl::App("dedupe") @@ -1146,7 +1237,7 @@ int run(int argc, char** argv) { {"store", cmd_self_store}, {"init", cmd_self_init}, {"doctor", cmd_doctor}, - {"env", cmd_env}, + {"env", cmd_env_any}, {"config", cmd_self_config}, {"version", cmd_self_version}, {"explain", cmd_explain_action}, @@ -1158,7 +1249,7 @@ int run(int argc, char** argv) { // `mcpp self explain E0001`. .subcommand(cl::App("explain") .description("Show extended description for an error code") - .arg(cl::Arg("code").help("Error code such as E0001").required()) + .arg(cl::Arg("code").help("Error code such as E0001, or a refusal's reason token such as platform-version-group").required()) .action(wrap_rc(cmd_explain_action))) // ─── bareword `version` alias ───────────────────────────────── @@ -1212,11 +1303,13 @@ int run(int argc, char** argv) { .action(wrap_rc([&passthrough](const cl::ParsedArgs& p) { return cmd_link_output(p, std::span(passthrough)); }))) - .subcommand(cl::App("okimage-finish") - .description("(internal: invoked by ninja for -openkal) Write an ok image's requirements note and check it with okld, then publish it") - .option(cl::Option("okld").takes_value().value_name("PATH").help("the okld program")) + .subcommand(cl::App("platform-step") + .description("(internal: invoked by ninja for a platform's link) `capture --out F -- argv…` saves a program's output; `seal --steps F -- [needed…]` runs the platform's seal steps, then publishes") + .arg(cl::Arg("mode").help("capture | seal")) + .option(cl::Option("out").takes_value().value_name("PATH").help("capture: the file the output is saved as")) + .option(cl::Option("steps-hex").takes_value().value_name("HEX").help("seal: the steps, one per line, words separated by a tab, hex-encoded (the command is the steps, so a changed step reseals)")) .action(wrap_rc([&passthrough](const cl::ParsedArgs& p) { - return cmd_okimage_finish(p, std::span(passthrough)); + return cmd_platform_step(p, std::span(passthrough)); }))) .subcommand(cl::App("place-dlls") .description("(internal: invoked by ninja) Place beside a Windows program the DLLs it imports from its runtime search directories") @@ -1687,7 +1780,7 @@ int run(int argc, char** argv) { "update", "search", "publish", "pack", "emit", "xpkg", "toolchain", "cache", "index", "self", "explain", "version", "dyndep", "why", "resolve", "stage", "bmi-equal", "coff-def", - "place-dlls", "link-output", "okimage-finish", + "place-dlls", "link-output", "platform-step", "bmi-compile", "bmi-supervise", "bmi-await", }); bool ok = false; diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index d0d729de5..940eb604f 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -9,6 +9,10 @@ module; export module mcpp.cli.cmd_build; import std; +import mcpp.platform; +import mcpp.toolchain.platform; +import mcpp.toolchain.triple; +import mcpp.build.runner_lookup; import mcpplibs.cmdline; import mcpp.build.prepare; import mcpp.build.execute; @@ -36,6 +40,7 @@ import mcpp.hooks; import mcpp.libs.json; import mcpp.log; import mcpplibs.os.fs; +import mcpplibs.os.process; // platform-step: a program's output alone import mcpp.invocation; import mcpp.effects; import mcpp.run; @@ -974,7 +979,12 @@ export int cmd_run(const mcpplibs::cmdline::ParsedArgs& parsed, // pack --format`; `run` only adds the pairing with `--no-runner`, checked // in build_run_target where the runner is actually chosen. std::string format; - if (auto f = parsed.value("format")) format = *f; + if (auto f = parsed.value("pkg-format")) format = *f; + else if (auto f = parsed.value("format")) { + // The earlier spelling (`mcpp pack --pkg-format`'s note says why). + mcpp::ui::warning("`mcpp run --format` is the earlier spelling of --pkg-format"); + format = *f; + } if (parsed.is_flag_set("list-runners")) return mcpp::build::list_runners(package_filter, cache_mode, no_cache, target_triple, features, profile, accel); @@ -1284,6 +1294,34 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, mcpp::ui::plain(std::format(" slowest: {}", slowest)); return rc; } + // A PLATFORM PACKAGE IS TESTED ON THE TARGETS IT SERVES (SPEC-013 §2): + // with no --target, its tests are built for each of its `targets` with the + // package itself as the platform, and run where its runner can run them + // (`host = "same-arch"`: the target of this machine's architecture); the + // others are built and reported as not run. + if (ov.target_triple.empty() && ov.package_filter.empty()) { + std::error_code ec; + auto root = mcpp::project::find_manifest_root(std::filesystem::current_path(ec)); + auto m = root ? mcpp::manifest::load(*root / "mcpp.toml") + : std::expected( + std::unexpected(mcpp::manifest::ManifestError{})); + if (m && m->platformDeclared && !m->platform.targets.value.empty()) { + mcpp::invocation::set("MCPP_PLATFORM", "path:" + root->generic_string()); + int rc = 0; + for (auto const& t : m->platform.targets.value) { + auto tov = ov; + tov.target_triple = t; + auto tto = to; + auto tt = mcpp::toolchain::triple::parse(t); + const bool here = tt && tt->arch == mcpp::toolchain::platform::host_arch; + if (!here && m->platform.runner.host == "same-arch") tto.noRun = true; + mcpp::ui::info("Testing", std::format("platform {} on {}{}", m->package.name, t, + tto.noRun ? " (built, not run here)" : "")); + if (int r = mcpp::build::run_tests(passthrough, tov, tto); r != 0 && rc == 0) rc = r; + } + return rc; + } + } return mcpp::build::run_tests(passthrough, ov, to); } @@ -1476,58 +1514,246 @@ export int cmd_link_output(const mcpplibs::cmdline::ParsedArgs& parsed, return mcpp::run::run_exec(std::vector(command.begin(), command.end())); } -// `mcpp okimage-finish --okld -- […]` -// (internal: invoked by build.ninja for the target `-openkal`; design -// 2026-10-26 part 2, §8.5). +// `mcpp platform-step capture --out -- ` +// `mcpp platform-step seal --steps --digest -- […]` +// (internal: invoked by build.ninja for a platform's link; SPEC-013 §4, §4.6). +// +// CAPTURE saves a program's standard output as a link input (an ok image's +// notes, from `okld note-source`), written beside its name and renamed, so an +// interrupted run leaves no half file. // -// AN IMAGE IS PUBLISHED ONLY ONCE IT MEETS THE CONVENTION. The linked file is -// copied beside the image's name, `okld note` writes its requirements note from -// its strong imports, `okld check` checks every clause of SPEC 12.2 against it -// and the images it needs, and only then does the copy take the name. A check -// that fails leaves no image behind, and the edge runs again next time. -export int cmd_okimage_finish(const mcpplibs::cmdline::ParsedArgs& parsed, - std::span rest) { +// SEAL PUBLISHES AN ARTIFACT ONLY ONCE EVERY STEP ACCEPTED IT. The linked file +// is copied beside the artifact's name; each step runs on the copy, `{}` being +// the copy and `{needed}` the shared artifacts it needs (an option before it +// repeated for each); only then does the copy take the name. A step that fails +// leaves the previous artifact in place and no copy behind, names the step +// and prints what it said; the edge runs again next time. +export int cmd_platform_step(const mcpplibs::cmdline::ParsedArgs& parsed, + std::span rest) { namespace fs = std::filesystem; - const std::string okld = parsed.option_or_empty("okld").value(); - if (okld.empty() || rest.size() < 2) { - std::println(stderr, "error: okimage-finish needs --okld -- [needed…]"); - return 2; - } - const fs::path linked = rest[0], image = rest[1]; - auto tmp = image; tmp += ".tmp"; - std::error_code ec; - fs::copy_file(linked, tmp, fs::copy_options::overwrite_existing, ec); - if (ec) { - std::println(stderr, "error: cannot copy {} to {}: {}", linked.string(), tmp.string(), ec.message()); - return 1; - } - auto run = [&](std::vector argv) { - int spawnError = 0; - auto r = mcpp::run::capture_exec(argv, {}, {}, &spawnError); - if (spawnError != 0) { - std::println(stderr, "error: cannot start {}: {}", okld, - std::generic_category().message(spawnError)); + const std::string mode = parsed.positional_count() > 0 ? parsed.positional(0) : std::string(); + // The program's standard output, kept apart: its standard error is the + // build's, passed on (a warning it writes there is not part of what it + // produces, which `capture` saves as a source file). + auto run = [&](const std::vector& argv, std::string* out) { + namespace proc = mcpplibs::os::process; + proc::spawn_options o; + o.program = argv.front(); + o.args.assign(argv.begin() + 1, argv.end()); + std::string shown; + for (auto const& a : argv) shown += (shown.empty() ? "" : " ") + a; + auto r = proc::run(std::move(o)); + if (!r) { + std::println(stderr, "error: cannot start {}", shown); return false; } - if (r.exit_code != 0) { - std::print(stderr, "{}", r.output); + std::print(stderr, "{}", r->err); + if (!r->status.success()) { + // What a refusing step wrote is its reason (`okld check` names the + // import it refused), on either stream. + std::print(stderr, "{}", r->out); + std::println(stderr, "error: `{}` exited {}", shown, r->status.shell_code()); return false; } + if (out) *out = std::move(r->out); return true; }; - std::vector check{ okld, "check", tmp.string() }; - for (std::size_t i = 2; i < rest.size(); ++i) { check.push_back("--with"); check.push_back(rest[i]); } - if (!run({ okld, "note", tmp.string() }) || !run(check)) { - fs::remove(tmp, ec); - std::println(stderr, "error: {} does not meet the ok-image convention (openkal SPEC 12)", - image.filename().string()); - return 1; + std::error_code ec; + if (mode == "capture") { + const std::string out = parsed.option_or_empty("out").value(); + if (out.empty() || rest.empty()) { + std::println(stderr, "error: platform-step capture needs --out -- "); + return 2; + } + std::string text; + if (!run(std::vector(rest.begin(), rest.end()), &text)) return 1; + fs::create_directories(fs::path(out).parent_path(), ec); + auto tmp = fs::path(out); + tmp += ".tmp"; + { + std::ofstream f(tmp, std::ios::binary | std::ios::trunc); + f << text; + if (!f) { std::println(stderr, "error: cannot write {}", tmp.string()); return 1; } + } + fs::rename(tmp, out, ec); + if (ec) { std::println(stderr, "error: cannot publish {}: {}", out, ec.message()); return 1; } + return 0; + } + if (mode == "seal") { + const std::string hex = parsed.option_or_empty("steps-hex").value(); + if (hex.empty() || hex.size() % 2 != 0 || rest.size() < 2) { + std::println(stderr, "error: platform-step seal needs --steps-hex -- [needed…]"); + return 2; + } + std::string text; + for (std::size_t i = 0; i < hex.size(); i += 2) { + unsigned v = 0; + auto [p, e] = std::from_chars(hex.data() + i, hex.data() + i + 2, v, 16); + if (e != std::errc{} || p != hex.data() + i + 2) { + std::println(stderr, "error: platform-step seal: the steps are not hexadecimal"); + return 2; + } + text.push_back(static_cast(v)); + } + std::vector> steps; + { + std::istringstream in(text); + std::string line; + while (std::getline(in, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.empty()) continue; + std::vector step; + std::size_t from = 0; + for (;;) { + auto tab = line.find('\t', from); + step.push_back(line.substr(from, tab == std::string::npos ? std::string::npos : tab - from)); + if (tab == std::string::npos) break; + from = tab + 1; + } + steps.push_back(std::move(step)); + } + } + // A seal with no step would publish what it was asked to seal. + if (steps.empty()) { + std::println(stderr, "error: platform-step seal was given no step"); + return 2; + } + const fs::path linked = rest[0], artifact = rest[1]; + const std::vector needed(rest.begin() + 2, rest.end()); + auto tmp = artifact; + tmp += ".tmp"; + fs::copy_file(linked, tmp, fs::copy_options::overwrite_existing, ec); + if (ec) { + std::println(stderr, "error: cannot copy {} to {}: {}", linked.string(), tmp.string(), ec.message()); + return 1; + } + for (auto const& step : steps) { + if (!run(mcpp::platform::expand_step(step, tmp.string(), needed), nullptr)) { + fs::remove(tmp, ec); + std::println(stderr, "error: {} was not sealed: a step of its platform's " + "[platform.link] seal refused it", artifact.filename().string()); + return 1; + } + } + fs::rename(tmp, artifact, ec); + if (ec) { + std::println(stderr, "error: cannot publish {}: {}", artifact.string(), ec.message()); + return 1; + } + return 0; + } + std::println(stderr, "error: platform-step takes `capture` or `seal`"); + return 2; +} + +// `mcpp self env --target ` / `--host` (SPEC-013 §9): one configuration, +// prepared and not built -- the toolchain, the platform and its source, the +// tools it builds for this machine, the runner and where its program is. +export int cmd_env_configuration(const mcpplibs::cmdline::ParsedArgs& parsed) { + bool json = false; + if (auto f = parsed.value("format")) { + if (!mcpp::wire::parse_format(*f)) { + std::println(stderr, "error: {}", mcpp::wire::unsupported_format(*f)); + return 2; + } + json = true; } - fs::rename(tmp, image, ec); - if (ec) { - std::println(stderr, "error: cannot publish {}: {}", image.string(), ec.message()); + const std::string target = parsed.value("target").value_or(""); + const bool host = parsed.is_flag_set("host"); + const bool wasQuiet = mcpp::ui::is_quiet(); + if (json) mcpp::ui::set_quiet(true); + auto ctx = mcpp::build::prepare_configuration(host ? std::string_view{} : std::string_view(target)); + mcpp::ui::set_quiet(wasQuiet); + if (!ctx) { + if (json) { + mcpp::wire::Envelope env{.kind = "mcpp.env", .effects = {}}; + env.data = nullptr; + env.diagnostics.push_back({"MCPP_CONFIGURATION_FAILED", mcpp::wire::Severity::Error, + ctx.error()}); + std::println("{}", mcpp::wire::to_json(env).dump()); + } else { + mcpp::ui::error(ctx.error()); + } return 1; } + auto const& P = host ? ctx->hostPlatform : ctx->plan.platform; + // The graph reported: the host graph's triple and compiler with --host. + const std::string triple = host ? ctx->hostTriple : ctx->tc.targetTriple; + const std::string compiler = host ? ctx->hostCompiler : ctx->tc.binaryPath.generic_string(); + const std::string compilerOrigin = host ? ctx->hostCompilerOrigin : ctx->compilerChoice.origin; + auto const& runner = P.fields.runner; + std::string runnerProgram; + if (runner.set && !runner.cmd.empty()) { + // The platform's own tool when it is one (located by the build that + // used it), else the program the runner names on this machine. + if (auto it = P.tools.find(runner.cmd.front()); it != P.tools.end()) { + runnerProgram = it->second; + } else { + auto found = mcpp::build::runner_lookup::locate( + runner.cmd.front(), ctx->xlingsDepBinDirs, + mcpp::invocation::get("PATH").value_or("")); + if (found.program) runnerProgram = found.program->generic_string(); + } + } + if (json) { + nlohmann::json tools = nlohmann::json::object(); + for (auto const& [name, path] : P.tools) tools[name] = path; + nlohmann::json data{ + {"triple", triple}, + {"graph", host ? "host" : "target"}, + {"toolchain", {{"compiler", compiler}, {"origin", compilerOrigin}}}, + {"platform", nlohmann::json::parse(mcpp::platform::to_json(P), nullptr, false)}, + {"tools", std::move(tools)}, + {"runner", !runner.set ? nlohmann::json(nullptr) : nlohmann::json{ + {"argv", runner.cmd}, {"host", runner.host}, {"exclusive", runner.exclusive}, + {"program", runnerProgram.empty() ? nlohmann::json(nullptr) : nlohmann::json(runnerProgram)}, + {"source", mcpp::platform::origin_text(runner.origin)}}}, + {"runUnder", ctx->runUnder}, + }; + mcpp::wire::Envelope env{.kind = "mcpp.env", .effects = {}}; + env.data = std::move(data); + for (auto const& n : ctx->planNotes) + env.diagnostics.push_back({n.code, n.severity, n.message}); + mcpp::wire::emit(env); + return 0; + } + for (auto const& n : ctx->planNotes) mcpp::ui::warning(n.message); + std::println("{:<10} {}", host ? "host" : "target", triple); + std::println("{:<10} {} ({})", "compiler", compiler, compilerOrigin); + std::println("{:<10} {}{}", "platform", P.display(), + P.active ? std::format(" ({})", mcpp::platform::origin_text(P.selection.origin)) + : std::string()); + for (auto const& [label, value] : mcpp::platform::report_rows(P)) + std::println("{:<10} {:<17} {}", "", label, value); + for (auto const& [name, path] : P.tools) + std::println("{:<10} {:<17} {}", "tool", name, path); + if (runner.set) { + std::string argv; + for (auto const& a : runner.cmd) argv += (argv.empty() ? "" : " ") + a; + std::println("{:<10} {} ({}){}", "runner", argv, mcpp::platform::origin_text(runner.origin), + runnerProgram.empty() ? std::string(" -- not found on this machine") + : " -> " + runnerProgram); + } + if (!ctx->runUnder.empty()) { + std::string argv; + for (auto const& a : ctx->runUnder) argv += (argv.empty() ? "" : " ") + a; + std::println("{:<10} {} ({})", "run_under", argv, ctx->runUnderOrigin); + } + return 0; +} + + +// `mcpp toolchain install --target ` with no compiler named installs THE +// CONFIGURATION (SPEC-013 §7): the toolchain (already installed by the +// caller), then the platform package, the packages the platform adds and the +// tools it builds for this machine, so a build of the target works with +// `--offline` afterwards. +export int cmd_install_configuration(std::string_view target) { + auto ctx = mcpp::build::prepare_configuration(target); + if (!ctx) { mcpp::ui::error(ctx.error()); return 1; } + if (ctx->plan.platform.active) + mcpp::ui::info("Installed", std::format("{} for {}: {} tool(s) built for this machine", + ctx->plan.platform.display(), target, ctx->plan.platform.tools.size())); return 0; } diff --git a/src/cli/cmd_publish.cppm b/src/cli/cmd_publish.cppm index 89509e2f8..d57e15048 100644 --- a/src/cli/cmd_publish.cppm +++ b/src/cli/cmd_publish.cppm @@ -28,12 +28,53 @@ import mcpp.wire; import mcpp.home; // --list-formats plans outside the project import mcpp.pack.stage_tree; // the built-in formats import mcpp.toolchain.fingerprint; // hash_string +import mcpp.toolchain.platform; // host_arch: the static row of `--linkage static` +import mcpp.pack.compress; // --compress +import mcpp.toolchain.triple; // which --target values are images' rows +import mcpp.pack.distribution; // --merge +import mcpplibs.os.platform; namespace mcpp::cli { // `mcpp emit xpkg ...` — only one subcommand defined, so the action sits // directly on the `emit xpkg` nested subcommand and receives its ParsedArgs. export int cmd_emit_xpkg(const mcpplibs::cmdline::ParsedArgs& parsed) { + // `--package`: the distribution description of packages `mcpp pack` wrote + // (SPEC-014 §8), read from their mcpp-package.json. + if (parsed.option("package")) { + namespace d = mcpp::pack::distribution; + std::vector assets; + std::error_code ec; + auto made = d::work_directory("emit"); + if (!made) { mcpp::ui::error(made.error()); return 1; } + const auto work = *made; + for (auto const& p : parsed.option("package")->get().values) { + const std::filesystem::path file{p}; + // An entry names a published file, which a directory is not. + if (std::filesystem::is_directory(file, ec)) { + mcpp::ui::error(std::format("--package {} is a directory; an index entry names the " + "archive that is published (--pkg-format zip | tar)", p)); + std::filesystem::remove_all(work, ec); + return 2; + } + auto root = d::unpack(file, work / std::to_string(assets.size())); + if (!root) { mcpp::ui::error(root.error()); std::filesystem::remove_all(work, ec); return 2; } + auto pkg = d::read(*root); + if (!pkg) { mcpp::ui::error(pkg.error()); std::filesystem::remove_all(work, ec); return 2; } + assets.push_back({ file, std::move(*pkg), d::sha256_file(file), root->filename().string() }); + } + std::filesystem::remove_all(work, ec); + auto lua = d::xpkg_descriptor(assets, parsed.value("url").value_or("")); + if (!lua) { mcpp::ui::error(lua.error()); return 2; } + if (auto out = parsed.value("output")) { + std::ofstream os(*out, std::ios::binary | std::ios::trunc); + os << *lua; + if (!os) { mcpp::ui::error(std::format("cannot write '{}'", *out)); return 1; } + return 0; + } + std::print("{}", *lua); + return 0; + } return mcpp::publish::emit_xpkg_to( parsed.option_or_empty("version").value(), std::filesystem::path{parsed.option_or_empty("output").value()}, @@ -175,6 +216,27 @@ export int cmd_place_dlls(const mcpplibs::cmdline::ParsedArgs& parsed) { namespace { +// THE PACKAGE FORMAT IS `--pkg-format`. `--format` names the output encoding +// of a reporting command everywhere else in mcpp (`self env --format json`), +// and `mcpp pack`'s machine output is `--message-format`; pack's own +// `--format` is accepted as the earlier spelling and says so. +std::optional pack_format_option(const mcpplibs::cmdline::ParsedArgs& parsed) { + auto pkg = parsed.value("pkg-format"); + auto old = parsed.value("format"); + // Said once per command, however often the value is read. + static bool warned = false; + if (old && !std::exchange(warned, true)) { + if (pkg && *pkg != *old) { + mcpp::ui::warning(std::format( + "--format '{}' and --pkg-format '{}' disagree; --pkg-format is used", *old, *pkg)); + } else if (!pkg) { + mcpp::ui::warning("`mcpp pack --format` is the earlier spelling of --pkg-format, " + "the package format; --format names an output encoding elsewhere"); + } + } + return pkg ? pkg : old; +} + int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, mcpp::pack::PackOutcome* report, mcpp::pack::LibraryPackReport* libraryReport, @@ -307,18 +369,22 @@ export int cmd_pack(const mcpplibs::cmdline::ParsedArgs& parsed) { std::error_code ec; nlohmann::json t = nlohmann::json::array(); for (auto const& x : targets) t.push_back(x); + const bool dir = std::filesystem::is_directory(p, ec); return nlohmann::json{ {"path", std::filesystem::absolute(p, ec).lexically_normal().string()}, - {"type", std::filesystem::is_directory(p, ec) ? "directory" : "file"}, + {"type", dir ? "directory" : "file"}, {"format", format}, {"targets", std::move(t)}, + // SPEC-014 §9: the digest of a file, which a release publishes. + {"sha256", dir ? nlohmann::json(nullptr) + : nlohmann::json(mcpp::pack::distribution::sha256_file(p))}, }; }; nlohmann::json artifacts = nlohmann::json::array(); nlohmann::json stage = nullptr; nlohmann::json stages = nullptr; if (libraryRoute) { - const auto fmt = parsed.value("format").value_or("tar"); + const auto fmt = pack_format_option(parsed).value_or("tar"); artifacts.push_back(artifact_json(library.artifact, fmt, library.targets)); } else if (outcome.members.size() > 1) { // Several members (member selection design 2026-09-30, K1). Fields are @@ -352,6 +418,16 @@ export int cmd_pack(const mcpplibs::cmdline::ParsedArgs& parsed) { } env.data = nlohmann::json{{"artifacts", std::move(artifacts)}, {"stage", std::move(stage)}}; if (!stages.is_null()) env.data["stages"] = std::move(stages); + // The three axes and the legs the package was made with (SPEC-014 §9). + if (!libraryRoute) { + auto const& first = outcome.members.empty() ? mcpp::pack::PackOutcome::Member{} + : outcome.members.front(); + env.data["linkage"] = outcome.linkage.empty() ? first.linkage : outcome.linkage; + env.data["mode"] = outcome.mode.empty() ? first.mode : outcome.mode; + env.data["pkgFormat"] = outcome.format; + env.data["legs"] = outcome.targets; + env.data["systems"] = outcome.systems; + } mcpp::wire::emit(env); return 0; } @@ -506,20 +582,188 @@ int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, // exists to remove. The refusal moves to `build_and_pack`, after build // programs have declared what they provide and before anything is // compiled, where it can name what IS available instead of a fixed list. - if (auto v = parsed.value("format")) { - if (*v == "tar") opts.format = mcpp::pack::Format::Tar; - else if (*v == "dir") opts.format = mcpp::pack::Format::Dir; - else if (v->empty()) { - mcpp::ui::error("--format needs a value: tar | dir | a format the " - "resolved graph provides"); - return 2; - } else { + // THE PROJECT'S DEFAULTS OF THE THREE AXES (`[pack]`, SPEC-014 §1), read + // from the package this command packs; a flag states over each. + mcpp::manifest::PackConfig packCfg; + std::string projectTarget; // `[build] target`: the project's own default triple + if (auto root = mcpp::project::find_manifest_root(std::filesystem::current_path())) + if (auto eff = mcpp::project::load_effective_manifest(*root)) { + packCfg = eff->manifest.packConfig; + projectTarget = eff->manifest.buildConfig.target; + } + if (parsed.option("merge")) { + std::vector inputs; + for (auto const& v : parsed.option("merge")->get().values) { + std::size_t at = 0; + while (at <= v.size()) { + auto comma = v.find(',', at); + if (comma == std::string::npos) comma = v.size(); + if (comma > at) inputs.push_back(v.substr(at, comma - at)); + at = comma + 1; + } + } + mcpp::pack::distribution::MergeRequest req; + for (auto const& i : inputs) req.inputs.emplace_back(i); + if (auto v = parsed.value("output")) req.output = *v; + if (auto f = pack_format_option(parsed)) req.format = *f; + else if (!packCfg.format.empty()) req.format = packCfg.format; + // Only the flag's compression is the command's (a project's `[pack] + // compress` describes that project's packs, not a merge of packages); + // `merge` decides the container and refuses a compression that does + // not fit it. + if (auto text = parsed.value("compress")) { + auto c = mcpp::pack::compress::parse(*text); + if (!c) { mcpp::ui::error(std::format("--compress: {}", c.error())); return 2; } + req.compression = *c; + } + auto merged = mcpp::pack::distribution::merge(req); + if (!merged) { mcpp::ui::error(merged.error()); return 1; } + mcpp::ui::status("Packed", merged->string()); + if (report) { + report->rc = 0; + report->artifacts.push_back(*merged); + std::error_code mec; + const auto name = merged->filename().string(); + report->format = std::filesystem::is_directory(*merged, mec) ? std::string("dir") + : name.ends_with(".zip") ? std::string("zip") : std::string("tar"); + // What the merged package says of itself. + if (auto work = mcpp::pack::distribution::work_directory("merged")) { + if (auto root = mcpp::pack::distribution::unpack(*merged, *work / "tree")) + if (auto pkg = mcpp::pack::distribution::read(*root)) { + report->targets = pkg->legs; + report->systems = pkg->systems; + report->linkage = pkg->linkage; + report->mode = pkg->mode; + } + std::filesystem::remove_all(*work, mec); + } + } + return 0; + } + + std::string formatText; + if (auto v = pack_format_option(parsed)) formatText = *v; + else formatText = packCfg.format; + if (auto v = pack_format_option(parsed); v && v->empty()) { + mcpp::ui::error("--pkg-format needs a value: tar | zip | dir | single-file | a format " + "the resolved graph provides"); + return 2; + } + if (!formatText.empty()) { + opts.formatStated = true; + if (formatText == "tar") opts.format = mcpp::pack::Format::Tar; + else if (formatText == "dir") opts.format = mcpp::pack::Format::Dir; + else if (formatText == "zip") opts.format = mcpp::pack::Format::Zip; + else if (formatText == "single-file") opts.format = mcpp::pack::Format::SingleFile; + else { opts.format = mcpp::pack::Format::Dispatched; - opts.formatName = *v; + opts.formatName = formatText; } } if (auto v = parsed.value("output")) opts.output = outputFromUser ? outputFromUser->string() : *v; + // ─── Linkage, compression, universal, systems, signer ──────────── + { + std::string linkage = parsed.value("linkage").value_or(packCfg.linkage); + if (!linkage.empty()) { + if (linkage == "static") opts.linkage = mcpp::pack::Linkage::Static; + else if (linkage == "shared") opts.linkage = mcpp::pack::Linkage::Shared; + else { + mcpp::ui::error(std::format("invalid --linkage '{}'; expected: static | shared", + linkage)); + return 2; + } + } + // `--mode static` IS `--linkage static --mode self-contained` (SPEC-014 + // §1): the program carries its libraries, and the machine provides + // nothing. + if (opts.mode == mcpp::pack::Mode::Static) { + if (opts.linkage == mcpp::pack::Linkage::Shared) { + mcpp::ui::error("--mode static is --linkage static --mode self-contained; " + "--linkage shared contradicts it.\n" + " use: --linkage shared --mode self-contained"); + return 2; + } + opts.linkage = mcpp::pack::Linkage::Static; + } + // `--compress` states the container's compression; `[pack] compress` + // is the project's default, which a container it does not fit (a + // directory, a zip given zstd, a single file given xz) leaves aside. + const auto compressFlag = parsed.value("compress"); + std::string compressText = compressFlag.value_or(packCfg.compress); + if (!compressText.empty()) { + auto c = mcpp::pack::compress::parse(compressText); + if (!c) { + mcpp::ui::error(std::format("{}: {}", compressFlag ? "--compress" : "[pack] compress", + c.error())); + return 2; + } + opts.compression = *c; + opts.compressionFromFlag = compressFlag.has_value(); + } + using mcpp::pack::compress::Algorithm; + const auto alg = opts.compression.algorithm; + const bool fits = + !(opts.format == mcpp::pack::Format::Dir || opts.format == mcpp::pack::Format::Dispatched) + && !(opts.format == mcpp::pack::Format::Zip && (alg == Algorithm::Zstd || alg == Algorithm::Xz)) + && !(opts.format == mcpp::pack::Format::SingleFile && (alg == Algorithm::Gzip || alg == Algorithm::Xz)); + if (opts.compression.stated && !compressFlag && !fits) opts.compression = {}; + if (opts.compression.stated) { + if (opts.format == mcpp::pack::Format::Dir || opts.format == mcpp::pack::Format::Dispatched) { + mcpp::ui::error(std::format( + "--compress {} compresses a container, and --pkg-format {} writes none.\n" + " use: --pkg-format tar | zip | single-file", + mcpp::pack::compress::name(alg), + opts.format == mcpp::pack::Format::Dir ? std::string("dir") : opts.formatName)); + return 2; + } + if (opts.format == mcpp::pack::Format::Zip && (alg == Algorithm::Zstd || alg == Algorithm::Xz)) { + mcpp::ui::error(std::format( + "a zip's members are deflated (gzip) or stored (none); --compress {} is a " + "tar's.\n use: --pkg-format tar --compress {}, or --compress gzip | none", + mcpp::pack::compress::name(alg), mcpp::pack::compress::name(alg))); + return 2; + } + if (opts.format == mcpp::pack::Format::SingleFile + && (alg == Algorithm::Gzip || alg == Algorithm::Xz)) { + mcpp::ui::error(std::format( + "a single file's payload is zstd (or none): its launcher decompresses one " + "codec.\n use: --compress zstd[:] | none")); + return 2; + } + } else if (opts.format == mcpp::pack::Format::SingleFile) { + opts.compression.algorithm = Algorithm::Zstd; + } + if (parsed.is_flag_set("no-universal")) opts.universal = false; + else opts.universal = packCfg.universal; + if (auto v = parsed.value("systems")) { + std::size_t at = 0; + while (at <= v->size()) { + auto comma = v->find(',', at); + if (comma == std::string::npos) comma = v->size(); + auto word = v->substr(at, comma - at); + if (!word.empty()) { + if (word != "linux" && word != "macos" && word != "windows") { + mcpp::ui::error(std::format( + "--systems names '{}'; the systems are linux, macos and windows", word)); + return 2; + } + if (std::ranges::find(opts.systems, word) == opts.systems.end()) + opts.systems.push_back(word); + } + at = comma + 1; + } + } + opts.sign = packCfg.sign; + // A single file carries what it runs: the machine provides nothing. + if (opts.format == mcpp::pack::Format::SingleFile && opts.mode == mcpp::pack::Mode::None) { + mcpp::ui::error("--pkg-format single-file carries what the program runs, and --mode " + "system leaves it to the machine.\n" + " use: --mode vendored | self-contained | static"); + return 2; + } + } + // `value()`, not `option_or_empty()`, for `profile` — and NOT for // anything whose name a positional shares. `ParsedArgs::value()` falls back // to a same-named positional when the option is unset, which is how @@ -543,7 +787,30 @@ int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, // form is not a shared object. std::vector triples; if (auto o = parsed.option("target")) triples = o->get().values; + // `--linkage static` OF A LINUX PROGRAM IS THE MUSL ROW (SPEC-014 §1): a + // program with no shared library at all, the C library's included, which + // is what `--mode static` has always built. On another system the + // libraries of the graph are linked in and the system's stay its own. + if (opts.linkage == mcpp::pack::Linkage::Static) { + // This machine's row only when the project names no target of its own. + if (triples.empty() && projectTarget.empty()) { + if constexpr (mcpplibs::os::is_linux) + triples.push_back(std::format("{}-linux-musl", mcpp::toolchain::platform::host_arch)); + } else { + for (auto& t : triples) + if (auto at = t.find("-linux-gnu"); at != std::string::npos) + t.replace(at, 10, "-linux-musl"); + } + } if (!triples.empty()) opts.targetTriple = triples.back(); + // A program with no shared library is packed whole whatever the mode + // asked: there is nothing beside it to vendor, and nothing for the machine + // to provide. + if (opts.linkage == mcpp::pack::Linkage::Static + && opts.targetTriple.find("-linux-musl") != std::string::npos) { + opts.mode = mcpp::pack::Mode::Static; + modeFromUser = true; + } // ─── Several members ───────────────────────────────────────────── // @@ -604,12 +871,12 @@ int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, // a library package while the user asked for an installer. if (opts.format == mcpp::pack::Format::Dispatched) { mcpp::ui::error(std::format( - "--format {} is a distributable produced from a program's staged " + "--pkg-format {} is a distributable produced from a program's staged " "bundle, and '{}' is a library target.\n" " A library package ships an interface plus prebuilt binaries " "per triple; there is no\n" " single staged tree to hand a distribution format.\n" - " use: --format tar | dir, or name a program target", + " use: --pkg-format tar | dir, or name a program target", opts.formatName, route->targetName)); return 2; } @@ -622,6 +889,19 @@ int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, return mcpp::pack::build_and_pack_library(route->targetName, triples, opts, libraryReport); } + // SEVERAL IMAGES' TARGETS ARE ONE UNIVERSAL PACKAGE'S LEGS (SPEC-014 + // §1.4): the pack builds the first and the others as its legs; whether the + // platform packs them into one package is decided once it is known. + if (triples.size() > 1 && std::ranges::all_of(triples, [](std::string const& t) { + auto p = mcpp::toolchain::triple::parse(t); + return p && p->loader_supplies_os(); + })) { + opts.legTargets = triples; + opts.targetTriple = triples.front(); + auto out = mcpp::pack::build_and_pack(std::move(opts), modeFromUser, route->targetName); + if (report) *report = out; + return out.rc; + } if (triples.size() > 1) { // #630 A9: THE ROUTE IS CHOSEN BY THE ARTIFACT'S FORM, NOT BY THE // TARGET'S KIND. An `app` whose resolved link form is a shared diff --git a/src/config.cppm b/src/config.cppm index f834615b3..1b4067740 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -24,6 +24,8 @@ import std; import mcpp.home; import mcpp.modgraph.glob; // try_narrow: the one UTF-8 spelling of a path import mcpp.libs.toml; +import mcpp.manifest.platform; +import mcpp.manifest.platform_toml; import mcpp.libs.json; import mcpp.pm.index_spec; import mcpp.provider; @@ -177,6 +179,12 @@ struct GlobalConfig { std::filesystem::path runUnderFile; }; std::map targetDefaults; + // This machine's platform choices and overrides (SPEC-013 §3, §5): the + // platform part of each `[target.]` table, the system file's + // before the user's (a later one applies later), and `[host]`, the host + // graph's platform. A project's mcpp.toml outranks them. + std::vector platformOverrides; + mcpp::manifest::platform::Override hostPlatform; // Resolved xlings home (registryDir unless overridden) std::filesystem::path xlingsHome() const { @@ -371,7 +379,9 @@ parse_payload_overrides(const mcpp::libs::toml::Document& doc); std::expected parse_target_defaults(const mcpp::libs::toml::Document& doc, const std::filesystem::path& file, - std::map& into); + std::map& into, + std::vector* platforms = nullptr, + mcpp::manifest::platform::Override* host = nullptr); // Normalize legacy index naming in a loaded config (exported for tests): // org migration mcpp-community/mcpp-index -> mcpplibs/mcpp-index, index name @@ -718,16 +728,54 @@ void canonicalize_legacy_index_names(GlobalConfig& cfg) { std::expected parse_target_defaults(const mcpp::libs::toml::Document& doc, const std::filesystem::path& file, - std::map& into) { + std::map& into, + std::vector* platforms, + mcpp::manifest::platform::Override* host) { + namespace mp = mcpp::manifest::platform; + // What the reader warns of (an unknown key, SPEC-013 §18.1), said once + // per process for each file: the configuration is read by every prepare. + struct Said { + const std::filesystem::path& file; + std::vector warnings; + ~Said() { + static std::mutex m; + static std::set said; + std::lock_guard lock(m); + for (auto const& w : warnings) + if (said.insert(file.string() + '\n' + w).second) + std::println(stderr, "warning: {}: {}", file.string(), w); + } + } said{ file, {} }; + const mp::Reader reader{file, mp::Tier::Config, &said.warnings}; + // `[host]`: the platform of this machine's host graph (SPEC-013 §6.1). A + // nearer file's choice stands. + if (auto* ht = doc.get_table("host"); ht && host && !host->platformSet) { + auto ov = reader.override_(*ht, ""); + if (!ov) return std::unexpected(std::format("{} {}", file.string(), ov.error())); + *host = std::move(*ov); + } auto* tt = doc.get_table("target"); if (!tt) return {}; + std::vector ours; for (auto const& [triple, body] : *tt) { if (!body.is_table()) return std::unexpected(std::format( "{} [target.{}] must be a table with `sysroot` and/or `run_under`", file.string(), triple)); auto& d = into[triple]; + // The platform keys (SPEC-013 §5.2), read by the reader a manifest uses. + bool platformKeys = false; + for (auto k : mp::kOverrideKeys) platformKeys |= body.as_table().contains(k); + platformKeys |= body.as_table().contains("runner") || body.as_table().contains("runners"); + if (platformKeys && platforms) { + auto ov = reader.override_(body.as_table(), triple); + if (!ov) return std::unexpected(std::format("{} {}", file.string(), ov.error())); + ours.push_back(std::move(*ov)); + } for (auto const& [k, v] : body.as_table()) { + if (std::ranges::find(mp::kOverrideKeys, k) != std::end(mp::kOverrideKeys) + || k == "runner" || k == "runners") + continue; if (k == "sysroot") { if (d.sysrootDeclared) continue; // a nearer file said it std::string text; @@ -781,11 +829,15 @@ parse_target_defaults(const mcpp::libs::toml::Document& doc, } else { return std::unexpected(std::format( "{} [target.{}]: '{}' is not a key of a machine's target defaults; " - "the keys are `sysroot` and `run_under` (everything else about a " - "target belongs in the project's mcpp.toml)", file.string(), triple, k)); + "the keys are `sysroot`, `run_under` and the platform's: platform, layers, " + "tools, carrier, link, runner, runners (everything else about a target " + "belongs in the project's mcpp.toml)", file.string(), triple, k)); } } } + // The file read first is the nearer one; a later override applies later, + // so the nearer file's tables go after those already held. + if (platforms) platforms->insert(platforms->begin(), ours.begin(), ours.end()); return {}; } @@ -984,11 +1036,13 @@ std::expected load_or_init( else return std::unexpected(ConfigError{overrides.error()}); // [target.]: this machine's defaults, the user's first. - if (auto r = parse_target_defaults(*doc, cfg.configFile, cfg.targetDefaults); !r) + if (auto r = parse_target_defaults(*doc, cfg.configFile, cfg.targetDefaults, + &cfg.platformOverrides, &cfg.hostPlatform); !r) return std::unexpected(ConfigError{r.error()}); if (systemDoc) if (auto r = parse_target_defaults(*systemDoc, mcpp::home::system_config_file(), - cfg.targetDefaults); !r) + cfg.targetDefaults, &cfg.platformOverrides, + &cfg.hostPlatform); !r) return std::unexpected(ConfigError{r.error()}); // A read-only report must not create its log directory either. diff --git a/src/doctor.cppm b/src/doctor.cppm index 81c422c9a..fecbaba81 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -1355,8 +1355,12 @@ export int why_toolchain_json(std::string_view target, std::string_view tcSpec) {"layer", std::string(label)}, {"interface", l.interfaceName}, {"impl", l.impl}, + // `platform` for a layer the target's platform added (SPEC-013 + // §8), as the report says it. {"origin", l.absent() ? std::string("none") + : l.byPlatform + ? std::string("platform") : std::string(mcpp::targetside::origin_name(l.origin))}, {"subset", l.subset}, }; @@ -1797,6 +1801,30 @@ export int explain_code(std::string_view code) { "silently misbehave, so resolution stops instead. Upgrade mcpp:\n" " curl -fsSL https://github.com/mcpp-community/mcpp/releases/latest/download/install.sh | bash\n" "To bypass for debugging only: MCPP_INDEX_FLOOR=ignore mcpp build"}, + // The platform's refusals (SPEC-013 §14), by their reason token. + {"platform-not-a-platform", "the selected package states no platform", + "The package selected as the target's platform (--platform, MCPP_PLATFORM,\n" + "[target.] platform, config.toml, the target table) has no [platform]\n" + "section. Select a platform package, or none: --platform none."}, + {"platform-target-unserved", "the platform does not state this target", + "The platform's [platform] targets (and [platform.hosted] targets) do not name\n" + "the target being built. Build one of the targets it names, or select another\n" + "platform for this target under [target.] platform."}, + {"platform-tool-absent", "a tool the platform names is not available", + "The platform links or seals with a tool ([platform.tools]) that the graph did\n" + "not build and this machine does not have. Name the package that builds it:\n" + " [target.'cfg(platform = \"\")'.tools]\n" + " = \"@\""}, + {"platform-version-group", "a family package outside the platform's version group", + "The platform's [platform.versions] names one version of each package of its\n" + "family, whose pins are exact; the graph holds another version of one. Select\n" + "the platform release of that version, or write the group's version where the\n" + "package is requested. `mcpp why` names who requested it."}, + {"platform-phase-directive", "a platform plugin stated what a platform cannot", + "A platform's build.mcpp runs in the platform phase (mcpp::phase() == \"platform\")\n" + "and states the platform's build, link and runner fields and the package formats\n" + "it provides (pack-format, artifact actions). Move any other directive to a\n" + "package of the graph."}, }; for (auto& e : table) { if (e.code == code) { diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index 5c6a10228..4e688d7a1 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -185,6 +185,22 @@ inline std::filesystem::path resolve_lld(const std::filesystem::path& driver) { return {}; } +// THE ELF LINKER OF A PAYLOAD, under any of the names lld answers to: +// `ld.lld`, the generic `lld`, or `lld-link` -- one program that takes its +// flavour from its name or from a leading `-flavor`. A payload that ships the +// COFF name alone (the llvm payload for Windows) still links an ELF file with +// `-flavor gnu`, which the caller adds unless the name is `ld.lld`. +inline std::filesystem::path resolve_lld_any(const std::filesystem::path& driver) { + if (auto p = resolve_lld(driver); !p.empty()) return p; + if (driver.empty()) return {}; + std::error_code ec; + for (const char* name : { "lld-link", "lld-link.exe" }) { + auto p = driver.parent_path() / name; + if (std::filesystem::exists(p, ec)) return p; + } + return {}; +} + // llvm-objcopy inside the same payload as the driver, for the flat image a // flasher takes. Derived from the driver's path for the same reason // `resolve_lld` is: a search is how the wrong tool gets picked. Returns "" for diff --git a/src/okimage/okimage.cppm b/src/okimage/okimage.cppm deleted file mode 100644 index 4f9490900..000000000 --- a/src/okimage/okimage.cppm +++ /dev/null @@ -1,124 +0,0 @@ -// mcpp.okimage — what an ok image IS, for the target `-openkal` -// (openkal SPEC clause 12; design 2026-10-26 part 2, §8.5). -// -// One artifact per architecture, which a loader (`okrun`) runs on every system -// of that architecture. The facts below are the convention's, stated once: -// the compile flags that keep an image runnable everywhere, the link that -// produces the shape the loader accepts, and the note the loader reads. The -// format itself -- its reader, its checker, its signature -- is libokld's, and -// the engine reaches it through `okld`, a program of the dependency graph, -// never through a second implementation here. -// -// WHAT IS NOT HERE: the C library and the C++ run-time inside an image. They -// are packages (`provides = ["mcpp:c-abi=…"]`), and an image with neither is -// legal: its entry is its own `ok_main`. - -export module mcpp.okimage; - -import std; - -export namespace mcpp::okimage { - -// The entry of an executable image (SPEC 12.2): `int ok_main(void)`, in the -// image's convention, whose value is the image's status. A C library inside -// the image defines it and calls `main` with `kal_env_arg`'s arguments. -inline constexpr std::string_view kEntry = "ok_main"; - -// The programs of the graph the engine runs for this target. -inline constexpr std::string_view kTool = "okld"; // note, check -inline constexpr std::string_view kRunner = "okrun"; // `mcpp run`, `mcpp test` - -// FLAGS EVERY TRANSLATION UNIT OF AN IMAGE CARRIES (SPEC 12.2). -// -// -fPIC the image is ET_DYN and has no text relocation; -// -femulated-tls a variable is found through `__emutls_get_address`, -// which the image supplies: one system does not let a -// program set the thread pointer, another does not let it -// change it; -// -ffixed-x18 (AArch64) two systems reserve the platform register; -// -mno-outline-atomics -// (AArch64) the outlined helpers read the processor's -// features through an operating system's interface; -// -fvisibility=hidden -// an image exports what its source marks -// `visibility("default")` and nothing else, as the -// reference build does: everything else is the image's -// own, so the link can drop what nothing reaches, and the -// requirements note names only the interfaces the program -// uses, not every one its C library could. -// -fasynchronous-unwind-tables -// every frame, C's included, can be unwound: a C++ -// exception crosses the C of the unwinder itself -// (`_Unwind_RaiseException`) and of the C library. The -// hosted triples default to it; `-unknown-none-elf` -// does not -- measured, `_Unwind_Backtrace` in an image -// over openkal-llvm-runtime found no frame at all, and -// every `throw` terminated. -// -Xclang -fstack-clash-protection -// a frame larger than a page is probed page by page: one -// system commits a thread's stack as its guard page is -// touched, and a frame that steps over the guard faults -- -// measured, an image's 64 KiB buffer ended it on Windows -// and nowhere else. Passed to the front end because the -// driver enables the option for hosted triples only. -inline std::vector compile_flags(std::string_view arch) { - std::vector out{ "-fPIC", "-femulated-tls", "-fvisibility=hidden", - "-fasynchronous-unwind-tables", - "-Xclang", "-fstack-clash-protection" }; - if (arch == "aarch64") { - out.emplace_back("-ffixed-x18"); - out.emplace_back("-mno-outline-atomics"); - } - return out; -} - -// THE LINK, BY `ld.lld` ITSELF (SPEC 12.2). Position-independent, every -// loadable segment aligned to 64 KiB (the largest protection granularity among -// the systems), bound now, no executable stack, both hash tables (a loader may -// use either), what nothing reaches discarded, and the unwind index an -// unwinder finds through `dl_iterate_phdr`. -// -// Measured, a C program over openkal-musl before `--gc-sections` and hidden -// visibility: the whole C library was in the image and exported, and `okld -// check` refused it for importing `__mulxc3` -- from complex arithmetic the -// program never called. -inline std::string link_flags() { - return " -shared -z max-page-size=65536 -z now -z noexecstack --hash-style=both" - " --gc-sections --eh-frame-hdr"; -} - -// The notes every image carries (SPEC 12.4): the requirements, which `okld -// note` writes from the image's strong imports after the link, and the -// signature, all zero until `okld sign`. The same text `okld note-source` -// prints. -inline std::string_view note_source() { - return - "# The two notes of an ok image (openkal SPEC 12.4), written by mcpp.\n" - "\t.section .note.openkal,\"a\",@note\n" - "\t.p2align 2\n" - "\t.long 8, 16, 1\n" - "\t.asciz \"OPENKAL\"\n" - "\t.long 1, 0\n" - "\t.quad 0\n" - "\t.p2align 2\n" - "\t.long 8, 96, 2\n" - "\t.asciz \"OPENKAL\"\n" - "\t.fill 96, 1, 0\n"; -} - -// The diagnostic for a build of this target with no `okld` to finish it. It -// names the dependency that supplies both programs, and where to write it. -inline std::string no_tool_message(std::string_view triple, std::string_view tool) { - return std::format( - "'{}' builds ok images, and `{}` -- the format's tool, which writes an " - "image's requirements note and checks the convention -- is not provided.\n" - " Declare the package that builds it for this machine:\n" - "\n" - " [build-dependencies]\n" - " openkal-tools = {{ version = \"0.1.0\", tools = [\"okld\", \"okrun\"] }}\n" - "\n" - " `okrun` is then the runner of `mcpp run` and `mcpp test` for this target.", - triple, tool); -} - -} // namespace mcpp::okimage diff --git a/src/pack/compress.cppm b/src/pack/compress.cppm new file mode 100644 index 000000000..3e01ace24 --- /dev/null +++ b/src/pack/compress.cppm @@ -0,0 +1,184 @@ +// mcpp.pack.compress — the compression of a package's container (SPEC-014 §5). +// +// `mcpp pack` writes an archive in one of three compressions or none, chosen by +// `--compress [:]` or `[pack] compress`: gzip (the default of +// `tar`, as every earlier release wrote it), zstd and xz. A zip's members are +// deflated or stored; a single file's payload is zstd. The codecs are the +// reference libraries (zlib, zstd, liblzma), dependencies of mcpp, so a +// package is written the same way on every host and nothing is looked for on +// PATH. +// +// Deterministic by construction: a gzip member carries no name, no time and the +// system byte 255 (unknown) whatever host wrote it, a zstd frame states its +// content size and its checksum, an xz stream checks with CRC64. Two packs of +// one tree produce the same bytes, on every host. + +export module mcpp.pack.compress; + +import std; + +export namespace mcpp::pack::compress { + +enum class Algorithm { None, Gzip, Zstd, Xz }; + +struct Choice { + Algorithm algorithm = Algorithm::Gzip; + // -1: the codec's own default (gzip 6, zstd 19, xz 6). Any other value is + // the level stated, honoured as it is: `xz:0` is xz's fastest preset. + int level = -1; + bool stated = false; // written by the user (flag or manifest) +}; + +// `none`, `gzip`, `zstd`, `xz`, each optionally `:` (gzip 1-9, zstd +// 1-22, xz 0-9). +std::expected parse(std::string_view text); + +std::string_view name(Algorithm a); + +// The extension of a tar archive under `a`: `.tar.gz`, `.tar.zst`, `.tar.xz`, +// `.tar`. +std::string_view tar_extension(Algorithm a); + +// The whole of `in`, compressed to `out` under `c` (a gzip member, a zstd +// frame, an xz stream; a copy for None). +std::expected file(const std::filesystem::path& in, + const std::filesystem::path& out, Choice c); + +// `in`, compressed under `c`. +std::expected bytes(std::string_view in, Choice c); + +// `in` as raw DEFLATE (a zip member's method 8), at `level` (-1: zlib's +// default). +std::expected deflate_raw(std::string_view in, int level); + +// What `bytes` compressed under `a`: a gzip member, a zstd frame, an xz +// stream, or the bytes themselves. `--merge` reads the archives it merges so. +std::expected decompress(std::string_view in, Algorithm a); + +// A raw DEFLATE stream's content, `size` bytes long (a zip member's method 8). +// A stream that holds more than `size` bytes is refused once it passes them, +// not inflated to its end: the size is the archive's word for the member, and +// a reader trusts no more of it than that. +std::expected inflate_raw(std::string_view in, std::size_t size); + +} // namespace mcpp::pack::compress + +// The codecs themselves, in compress_codecs.cpp: the only unit that sees the +// libraries' headers. +namespace mcpp::pack::compress::codec { +std::expected gzip(std::string_view in, int level); +std::expected zstd(std::string_view in, int level); +std::expected xz(std::string_view in, int level); +std::expected deflate(std::string_view in, int level); +std::expected unzstd(std::string_view frame); +std::expected gunzip(std::string_view in); +std::expected unxz(std::string_view in); +std::expected inflate(std::string_view in, std::size_t size); +} // namespace mcpp::pack::compress::codec + +namespace mcpp::pack::compress { + +std::string_view name(Algorithm a) { + switch (a) { + case Algorithm::None: return "none"; + case Algorithm::Gzip: return "gzip"; + case Algorithm::Zstd: return "zstd"; + case Algorithm::Xz: return "xz"; + } + return "gzip"; +} + +std::string_view tar_extension(Algorithm a) { + switch (a) { + case Algorithm::None: return ".tar"; + case Algorithm::Gzip: return ".tar.gz"; + case Algorithm::Zstd: return ".tar.zst"; + case Algorithm::Xz: return ".tar.xz"; + } + return ".tar.gz"; +} + +std::expected parse(std::string_view text) { + Choice c; + c.stated = true; + std::string_view alg = text; + std::string_view lvl; + if (auto colon = text.find(':'); colon != std::string_view::npos) { + alg = text.substr(0, colon); + lvl = text.substr(colon + 1); + } + struct Row { std::string_view word; Algorithm a; int lo, hi; }; + constexpr Row rows[] = { + {"none", Algorithm::None, 0, 0}, + {"gzip", Algorithm::Gzip, 1, 9}, + {"zstd", Algorithm::Zstd, 1, 22}, + {"xz", Algorithm::Xz, 0, 9}, + }; + const Row* row = nullptr; + for (auto const& r : rows) if (r.word == alg) row = &r; + if (!row) + return std::unexpected(std::format( + "compression '{}' is not one of none | gzip | zstd | xz (each with an optional " + ":)", alg)); + c.algorithm = row->a; + if (!lvl.empty()) { + if (row->a == Algorithm::None) + return std::unexpected(std::string("compression 'none' takes no level")); + int v = 0; + auto [p, ec] = std::from_chars(lvl.data(), lvl.data() + lvl.size(), v); + if (ec != std::errc{} || p != lvl.data() + lvl.size() || v < row->lo || v > row->hi) + return std::unexpected(std::format( + "the level of {} is {} to {}; '{}' is not", row->word, row->lo, row->hi, lvl)); + c.level = v; + } + return c; +} + +std::expected bytes(std::string_view in, Choice c) { + switch (c.algorithm) { + case Algorithm::None: return std::string(in); + case Algorithm::Gzip: return codec::gzip(in, c.level); + case Algorithm::Zstd: return codec::zstd(in, c.level); + case Algorithm::Xz: return codec::xz(in, c.level); + } + return std::string(in); +} + +std::expected file(const std::filesystem::path& in, + const std::filesystem::path& out, Choice c) { + std::string data; + { + std::ifstream is(in, std::ios::binary); + if (!is) return std::unexpected(std::format("cannot read '{}'", in.string())); + data.assign(std::istreambuf_iterator(is), {}); + } + auto packed = bytes(data, c); + if (!packed) return std::unexpected(std::format("{}: {}", in.string(), packed.error())); + std::error_code ec; + if (out.has_parent_path()) std::filesystem::create_directories(out.parent_path(), ec); + std::ofstream os(out, std::ios::binary | std::ios::trunc); + if (!os) return std::unexpected(std::format("cannot write '{}'", out.string())); + os.write(packed->data(), static_cast(packed->size())); + if (!os) return std::unexpected(std::format("failed writing '{}'", out.string())); + return {}; +} + +std::expected deflate_raw(std::string_view in, int level) { + return codec::deflate(in, level); +} + +std::expected decompress(std::string_view in, Algorithm a) { + switch (a) { + case Algorithm::None: return std::string(in); + case Algorithm::Gzip: return codec::gunzip(in); + case Algorithm::Zstd: return codec::unzstd(in); + case Algorithm::Xz: return codec::unxz(in); + } + return std::string(in); +} + +std::expected inflate_raw(std::string_view in, std::size_t size) { + return codec::inflate(in, size); +} + +} // namespace mcpp::pack::compress diff --git a/src/pack/compress_codecs.cpp b/src/pack/compress_codecs.cpp new file mode 100644 index 000000000..6de8bac42 --- /dev/null +++ b/src/pack/compress_codecs.cpp @@ -0,0 +1,186 @@ +// The codecs of mcpp.pack.compress: zlib (gzip, raw deflate), zstd and liblzma +// (xz), each through its one-shot interface. The only unit that includes the +// libraries' headers. +module; +#include +#include +#include + +module mcpp.pack.compress; + +import std; + +namespace mcpp::pack::compress::codec { + +namespace { + +// zlib's deflate with `windowBits`: 15 + 16 is a gzip member, -15 is raw +// DEFLATE. A gzip member's header is stated rather than left to zlib, whose own +// header names the system zlib was built for (3 on Unix, 10 or 11 on Windows): +// no name, no time, the system 255 (unknown), so the same tree is the same +// bytes on every host. +std::expected zlib_deflate(std::string_view in, int level, + int windowBits) { + z_stream s{}; + const int lvl = level < 0 ? Z_DEFAULT_COMPRESSION : level; + if (deflateInit2(&s, lvl, Z_DEFLATED, windowBits, 8, Z_DEFAULT_STRATEGY) != Z_OK) + return std::unexpected(std::string("zlib: deflateInit2 failed")); + gz_header header{}; + header.os = 255; + if (windowBits > 15 && deflateSetHeader(&s, &header) != Z_OK) { + deflateEnd(&s); + return std::unexpected(std::string("zlib: deflateSetHeader failed")); + } + std::string out; + out.resize(deflateBound(&s, static_cast(in.size())) + 64); + s.next_in = reinterpret_cast(const_cast(in.data())); + s.avail_in = static_cast(in.size()); + s.next_out = reinterpret_cast(out.data()); + s.avail_out = static_cast(out.size()); + const int rc = ::deflate(&s, Z_FINISH); + const auto written = s.total_out; + deflateEnd(&s); + if (rc != Z_STREAM_END) return std::unexpected(std::format("zlib: deflate returned {}", rc)); + out.resize(written); + return out; +} + +} // namespace + +std::expected gzip(std::string_view in, int level) { + if (in.size() > std::numeric_limits::max()) + return std::unexpected(std::string("gzip: an input over 4 GiB")); + return zlib_deflate(in, level, 15 + 16); +} + +std::expected deflate(std::string_view in, int level) { + if (in.size() > std::numeric_limits::max()) + return std::unexpected(std::string("deflate: an input over 4 GiB")); + return zlib_deflate(in, level, -15); +} + +std::expected zstd(std::string_view in, int level) { + ZSTD_CCtx* cx = ZSTD_createCCtx(); + if (!cx) return std::unexpected(std::string("zstd: cannot create a context")); + ZSTD_CCtx_setParameter(cx, ZSTD_c_compressionLevel, level < 0 ? 19 : level); + ZSTD_CCtx_setParameter(cx, ZSTD_c_checksumFlag, 1); + ZSTD_CCtx_setParameter(cx, ZSTD_c_contentSizeFlag, 1); + std::string out; + out.resize(ZSTD_compressBound(in.size())); + const std::size_t n = ZSTD_compress2(cx, out.data(), out.size(), in.data(), in.size()); + ZSTD_freeCCtx(cx); + if (ZSTD_isError(n)) return std::unexpected(std::format("zstd: {}", ZSTD_getErrorName(n))); + out.resize(n); + return out; +} + +std::expected unzstd(std::string_view frame) { + const auto size = ZSTD_getFrameContentSize(frame.data(), frame.size()); + if (size == ZSTD_CONTENTSIZE_ERROR || size == ZSTD_CONTENTSIZE_UNKNOWN) + return std::unexpected(std::string("zstd: the frame does not state its content size")); + std::string out; + out.resize(static_cast(size)); + const std::size_t n = ZSTD_decompress(out.data(), out.size(), frame.data(), frame.size()); + if (ZSTD_isError(n)) return std::unexpected(std::format("zstd: {}", ZSTD_getErrorName(n))); + out.resize(n); + return out; +} + +std::expected xz(std::string_view in, int level) { + std::string out; + out.resize(lzma_stream_buffer_bound(in.size())); + std::size_t pos = 0; + const lzma_ret rc = lzma_easy_buffer_encode( + static_cast(level < 0 ? 6 : level), LZMA_CHECK_CRC64, nullptr, + reinterpret_cast(in.data()), in.size(), + reinterpret_cast(out.data()), &pos, out.size()); + if (rc != LZMA_OK) return std::unexpected(std::format("xz: lzma_easy_buffer_encode returned {}", + static_cast(rc))); + out.resize(pos); + return out; +} + +namespace { + +// zlib's inflate until the stream ends: a gzip member (windowBits 15 + 32 +// detects the header), its output growing, or raw DEFLATE (-15) of a stated +// `size`, its output never more than that. +std::expected zlib_inflate(std::string_view in, int windowBits, + std::optional size) { + if (in.size() > std::numeric_limits::max()) + return std::unexpected(std::string("zlib: an input over 4 GiB")); + // DEFLATE expands at most 1032 times (a run of one byte): a stated size + // beyond that is a lie, refused before anything is allocated, and a stream + // that grows past it is refused as it grows. + const std::size_t most = in.size() * 1032 + 64; + if (size && *size > most) + return std::unexpected(std::format("zlib: {} bytes cannot hold the {} it states", in.size(), *size)); + z_stream s{}; + if (inflateInit2(&s, windowBits) != Z_OK) + return std::unexpected(std::string("zlib: inflateInit2 failed")); + std::string out; + // One byte past a stated size: a stream that fills it holds more than it + // said. + out.resize(size ? *size + 1 : in.size() * 4 + 1024); + s.next_in = reinterpret_cast(const_cast(in.data())); + s.avail_in = static_cast(in.size()); + int rc = Z_OK; + while (rc == Z_OK) { + if (s.total_out >= out.size()) { + if (size) { + inflateEnd(&s); + return std::unexpected(std::format("zlib: the stream holds more than its {} bytes", + *size)); + } + if (out.size() > most) { + inflateEnd(&s); + return std::unexpected(std::string("zlib: the stream expands beyond what DEFLATE can")); + } + out.resize(std::min(out.size() * 2, most + 1)); + } + s.next_out = reinterpret_cast(out.data() + s.total_out); + s.avail_out = static_cast(std::min(out.size() - s.total_out, + std::numeric_limits::max())); + rc = ::inflate(&s, Z_NO_FLUSH); + } + const auto written = s.total_out; + inflateEnd(&s); + if (rc != Z_STREAM_END) return std::unexpected(std::format("zlib: inflate returned {}", rc)); + out.resize(written); + return out; +} + +} // namespace + +std::expected gunzip(std::string_view in) { + return zlib_inflate(in, 15 + 32, std::nullopt); +} + +std::expected inflate(std::string_view in, std::size_t size) { + return zlib_inflate(in, -15, size); +} + +std::expected unxz(std::string_view in) { + lzma_stream s = LZMA_STREAM_INIT; + if (lzma_stream_decoder(&s, UINT64_MAX, 0) != LZMA_OK) + return std::unexpected(std::string("xz: cannot start a decoder")); + std::string out; + out.resize(in.size() * 4 + 1024); + s.next_in = reinterpret_cast(in.data()); + s.avail_in = in.size(); + lzma_ret rc = LZMA_OK; + while (rc == LZMA_OK) { + if (s.total_out >= out.size()) out.resize(out.size() * 2); + s.next_out = reinterpret_cast(out.data() + s.total_out); + s.avail_out = out.size() - s.total_out; + rc = lzma_code(&s, LZMA_FINISH); + } + const auto written = s.total_out; + lzma_end(&s); + if (rc != LZMA_STREAM_END) + return std::unexpected(std::format("xz: lzma_code returned {}", static_cast(rc))); + out.resize(written); + return out; +} + +} // namespace mcpp::pack::compress::codec diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm new file mode 100644 index 000000000..4cb8979e7 --- /dev/null +++ b/src/pack/distribution.cppm @@ -0,0 +1,1787 @@ +// mcpp.pack.distribution — what a package is beyond its staged files +// (SPEC-014 §4, §5): the containers, the package's metadata +// (`mcpp-package.json`), the launchers of a universal package, the payload and +// the launcher program of a single file, and `--merge`. +// +// THE ENGINE KNOWS NO PLATFORM HERE. A launcher is generated from the +// platform's `runner` and the legs; a loader of a self-contained universal +// package is the platform's carrier with nothing carried; a single file's +// launcher is the engine's own program, written below as source and built for +// the target at pack time like any program. + +export module mcpp.pack.distribution; + +import std; +import mcpp.libs.json; +import mcpp.pack.compress; +import mcpp.pack.zip; +import mcpplibs.os; + +export namespace mcpp::pack::distribution { + +// ─── digests ────────────────────────────────────────────────────────────── + +// SHA-256 of `bytes`, of a file's content, in lowercase hexadecimal. +std::string sha256(std::string_view bytes); +std::string sha256_file(const std::filesystem::path& file); + +// ─── containers ─────────────────────────────────────────────────────────── + +// `root` (a staged tree) as a tar whose one top-level directory is +// `root.filename()`, compressed as `c` says. The tar is written here, the same +// bytes for the same tree on every host (see `tar_bytes`). +std::expected write_tar(const std::filesystem::path& root, + const std::filesystem::path& archive, + mcpp::pack::compress::Choice c); +// The same as a zip: members deflated unless `c` is `none`, links kept, a +// file executable as its mode says or when it is named `program`. +std::expected write_zip(const std::filesystem::path& root, + const std::filesystem::path& archive, + mcpp::pack::compress::Choice c, + std::string_view program = {}); + +// A package this command wrote, as a tree under `into`: a directory as it is, +// an archive (`.zip`, `.tar`, `.tar.gz`, `.tgz`, `.tar.zst`, `.tar.xz`) +// extracted into `into`, which is new or empty. Returns the tree's root, the +// archive's one top-level directory. +// +// An archive is read here, not by a program on PATH, and creates nothing +// outside `into`: every header is read within the archive, every zip member's +// CRC-32 checked; a member whose path is absolute or holds `..`, one that lies +// under a link member, and a link whose target leaves the tree are refused, +// before anything is written; the links are made after every file. +std::expected +unpack(const std::filesystem::path& package, const std::filesystem::path& into); + +// ─── launchers (SPEC-014 §4.3) ──────────────────────────────────────────── + +struct Launch { + std::string name; // bin/, bin/.cmd + // The platform's `runner.cmd`, expanded as `mcpp run` expands it: the + // first `{}` of a word is the image (`{}`, `--image={}`), and the image is + // appended when no word holds one. + std::vector runner; + std::map entry; // arch -> the entry image's file name + // The loader a self-contained package carries under + // `libexec//-/`; empty when it carries none. + std::string loader; +}; + +// Whether both launchers can state `l`: a `.cmd` keeps a word literal inside +// double quotes, so a word holding a double quote or a line break cannot be +// written in one. Checked before a launcher is written; `windows_launcher` of +// a `Launch` this refuses is a `.cmd` that says so and runs nothing. +std::expected launchable(const Launch& l); + +std::string posix_launcher(const Launch& l); +std::string windows_launcher(const Launch& l); + +// ─── mcpp-package.json (SPEC-014 §4.5) ──────────────────────────────────── + +inline constexpr std::string_view kFileName = "mcpp-package.json"; + +struct File { + std::string path; // relative to the tree's root, `/`-separated + std::string role; // program | image | library | loader | launcher | resource | metadata + std::string leg; // the leg's triple; empty: shared by every leg + std::string sha256; +}; + +struct Package { + int schema = 1; + std::string name, version; + std::string platform; // ".@"; empty: none + std::string linkage, mode; + std::vector legs; // canonical triples + std::vector systems; // `-` of the loaders under libexec/ + std::vector runner; // empty for a native package + std::map entry; // arch -> path of the entry (universal) + std::string program; // the program's path (one native leg) + std::vector files; +}; + +// What a caller knows of a file's role and leg; every other file's role is +// read from where it lies (`libexec/` a loader, `lib/` a library, ...). +struct Stated { + std::map role; // path -> role + std::map leg; // path -> leg +}; + +// Every file of the tree described, digested, and the document written at +// `root/mcpp-package.json`. +std::expected describe(const std::filesystem::path& root, Package pkg, + const Stated& stated); +std::string to_json(const Package& p); +// `root/mcpp-package.json`, every path it names (`files[].path`, the entries, +// the program) checked to be a normalised path inside the package. +std::expected read(const std::filesystem::path& root); + +// The `-` directories of `root/libexec//`: the systems whose +// loaders a self-contained universal package carries, sorted. +std::vector loader_systems(const std::filesystem::path& root); + +// ─── a single file (SPEC-014 §5.2) ──────────────────────────────────────── + +// The tree under `root` as the payload a single file's launcher extracts: the +// record stream `MCPPTREE1\n` then, per entry, a kind byte (`d` directory, +// `f` file, `x` executable file, `l` link), a 32-bit path length, the path +// (`/`-separated), a 64-bit data length and the data (a link's data is its +// target). Little-endian; entries in path order. +std::expected tree_payload(const std::filesystem::path& root); + +// The launcher program's sources: its manifest and its entry. It reads the +// carried payload (`mcpp_carried_begin` .. `mcpp_carried_end`, a zstd frame +// of `tree_payload`), extracts it once into the user's cache under the +// payload's sha256 (`mcpp_carried_digest`), and executes the program at +// `mcpp_carried_name` with its own arguments. On POSIX it runs nothing from a +// cache directory another user owns. +std::string launcher_manifest(std::string_view name, std::string_view version); +std::string_view launcher_main(); + +// ─── the distribution description (SPEC-014 §8) ─────────────────────────── + +// A package written by `mcpp pack`, as an index entry reads it. +struct Asset { + std::filesystem::path file; + Package pkg; + std::string sha256; + std::string top; // the archive's one top-level directory +}; + +// The xim-pkgindex entry (spec 2) of `assets`, fetched from `/`: a universal package is one asset every system installs, and its +// runner is a dependency unless the package carries its loaders; packages of +// one system and architecture each are that system's entry, the architecture +// in the file name a `${arch}` of one URL. The install moves the archive's +// one top-level directory into place; the program is registered from its +// launcher (`bin/`, `bin/.cmd` on Windows) or the program itself. +std::expected xpkg_descriptor(const std::vector& assets, + std::string_view urlPrefix); + +// ─── --merge (SPEC-014 §4.6) ────────────────────────────────────────────── + +struct MergeRequest { + std::vector inputs; + std::filesystem::path output; // empty: ./ + std::string format; // tar | zip | dir; empty: the first input's + mcpp::pack::compress::Choice compression; +}; + +// The inputs' trees as one package. Every file is checked against the digest +// its package states before it is copied; the output is new, or an archive +// that is a package and none of the inputs (`dir` writes a new directory only). +std::expected merge(const MergeRequest& req); + +// A new directory under the system's temporary directory, made by this call +// and no other (a name another made first is passed over) and open to this +// user alone: where packages are unpacked to be read. +std::expected work_directory(std::string_view purpose); + +} // namespace mcpp::pack::distribution + +namespace mcpp::pack::distribution { + +namespace fs = std::filesystem; + +// ─── SHA-256 (FIPS 180-4) ───────────────────────────────────────────────── + +namespace { + +struct Sha256 { + std::array h{0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + std::array block{}; + std::size_t used = 0; + std::uint64_t bits = 0; + + static std::uint32_t rotr(std::uint32_t x, int n) { return (x >> n) | (x << (32 - n)); } + + void compress() { + static constexpr std::uint32_t k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2}; + std::uint32_t w[64]; + for (int i = 0; i < 16; ++i) + w[i] = (std::uint32_t(block[4 * i]) << 24) | (std::uint32_t(block[4 * i + 1]) << 16) + | (std::uint32_t(block[4 * i + 2]) << 8) | std::uint32_t(block[4 * i + 3]); + for (int i = 16; i < 64; ++i) { + const auto s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const auto s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + auto [a, b, c, d, e, f, g, hh] = h; + for (int i = 0; i < 64; ++i) { + const auto t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + + k[i] + w[i]; + const auto t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); + hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + } + + void update(std::string_view data) { + for (unsigned char ch : data) { + block[used++] = ch; + if (used == 64) { compress(); used = 0; } + } + bits += static_cast(data.size()) * 8; + } + + std::string hex() { + const auto total = bits; + block[used++] = 0x80; + if (used > 56) { while (used < 64) block[used++] = 0; compress(); used = 0; } + while (used < 56) block[used++] = 0; + for (int i = 7; i >= 0; --i) block[used++] = static_cast(total >> (8 * i)); + compress(); + std::string out; + for (auto v : h) out += std::format("{:08x}", v); + return out; + } +}; + +std::optional slurp(const fs::path& p) { + std::ifstream in(p, std::ios::binary); + if (!in) return std::nullopt; + return std::string(std::istreambuf_iterator(in), {}); +} + +bool spit(const fs::path& p, std::string_view bytes) { + std::error_code ec; + if (p.has_parent_path()) fs::create_directories(p.parent_path(), ec); + std::ofstream out(p, std::ios::binary | std::ios::trunc); + out.write(bytes.data(), static_cast(bytes.size())); + return bool(out); +} + +// Whether a file is executable. A POSIX host's mode says. Windows keeps no +// such bit (its libraries report every file executable), so there what the +// file is says: a program or library image (ELF, Mach-O, PE) or a script +// (`#!`), which are the files a POSIX host's linkers and launcher writer leave +// executable; the tree's bytes are then the same on every host. +bool executable_file(const fs::path& p) { + if constexpr (mcpplibs::os::is_windows) { + std::ifstream in(p, std::ios::binary); + char head[4]{}; + in.read(head, sizeof head); + const std::string_view h(head, static_cast(in.gcount())); + return h.starts_with("#!") || h.starts_with("MZ") || h == std::string_view("\x7f" "ELF", 4) + || h == std::string_view("\xcf\xfa\xed\xfe", 4) || h == std::string_view("\xce\xfa\xed\xfe", 4) + || h == std::string_view("\xca\xfe\xba\xbe", 4); + } else { + std::error_code ec; + const auto perms = fs::status(p, ec).permissions(); + if (ec) return false; + return (perms & fs::perms::owner_exec) != fs::perms::none; + } +} + +// One entry of a staged tree: its path under the root (`/`-separated), what it +// is (`d` a directory, `f` a file, `x` an executable file, `l` a link) and a +// link's target. +struct Node { + std::string path; + char kind = 'f'; + std::string target; +}; + +// Every entry under `root`, links not followed, in path order (bytewise: a +// directory before what it holds). Whatever cannot be read is an error, not +// an entry left out. +std::expected, std::string> walk(const fs::path& root) { + std::vector out; + std::error_code ec; + for (auto it = fs::recursive_directory_iterator(root, ec); + !ec && it != fs::recursive_directory_iterator(); it.increment(ec)) { + std::error_code entry; + const auto st = it->symlink_status(entry); + Node n; + n.path = it->path().lexically_relative(root).generic_string(); + if (fs::is_symlink(st)) { + n.kind = 'l'; + n.target = fs::read_symlink(it->path(), entry).generic_string(); + } else if (fs::is_directory(st)) { + n.kind = 'd'; + } else if (fs::is_regular_file(st)) { + n.kind = executable_file(it->path()) ? 'x' : 'f'; + } else if (!entry) { + continue; // a socket, a device: nothing a package holds + } + if (entry) return std::unexpected(std::format("cannot read '{}': {}", it->path().string(), entry.message())); + out.push_back(std::move(n)); + } + if (ec) return std::unexpected(std::format("cannot walk '{}': {}", root.string(), ec.message())); + std::sort(out.begin(), out.end(), [](const Node& a, const Node& b) { return a.path < b.path; }); + return out; +} + +} // namespace + +std::string sha256(std::string_view bytes) { + Sha256 s; + s.update(bytes); + return s.hex(); +} + +std::string sha256_file(const fs::path& file) { + std::ifstream in(file, std::ios::binary); + if (!in) return {}; + Sha256 s; + std::array buf; + while (in) { + in.read(buf.data(), buf.size()); + if (auto n = in.gcount(); n > 0) s.update(std::string_view(buf.data(), static_cast(n))); + } + return s.hex(); +} + +// ─── containers ─────────────────────────────────────────────────────────── + +namespace { + +// ── the tar mcpp writes ── +// +// POSIX ustar, written here rather than by a `tar` on PATH, whose bytes are +// its own: GNU tar records the owner, the group and the times of the files, +// and the order the file system lists a directory in, so two packs of one +// tree differed. These bytes are the same on every host: the entries in path +// order, every owner 0 and unnamed, every time 0, the mode 0755 (a directory, +// an executable file), 0644 (another file) or 0777 (a link). A name or a link +// target over the 100 bytes of its field, or a size over the 8 GiB of its, is +// stated in a pax extended header ahead of the entry (POSIX.1-2001), which +// every tar reads. + +constexpr std::size_t kBlock = 512; + +std::size_t padding(std::size_t n) { return (kBlock - n % kBlock) % kBlock; } + +// `v` in octal, `width - 1` digits and a NUL. +void octal(char* field, std::size_t width, std::uint64_t v) { + for (std::size_t i = width - 1; i-- > 0; v >>= 3) field[i] = static_cast('0' + (v & 7)); + field[width - 1] = '\0'; +} + +void tar_header(std::string& out, std::string_view name, char type, unsigned mode, + std::uint64_t size, std::string_view link) { + std::array h{}; + std::copy_n(name.data(), std::min(name.size(), 100), h.data()); + octal(h.data() + 100, 8, mode); + octal(h.data() + 108, 8, 0); // uid + octal(h.data() + 116, 8, 0); // gid + octal(h.data() + 124, 12, size <= 077777777777ull ? size : 0); + octal(h.data() + 136, 12, 0); // mtime + std::fill_n(h.data() + 148, 8, ' '); + h[156] = type; + std::copy_n(link.data(), std::min(link.size(), 100), h.data() + 157); + std::copy_n("ustar\0" "00", 8, h.data() + 257); + octal(h.data() + 329, 8, 0); // devmajor + octal(h.data() + 337, 8, 0); // devminor + unsigned sum = 0; + for (char ch : h) sum += static_cast(ch); + octal(h.data() + 148, 7, sum); // six digits and a NUL; the space stays + out.append(h.data(), h.size()); +} + +// One pax record: " =\n", the length counting its own +// digits. +std::string pax_record(std::string_view key, std::string_view value) { + const auto rest = key.size() + value.size() + 3; + auto length = rest + 1; + while (length != rest + std::to_string(length).size()) length = rest + std::to_string(length).size(); + return std::format("{} {}={}\n", length, key, value); +} + +void tar_entry(std::string& out, std::string_view name, char type, unsigned mode, + std::string_view data, std::string_view link) { + std::string pax; + if (name.size() > 100) pax += pax_record("path", name); + if (link.size() > 100) pax += pax_record("linkpath", link); + if (data.size() > 077777777777ull) pax += pax_record("size", std::to_string(data.size())); + if (!pax.empty()) { + tar_header(out, "././@PaxHeader", 'x', 0644, pax.size(), {}); + out += pax; + out.append(padding(pax.size()), '\0'); + } + tar_header(out, name, type, mode, data.size(), link); + out += data; + out.append(padding(data.size()), '\0'); +} + +// The tar of `root`: its directory, then every entry under it. +std::expected tar_bytes(const fs::path& root) { + auto nodes = walk(root); + if (!nodes) return std::unexpected(nodes.error()); + const auto top = (root.has_filename() ? root : root.parent_path()).filename().generic_string(); + std::string out; + tar_entry(out, top + "/", '5', 0755, {}, {}); + for (auto const& n : *nodes) { + const auto name = top + "/" + n.path; + if (n.kind == 'd') { tar_entry(out, name + "/", '5', 0755, {}, {}); continue; } + if (n.kind == 'l') { tar_entry(out, name, '2', 0777, {}, n.target); continue; } + auto data = slurp(root / fs::path(n.path)); + if (!data) return std::unexpected(std::format("cannot read '{}'", (root / n.path).string())); + tar_entry(out, name, '0', n.kind == 'x' ? 0755 : 0644, *data, {}); + } + out.append(2 * kBlock, '\0'); + return out; +} + +} // namespace + +std::expected write_tar(const fs::path& root, const fs::path& archive, + mcpp::pack::compress::Choice c) { + auto tar = tar_bytes(root); + if (!tar) return std::unexpected(tar.error()); + auto packed = mcpp::pack::compress::bytes(*tar, c); + if (!packed) return std::unexpected(std::format("{}: {}", archive.string(), packed.error())); + if (!spit(archive, *packed)) return std::unexpected(std::format("cannot write '{}'", archive.string())); + return {}; +} + +std::expected write_zip(const fs::path& root, const fs::path& archive, + mcpp::pack::compress::Choice c, + std::string_view program) { + auto nodes = walk(root); + if (!nodes) return std::unexpected(nodes.error()); + const auto top = (root.has_filename() ? root : root.parent_path()).filename().generic_string(); + // In path order, as `walk` returns the tree. + std::vector entries; + for (auto const& n : *nodes) { + if (n.kind == 'd' || (n.kind == 'l' && n.target.empty())) continue; + mcpp::pack::zip::Entry entry; + entry.name = top + "/" + n.path; + entry.source = root / fs::path(n.path); + entry.symlinkTarget = n.target; + entry.executable = n.kind == 'x' + || (n.kind == 'f' && !program.empty() && entry.source.filename() == program); + entries.push_back(std::move(entry)); + } + mcpp::pack::zip::WriteOptions o; + o.deflate = c.algorithm != mcpp::pack::compress::Algorithm::None; + o.level = c.algorithm == mcpp::pack::compress::Algorithm::Gzip ? c.level : -1; + return mcpp::pack::zip::write(archive, entries, o); +} + +namespace { + +// ── what an archive holds ── + +std::uint16_t get16(std::string_view b, std::size_t at) { + return static_cast(static_cast(b[at]) + | (static_cast(b[at + 1]) << 8)); +} +std::uint32_t get32(std::string_view b, std::size_t at) { + return static_cast(get16(b, at)) | (static_cast(get16(b, at + 2)) << 16); +} + +// `s` split at every `/`, empty parts kept. +std::vector parts(std::string_view s) { + std::vector out; + for (std::size_t at = 0; at <= s.size();) { + auto slash = s.find('/', at); + if (slash == std::string_view::npos) slash = s.size(); + out.push_back(s.substr(at, slash - at)); + at = slash + 1; + } + return out; +} + +// What can never be a path inside a tree on every system: empty, absolute +// (`/x`, `C:`), with a backslash or a NUL. +bool foreign(std::string_view p) { + return p.empty() || p.front() == '/' || p.find('\\') != std::string_view::npos + || p.find('\0') != std::string_view::npos || (p.size() > 1 && p[1] == ':'); +} + +// The path inside the extraction root a member's name names, normalised: +// relative, `/`-separated, without `.` or empty parts or a trailing `/`; empty +// for the root itself (`./`, which `tar -C dir .` writes). None when it would +// leave the root. A `..` is refused wherever it is, `a/../b` too, which stays +// inside as words: through a link member `a` it would not, and no archive mcpp +// writes holds one. +std::optional member_path(std::string_view name) { + if (foreign(name)) return std::nullopt; + std::string out; + for (auto part : parts(name)) { + if (part.empty() || part == ".") continue; + if (part == "..") return std::nullopt; + if (!out.empty()) out += '/'; + out += part; + } + return out; +} + +// Whether the link member at `path` whose target is `target` resolves inside +// the extraction root: the target is relative and, walked from the link's +// directory, never climbs above the root and goes on from no link member (one +// may be where it ends: that link's own target is checked the same way). +// A member path as a file system that ignores case sees it (APFS, NTFS): two +// members, or a link and a path, that differ in case alone are one there. +std::string folded(std::string_view path) { + std::string out(path); + for (auto& c : out) c = static_cast(std::tolower(static_cast(c))); + return out; +} + +// `links` holds the link members' paths `folded`. +bool link_inside(std::string_view path, std::string_view target, const std::set& links) { + if (foreign(target)) return false; + // Where the walk is, a path under the root (empty: the root itself); the + // link's directory first. + const auto slash = path.rfind('/'); + std::string here(slash == std::string_view::npos ? std::string_view() : path.substr(0, slash)); + for (auto part : parts(target)) { + if (part.empty() || part == ".") continue; + if (!here.empty() && links.contains(folded(here))) return false; + if (part == "..") { + if (here.empty()) return false; + const auto up = here.rfind('/'); + here.resize(up == std::string::npos ? 0 : up); + } else { + here += (here.empty() ? "" : "/") + std::string(part); + } + } + return true; +} + +// One member of an archive: its path (`member_path`) and what it is. A file's +// bytes are as stored (`method` 8: DEFLATEd); a link's target is read at once. +struct Member { + std::string path; + char kind = 'f'; // 'd' directory, 'f' file, 'l' link + bool executable = false; + std::string_view stored; + std::uint16_t method = 0; + std::uint64_t size = 0; + std::optional crc; // a zip member's CRC-32 + std::string target; +}; + +// A file member's content: inflated if it was deflated, its length and its +// CRC-32 (a zip's) checked against the archive's. +std::expected contents(const Member& m) { + std::string data; + if (m.method == 8) { + auto d = mcpp::pack::compress::inflate_raw(m.stored, static_cast(m.size)); + if (!d) return std::unexpected(std::format("'{}': {}", m.path, d.error())); + data = std::move(*d); + } else { + data.assign(m.stored); + } + if (data.size() != m.size) + return std::unexpected(std::format("'{}' holds {} bytes; the archive states {}", m.path, + data.size(), m.size)); + if (m.crc && mcpp::pack::zip::crc32(data) != *m.crc) + return std::unexpected(std::format("'{}' fails its CRC-32: {:08x}, and the archive states {:08x}", + m.path, mcpp::pack::zip::crc32(data), *m.crc)); + return data; +} + +// The members of a zip, read through its central directory (stored and +// deflated, as mcpp writes them). Every field is read within the archive: an +// offset or a length that points past it is refused, never followed. +std::expected, std::string> zip_members(std::string_view b) { + auto within = [&](std::uint64_t at, std::uint64_t n) { return at <= b.size() && n <= b.size() - at; }; + if (b.size() < 22) return std::unexpected(std::string("not a zip: shorter than its end record")); + // The end of the central directory: the last 22 bytes, or as far before + // them as its comment (65535 bytes at most) reaches. + std::size_t eocd = std::string_view::npos; + const std::size_t lowest = b.size() - 22 > 65535 ? b.size() - 22 - 65535 : 0; + for (std::size_t i = b.size() - 22;; --i) { + if (get32(b, i) == 0x06054b50 && within(i + 22, get16(b, i + 20))) { eocd = i; break; } + if (i == lowest) break; + } + if (eocd == std::string_view::npos) return std::unexpected(std::string("not a zip: no end of central directory")); + const auto count = get16(b, eocd + 10); + std::size_t at = get32(b, eocd + 16); + if (count == 0xFFFF || at == 0xFFFFFFFF) + return std::unexpected(std::string("a ZIP64 archive, which mcpp neither writes nor reads")); + std::vector out; + for (std::uint16_t n = 0; n < count; ++n) { + if (!within(at, 46) || get32(b, at) != 0x02014b50) + return std::unexpected(std::format("the central directory is malformed at {}", at)); + const auto flags = get16(b, at + 8), method = get16(b, at + 10); + const auto crc = get32(b, at + 16), packed = get32(b, at + 20), size = get32(b, at + 24); + const auto nameLen = get16(b, at + 28), extraLen = get16(b, at + 30), commentLen = get16(b, at + 32); + const auto mode = get32(b, at + 38) >> 16; + const auto local = get32(b, at + 42); + if (!within(at + 46, std::uint64_t(nameLen) + extraLen + commentLen)) + return std::unexpected(std::string("the central directory is truncated")); + const auto name = b.substr(at + 46, nameLen); + at += 46 + std::size_t(nameLen) + extraLen + commentLen; + const bool dir = name.ends_with('/') || (mode & 0170000u) == 0040000u; + auto path = member_path(name); + if (!path || (path->empty() && !dir)) + return std::unexpected(std::format("a member names '{}', which is not a path inside the archive", + name)); + if (path->empty()) continue; // the root, there already + if (flags & 1) return std::unexpected(std::format("'{}' is encrypted", name)); + if (method != 0 && method != 8) + return std::unexpected(std::format("'{}' uses method {}", name, method)); + if (packed == 0xFFFFFFFF || size == 0xFFFFFFFF || local == 0xFFFFFFFF) + return std::unexpected(std::format("'{}' is a ZIP64 member, which mcpp neither writes nor reads", + name)); + Member m; + m.path = std::move(*path); + if (dir) { + m.kind = 'd'; + out.push_back(std::move(m)); + continue; + } + if (!within(local, 30) || get32(b, local) != 0x04034b50) + return std::unexpected(std::format("'{}' has no local header at {}", name, local)); + const std::uint64_t dataAt = std::uint64_t(local) + 30 + get16(b, local + 26) + get16(b, local + 28); + if (!within(dataAt, packed)) return std::unexpected(std::format("'{}' is truncated", name)); + m.stored = b.substr(static_cast(dataAt), packed); + m.method = method; + m.size = size; + m.crc = crc; + m.executable = (mode & 0100u) != 0; + if ((mode & 0170000u) == 0120000u) { + auto target = contents(m); + if (!target) return std::unexpected(target.error()); + m.kind = 'l'; + m.target = std::move(*target); + m.stored = {}; + } + out.push_back(std::move(m)); + } + return out; +} + +// A numeric field of a tar header: octal digits (after spaces, ended by a +// space or a NUL), or base-256 when the first byte's high bit is set. +std::optional tar_number(std::string_view f) { + std::uint64_t v = 0; + if (!f.empty() && (static_cast(f.front()) & 0x80)) { + if (static_cast(f.front()) != 0x80) return std::nullopt; // negative, or too large + for (char ch : f.substr(1)) { + if (v >> 56) return std::nullopt; + v = v << 8 | static_cast(ch); + } + return v; + } + std::size_t i = f.find_first_not_of(' '); + if (i == std::string_view::npos) return 0; + for (; i < f.size() && f[i] >= '0' && f[i] <= '7'; ++i) { + if (v >> 60) return std::nullopt; + v = v * 8 + static_cast(f[i] - '0'); + } + if (i < f.size() && f[i] != ' ' && f[i] != '\0') return std::nullopt; + return v; +} + +// A text field of a tar header, up to its first NUL. +std::string_view tar_text(std::string_view f) { return f.substr(0, f.find('\0')); } + +// The records of a pax extended header that name the next entry: its path, +// its link's target, its size. The others (times, owners) are read past. +std::expected pax_records(std::string_view data, std::string& path, std::string& link, + std::optional& size) { + while (!data.empty()) { + const auto space = data.find(' '); + std::uint64_t length = 0; + if (space == std::string_view::npos + || std::from_chars(data.data(), data.data() + space, length).ec != std::errc{} + || length <= space + 1 || length > data.size() || data[length - 1] != '\n') + return std::unexpected(std::string("a pax header is malformed")); + const auto record = data.substr(space + 1, length - space - 2); + data.remove_prefix(length); + const auto eq = record.find('='); + if (eq == std::string_view::npos) return std::unexpected(std::string("a pax record has no '='")); + const auto key = record.substr(0, eq), value = record.substr(eq + 1); + if (key == "path") path = value; + else if (key == "linkpath") link = value; + else if (key == "size") { + std::uint64_t v = 0; + if (std::from_chars(value.data(), value.data() + value.size(), v).ec != std::errc{}) + return std::unexpected(std::format("a pax size '{}' is not a number", value)); + size = v; + } + } + return {}; +} + +// The members of a tar: ustar, its pax extended headers and GNU's long names +// (the archives earlier releases wrote through GNU tar). Every header's +// checksum is checked and every length read within the archive. +std::expected, std::string> tar_members(std::string_view b) { + std::vector out; + std::string longName, longLink; // the next entry's, from a pax or GNU header + std::optional longSize; + std::size_t at = 0; + while (true) { + if (at == b.size()) break; // no end blocks: as GNU tar, the end of the archive + if (b.size() - at < kBlock) return std::unexpected(std::string("the tar ends inside a header")); + const auto h = b.substr(at, kBlock); + if (h.find_first_not_of('\0') == std::string_view::npos) break; // the end of the archive + unsigned sum = 0; + int signedSum = 0; + for (std::size_t i = 0; i < kBlock; ++i) { + const char ch = i >= 148 && i < 156 ? ' ' : h[i]; + sum += static_cast(ch); + signedSum += static_cast(ch); + } + const auto stated = tar_number(h.substr(148, 8)); + if (!stated || (*stated != sum && *stated != static_cast(signedSum))) + return std::unexpected(std::format("the tar header at {} fails its checksum", at)); + const char type = h[156]; + const bool meta = type == 'x' || type == 'g' || type == 'L' || type == 'K'; + auto size = tar_number(h.substr(124, 12)); + if (!size) return std::unexpected(std::format("the tar header at {} states no size", at)); + if (longSize && !meta) size = longSize; + at += kBlock; + if (*size > b.size() - at || padding(static_cast(*size)) > b.size() - at - *size) + return std::unexpected(std::format("the tar entry at {} is truncated", at - kBlock)); + const auto data = b.substr(at, static_cast(*size)); + at += static_cast(*size) + padding(static_cast(*size)); + if (type == 'g') continue; + if (type == 'x') { + if (auto r = pax_records(data, longName, longLink, longSize); !r) return std::unexpected(r.error()); + continue; + } + if (type == 'L') { longName = tar_text(data); continue; } + if (type == 'K') { longLink = tar_text(data); continue; } + std::string name(longName); + if (name.empty()) { + name = tar_text(h.substr(0, 100)); + // POSIX ustar's prefix; GNU's own magic ("ustar ") puts other + // things there. + if (const auto prefix = tar_text(h.substr(345, 155)); h.substr(257, 6) == std::string_view("ustar\0", 6) + && !prefix.empty()) + name = std::string(prefix) + "/" + name; + } + const std::string link = longLink.empty() ? std::string(tar_text(h.substr(157, 100))) : longLink; + longName.clear(); + longLink.clear(); + longSize.reset(); + const bool dir = type == '5' || ((type == '0' || type == '\0') && name.ends_with('/')); + auto path = member_path(name); + if (!path || (path->empty() && !dir)) + return std::unexpected(std::format("a member names '{}', which is not a path inside the archive", + name)); + if (path->empty()) continue; // the root, there already + Member m; + m.path = std::move(*path); + if (dir) { + m.kind = 'd'; + } else if (type == '0' || type == '\0' || type == '7') { + m.stored = data; + m.size = data.size(); + m.executable = (tar_number(h.substr(100, 8)).value_or(0) & 0100u) != 0; + } else if (type == '2') { + m.kind = 'l'; + m.target = link; + } else if (type == '1') { + return std::unexpected(std::format("'{}' is a hard link, which mcpp does not write", name)); + } else { + return std::unexpected(std::format("'{}' is a tar entry of type '{}', which a package does not " + "hold", name, type)); + } + out.push_back(std::move(m)); + } + return out; +} + +// The members created under `into`, and nothing outside it. Every path is +// checked before anything is written: none twice, none under a link member, +// no link whose target leaves the tree. Then the directories and the files +// are written, and the links made last, so no member is written through one. +std::expected place(const std::vector& members, const fs::path& into) { + // Compared `folded`: on a file system that ignores case `A/B` is `a/b`. + std::set paths, links; + for (auto const& m : members) { + if (!paths.insert(folded(m.path)).second) + return std::unexpected(std::format("'{}' is in the archive twice", m.path)); + if (m.kind == 'l') links.insert(folded(m.path)); + } + for (auto const& m : members) { + for (auto slash = m.path.find('/'); slash != std::string::npos; slash = m.path.find('/', slash + 1)) + if (links.contains(folded(m.path.substr(0, slash)))) + return std::unexpected(std::format("'{}' lies under the link '{}'", m.path, m.path.substr(0, slash))); + if (m.kind == 'l' && !link_inside(m.path, m.target, links)) + return std::unexpected(std::format("the link '{}' points to '{}', outside the package", m.path, + m.target)); + } + std::error_code ec; + for (auto const& m : members) { + const auto target = into / fs::path(m.path); + if (m.kind == 'd') { + fs::create_directories(target, ec); + if (ec) return std::unexpected(std::format("cannot create '{}': {}", target.string(), ec.message())); + continue; + } + if (m.kind != 'f') continue; + auto data = contents(m); + if (!data) return std::unexpected(data.error()); + if (!spit(target, *data)) return std::unexpected(std::format("cannot write '{}'", target.string())); + if (m.executable) + fs::permissions(target, fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec, + fs::perm_options::add, ec); + } + for (auto const& m : members) { + if (m.kind != 'l') continue; + const auto link = into / fs::path(m.path); + fs::create_directories(link.parent_path(), ec); + if (fs::is_directory(link.parent_path() / fs::path(m.target), ec)) + fs::create_directory_symlink(fs::path(m.target), link, ec); + else + fs::create_symlink(fs::path(m.target), link, ec); + if (ec) return std::unexpected(std::format("cannot link '{}': {}", link.string(), ec.message())); + } + // And as the file system resolves them: every link ends under the root. + const auto root = fs::weakly_canonical(into, ec); + for (auto const& m : members) { + if (m.kind != 'l') continue; + const auto real = fs::weakly_canonical(into / fs::path(m.path), ec); + const auto [r, _] = std::mismatch(root.begin(), root.end(), real.begin(), real.end()); + if (ec || r != root.end()) + return std::unexpected(std::format("the link '{}' points to '{}', outside the package", m.path, + m.target)); + } + return {}; +} + +} // namespace + +std::expected unpack(const fs::path& package, const fs::path& into) { + std::error_code ec; + if (fs::is_directory(package, ec)) return package; + if (!fs::is_regular_file(package, ec)) + return std::unexpected(std::format("'{}' is neither a package directory nor an archive", + package.string())); + const auto name = package.filename().string(); + using mcpp::pack::compress::Algorithm; + const bool zip = name.ends_with(".zip"); + const std::optional a = name.ends_with(".tar.gz") || name.ends_with(".tgz") ? Algorithm::Gzip + : name.ends_with(".tar.zst") ? Algorithm::Zstd + : name.ends_with(".tar.xz") ? Algorithm::Xz + : name.ends_with(".tar") ? std::optional(Algorithm::None) + : std::nullopt; + if (!zip && !a) + return std::unexpected(std::format( + "'{}' is not a package this command writes (.zip, .tar, .tar.gz, .tar.zst, .tar.xz, " + "or a directory)", name)); + if (fs::exists(fs::symlink_status(into, ec)) && !(fs::is_directory(fs::symlink_status(into, ec)) + && fs::is_empty(into, ec))) + return std::unexpected(std::format("'{}' is not an empty directory to unpack {} into", + into.string(), name)); + fs::create_directories(into, ec); + auto bytes = slurp(package); + if (!bytes) return std::unexpected(std::format("cannot read '{}'", package.string())); + // The members point into the archive's bytes, or into its tar. + std::string plain; + std::expected, std::string> members; + if (zip) { + members = zip_members(*bytes); + } else { + auto t = mcpp::pack::compress::decompress(*bytes, *a); + if (!t) return std::unexpected(std::format("{}: {}", name, t.error())); + plain = std::move(*t); + members = tar_members(plain); + } + if (!members) return std::unexpected(std::format("{}: {}", name, members.error())); + if (auto r = place(*members, into); !r) return std::unexpected(std::format("{}: {}", name, r.error())); + // The archive's one top-level directory (a link there is none). + std::vector tops; + for (auto const& e : fs::directory_iterator(into, ec)) + if (fs::is_directory(e.symlink_status(ec))) tops.push_back(e.path()); + if (tops.size() != 1) + return std::unexpected(std::format("'{}' has {} top-level directories; a package has one", + name, tops.size())); + return tops.front(); +} + +// ─── launchers ──────────────────────────────────────────────────────────── + +namespace { + +// A word of a POSIX shell command, single-quoted. +std::string sh_word(std::string_view w) { + std::string out = "'"; + for (char ch : w) { + if (ch == '\'') out += "'\\''"; + else out.push_back(ch); + } + out += "'"; + return out; +} + +// Text of a .cmd inside double quotes: `%` doubled, the one character a batch +// file reads there. +std::string cmd_quoted(std::string_view w) { + std::string out; + for (char ch : w) { + if (ch == '%') out += "%%"; + else out.push_back(ch); + } + return out; +} + +// Text of a .cmd outside quotes (an `echo`): `%` doubled and every character +// the command line reads escaped with `^`. +std::string cmd_bare(std::string_view w) { + std::string out; + for (char ch : w) { + if (ch == '%') out += "%%"; + else if (std::string_view("&|<>^()").find(ch) != std::string_view::npos) out += {'^', ch}; + else out.push_back(ch); + } + return out; +} + +// How a launcher's language writes the runner's words: `literal` a piece of +// a word as text, `image` the image's variable, `around` what encloses each +// word. +struct Quoting { + std::string (*literal)(std::string_view); + std::string_view image; + std::string_view around; +}; + +// The runner's arguments, its words after the program, by `mcpp run`'s rule +// (mcpp.freestanding's `expand`): a word's first `{}` is the image, in +// `--image={}` as well as in `{}`, and the image is appended when no word +// holds one. +std::string runner_args(const std::vector& runner, const Quoting& q) { + std::string out; + bool placed = false; + for (std::size_t i = 1; i < runner.size(); ++i) { + const std::string_view w = runner[i]; + out += std::format(" {}", q.around); + if (const auto at = w.find("{}"); at == std::string_view::npos) { + out += q.literal(w); + } else { + placed = true; + if (at > 0) out += q.literal(w.substr(0, at)); + out += q.image; + if (at + 2 < w.size()) out += q.literal(w.substr(at + 2)); + } + out += q.around; + } + if (!placed) out += std::format(" {0}{1}{0}", q.around, q.image); + return out; +} + +std::string arch_list(const Launch& l) { + std::string arches; + for (auto const& [arch, _] : l.entry) arches += (arches.empty() ? "" : " ") + arch; + return arches; +} + +} // namespace + +std::expected launchable(const Launch& l) { + std::vector words{l.name, l.loader}; + words.insert(words.end(), l.runner.begin(), l.runner.end()); + for (auto const& [arch, file] : l.entry) words.insert(words.end(), {arch, file}); + for (auto w : words) + // A word ending in `\` would make the quote that closes it a quote of + // the word for the program reading its arguments. + if (w.find_first_of(std::string_view("\"\r\n\0", 4)) != std::string_view::npos || w.ends_with('\\')) + return std::unexpected(std::format( + "the launcher of {} cannot state '{}': a word of a .cmd holds no double quote and no " + "line break, and does not end in a backslash", l.name, w)); + return {}; +} + +std::string posix_launcher(const Launch& l) { + std::string cases; + for (auto const& [arch, file] : l.entry) + cases += std::format(" {}) entry={} ;;\n", sh_word(arch), sh_word(file)); + // The runner: its program found in the package, then on PATH; none, the + // image itself. + std::string find, args = " \"$@\""; + if (l.runner.empty()) { + find = "runner=$image\n"; + } else { + args = runner_args(l.runner, { sh_word, "\"$image\"", "" }) + args; + find = std::format("runner={}\n", sh_word(l.runner.front())); + if (!l.loader.empty()) + find = std::format( + "loader={0}\n" + "runner=\"$top/libexec/$loader/$os-$arch/$loader\"\n" + "[ -x \"$runner\" ] || runner={1}\n", sh_word(l.loader), sh_word(l.runner.front())); + } + return std::format( + "#!/bin/sh\n" + "# Generated by mcpp pack (SPEC-014 §4.3): runs the package's program on this\n" + "# machine's architecture through the platform's runner.\n" + "#\n" + "# The package's directory is found through the links a person may have made\n" + "# to this file, by `readlink` without -f, which not every system has.\n" + "self=$0\n" + "n=0\n" + "while [ -h \"$self\" ] && [ \"$n\" -lt 40 ]; do\n" + " link=$(readlink -- \"$self\") || break\n" + " case $link in\n" + " /*) self=$link ;;\n" + " *) self=$(dirname -- \"$self\")/$link ;;\n" + " esac\n" + " n=$((n + 1))\n" + "done\n" + "top=$(CDPATH= cd -P -- \"$(dirname -- \"$self\")/..\" && pwd -P)\n" + "case \"$(uname -m)\" in\n" + " x86_64|amd64|AMD64) arch=x86_64 ;;\n" + " aarch64|arm64|ARM64) arch=aarch64 ;;\n" + " *) arch=$(uname -m) ;;\n" + "esac\n" + "case \"$(uname -s)\" in\n" + " Linux) os=linux ;;\n" + " Darwin) os=macos ;;\n" + " *) os=$(uname -s) ;;\n" + "esac\n" + "case \"$arch\" in\n" + "{1}" + " *) printf '%s: this machine is %s; the package holds %s\\n' {0} \"$arch\" {2} >&2; exit 126 ;;\n" + "esac\n" + "image=\"$top/lib/$arch/$entry\"\n" + "{3}" + "exec \"$runner\"{4}\n", + sh_word(l.name), cases, sh_word(arch_list(l)), find, args); +} + +std::string windows_launcher(const Launch& l) { + if (!launchable(l)) + return "@echo off\r\n" + "rem Generated by mcpp pack (SPEC-014 §4.3).\r\n" + "echo This package's runner has a word a .cmd cannot state; run it from a POSIX shell. 1>&2\r\n" + "exit /b 126\r\n"; + std::string picks; + for (auto const& [arch, file] : l.entry) + picks += std::format("if \"%arch%\"==\"{0}\" set \"image=%top%\\lib\\{0}\\{1}\"\r\n", + cmd_quoted(arch), cmd_quoted(file)); + std::string find = "set \"runner=%image%\"\r\n", args; + if (!l.runner.empty()) { + args = runner_args(l.runner, { cmd_quoted, "%image%", "\"" }); + const auto program = cmd_quoted(l.runner.front()); + find = std::format("set \"runner={}\"\r\n", program); + if (!l.loader.empty()) + find = std::format( + "set \"runner=%top%\\libexec\\{0}\\windows-%arch%\\{0}.exe\"\r\n" + "if not exist \"%runner%\" set \"runner={1}\"\r\n", cmd_quoted(l.loader), program); + } + return std::format( + "@echo off\r\n" + "rem Generated by mcpp pack (SPEC-014 §4.3): runs the package's program on this\r\n" + "rem machine's architecture through the platform's runner.\r\n" + "setlocal DisableDelayedExpansion\r\n" + "for %%I in (\"%~dp0..\") do set \"top=%%~fI\"\r\n" + "set \"machine=%PROCESSOR_ARCHITECTURE%\"\r\n" + "if defined PROCESSOR_ARCHITEW6432 set \"machine=%PROCESSOR_ARCHITEW6432%\"\r\n" + "set \"arch=%machine%\"\r\n" + "if /i \"%machine%\"==\"AMD64\" set \"arch=x86_64\"\r\n" + "if /i \"%machine%\"==\"ARM64\" set \"arch=aarch64\"\r\n" + "set \"image=\"\r\n" + "{1}" + "if not defined image (\r\n" + " echo {0}: this machine is %arch%; the package holds {2} 1>&2\r\n" + " exit /b 126\r\n" + ")\r\n" + "{3}" + "\"%runner%\"{4} %*\r\n" + "exit /b %ERRORLEVEL%\r\n", + cmd_bare(l.name), picks, cmd_bare(arch_list(l)), find, args); +} + +// ─── mcpp-package.json ──────────────────────────────────────────────────── + +namespace { + +// A shared object's name: `.so`, or `.so.` and a version of digits and dots +// (`libz.so.1.3`); not a name that merely holds `.so` (`notes.sol`). +bool shared_object(std::string_view file) { + const auto at = file.rfind(".so"); + if (at == std::string_view::npos) return false; + const auto rest = file.substr(at + 3); + return rest.empty() + || (rest.size() > 1 && rest.front() == '.' && rest.find_first_not_of("0123456789.") == std::string_view::npos); +} + +std::string role_of(std::string_view path, bool executable) { + auto ends = [&](std::string_view s) { return path.ends_with(s); }; + const auto file = path.substr(path.rfind('/') == std::string_view::npos ? 0 : path.rfind('/') + 1); + if (path.starts_with("libexec/")) return "loader"; + if (file.starts_with("README") || file.starts_with("LICENSE") || file == "HOST-REQUIREMENTS" + || path.starts_with("share/licenses/")) + return "metadata"; + if (ends(".dll") || ends(".dylib") || shared_object(file) || ends(".okso")) + return "library"; + if (ends(".oke")) return "image"; + if (path.starts_with("bin/") && (executable || ends(".exe") || ends(".js") || ends(".wasm"))) + return "program"; + if (ends(".cmd") || ends(".sh")) return "launcher"; + // An executable at the root: the wrapper that starts the program. + if (executable && path.find('/') == std::string_view::npos) return "launcher"; + return "resource"; +} + +} // namespace + +std::string to_json(const Package& p) { + nlohmann::json files = nlohmann::json::array(); + for (auto const& f : p.files) + files.push_back({{"path", f.path}, {"role", f.role}, + {"leg", f.leg.empty() ? nlohmann::json(nullptr) : nlohmann::json(f.leg)}, + {"sha256", f.sha256}}); + nlohmann::json launch = nlohmann::json::object(); + if (!p.runner.empty()) { + launch["runner"] = p.runner; + nlohmann::json entry = nlohmann::json::object(); + for (auto const& [arch, path] : p.entry) entry[arch] = path; + launch["entry"] = std::move(entry); + } else { + launch["program"] = p.program.empty() ? nlohmann::json(nullptr) : nlohmann::json(p.program); + } + nlohmann::ordered_json j; + j["schema"] = p.schema; + j["name"] = p.name; + j["version"] = p.version; + j["platform"] = p.platform.empty() ? nlohmann::json(nullptr) : nlohmann::json(p.platform); + j["linkage"] = p.linkage; + j["mode"] = p.mode; + j["legs"] = p.legs; + j["systems"] = p.systems; + j["launch"] = std::move(launch); + j["files"] = std::move(files); + return j.dump(2) + "\n"; +} + +std::expected describe(const fs::path& root, Package pkg, const Stated& stated) { + pkg.files.clear(); + auto nodes = walk(root); + if (!nodes) return std::unexpected(nodes.error()); + // The files, in path order; a link is not a file of the package. + for (auto const& n : *nodes) { + if ((n.kind != 'f' && n.kind != 'x') || n.path == kFileName) continue; + File f; + f.path = n.path; + if (auto r = stated.role.find(n.path); r != stated.role.end()) f.role = r->second; + else f.role = role_of(n.path, n.kind == 'x'); + if (auto l = stated.leg.find(n.path); l != stated.leg.end()) f.leg = l->second; + else if (pkg.legs.size() == 1 && f.role != "metadata" && f.role != "resource") f.leg = pkg.legs.front(); + f.sha256 = sha256_file(root / fs::path(n.path)); + pkg.files.push_back(std::move(f)); + } + if (!spit(root / kFileName, to_json(pkg))) + return std::unexpected(std::format("cannot write '{}'", (root / kFileName).string())); + return pkg; +} + +std::expected read(const fs::path& root) { + auto text = slurp(root / kFileName); + if (!text) return std::unexpected(std::format("'{}' has no {}", root.string(), kFileName)); + Package p; + try { + auto j = nlohmann::json::parse(*text); + p.schema = j.value("schema", 0); + p.name = j.value("name", std::string{}); + p.version = j.value("version", std::string{}); + if (j.contains("platform") && j["platform"].is_string()) p.platform = j["platform"].get(); + p.linkage = j.value("linkage", std::string{}); + p.mode = j.value("mode", std::string{}); + p.legs = j.value("legs", std::vector{}); + p.systems = j.value("systems", std::vector{}); + if (j.contains("launch")) { + auto const& l = j["launch"]; + if (l.contains("runner")) p.runner = l["runner"].get>(); + if (l.contains("entry") && l["entry"].is_object()) + for (auto it = l["entry"].begin(); it != l["entry"].end(); ++it) + p.entry[it.key()] = it.value().get(); + if (l.contains("program") && l["program"].is_string()) p.program = l["program"].get(); + } + for (auto const& f : j.value("files", nlohmann::json::array())) { + File x; + x.path = f.value("path", std::string{}); + x.role = f.value("role", std::string{}); + if (f.contains("leg") && f["leg"].is_string()) x.leg = f["leg"].get(); + x.sha256 = f.value("sha256", std::string{}); + p.files.push_back(std::move(x)); + } + } catch (const std::exception& e) { + return std::unexpected(std::format("{}: {}", (root / kFileName).string(), e.what())); + } + if (p.schema != 1) + return std::unexpected(std::format("{} states schema {}; this mcpp reads schema 1", + (root / kFileName).string(), p.schema)); + // Every path it names is one inside the package, written as `describe` + // writes it: what reads the package (--merge, the index entry) opens them. + std::vector named; + for (auto const& f : p.files) named.push_back(f.path); + for (auto const& [_, e] : p.entry) named.push_back(e); + if (!p.program.empty()) named.push_back(p.program); + for (auto n : named) + if (n.empty() || member_path(n) != n) + return std::unexpected(std::format("{} names '{}', which is not a path inside the package", + (root / kFileName).string(), n)); + return p; +} + +std::vector loader_systems(const fs::path& root) { + std::vector out; + std::error_code ec; + const auto libexec = root / "libexec"; + if (!fs::is_directory(libexec, ec)) return out; + // One walk, two levels deep: `libexec//-`. + for (auto it = fs::recursive_directory_iterator(libexec, ec); + !ec && it != fs::recursive_directory_iterator(); it.increment(ec)) { + if (it.depth() < 1) continue; + it.disable_recursion_pending(); + if (it->is_directory(ec)) out.push_back(it->path().filename().string()); + } + std::ranges::sort(out); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return out; +} + +// ─── a single file ──────────────────────────────────────────────────────── + +std::expected tree_payload(const fs::path& root) { + auto nodes = walk(root); + if (!nodes) return std::unexpected(nodes.error()); + std::string out = "MCPPTREE1\n"; + auto put = [&](std::uint64_t v, int bytes) { + for (int i = 0; i < bytes; ++i) out.push_back(static_cast((v >> (8 * i)) & 0xff)); + }; + for (auto const& n : *nodes) { + std::string data = n.target; + if (n.kind == 'f' || n.kind == 'x') { + auto bytes = slurp(root / fs::path(n.path)); + if (!bytes) return std::unexpected(std::format("cannot read '{}'", (root / n.path).string())); + data = std::move(*bytes); + } + out.push_back(n.kind); + put(n.path.size(), 4); + out += n.path; + put(data.size(), 8); + out += data; + } + return out; +} + +std::string launcher_manifest(std::string_view name, std::string_view version) { + return std::format( + "# Generated by `mcpp pack --pkg-format single-file`: the launcher of {0}, which\n" + "# carries the package as a zstd frame and runs it from the user's cache.\n" + "[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.2.0\"\n\n" + "[dependencies.compat]\nzstd = \"1.5.7\"\n", + name, version.empty() ? std::string_view("0.0.0") : version); +} + +std::string_view launcher_main() { + return R"MCPP(// The launcher of a single file (SPEC-014 §5.2), written by mcpp pack. +// +// The package it runs is carried in this program as one zstd frame of a record +// stream (`mcpp_carried_begin` .. `mcpp_carried_end`). The first run extracts +// it into the user's cache, under the sha256 mcpp wrote (`mcpp_carried_digest`): +// into a name of its own beside the destination, renamed into place when every +// file is written, so a directory that is there is complete. Every later run +// executes the program at once. The frame carries its content's checksum, and +// zstd refuses a payload that does not match it. +// +// Nothing runs from a directory another user owns. On POSIX the cache's +// directories this program makes (`mcpp/`, `single-file/`) and the tree it runs +// from are this user's and no links, or the program refuses: another user who +// owned one could have put a program of theirs under the payload's name. +#include + +import std; +import mcpplibs.os; + +extern "C" const unsigned char mcpp_carried_begin[]; +extern "C" const unsigned char mcpp_carried_end[]; +extern "C" const char mcpp_carried_name[]; +extern "C" const char mcpp_carried_digest[]; + +namespace { + +namespace fs = std::filesystem; +namespace os = mcpplibs::os; + +const std::string_view kName{mcpp_carried_name}; +const std::string_view kDigest{mcpp_carried_digest}; + +int fail(std::string_view what) { + std::cerr << std::format("{}: {}\n", kName, what); + return 127; +} + +bool safe(std::string_view name) { + if (name.empty() || name.front() == '/' || name.find('\\') != std::string_view::npos + || (name.size() > 1 && name[1] == ':')) + return false; + for (std::size_t at = 0; at <= name.size();) { + auto slash = name.find('/', at); + if (slash == std::string_view::npos) slash = name.size(); + if (name.substr(at, slash - at) == "..") return false; + at = slash + 1; + } + return true; +} + +std::optional extract(const fs::path& into) { + const auto* begin = reinterpret_cast(mcpp_carried_begin); + const auto size = static_cast(mcpp_carried_end - mcpp_carried_begin); + const auto content = ZSTD_getFrameContentSize(begin, size); + if (content == ZSTD_CONTENTSIZE_ERROR || content == ZSTD_CONTENTSIZE_UNKNOWN) + return "the carried payload is not a zstd frame"; + std::string tree(static_cast(content), '\0'); + const auto n = ZSTD_decompress(tree.data(), tree.size(), begin, size); + if (ZSTD_isError(n)) return std::format("the carried payload: {}", ZSTD_getErrorName(n)); + std::string_view t(tree); + if (!t.starts_with("MCPPTREE1\n")) return "the carried payload is not a tree"; + std::size_t at = 10; + auto get = [&](int bytes) { + std::uint64_t v = 0; + for (int i = 0; i < bytes; ++i) v |= std::uint64_t(static_cast(t[at + i])) << (8 * i); + at += static_cast(bytes); + return v; + }; + std::error_code ec; + while (at < t.size()) { + if (at + 13 > t.size()) return "the carried tree is truncated"; + const char kind = t[at++]; + const auto pathLen = get(4); + if (at + pathLen + 8 > t.size()) return "the carried tree is truncated"; + const std::string_view name = t.substr(at, pathLen); + at += pathLen; + const auto dataLen = get(8); + if (at + dataLen > t.size()) return "the carried tree is truncated"; + const std::string_view data = t.substr(at, dataLen); + at += dataLen; + if (!safe(name)) return std::format("the carried tree names '{}'", name); + const auto target = into / fs::path(std::string(name)); + fs::create_directories(target.parent_path(), ec); + if (kind == 'd') { fs::create_directories(target, ec); continue; } + if (kind == 'l') { + if (auto r = os::fs::create_link(fs::path(std::string(data)), target, false); !r) + return std::format("cannot link {}", target.string()); + continue; + } + std::ofstream out(target, std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast(data.size())); + if (!out) return std::format("cannot write {}", target.string()); + out.close(); + if (kind == 'x') (void)os::fs::set_executable(target, true); + } + std::ofstream(into / ".mcpp-complete") << kDigest << '\n'; + return std::nullopt; +} + +// Whether `dir` is a directory, not a link, and (on POSIX) this user's. +bool own(const fs::path& dir) { + std::error_code ec; + if (!fs::is_directory(fs::symlink_status(dir, ec))) return false; + if constexpr (os::is_posix) return os::fs::owner_of(dir) == os::fs::owner::this_user; + else return true; +} + +int foreign(const fs::path& dir) { + return fail(std::format("{} is not a directory of this user's; nothing is run from it", dir.string())); +} + +} // namespace + +int main(int argc, char** argv) { + auto cache = os::dirs::cache(); + if (!cache) return fail("this machine names no cache directory for its user"); + const fs::path base = *cache / "mcpp" / "single-file"; + const fs::path root = base / std::string(kDigest); + std::error_code ec; + fs::create_directories(base, ec); + if (!own(base.parent_path())) return foreign(base.parent_path()); + if (!own(base)) return foreign(base); + if (!fs::exists(root / ".mcpp-complete", ec)) { + if constexpr (os::is_posix) + fs::permissions(base, fs::perms::owner_all, fs::perm_options::replace, ec); + const auto self = os::process::self(); + const fs::path tmp = base / std::format(".{}-{}", kDigest, + self ? self->id : std::uint64_t{0}); + fs::remove_all(tmp, ec); + if (auto why = extract(tmp)) { fs::remove_all(tmp, ec); return fail(*why); } + if (auto r = os::fs::rename(tmp, root, /*replace=*/false); !r) { + // Another run placed it first; that one is complete. + fs::remove_all(tmp, ec); + if (!fs::exists(root / ".mcpp-complete", ec)) + return fail(std::format("cannot place the program under {}", root.string())); + } + } + if (!own(root)) return foreign(root); + const fs::path program = root / fs::path(std::string(kName)); + os::process::spawn_options o; + o.program = program; + for (int i = 1; i < argc; ++i) o.args.emplace_back(argv[i]); + o.in = o.out = o.err = os::process::stdio::inherit; + (void)os::process::exec(std::move(o)); + return fail(std::format("cannot start {}", program.string())); +} +)MCPP"; +} + +// ─── the distribution description ───────────────────────────────────────── + +namespace { + +// The xlings system key of a triple's system. +std::string xlings_system(std::string_view triple) { + if (triple.find("-windows") != std::string_view::npos) return "windows"; + if (triple.find("-macos") != std::string_view::npos || triple.find("-darwin") != std::string_view::npos + || triple.find("-apple") != std::string_view::npos) + return "macosx"; + return "linux"; +} + +// A text inside a Lua double-quoted literal: what the metadata states is +// written as a string, never as code. +std::string lua_text(std::string_view s) { + std::string out; + for (unsigned char c : s) { + if (c == '\\' || c == '"') { out += '\\'; out += static_cast(c); } + else if (c < 0x20 || c == 0x7f) out += std::format("\\{:03}", static_cast(c)); + else out += static_cast(c); + } + return out; +} + +std::string lua_list(const std::vector& v) { + std::string out = "{"; + for (std::size_t i = 0; i < v.size(); ++i) out += std::format("{}\"{}\"", i ? ", " : "", lua_text(v[i])); + return out + "}"; +} + +} // namespace + +std::expected xpkg_descriptor(const std::vector& assets, + std::string_view urlPrefix) { + if (assets.empty()) return std::unexpected(std::string("no package to describe")); + auto const& first = assets.front().pkg; + std::string prefix(urlPrefix); + while (!prefix.empty() && prefix.back() == '/') prefix.pop_back(); + if (prefix.empty()) + return std::unexpected(std::string("--url names where the packages are published")); + for (auto const& a : assets) + if (a.pkg.name != first.name || a.pkg.version != first.version) + return std::unexpected(std::format("{} is {} {}, and {} is {} {}: one entry describes one " + "package", assets.front().file.filename().string(), + first.name, first.version, a.file.filename().string(), + a.pkg.name, a.pkg.version)); + const bool universal = !first.runner.empty(); + std::vector archs; + // system -> block text + std::map blocks; + if (universal) { + if (assets.size() != 1) + return std::unexpected(std::string("a universal package is one asset; merge its legs first " + "(mcpp pack --merge)")); + auto const& a = assets.front(); + for (auto const& [arch, _] : first.entry) archs.push_back(arch); + std::string deps; + if (first.mode != "self-contained") + deps = std::format(" deps = {{ \"{}\" }},\n", + lua_text(std::filesystem::path(first.runner.front()).stem().string())); + for (auto system : { "linux", "macosx", "windows" }) + blocks[system] = std::format( + "{0} [\"latest\"] = {{ ref = \"{1}\" }},\n" + " [\"{1}\"] = {{\n url = \"{2}/{3}\",\n sha256 = \"{4}\",\n }},\n", + deps, lua_text(first.version), lua_text(prefix), lua_text(a.file.filename().string()), + lua_text(a.sha256)); + } else { + // system -> (arch -> asset) + std::map> bySystem; + for (auto const& a : assets) { + if (a.pkg.legs.size() != 1) + return std::unexpected(std::format("{} holds {} legs; an entry per system describes " + "packages of one leg", a.file.filename().string(), + a.pkg.legs.size())); + const auto& triple = a.pkg.legs.front(); + const auto arch = triple.substr(0, triple.find('-')); + bySystem[xlings_system(triple)][arch] = &a; + if (std::ranges::find(archs, arch) == archs.end()) archs.push_back(arch); + } + for (auto const& [system, byArch] : bySystem) { + // One URL with `${arch}`: the file names may differ by the + // architecture alone. + std::string tmpl; + std::string shas; + for (auto const& [arch, a] : byArch) { + auto name = a->file.filename().string(); + if (auto at = name.find(arch); at != std::string::npos) name.replace(at, arch.size(), "${arch}"); + if (!tmpl.empty() && tmpl != name) + return std::unexpected(std::format("the {} packages' names differ beyond the " + "architecture ({} and {})", system, tmpl, name)); + tmpl = name; + shas += std::format(" [\"{}\"] = \"{}\",\n", lua_text(arch), + lua_text(a->sha256)); + } + blocks[system] = std::format( + " [\"latest\"] = {{ ref = \"{0}\" }},\n" + " [\"{0}\"] = {{\n url = \"{1}/{2}\",\n sha256 = {{\n{3} }},\n }},\n", + lua_text(first.version), lua_text(prefix), lua_text(tmpl), shas); + } + } + std::ranges::sort(archs); + std::string xpm; + for (auto const& [system, text] : blocks) xpm += std::format(" {} = {{\n{} }},\n", system, text); + + // The command a person runs, and the entry's name: the launcher of a + // universal package (named after its images, `bin/`), else the + // program -- not the package, which may be named otherwise + // (`mcpp-language-server` runs as `mcppls`). + std::string command; + if (universal && !first.entry.empty()) + command = std::filesystem::path(first.entry.begin()->second).stem().string(); + else if (!first.program.empty()) + command = std::filesystem::path(first.program).stem().string(); + if (command.empty()) command = first.name; + // Where the program lies in the installed tree. + std::string bindir = "\"bin\"", alias; + if (universal) { + alias = std::format("if os.host() == \"windows\" then opts.alias = \"{}.cmd\" end\n ", + lua_text(command)); + } else { + const auto program = std::filesystem::path(first.program); + bindir = program.has_parent_path() ? std::format("\"{}\"", lua_text(program.parent_path().generic_string())) + : std::string("\".\""); + } + // The archives' top-level directory: the one of a single asset, else what + // the assets' names share (they differ by system and architecture). + std::string top = assets.front().top; + for (auto const& a : assets) { + const auto [p, q] = std::ranges::mismatch(top, a.top); + top.erase(static_cast(p - top.begin())); + } + const auto wrapper = lua_text(assets.size() == 1 ? top : top + "*"); + return std::format( + "-- Generated by `mcpp emit xpkg --package` from the packages' mcpp-package.json\n" + "-- (SPEC-014 §8).\n" + "package = {{\n" + " spec = \"2\",\n" + " name = \"{0}\",\n" + " description = \"{8} {1}{2}\",\n" + " type = \"package\",\n" + " archs = {3},\n" + " programs = {{\"{0}\"}},\n" + " xvm_enable = true,\n" + " xpm = {{\n{4} }},\n" + "}}\n\n" + "import(\"xim.libxpkg.pkginfo\")\n" + "import(\"xim.libxpkg.xvm\")\n\n" + "function install()\n" + " -- The archive's one top-level directory is the package.\n" + " os.tryrm(pkginfo.install_dir())\n" + " for _, d in ipairs(os.dirs(\"{5}\")) do\n" + " os.mv(d, pkginfo.install_dir())\n" + " return true\n" + " end\n" + " return false\n" + "end\n\n" + "function config()\n" + " local opts = {{ bindir = path.join(pkginfo.install_dir(), {6}) }}\n" + " {7}xvm.add(\"{0}\", opts)\n" + " return true\n" + "end\n\n" + "function uninstall()\n" + " xvm.remove(\"{0}\")\n" + " return true\n" + "end\n", + lua_text(command), lua_text(first.version), + universal ? std::format(", one package for every system ({})", lua_text(first.platform)) : std::string(), + lua_list(archs), xpm, wrapper, bindir, alias, lua_text(first.name)); +} + +// ─── --merge ────────────────────────────────────────────────────────────── + +namespace { + +// `p` absolute, normalised, and with a name: `pkg/` is `pkg`, `.` the current +// directory. +fs::path named(const fs::path& p) { + std::error_code ec; + auto out = fs::absolute(p, ec).lexically_normal(); + if (!out.has_filename() && out.has_relative_path()) out = out.parent_path(); + return out; +} + +} // namespace + +std::expected work_directory(std::string_view purpose) { + std::error_code ec; + const auto base = fs::temp_directory_path(ec); + if (ec) return std::unexpected(std::format("no temporary directory: {}", ec.message())); + std::random_device rd; + for (int attempt = 0; attempt < 16; ++attempt) { + const auto p = base / std::format("mcpp-{}-{:08x}{:08x}", purpose, rd(), rd()); + if (!fs::create_directory(p, ec)) { + if (ec) return std::unexpected(std::format("cannot create '{}': {}", p.string(), ec.message())); + continue; // another's + } + fs::permissions(p, fs::perms::owner_all, fs::perm_options::replace, ec); + return p; + } + return std::unexpected(std::format("no new directory could be made under '{}'", base.string())); +} + +std::expected merge(const MergeRequest& req) { + if (req.inputs.size() < 2) + return std::unexpected(std::string("--merge reads two packages or more")); + std::error_code ec; + auto made = work_directory("merge"); + if (!made) return std::unexpected(made.error()); + const auto work = *made; + struct Cleanup { fs::path p; ~Cleanup() { std::error_code e; fs::remove_all(p, e); } } cleanup{work}; + + std::vector inputs; + for (auto const& i : req.inputs) inputs.push_back(named(i)); + std::vector> trees; + for (std::size_t i = 0; i < inputs.size(); ++i) { + auto root = unpack(inputs[i], work / std::format("in{}", i)); + if (!root) return std::unexpected(root.error()); + auto pkg = read(*root); + if (!pkg) return std::unexpected(pkg.error()); + trees.emplace_back(*root, std::move(*pkg)); + } + auto const& first = trees.front().second; + for (std::size_t i = 1; i < trees.size(); ++i) { + auto const& p = trees[i].second; + auto differ = [&](std::string_view field, std::string const& a, std::string const& b) + -> std::optional { + if (a == b) return std::nullopt; + return std::format("--merge reads one package's legs and systems: {} has {} '{}', and {} " + "has '{}'", req.inputs.front().string(), field, a, req.inputs[i].string(), b); + }; + for (auto r : { differ("name", first.name, p.name), differ("version", first.version, p.version), + differ("platform", first.platform, p.platform), + differ("linkage", first.linkage, p.linkage), differ("mode", first.mode, p.mode) }) + if (r) return std::unexpected(*r); + } + const auto wrapper = trees.front().first.filename(); + + // The container: as asked, else as the output's name says, else as the + // first input was written. + std::string format = req.format; + const auto outName = req.output.filename().string(); + const auto firstName = !req.output.empty() && (outName.ends_with(".zip") || outName.find(".tar") != std::string::npos) + ? outName : inputs.front().filename().string(); + if (format.empty()) + format = firstName.ends_with(".zip") ? "zip" + : firstName.find(".tar") != std::string::npos ? "tar" + : fs::is_directory(inputs.front(), ec) ? "dir" : "tar"; + if (format != "tar" && format != "zip" && format != "dir") + return std::unexpected(std::format("--merge writes tar, zip or dir; '{}' is a format a package " + "provides, which reads a staged tree: merge to dir first", + format)); + auto compression = req.compression; + if (!compression.stated && format == "tar") { + using mcpp::pack::compress::Algorithm; + compression.algorithm = firstName.ends_with(".tar.zst") ? Algorithm::Zstd + : firstName.ends_with(".tar.xz") ? Algorithm::Xz + : firstName.ends_with(".tar") ? Algorithm::None + : Algorithm::Gzip; + } + // The compression fits the container and the name the output is given. + { + using mcpp::pack::compress::Algorithm; + const auto a = compression.algorithm; + if (compression.stated && format == "dir") + return std::unexpected(std::string("--compress compresses a container, and the merge writes a " + "directory")); + if (compression.stated && format == "zip" && (a == Algorithm::Zstd || a == Algorithm::Xz)) + return std::unexpected(std::format("a zip's members are deflated (gzip) or stored (none); " + "--compress {} is a tar's", mcpp::pack::compress::name(a))); + const auto want = mcpp::pack::compress::tar_extension(a); + const bool namedTar = outName.find(".tar") != std::string::npos || outName.ends_with(".tgz"); + if (format == "tar" && namedTar && !outName.ends_with(want) + && !(a == Algorithm::Gzip && outName.ends_with(".tgz"))) + return std::unexpected(std::format("-o {} names another compression than the archive's ({})", + outName, want)); + } + const auto ext = format == "zip" ? std::string(".zip") : format == "dir" ? std::string() + : std::string(mcpp::pack::compress::tar_extension(compression.algorithm)); + const fs::path dest = named(req.output.empty() ? fs::path(wrapper.string() + ext) : req.output); + + // What is there already is replaced only when it is an archive this + // command wrote and none of the inputs: --merge never removes a directory, + // nor a file it cannot read as a package. + for (std::size_t i = 0; i < inputs.size(); ++i) + if (dest == inputs[i] || fs::equivalent(dest, inputs[i], ec)) + return std::unexpected(std::format("--merge: the output {} is the input {}", dest.string(), + req.inputs[i].string())); + if (const auto st = fs::symlink_status(dest, ec); fs::exists(st)) { + if (format == "dir") + return std::unexpected(std::format("--merge: {} exists; a merged directory is written as a new " + "one", dest.string())); + auto old = fs::is_regular_file(st) ? unpack(dest, work / "existing") + : std::unexpected(std::string("not a file")); + if (!old || !read(*old)) + return std::unexpected(std::format("--merge: {} exists and is not a package; it is not replaced", + dest.string())); + } + + const auto out = work / "merged" / wrapper; + fs::create_directories(out, ec); + Package merged = first; + merged.legs.clear(); + merged.systems.clear(); + merged.entry.clear(); + Stated stated; + std::map> seen; // path -> (digest, input) + for (std::size_t i = 0; i < trees.size(); ++i) { + auto const& [root, pkg] = trees[i]; + for (auto const& l : pkg.legs) + if (std::ranges::find(merged.legs, l) == merged.legs.end()) merged.legs.push_back(l); + for (auto const& s : pkg.systems) + if (std::ranges::find(merged.systems, s) == merged.systems.end()) merged.systems.push_back(s); + for (auto const& [arch, e] : pkg.entry) merged.entry[arch] = e; + // Each file as it is, not as its package says: a file changed after the + // package was written is refused, and two inputs agree on a file when + // its bytes do. + for (auto const& f : pkg.files) { + const auto digest = sha256_file(root / fs::path(f.path)); + if (digest != f.sha256) + return std::unexpected(std::format( + "--merge: '{}' in {} has sha256 {}, and its {} states {}", f.path, + req.inputs[i].string(), digest.empty() ? std::string("(no such file)") : digest, kFileName, + f.sha256)); + if (auto it = seen.find(f.path); it != seen.end()) { + if (it->second.first != digest) + return std::unexpected(std::format( + "--merge: '{}' is in {} with sha256 {} and in {} with sha256 {}; one package " + "holds one of each file", f.path, it->second.second, it->second.first, + req.inputs[i].string(), digest)); + continue; + } + seen[f.path] = {digest, req.inputs[i].string()}; + stated.role[f.path] = f.role; + if (!f.leg.empty()) stated.leg[f.path] = f.leg; + fs::create_directories((out / f.path).parent_path(), ec); + fs::copy_file(root / f.path, out / f.path, fs::copy_options::overwrite_existing, ec); + if (ec) return std::unexpected(std::format("cannot copy '{}': {}", f.path, ec.message())); + } + } + std::ranges::sort(merged.legs); + std::ranges::sort(merged.systems); + if (auto d = describe(out, merged, stated); !d) return std::unexpected(d.error()); + + if (format == "dir") { + fs::create_directories(dest.parent_path(), ec); + fs::copy(out, dest, fs::copy_options::recursive | fs::copy_options::copy_symlinks, ec); + if (ec) return std::unexpected(std::format("cannot write '{}': {}", dest.string(), ec.message())); + return dest; + } + auto r = format == "zip" ? write_zip(out, dest, compression) : write_tar(out, dest, compression); + if (!r) return std::unexpected(r.error()); + return dest; +} + +} // namespace mcpp::pack::distribution diff --git a/src/pack/library.cppm b/src/pack/library.cppm index 5c56a9811..9d76773fa 100644 --- a/src/pack/library.cppm +++ b/src/pack/library.cppm @@ -42,6 +42,8 @@ import mcpp.pack.relocate; import mcpp.pack.strip; import mcpp.source_kind; // builtin_extension_table — what needs no declaring import mcpp.pack.zip; +import mcpp.pack.compress; +import mcpp.pack.distribution; import mcpplibs.os.platform; import mcpp.build.command_line; import mcpp.run; @@ -89,6 +91,8 @@ struct LibraryPackPlan { std::filesystem::path archivePath; // …/.tar.gz | .zip bool writeArchive = true; // false = `--format dir` bool zip = false; // PE targets get a .zip + mcpp::pack::compress::Choice compression; // the tar's, or the zip's members' + std::string namespace_, packageName, packageVersion, builtBy; std::string targetName; @@ -497,27 +501,15 @@ run_library_pack(const LibraryPackPlan& plan) .source = plan.stagingRoot / rel, }); } - if (auto r = zip::write(plan.archivePath, entries); !r) + zip::WriteOptions zo; + zo.deflate = plan.compression.algorithm != mcpp::pack::compress::Algorithm::None; + if (plan.compression.algorithm == mcpp::pack::compress::Algorithm::Gzip) zo.level = plan.compression.level; + if (auto r = zip::write(plan.archivePath, entries, zo); !r) return std::unexpected(LibraryPackError{ r.error() }); } else { - // `--force-local` on Windows, and it is not optional there: GNU tar - // reads `C:/path/x.tar.gz` as the rsh form `host:path`, tries to resolve - // a machine called `C`, and fails with - // tar (child): Cannot connect to C: resolve failed - // which names neither tar's argument nor the drive letter as the cause. - // - // Reached only by a package with more than one leg: a single PE leg is - // written as a zip, so a Windows host never ran this path until a fat - // package existed. Same fix, same reason, in pack.cppm's make_tarball. - std::vector argv{ "tar" }; - if constexpr (mcpplibs::os::is_windows) argv.push_back("--force-local"); - argv.insert(argv.end(), { "-czf", plan.archivePath.string(), - "-C", plan.stagingRoot.parent_path().string(), - plan.stagingRoot.filename().string() }); - auto r = mcpp::run::execute(argv); - if (r.exit_code != 0) - return std::unexpected(LibraryPackError{ std::format( - "tar failed (rc={}): {}", r.exit_code, r.output) }); + // Written by mcpp, as a program's tar is: the same bytes on every host. + if (auto r = mcpp::pack::distribution::write_tar(plan.stagingRoot, plan.archivePath, plan.compression); !r) + return std::unexpected(LibraryPackError{ r.error() }); } return plan.archivePath; } diff --git a/src/pack/library_pipeline.cppm b/src/pack/library_pipeline.cppm index 58a3dcc62..0fcb96b9c 100644 --- a/src/pack/library_pipeline.cppm +++ b/src/pack/library_pipeline.cppm @@ -38,6 +38,7 @@ import mcpp.pack; import mcpp.pack.abi_tag; import mcpp.pack.interface; import mcpp.pack.library; +import mcpp.pack.compress; import mcpp.pack.strip; import mcpp.toolchain.model; import mcpp.toolchain.dialect; @@ -137,9 +138,19 @@ export int build_and_pack_library(const std::string& targetName, mcpp::build::progress::defer_finished(); } + // A library's package is a tree, a tar or a zip; a single file runs a + // program, and a library has none. + if (opts.format == mcpp::pack::Format::SingleFile) { + mcpp::ui::error(std::format( + "--pkg-format single-file packs a program, and '{}' is a library.\n" + " use: --pkg-format tar | zip | dir", targetName)); + return 2; + } LibraryPackPlan plan; plan.builtBy = std::string(mcpp::MCPP_VERSION); - plan.writeArchive = opts.format == mcpp::pack::Format::Tar; + plan.writeArchive = opts.format == mcpp::pack::Format::Tar + || opts.format == mcpp::pack::Format::Zip; + plan.compression = opts.compression; InterfaceClosure closure; bool haveClosure = false; @@ -529,15 +540,34 @@ export int build_and_pack_library(const std::string& targetName, } // ── where it lands ──────────────────────────────────────────────── - const bool zip = plan.legs.size() == 1 - && plan.legs[0].triple.find("windows") != std::string::npos; + // The container: as stated, else a zip for a Windows leg and a tar + // elsewhere, as a program's is. + const bool zip = opts.format == mcpp::pack::Format::Zip + || (!opts.formatStated && plan.legs.size() == 1 + && plan.legs[0].triple.find("windows") != std::string::npos); auto dirName = plan.legs.size() == 1 ? std::format("{}-{}-{}", plan.packageName, plan.packageVersion, plan.legs[0].abiTag) : std::format("{}-{}", plan.packageName, plan.packageVersion); plan.zip = zip; + // A zip deflates or stores, as a program's does (SPEC-014 §5.1): a zstd or + // xz the command stated is refused, the project's default left aside. + if (zip && (plan.compression.algorithm == mcpp::pack::compress::Algorithm::Zstd + || plan.compression.algorithm == mcpp::pack::compress::Algorithm::Xz)) { + if (opts.compressionFromFlag) { + mcpp::ui::error(std::format( + "'{}' is packed as a zip, whose members are deflated or stored; --compress {} is a " + "tar's.\n use: --pkg-format tar --compress {}, or --compress gzip | none", targetName, + mcpp::pack::compress::name(plan.compression.algorithm), + mcpp::pack::compress::name(plan.compression.algorithm))); + return 2; + } + plan.compression = {}; + } plan.stagingRoot = plan.projectRoot / "target" / "dist" / dirName; plan.archivePath = plan.projectRoot / "target" / "dist" - / (dirName + (zip ? ".zip" : ".tar.gz")); + / (dirName + (zip ? std::string(".zip") + : std::string(mcpp::pack::compress::tar_extension( + plan.compression.algorithm)))); if (!opts.output.empty()) { auto o = std::filesystem::path(opts.output); plan.archivePath = o.has_parent_path() ? o diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 873cc30c3..9e1762444 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -47,6 +47,9 @@ import mcpp.pack.relocate; import mcpp.pack.stage_tree; import mcpp.pack.strip; import mcpp.pack.zip; +import mcpp.pack.compress; +import mcpp.pack.distribution; +import mcpp.modgraph.glob; // [pack] include / exclude import mcpplibs.os.platform; import mcpp.toolchain.platform; import mcpp.invocation; @@ -61,6 +64,7 @@ import mcpp.toolchain.triple; import mcpp.toolchain.base; import mcpp.ui; import mcpp.runtime.elf; +import mcpp.platform; export namespace mcpp::pack { @@ -81,7 +85,28 @@ enum class Mode { None, Static, BundleProject, BundleAll }; // anyone else's release. dpkg's control fields, AppImage's runtime, WiX's // schema and Apple's notarisation each couple an mcpp release to a release mcpp // does not control. -enum class Format { Tar, Dir, Dispatched }; +enum class Format { Tar, Dir, Zip, SingleFile, Dispatched }; + +// THE LINKAGE AXIS (SPEC-014 §1): a property of the program -- whether the +// libraries of its graph are in it or beside it -- which a pack states for the +// build it makes (`--linkage`, `[pack] linkage`). Unstated: as the project +// builds it. +enum class Linkage { Unstated, Static, Shared }; + +// The container an archive format writes, decided once in `make_plan`: a tar +// (compressed as `Options::compression` says), a zip, or none (a directory, a +// single file, a dispatched format). +enum class ArchiveKind { None, Tar, Zip }; + +// One more architecture of a universal package (SPEC-014 §3): a leg of the +// platform's targets other than the one the pack built first, built by the +// pipeline before staging. `artifact` is that leg's program image; the images +// it names are found beside it, as for the first leg. +struct LoadedLeg { + std::string triple; // canonical + std::string arch; // the directory under `lib/` + std::filesystem::path artifact; +}; // What happens when a FORMAT's dependency closure cannot be resolved on this // host, as a function of the format alone — never of the reason (a Mach-O @@ -105,6 +130,29 @@ ClosureUnavailableOutcome closure_unavailable_outcome(Format format) { struct Options { Mode mode = Mode::BundleProject; + // The three axes (SPEC-014 §1) beyond `mode` and `format`: the linkage + // the pack builds with, whether the format was stated (a format left to + // the target is a zip for a Windows program and a tar elsewhere), and the + // container's compression. + Linkage linkage = Linkage::Unstated; + bool formatStated = false; + mcpp::pack::compress::Choice compression; + bool compressionFromFlag = false; // --compress, not [pack] + // Universal packages (SPEC-014 §3). `universal`: nullopt follows the + // platform's `universal`; false packs the selected target alone. When the + // pipeline decides a universal package it sets `universalName` (the + // platform's name, which names the package instead of a triple) and + // `systems` (the systems whose loaders a self-contained package carries, + // ``, from `--systems`; empty: this machine's). + std::optional universal; + std::string universalName; + std::vector systems; + // Several `--target` naming a universal platform's targets: the legs, in + // place of the platform's whole `targets` (SPEC-014 §1.4). + std::vector legTargets; + // `[pack] sign`: run on each program, image, library and loader of the + // staged tree before the container is written, `{}` the file. + std::vector sign; // The build stated its target base (design 2026-10-23 §4.11): the base it // was built against becomes the C library floor the package states. bool baseStated = false; @@ -229,6 +277,14 @@ struct Plan { std::string binaryName; // basename(builtBinary) std::filesystem::path stagingRoot; // target/dist// std::filesystem::path archivePath; // …/.tar.gz | .zip + ArchiveKind archive = ArchiveKind::None; + // The linkage the package's `mcpp-package.json` states: `static`, + // `shared`, or `declared` (each library as it declares itself). Set by the + // caller from the build. + std::string linkageName = "declared"; + // The other legs of a universal package (SPEC-014 §3), set by the caller + // after `make_plan` as `extraSharedLegs` is. + std::vector loadedLegs; std::string packageName; std::string packageVersion; std::string triple; // e.g. "x86_64-linux-musl" @@ -251,10 +307,13 @@ struct Plan { // the file `.js` in the first place (see `artifact_naming`), so it is the // one fact this format IS recorded under. bool targetIsWasm = false; - // Is the artifact an ok image (`-openkal`, openkal SPEC 12)? From the - // triple, as for wasm: the file is an ELF shared object that no loader of - // this machine runs, and its closure is the images it names. - bool targetIsOkImage = false; + // Is the artifact one a loader runs (an ok image, `-openkal`)? From + // the triple, as for wasm: the file is an ELF shared object that no loader + // of this machine runs, and its closure is the images it names. + bool targetIsLoaded = false; + // The target's platform (SPEC-013): the carrier a self-contained pack of + // a loaded artifact is built over (§4.7). + mcpp::platform::Resolved platform; // #622 A3/A10: is `builtBinary` an `Application` target whose link form // on THIS row is a shared object (`*-linux-android`, // `toolchain::triple::application_form`)? Read from the CALLER, which @@ -473,6 +532,24 @@ make_plan(const mcpp::manifest::Manifest& manifest, std::expected run(const Plan& plan, const mcpp::config::GlobalConfig& cfg); +// THE SINGLE FILE (SPEC-014 §5.2), made from the tree `run` staged: for an +// image, the platform's carrier carrying it (or, when it names shared images, +// a stored zip of all of them aligned for mapping), one file per system of +// `Options::systems`; for a native program, the engine's launcher carrying the +// tree as a zstd frame. Returns the files written. +std::expected, Error> single_file(const Plan& plan); + +// The row a program of `system` (linux | macos | windows) and `arch` is built +// on -- an image's single file, a universal package's runner -- when this mcpp +// builds no program for it: a row its table states as planned (`x86_64-macos`) +// or does not state (`aarch64-windows-gnu`). Empty when it builds one. +std::string unbuilt_row(std::string_view system, std::string_view arch); + +// The tree made a package: signed and described (`mcpp-package.json`). `run` +// does it before writing a container; a dispatched format whose tree was +// staged without its closure is finalized by the caller. +std::expected finalize_staged(const Plan& plan); + // Helpers used by cli.cppm to render mode names + parse `--mode`. // Canonical name shown in `--help`/diagnostics (renamed for legibility). std::string_view mode_cli_name(Mode m); @@ -536,9 +613,11 @@ namespace detail { // `.zip` whoever built it, and a `.tar.gz` full of DLLs is a package most // Windows users cannot open without installing something first. std::string default_archive_name(std::string_view name, std::string_view version, - std::string_view triple, Mode mode, bool pe) + std::string_view triple, Mode mode, ArchiveKind kind, + mcpp::pack::compress::Algorithm compression) { - std::string_view ext = pe ? ".zip" : ".tar.gz"; + std::string_view ext = kind == ArchiveKind::Zip ? std::string_view(".zip") + : mcpp::pack::compress::tar_extension(compression); auto sfx = mode_tarball_suffix(mode); if (sfx.empty()) return std::format("{}-{}-{}{}", name, version, triple, ext); @@ -552,11 +631,14 @@ std::string default_archive_name(std::string_view name, std::string_view version std::string wrapper_dirname_from_archive(const std::filesystem::path& archive) { auto name = archive.filename().string(); for (auto suffix : {std::string_view{".tar.gz"}, std::string_view{".tgz"}, - std::string_view{".zip"}}) { + std::string_view{".tar.zst"}, std::string_view{".tar.xz"}, + std::string_view{".tar"}, std::string_view{".zip"}}) { if (name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); } - // No recognised compression suffix — fall back to the bare stem. - return archive.stem().string(); + // No archive's suffix: the name as written (`-o app-0.1.0-openkal` with + // `--pkg-format dir` is that directory; a stem would cut at the version's + // last dot and write `app-0.1`). + return name; } } // namespace detail @@ -661,7 +743,7 @@ make_plan(const mcpp::manifest::Manifest& manifest, if (auto t = mcpp::toolchain::triple::parse(p.triple)) { p.targetIsWasm = t->object_format() == mcpp::toolchain::triple::ObjectFormat::Wasm; - p.targetIsOkImage = t->is_openkal(); + p.targetIsLoaded = t->loader_supplies_os(); } // THE CONTRACT REACHES PACKAGING. Until now it stopped at the compile and @@ -706,10 +788,37 @@ make_plan(const mcpp::manifest::Manifest& manifest, if (opts.carryToolchainRuntime) for (auto const& d : opts.toolchainRuntimeDirs) p.searchDirs.push_back(d); + // THE CONTAINER. A format left unstated follows the artifact -- a zip for + // a Windows program, which its users open without installing anything, a + // tar elsewhere; `tar` and `zip` stated are what they say (SPEC-014 §1). + p.archive = opts.format == Format::Zip ? ArchiveKind::Zip + : opts.format != Format::Tar ? ArchiveKind::None + : (p.targetIsPe && !opts.formatStated) ? ArchiveKind::Zip + : ArchiveKind::Tar; + // A zip deflates or stores: a zstd or xz the format left to the artifact + // cannot honour is refused when the command stated it, and the project's + // default leaves it aside (SPEC-014 §5.1). + if (p.archive == ArchiveKind::Zip + && (opts.compression.algorithm == mcpp::pack::compress::Algorithm::Zstd + || opts.compression.algorithm == mcpp::pack::compress::Algorithm::Xz)) { + if (opts.compressionFromFlag) + return std::unexpected(Error{std::format( + "{} is packed as a zip (a Windows program's archive), whose members are deflated " + "or stored; --compress {} is a tar's.\n use: --pkg-format tar --compress {}, or " + "--compress gzip | none", p.binaryName, + mcpp::pack::compress::name(opts.compression.algorithm), + mcpp::pack::compress::name(opts.compression.algorithm))}); + p.opts.compression = {}; + } + // A universal package is named after its platform, not a triple: it is + // the same package on every system and architecture of the platform. + const std::string nameTriple = opts.universalName.empty() ? p.triple : opts.universalName; auto distDir = projectRoot / "target" / "dist"; if (opts.output.empty()) { p.archivePath = distDir / detail::default_archive_name( - p.packageName, p.packageVersion, p.triple, opts.mode, p.targetIsPe); + p.packageName, p.packageVersion, nameTriple, opts.mode, + p.archive == ArchiveKind::None ? ArchiveKind::Tar : p.archive, + opts.compression.algorithm); } else if (!opts.output.has_parent_path()) { // `-o name.tar.gz` (bare filename) → place in target/dist/. // `-o ./name.tar.gz` or `-o sub/name.tar.gz` → use as-is. @@ -723,6 +832,10 @@ make_plan(const mcpp::manifest::Manifest& manifest, // matches what GUI extractors create on click and what `tar -xzf` // produces on the CLI. p.stagingRoot = distDir / detail::wrapper_dirname_from_archive(p.archivePath); + // A single file takes the tree's name in `target/dist`; its tree is + // staged outside it. + if (opts.format == Format::SingleFile) + p.stagingRoot = projectRoot / "target" / "single-file" / "stage" / p.stagingRoot.filename(); p.includeGlobs = manifest.packConfig.include; p.excludeGlobs = manifest.packConfig.exclude; @@ -1213,9 +1326,12 @@ void copy_if_exists(const std::filesystem::path& src, // every one of them, so a missing file is an error naming it rather than a // bundle that silently lacks it. std::expected -stage_runtime_files(const Plan& plan, const std::filesystem::path& stagedExeDir) +stage_runtime_files(const Plan& plan, const std::filesystem::path& stagedExeDir, + const std::filesystem::path& builtExeDir = {}) { - const auto builtDir = plan.builtBinary.parent_path(); + // The leg's own build directory: another architecture's leg deployed its + // own files beside its own artifact. + const auto builtDir = builtExeDir.empty() ? plan.builtBinary.parent_path() : builtExeDir; for (auto const& rel : plan.opts.runtimeFiles) { const auto src = builtDir / rel; const auto dst = stagedExeDir / rel; @@ -1395,27 +1511,152 @@ stage_closure(const Plan& plan, const ClosureRead& read, std::expected make_tarball(const std::filesystem::path& stagingRoot, - const std::filesystem::path& tarballPath) + const std::filesystem::path& tarballPath, + mcpp::pack::compress::Choice compression = {}) { - std::error_code ec; - if (tarballPath.has_parent_path()) - std::filesystem::create_directories(tarballPath.parent_path(), ec); // Pack with a top-level wrapper directory whose name matches the // tarball stem (computed by make_plan via wrapper_dirname_from_tarball). - // This keeps click-to-extract and `tar -xzf` aligned: both surface a - // single self-contained directory in the user's cwd. - // `--force-local` on Windows: GNU tar reads `C:/path/x.tar.gz` as the rsh - // form `host:path` and dies with `Cannot connect to C: resolve failed`, - // naming neither its argument nor the drive letter. Latent here rather than - // observed — a PE target is written as a zip — but it is the same command - // with the same hazard as the library packer's, and the two should not - // differ in whether they survive being run on Windows. - std::vector argv{ "tar" }; - if constexpr (mcpplibs::os::is_windows) argv.push_back("--force-local"); - argv.insert(argv.end(), { "-czf", tarballPath.string(), "-C", stagingRoot.parent_path().string(), - stagingRoot.filename().string() }); - auto r = run_capture(argv); - if (!r) return std::unexpected(Error{r.error()}); + // This keeps click-to-extract and `tar -xf` aligned: both surface a + // single self-contained directory in the user's cwd. The codec is mcpp's + // own (mcpp.pack.distribution::write_tar). + if (auto r = mcpp::pack::distribution::write_tar(stagingRoot, tarballPath, compression); !r) + return std::unexpected(Error{r.error()}); + return {}; +} + +std::expected +make_zip(const std::filesystem::path& stagingRoot, const std::filesystem::path& archive, + mcpp::pack::compress::Choice compression, std::string_view program = {}) +{ + if (auto r = mcpp::pack::distribution::write_zip(stagingRoot, archive, compression, program); !r) + return std::unexpected(Error{r.error()}); + return {}; +} + +// THE TREE MADE A PACKAGE (SPEC-014 §4.5, §7): each program, image, library +// and loader signed by `[pack] sign`, then every file described in +// `mcpp-package.json` with its role, its leg and its digest. Before the +// container, so the archive holds what the document says. +// `[pack] include`: the project's files the globs name, at the same path +// below the tree's root, once whatever the legs (a `share/**` is the +// package's, not an architecture's); `[pack] exclude` drops what it names. +std::expected, Error> stage_included(const Plan& plan) { + namespace fs = std::filesystem; + std::vector staged; + if (plan.includeGlobs.empty()) return staged; + std::vector files; + auto excluded = [&](const fs::path& f) { + return std::ranges::any_of(plan.excludeGlobs, [&](std::string const& g) { + return mcpp::modgraph::path_matches_glob(f, plan.projectRoot, g); + }); + }; + auto onFile = [&](const fs::path& f) { + const bool named = std::ranges::any_of(plan.includeGlobs, [&](std::string const& g) { + return mcpp::modgraph::path_matches_glob(f, plan.projectRoot, g); + }); + if (named && !excluded(f)) files.push_back(f); + }; + mcpp::modgraph::walk_glob_tree(plan.projectRoot, plan.projectRoot, + [](const fs::path&) { return true; }, onFile); + std::error_code ec; + for (auto const& f : files) { + const auto to = plan.stagingRoot / f.lexically_relative(plan.projectRoot); + fs::create_directories(to.parent_path(), ec); + fs::copy_file(f, to, fs::copy_options::overwrite_existing, ec); + if (ec) return std::unexpected(Error{std::format("[pack] include: cannot stage {}: {}", + f.string(), ec.message())}); + staged.push_back(f.lexically_relative(plan.projectRoot).generic_string()); + } + return staged; +} + +std::expected finalize_tree(const Plan& plan) { + namespace fs = std::filesystem; + namespace d = mcpp::pack::distribution; + std::error_code ec; + if (!fs::is_directory(plan.stagingRoot, ec)) return {}; + auto included = stage_included(plan); + if (!included) return std::unexpected(included.error()); + const auto canonical = [](std::string const& t) { + if (t.empty()) return mcpp::toolchain::triple::host_triple().str(); + auto parsed = mcpp::toolchain::triple::parse(t); + return parsed ? parsed->str() : t; + }; + d::Package pkg; + pkg.name = plan.packageName; + pkg.version = plan.packageVersion; + if (plan.platform.active) pkg.platform = std::format("{}@{}", plan.platform.package, plan.platform.version); + pkg.linkage = plan.linkageName; + pkg.mode = std::string(mode_cli_name(plan.opts.mode == Mode::Static && plan.targetIsLoaded + ? Mode::BundleAll : plan.opts.mode)); + pkg.legs.push_back(canonical(plan.triple)); + for (auto const& l : plan.loadedLegs) pkg.legs.push_back(l.triple); + for (auto const& l : plan.extraSharedLegs) pkg.legs.push_back(l.triple); + d::Stated stated; + if (!plan.opts.universalName.empty()) { + pkg.runner = plan.platform.fields.runner.cmd; + const auto stem = fs::path(plan.binaryName).stem().string(); + stated.role["bin/" + stem] = "launcher"; + stated.role["bin/" + stem + ".cmd"] = "launcher"; + std::vector> legs{ + { canonical(plan.triple), [&] { auto t = mcpp::toolchain::triple::parse(plan.triple); + return t ? t->arch : plan.triple; }() } }; + for (auto const& l : plan.loadedLegs) legs.emplace_back(l.triple, l.arch); + // Every file of `lib//` is an image of that leg. + auto state_leg = [&](std::string const& triple, std::string const& arch) { + for (auto const& e : fs::directory_iterator(plan.stagingRoot / "lib" / arch, ec)) { + const auto rel = std::format("lib/{}/{}", arch, e.path().filename().string()); + stated.role[rel] = "image"; + stated.leg[rel] = triple; + } + }; + for (auto const& [triple, arch] : legs) { + pkg.entry[arch] = std::format("lib/{}/{}", arch, plan.binaryName); + state_leg(triple, arch); + } + pkg.systems = d::loader_systems(plan.stagingRoot); + } else { + pkg.program = plan.targetIsPe ? plan.binaryName : "bin/" + plan.binaryName; + if (plan.targetIsLoaded && plan.opts.mode == Mode::BundleAll) + pkg.program = "bin/" + fs::path(plan.binaryName).stem().string() + + std::string(mcpplibs::os::exe_suffix); + stated.role[pkg.program] = "program"; + } + // What `[pack] include` named is the package's resource, whatever its name. + for (auto const& rel : *included) stated.role[rel] = "resource"; + auto described = d::describe(plan.stagingRoot, pkg, stated); + if (!described) return std::unexpected(Error{described.error()}); + if (plan.opts.sign.empty()) return {}; + for (auto const& f : described->files) { + if (f.role != "program" && f.role != "image" && f.role != "library" && f.role != "loader") + continue; + std::vector argv; + for (auto const& a : plan.opts.sign) + argv.push_back(a == "{}" ? (plan.stagingRoot / f.path).string() : a); + int spawnError = 0; + auto r = mcpp::run::capture_exec(argv, {}, {}, &spawnError); + if (spawnError != 0 || r.exit_code != 0) + return std::unexpected(Error{std::format( + "[pack] sign failed on {} (exit {}): {}\n{}", f.path, r.exit_code, + mcpp::build::command_line::display(argv), r.output)}); + } + // The digests of what was signed. + if (auto again = d::describe(plan.stagingRoot, pkg, stated); !again) + return std::unexpected(Error{again.error()}); + return {}; +} + +// The container of a built-in archive format, or nothing for a directory, a +// single file or a dispatched format; the tree is finalized first. +std::expected write_container(const Plan& plan) { + if (auto r = finalize_tree(plan); !r) return r; + switch (plan.archive) { + case ArchiveKind::None: return {}; + case ArchiveKind::Tar: return make_tarball(plan.stagingRoot, plan.archivePath, + plan.opts.compression); + case ArchiveKind::Zip: return make_zip(plan.stagingRoot, plan.archivePath, + plan.opts.compression, plan.binaryName); + } return {}; } @@ -1883,35 +2124,12 @@ run_pe(const Plan& plan) return std::unexpected(r.error()); } - if (plan.opts.format != Format::Tar) return needs; - - std::vector entries; - const auto wrapper = plan.stagingRoot.filename().string(); - for (auto const& e : - std::filesystem::recursive_directory_iterator(plan.stagingRoot, ec)) { - if (!e.is_regular_file(ec)) continue; - auto rel = std::filesystem::relative(e.path(), plan.stagingRoot, ec); - if (ec) continue; - entries.push_back({ - wrapper + "/" + rel.generic_string(), - e.path(), - e.path().filename() == plan.builtBinary.filename(), - }); - } - // Deterministic order: a directory iteration order that leaks into an - // archive is how two identical builds get two different checksums. - // - // Plain `std::sort` with an explicit comparator, not - // `std::ranges::sort(entries, {}, &Entry::name)`. A ranges projection - // naming a member of an IMPORTED type is one of three module-boundary - // shapes this function used to carry, and together they crashed the clang - // 20.1.7 frontend (0xC0000005, no diagnostic) on every Windows job. See - // the note at the top of run_pe. - std::sort(entries.begin(), entries.end(), - [](const mcpp::pack::zip::Entry& a, - const mcpp::pack::zip::Entry& b) { return a.name < b.name; }); - if (auto r = mcpp::pack::zip::write(plan.archivePath, entries); !r) - return std::unexpected(Error{r.error()}); + // The container: a zip unless `tar` was stated (`write_container`). + // Plain `std::sort` with an explicit comparator there, not a ranges + // projection onto an imported type's member: that shape was one of three + // that crashed the clang 20.1.7 frontend (0xC0000005, no diagnostic) on + // every Windows job. See the note at the top of run_pe. + if (auto r = write_container(plan); !r) return std::unexpected(r.error()); return needs; } @@ -1969,81 +2187,240 @@ run_wasm(const Plan& plan) copy_if_exists(plan.projectRoot / "README.md", plan.stagingRoot); copy_if_exists(plan.projectRoot / "LICENSE", plan.stagingRoot); - if (plan.opts.format != Format::Tar) return {}; - return make_tarball(plan.stagingRoot, plan.archivePath); + return write_container(plan); } -// AN OK IMAGE (`-openkal`; design 2026-10-26 part 2, §8.2 and §8.5). +// AN ARTIFACT A LOADER RUNS (an ok image, `-openkal`; design +// 2026-10-26 part 2, §8.2 and §8.5; SPEC-013 §4.7). // // The modes keep their meaning -- what the machine that runs the package must -// provide -- and on this row they are: +// provide -- and on such a row they are: +// +// system the artifact alone; the machine provides the loader and +// every artifact it names (M2); +// vendored the artifact and the artifacts it names (`DT_NEEDED`), +// beside it; the machine provides the loader (M2, the default); +// self-contained a program of THIS machine that carries the artifact as a +// section and runs it through the platform's CARRIER (M3), +// with the artifacts it names beside it; the machine provides +// nothing. The artifact is not recompiled: the program is the +// carrier package, the carrier's entry source and the bytes, +// built by mcpp with that system's own conventions -- the +// bootstrap boundary (§6.2), so no platform is selected for it. // -// system the image alone; the machine provides `okrun` and every -// image it names (M2); -// vendored the image and the images it names (`DT_NEEDED`), beside it; -// the machine provides `okrun` (M2, the default); -// self-contained a program of THIS machine that carries the image as a -// section and runs it through libokrun (M3), with the images -// it names beside it; the machine provides nothing. The image -// is not recompiled: the program is a generated entry, the -// image's bytes and `openkal-run`, built by mcpp for this -// machine. Another system's M3 is packed on that system. -// static refused: an image is loaded, never linked into one file -// with its loader by `ld`. +// A UNIVERSAL PACKAGE (SPEC-014 §3, §4.2) holds every target of a platform +// that states `universal = true`: one `lib//` per leg, launchers in +// `bin/` that pick the machine's architecture and start the platform's +// runner, and under `self-contained` the runner's program for each system of +// `--systems` and each architecture -- the carrier built with nothing carried, +// which runs the image its first argument names -- under +// `libexec//-/`. +// +// `--linkage static` is a property of the image (no `.okso`), and `--mode +// static` is `--linkage static --mode self-contained`. +namespace { + +// A reference as a dependency entry of the generated manifest. +std::string dependency_entry(const mcpp::manifest::platform::PackageRef& r) { + auto const& sp = r.spec; + const std::string ns = sp.namespace_.empty() ? std::string("mcpplibs") : sp.namespace_; + std::string value; + if (!sp.path.empty()) + value = std::format("{{ path = \"{}\" }}", sp.path); + else if (!sp.git.empty()) + value = std::format("{{ git = \"{}\", {} = \"{}\" }}", sp.git, + sp.gitRefKind.empty() ? "rev" : sp.gitRefKind, sp.gitRev); + else + value = std::format("\"{}\"", sp.version); + return std::format("[dependencies.{}]\n{} = {}\n", ns, sp.shortName, value); +} + +// The system names a package speaks of, and this machine's. +constexpr std::string_view host_system() { + if constexpr (mcpplibs::os::is_windows) return "windows"; + else if constexpr (mcpplibs::os::is_macos) return "macos"; + else return "linux"; +} + +// The triple a program of `system` on `arch` is built for, and the mode it +// is packed in: a Linux program is static (musl), the one form that runs on +// every distribution; a program of this machine's system and architecture on +// macOS or Windows takes this machine's own row; another system's is its +// conventional row. `single`: the program is the whole package (a single +// file), so on Windows it takes its C++ run-time from the system. +struct ProgramRow { std::string triple; std::string mode; std::string platform; }; + +// The platform a generated program of `triple` is built over, as `--platform` +// names it: the target's platform where its hosted arrangement serves the +// triple (SPEC-013 §5.4), else none. A Linux program over the hosted +// arrangement is static on every architecture, its C library and C++ run-time +// built by the graph with the platform's toolchain; the musl row's own payload +// is a cross GCC published for an x86_64 host only. +std::string hosted_platform_for(const mcpp::platform::Resolved* P, std::string_view triple) { + if (!P || !P->active || !P->fields.hosted.set) return {}; + auto t = mcpp::toolchain::triple::parse(triple); + if (!t) return {}; + auto const& targets = P->fields.hosted.targets; + const bool listed = !targets.set || std::ranges::any_of(targets.value, [&](std::string const& x) { + auto h = mcpp::toolchain::triple::parse(x); + return h && h->str() == t->str(); + }); + if (!listed) return {}; + if (!P->selection.ref.spec.path.empty()) return "path:" + P->root.generic_string(); + return std::format("{}@{}", P->package, P->version); +} + +ProgramRow program_row(std::string_view system, std::string_view arch, bool single, + const mcpp::platform::Resolved* P = nullptr) { + if (system == "linux") { + const auto triple = std::format("{}-linux-musl", arch); + return { triple, "static", hosted_platform_for(P, triple) }; + } + // Another system's program is built over the platform's hosted arrangement + // where it serves the row, as Linux's is: the same program whichever + // machine packs it, needing no SDK and no C run-time of that system. + // Measured without it: on Linux an aarch64-macos runner needed the macOS + // SDK no payload carries, and a runner packed on Windows took the MSVC + // row and VCRUNTIME140.dll with it. + const auto triple = std::format("{}-{}", arch, system == "macos" ? "macos" : "windows-gnu"); + const auto mode = system == "macos" || !single ? "vendored" : "system"; + if (auto hosted = hosted_platform_for(P, triple); !hosted.empty()) return { triple, mode, hosted }; + const bool here = system == host_system() && arch == mcpp::toolchain::platform::host_arch; + return { here ? std::string() : triple, mode }; +} + +// The bytes of `carried` as a section of a program of `system`, with the +// symbols the carrier's entry reads; `name` and `digest` as strings. An empty +// `carried` places nothing: `mcpp_carried_begin == mcpp_carried_end`. +std::string carried_assembly(std::string_view system, std::string_view sectionName, + const std::filesystem::path& carried, std::string_view name, + std::string_view digest) { + const bool macho = system == "macos"; + const bool coff = system == "windows"; + const std::string sym = macho ? "_" : ""; + std::string bare(sectionName); + while (!bare.empty() && bare.front() == '.') bare.erase(bare.begin()); + std::string upper = bare; + for (auto& ch : upper) ch = static_cast(std::toupper(static_cast(ch))); + const std::string section = macho ? std::format("\t.section __{},__carried\n", upper.substr(0, 14)) + : coff ? std::format("\t.section .{},\"dr\"\n", bare) + : std::format("\t.section .{},\"a\",@progbits\n", bare); + std::string text = std::format( + "# What the program carries, unchanged, and its name (generated by mcpp pack).\n{}" + "\t.p2align 4\n\t.globl {}mcpp_carried_begin\n{}mcpp_carried_begin:\n", + section, sym, sym); + if (!carried.empty()) + text += std::format("\t.incbin \"{}\"\n", std::filesystem::absolute(carried).generic_string()); + text += std::format( + "\t.globl {0}mcpp_carried_end\n{0}mcpp_carried_end:\n" + "\t.globl {0}mcpp_carried_name\n{0}mcpp_carried_name:\n\t.asciz \"{1}\"\n" + "\t.globl {0}mcpp_carried_digest\n{0}mcpp_carried_digest:\n\t.asciz \"{2}\"\n", + sym, name, digest); + return text; +} + +// A program generated under `dir` (its manifest, entry and carried section) +// packed for `row` as a directory; returns the directory that holds the +// program and what it needs beside it: the tree's `bin/`, or, for a Windows +// program, whose tree is flat, the tree itself. std::expected -okimage_build_selfcontained(const Plan& plan, const std::filesystem::path& image) -{ +pack_generated_program(const std::filesystem::path& dir, const ProgramRow& row) { namespace fs = std::filesystem; - // `openkal-run` from the source the project's `openkal-tools` came from: - // the same repository at the same reference selects its member by key - // (05 -- Dependencies), and a published version has a sibling of the same - // version. - // - // THE DECLARATION THE BUILD RESOLVED: after workspace inheritance, and - // with the conditional tables whose predicate the image's target meets - // (`[target.'cfg(os = "openkal")'.build-dependencies]`, which is where a - // program built for systems as well as images declares it). The raw file's - // `[build-dependencies]` alone missed both. - auto eff = mcpp::project::load_effective_manifest(plan.projectRoot); - if (!eff) return std::unexpected(Error{std::format( - "cannot read {}: {}", (plan.projectRoot / "mcpp.toml").string(), eff.error())}); - const auto& man = eff->manifest; - std::optional tools; - auto find_in = [&](const std::map& deps) { - for (auto const& [key, spec] : deps) - if (key == "openkal-tools" || spec.shortName == "openkal-tools") tools = spec; - }; - find_in(man.buildDependencies); - const auto ctx = mcpp::build::cfgpred::context_for(plan.triple); - for (auto const& cc : man.conditionalConfigs) - if (!mcpp::build::cfgpred::uses_layer(cc.predicate) - && mcpp::build::cfgpred::matches(cc.predicate, ctx)) - find_in(cc.buildDependencies); - if (!tools) return std::unexpected(Error{ - "--mode self-contained of an ok image builds a program over `openkal-run` from the " - "source of the project's `openkal-tools`, and its [build-dependencies] declare none"}); - std::string runSpec; - if (!tools->git.empty()) { - runSpec = std::format("{{ git = \"{}\", {} = \"{}\" }}", tools->git, - tools->gitRefKind.empty() ? "rev" : tools->gitRefKind, tools->gitRev); - } else if (!tools->path.empty()) { - auto root = fs::path(tools->path); - if (root.is_relative()) root = plan.projectRoot / root; - runSpec = std::format("{{ path = \"{}\" }}", (root.parent_path() / "run").generic_string()); - } else { - runSpec = std::format("\"{}\"", tools->version); + std::error_code ec; + const auto out = dir / "target" / "dist" / "packed"; + fs::remove_all(out, ec); + std::vector argv{ mcpp::invocation::self_exe_path().string(), "pack", + "--pkg-format", "dir", "-o", "packed", + "--platform", row.platform.empty() ? std::string("none") : row.platform, + "--mode", row.mode }; + if (!row.triple.empty()) argv.insert(argv.end(), { "--target", row.triple }); + int spawnError = 0; + auto built = mcpp::run::capture_exec(argv, {}, dir.string(), &spawnError); + if (spawnError != 0 || built.exit_code != 0) + return std::unexpected(Error{std::format( + "packing the program under {} failed:\n{}", dir.string(), built.output)}); + if (fs::is_directory(out / "bin", ec)) return out / "bin"; + // A flat tree (PE) holds the generated program's own description beside + // it; what is placed from here is the program, and the package it is + // placed into describes itself. + if (fs::is_directory(out, ec)) { + fs::remove(out / "mcpp-package.json", ec); + return out; } + return std::unexpected(Error{std::format("the program's package under {} was not written", + out.string())}); +} - const auto stem = fs::path(plan.binaryName).stem().string(); - const auto dir = plan.projectRoot / "target" / "okimage-pack" / stem; +// THE STATIC ROW'S TOOLCHAIN, STATED: the carrier's own graph supplies a +// kernel interface, which would otherwise cancel the row's convention and +// leave a compiler without a musl C runtime to link with (L10, measured: clang +// took the host GCC's `crtbeginT.o`). The row's own pin on this machine's +// architecture; for another architecture's row the platform's toolchain, +// which every host carries (the row's cross GCC is published for an x86_64 +// host only: measured, `x86_64-linux-musl-gcc@16.1.0` has no build for an +// aarch64 Linux host). +std::string static_row_pin(const ProgramRow& row, std::string_view platformToolchain = {}) { + if (!row.triple.ends_with("-linux-musl") || !row.platform.empty()) return {}; + auto t = mcpp::toolchain::triple::parse(row.triple); + if (!t) return {}; + std::string pin; + if (t->arch != mcpp::toolchain::platform::host_arch && !platformToolchain.empty()) + pin = std::string(platformToolchain); + else if (auto* k = mcpp::toolchain::triple::find_known_target(*t); k && !k->pin.empty()) + pin = std::string(k->pin); + if (pin.empty()) return {}; + return std::format("\n[target.{}]\ntoolchain = \"{}\"\n", row.triple, pin); +} + +// 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. +bool row_is_built(const ProgramRow& row) { + if (row.triple.empty()) return true; // this machine's own row + auto t = mcpp::toolchain::triple::parse(row.triple); + if (!t) return false; + auto* k = mcpp::toolchain::triple::find_known_target(*t); + return k && k->tier != "planned"; +} + +} // namespace + +// What a carrier carries and for which system it is built. +struct CarrierBuild { + std::filesystem::path carried; // empty: nothing (the runner's form) + std::string carriedName; // the artifact's name, as the carrier reports it + std::string stem; // the program's name + std::string system; // linux | macos | windows + std::string arch; + bool single = false; +}; + +std::expected +carrier_build(const Plan& plan, const CarrierBuild& b) +{ + namespace fs = std::filesystem; + auto const& c = plan.platform.fields.carrier; + if (!plan.platform.active || !c.set) + return std::unexpected(Error{std::format( + "--mode self-contained of {} builds a program of the machine over its platform's " + "carrier, and {} states none ([platform] carrier = {{ package, section, main }}).", + plan.binaryName, plan.platform.display())}); std::error_code ec; + if (c.main.empty() || !fs::is_regular_file(c.main, ec)) + return std::unexpected(Error{std::format( + "the carrier of {} names its entry source {}, which is not a file ({})", + plan.platform.display(), c.main.empty() ? std::string("(none)") : c.main, + mcpp::platform::origin_text(c.origin))}); + + const auto row = program_row(b.system, b.arch, b.single, &plan.platform); + const auto dir = plan.projectRoot / "target" / "carrier-pack" + / std::format("{}-{}-{}", b.stem, b.system, b.arch); fs::create_directories(dir / "src", ec); if (ec) return std::unexpected(Error{std::format("cannot create {}: {}", dir.string(), ec.message())}); - // RESOLVED AS THE BUILD THAT MADE THE IMAGE RESOLVED ITS TOOLS. The + // RESOLVED AS THE BUILD THAT MADE THE ARTIFACT RESOLVED ITS PLATFORM. The // generated project is this pack's, not a project of its own: a lock it - // kept from an earlier pack held `openkal-run` at that pack's commit while - // the image's tools had moved on, and the program then carried a runner - // older than the one the image was checked by. + // kept from an earlier pack held the carrier at that pack's commit while + // the platform had moved on. fs::remove(dir / "mcpp.lock", ec); auto write = [&](const fs::path& p, std::string_view text) -> bool { @@ -2052,130 +2429,187 @@ okimage_build_selfcontained(const Plan& plan, const std::filesystem::path& image return bool(out); }; const std::string manifest = std::format( - "# Generated by `mcpp pack --mode self-contained` for {}: a program of this\n" - "# machine that carries the ok image as a section and runs it (M3).\n" + "# Generated by `mcpp pack` for {}: a program of {}-{} that {} through {}'s\n" + "# carrier (SPEC-013 §11, SPEC-014 §4.2).\n" "[package]\nname = \"{}\"\nversion = \"{}\"\n\n" "[targets.{}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" - "[build]\nsources = [\"src/main.cpp\", \"src/image.S\"]\n\n" - "[dependencies]\nopenkal-run = {}\n", - plan.binaryName, stem, plan.packageVersion.empty() ? "0.0.0" : plan.packageVersion, - stem, runSpec); - // The section, in the syntax of this machine's object format; the symbol - // spelling carries Mach-O's leading underscore. - const bool macho = mcpplibs::os::is_macos; - const bool coff = mcpplibs::os::is_windows; - const std::string sym = macho ? "_" : ""; - const std::string section = macho ? "\t.section __OKE,__image\n" - : coff ? "\t.section .oke,\"dr\"\n" - : "\t.section .oke,\"a\",@progbits\n"; - const std::string assembly = std::format( - "# The image's bytes, unchanged (generated by mcpp pack).\n{}" - "\t.p2align 4\n\t.globl {}ok_image_begin\n{}ok_image_begin:\n" - "\t.incbin \"{}\"\n\t.globl {}ok_image_end\n{}ok_image_end:\n", - section, sym, sym, fs::absolute(image).generic_string(), sym, sym); - const std::string main = std::format( - "// Generated by mcpp pack: runs the image this program carries (M3).\n" - "#include \n" - "#include \n" - "extern \"C\" const unsigned char ok_image_begin[], ok_image_end[];\n" - "int main(int argc, char** argv) {{\n" - " char why[512];\n" - " okrun_options options{{}};\n" - " options.carried = 1; // the program is the image's name, and what starts it again\n" - " okrun_run_bytes(ok_image_begin, static_cast(ok_image_end - ok_image_begin),\n" - " \"{}\", argc, const_cast(argv), &options, why, sizeof why);\n" - " std::fprintf(stderr, \"{}: %s\\n\", why);\n" - " return 127;\n" - "}}\n", - plan.binaryName, stem); - if (!write(dir / "mcpp.toml", manifest) || !write(dir / "src" / "image.S", assembly) - || !write(dir / "src" / "main.cpp", main)) + "[build]\nsources = [\"src/main.cpp\", \"src/carried.S\"]\n\n" + "{}{}", + plan.binaryName, b.system, b.arch, + b.carried.empty() ? "runs the image it is given" : "carries it and runs it", + plan.platform.display(), b.stem, + plan.packageVersion.empty() ? "0.0.0" : plan.packageVersion, b.stem, + dependency_entry(c.package), + static_row_pin(row, plan.platform.fields.toolchain.set ? plan.platform.fields.toolchain.value + : std::string())); + std::string mainText; + { + std::ifstream in(c.main, std::ios::binary); + mainText.assign(std::istreambuf_iterator(in), {}); + } + if (!write(dir / "mcpp.toml", manifest) + || !write(dir / "src" / "carried.S", + carried_assembly(b.system, c.section, b.carried, b.carriedName, {})) + || !write(dir / "src" / "main.cpp", mainText)) return std::unexpected(Error{std::format("cannot write the program's sources under {}", dir.string())}); // PACKED, NOT MERELY BUILT: the program obeys every rule this command // keeps for a program -- no build-machine path, its closure beside it. On - // Linux it is static (musl), the one form that runs on every distribution; - // on macOS and Windows the system's own libraries are the only ones it - // names. - mcpp::ui::info("Packing", std::format("{} into a program of this machine (okrun, M3)", plan.binaryName)); - // `-o` with a bare name: the package directory is `target/dist/`. - const auto out = dir / "target" / "dist" / "packed"; - fs::remove_all(out, ec); - std::vector argv{ mcpp::invocation::self_exe_path().string(), "pack", - "--format", "dir", "-o", "packed" }; - if constexpr (mcpplibs::os::is_linux) { - argv.insert(argv.end(), { "--target", std::format("{}-linux-musl", mcpp::toolchain::platform::host_arch), - "--mode", "static" }); - } else { - argv.insert(argv.end(), { "--mode", "vendored" }); + // macOS and Windows the system's own libraries are the only ones it names. + mcpp::ui::info("Packing", b.carried.empty() + ? std::format("{} for {}-{} ({}'s carrier, the runner of the package)", b.stem, + b.system, b.arch, plan.platform.display()) + : std::format("{} into a program of {}-{} ({}'s carrier, M3)", plan.binaryName, + b.system, b.arch, plan.platform.display())); + return pack_generated_program(dir, row); +} + +namespace { + +// The images an image names (`DT_NEEDED`), found where its build placed them. +std::expected, Error> +needed_images(const std::filesystem::path& artifact, std::string_view binaryName) { + namespace fs = std::filesystem; + std::vector needed; + auto facts = mcpp::runtime::elf::inspect_elf_runtime(artifact); + if (!facts) return std::unexpected(Error{facts.error()}); + const auto builtDir = artifact.parent_path(); + std::error_code ec; + for (auto const& name : facts->needed) { + fs::path hit; + for (auto const& d : { builtDir, builtDir.parent_path() / "lib" }) + if (fs::is_regular_file(d / name, ec)) { hit = d / name; break; } + if (hit.empty()) return std::unexpected(Error{std::format( + "{} names the image '{}', which the build did not place beside it", binaryName, name)}); + needed.push_back(hit); } - int spawnError = 0; - auto built = mcpp::run::capture_exec(argv, {}, dir.string(), &spawnError); - if (spawnError != 0 || built.exit_code != 0) - return std::unexpected(Error{std::format( - "packing the self-contained program failed (in {}):\n{}", dir.string(), built.output)}); - if (!fs::is_directory(out / "bin", ec)) - return std::unexpected(Error{std::format( - "the self-contained program's package under {} has no bin/", out.string())}); - return out; + return needed; } +std::string arch_of(std::string_view triple) { + auto t = mcpp::toolchain::triple::parse(triple); + return t ? t->arch : std::string(triple.substr(0, triple.find('-'))); +} + +} // namespace + std::expected -run_okimage(const Plan& plan) +run_loaded(const Plan& plan) { namespace fs = std::filesystem; - if (plan.opts.mode == Mode::Static) - return std::unexpected(Error{std::format( - "--mode static does not apply to an ok image: an image is loaded, not linked " - "with its loader into one file.\n" - " `--mode self-contained` packs a program of this machine that carries {} " - "and needs nothing installed.", plan.binaryName)}); std::error_code ec; fs::remove_all(plan.stagingRoot, ec); fs::create_directories(plan.stagingRoot / "bin", ec); if (ec) return std::unexpected(Error{std::format( "cannot create staging '{}': {}", plan.stagingRoot.string(), ec.message())}); - - // The images it names, found where the build placed them. - std::vector needed; - if (plan.opts.mode != Mode::None) { - auto facts = mcpp::runtime::elf::inspect_elf_runtime(plan.builtBinary); - if (!facts) return std::unexpected(Error{facts.error()}); - const auto builtDir = plan.builtBinary.parent_path(); - for (auto const& name : facts->needed) { - fs::path hit; - for (auto const& d : { builtDir, builtDir.parent_path() / "lib" }) - if (fs::is_regular_file(d / name, ec)) { hit = d / name; break; } - if (hit.empty()) return std::unexpected(Error{std::format( - "{} names the image '{}', which the build did not place beside it", - plan.binaryName, name)}); - needed.push_back(hit); + auto copy = [&](const fs::path& from, const fs::path& to) -> std::expected { + fs::create_directories(to.parent_path(), ec); + fs::copy_file(from, to, fs::copy_options::overwrite_existing, ec); + if (ec) return std::unexpected(Error{std::format("copy {} failed: {}", from.string(), ec.message())}); + return {}; + }; + const bool bundles = plan.opts.mode != Mode::None; + + if (plan.opts.universalName.empty()) { + // ONE LEG: the image in `bin/`, the images it names beside it, or + // under `self-contained` the carrier that carries it (M3). + std::vector needed; + if (bundles) { + auto n = needed_images(plan.builtBinary, plan.binaryName); + if (!n) return std::unexpected(n.error()); + needed = std::move(*n); } - } - - fs::path staged; - if (plan.opts.mode == Mode::BundleAll) { - auto packed = okimage_build_selfcontained(plan, plan.builtBinary); - if (!packed) return std::unexpected(packed.error()); - fs::copy(*packed, plan.stagingRoot, - fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec); - staged = plan.stagingRoot / "bin" - / (fs::path(plan.binaryName).stem().string() + std::string(mcpplibs::os::exe_suffix)); + fs::path staged; + // A single file is made from the images (`single_file`); the tree + // holds them as `vendored` does. + if (plan.opts.mode == Mode::BundleAll && plan.opts.format != Format::SingleFile) { + auto packed = carrier_build(plan, { plan.builtBinary, plan.binaryName, + fs::path(plan.binaryName).stem().string(), std::string(host_system()), + std::string(mcpp::toolchain::platform::host_arch), false }); + if (!packed) return std::unexpected(packed.error()); + fs::copy(*packed, plan.stagingRoot / "bin", + fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec); + staged = plan.stagingRoot / "bin" + / (fs::path(plan.binaryName).stem().string() + std::string(mcpplibs::os::exe_suffix)); + } else { + staged = plan.stagingRoot / "bin" / plan.binaryName; + if (auto r = copy(plan.builtBinary, staged); !r) return r; + } + for (auto const& n : needed) + if (auto r = copy(n, plan.stagingRoot / "bin" / n.filename()); !r) return r; + if (auto r = stage_runtime_files(plan, staged.parent_path()); !r) return r; } else { - staged = plan.stagingRoot / "bin" / plan.binaryName; - fs::copy_file(plan.builtBinary, staged, fs::copy_options::overwrite_existing, ec); - } - if (ec) return std::unexpected(Error{std::format("copy {} failed: {}", staged.string(), ec.message())}); - for (auto const& n : needed) { - fs::copy_file(n, plan.stagingRoot / "bin" / n.filename(), fs::copy_options::overwrite_existing, ec); - if (ec) return std::unexpected(Error{std::format("copy {} failed: {}", n.string(), ec.message())}); + // A UNIVERSAL PACKAGE: a `lib//` per leg, the launchers, and the + // runner's program per system and architecture under `self-contained`. + auto const& runner = plan.platform.fields.runner; + if (!runner.set || runner.cmd.empty()) + return std::unexpected(Error{std::format( + "a universal package of {} starts its images through the platform's runner, and " + "{} states none.\n use: --no-universal", + plan.binaryName, plan.platform.display())}); + struct Leg { std::string triple, arch; fs::path artifact; }; + std::vector legs{ { plan.triple, arch_of(plan.triple), plan.builtBinary } }; + for (auto const& l : plan.loadedLegs) legs.push_back({ l.triple, l.arch, l.artifact }); + mcpp::pack::distribution::Launch launch; + launch.name = fs::path(plan.binaryName).stem().string(); + launch.runner = runner.cmd; + for (auto const& leg : legs) launch.entry[leg.arch] = leg.artifact.filename().string(); + if (plan.opts.mode == Mode::BundleAll) launch.loader = fs::path(runner.cmd.front()).stem().string(); + // A word a `.cmd` cannot hold is refused now, before anything is staged. + if (auto ok = mcpp::pack::distribution::launchable(launch); !ok) + return std::unexpected(Error{ok.error()}); + for (auto const& leg : legs) { + const auto dir = plan.stagingRoot / "lib" / leg.arch; + if (auto r = copy(leg.artifact, dir / leg.artifact.filename()); !r) return r; + if (bundles) { + auto n = needed_images(leg.artifact, plan.binaryName); + if (!n) return std::unexpected(n.error()); + for (auto const& f : *n) + if (auto r = copy(f, dir / f.filename()); !r) return r; + } + if (auto r = stage_runtime_files(plan, dir, leg.artifact.parent_path()); !r) return r; + } + if (plan.opts.mode == Mode::BundleAll) { + std::vector systems = plan.opts.systems; + if (systems.empty()) systems.emplace_back(host_system()); + for (auto const& system : systems) + for (auto const& leg : legs) { + // A row this mcpp builds no program for (a planned + // target, `x86_64-macos`) carries no runner: said, and + // the package's metadata lists the systems it carries. + if (!row_is_built(program_row(system, leg.arch, false, &plan.platform))) { + mcpp::ui::warning(std::format( + "no runner for {}-{} in the package: this mcpp builds no program for " + "that system and architecture; the launcher there looks for `{}` on PATH", + system, leg.arch, launch.loader)); + continue; + } + auto packed = carrier_build(plan, { {}, {}, launch.loader, system, leg.arch, false }); + if (!packed) return std::unexpected(packed.error()); + const auto into = plan.stagingRoot / "libexec" / launch.loader + / std::format("{}-{}", system, leg.arch); + fs::create_directories(into, ec); + fs::copy(*packed, into, + fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec); + if (ec) return std::unexpected(Error{std::format( + "cannot place the runner for {}-{}: {}", system, leg.arch, ec.message())}); + } + } + const auto posix = plan.stagingRoot / "bin" / launch.name; + { + std::ofstream out(posix, std::ios::binary | std::ios::trunc); + out << mcpp::pack::distribution::posix_launcher(launch); + } + fs::permissions(posix, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec + | fs::perms::others_read | fs::perms::others_exec, + fs::perm_options::replace, ec); + std::ofstream(plan.stagingRoot / "bin" / (launch.name + ".cmd"), std::ios::binary | std::ios::trunc) + << mcpp::pack::distribution::windows_launcher(launch); } - if (auto r = stage_runtime_files(plan, staged.parent_path()); !r) return r; copy_if_exists(plan.projectRoot / "README.md", plan.stagingRoot); copy_if_exists(plan.projectRoot / "LICENSE", plan.stagingRoot); - if (plan.opts.format != Format::Tar) return {}; - return make_tarball(plan.stagingRoot, plan.archivePath); + return write_container(plan); } // #622 A3/A10: the Application half whose form on this row is a shared @@ -2324,9 +2758,7 @@ run_shared_program(const Plan& plan) " or link_library_dirs, or the compiler's library search path."), std::move(needs)); - if (plan.opts.format == Format::Tar) - if (auto r = make_tarball(plan.stagingRoot, plan.archivePath); !r) - return std::unexpected(r.error()); + if (auto r = write_container(plan); !r) return std::unexpected(r.error()); return ClosureResult{true, {}, std::move(needs)}; } @@ -2369,8 +2801,8 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) // An ok image: the images it names travel beside it, and under // `self-contained` the image is carried inside a program of this machine. - if (plan.targetIsOkImage) { - if (auto r = detail::run_okimage(plan); !r) return std::unexpected(r.error()); + if (plan.targetIsLoaded) { + if (auto r = detail::run_loaded(plan); !r) return std::unexpected(r.error()); return ClosureResult{}; } @@ -2427,9 +2859,7 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) std::move(needs)); if (auto r = write_topentry_wrapper(plan.stagingRoot, plan.binaryName); !r) return std::unexpected(Error{r.error()}); - if (plan.opts.format == Format::Tar) - if (auto r = make_tarball(plan.stagingRoot, plan.archivePath); !r) - return std::unexpected(r.error()); + if (auto r = write_container(plan); !r) return std::unexpected(r.error()); return ClosureResult{true, {}, std::move(needs)}; } @@ -2785,12 +3215,143 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) } // Output. - if (plan.opts.format == Format::Tar) { - if (auto r = make_tarball(plan.stagingRoot, plan.archivePath); !r) - return std::unexpected(r.error()); - } + if (auto r = write_container(plan); !r) return std::unexpected(r.error()); return ClosureResult{true, {}, std::move(needs)}; } } +std::expected finalize_staged(const Plan& plan) { + return detail::finalize_tree(plan); +} + +std::string unbuilt_row(std::string_view system, std::string_view arch) { + const auto row = detail::program_row(system, arch, true); + return detail::row_is_built(row) ? std::string() : row.triple; +} + +std::expected, Error> single_file(const Plan& plan) { + namespace fs = std::filesystem; + namespace d = mcpp::pack::distribution; + std::error_code ec; + const auto distDir = plan.projectRoot / "target" / "dist"; + auto output_for = [&](std::string_view qualifier, std::string_view suffix) { + if (plan.opts.output.empty()) + return distDir / std::format("{}-{}-{}{}", plan.packageName, plan.packageVersion, + qualifier, suffix); + if (!plan.opts.output.has_parent_path()) return distDir / plan.opts.output; + return plan.opts.output; + }; + std::vector written; + if (plan.targetIsLoaded) { + // THE IMAGE, OR THE IMAGES AS ONE STORED ZIP: what the carrier's + // entry reads between `mcpp_carried_begin` and `mcpp_carried_end` + // (SPEC-013 §11): the image alone when it names no other, else every + // image of `bin/` as members of a zip whose data lies on 64 KiB + // boundaries, the entry named by `mcpp_carried_name`. + std::vector images; + for (auto const& e : fs::directory_iterator(plan.stagingRoot / "bin", ec)) + if (e.is_regular_file(ec)) images.push_back(e.path()); + std::ranges::sort(images); + fs::path carried = plan.stagingRoot / "bin" / plan.binaryName; + if (images.size() > 1) { + carried = plan.projectRoot / "target" / "carrier-pack" + / (fs::path(plan.binaryName).stem().string() + ".carried.zip"); + std::vector entries; + for (auto const& i : images) entries.push_back({ i.filename().string(), i, false, {} }); + mcpp::pack::zip::WriteOptions o; + o.align = 65536; + if (auto r = mcpp::pack::zip::write(carried, entries, o); !r) + return std::unexpected(Error{r.error()}); + } + auto t = mcpp::toolchain::triple::parse(plan.triple); + const std::string arch = t ? t->arch : std::string(mcpp::toolchain::platform::host_arch); + std::vector systems = plan.opts.systems; + if (systems.empty()) systems.emplace_back(detail::host_system()); + if (systems.size() > 1 && !plan.opts.output.empty()) + return std::unexpected(Error{std::format( + "--output names one file, and --systems makes one single file per system ({}).\n" + " use: leave --output out; each file is named ---", + systems.size())}); + for (auto const& system : systems) { + const auto stem = fs::path(plan.binaryName).stem().string(); + auto packed = detail::carrier_build(plan, { carried, plan.binaryName, stem, system, arch, true }); + if (!packed) return std::unexpected(packed.error()); + const auto suffix = system == "windows" ? std::string(".exe") : std::string(); + const auto program = *packed / (stem + suffix); + if (!fs::is_regular_file(program, ec)) + return std::unexpected(Error{std::format("the carrier built for {}-{} has no {}", + system, arch, program.filename().string())}); + const auto out = output_for(std::format("{}-{}", system, arch), suffix); + fs::create_directories(out.parent_path(), ec); + fs::copy_file(program, out, fs::copy_options::overwrite_existing, ec); + if (ec) return std::unexpected(Error{std::format("cannot write {}: {}", out.string(), ec.message())}); + written.push_back(out); + } + } else { + // THE ENGINE'S LAUNCHER CARRYING THE TREE. The entry: the program a + // person runs from the unpacked tree -- a Windows program at the root, + // a self-contained Linux tree's wrapper at the root, `bin/` + // otherwise. + std::string entry = "bin/" + plan.binaryName; + if (plan.targetIsPe) entry = plan.binaryName; + else if (plan.opts.mode == Mode::BundleAll && fs::is_regular_file(plan.stagingRoot / plan.binaryName, ec)) + entry = plan.binaryName; + auto tree = d::tree_payload(plan.stagingRoot); + if (!tree) return std::unexpected(Error{tree.error()}); + auto c = plan.opts.compression; + c.algorithm = mcpp::pack::compress::Algorithm::Zstd; // `none` stores at level 1 + if (plan.opts.compression.algorithm == mcpp::pack::compress::Algorithm::None) c.level = 1; + auto payload = mcpp::pack::compress::bytes(*tree, c); + if (!payload) return std::unexpected(Error{payload.error()}); + const auto digest = d::sha256(*payload); + const auto stem = fs::path(plan.binaryName).stem().string(); + const auto dir = plan.projectRoot / "target" / "single-file" / stem; + fs::create_directories(dir / "src", ec); + fs::remove(dir / "mcpp.lock", ec); + auto write = [&](const fs::path& p, std::string_view text) -> bool { + std::ofstream out(p, std::ios::binary | std::ios::trunc); + out.write(text.data(), static_cast(text.size())); + return bool(out); + }; + auto t = mcpp::toolchain::triple::parse(plan.triple); + const std::string arch = t ? t->arch : std::string(mcpp::toolchain::platform::host_arch); + const std::string system = !t ? std::string(detail::host_system()) + : t->os == "windows" ? std::string("windows") + : t->os == "macos" ? std::string("macos") : std::string("linux"); + const auto row = detail::program_row(system, arch, true); + if (!write(dir / "payload.zst", *payload) + || !write(dir / "mcpp.toml", d::launcher_manifest(stem, plan.packageVersion) + detail::static_row_pin(row)) + || !write(dir / "src" / "main.cpp", d::launcher_main()) + || !write(dir / "src" / "carried.S", + detail::carried_assembly(system, ".mcppsf", dir / "payload.zst", entry, digest))) + return std::unexpected(Error{std::format("cannot write the launcher's sources under {}", dir.string())}); + mcpp::ui::info("Packing", std::format("{} into one file for {}-{} ({} payload, sha256 {})", + plan.binaryName, system, arch, + mcpp::pack::compress::name(c.algorithm), digest.substr(0, 12))); + auto packed = detail::pack_generated_program(dir, row); + if (!packed) return std::unexpected(packed.error()); + const auto suffix = system == "windows" ? std::string(".exe") : std::string(); + const auto program = *packed / (stem + suffix); + const auto out = output_for(t ? t->str() : plan.triple, suffix); + fs::create_directories(out.parent_path(), ec); + fs::copy_file(program, out, fs::copy_options::overwrite_existing, ec); + if (ec) return std::unexpected(Error{std::format("cannot write {}: {}", out.string(), ec.message())}); + written.push_back(out); + } + // The signer signs the file a person runs, too. + if (!plan.opts.sign.empty()) + for (auto const& f : written) { + std::vector argv; + for (auto const& a : plan.opts.sign) argv.push_back(a == "{}" ? f.string() : a); + int spawnError = 0; + auto r = mcpp::run::capture_exec(argv, {}, {}, &spawnError); + if (spawnError != 0 || r.exit_code != 0) + return std::unexpected(Error{std::format("[pack] sign failed on {} (exit {}): {}\n{}", + f.string(), r.exit_code, + mcpp::build::command_line::display(argv), + r.output)}); + } + return written; +} + } // namespace mcpp::pack diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 88bb98060..b5a1e03e8 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -34,6 +34,9 @@ import mcpp.log; import mcpp.ui; import mcpp.provider; import mcpp.provider.select; +import mcpp.toolchain.platform; // host_arch: the static row +import mcpp.pack.distribution; // the package's metadata, for a dispatched format +import mcpp.platform; // serves: a leg the platform states namespace mcpp::pack { @@ -61,6 +64,11 @@ export struct PackOutcome { std::filesystem::path stageDir; std::filesystem::path stageManifest; std::string closure; + // The axes the package was made with (SPEC-014 §9): `linkage`, `mode`, + // and the systems whose loaders it carries. + std::string linkage; + std::string mode; + std::vector systems; // Whether a build program ran in this pack, for the envelope's `effects`. bool ranBuildPrograms = false; // What one packed member answered (member selection design 2026-09-30, @@ -77,6 +85,9 @@ export struct PackOutcome { std::filesystem::path stageDir; std::filesystem::path stageManifest; std::string closure; + std::string linkage; + std::string mode; + std::vector systems; }; std::vector members; }; @@ -393,6 +404,42 @@ bool member_carries(const mcpp::build::BuildContext& ctx, const MemberJob& m, // // Called inside the member's view (`with_member`): `ctx.manifest`, // `ctx.projectRoot` and the plan's link group are the member's. +// Whether a pack of `ctx`'s target is a universal package (SPEC-014 §3): an +// image of a platform that states `universal = true`, not refused by the +// command or `[pack] universal`, in a format other than a single file. +bool packs_universal(const mcpp::build::BuildContext& ctx, const Options& opts) { + auto t = mcpp::toolchain::triple::parse(ctx.tc.targetTriple); + auto const& P = ctx.plan.platform; + return t && t->loader_supplies_os() && P.active && P.fields.universal.set + && P.fields.universal.value && opts.universal.value_or(true) + && opts.format != mcpp::pack::Format::SingleFile; +} + +// The combinations of a member's pack refused before anything is built +// (SPEC-014 §1.5): what its options and the plan's platform already decide. +std::optional refused_combination(const mcpp::build::BuildContext& ctx, const Options& opts) { + // A single file carries what it runs, whichever source stated the mode. + if (opts.format == mcpp::pack::Format::SingleFile && opts.mode == mcpp::pack::Mode::None) + return std::string("--pkg-format single-file carries what the program runs, and the mode is " + "system ([pack] default_mode).\n use: --mode vendored | self-contained"); + if (!opts.legTargets.empty() && !packs_universal(ctx, opts)) + return std::string("several --target name one package's legs only where the target's platform " + "packs them into one universal package (`[platform] universal = true`).\n" + " use: one pack per target"); + if (!packs_universal(ctx, opts)) return std::nullopt; + auto const& P = ctx.plan.platform; + if (!P.fields.runner.set || P.fields.runner.cmd.empty()) + return std::format("a universal package starts its images through the platform's runner, and " + "{} states none.\n use: --no-universal", P.display()); + auto t = mcpp::toolchain::triple::parse(ctx.tc.targetTriple); + for (auto const& target : opts.legTargets) { + auto lt = mcpp::toolchain::triple::parse(target); + if (lt && t && lt->str() != t->str() && !mcpp::platform::serves(P.fields, P.package, *lt)) + return std::format("--target {} is not a target of {}", lt->str(), P.display()); + } + return std::nullopt; +} + std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { auto& ctx = *g.ctx; const bool sharedPlan = ctx.workspaceMembers.size() > 1; @@ -559,6 +606,64 @@ std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { } } + // ─── A loaded artifact: the static row, and the universal package ── + // + // `--mode static` of an image is `--linkage static --mode self-contained` + // (SPEC-014 §1.2): one `.oke`, carried. A platform that states `universal + // = true` packs every one of its targets into one package unless the + // command or `[pack] universal` says otherwise (§3); a single file is one + // system's and one architecture's, so it is never universal. + std::vector loadedLegs; + { + auto t = mcpp::toolchain::triple::parse(ctx.tc.targetTriple); + const bool loaded = t && t->loader_supplies_os(); + if (loaded && opts.mode == mcpp::pack::Mode::Static) opts.mode = mcpp::pack::Mode::BundleAll; + auto const& P = ctx.plan.platform; + if (auto why = refused_combination(ctx, opts)) return Refusal{2, about(run, m, *why)}; + if (packs_universal(ctx, opts)) { + opts.universalName = mcpp::manifest::platform::platform_name( + P.package.substr(P.package.find('.') + 1)); + // The legs: those the command named, else every target of the + // platform. + auto const& legs = opts.legTargets.empty() ? P.fields.targets.value : opts.legTargets; + for (auto const& target : legs) { + auto lt = mcpp::toolchain::triple::parse(target); + if (!lt || lt->str() == t->str()) continue; + mcpp::build::BuildOverrides ov = g.ov; + ov.target_triple = lt->str(); + // The member's own plan, in a workspace; the project's otherwise. + ov.workspace_members.clear(); + ov.workspace_request.clear(); + if (m.inWorkspace && !m.path.empty()) { + ov.workspace_members = { m.path }; + ov.workspace_request = { m.path }; + } + auto legCtx = mcpp::build::prepare_build(false, false, {}, ov); + if (!legCtx) return Refusal{1, about(run, m, std::format( + "the {} leg of the universal package: {}", lt->str(), legCtx.error()))}; + auto be = mcpp::build::make_ninja_backend(); + mcpp::build::BuildOptions bo; + if (auto br = be->build(legCtx->plan, bo); !br) { + mcpp::build::report_failed_drive(br.error()); + return Refusal{1, about(run, m, std::format("the {} leg did not build", lt->str()))}; + } + mcpp::build::populate_dependency_cache(*legCtx); + std::filesystem::path artifact; + for (auto const& lu : legCtx->plan.linkUnits) + if (lu.targetName == chosenLu->targetName && lu.kind == chosenLu->kind + && !lu.dependencyOwned + && (legCtx->workspaceMembers.size() < 2 || lu.memberOf == m.name)) { + artifact = legCtx->outputDir / lu.output; + break; + } + if (artifact.empty()) return Refusal{1, about(run, m, std::format( + "the {} leg of the universal package built no '{}'", lt->str(), chosenLu->targetName))}; + mcpp::ui::status("Packed leg", lt->str()); + loadedLegs.push_back({ lt->str(), lt->arch, artifact }); + } + } + } + // ─── Build the plan ────────────────────────────────────────────── opts.baseStated = ctx.tc.base.stated(); auto plan = mcpp::pack::make_plan(ctx.manifest, *run.cfg, opts, @@ -568,6 +673,13 @@ std::optional member_plan(PackRun& run, GroupJob& g, MemberJob& m) { // which appears in the project's own manifest. ctx.plan.runtimeRequirements, programIsSharedObject); if (!plan) return Refusal{1, about(run, m, plan.error().message)}; + plan->platform = ctx.plan.platform; + plan->loadedLegs = std::move(loadedLegs); + plan->linkageName = opts.linkage == mcpp::pack::Linkage::Static ? "static" + : opts.linkage == mcpp::pack::Linkage::Shared ? "shared" + : opts.mode == mcpp::pack::Mode::Static ? "static" + : ctx.plan.manifest.buildConfig.dependencyLinkage.empty() + ? "declared" : ctx.plan.manifest.buildConfig.dependencyLinkage; // `--output ` with several members: each member's archive, or its tree // for `--format dir`, is written below the directory under the name it would // have by default. The staging tree of an archive, and of a dispatched @@ -679,7 +791,7 @@ std::optional prepare_group(PackRun& run, GroupJob& g) { // prepare with the same overrides plus two fields -- and a second // overrides object left behind here would make that pass differ from // this build in a way nothing states. - g.ov.target_triple = "x86_64-linux-musl"; + g.ov.target_triple = std::format("{}-linux-musl", mcpp::toolchain::platform::host_arch); // Quiet on the same grounds as the first prepare: this one also runs // before `--format` has been validated. auto ctx2 = prepare(); @@ -694,6 +806,61 @@ std::optional prepare_group(PackRun& run, GroupJob& g) { std::optional check_format(PackRun& run, GroupJob& g) { auto const& ctx = *g.ctx; const auto& formatName = run.opts.formatName; + // What each member's options and the platform decide. + for (auto const& m : g.members) { + std::optional why; + mcpp::build::with_member(*g.ctx, m.inWorkspace ? m.name : std::string_view{}, [&] { + why = refused_combination(ctx, m.request); + }); + if (why) return Refusal{2, about(run, m, *why)}; + } + // ─── `--systems` names the loaders a package carries ───────────── + // + // A self-contained universal package carries a loader per system, and a + // single file of an image is made per system; nothing else is made per + // system. Refused here, before the build (SPEC-014 §1.5). + if (!run.opts.systems.empty()) { + auto t = mcpp::toolchain::triple::parse(ctx.tc.targetTriple); + const bool loaded = t && t->loader_supplies_os(); + const bool universal = packs_universal(ctx, run.opts); + const bool selfContained = run.opts.mode == mcpp::pack::Mode::BundleAll + || run.opts.mode == mcpp::pack::Mode::Static; + if (!(universal && selfContained) && !(loaded && run.opts.format == mcpp::pack::Format::SingleFile)) + return Refusal{2, universal + ? std::format("--systems names the systems whose loaders a self-contained package " + "carries, and --mode {} carries none.\n use: --mode self-contained", + mcpp::pack::mode_cli_name(run.opts.mode)) + : std::string("--systems names the systems whose loaders a self-contained universal " + "package carries, or for which a single file of an image is made; this " + "pack is neither.\n use: --mode self-contained over a platform that " + "states universal = true, or --pkg-format single-file")}; + // A single file is made per system, and `-o` names one file. + if (run.opts.format == mcpp::pack::Format::SingleFile && run.opts.systems.size() > 1 + && !run.opts.output.empty()) + return Refusal{2, std::format( + "--output names one file, and --systems makes one single file per system ({}).\n" + " use: leave --output out; each file is named ---", + run.opts.systems.size())}; + // A single file of an image is made for each system on this mcpp's + // row of it; a row it builds no program for is refused before the build. + if (loaded && run.opts.format == mcpp::pack::Format::SingleFile) { + std::vector unbuilt, built; + for (auto const& system : run.opts.systems) { + auto row = mcpp::pack::unbuilt_row(system, t->arch); + (row.empty() ? built : unbuilt).push_back(row.empty() ? system : std::format("{} ({})", system, row)); + } + if (!unbuilt.empty()) { + std::string say, use; + for (auto const& u : unbuilt) say += (say.empty() ? "" : ", ") + u; + for (auto const& b : built) use += (use.empty() ? "" : ",") + b; + return Refusal{2, std::format( + "--systems: this mcpp builds no program for {}, so no single file of the {} " + "image is made there.\n use: {}", say, t->str(), + use.empty() ? std::string("--systems naming a system this mcpp builds for") + : "--systems " + use)}; + } + } + } // ─── Is the requested format one anything provides? ────────────── // // BEFORE THE BUILD, because a refusal that arrives after a full compile is @@ -715,9 +882,9 @@ std::optional check_format(PackRun& run, GroupJob& g) { avail += ", " + f; } return Refusal{2, std::format( - "unknown --format '{}'.\n" + "unknown --pkg-format '{}'.\n" " available in this build: {}\n" - " A format past `tar` and `dir` comes from a package in the " + " A format past the built-in ones comes from a package in the " "resolved graph, which declares\n" " it with `mcpp::provides_pack_format(\"\")` in its build " "program. Add the package\n" @@ -757,7 +924,7 @@ std::optional check_format(PackRun& run, GroupJob& g) { } if (!missing.empty()) return Refusal{2, std::format( - "--format '{}' is not provided for every packed member.{}\n" + "--pkg-format '{}' is not provided for every packed member.{}\n" " A member is packed into its own staged tree by a provider in its own build " "program, or in a\n" " package that only that member reaches.{}", @@ -783,7 +950,7 @@ std::optional check_format(PackRun& run, GroupJob& g) { if (mcpp::pack::is_builtin_pack_format(f)) mcpp::ui::warning(std::format( "a package in this graph declares `mcpp:pack-format={}`, which " - "is one of the archive shapes `mcpp pack` owns; `--format {}` " + "is one of the archive shapes `mcpp pack` owns; `--pkg-format {}` " "will always select the built-in and never that package", f, f)); return std::nullopt; } @@ -879,7 +1046,7 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { return Refusal{1, about(run, m, r.error().message)}; m.stageFailure = r.error().message; mcpp::ui::warning(about(run, m, std::format( - "no staged tree for --format {}: {}\n" + "no staged tree for --pkg-format {}: {}\n" " A format that consumes ${{mcpp.stage_dir}} cannot be produced " "here; one that names a\n" " built file with ${{mcpp.target_file:}} is unaffected.", @@ -907,14 +1074,27 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { // closure was walked. Best-effort: see write_stage_manifest. Skipped when // no tree exists, so no manifest describes a tree that is not there. if (m.stageFailure.empty()) mcpp::pack::write_stage_manifest(plan->stagingRoot, m.closure); + // A tree a dispatched format reads holds `mcpp-package.json` whether or + // not its closure was walked (SPEC-014 §6). + if (m.stageFailure.empty() && opts.format == mcpp::pack::Format::Dispatched) { + std::error_code jec; + if (!std::filesystem::exists(plan->stagingRoot / mcpp::pack::distribution::kFileName, jec)) + if (auto r = mcpp::pack::finalize_staged(*plan); !r) + return Refusal{1, about(run, m, r.error().message)}; + } // #649 E9: the outcome a machine reader receives, from values this pass // has already answered. Every return goes through it. auto& out = m.result; out.name = m.name; - out.format = opts.format == mcpp::pack::Format::Tar ? std::string("tar") + out.format = opts.format == mcpp::pack::Format::Tar + ? (plan->archive == mcpp::pack::ArchiveKind::Zip ? std::string("zip") : std::string("tar")) : opts.format == mcpp::pack::Format::Dir ? std::string("dir") + : opts.format == mcpp::pack::Format::Zip ? std::string("zip") + : opts.format == mcpp::pack::Format::SingleFile ? std::string("single-file") : opts.formatName; + out.linkage = plan->linkageName; + out.mode = std::string(mcpp::pack::mode_cli_name(opts.mode)); const auto canonical = [](std::string const& t) { if (t.empty()) return mcpp::toolchain::triple::host_triple().str(); auto parsed = mcpp::toolchain::triple::parse(t); @@ -922,6 +1102,8 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { }; out.targets.push_back(canonical(plan->triple)); for (auto const& leg : plan->extraSharedLegs) out.targets.push_back(leg.triple); + for (auto const& leg : plan->loadedLegs) out.targets.push_back(leg.triple); + out.systems = mcpp::pack::distribution::loader_systems(plan->stagingRoot); if (m.stageFailure.empty()) { out.stageDir = plan->stagingRoot; out.stageManifest = mcpp::pack::stage_manifest_path(plan->stagingRoot); @@ -937,8 +1119,15 @@ std::optional stage_member(PackRun& run, GroupJob& g, MemberJob& m) { m.pathCtx = mcpp::fetcher::make_path_ctx( &cfg, run.selection ? run.selection->root : ctx.projectRoot); - if (opts.format != mcpp::pack::Format::Dispatched) { - auto outPath = (opts.format == mcpp::pack::Format::Tar) + if (opts.format == mcpp::pack::Format::SingleFile) { + auto files = mcpp::pack::single_file(*plan); + if (!files) return Refusal{1, about(run, m, files.error().message)}; + for (auto& f : *files) { + mcpp::ui::status("Packed", mcpp::ui::shorten_path(f, m.pathCtx)); + out.artifacts.push_back(std::move(f)); + } + } else if (opts.format != mcpp::pack::Format::Dispatched) { + auto outPath = plan->archive != mcpp::pack::ArchiveKind::None ? plan->archivePath : plan->stagingRoot; mcpp::ui::status("Packed", mcpp::ui::shorten_path(outPath, m.pathCtx)); out.artifacts.push_back(std::move(outPath)); @@ -1022,7 +1211,7 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba for (auto& m : g.members) { if (m.result.rc != 0 || m.stageFailure.empty()) continue; mcpp::ui::error(about(run, m, std::format( - "no staged tree for --format {}: {}", run.opts.formatName, m.stageFailure))); + "no staged tree for --pkg-format {}: {}", run.opts.formatName, m.stageFailure))); m.result.rc = 1; } if (std::ranges::none_of(g.members, [](const MemberJob& m) { return m.result.rc == 0; })) @@ -1102,7 +1291,7 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba auto [at, fresh] = writer.try_emplace(absolute_of(o), m.name); if (!fresh && at->second != m.name) return Refusal{1, std::format( - "members '{}' and '{}' would both write {} for --format '{}'.\n" + "members '{}' and '{}' would both write {} for --pkg-format '{}'.\n" " A file has one producer: write each member's output below a " "directory of its own,\n" " for example under `mcpp::out_dir()`, which is the member's.", @@ -1124,7 +1313,7 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba auto it = submitted.find(m.name); if (it != submitted.end() && !it->second.outputs.empty()) { any = true; continue; } mcpp::ui::error(about(run, m, std::format( - "no action claimed --format '{}'.\n" + "no action claimed --pkg-format '{}'.\n" " A package declared it provides this format, and no build " "program submitted a new\n" " `role = \"artifact\"` action when it was asked for.\n" @@ -1141,7 +1330,7 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba for (auto const& m : g.members) if (m.result.rc == 0) - mcpp::ui::info("Distributing", std::format("{} v{} (--format {})", + mcpp::ui::info("Distributing", std::format("{} v{} (--pkg-format {})", m.plan->packageName, m.plan->packageVersion, run.opts.formatName)); // NO EXPLICIT GOALS. Everything but the dist edges is already up to @@ -1195,9 +1384,9 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba // A drive that failed says why above; its missing file is // the member's failure, not a success that carried nothing. mcpp::ui::error(about(run, m, driveFailed - ? std::format("--format {} did not produce {}: its step failed", + ? std::format("--pkg-format {} did not produce {}: its step failed", run.opts.formatName, abs.string()) - : std::format("--format {} reported success and produced nothing at {}", + : std::format("--pkg-format {} reported success and produced nothing at {}", run.opts.formatName, abs.string()))); m.result.rc = 1; produced = false; @@ -1233,7 +1422,7 @@ std::optional check_destinations(PackRun& run, std::vector& g // archive for `tar`; a dispatched format's products are its // provider's, and are compared when they are submitted. std::vector writes{m.plan->stagingRoot}; - if (m.opts.format == mcpp::pack::Format::Tar) writes.push_back(m.plan->archivePath); + if (m.plan->archive != mcpp::pack::ArchiveKind::None) writes.push_back(m.plan->archivePath); for (auto const& w : writes) { auto [at, fresh] = writer.try_emplace(w.lexically_normal(), m.name); if (!fresh) @@ -1262,9 +1451,13 @@ PackOutcome run_pack(PackRun run) { // ─── Build first (pack implies a fresh build) ──────────────────── mcpp::build::BuildOverrides base; if (run.opts.mode == mcpp::pack::Mode::Static && run.opts.targetTriple.empty()) - base.target_triple = "x86_64-linux-musl"; + base.target_triple = std::format("{}-linux-musl", mcpp::toolchain::platform::host_arch); else base.target_triple = run.opts.targetTriple; + // The linkage axis (SPEC-014 §1): the whole graph's dependency linkage for + // this build. + if (run.opts.linkage == mcpp::pack::Linkage::Static) base.dependency_linkage = "static"; + if (run.opts.linkage == mcpp::pack::Linkage::Shared) base.dependency_linkage = "shared"; // A bundled program leaves this machine: release is the fallback, not dev. // `[build] default-profile` still decides when the project states one. base.profile = run.opts.profile; @@ -1424,6 +1617,9 @@ PackOutcome run_pack(PackRun run) { out.stageDir = first.stageDir; out.stageManifest = first.stageManifest; out.closure = first.closure; + out.linkage = first.linkage; + out.mode = first.mode; + out.systems = first.systems; return out; } diff --git a/src/pack/stage_tree.cppm b/src/pack/stage_tree.cppm index c38ea83a8..f1e682fc3 100644 --- a/src/pack/stage_tree.cppm +++ b/src/pack/stage_tree.cppm @@ -209,13 +209,13 @@ bool write_stage_manifest(const std::filesystem::path& stagingRoot, return static_cast(os); } -// The two names `--format` answers for without consulting the graph. +// The names `--pkg-format` answers for without consulting the graph. // // Held here rather than in the CLI because two layers need the same list: the // parser decides whether a value is a built-in or a dispatch, and the // declaration check refuses a package that claims one of them. A package // claiming `tar` would be silently unreachable, since the built-in wins. -constexpr std::array kBuiltinPackFormats{"tar", "dir"}; +constexpr std::array kBuiltinPackFormats{"tar", "zip", "dir", "single-file"}; bool is_builtin_pack_format(std::string_view name) { return std::ranges::find(kBuiltinPackFormats, name) != kBuiltinPackFormats.end(); diff --git a/src/pack/zip.cppm b/src/pack/zip.cppm index 0460a580a..4cd2a03ca 100644 --- a/src/pack/zip.cppm +++ b/src/pack/zip.cppm @@ -11,12 +11,11 @@ // own OS is the thing being fixed; picking an archiver that only exists there // would reintroduce it one layer down. // -// WHY STORED, NOT DEFLATED. Entries are written with method 0 (stored). That -// is a real cost — a release zip is roughly the size of its contents — and it -// is the honest trade for now: a DEFLATE encoder is the only part of this -// file that could produce an archive which UNPACKS WRONG rather than failing -// loudly, and mcpp has no zlib to borrow one from. Adding compression later -// changes only `write()`; the archive layout below does not move. +// STORED OR DEFLATED. A member is DEFLATEd (method 8) by zlib, the reference +// encoder, when the archive asks for it and the member shrinks; otherwise it is +// stored. A package's zip is deflated by default (SPEC-014 §5); `--compress +// none` stores every member, and a member whose data is aligned (a mapped +// image) is stored whatever was asked. // // Deterministic by construction: no timestamps are read (every entry gets a // fixed DOS date), and entries are written in the order given. Two packs of @@ -26,6 +25,7 @@ export module mcpp.pack.zip; import std; +import mcpp.pack.compress; // DEFLATE for a member that shrinks export namespace mcpp::pack::zip { @@ -41,6 +41,24 @@ struct Entry { // file — which matters because a Windows artifact is routinely inspected // (and cross-tested under wine) from Linux. bool executable = false; + // A symbolic link: the member is the link (Unix mode 0120777) and its data + // is the target, as Info-ZIP writes and reads one. `source` is unread. + std::string symlinkTarget; +}; + +// How the members are written. +// +// deflate each member is DEFLATEd (method 8) when that makes it smaller, +// and stored otherwise; `level` -1 is zlib's default. +// align a STORED member's data starts at a multiple of this many bytes +// from the start of the archive, the local header's extra field +// padding the difference (as zipalign does): a reader maps the +// member where it lies. 0: no alignment. A member that is aligned +// is stored, whatever `deflate` says. +struct WriteOptions { + bool deflate = false; + int level = -1; + std::uint32_t align = 0; }; // Write `entries` to `out`, creating parent directories as needed. @@ -51,7 +69,8 @@ struct Entry { // clang 20.1.7 (MSVC ABI) segfaulted compiling the consumer — see the note at // mcpp.pack's `run_pe`. There is exactly one caller. std::expected -write(const std::filesystem::path& out, const std::vector& entries); +write(const std::filesystem::path& out, const std::vector& entries, + const WriteOptions& options = {}); // CRC-32 (IEEE), exposed for tests: an archive whose CRCs are wrong extracts // with a warning on some tools and silently on others, so this is worth @@ -97,8 +116,8 @@ constexpr std::uint16_t kDosDate = 0x0021; // (1980-1980)<<9 | 1<<5 | 1 // External attributes. The high 16 bits are the Unix mode when the // version-made-by byte says Unix (3); the low byte is the DOS attribute set. -std::uint32_t external_attrs(bool executable) { - std::uint32_t mode = executable ? 0100755u : 0100644u; +std::uint32_t external_attrs(bool executable, bool symlink = false) { + std::uint32_t mode = symlink ? 0120777u : executable ? 0100755u : 0100644u; return mode << 16; } @@ -121,22 +140,25 @@ std::uint32_t crc32(std::string_view data) { } std::expected -write(const std::filesystem::path& out, const std::vector& entries) { +write(const std::filesystem::path& out, const std::vector& entries, + const WriteOptions& options) { std::error_code ec; if (out.has_parent_path()) std::filesystem::create_directories(out.parent_path(), ec); struct Central { std::string name; - std::uint32_t crc = 0, size = 0, offset = 0; - bool executable = false; + std::uint32_t crc = 0, size = 0, packed = 0, offset = 0; + std::uint16_t method = 0; + bool executable = false, symlink = false; }; std::vector central; central.reserve(entries.size()); std::string blob; for (auto const& e : entries) { - auto data = detail::slurp(e.source); + const bool symlink = !e.symlinkTarget.empty(); + auto data = symlink ? std::optional(e.symlinkTarget) : detail::slurp(e.source); if (!data) return std::unexpected(std::format( "cannot read '{}' for the archive", e.source.string())); @@ -155,20 +177,60 @@ write(const std::filesystem::path& out, const std::vector& entries) { c.size = static_cast(data->size()); c.offset = static_cast(blob.size()); c.executable = e.executable; + c.symlink = symlink; + + // The member's bytes: DEFLATEd when asked and smaller, stored + // otherwise, and stored when its data is to be aligned. + std::string packed; + if (options.deflate && options.align == 0 && !symlink && !data->empty()) { + auto d = mcpp::pack::compress::deflate_raw(*data, options.level); + if (!d) return std::unexpected(std::format("'{}': {}", e.source.string(), d.error())); + if (d->size() < data->size()) { packed = std::move(*d); c.method = 8; } + } + const std::string& body = c.method == 8 ? packed : *data; + c.packed = static_cast(body.size()); + + // The padding that puts a stored member's data on the boundary, as + // zipalign writes it: an extra field (id 0xD935) holding the alignment + // and zero bytes. A gap too short for a field (under six bytes) lies + // before the local header instead, where no directory entry names it + // and a reader through the central directory never looks. + std::string extraField; + if (options.align > 1 && c.method == 0) { + auto pad_at = [&](std::size_t headerAt) { + const auto dataAt = headerAt + 30 + c.name.size(); + return (options.align - dataAt % options.align) % options.align; + }; + auto need = pad_at(blob.size()); + if (need != 0 && need < 6) { + blob.append(need, '\0'); + need = pad_at(blob.size()); + // After the gap the data is on the boundary; nothing more. + } + if (need >= 6) { + detail::put16(extraField, 0xD935); + detail::put16(extraField, static_cast(need - 4)); + detail::put16(extraField, static_cast( + options.align > 0xFFFF ? 0 : options.align)); + extraField.append(need - 6, '\0'); + } + c.offset = static_cast(blob.size()); + } detail::put32(blob, 0x04034b50); // local file header detail::put16(blob, 20); // version needed (2.0) detail::put16(blob, 0); // flags — no data descriptor - detail::put16(blob, 0); // method 0: stored + detail::put16(blob, c.method); // 0 stored, 8 deflated detail::put16(blob, detail::kDosTime); detail::put16(blob, detail::kDosDate); detail::put32(blob, c.crc); - detail::put32(blob, c.size); // compressed == uncompressed + detail::put32(blob, c.packed); detail::put32(blob, c.size); detail::put16(blob, static_cast(c.name.size())); - detail::put16(blob, 0); // extra field length + detail::put16(blob, static_cast(extraField.size())); blob += c.name; - blob += *data; + blob += extraField; + blob += body; central.push_back(std::move(c)); } @@ -181,18 +243,18 @@ write(const std::filesystem::path& out, const std::vector& entries) { detail::put16(blob, (3 << 8) | 20); detail::put16(blob, 20); // version needed detail::put16(blob, 0); - detail::put16(blob, 0); // stored + detail::put16(blob, c.method); detail::put16(blob, detail::kDosTime); detail::put16(blob, detail::kDosDate); detail::put32(blob, c.crc); - detail::put32(blob, c.size); + detail::put32(blob, c.packed); detail::put32(blob, c.size); detail::put16(blob, static_cast(c.name.size())); detail::put16(blob, 0); // extra detail::put16(blob, 0); // comment detail::put16(blob, 0); // disk number detail::put16(blob, 0); // internal attrs - detail::put32(blob, detail::external_attrs(c.executable)); + detail::put32(blob, detail::external_attrs(c.executable, c.symlink)); detail::put32(blob, c.offset); blob += c.name; } diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm new file mode 100644 index 000000000..a00609753 --- /dev/null +++ b/src/platform/platform.cppm @@ -0,0 +1,568 @@ +// mcpp.platform -- the engine's half of platforms (SPEC-013; design +// 2026-10-28-platforms-design.md §3-§9). +// +// The engine holds no knowledge of any one platform. It provides the +// mechanism: which platform a graph uses (selection), what the platform's +// statement is once every tier has spoken (merge), the flags it contributes to +// a target (evaluation of its cfg tables), how an artifact is named and sealed +// (patterns and argv templates), and how all of it is reported. The data comes +// from a platform package's `[platform]` section (mcpp.manifest.platform) and +// from the tables that override it; the prepare phases apply the result to the +// graph, the toolchain, the flags, the link, the runner and the pack. +// +// Everything here is a pure function of its arguments, and the unit tests +// state the rules as tables (tests/unit/test_platform_*.cpp). Whether a +// `[target.]` applies is asked of the caller (`Matches`), which +// holds the one evaluator of selectors (mcpp.build.prepare_inputs), so this +// module depends on the data model alone. (Importing prepare_inputs here also +// made GCC 16.1 crash importing mcpp.cli, mcpp#848.) + +export module mcpp.platform; + +import std; +import mcpp.manifest; +import mcpp.toolchain.triple; +import mcpp.invocation; + +export namespace mcpp::platform { + +// Whether a selector (`cfg(...)`, a triple, an alias) applies to the target +// of this graph. +using Matches = std::function; + +// ── Selection (§5.1, §6.1) ────────────────────────────────────────────────── + +// Which platform one graph uses. `none` is the engine's own conventions for +// the target (a host target's, or nothing at all). +struct Selection { + mcpp::manifest::platform::PackageRef ref; + mcpp::manifest::platform::Origin origin; + bool none = true; + // A host triple selected the platform: only its `[platform.hosted]` part + // applies (§5.4). + bool hosted = false; +}; + +// The value a flag or an environment variable gives: `none`, a package +// reference `@`, or a directory (`path:`, or anything +// spelled as a path), relative to where mcpp was started. +std::expected parse_choice(std::string_view value, mcpp::manifest::platform::Origin origin); + +// The tables of `tables` whose selector the context meets, least specific +// first (SPEC-004 §3.1.1), so that applying them in order lets the most +// specific one decide. +std::vector matching(std::span tables, + const Matches& matches); + +// Everything a selection reads, in precedence order. +struct ChoiceInputs { + std::string_view flag; // MCPP_PLATFORM (`--platform`) + std::string_view env; // MCPP_TARGET__PLATFORM + std::string envKey; + std::span project; // the plan root's tables, the workspace layer's first + std::span config; // config.toml's + std::string_view column; // the target table's platform column + std::string flagKey = "--platform"; // or MCPP_PLATFORM, from the environment +}; + +// The target graph's platform: the flag, the environment, the project, this +// machine's configuration, the target table, in that order; the first to +// answer decides. +std::expected select_target(const ChoiceInputs& in, const Matches& matches); + +// The host graph's platform: `--host-platform` / MCPP_HOST_PLATFORM, the +// project's `[host]`, config.toml's `[host]`; otherwise the host conventions. +std::expected select_host(std::string_view flagOrEnv, bool fromFlag, + const mcpp::manifest::platform::Override& project, + const mcpp::manifest::platform::Override& config); + +// ── Merge (§4.5) ──────────────────────────────────────────────────────────── + +// `over` replaces `base` field by field, with two exceptions: flag lists +// concatenate (the platform's first, so a convention cannot be removed, only +// added to), and `link.inputs` and `link.seal` are replaced whole (they are +// one sequence each). Maps (`layers`, `tools`, `runners`, `versions`) are +// merged key by key. +void merge(mcpp::manifest::platform::Statement& base, const mcpp::manifest::platform::Statement& over); + +// The part of a platform that applies when a host triple selects it: its +// `[platform.hosted]` toolchain and layers, and nothing else (§5.4). +mcpp::manifest::platform::Statement hosted_view(const mcpp::manifest::platform::Statement& platform); + +// Whether the platform serves the triple, or refuses with the reason. A host +// triple is served by a platform with a `[platform.hosted]` table. +std::expected +serves(const mcpp::manifest::platform::Statement& platform, std::string_view package, + const mcpp::toolchain::triple::Triple& t); + +// The four flag lists the platform gives every unit of a target: the +// unconditional ones, then each cfg table the context meets, in file order. +struct TargetFlags { + std::vector cflags, cxxflags, asmflags, ldflags; +}; +TargetFlags evaluate_flags(const mcpp::manifest::platform::Statement& s, const Matches& matches); + +// ── Artifacts (§4, §4.6) ──────────────────────────────────────────────────── + +// `"lib{name}.okso"` -> {"lib", ".okso"}. A pattern names `{name}` exactly once. +std::expected, std::string> +split_name_pattern(std::string_view pattern); + +// One seal step for one artifact: `{}` is the artifact; an argument `{needed}` +// stands for the shared artifacts it needs, one argument each, and when the +// argument before it is an option that takes a value (`--with`: no `=`, no +// `{}`), that option is repeated before each of them (both are left out when +// it needs none). +std::vector expand_step(const std::vector& step, + std::string_view artifact, + std::span needed); + +// ── What the build uses (§9) ──────────────────────────────────────────────── + +// The platform of one graph after every tier has spoken: what the flags, the +// link, the runner, the pack and the report read. +struct Resolved { + bool active = false; // a platform package is in use + Selection selection; + std::string package; // "mcpplibs.openkal-platform" + std::string version; // "0.17.1" + std::filesystem::path root; // its directory + mcpp::manifest::platform::Statement fields; // merged + TargetFlags flags; // evaluated for the target + // Per layer the platform names: who provides it in this build, and + // whether that is the platform's package or one the graph already had. + struct Layer { std::string provider; mcpp::manifest::platform::Tier tier = mcpp::manifest::platform::Tier::Platform; }; + std::map layers; + // Per tool: the program the build runs. + std::map tools; + + // The platform's part of a configuration's identity: name, version and + // source, so that another platform is another build directory (§8.3). + std::string identity() const; + // `openkal-platform 0.17.1` + std::string display() const; +}; + +// The `source` column of the report and of JSON: `built-in`, `platform`, +// `config.toml`, `mcpp.toml`, `env`, `flag`, `graph`, `build-program`. +std::string origin_text(const mcpp::manifest::platform::Origin& o); + +// The lines `Platform …` and its fields, as the target report prints them. +std::vector> report_rows(const Resolved& r); + +// The JSON object of the `plan` event's `platform` / `hostPlatform`. +std::string to_json(const Resolved& r); + +} // namespace mcpp::platform + +// ── implementation ────────────────────────────────────────────────────────── + +namespace mcpp::platform { + +namespace { + +namespace mp = mcpp::manifest::platform; + +bool names_a_path(std::string_view v) { + if (v.empty()) return false; + if (v.starts_with("path:")) return true; + if (v.starts_with("/") || v.starts_with("./") || v.starts_with("../") || v.starts_with("~") + || v == "." || v == "..") + return true; + if (v.size() > 2 && v[1] == ':' && (v[2] == '\\' || v[2] == '/')) return true; // C:\… + return v.find('@') == std::string_view::npos + && (v.find('/') != std::string_view::npos || v.find('\\') != std::string_view::npos); +} + +void append(std::vector& to, const std::vector& from) { + to.insert(to.end(), from.begin(), from.end()); +} + +std::string json_escape(std::string_view s) { + std::string o; + o.reserve(s.size() + 2); + for (char c : s) { + switch (c) { + case '"': o += "\\\""; break; + case '\\': o += "\\\\"; break; + case '\n': o += "\\n"; break; + case '\t': o += "\\t"; break; + case '\r': o += "\\r"; break; + default: + if (static_cast(c) < 0x20) o += std::format("\\u{:04x}", int(c)); + else o += c; + } + } + return o; +} + +std::string q(std::string_view s) { return "\"" + json_escape(s) + "\""; } + +// An option that takes the next argument as its value (`--with`, `-L`): no +// `=` and no `{}` of its own. +bool is_bare_option(std::string_view a) { + return a.starts_with("-") && a.find('=') == std::string_view::npos + && a.find("{}") == std::string_view::npos; +} + +std::string argv_text(const std::vector& v) { + std::string out; + for (auto const& a : v) { + if (!out.empty()) out += ' '; + out += a.find(' ') == std::string::npos ? a : "\"" + a + "\""; + } + return out; +} + +} // namespace + +std::expected parse_choice(std::string_view value, mp::Origin origin) { + Selection s; + s.origin = std::move(origin); + if (value == "none") return s; + if (names_a_path(value)) { + std::string dir(value.starts_with("path:") ? value.substr(5) : value); + if (dir.starts_with("~")) + if (auto h = mcpp::invocation::get("HOME"); h && !h->empty()) dir = *h + dir.substr(1); + std::error_code ec; + auto p = std::filesystem::absolute(std::filesystem::path(dir), ec).lexically_normal(); + s.ref.spec.path = p.generic_string(); + s.none = false; + return s; + } + auto r = mp::parse_ref_text(value); + if (!r) return std::unexpected(std::format("{}: {}", s.origin.key, r.error())); + s.ref = std::move(*r); + s.none = false; + return s; +} + +std::vector matching(std::span tables, + const Matches& matches) { + std::vector out; + for (auto const& t : tables) + if (matches(t.selector)) out.push_back(&t); + std::ranges::stable_sort(out, [](const mp::Override* a, const mp::Override* b) { + return mcpp::manifest::cfg::specificity(a->selector) + < mcpp::manifest::cfg::specificity(b->selector); + }); + return out; +} + +std::expected select_target(const ChoiceInputs& in, const Matches& matches) { + if (!in.flag.empty()) + return parse_choice(in.flag, mp::Origin{mp::Tier::Flag, {}, 0, in.flagKey}); + if (!in.env.empty()) + return parse_choice(in.env, mp::Origin{mp::Tier::Env, {}, 0, in.envKey}); + for (auto span : {in.project, in.config}) { + const mp::Override* chosen = nullptr; + for (auto const* o : matching(span, matches)) + if (o->platformSet) chosen = o; + if (!chosen) continue; + Selection s; + s.origin = chosen->platformOrigin; + if (s.origin.key.empty()) + s.origin.key = std::format("[target.{}].platform", chosen->selector); + if (chosen->platformNone) return s; + s.ref = chosen->platform; + s.none = false; + return s; + } + if (!in.column.empty()) { + auto s = parse_choice(in.column, mp::Origin{mp::Tier::BuiltIn, {}, 0, "the target table"}); + if (!s) return s; + return s; + } + Selection none; + none.origin = mp::Origin{mp::Tier::BuiltIn, {}, 0, "the target table"}; + return none; +} + +std::expected select_host(std::string_view flagOrEnv, bool fromFlag, + const mp::Override& project, + const mp::Override& config) { + if (!flagOrEnv.empty()) + return parse_choice(flagOrEnv, mp::Origin{fromFlag ? mp::Tier::Flag : mp::Tier::Env, {}, 0, + fromFlag ? "--host-platform" : "MCPP_HOST_PLATFORM"}); + for (auto const* o : {&project, &config}) { + if (!o->platformSet) continue; + Selection s; + s.origin = o->platformOrigin; + if (s.origin.key.empty()) s.origin.key = "[host].platform"; + if (o->platformNone) return s; + s.ref = o->platform; + s.none = false; + return s; + } + Selection none; + none.origin = mp::Origin{mp::Tier::BuiltIn, {}, 0, "host conventions"}; + return none; +} + +void merge(mp::Statement& b, const mp::Statement& o) { + auto text = [](mp::Text& to, const mp::Text& from) { if (from.set) to = from; }; + auto words = [](mp::Words& to, const mp::Words& from) { if (from.set) to = from; }; + words(b.targets, o.targets); + text(b.toolchain, o.toolchain); + if (o.sysroot.set) { b.sysroot = o.sysroot; b.sysrootProvides = o.sysrootProvides; } + // Field by field: an argv-only statement keeps the platform's `host` and + // `exclusive`; a table, or a program's `run-host`, states what it states. + if (o.runner.set) { + b.runner.cmd = o.runner.cmd; + b.runner.origin = o.runner.origin; + b.runner.set = true; + } + if (!o.runner.host.empty()) b.runner.host = o.runner.host; + if (o.runner.exclusiveSet) { + b.runner.exclusive = o.runner.exclusive; + b.runner.exclusiveSet = true; + } + for (auto const& [k, v] : o.runners) b.runners[k] = v; + if (o.carrier.set) b.carrier = o.carrier; + text(b.plugin, o.plugin); + if (o.universal.set) b.universal = o.universal; + for (auto const& [k, v] : o.layers) b.layers[k] = v; + for (auto const& [k, v] : o.tools) b.tools[k] = v; + append(b.build.cflags, o.build.cflags); + append(b.build.cxxflags, o.build.cxxflags); + append(b.build.asmflags, o.build.asmflags); + append(b.build.ldflags, o.build.ldflags); + for (auto const& c : o.buildCfg) b.buildCfg.push_back(c); + text(b.link.driver, o.link.driver); + if (o.link.flags.set) { + append(b.link.flags.value, o.link.flags.value); + if (!b.link.flags.set) b.link.flags.origin = o.link.flags.origin; + b.link.flags.set = true; + } + text(b.link.start, o.link.start); + text(b.link.script, o.link.script); + if (o.link.inputsSet) { + b.link.inputs = o.link.inputs; + b.link.inputsOrigin = o.link.inputsOrigin; + b.link.inputsSet = true; + } + text(b.link.exe, o.link.exe); + text(b.link.shared, o.link.shared); + if (o.link.sealSet) { + b.link.seal = o.link.seal; + b.link.sealOrigin = o.link.sealOrigin; + b.link.sealSet = true; + } + for (auto const& [k, v] : o.versions) b.versions[k] = v; + if (!o.versions.empty()) b.versionsOrigin = o.versionsOrigin; + if (o.hosted.set) { + text(b.hosted.toolchain, o.hosted.toolchain); + for (auto const& [k, v] : o.hosted.layers) b.hosted.layers[k] = v; + b.hosted.set = true; + } +} + +mp::Statement hosted_view(const mp::Statement& p) { + mp::Statement s; + s.targets = p.targets; + s.toolchain = p.hosted.toolchain; + s.layers = p.hosted.layers; + s.versions = p.versions; + s.versionsOrigin = p.versionsOrigin; + return s; +} + +std::expected +serves(const mp::Statement& p, std::string_view package, const mcpp::toolchain::triple::Triple& t) { + for (auto const& w : p.targets.value) { + auto pt = mcpp::toolchain::triple::parse(w); + if (pt ? pt->str() == t.str() : w == t.str()) return false; + } + std::string list; + for (auto const& w : p.targets.value) { if (!list.empty()) list += ", "; list += w; } + if (p.hosted.set) { + if (!p.hosted.targets.set) return true; + std::string hosts; + for (auto const& w : p.hosted.targets.value) { + auto pt = mcpp::toolchain::triple::parse(w); + if (pt ? pt->str() == t.str() : w == t.str()) return true; + if (!hosts.empty()) hosts += ", "; + hosts += w; + } + // The arrangement is for some host triples and not this one (a C + // library of the graph on Windows needs the GNU ABI): name the row of + // the same system and architecture it does serve. + std::string near; + for (auto const& w : p.hosted.targets.value) + if (auto pt = mcpp::toolchain::triple::parse(w); pt && pt->arch == t.arch && pt->os == t.os) + near = pt->str(); + return std::unexpected(std::format( + "platform {} hosts {} and not {} ([platform.hosted] targets).{}", + package, hosts, t.str(), + near.empty() ? std::string() + : std::format("\n Build for {} (--target {}, or [build] target = \"{}\")", + near, near, near))); + } + return std::unexpected(std::format( + "platform {} serves {} and not {}: it has no [platform.hosted] table, the part a host " + "triple may take (SPEC-013 §5.4).\n" + " Build for one of its targets (--target {}), or select another platform for {}", + package, list, t.str(), p.targets.value.empty() ? std::string("") : p.targets.value.front(), + t.str())); +} + +TargetFlags evaluate_flags(const mp::Statement& s, const Matches& matches) { + TargetFlags f; + append(f.cflags, s.build.cflags); + append(f.cxxflags, s.build.cxxflags); + append(f.asmflags, s.build.asmflags); + append(f.ldflags, s.build.ldflags); + for (auto const& t : s.buildCfg) { + if (!matches(t.predicate)) continue; + append(f.cflags, t.flags.cflags); + append(f.cxxflags, t.flags.cxxflags); + append(f.asmflags, t.flags.asmflags); + append(f.ldflags, t.flags.ldflags); + } + return f; +} + +std::expected, std::string> +split_name_pattern(std::string_view pattern) { + constexpr std::string_view kName = "{name}"; + const auto at = pattern.find(kName); + if (at == std::string_view::npos || pattern.find(kName, at + 1) != std::string_view::npos) + return std::unexpected(std::format( + "'{}' names the artifact as {{name}} exactly once, for example \"{{name}}.oke\"", pattern)); + auto prefix = std::string(pattern.substr(0, at)); + auto suffix = std::string(pattern.substr(at + kName.size())); + if (prefix.find_first_of("/\\") != std::string::npos || suffix.find_first_of("/\\") != std::string::npos) + return std::unexpected(std::format("'{}' is a file name pattern; it names no directory", pattern)); + return std::pair{ std::move(prefix), std::move(suffix) }; +} + +std::vector expand_step(const std::vector& step, + std::string_view artifact, + std::span needed) { + std::vector out; + for (std::size_t i = 0; i < step.size(); ++i) { + auto const& a = step[i]; + if (a == "{needed}") { + // The option in front of it, if any, was held back below. + const bool optioned = i > 0 && is_bare_option(step[i - 1]); + for (auto const& n : needed) { + if (optioned) out.push_back(step[i - 1]); + out.push_back(n); + } + continue; + } + if (i + 1 < step.size() && step[i + 1] == "{needed}" && is_bare_option(a)) continue; + if (a == "{}") { out.emplace_back(artifact); continue; } + // `{}` inside an argument (`--image={}`) stands for the artifact too. + std::string v = a; + for (std::size_t p = v.find("{}"); p != std::string::npos; p = v.find("{}", p + artifact.size())) + v.replace(p, 2, artifact); + out.push_back(std::move(v)); + } + return out; +} + +std::string Resolved::identity() const { + if (!active) return {}; + std::string id = std::format("{}@{}", package, version); + if (!selection.ref.spec.path.empty()) id += "|path:" + selection.ref.spec.path; + if (!selection.ref.spec.git.empty()) + id += std::format("|git:{}#{}", selection.ref.spec.git, selection.ref.spec.gitRev); + return id; +} + +std::string Resolved::display() const { + if (!active) return "host conventions"; + auto dot = package.find('.'); + return std::format("{} {}", dot == std::string::npos ? package : package.substr(dot + 1), version); +} + +std::string origin_text(const mp::Origin& o) { + switch (o.tier) { + case mp::Tier::Env: + case mp::Tier::Flag: return o.key; + case mp::Tier::Config: + case mp::Tier::Manifest: { + std::string file = std::filesystem::path(o.file).filename().generic_string(); + if (file.empty()) file = std::string(mp::tier_name(o.tier)); + return o.line > 0 ? std::format("{}:{}", file, o.line) : file; + } + default: return std::string(mp::tier_name(o.tier)); + } +} + +std::vector> report_rows(const Resolved& r) { + std::vector> rows; + if (!r.active) return rows; + auto const& f = r.fields; + for (auto const& [layer, l] : r.layers) + rows.emplace_back(layer, std::format("{} ({})", l.provider, mp::tier_name(l.tier))); + if (!f.tools.empty()) { + // Tools grouped by the package that builds them. + std::map, std::string>> by; + for (auto const& [name, t] : f.tools) { + auto key = mp::display(t.ref); + by[key].first.push_back(name); + by[key].second = origin_text(t.origin); + } + for (auto const& [pkg, v] : by) { + std::string names; + for (auto const& n : v.first) { if (!names.empty()) names += ", "; names += n; } + rows.emplace_back("tools", std::format("{} ({}, {})", names, pkg, v.second)); + } + } + if (f.runner.set) + rows.emplace_back("runner", std::format("{} ({})", f.runner.cmd.empty() ? std::string() : f.runner.cmd.front(), + origin_text(f.runner.origin))); + for (auto const& [name, w] : f.runners) + rows.emplace_back("runner " + name, std::format("{} ({})", argv_text(w.value), origin_text(w.origin))); + if (f.link.driver.set && f.link.driver.value != "compiler") + rows.emplace_back("link", std::format("{}{} ({})", f.link.driver.value, + f.link.start.set ? ", start " + f.link.start.value : "", + origin_text(f.link.driver.origin))); + return rows; +} + +std::string to_json(const Resolved& r) { + if (!r.active) return "null"; + auto const& f = r.fields; + std::string o = "{"; + o += "\"name\":" + q(r.package); + o += ",\"version\":" + q(r.version); + o += ",\"source\":" + q(origin_text(r.selection.origin)); + o += ",\"hosted\":" + std::string(r.selection.hosted ? "true" : "false"); + o += ",\"fields\":{"; + bool first = true; + auto field = [&](std::string_view key, std::string value, const mp::Origin& origin) { + if (!first) o += ","; + first = false; + o += q(key) + ":{\"value\":" + value + ",\"source\":" + q(origin_text(origin)) + "}"; + }; + auto arr = [](const std::vector& v) { + std::string s = "["; + for (std::size_t i = 0; i < v.size(); ++i) { if (i) s += ","; s += q(v[i]); } + return s + "]"; + }; + if (f.toolchain.set) field("toolchain", q(f.toolchain.value), f.toolchain.origin); + if (f.runner.set) field("runner", arr(f.runner.cmd), f.runner.origin); + for (auto const& [k, w] : f.runners) field("runners." + k, arr(w.value), w.origin); + for (auto const& [layer, l] : r.layers) { + mp::Origin origin{l.tier, {}, 0, {}}; + if (auto it = f.layers.find(layer); it != f.layers.end() && l.tier != mp::Tier::Graph) + origin = it->second.origin; + field("layers." + layer, q(l.provider), origin); + } + for (auto const& [name, t] : f.tools) { + auto it = r.tools.find(name); + field("tools." + name, it == r.tools.end() ? q(mp::display(t.ref)) : q(it->second), t.origin); + } + if (f.link.driver.set) field("link.driver", q(f.link.driver.value), f.link.driver.origin); + if (f.link.start.set) field("link.start", q(f.link.start.value), f.link.start.origin); + if (f.link.exe.set) field("link.exe", q(f.link.exe.value), f.link.exe.origin); + if (f.link.shared.set) field("link.shared", q(f.link.shared.value), f.link.shared.origin); + if (f.carrier.set) field("carrier", q(mp::display(f.carrier.package)), f.carrier.origin); + o += "}}"; + return o; +} + +} // namespace mcpp::platform diff --git a/src/pm/lock_io.cppm b/src/pm/lock_io.cppm index be0b53239..99f43752f 100644 --- a/src/pm/lock_io.cppm +++ b/src/pm/lock_io.cppm @@ -32,6 +32,16 @@ struct LockedPackage { std::string version; std::string source; // e.g. "index+mcpplibs@abc123def..." std::string hash; // "sha256:..." or "fnv1a:..." + // Who put the package in the graph when the project did not (SPEC-013 + // §8.3): "platform" for a package the target's platform added (the + // platform package itself, a layer's provider, a tool's package). Empty + // for a package the project declared or one reached through it; a lock + // written before the field reads as empty. + std::string origin; + // The configurations that resolve it, by target triple (sorted): a build + // for one target rewrites its own and keeps the others' (SPEC-013 §15.1). + // Empty in a lock written before the field. + std::vector targets; }; // Parsed form of a git source string as written to mcpp.lock. @@ -137,6 +147,10 @@ std::expected load(const std::filesystem::path& path) { if (auto it = tt.find("version"); it != tt.end() && it->second.is_string()) lp.version = it->second.as_string(); if (auto it = tt.find("source"); it != tt.end() && it->second.is_string()) lp.source = it->second.as_string(); if (auto it = tt.find("hash"); it != tt.end() && it->second.is_string()) lp.hash = it->second.as_string(); + if (auto it = tt.find("origin"); it != tt.end() && it->second.is_string()) lp.origin = it->second.as_string(); + if (auto it = tt.find("targets"); it != tt.end() && it->second.is_array()) + for (auto const& e : it->second.as_array()) + if (e.is_string()) lp.targets.push_back(e.as_string()); // v1 → v2 migration: default namespace to "mcpplibs". if (fileVersion < 2 && lp.namespace_.empty()) { @@ -187,7 +201,14 @@ std::string serialize(const Lockfile& lock) { } out += std::format("version = {}\n", t::escape_string(p.version)); out += std::format("source = {}\n", t::escape_string(p.source)); - out += std::format("hash = {}\n\n", t::escape_string(p.hash)); + out += std::format("hash = {}\n", t::escape_string(p.hash)); + if (!p.origin.empty()) out += std::format("origin = {}\n", t::escape_string(p.origin)); + if (!p.targets.empty()) { + std::string list; + for (auto const& t : p.targets) list += (list.empty() ? "" : ", ") + t::escape_string(t); + out += std::format("targets = [{}]\n", list); + } + out += "\n"; } return out; } diff --git a/src/project.cppm b/src/project.cppm index e3eb80741..0d0b4e1d2 100644 --- a/src/project.cppm +++ b/src/project.cppm @@ -398,6 +398,14 @@ void merge_layer_root_position(mcpp::manifest::Manifest& member, if (to.runner.empty()) to.runner = from.runner; for (auto const& [n, argv] : from.namedRunners) to.namedRunners.try_emplace(n, argv); } + + // The platform part of `[workspace.target.]` (SPEC-013 §13): the + // workspace's tables merge first, so of two tables of one selector the + // member's wins field by field; between selectors the more specific wins, + // as for the other keys above. `[host]` is not a workspace table. + member.platformOverrides.insert(member.platformOverrides.begin(), + layer.platformOverrides.begin(), + layer.platformOverrides.end()); } } // namespace detail @@ -1073,6 +1081,14 @@ export std::string root_position_key( for (auto const& [name, argv] : e.namedRunners) list("target." + triple + ".runners." + name, argv); } + // The platform rows (SPEC-013 §4.5). A row that asks which platform is + // selected cannot be decided before the platform is, so it always counts. + for (auto const& o : m.platformOverrides) { + if (applies && !applies(o.selector) && o.selector.find("platform") == std::string::npos) + continue; + field("platform", mcpp::manifest::platform::identity(o)); + } + if (m.hostPlatform.platformSet) field("host.platform", mcpp::manifest::platform::identity(m.hostPlatform)); field("standard", m.package.standard); auto const& b = m.buildConfig; auto dialect = b.dialectCxxflags; @@ -1147,6 +1163,8 @@ virtual_workspace_root(const mcpp::manifest::Manifest& workspace, v.cppStandard = first.cppStandard; v.toolchain = first.toolchain; v.targetOverrides = first.targetOverrides; + v.platformOverrides = first.platformOverrides; + v.hostPlatform = first.hostPlatform; v.indices = first.indices; v.profiles = first.profiles; v.capabilityPins = first.capabilityPins; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index a1809bf61..62e8c7b1c 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -648,6 +648,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, // So the convention travels in its own field, empty when there is // none. Same defect family as the release this was written for. std::string pin; // the target table's convention pin, or empty + std::string platform; // the target table's platform column (SPEC-013 §7), or empty std::string status; // installed | available | planned bool isDefault = false; int rank = 0; // display order: installed < available < planned @@ -691,7 +692,11 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, r.note = note_for(t); r.toolchain = tcLabel; for (auto const& info : mcpp::toolchain::triple::known_targets()) - if (info.canonical == r.target) { r.pin = std::string(info.pin); break; } + if (info.canonical == r.target) { + r.pin = std::string(info.pin); + r.platform = std::string(info.platform); + break; + } r.status = "installed"; r.rank = 0; r.isDefault = defSpec @@ -770,6 +775,7 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, if (auto at = pin.find('@'); at != std::string::npos) pin[at] = ' '; r.toolchain = pin.empty() ? "—" : pin; r.pin = std::string(info.pin); + r.platform = std::string(info.platform); // Three answers, not two. "available" means a payload here produces it; // "via dependency graph" means the compiler is here and the system has // to come from packages — a different thing to do next, so a different @@ -793,6 +799,9 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, {"note", r.note}, {"toolchain", r.toolchain}, {"pin", r.pin}, + // The platform a build of the target uses when nothing selects + // one (SPEC-013 §7); null: the engine's own conventions. + {"platform", r.platform.empty() ? nlohmann::json(nullptr) : nlohmann::json(r.platform)}, {"status", r.status}, {"default", r.isDefault}, }); @@ -811,12 +820,19 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, if (!targetRows.empty()) { std::println(""); std::println("Targets:"); - std::println(" {:<3}{:<24}{:<22}{:<18}{}", - "", "TARGET", "NOTE", "TOOLCHAIN", "STATUS"); + std::println(" {:<3}{:<24}{:<22}{:<18}{:<26}{}", + "", "TARGET", "NOTE", "TOOLCHAIN", "PLATFORM", "STATUS"); for (auto& r : targetRows) { - std::println(" {:<3}{:<24}{:<22}{:<18}{}", + // The platform package without its namespace, `name version`. + std::string platform = "—"; + if (!r.platform.empty()) { + platform = r.platform.substr(r.platform.find('.') == std::string::npos + ? 0 : r.platform.find('.') + 1); + if (auto at = platform.find('@'); at != std::string::npos) platform[at] = ' '; + } + std::println(" {:<3}{:<24}{:<22}{:<18}{:<26}{}", r.isDefault ? "*" : "", - r.target, r.note, r.toolchain, r.status); + r.target, r.note, r.toolchain, platform, r.status); } } diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 49b907536..03b36e1fb 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -1442,7 +1442,7 @@ bool host_can_serve(const triple::Triple& target) { if (target.is_freestanding()) return true; // An ok image the same way: clang and lld emit it on every host, and what // the image holds above `kal_*` is the graph's. - if (target.is_openkal()) return true; + if (target.loader_supplies_os()) return true; return false; } diff --git a/tests/e2e/202_machine_output_contract.sh b/tests/e2e/202_machine_output_contract.sh index 9e19c640a..f39954531 100755 --- a/tests/e2e/202_machine_output_contract.sh +++ b/tests/e2e/202_machine_output_contract.sh @@ -9,7 +9,7 @@ # # mcpp cache list --format json -> stdout: "Error: unknown option: --format" # stderr: (empty) rc=1 -# mcpp pack --format bogus -> stderr: "invalid --format ..." rc=2 +# mcpp pack --pkg-format bogus -> stderr: "invalid --pkg-format ..." rc=2 # # Two shapes for the same class of mistake, and the first one writes human text # into the channel a protocol owns. `mcpp cache list --format json | jq` gets a @@ -50,7 +50,7 @@ check "unknown option (top level)" 2 "$MCPP" --no-such-option # is the branch that answers "I know the option, not that value". check "unsupported value (self env)" 2 "$MCPP" self env --format yaml check "unsupported value (cache list)" 2 "$MCPP" cache list --format yaml -check "unsupported value (pack)" 2 "$MCPP" pack --format bogus +check "unsupported value (pack)" 2 "$MCPP" pack --pkg-format bogus # And the supported ones must still be JSON on stdout. Asserted here because # every check above is about what does NOT happen; without this, deleting the diff --git a/tests/e2e/242_pack_library_interface_and_headers.sh b/tests/e2e/242_pack_library_interface_and_headers.sh index f7eb5c306..a9b7652a0 100755 --- a/tests/e2e/242_pack_library_interface_and_headers.sh +++ b/tests/e2e/242_pack_library_interface_and_headers.sh @@ -65,6 +65,20 @@ EOF cd mathkit "$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } +# The container as stated, compressed by mcpp itself; a single file is a +# program's, not a library's. +"$MCPP" pack mathkit --pkg-format tar --compress zstd > pack-zst.log 2>&1 || { cat pack-zst.log; echo "pack --compress zstd failed"; exit 1; } +zst="$(find target/dist -maxdepth 1 -type f -name 'mathkit-0.1.0-*.tar.zst' | head -1)" +[[ -n "$zst" ]] || { cat pack-zst.log; ls target/dist; echo "no .tar.zst for --compress zstd"; exit 1; } +[[ "$(head -c 4 "$zst" | od -An -tx1 | tr -d ' \n')" == "28b52ffd" ]] || { echo "$zst is not a zstd frame"; exit 1; } +"$MCPP" pack mathkit --pkg-format zip > pack-zip.log 2>&1 || { cat pack-zip.log; echo "pack --pkg-format zip failed"; exit 1; } +ls target/dist/mathkit-0.1.0-*.zip > /dev/null 2>&1 || { cat pack-zip.log; ls target/dist; echo "no zip for --pkg-format zip"; exit 1; } +if "$MCPP" pack mathkit --pkg-format single-file > pack-sf.log 2>&1; then + cat pack-sf.log; echo "a library was packed as a single file"; exit 1 +fi +grep -q "is a library" pack-sf.log || { cat pack-sf.log; echo "the single-file refusal does not say why"; exit 1; } +"$MCPP" pack mathkit > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } + pkg="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" # The manifest below is FILE CONTENT: on Git Bash a shell-spelled # /tmp/... path is read by a native mcpp.exe as "root of the current diff --git a/tests/e2e/257_shared_library_pe.sh b/tests/e2e/257_shared_library_pe.sh index 71ec74e0d..133ee9412 100755 --- a/tests/e2e/257_shared_library_pe.sh +++ b/tests/e2e/257_shared_library_pe.sh @@ -92,7 +92,7 @@ grep -qE "^build bin/libmathkit\.dll \| bin/libmathkit\.dll\.a : " "$nj" || { # ── 2. the package carries both, and names the right one to link ──────── rm -rf target -"$MCPP" pack mathkit --target "$TRIPLE" --format dir > pack.log 2>&1 \ +"$MCPP" pack mathkit --target "$TRIPLE" --pkg-format dir > pack.log 2>&1 \ || { cat pack.log; echo "pack failed"; exit 1; } pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0*' | head -1)" [[ -f "$pkg/lib/$TRIPLE/libmathkit.dll" ]] || { find "$pkg" -type f; echo "FAIL: no DLL in the package"; exit 1; } diff --git a/tests/e2e/259_shared_library_macho.sh b/tests/e2e/259_shared_library_macho.sh index 358b4dedd..ca54f511d 100755 --- a/tests/e2e/259_shared_library_macho.sh +++ b/tests/e2e/259_shared_library_macho.sh @@ -74,7 +74,7 @@ fi # ── 3. pack it ────────────────────────────────────────────────────────── rm -rf target -"$MCPP" pack mathkit --format dir > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } +"$MCPP" pack mathkit --pkg-format dir > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } pkgrel="$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0*' | head -1)" [[ -n "$pkgrel" ]] || { cat pack.log; echo "FAIL: no package"; exit 1; } # OUT of the producer's tree, so the next step can delete that tree entirely. diff --git a/tests/e2e/261_pack_follows_the_module_extension.sh b/tests/e2e/261_pack_follows_the_module_extension.sh index efed13543..423defcf0 100755 --- a/tests/e2e/261_pack_follows_the_module_extension.sh +++ b/tests/e2e/261_pack_follows_the_module_extension.sh @@ -62,7 +62,7 @@ kind = "lib" EOF cd mathkit -"$MCPP" pack mathkit --format dir > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } +"$MCPP" pack mathkit --pkg-format dir > pack.log 2>&1 || { cat pack.log; echo "pack failed"; exit 1; } pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)" # ── the closure actually started ──────────────────────────────────────── @@ -139,7 +139,7 @@ sources = ["src/*.cppm"] [targets.plain] kind = "lib" EOF -( cd plain && "$MCPP" pack plain --format dir > pack.log 2>&1 ) || { +( cd plain && "$MCPP" pack plain --pkg-format dir > pack.log 2>&1 ) || { cat plain/pack.log; echo "plain pack failed"; exit 1; } plainpkg="$(find plain/target/dist -maxdepth 1 -type d -name 'plain-0.1.0-*' | head -1)" grep -q 'module_extensions' "$plainpkg/mcpp.toml" && { diff --git a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh index b3ea0ba94..0307579d1 100755 --- a/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh +++ b/tests/e2e/292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh @@ -96,4 +96,34 @@ case "$hosted" in exit 1 ;; esac +# ── Half three: a kernel-abi package over the system's C library ────────── +# +# On a hosted target a kernel-abi package (openkal-linux under mcpplibs.os) +# sits over the system's C library, which the row's payload still supplies: +# it supplies no system, and the row's convention stands. Measured on +# linux-aarch64: it cancelled the musl row's convention, and the machine's +# llvm compiled the musl target against glibc's headers. With a compiler of +# another family named and nothing in the payload's place, the decision is +# the refusal half one of e2e 298 states. +mkdir -p "$work/kabi/src" +cat > "$work/kabi/mcpp.toml" < "$work/kabi/src/main.cpp" +kabi="$(cd "$work/kabi" && "$MCPP" build --target x86_64-linux-musl 2>&1 || true)" +case "$kabi" in + *"takes its C library from"*) + echo " ok a kernel-abi package on a hosted target leaves the row's convention standing" ;; + *) + echo "FAIL: a kernel-abi package over the system's C library cancelled the row's convention" + printf '%s\n' "$kabi" | grep -iE 'Resolved|error' | head -4 | sed 's/^/ /' + exit 1 ;; +esac + echo "OK: naming a layer changes the system, not the compiler that emits the target" diff --git a/tests/e2e/330_runner_hosted_targets.sh b/tests/e2e/330_runner_hosted_targets.sh index f4eae9a69..3392503c8 100644 --- a/tests/e2e/330_runner_hosted_targets.sh +++ b/tests/e2e/330_runner_hosted_targets.sh @@ -169,7 +169,7 @@ fi printf '\n[target.%s]\nrunnerX = ["x"]\n' "$HOST" >> mcpp.toml out=$("$MCPP" build 2>&1) grep -q "unsupported key 'runnerX'" <<<"$out" || fail "array typo not reported: $out" -grep -q "Supported keys: cxx_runtime, linkage, min_api_level, run_under, runner, sysroot, toolchain" <<<"$out" \ +grep -q "Supported keys: cxx_runtime, linkage, min_api_level, platform, run_under, runner, sysroot, toolchain" <<<"$out" \ || fail "runner missing from the supported-keys list: $out" echo "PASS: 330_runner_hosted_targets" diff --git a/tests/e2e/638_pack_format_dispatch.sh b/tests/e2e/638_pack_format_dispatch.sh index 9dc346bbb..cf8d338dc 100755 --- a/tests/e2e/638_pack_format_dispatch.sh +++ b/tests/e2e/638_pack_format_dispatch.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # requires: gcc -# 638_pack_format_dispatch.sh — `mcpp pack --format ` dispatches to a +# 638_pack_format_dispatch.sh — `mcpp pack --pkg-format ` dispatches to a # package, and no distribution format lives in the engine. # # `tar` and `dir` answer the same question `msi` and `appimage` answer -- what @@ -125,15 +125,15 @@ G=$(find_graph) # ── 2. an unknown format is refused, BEFORE the build, naming what exists ── set +e -"$MCPP" pack --format bogus > b2.log 2>&1 +"$MCPP" pack --pkg-format bogus > b2.log 2>&1 rc=$? set -e [ "$rc" -ne 0 ] || { cat b2.log; echo "FAIL: --format bogus was accepted"; exit 1; } -grep -q "unknown --format 'bogus'" b2.log \ +grep -q "unknown --pkg-format 'bogus'" b2.log \ || { cat b2.log; echo "FAIL: the refusal does not name the value"; exit 1; } # THE LIST IS THE POINT. A refusal that named a fixed list would be the # coupling this mechanism removes; this one names the graph's own answer. -grep -q "available in this build: tar, dir, zap" b2.log \ +grep -q "available in this build: tar, zip, dir, single-file, zap" b2.log \ || { cat b2.log; echo "FAIL: the refusal does not name what is available"; exit 1; } # Nothing was compiled to find that out. grep -q "Compiling app" b2.log \ @@ -146,15 +146,15 @@ grep -q "Compiling app" b2.log \ # cannot be decided until prepare has run -- and prepare narrates what it # resolves. Deciding it later is what broke the contract once. set +e -so=$("$MCPP" pack --format bogus 2>/dev/null); rc=$? -se=$("$MCPP" pack --format bogus 2>&1 >/dev/null) +so=$("$MCPP" pack --pkg-format bogus 2>/dev/null); rc=$? +se=$("$MCPP" pack --pkg-format bogus 2>&1 >/dev/null) set -e [ -z "$so" ] || { echo "FAIL: the refusal wrote to stdout: $(echo "$so" | head -1)"; exit 1; } [ -n "$se" ] || { echo "FAIL: the refusal said nothing on stderr"; exit 1; } [ "$rc" = 2 ] || { echo "FAIL: the refusal exited $rc, expected 2"; exit 1; } # ── 3. the dispatched format produces a file, and it saw the staged tree ─── -"$MCPP" pack --format zap > b3.log 2>&1 || { cat b3.log; echo "FAIL: pack --format zap failed"; exit 1; } +"$MCPP" pack --pkg-format zap > b3.log 2>&1 || { cat b3.log; echo "FAIL: pack --pkg-format zap failed"; exit 1; } Z=$(find target -name 'app.zap' | head -1) [ -n "$Z" ] || { cat b3.log; echo "FAIL: --format zap produced nothing"; exit 1; } # The engine handed the build program the rest of `[package]`. @@ -242,7 +242,7 @@ int main() { } EOF set +e -"$MCPP" pack --format zap > b6.log 2>&1 +"$MCPP" pack --pkg-format zap > b6.log 2>&1 rc=$? set -e [ "$rc" -ne 0 ] || { cat b6.log; echo "FAIL: a source action reached the staged tree"; exit 1; } @@ -261,11 +261,11 @@ import mcpp; int main() { mcpp::provides_pack_format("zap"); return 0; } EOF set +e -"$MCPP" pack --format zap > b7.log 2>&1 +"$MCPP" pack --pkg-format zap > b7.log 2>&1 rc=$? set -e [ "$rc" -ne 0 ] || { cat b7.log; echo "FAIL: a format nothing claimed reported success"; exit 1; } -grep -q "no action claimed --format 'zap'" b7.log \ +grep -q "no action claimed --pkg-format 'zap'" b7.log \ || { cat b7.log; echo "FAIL: the refusal does not name the unclaimed format"; exit 1; } # ── 8. an artifact action that predates the request is not the package ───── @@ -312,7 +312,7 @@ int main() { return 0; } EOF -"$MCPP" pack --format zap > b8.log 2>&1 || { cat b8.log; echo "FAIL: a member that reads no staged tree was refused"; exit 1; } +"$MCPP" pack --pkg-format zap > b8.log 2>&1 || { cat b8.log; echo "FAIL: a member that reads no staged tree was refused"; exit 1; } grep -q "app.zap" b8.log \ || { cat b8.log; echo "FAIL: the gated action was not reported as the package"; exit 1; } grep -q "size.stamp" b8.log \ @@ -357,7 +357,7 @@ int main() { return 0; } EOF -"$MCPP" pack --format zap > b9.log 2>&1 || { cat b9.log; echo "FAIL: a provider that reads no staged tree was refused"; exit 1; } +"$MCPP" pack --pkg-format zap > b9.log 2>&1 || { cat b9.log; echo "FAIL: a provider that reads no staged tree was refused"; exit 1; } grep -q "app.zap" b9.log \ || { cat b9.log; echo "FAIL: the provider was not reported as the package"; exit 1; } # It really did not consume the tree: no stage manifest edge was added, so the diff --git a/tests/e2e/650_wasm_row_names_its_launcher.sh b/tests/e2e/650_wasm_row_names_its_launcher.sh index 4b22add5b..e8e0eb279 100755 --- a/tests/e2e/650_wasm_row_names_its_launcher.sh +++ b/tests/e2e/650_wasm_row_names_its_launcher.sh @@ -114,8 +114,8 @@ echo " ok: bin/app.js and bin/app.wasm exist, and no bare bin/app does" grep -q '^1-2-3$' run.log || fail "mcpp run did not print 1-2-3" run.log echo " ok: mcpp run prints 1-2-3" -"$MCPP" pack --target wasm32-emscripten --format dir > pack.log 2>&1 \ - || fail "mcpp pack --format dir failed" pack.log +"$MCPP" pack --target wasm32-emscripten --pkg-format dir > pack.log 2>&1 \ + || fail "mcpp pack --pkg-format dir failed" pack.log staged=$(find target/dist -maxdepth 1 -name 'app-0.1.0-wasm32-emscripten' -type d | head -1) [ -n "$staged" ] || fail "no staged directory under target/dist" pack.log [ -f "$staged/bin/app.js" ] || fail "the staged tree has no bin/app.js" pack.log @@ -124,7 +124,7 @@ manifest="target/dist/$(basename "$staged").stage-manifest" [ -f "$manifest" ] || fail "no .stage-manifest sibling of the staged tree" pack.log grep -q 'bin/app.js' "$manifest" || fail "the stage manifest does not list bin/app.js" "$manifest" grep -q 'bin/app.wasm' "$manifest" || fail "the stage manifest does not list bin/app.wasm" "$manifest" -echo " ok: mcpp pack --format dir stages both files and the manifest lists both" +echo " ok: mcpp pack --pkg-format dir stages both files and the manifest lists both" # `--no-entry` reaches the link line as an ordinary ldflag on this row -- no # engine mechanism checks for a `main` symbol (docs/21, docs/22). diff --git a/tests/e2e/651_a_build_program_deploys_what_it_generated.sh b/tests/e2e/651_a_build_program_deploys_what_it_generated.sh index 9fecef01d..368d72e1d 100755 --- a/tests/e2e/651_a_build_program_deploys_what_it_generated.sh +++ b/tests/e2e/651_a_build_program_deploys_what_it_generated.sh @@ -156,10 +156,10 @@ grep -qE "^ *build\.mcpp .* cached" b3.log \ grep -qx "changed resource" "$DEPLOYED" \ || fail "the restored file does not carry the action's latest output" "$DEPLOYED" -# ── 5. `mcpp pack --format dir` stages the deployed file ──────────────────── -"$MCPP" pack --format dir > b4.log 2>&1 || fail "pack --format dir failed" b4.log +# ── 5. `mcpp pack --pkg-format dir` stages the deployed file ──────────────────── +"$MCPP" pack --pkg-format dir > b4.log 2>&1 || fail "pack --pkg-format dir failed" b4.log STAGED=$(find target/dist -name res.bin | head -1) -[ -n "$STAGED" ] || fail "mcpp pack --format dir did not stage the deployed file" b4.log +[ -n "$STAGED" ] || fail "mcpp pack --pkg-format dir did not stage the deployed file" b4.log case "$STAGED" in */bin/app.resources/res.bin) : ;; *) fail "the staged file is not at bin/app.resources/res.bin" b4.log ;; diff --git a/tests/e2e/652_an_application_is_a_kind.sh b/tests/e2e/652_an_application_is_a_kind.sh index 0c7052fe1..ad1decb8f 100755 --- a/tests/e2e/652_an_application_is_a_kind.sh +++ b/tests/e2e/652_an_application_is_a_kind.sh @@ -97,7 +97,7 @@ grep -q "TOOL-RUN" <<<"$out" || fail "mcpp run tool did not print its marker" <( echo "mcpp run tool (bin) unaffected OK" # ── 5. `mcpp pack` treats `app` as the program route, same as `bin` ──────── -"$MCPP" pack --format dir > pack.log 2>&1 || fail "mcpp pack failed" pack.log +"$MCPP" pack --pkg-format dir > pack.log 2>&1 || fail "mcpp pack failed" pack.log staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) [ -n "$staged" ] || fail "no staged tree under target/dist" pack.log [ -x "${staged}bin/myapp" ] || fail "the staged tree has no bin/myapp" pack.log diff --git a/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh b/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh index 46c6b7f51..048b2eb45 100755 --- a/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh +++ b/tests/e2e/652b_an_application_on_android_is_a_shared_library.sh @@ -72,10 +72,10 @@ echo "bin keeps meaning binary on Android OK" # ── 3. `mcpp run` of the app, with no --format, is refused naming it ─────── out=$("$MCPP" run myapp --target "$TARGET" 2>&1) \ && fail "mcpp run of an app whose form is a library must be refused" <(echo "$out") -expected="error: 'myapp' is an application, and on $TARGET an application is a shared library that a package installs. Run it through a distributable: mcpp run --format , where is one of: none declared" +expected="error: 'myapp' is an application, and on $TARGET an application is a shared library that a package installs. Run it through a distributable: mcpp run --pkg-format , where is one of: none declared" grep -qF "$expected" <<<"$out" \ || fail "the refusal sentence does not match the design record's exact wording" <(echo "$out") -echo "mcpp run of the app without --format is refused, naming --format OK" +echo "mcpp run of the app without --pkg-format is refused, naming --pkg-format OK" # Negative-direction control for (3): `tool` is a real Binary on this same # row, so it is not caught by the app-only refusal above. @@ -144,8 +144,8 @@ int main() { } EOF -"$MCPP" pack --format blob --target "$TARGET" > pack.log 2>&1 \ - || fail "mcpp pack --format blob on Android failed" pack.log +"$MCPP" pack --pkg-format blob --target "$TARGET" > pack.log 2>&1 \ + || fail "mcpp pack --pkg-format blob on Android failed" pack.log staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) [ -n "$staged" ] || fail "no staged tree under target/dist for the Android pack" pack.log [ -f "${staged}lib/libmyapp.so" ] \ @@ -154,7 +154,7 @@ staged=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) || fail "the deploy'd file was not staged under bin/myres/ on the Android row" pack.log [ -n "$(find target -name 'myapp.blob' 2>/dev/null)" ] \ || fail "the reported artifact myapp.blob does not exist" pack.log -echo "mcpp pack --format blob on Android stages lib/libmyapp.so OK" +echo "mcpp pack --pkg-format blob on Android stages lib/libmyapp.so OK" # ── 5. MCPP_TARGET_MIN_PLATFORM_VERSION: numeric on Android, empty on host ─ # `mcpp::out_dir()` for a build PROGRAM is one directory per configuration and diff --git a/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh b/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh index 7975d1e53..ba0e4af13 100755 --- a/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh +++ b/tests/e2e/656_run_hands_the_distributable_to_the_runner.sh @@ -4,7 +4,7 @@ # ` (#622 A10, design record # 2026-09-12-622-a-ui-framework-on-android-ios-and-web.md §2.10). # -# `mcpp run --format ` IS `mcpp pack --format ` -- the two +# `mcpp run --pkg-format ` IS `mcpp pack --pkg-format ` -- the two # prepares, the build, the staging, the provider's action -- followed by the # ORDINARY run, with the artifact the pack pipeline reported as the operand. # Runner resolution is unchanged: this project's `[target.] runner`. @@ -124,7 +124,7 @@ HOST=$(ls target | head -1) # THE PLAIN GRAPH'S OWN PATH, captured now rather than re-derived at the # end. `--format` (a `pack_format` override) resolves to a DIFFERENT # fingerprint directory than a plain build -- see mcpp.build.plan's -# fingerprint inputs -- so once `run --format blob` has run, target/ holds +# fingerprint inputs -- so once `run --pkg-format blob` has run, target/ holds # both a plain and a "blob" build.ninja side by side; a plain build never # touches the "blob" one, and `ninja` files already at the byte content # being written are not re-touched, so their mtimes do not order the two. @@ -143,8 +143,8 @@ chmod +x "$TMP/runner.sh" printf '\n[target.%s]\nrunner = ["%s"]\n' "$HOST" "$TMP/runner.sh" >> mcpp.toml -# ── 1. `mcpp run --format blob` hands the runner the distributable ──────── -out=$("$MCPP" run --format blob 2>&1) || fail "mcpp run --format blob failed" <(echo "$out") +# ── 1. `mcpp run --pkg-format blob` hands the runner the distributable ──────── +out=$("$MCPP" run --pkg-format blob 2>&1) || fail "mcpp run --pkg-format blob failed" <(echo "$out") grep -q "RUNNER: .*/app\.final$" <<<"$out" \ || fail "the runner's operand was not the terminal artifact app.final" <(echo "$out") grep -q "RUNNER: .*/app\.blob$" <<<"$out" \ @@ -155,7 +155,7 @@ grep -q "Packed .*app\.blob" <<<"$out" \ && fail "the intermediate app.blob was reported as Packed" <(echo "$out") grep -q "1-2-3" <<<"$out" \ || fail "the program's marker did not print through the runner" <(echo "$out") -echo "mcpp run --format blob hands the runner the distributable OK" +echo "mcpp run --pkg-format blob hands the runner the distributable OK" # ── 2. a plain `mcpp run` is unaffected: the operand is the link output ─── out=$("$MCPP" run 2>&1) || fail "mcpp run failed" <(echo "$out") @@ -166,31 +166,31 @@ echo "mcpp run alone is unaffected OK" # ── 3. an unknown format is refused, naming what the graph provides ─────── set +e -out=$("$MCPP" run --format bogus 2>&1); rc=$? +out=$("$MCPP" run --pkg-format bogus 2>&1); rc=$? set -e -[ "$rc" -ne 0 ] || fail "mcpp run --format bogus was accepted" <(echo "$out") -grep -q "unknown --format 'bogus'" <<<"$out" \ +[ "$rc" -ne 0 ] || fail "mcpp run --pkg-format bogus was accepted" <(echo "$out") +grep -q "unknown --pkg-format 'bogus'" <<<"$out" \ || fail "the refusal does not name the value" <(echo "$out") grep -q "available in this build:.*blob" <<<"$out" \ || fail "the refusal does not name what is available" <(echo "$out") -echo "mcpp run --format bogus is refused, naming blob OK" +echo "mcpp run --pkg-format bogus is refused, naming blob OK" # ── 4. --format with --no-runner is refused before anything runs ────────── set +e -out=$("$MCPP" run --format blob --no-runner 2>&1); rc=$? +out=$("$MCPP" run --pkg-format blob --no-runner 2>&1); rc=$? set -e [ "$rc" -ne 0 ] || fail "--format with --no-runner was accepted" <(echo "$out") -grep -q -- "--format and --no-runner cannot be combined" <<<"$out" \ +grep -q -- "--pkg-format and --no-runner cannot be combined" <<<"$out" \ || fail "the refusal does not name the pairing" <(echo "$out") grep -q "1-2-3" <<<"$out" && fail "the program ran despite the refusal" <(echo "$out") echo "--format with --no-runner is refused OK" # ── 5. a plain build afterward has a plain graph, with no blob edge ─────── find target -name 'app.blob' -delete 2>/dev/null || true -"$MCPP" build > b5.log 2>&1 || fail "the build after run --format failed" b5.log +"$MCPP" build > b5.log 2>&1 || fail "the build after run --pkg-format failed" b5.log [ -f "$G0" ] || fail "the plain build's own build.ninja disappeared" b5.log [ "$(graph_dist "$G0")" = "none" ] \ - || { sed -n '2p' "$G0"; fail "the graph after run --format is not dist=none" b5.log; } + || { sed -n '2p' "$G0"; fail "the graph after run --pkg-format is not dist=none" b5.log; } grep -q "blob" "$G0" \ && fail "a plain build's graph still carries the blob edge" b5.log [ -z "$(find target -name 'app.blob' 2>/dev/null)" ] \ diff --git a/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh b/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh index 2ad8c7de1..baad1bcfb 100755 --- a/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh +++ b/tests/e2e/662_pack_stages_declared_files_before_the_closure.sh @@ -67,7 +67,7 @@ linux = "gcc@16.1.0" deploy_files = ["share/notes.txt"] EOF -"$MCPP" pack --format dir > dir.log 2>&1 || fail "mcpp pack --format dir failed" dir.log +"$MCPP" pack --pkg-format dir > dir.log 2>&1 || fail "mcpp pack --pkg-format dir failed" dir.log tree=$(find target/dist -maxdepth 1 -type d -name 'app-0.1.0-*' | head -1) [ -n "$tree" ] || fail "no staged directory under target/dist" dir.log @@ -147,7 +147,7 @@ int main() { } EOF -"$MCPP" pack --format zap > zap.log 2>&1 || fail "mcpp pack --format zap failed" zap.log +"$MCPP" pack --pkg-format zap > zap.log 2>&1 || fail "mcpp pack --pkg-format zap failed" zap.log Z=$(find target -name 'app.zap' | head -1) [ -n "$Z" ] || fail "--format zap produced nothing" zap.log diff --git a/tests/e2e/664_a_universal_apk_is_two_legs_in_one_tree.sh b/tests/e2e/664_a_universal_apk_is_two_legs_in_one_tree.sh index af5f92cf6..4625f9dd0 100755 --- a/tests/e2e/664_a_universal_apk_is_two_legs_in_one_tree.sh +++ b/tests/e2e/664_a_universal_apk_is_two_legs_in_one_tree.sh @@ -46,7 +46,7 @@ printf "resource-1\n" > res.txt # empty. Also asserted by 652b for the tarball route; repeated here, against # `--format dir`, because THIS is the format the several-triple case below # also uses, and the two must be compared under the same format. -"$MCPP" pack myapp --target aarch64-linux-android --format dir > pack1.log 2>&1 \ +"$MCPP" pack myapp --target aarch64-linux-android --pkg-format dir > pack1.log 2>&1 \ || fail "single-target Android pack failed" pack1.log staged1=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) [ -n "$staged1" ] || fail "no staged tree for the single-target pack" pack1.log @@ -61,7 +61,7 @@ echo "one --target stages flat lib/libmyapp.so, unchanged OK" # ── 2. two Android triples: one tree, two lib// legs ───────────────── rm -rf target/dist "$MCPP" pack myapp --target aarch64-linux-android --target x86_64-linux-android \ - --format dir > pack2.log 2>&1 \ + --pkg-format dir > pack2.log 2>&1 \ || fail "two-target Android pack failed" pack2.log staged2=$(ls -d target/dist/myapp-0.1.0-*/ 2>/dev/null | head -1) [ -n "$staged2" ] || fail "no staged tree for the two-target pack" pack2.log diff --git a/tests/e2e/666_a_macho_program_reaches_a_dispatched_format_with_its_tree.sh b/tests/e2e/666_a_macho_program_reaches_a_dispatched_format_with_its_tree.sh index 9b81c0742..9786c86aa 100755 --- a/tests/e2e/666_a_macho_program_reaches_a_dispatched_format_with_its_tree.sh +++ b/tests/e2e/666_a_macho_program_reaches_a_dispatched_format_with_its_tree.sh @@ -63,7 +63,7 @@ int main() { EOF # The built-in archive stages the program: its closure is only the OS's. -"$MCPP" pack --format dir > dir.log 2>&1 || fail "--format dir of a Mach-O program failed" dir.log +"$MCPP" pack --pkg-format dir > dir.log 2>&1 || fail "--pkg-format dir of a Mach-O program failed" dir.log D=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'zapapp-0.1.0-*' | head -1) [ -n "$D" ] || fail "--format dir staged no tree" dir.log [ -x "$D/bin/zapapp" ] || fail "the staged tree has no bin/zapapp" dir.log @@ -71,7 +71,7 @@ D=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'zapapp-0.1.0-*' | he || fail "the dir manifest does not say closure = walked" "$D.stage-manifest" echo " --format dir stages a Mach-O program with closure = walked" -"$MCPP" pack --format zap > zap.log 2>&1 || fail "mcpp pack --format zap failed" zap.log +"$MCPP" pack --pkg-format zap > zap.log 2>&1 || fail "mcpp pack --pkg-format zap failed" zap.log if grep -q 'staged without its dependency closure' zap.log; then fail "pack still reports the tree as staged without its closure" zap.log fi diff --git a/tests/e2e/667_an_android_pack_stages_its_closure.sh b/tests/e2e/667_an_android_pack_stages_its_closure.sh index 0f66d6c81..ad51959cc 100755 --- a/tests/e2e/667_an_android_pack_stages_its_closure.sh +++ b/tests/e2e/667_an_android_pack_stages_its_closure.sh @@ -48,8 +48,8 @@ printf 'import fw;\nint main() { return fw_anchor() == 41 ? 0 : 1; }\n' > app/sr cd app # ── 1. one triple: the closure is staged under lib/ ─────────────────────── -"$MCPP" pack --target x86_64-linux-android --format dir > dir.log 2>&1 \ - || fail "mcpp pack --format dir failed" dir.log +"$MCPP" pack --target x86_64-linux-android --pkg-format dir > dir.log 2>&1 \ + || fail "mcpp pack --pkg-format dir failed" dir.log tree=target/dist/app-0.1.0-x86_64-linux-android [ -d "$tree" ] || fail "no staged tree at $tree" dir.log got=$(cd "$tree/lib" && find . -type f | sort | tr '\n' ' ') @@ -90,7 +90,7 @@ libraries = ["ext"] EOF printf 'import fw;\nextern "C" int ext_value();\nint main() { return fw_anchor() == 41 && ext_value() == 1 ? 0 : 1; }\n' > src/main.cpp -"$MCPP" pack --target x86_64-linux-android --format dir > ext.log 2>&1 \ +"$MCPP" pack --target x86_64-linux-android --pkg-format dir > ext.log 2>&1 \ || fail "the pack with a prebuilt library failed" ext.log [ -f "$tree/lib/libext.so" ] || fail "the prebuilt libext.so is not staged" ext.log grep -qxF "needs${TAB}libext.so${TAB}lib/libext.so" "$manifest" \ @@ -100,7 +100,7 @@ echo " ok: a library from [runtime] link_library_dirs is staged" # The build is up to date, so the pack does not relink; the file is simply gone. rm ext/lib/libext.so for fmt in dir tar; do - if "$MCPP" pack --target x86_64-linux-android --format "$fmt" > "gone-$fmt.log" 2>&1; then + if "$MCPP" pack --target x86_64-linux-android --pkg-format "$fmt" > "gone-$fmt.log" 2>&1; then fail "--format $fmt succeeded with libext.so gone" "gone-$fmt.log" fi grep -q "libext.so: found in none of" "gone-$fmt.log" \ @@ -140,7 +140,7 @@ int main() { return 0; } EOF -"$MCPP" pack --target x86_64-linux-android --format copy > copy.log 2>&1 \ +"$MCPP" pack --target x86_64-linux-android --pkg-format copy > copy.log 2>&1 \ || fail "the dispatched format failed" copy.log seen=$(find target -name stage.txt | head -1) [ -f "$seen" ] || fail "the provider wrote nothing" copy.log @@ -166,7 +166,7 @@ demo.fw = { path = "../fw", linkage = "shared" } EOF printf 'import fw;\nint main() { return fw_anchor() == 41 ? 0 : 1; }\n' > src/main.cpp rm -rf target/dist -"$MCPP" pack --target x86_64-linux-android --target aarch64-linux-android --format dir \ +"$MCPP" pack --target x86_64-linux-android --target aarch64-linux-android --pkg-format dir \ > two.log 2>&1 || fail "the two-triple pack failed" two.log tree2=$(find target/dist -mindepth 1 -maxdepth 1 -type d | head -1) [ -n "$tree2" ] || fail "no staged tree for two triples" two.log diff --git a/tests/e2e/668_a_macho_program_is_staged_with_its_closure.sh b/tests/e2e/668_a_macho_program_is_staged_with_its_closure.sh index 3fb15c4b6..59a27708a 100755 --- a/tests/e2e/668_a_macho_program_is_staged_with_its_closure.sh +++ b/tests/e2e/668_a_macho_program_is_staged_with_its_closure.sh @@ -11,7 +11,7 @@ # removed -- a tree that ran only while the build directory existed would pass # every check but that one. Measured on macos-15 before the change (the # triage record's reading C5): exit 7 staged by hand, exit 134 without the -# dylib, and `mcpp pack --format dir` refused the program. +# dylib, and `mcpp pack --pkg-format dir` refused the program. set -e TMP=$(mktemp -d) @@ -51,7 +51,7 @@ EOF cd app # ── 1. --format dir stages the program and its dylib ────────────────────── -"$MCPP" pack --format dir > dir.log 2>&1 || fail "mcpp pack --format dir failed" dir.log +"$MCPP" pack --pkg-format dir > dir.log 2>&1 || fail "mcpp pack --pkg-format dir failed" dir.log tree=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'app-0.1.0-*' | head -1) [ -n "$tree" ] || fail "no staged tree under target/dist" dir.log [ -x "$tree/bin/app" ] || fail "the staged tree has no bin/app" dir.log diff --git a/tests/e2e/669_an_elf_host_pack_states_its_closure.sh b/tests/e2e/669_an_elf_host_pack_states_its_closure.sh index 7302d8f97..78c4e7b3c 100755 --- a/tests/e2e/669_an_elf_host_pack_states_its_closure.sh +++ b/tests/e2e/669_an_elf_host_pack_states_its_closure.sh @@ -77,7 +77,7 @@ named=$(find "$bin" -maxdepth 1 -name 'libnamed.so' -type f | head -1) echo " ok: libfw.so records its file name; a declared soname keeps libnamed.so.1" # ── 2. A3: the manifest states the closure the tree carries ─────────────── -"$MCPP" pack --format dir > dir.log 2>&1 || fail "mcpp pack --format dir failed" dir.log +"$MCPP" pack --pkg-format dir > dir.log 2>&1 || fail "mcpp pack --pkg-format dir failed" dir.log tree=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'app-0.1.0-*' | head -1) [ -n "$tree" ] || fail "no staged tree" dir.log manifest="$tree.stage-manifest" @@ -111,12 +111,12 @@ ldflags = ["-L$TMP/ext/lib", "-lext", "-Wl,-rpath,$TMP/ext/lib"] EOF printf 'int ext_value();\nint main() { return ext_value() == 1 ? 0 : 1; }\n' > extapp/src/main.cpp cd extapp -"$MCPP" pack --format dir > ext.log 2>&1 || fail "the pack with a prebuilt library failed" ext.log +"$MCPP" pack --pkg-format dir > ext.log 2>&1 || fail "the pack with a prebuilt library failed" ext.log etree=$(find target/dist -mindepth 1 -maxdepth 1 -type d -name 'extapp-0.1.0-*' | head -1) grep -qxF "needs${TAB}libext.so${TAB}lib/libext.so" "$etree.stage-manifest" \ || fail "libext.so is not stated as bundled" "$etree.stage-manifest" rm "$TMP/ext/lib/libext.so" -if "$MCPP" pack --format dir > gone.log 2>&1; then +if "$MCPP" pack --pkg-format dir > gone.log 2>&1; then fail "--format dir succeeded with libext.so gone" gone.log fi grep -q "libext.so: the loader finds no file for it" gone.log \ diff --git a/tests/e2e/671_toolchain_is_an_option_of_run_test_and_pack.sh b/tests/e2e/671_toolchain_is_an_option_of_run_test_and_pack.sh index d0d2771d8..7c69d249e 100644 --- a/tests/e2e/671_toolchain_is_an_option_of_run_test_and_pack.sh +++ b/tests/e2e/671_toolchain_is_an_option_of_run_test_and_pack.sh @@ -34,7 +34,7 @@ fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2 cd tcopt # ── 1. the option is accepted and its value reaches resolution ───────────── -for spelling in "run" "test" "pack --format dir"; do +for spelling in "run" "test" "pack --pkg-format dir"; do read -ra words <<<"$spelling" set +e "$MCPP" "${words[@]}" --toolchain nosuch@0.0.1 > c1.log 2>&1 diff --git a/tests/e2e/672_a_runner_receives_the_files_the_artifact_carries.sh b/tests/e2e/672_a_runner_receives_the_files_the_artifact_carries.sh index 24374053e..82e4601dd 100644 --- a/tests/e2e/672_a_runner_receives_the_files_the_artifact_carries.sh +++ b/tests/e2e/672_a_runner_receives_the_files_the_artifact_carries.sh @@ -208,11 +208,11 @@ int main() { return 0; } EOF -"$MCPP" run --format blob > r5.log 2>&1 || fail "mcpp run --format blob failed" r5.log +"$MCPP" run --pkg-format blob > r5.log 2>&1 || fail "mcpp run --pkg-format blob failed" r5.log rec="$TMP/rec/lone.blob.rec" [ -f "$rec" ] && ! grep -q '^UNSET$' "$rec" || fail "the distributable's runner did not receive the variable" r5.log "$rec" list=$(sed -n 's/^LIST=//p' "$rec") [ -f "$list" ] && [ ! -s "$list" ] || fail "the distributable's list is not an existing empty file" "$rec" -echo "a distributable run with --format receives an empty list OK" +echo "a distributable run with --pkg-format receives an empty list OK" echo "PASS: 672_a_runner_receives_the_files_the_artifact_carries" diff --git a/tests/e2e/674_a_format_is_run_by_the_runner_named_after_it.sh b/tests/e2e/674_a_format_is_run_by_the_runner_named_after_it.sh index a5ecc9196..ec7a27f6c 100644 --- a/tests/e2e/674_a_format_is_run_by_the_runner_named_after_it.sh +++ b/tests/e2e/674_a_format_is_run_by_the_runner_named_after_it.sh @@ -6,8 +6,8 @@ # spawn (#634 B3). # # Measured on macos-15 against 2026.9.14.1: with a named runner `app` declared, -# `mcpp run --format app --runner app` ran the bundle and returned its status, -# while `mcpp run --format app` alone handed the bundle directory to the kernel: +# `mcpp run --pkg-format app --runner app` ran the bundle and returned its status, +# while `mcpp run --pkg-format app` alone handed the bundle directory to the kernel: # # ...RApp.app could not be started: Permission denied (error 13) exit 126 # @@ -112,7 +112,7 @@ cp mcpp.toml mcpp.toml.base # ── 1. the runner named after the format ────────────────────────────────── set +e -"$MCPP" run --format blob > r1.log 2>&1; rc=$? +"$MCPP" run --pkg-format blob > r1.log 2>&1; rc=$? set -e grep -q "RUNNER-blob: .*/app\.blob$" r1.log \ || fail "the runner named 'blob' did not receive the distributable" r1.log @@ -120,11 +120,11 @@ grep -q "PROGRAM-RAN" r1.log || fail "the program did not run through the runner [ "$rc" -eq 7 ] || fail "the program's status 7 did not come back (got $rc)" r1.log grep -q "Running \`.*runner-blob.sh" r1.log \ || fail "the status line does not say Running for the format's runner" r1.log -echo "mcpp run --format blob runs through the runner named blob OK" +echo "mcpp run --pkg-format blob runs through the runner named blob OK" # ── 2. a typed --runner wins ─────────────────────────────────────────────── set +e -"$MCPP" run --format blob --runner other > r2.log 2>&1; rc=$? +"$MCPP" run --pkg-format blob --runner other > r2.log 2>&1; rc=$? set -e grep -q "RUNNER-other: .*/app\.blob$" r2.log || fail "--runner other did not win" r2.log grep -q "RUNNER-blob" r2.log && fail "the format's runner ran although --runner was typed" r2.log @@ -142,7 +142,7 @@ echo "a plain mcpp run executes the link output directly OK" # ── 4. a directory no runner reaches ─────────────────────────────────────── set +e -"$MCPP" run --format bundle > r4.log 2>&1; rc=$? +"$MCPP" run --pkg-format bundle > r4.log 2>&1; rc=$? set -e [ "$rc" -eq 126 ] || fail "the directory without a runner did not exit 126 (got $rc)" r4.log grep -q "produced a directory" r4.log || fail "the refusal does not say the distributable is a directory" r4.log @@ -155,7 +155,7 @@ echo "a directory no runner reaches is refused before the spawn OK" # ── 5. a manifest runner named after the format reaches the directory ───── printf 'bundle = ["%s"]\n' "$TMP/runner-bundle.sh" >> mcpp.toml set +e -"$MCPP" run --format bundle > r5.log 2>&1; rc=$? +"$MCPP" run --pkg-format bundle > r5.log 2>&1; rc=$? set -e grep -q "RUNNER-bundle: .*/app\.bundle$" r5.log || fail "the runner named 'bundle' did not receive the directory" r5.log [ "$rc" -eq 7 ] || fail "the bundle's program status did not come back (got $rc)" r5.log @@ -165,7 +165,7 @@ echo "a manifest runner named bundle runs the directory OK" cp mcpp.toml.base mcpp.toml printf '\n[target.%s]\nrunner = ["%s"]\n' "$HOST" "$TMP/runner-default.sh" >> mcpp.toml set +e -"$MCPP" run --format bundle > r6.log 2>&1; rc=$? +"$MCPP" run --pkg-format bundle > r6.log 2>&1; rc=$? set -e grep -q "RUNNER-default: .*/app\.bundle$" r6.log || fail "the default runner did not receive the directory" r6.log grep -q "no runner reaches it" r6.log && fail "a refusal was printed although a default runner exists" r6.log diff --git a/tests/e2e/689_pack_takes_features.sh b/tests/e2e/689_pack_takes_features.sh index 0552358bc..0bc4f0f0b 100644 --- a/tests/e2e/689_pack_takes_features.sh +++ b/tests/e2e/689_pack_takes_features.sh @@ -100,7 +100,7 @@ packed_answer() { # $1 = log; runs the program the last pack staged tool_entries() { find "$MCPP_HOME/build-cache" -path '*/tool/*' -name 'gen*' -type f 2>/dev/null | wc -l; } # ── The pack without the feature: no tool, the fallback source ───────────── -"$MCPP" pack --format dir > p0.log 2>&1 || fail "pack without the feature failed" p0.log +"$MCPP" pack --pkg-format dir > p0.log 2>&1 || fail "pack without the feature failed" p0.log [ "$(packed_answer p0.log)" = "ANSWER=0" ] || fail "pack without the feature: expected 0" p0.log [ "$(tool_entries)" -eq 0 ] || fail "a pack without the feature built the host tool" p0.log grep -q "host tool" p0.log && fail "a pack without the feature mentioned the host tool" p0.log @@ -108,7 +108,7 @@ echo "ok: a pack without --features builds no host tool" # ── The pack with the feature: the tool is built and its output shipped ──── rm -rf target -"$MCPP" pack --format dir --features installer > p1.log 2>&1 \ +"$MCPP" pack --pkg-format dir --features installer > p1.log 2>&1 \ || fail "mcpp pack --features installer failed" p1.log grep -q "unknown option" p1.log && fail "pack did not accept --features" p1.log grep -q "Building.*host tool" p1.log || fail "the pack with the feature did not build the tool" p1.log diff --git a/tests/e2e/722_pack_strips_what_the_graph_built.sh b/tests/e2e/722_pack_strips_what_the_graph_built.sh index 97b14633f..77dbe5919 100755 --- a/tests/e2e/722_pack_strips_what_the_graph_built.sh +++ b/tests/e2e/722_pack_strips_what_the_graph_built.sh @@ -102,7 +102,7 @@ printf 'int dep_answer();\nint main() { return dep_answer() == 42 ? 0 : 1; }\n' cd hostapp # ── A ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --format tar > a.log 2>&1 || fail "A: pack failed" a.log +"$MCPP" pack --pkg-format tar > a.log 2>&1 || fail "A: pack failed" a.log grep -q "Packing hostapp v0.1.0 (vendored, stripped)" a.log || fail "A: the Packing line" a.log rm -rf xa && mkdir xa && tar -xzf target/dist/hostapp-0.1.0-x86_64-linux-gnu.tar.gz -C xa lib=$(ls xa/*/lib/libdep.so); prog=$(ls xa/*/bin/hostapp) @@ -116,7 +116,7 @@ case "$fp" in "symtab=0 debug=0 "*) ;; *) fail "A: bin/hostapp is not stripped: echo "ok: A, the program and the graph-built library are stripped and run" # ── B ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --format tar --no-strip > b.log 2>&1 || fail "B: pack failed" b.log +"$MCPP" pack --pkg-format tar --no-strip > b.log 2>&1 || fail "B: pack failed" b.log grep -q "Packing hostapp v0.1.0 (vendored)" b.log || fail "B: the Packing line" b.log rm -rf xb && mkdir xb && tar -xzf target/dist/hostapp-0.1.0-x86_64-linux-gnu.tar.gz -C xb fb=$(elf_facts "$(ls xb/*/lib/libdep.so)") @@ -125,7 +125,7 @@ case "$fb" in "symtab=1 "*) ;; *) fail "B: --no-strip did not reach lib/libdep.s echo "ok: B, --no-strip reaches the graph-built library" # ── C ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --format tar --debug-symbols "$TMP/dbg" > c.log 2>&1 || fail "C: pack failed" c.log +"$MCPP" pack --pkg-format tar --debug-symbols "$TMP/dbg" > c.log 2>&1 || fail "C: pack failed" c.log [ -s "$TMP/dbg/libdep.so.debug" ] || fail "C: no libdep.so.debug" c.log rm -rf xc && mkdir xc && tar -xzf target/dist/hostapp-0.1.0-x86_64-linux-gnu.tar.gz -C xc fc=$(elf_facts "$(ls xc/*/lib/libdep.so)") @@ -158,10 +158,10 @@ int main() { } EOF -"$MCPP" pack --format probe --debug-symbols "$TMP/dbg2" > d.log 2>&1 || fail "D: pack failed" d.log +"$MCPP" pack --pkg-format probe --debug-symbols "$TMP/dbg2" > d.log 2>&1 || fail "D: pack failed" d.log probe=$(find target -name probe.txt | head -1) [ "$(cat "$probe")" = "strip=1 debug=$TMP/dbg2" ] || fail "D: the program read '$(cat "$probe" 2>/dev/null)'" d.log -"$MCPP" pack --format probe --no-strip > d2.log 2>&1 || fail "D: pack --no-strip failed" d2.log +"$MCPP" pack --pkg-format probe --no-strip > d2.log 2>&1 || fail "D: pack --no-strip failed" d2.log [ "$(cat "$probe")" = "strip=0 debug=" ] || fail "D: under --no-strip the program read '$(cat "$probe")'" d2.log echo "ok: D, a dispatched format's program reads the strip decision" @@ -194,7 +194,7 @@ int dep_answer(); int main() { return app_entry() == 43 ? 0 : 1; } EOF cd app - "$MCPP" pack --target x86_64-linux-android --format tar > e.log 2>&1 || fail "E: pack failed" e.log + "$MCPP" pack --target x86_64-linux-android --pkg-format tar > e.log 2>&1 || fail "E: pack failed" e.log grep -q "Packing app v0.1.0 (vendored, stripped)" e.log || fail "E: the Packing line" e.log rm -rf xe && mkdir xe && tar -xzf target/dist/app-0.1.0-x86_64-linux-android.tar.gz -C xe for f in libapp.so libdep.so libc++_shared.so; do diff --git a/tests/e2e/723_pack_reports_what_it_produced.sh b/tests/e2e/723_pack_reports_what_it_produced.sh index dd3bba3ac..61e12a1ba 100755 --- a/tests/e2e/723_pack_reports_what_it_produced.sh +++ b/tests/e2e/723_pack_reports_what_it_produced.sh @@ -11,11 +11,11 @@ # `mcpp build` given the same line built `dev`. # # Legs: -# A. `pack --format tar --message-format json`: stdout is one `mcpp.pack` +# A. `pack --pkg-format tar --message-format json`: stdout is one `mcpp.pack` # envelope and nothing else; the artifact path is absolute and exists, its # type is `file`, its format `tar`, its target the host triple; the staged # tree and its manifest exist, and the closure is `walked`. -# B. `pack --format dir --message-format json`: the artifact is a directory. +# B. `pack --pkg-format dir --message-format json`: the artifact is a directory. # C. `pack --release` and `pack --dev` are accepted; `--dev` builds `dev`. # D. `run --profile dev --release` builds `dev`, as `build` does. # E. `pack --message-format yaml` is refused with exit 2. @@ -42,7 +42,7 @@ printf '#include \nint main() { std::puts("app ran"); return 0; }\n' > a cd app # ── A ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --format tar --message-format json > a.json 2> a.err || fail "A: pack failed" a.err a.json +"$MCPP" pack --pkg-format tar --message-format json > a.json 2> a.err || fail "A: pack failed" a.err a.json python3 - a.json <<'EOF' || fail "A: the envelope" a.json a.err import json, os, sys text = open(sys.argv[1]).read() @@ -64,7 +64,7 @@ grep -q "Packed" a.err || fail "A: the human lines did not go to stderr" a.err echo "ok: A, one envelope on stdout names the archive, its legs and the staged tree" # ── B ─────────────────────────────────────────────────────────────────────── -"$MCPP" pack --format dir --message-format json > b.json 2> b.err || fail "B: pack failed" b.err +"$MCPP" pack --pkg-format dir --message-format json > b.json 2> b.err || fail "B: pack failed" b.err python3 - b.json <<'EOF' || fail "B: the envelope" b.json import json, os, sys a = json.load(open(sys.argv[1]))["data"]["artifacts"][0] @@ -78,13 +78,13 @@ echo "ok: B, a directory artifact is reported as one" # from an empty `target/`, so the one graph there is the one this pack wrote. built_dev() { grep -q -- " -O0" "$(ls target/*-linux-gnu/*/build.ninja | head -1)"; } rm -rf target -"$MCPP" pack --release --format tar > c1.log 2>&1 || fail "C: pack --release was refused" c1.log +"$MCPP" pack --release --pkg-format tar > c1.log 2>&1 || fail "C: pack --release was refused" c1.log built_dev && fail "C: pack --release built dev" c1.log rm -rf target -"$MCPP" pack --dev --format tar > c2.log 2>&1 || fail "C: pack --dev was refused" c2.log +"$MCPP" pack --dev --pkg-format tar > c2.log 2>&1 || fail "C: pack --dev was refused" c2.log built_dev || fail "C: pack --dev did not build dev" c2.log rm -rf target -"$MCPP" pack --profile release --dev --format tar > c3.log 2>&1 || fail "C: pack --profile --dev failed" c3.log +"$MCPP" pack --profile release --dev --pkg-format tar > c3.log 2>&1 || fail "C: pack --profile --dev failed" c3.log built_dev && fail "C: --dev won over --profile release on pack" c3.log echo "ok: C, pack takes --release and --dev, and --profile wins" @@ -97,7 +97,7 @@ echo "ok: D, run and build resolve --profile over the shorthands alike" # ── E ─────────────────────────────────────────────────────────────────────── set +e -"$MCPP" pack --format tar --message-format yaml > e.out 2> e.err +"$MCPP" pack --pkg-format tar --message-format yaml > e.out 2> e.err rc=$? set -e [ "$rc" -eq 2 ] || fail "E: --message-format yaml exited $rc" e.err diff --git a/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh b/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh index 246304739..957baf696 100755 --- a/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh +++ b/tests/e2e/779_a_build_program_declares_a_runtime_search_dir.sh @@ -22,7 +22,7 @@ # on a build.mcpp CACHE HIT (not a re-run) must keep the RUNPATH and keep # `mcpp run` working -- the same replay criterion `mcpp:deploy=` is held # to in 651_a_build_program_deploys_what_it_generated.sh. -# 4. `mcpp pack --format dir` STAGES IT into the bundle's `lib/` -- the +# 4. `mcpp pack --pkg-format dir` STAGES IT into the bundle's `lib/` -- the # same closure search `runtime_search_dirs` already feeds. Checked # LAST: `mcpp pack` builds under a different profile and would otherwise # invalidate the `dev`-profile build.mcpp cache the replay check (3) @@ -129,8 +129,8 @@ grep -F -- "-Wl,-rpath,$RTDIR" "$G2" >/dev/null \ "$MCPP" run > run2.log 2>&1 \ || fail "run failed after the directive was replayed from a build.mcpp cache hit" run2.log b2.log -# ── 4. mcpp pack --format dir stages it into the bundle's lib/ ───────────── -"$MCPP" pack --format dir > pack1.log 2>&1 || fail "pack --format dir failed" pack1.log +# ── 4. mcpp pack --pkg-format dir stages it into the bundle's lib/ ───────────── +"$MCPP" pack --pkg-format dir > pack1.log 2>&1 || fail "pack --pkg-format dir failed" pack1.log STAGED=$(find target/dist -name 'libruntime_plugin.so' | head -1) [ -n "$STAGED" ] || fail "the runtime-search-dir closure was not staged by mcpp pack" pack1.log case "$STAGED" in diff --git a/tests/e2e/794_a_windows_program_finds_a_dll_through_runtime_search_dir.sh b/tests/e2e/794_a_windows_program_finds_a_dll_through_runtime_search_dir.sh index c11588952..8262b5d2a 100644 --- a/tests/e2e/794_a_windows_program_finds_a_dll_through_runtime_search_dir.sh +++ b/tests/e2e/794_a_windows_program_finds_a_dll_through_runtime_search_dir.sh @@ -99,7 +99,7 @@ rc=0; out="$("$MCPP" run 2>&1)" || rc=$? [[ $rc -eq 0 ]] || { echo "FAIL: mcpp run failed (rc=$rc): $out"; exit 1; } # ── 2. `mcpp pack` places the DLL beside the packed executable ────────────── -"$MCPP" pack --format dir > pack.log 2>&1 || { cat pack.log; echo "FAIL: pack failed"; exit 1; } +"$MCPP" pack --pkg-format dir > pack.log 2>&1 || { cat pack.log; echo "FAIL: pack failed"; exit 1; } PACKED_EXE="$(find target/dist -name 'app.exe' | head -1)" [[ -n "$PACKED_EXE" ]] || { find target/dist -type f; echo "FAIL: no packed app.exe"; exit 1; } [[ -f "$(dirname "$PACKED_EXE")/libmathkit.dll" ]] || { diff --git a/tests/e2e/796_runtime_search_dir_on_pe_under_wine.sh b/tests/e2e/796_runtime_search_dir_on_pe_under_wine.sh index 2399d9782..e0308d887 100644 --- a/tests/e2e/796_runtime_search_dir_on_pe_under_wine.sh +++ b/tests/e2e/796_runtime_search_dir_on_pe_under_wine.sh @@ -121,7 +121,7 @@ rc=0; out="$("$MCPP" run --target "$TRIPLE" 2>&1)" || rc=$? [[ $rc -eq 0 ]] || { echo "FAIL: mcpp run failed (rc=$rc): $out"; exit 1; } # ── 2. `mcpp pack` places the DLL beside the packed executable ───────────── -"$MCPP" pack --target "$TRIPLE" --format dir > pack.log 2>&1 || { cat pack.log; echo "FAIL: pack failed"; exit 1; } +"$MCPP" pack --target "$TRIPLE" --pkg-format dir > pack.log 2>&1 || { cat pack.log; echo "FAIL: pack failed"; exit 1; } PACKED_EXE="$(find target/dist -name 'app.exe' | head -1)" [[ -n "$PACKED_EXE" ]] || { find target/dist; echo "FAIL: no packed app.exe"; exit 1; } [[ -f "$(dirname "$PACKED_EXE")/libmathkit.dll" ]] || { diff --git a/tests/e2e/814_the_llvm_row_defaults_to_the_dynamic_crt.sh b/tests/e2e/814_the_llvm_row_defaults_to_the_dynamic_crt.sh index 15ec7883c..1bc9ebb71 100755 --- a/tests/e2e/814_the_llvm_row_defaults_to_the_dynamic_crt.sh +++ b/tests/e2e/814_the_llvm_row_defaults_to_the_dynamic_crt.sh @@ -144,7 +144,7 @@ echo "ok: switching A, B, A keeps both std BMI identities valid" # and not this fixture's row: it never wrote `cxx_runtime` down). # # `resolution.json` is a property of the BUILD, not of one `pack` invocation, -# so the observable here is the package's own contents: `--format dir` avoids +# so the observable here is the package's own contents: `--pkg-format dir` avoids # needing an unzip step to look inside. MARKER="$TMP/marker"; touch "$MARKER" pack_out=$("$MCPP" pack 2>&1) || fail "the default pack failed" <(echo "$pack_out") @@ -155,7 +155,7 @@ DIST=$(find target/dist -maxdepth 1 -mindepth 1 -newer "$MARKER" | head -1) || fail "the default pack did not carry vcruntime140.dll" <(echo "$pack_out"; echo "$DIST") touch "$MARKER" -system_out=$("$MCPP" pack --mode system --format dir 2>&1) \ +system_out=$("$MCPP" pack --mode system --pkg-format dir 2>&1) \ || fail "--mode system refused a defaulted (never-declared) toolchain-coupled contract" \ <(echo "$system_out") SYSTEM_DIST=$(find target/dist -maxdepth 1 -mindepth 1 -newer "$MARKER" | head -1) diff --git a/tests/e2e/820_a_windows_program_takes_its_runtime_from_one_resolver.sh b/tests/e2e/820_a_windows_program_takes_its_runtime_from_one_resolver.sh index bde5b4852..581fb1c61 100755 --- a/tests/e2e/820_a_windows_program_takes_its_runtime_from_one_resolver.sh +++ b/tests/e2e/820_a_windows_program_takes_its_runtime_from_one_resolver.sh @@ -175,7 +175,7 @@ echo "ok: W4 a runtime file declared under host-coupled is refused" # ── W5 ────────────────────────────────────────────────────────────────────── touch marker -pack_out=$(cd w1 && "$MCPP" pack --format dir 2>&1) || fail "W5: mcpp pack failed" <(echo "$pack_out") +pack_out=$(cd w1 && "$MCPP" pack --pkg-format dir 2>&1) || fail "W5: mcpp pack failed" <(echo "$pack_out") DIST=$(find w1/target/dist -maxdepth 1 -mindepth 1 -newer marker | head -1) [[ -d "$DIST" ]] || fail "W5: no package directory" <(echo "$pack_out"; find w1/target/dist) packed=0 diff --git a/tests/e2e/841_pack_summarizes_many_outputs.sh b/tests/e2e/841_pack_summarizes_many_outputs.sh index e803c376f..0f1ebb380 100644 --- a/tests/e2e/841_pack_summarizes_many_outputs.sh +++ b/tests/e2e/841_pack_summarizes_many_outputs.sh @@ -57,7 +57,7 @@ int main() { } EOF -"$MCPP" pack --format tree > p1.log 2>&1 || fail "pack failed" p1.log +"$MCPP" pack --pkg-format tree > p1.log 2>&1 || fail "pack failed" p1.log # P1 n=$(grep -c "Packed" p1.log) @@ -67,12 +67,12 @@ grep -qE "Packed .*tree[/\\\\]data \(6 files\)" p1.log || fail "P1: data/ is not grep -qE "Packed .*tree[/\\\\]manifest$" p1.log || fail "P1: the single file is not named" p1.log # P2 -"$MCPP" pack --format tree --verbose > p2.log 2>&1 || fail "pack --verbose failed" p2.log +"$MCPP" pack --pkg-format tree --verbose > p2.log 2>&1 || fail "pack --verbose failed" p2.log n=$(grep -c "Packed" p2.log) [ "$n" = 13 ] || fail "P2: $n Packed lines under --verbose, expected 13" p2.log # P3 -"$MCPP" pack --format tree --message-format json > p3.json 2> p3.err || fail "pack json failed" p3.err +"$MCPP" pack --pkg-format tree --message-format json > p3.json 2> p3.err || fail "pack json failed" p3.err python3 - p3.json <<'EOF' || fail "P3" p3.json import json, sys a = json.load(open(sys.argv[1]))["data"]["artifacts"] diff --git a/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh b/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh index 009f8950f..d5cc1b6a2 100644 --- a/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh +++ b/tests/e2e/867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh @@ -17,7 +17,7 @@ # and the staged tree it was told. # # Criteria (#749, A to D): -# A. `mcpp pack --workspace --format zap` runs `core`'s program once in all, and +# A. `mcpp pack --workspace --pkg-format zap` runs `core`'s program once in all, and # each provider's program once for each of the two passes a dispatched # format prepares: once when no format is asked, and once when it is. # B. Each member's packed tree has the same files with the same content as @@ -144,7 +144,7 @@ stage_of() { ls -d "$1"/target/dist/"$1"-0.1.0-* | grep -v -e '\.stage-manifest$ objects() { awk -F'\t' '$4 ~ /\.o$/' "$1/.ninja_log" | wc -l | tr -d ' '; } # ── A ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --workspace --format zap > a.log 2>&1 || fail "A: the pack failed" a.log +"$MCPP" pack --workspace --pkg-format zap > a.log 2>&1 || fail "A: the pack failed" a.log # `core` is not a program, so it is not packed, and its program ran once in all: # in the pass that asks for no format, and never again for a member whose answer # does not depend on the request. @@ -181,7 +181,7 @@ for m in cli gui; do cp -R "$(stage_of $m)" multi/$m/tree cp "$(stage_of $m).stage-manifest" multi/$m/stage-manifest cp $(outdir $m)/$m.zap multi/$m/dist - "$MCPP" pack -p $m --format zap > b-$m.log 2>&1 || fail "B: mcpp pack -p $m failed" b-$m.log + "$MCPP" pack -p $m --pkg-format zap > b-$m.log 2>&1 || fail "B: mcpp pack -p $m failed" b-$m.log cp -R "$(stage_of $m)" single/$m/tree cp "$(stage_of $m).stage-manifest" single/$m/stage-manifest cp $(outdir $m)/$m.zap single/$m/dist @@ -196,7 +196,7 @@ dirs=$(find target -name build.ninja -not -path '*/.build-mcpp/*' -exec dirname [ "$(echo "$dirs" | wc -l | tr -d ' ')" = 1 ] || fail "C: expected one build directory, got: $dirs" c-build.log before=$(objects "$dirs") [ "$before" -gt 0 ] || fail "C: the build compiled nothing" "$dirs/.ninja_log" -"$MCPP" pack --workspace --format zap > c-pack.log 2>&1 || fail "C: the pack failed" c-pack.log +"$MCPP" pack --workspace --pkg-format zap > c-pack.log 2>&1 || fail "C: the pack failed" c-pack.log [ "$(find target -name build.ninja -not -path '*/.build-mcpp/*' | wc -l | tr -d ' ')" = 1 ] || fail "C: the pack used another build directory" c-pack.log after=$(objects "$dirs") [ "$after" = "$before" ] || fail "C: the pack compiled $((after - before)) object(s) after the build" "$dirs/.ninja_log" c-pack.log diff --git a/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh b/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh index 069d48eac..a9873e63c 100644 --- a/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh +++ b/tests/e2e/868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh @@ -113,7 +113,7 @@ printf 'export module zlib;\nexport int f() { return 1; }\n' > lib/src/lib.cppm # ── E ──────────────────────────────────────────────────────────────────────── set +e -"$MCPP" pack --mode system --workspace --format zap > e.log 2>&1 +"$MCPP" pack --mode system --workspace --pkg-format zap > e.log 2>&1 rc=$? set -e [ "$rc" -ne 0 ] || fail "E: a member without the format was packed" e.log @@ -238,13 +238,13 @@ for m in cli2 gui2; do program_member $m "prov = { path = \"$PROV_HOST\" }" done set +e -"$MCPP" pack --mode system --workspace --format zap > j.log 2>&1 +"$MCPP" pack --mode system --workspace --pkg-format zap > j.log 2>&1 rc=$? set -e [ "$rc" -ne 0 ] && grep -q "member 'cli2'" j.log && grep -q "member 'gui2'" j.log && grep -q "prov" j.log \ || fail "J: members served only by a shared provider were not refused by name" j.log compiled_nothing . || fail "J: the refusal arrived after a compile" j.log -"$MCPP" pack --mode system -p cli2 --format zap > j2.log 2>&1 || fail "J: one member over the same provider was not packed" j2.log +"$MCPP" pack --mode system -p cli2 --pkg-format zap > j2.log 2>&1 || fail "J: one member over the same provider was not packed" j2.log provided=$(find . -name prov.zap | head -1) [ -n "$provided" ] && grep -qx "./bin/cli2" "$provided" \ || fail "J: the provider was not given the member's tree" j2.log @@ -261,7 +261,7 @@ write_dist . program_member cli3 "prov = { path = \"$PROV_HOST\" }" program_member gui3 "" provider gui3 submit -"$MCPP" pack --mode system --workspace --format zap > j3.log 2>&1 || fail "J: a provider one member reaches did not serve it beside another member's own" j3.log +"$MCPP" pack --mode system --workspace --pkg-format zap > j3.log 2>&1 || fail "J: a provider one member reaches did not serve it beside another member's own" j3.log provided=$(find . -name prov.zap | head -1) [ -n "$provided" ] && grep -qx "./bin/cli3" "$provided" && ! grep -q "gui3" "$provided" \ || fail "J: the provider was not given cli3's tree alone" j3.log @@ -283,11 +283,11 @@ provider good submit program_member mute "" provider mute silent set +e -"$MCPP" pack --mode system --workspace --format zap > k.log 2>&1 +"$MCPP" pack --mode system --workspace --pkg-format zap > k.log 2>&1 rc=$? set -e [ "$rc" -ne 0 ] || fail "K: a member that claimed nothing was reported as packed" k.log -grep -q "member 'mute': no action claimed --format 'zap'" k.log \ +grep -q "member 'mute': no action claimed --pkg-format 'zap'" k.log \ || fail "K: the failure does not name the member" k.log [ -n "$(find target/.build-mcpp/out -path '*/good/good.zap' | head -1)" ] || fail "K: the member that claimed the format was not packed" k.log grep -q "Packed .*good.zap" k.log || fail "K: the member that was packed was not reported" k.log diff --git a/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh b/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh index 0e333989c..41e8fe0ff 100644 --- a/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh +++ b/tests/e2e/869_a_pack_over_several_members_names_each_member_in_its_report.sh @@ -113,7 +113,7 @@ for m in cli gui; do || fail "M: $m's archive is not below the directory --output names" m1.log done [ "$(find out -maxdepth 1 -name '*.tar.gz' | wc -l | tr -d ' ')" = 2 ] || fail "M: the directory holds more than the two archives" m1.log -"$MCPP" pack --mode system --workspace --format dir -o out-trees > m2.log 2>&1 || fail "M: --format dir with --output did not pack" m2.log +"$MCPP" pack --mode system --workspace --pkg-format dir -o out-trees > m2.log 2>&1 || fail "M: --pkg-format dir with --output did not pack" m2.log for m in cli gui; do t=$(find out-trees -maxdepth 1 -type d -name "$m-0.1.0-*" | head -1) [ -n "$t" ] && [ -f "$t/bin/$m" ] || fail "M: $m's tree is not below the directory --output names" m2.log @@ -144,7 +144,7 @@ for s in d["stages"]: assert set(s) == {"member", "dir", "manifest", "closure"}, s assert s["closure"] in ("walked", "not-walked"), s for a in d["artifacts"]: - assert set(a) == {"path", "type", "format", "targets", "member"}, a + assert set(a) == {"path", "type", "format", "targets", "member", "sha256"}, a EOF "$MCPP" pack --mode system -p cli --message-format json > o2.json 2> o2.log || fail "O: a pack of one member failed" o2.log python3 - <<'EOF' || fail "O: the envelope of one member changed" o2.json @@ -152,7 +152,7 @@ import json d = json.load(open("o2.json"))["data"] assert "stages" not in d, d assert isinstance(d["stage"], dict) and set(d["stage"]) == {"dir", "manifest", "closure"}, d -assert len(d["artifacts"]) == 1 and set(d["artifacts"][0]) == {"path", "type", "format", "targets"}, d +assert len(d["artifacts"]) == 1 and set(d["artifacts"][0]) == {"path", "type", "format", "targets", "sha256"}, d EOF echo "ok: O, the envelope lists every member's artifacts and keeps the single-member shape" diff --git a/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh b/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh index 8ca4e9258..f75e98e0b 100644 --- a/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh +++ b/tests/e2e/870_a_pack_over_several_members_resolves_and_fails_per_member.sh @@ -99,7 +99,7 @@ member c ctool c++20 zap() { ls -d target/.build-mcpp/out/*/"$1"/"$1".zap 2>/dev/null | head -1; } # ── A ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --workspace --format zap > a.log 2>&1 || fail "A: the pack failed" a.log +"$MCPP" pack --workspace --pkg-format zap > a.log 2>&1 || fail "A: the pack failed" a.log for m in a b c; do [ -x "$(zap $m)" ] || fail "A: no distributable for $m" a.log [ "$("$(zap $m)")" = "$m" ] || fail "A: $m's distributable runs '$("$(zap $m)")', not $m's program" a.log @@ -107,7 +107,7 @@ done echo "ok: A, each member's distributable is its own program, although a and b both name the target tool" # ── B ──────────────────────────────────────────────────────────────────────── -"$MCPP" pack --workspace --format zap --message-format json > b.json 2> b.err \ +"$MCPP" pack --workspace --pkg-format zap --message-format json > b.json 2> b.err \ || fail "B: the JSON pack failed" b.err b.json order=$(python3 -c 'import json,sys d = json.load(open(sys.argv[1])) @@ -118,7 +118,7 @@ echo "ok: B, the stages are listed in [workspace] members order across configura # ── C ──────────────────────────────────────────────────────────────────────── touch fail-b rc=0 -"$MCPP" pack --workspace --format zap > c.log 2>&1 || rc=$? +"$MCPP" pack --workspace --pkg-format zap > c.log 2>&1 || rc=$? [ "$rc" -ne 0 ] || fail "C: the pack exited 0 although b's step failed" c.log for m in a c; do [ -x "$(zap $m)" ] && [ "$("$(zap $m)")" = "$m" ] || fail "C: $m was not packed beside b's failure" c.log diff --git a/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh b/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh index 02218b517..db043f2cc 100644 --- a/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh +++ b/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh @@ -12,7 +12,7 @@ # and the open report. # # Criteria: -# A. A cold `mcpp pack --format tar` writes a `Compiling` line for the +# A. A cold `mcpp pack --pkg-format tar` writes a `Compiling` line for the # package and `Finished` before `Packing`. # B. Under `--message-format json`, standard output is one JSON document. # C. With `[build] jobs = 1`, no two compile steps overlap in `.ninja_log` @@ -84,7 +84,7 @@ no_overlap() { } # ── A ────────────────────────────────────────────────────────────────────── -"$MCPP" pack --format tar > a.log 2>&1 || fail "A: pack failed" a.log +"$MCPP" pack --pkg-format tar > a.log 2>&1 || fail "A: pack failed" a.log grep -q 'Compiling app v0.1.0' a.log || fail "A: the pack's build wrote no Compiling line" a.log fin=$(grep -n 'Finished' a.log | head -1 | cut -d: -f1) pk=$(grep -n 'Packing' a.log | head -1 | cut -d: -f1) @@ -94,7 +94,7 @@ echo "ok: A, the pack states its build and Finished precedes Packing" # ── B ────────────────────────────────────────────────────────────────────── rm -rf target -"$MCPP" pack --format tar --message-format json > b.json 2> b.err || fail "B: pack failed" b.err +"$MCPP" pack --pkg-format tar --message-format json > b.json 2> b.err || fail "B: pack failed" b.err python3 -c 'import json, sys; json.load(open(sys.argv[1]))' b.json \ || fail "B: standard output is not one JSON document" b.json b.err echo "ok: B, the JSON output is one document" @@ -107,7 +107,7 @@ rm -rf target; touch "$TMP/stamp"; sleep 1 "$MCPP" test > c2.log 2>&1 || fail "C: test failed" c2.log no_overlap "C test" rm -rf target; touch "$TMP/stamp"; sleep 1 -"$MCPP" pack --format tar > c3.log 2>&1 || fail "C: pack failed" c3.log +"$MCPP" pack --pkg-format tar > c3.log 2>&1 || fail "C: pack failed" c3.log no_overlap "C pack" echo "ok: C, build, test and pack each run one compile at a time under jobs = 1" diff --git a/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh b/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh index c6d64f5ee..04ae8ba5b 100755 --- a/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh +++ b/tests/e2e/942_an_ok_image_is_built_once_and_run_by_okrun.sh @@ -3,12 +3,13 @@ # 942 -- an ok image is built for `-openkal` and run by okrun (design # 2026-10-26 part 2, §8.5; openkal SPEC clause 12). # -# The target compiles for no operating system (`-unknown-none-elf`) with -# the convention's flags, links with `ld.lld` into an ET_DYN image entered at -# `ok_main`, and publishes the image only after `okld note` has written its -# requirements and `okld check` has checked every clause. `okld` and `okrun` are -# programs of the graph: `openkal-tools`, built for this machine through -# `[build-dependencies]`. `okrun` is the target's runner. +# The target's platform, openkal-platform (SPEC-013), compiles for no operating +# system (`-unknown-none-elf`) with the convention's flags, links with +# `ld.lld` into an ET_DYN image entered at `ok_main`, and publishes the image +# only after its seal -- `okld note`, then `okld check` -- has accepted it. +# `okld` and `okrun` are the platform's tools, built for this machine; `okrun` +# is its runner. The image here has no C library and no C++ run-time: the +# project switches the platform's layers off. # # Criteria: # A. Both rows build: `x86_64-openkal` and `aarch64-openkal` each produce @@ -20,15 +21,14 @@ # C. An image that imports a C library's name (`puts`) is refused by the # check, which names the name; the image under `bin/` is still the one the # last successful build published. -# D. `import std;` with no C++ run-time in the graph is refused before -# anything compiles, naming the packages that provide one. +# D. `import std;` with the platform's C++ run-time switched off is refused +# before anything compiles, saying it was switched off and where. # E. A second build is up to date. # F. `mcpp pack` stages the image (M2), and `--mode self-contained` stages a # program of this machine that carries it and runs with nothing installed # (M3), its image given the program's arguments and environment. -# G. A workspace's root package declares the tools as any root does: the -# image builds and runs through okrun (the selected package is not the -# graph's first, and its tools were not recorded as the root's). +# G. A workspace's root package is built as any root is: the image builds +# and runs through okrun (the selected package is not the graph's first). set -e TMP=$(mktemp -d) @@ -41,7 +41,11 @@ case "$(uname -m)" in *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; esac -TOOLS='openkal-tools = { version = "0.1.0", tools = ["okld", "okrun"] }' +# No C library, no C++ run-time, no compiler run-time: the image is its own. +NO_LAYERS='[target.'"'"'cfg(platform = "openkal")'"'"'.layers] +c-abi = false +"c++-abi" = false +compiler-runtime = false' mkdir -p "$TMP/app/src" cd "$TMP/app" @@ -54,8 +58,7 @@ version = "0.1.0" kind = "bin" main = "src/main.c" -[build-dependencies] -$TOOLS +$NO_LAYERS TOML cat > src/main.c <<'C' /* An image with no C library: it calls kal_* and supplies the two routines @@ -153,9 +156,9 @@ grep -q "Compiling" again.log && fail "E: the second build compiled again" again echo "ok: a second build is up to date" # F -"$MCPP" pack --target "$HOST_ARCH-openkal" --format dir -o m2 > m2.log 2>&1 || fail "F: pack (M2)" m2.log +"$MCPP" pack --target "$HOST_ARCH-openkal" --no-universal --pkg-format dir -o m2 > m2.log 2>&1 || fail "F: pack (M2)" m2.log [ -f target/dist/m2/bin/app.oke ] || fail "F: the M2 package has no bin/app.oke" m2.log -"$MCPP" pack --target "$HOST_ARCH-openkal" --mode self-contained --format dir -o m3 > m3.log 2>&1 \ +"$MCPP" pack --target "$HOST_ARCH-openkal" --no-universal --mode self-contained --pkg-format dir -o m3 > m3.log 2>&1 \ || fail "F: pack --mode self-contained (M3)" m3.log m3=$(ls target/dist/m3/bin/app target/dist/m3/bin/app.exe 2>/dev/null | head -1) [ -n "$m3" ] || fail "F: the M3 package has no program" m3.log @@ -176,7 +179,7 @@ if "$MCPP" build --target "$HOST_ARCH-openkal" > refuse.log 2>&1; then fail "C: an image importing puts was published" refuse.log fi grep -q "puts" refuse.log || fail "C: the refusal does not name the import" refuse.log -grep -q "ok-image convention" refuse.log || fail "C: the refusal does not name the convention" refuse.log +grep -q "was not sealed" refuse.log || fail "C: the refusal does not name the seal" refuse.log after=$(cksum < "$(ls target/"$HOST_ARCH"-openkal/*/bin/app.oke | head -1)" ) [ "$before" = "$after" ] || fail "C: the refused image took the name in bin/" refuse.log echo "ok: an image that imports a C library's name is refused and not published" @@ -189,16 +192,19 @@ cat > mcpp.toml < src/main.cpp if "$MCPP" build --target "$HOST_ARCH-openkal" > std.log 2>&1; then fail "D: import std built without a C++ run-time" std.log fi -grep -q "needs a C++ run-time inside the image" std.log || fail "D: the refusal's reason" std.log -grep -q "openkal-llvm-runtime" std.log || fail "D: the refusal does not name the provider" std.log -echo "ok: import std without a C++ run-time is refused, naming the provider" +grep -q "needs a C++ run-time inside the program" std.log || fail "D: the refusal's reason" std.log +grep -q "switched off" std.log || fail "D: the refusal does not say the layer was switched off" std.log +grep -q "mcpp.toml" std.log || fail "D: the refusal does not say where" std.log +echo "ok: import std with the C++ run-time switched off is refused, saying where" # G mkdir -p "$TMP/ws/src" "$TMP/ws/m/src" @@ -215,14 +221,13 @@ version = "0.1.0" kind = "bin" main = "src/main.c" -[build-dependencies] -$TOOLS +$NO_LAYERS TOML printf '[package]\nname = "m"\nversion = "0.1.0"\n\n[targets.m]\nkind = "lib"\n' > "$TMP/ws/m/mcpp.toml" printf 'int m_value(void) { return 0; }\n' > "$TMP/ws/m/src/m.c" (cd "$TMP/ws" && "$MCPP" run --target "$HOST_ARCH-openkal" -- one two > run.log 2>&1) \ || fail "G: a workspace root's image did not build or run" "$TMP/ws/run.log" grep -q "image: three arguments" "$TMP/ws/run.log" || fail "G: the workspace root's image" "$TMP/ws/run.log" -echo "ok: a workspace root declares the tools as any root does" +echo "ok: a workspace root's image is built and run as any root's" echo "PASS: 942 an ok image is built once and run by okrun" diff --git a/tests/e2e/949_a_version_the_index_lacks_refreshes_at_install.sh b/tests/e2e/949_a_version_the_index_lacks_refreshes_at_install.sh new file mode 100755 index 000000000..4da38843e --- /dev/null +++ b/tests/e2e/949_a_version_the_index_lacks_refreshes_at_install.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# requires: gcc fresh-sandbox +# 949 -- a version the local index lacks is refreshed for when its install is +# refused, and the package the descriptor named is the only one asked for. +# +# The walk refreshes when a descriptor is missing, and deliberately not when a +# descriptor is present without the version asked for: version tables are per +# system, so that absence is not proof. The provider's "not found" for the +# version is. Measured: `openkal-llvm-runtime = "0.16.3"` against an index +# synced before 0.16.3 was refused until `mcpp index update`, after mcpp had +# also tried `compat:openkal-llvm-runtime` and `compat.openkal-llvm-runtime` +# and shown them as downloading. +# +# Hermetic, as 945 is: the builtin index is fabricated, and every case is +# debounced or has `auto_refresh = false`, so the decision is observed +# without a sync. +# +# Criteria: +# A. The refused install reaches the refresh policy, which declines inside +# the debounce window and says so for that address. +# B. With `[index] auto_refresh = false` the same refusal is declined for +# that reason. +# C. A descriptor that was read names the package: no other namespace is +# tried or shown as downloading, and the refusal names one address. +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; } + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +INDEX="$MCPP_HOME/indices/mcpplibs" +mkdir -p "$INDEX/pkgs/l" +echo "abc1234" > "$INDEX/.xlings-index-version" +# The descriptor is here; the version asked for is not in it. +cat > "$INDEX/pkgs/l/laterpkg.lua" <<'EOF' +package = { + spec = "1", + namespace = "mcpplibs", + name = "laterpkg", + description = "A package whose next version the local index predates", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/laterpkg-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, +} +EOF + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "app" +version = "0.1.0" + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +laterpkg = "1.0.1" +TOML +echo 'int main() { return 0; }' > src/main.cpp + +# A +: > "$INDEX/.mcpp-index-updated" +MCPP_VERBOSE=1 "$MCPP" build > a.log 2>&1 && fail "A: the build installed a version no index carries" a.log +grep -qE "skip refresh for mcpplibs:laterpkg@1\.0\.1: index was just refreshed" a.log \ + || fail "A: the refused install did not reach the refresh policy" a.log +grep -qE "Refreshing +package index" a.log && fail "A: a sync inside the debounce window" a.log +echo "ok: a refused install asks for a refresh (declined inside the debounce window)" + +# B +touch -d '30 days ago' "$INDEX/.mcpp-index-updated" 2>/dev/null \ + || touch -A -300000 "$INDEX/.mcpp-index-updated" 2>/dev/null || true +printf '\n[index]\nauto_refresh = false\n' >> "$MCPP_HOME/config.toml" +MCPP_VERBOSE=1 "$MCPP" build > b.log 2>&1 && fail "B: the build installed a version no index carries" b.log +grep -qE "skip refresh for mcpplibs:laterpkg@1\.0\.1: \[index\] auto_refresh = false" b.log \ + || fail "B: auto_refresh = false did not decide the refused install" b.log +echo "ok: with auto_refresh = false the refused install is declined" + +# C +grep -qE "Downloading +compat" a.log b.log && fail "C: another namespace was shown as downloading" a.log +grep -q "wire address tried: mcpplibs:laterpkg@1.0.1$" a.log \ + || fail "C: the refusal does not name the one address asked for" a.log +echo "ok: the package the descriptor named is the only one asked for" + +echo "PASS: 949 a version the index lacks refreshes at install" diff --git a/tests/e2e/950_a_kernel_abi_package_over_the_system_c_library_is_in_the_shared_image.sh b/tests/e2e/950_a_kernel_abi_package_over_the_system_c_library_is_in_the_shared_image.sh new file mode 100755 index 000000000..c343ceb7c --- /dev/null +++ b/tests/e2e/950_a_kernel_abi_package_over_the_system_c_library_is_in_the_shared_image.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# requires: elf +# 950 -- a `kernel-abi` package over the platform's C library is linked into a +# shared image that reaches it, like any static package. +# +# THE DEFECT. Every package that provides an `mcpp:` layer was a boundary of +# static placement (#646 F1): never placed into a shared image, its objects +# went into the programs only. That is right for a C library or a C++ runtime, +# whose contract decides where they go. It is not right for openkal-linux +# (-macos, -windows) on a hosted target: there the C library is the system's, +# and the kernel-abi package is a static library over it that `mcpplibs.os` +# depends on like any other. A shared image reaching it was left with `kal_*` +# undefined. ELF linked it, and a host that had not linked the package itself +# could not load it -- measured with CPython importing an extension module +# built on `mcpplibs.os`: +# +# ImportError: .../_openmui.abi3.so: undefined symbol: kal_process_close +# +# and Mach-O and PE refuse the link outright. +# +# Two legs, both a program over a dependency's shared image: +# A the program reaches the kernel-abi package only through the image: the +# image defines its symbols and does not import them, as it does for the +# ordinary static package beside it. +# B the program reaches the package itself too: it links and runs as +# before. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +cd "$work" +mkdir -p kernel/src sys/src ext/src app/src + +cat > kernel/src/kernel.c <<'EOF' +int kal_fake_answer(void) { return 42; } +EOF +cat > kernel/mcpp.toml <<'EOF' +[package] +name = "fakekernel" +version = "0.1.0" +provides = ["mcpp:kernel-abi=openkal"] + +[targets.fakekernel] +kind = "lib" +sources = ["src/*.c"] +EOF + +cat > sys/src/sys.c <<'EOF' +extern int kal_fake_answer(void); +int sys_answer(void) { return kal_fake_answer(); } +EOF +cat > sys/mcpp.toml <<'EOF' +[package] +name = "fakesys" +version = "0.1.0" + +[targets.fakesys] +kind = "lib" +sources = ["src/*.c"] + +[dependencies] +fakekernel = { path = "../kernel" } +EOF + +cat > ext/src/ext.c <<'EOF' +extern int sys_answer(void); +int ext_answer(void) { return sys_answer(); } +EOF +cat > ext/mcpp.toml <<'EOF' +[package] +name = "ext" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.ext] +kind = "shared" + +[dependencies] +fakesys = { path = "../sys" } +EOF + +write_app() { # $1 = an extra dependency line, $2 = what main checks + cat > app/src/main.c < app/mcpp.toml <&1) || { echo "$out" >&2; echo "FAIL: $1: the program must build" >&2; exit 1; } + so="$(find app/target -name 'libext.so' -type f | head -1)" + bin="$(find app/target -path '*/bin/app' -type f | head -1)" + if [ -z "$so" ] || [ -z "$bin" ]; then + echo "$out" >&2; echo "FAIL: $1: libext.so or bin/app was not produced" >&2; exit 1 + fi + "$bin" || { echo "FAIL: $1: the program ran and answered $?" >&2; exit 1; } +} + +# ── A. the image is the package's one home ──────────────────────────────── +write_app "" "ext_answer() == 42" +build_app A +nm --defined-only "$so" | grep -qw sys_answer || { + echo "FAIL: A: the shared image does not define sys_answer (its static dependency)" >&2; exit 1; } +if nm -D --undefined-only "$so" | grep -qw kal_fake_answer; then + nm -D --undefined-only "$so" >&2 + echo "FAIL: A: the shared image imports kal_fake_answer from the program; a host" \ + "that did not link fakekernel cannot load it" >&2 + exit 1 +fi +nm --defined-only "$so" | grep -qw kal_fake_answer || { + echo "FAIL: A: the shared image does not define kal_fake_answer" >&2; exit 1; } +echo "OK: A (the shared image defines the kernel-abi package's symbols)" + +# ── B. the program reaches the package itself ───────────────────────────── +write_app 'fakekernel = { path = "../kernel" }' "ext_answer() == 42 && kal_fake_answer() == 42" +build_app B +echo "OK: B (a program reaching the package links and runs)" diff --git a/tests/e2e/951_an_ok_image_needs_no_declaration.sh b/tests/e2e/951_an_ok_image_needs_no_declaration.sh new file mode 100755 index 000000000..29a8e2967 --- /dev/null +++ b/tests/e2e/951_an_ok_image_needs_no_declaration.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# requires: llvm python3 +# 951 -- `-openkal` needs no declaration: the target's platform supplies +# the C library, the C++ run-time, the tools and the runner (SPEC-013; design +# 2026-10-28 §7, §10). +# +# The target table pins `openkal-platform` for both ok-image rows. A project +# that names nothing but its sources builds a C++ image with `import std`, runs +# it, tests it and packs it; what the platform added is reported as the +# platform's and locked as such; the overrides take their place by the rules +# of SPEC-013 §4.3-§4.5. +# +# Criteria: +# A. `mcpp new`-shaped project, `mcpp run --target -openkal`: the +# image runs through okrun, with exceptions and the standard library. +# B. The report names the platform and its source, marks the layers it added +# `platform`, and the toolchain line says the toolchain is the platform's. +# C. mcpp.lock records the packages the platform added, and those only they +# reach, with `origin = "platform"`; a build for this machine leaves the +# file as it is. +# D. `mcpp test` builds and runs a test image; `mcpp pack` stages the image +# and its loader-run program (M3). +# E. `mcpp self env --target … --format json` names the platform, its fields' +# sources and the runner's program; `mcpp toolchain list --format json` +# gives the row's platform. +# F. A package of the graph that provides a layer takes the platform's place +# (reported `graph`); a version of the family the platform's group does not +# name is refused, naming the group and both remedies. +# G. `--platform none` selects no platform for the target. +# H. After `mcpp toolchain install --target`, a project for the target +# builds and runs with `--offline`. +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) HOST_ARCH=x86_64 ;; + aarch64|arm64) HOST_ARCH=aarch64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac +T="$HOST_ARCH-openkal" + +mkdir -p "$TMP/app/src" "$TMP/app/tests" +cd "$TMP/app" +printf '[package]\nname = "app"\nversion = "0.1.0"\n' > mcpp.toml +cat > src/main.cpp <<'CPP' +import std; +int main(int argc, char** argv) { + try { throw std::runtime_error("caught"); } + catch (const std::exception& e) { std::println("image: {} ({} args)", e.what(), argc); } + return 0; +} +CPP +cat > tests/smoke.cpp <<'CPP' +import std; +int main() { std::vector v{2, 1}; std::ranges::sort(v); return v.front() == 1 ? 0 : 1; } +CPP + +# A +"$MCPP" run --target "$T" -- one two > run.log 2>&1 || fail "A: the image did not build or run" run.log +grep -q "image: caught (3 args)" run.log || fail "A: the image's output" run.log +echo "ok: a project that declares nothing runs as an ok image" + +# B +"$MCPP" build --target "$T" -v > build.log 2>&1 || fail "B: rebuild" build.log +rm -rf target +"$MCPP" build --target "$T" > fresh.log 2>&1 || fail "B: fresh build" fresh.log +# The source is the target table's column, or the variable a run before the +# platform's release selects it with. +grep -qE "Platform +openkal-platform [0-9.]+ \((built-in|MCPP_TARGET_[A-Z0-9_]+_PLATFORM)\)" fresh.log || fail "B: no Platform line naming the source" fresh.log +grep -qE "c-abi +musl +\(openkal-musl@[0-9.]+, platform\)" fresh.log || fail "B: the C library is not reported as the platform's" fresh.log +grep -qE "c\+\+-abi +libc\+\+ +\(openkal-llvm-runtime@[0-9.]+, platform\)" fresh.log || fail "B: the C++ run-time is not the platform's" fresh.log +grep -qE "tools +okld, okrun" fresh.log || fail "B: the tools are not reported" fresh.log +echo "ok: the report names the platform and what it added" + +# C +grep -A5 '"openkal-musl"' mcpp.lock | grep -q 'origin = "platform"' || fail "C: openkal-musl is not locked as the platform's" mcpp.lock +grep -A5 '"openkal-llvm-runtime"' mcpp.lock | grep -q 'origin = "platform"' || fail "C: openkal-llvm-runtime is not locked as the platform's" mcpp.lock +grep -A5 '"openkal-tools"' mcpp.lock | grep -q 'origin = "platform"' || fail "C: the platform's tools are not locked as the platform's" mcpp.lock +cp mcpp.lock lock.image +"$MCPP" build > c-host.log 2>&1 || fail "C: the build for this machine" c-host.log +cmp -s lock.image mcpp.lock || { diff lock.image mcpp.lock > c-lock.diff; fail "C: the build for this machine rewrote the lock" c-lock.diff; } +echo "ok: the lock says which packages the platform added, whichever configuration wrote it last" + +# D +"$MCPP" test --target "$T" > test.log 2>&1 || fail "D: mcpp test" test.log +grep -q "smoke ... ok" test.log || fail "D: the test image did not pass" test.log +"$MCPP" pack --target "$T" --no-universal --mode self-contained --pkg-format dir -o m3 > m3.log 2>&1 || fail "D: pack (M3)" m3.log +m3=$(ls target/dist/m3/bin/app target/dist/m3/bin/app.exe 2>/dev/null | head -1) +[ -n "$m3" ] || fail "D: the M3 package has no program" m3.log +"$m3" x > m3run.log 2>&1 || fail "D: the M3 program did not run" m3run.log +grep -q "image: caught (2 args)" m3run.log || fail "D: the M3 program's image" m3run.log +echo "ok: test and pack work with no declaration" + +# E +"$MCPP" self env --target "$T" --format json > env.json 2> env.err || fail "E: self env" env.err env.json +python3 - env.json <<'PY' || fail "E: self env --format json" env.json +import json, sys +d = json.load(open(sys.argv[1]))["data"] +p = d["platform"] +assert p and p["name"].endswith("openkal-platform"), p +assert p["source"] == "built-in" or p["source"].startswith("MCPP_TARGET_"), p["source"] +assert p["fields"]["runner"]["source"] == "platform" +assert p["fields"]["layers.c-abi"]["source"] == "platform" +assert d["runner"]["program"] and d["runner"]["program"].replace("\\", "/").split("/")[-1].startswith("okrun"), d["runner"] +PY +"$MCPP" toolchain list --format json > list.json 2>/dev/null || fail "E: toolchain list" list.json +python3 - list.json "$T" <<'PY' || fail "E: toolchain list's platform column" list.json +import json, sys +rows = [r for r in json.load(open(sys.argv[1]))["data"]["targets"] if r["target"] == sys.argv[2]] +assert rows and rows[0]["platform"].split("@")[0].endswith("openkal-platform"), rows +PY +echo "ok: self env and toolchain list state the platform" + +# F +mkdir -p "$TMP/own/src" +cd "$TMP/own" +cat > mcpp.toml <<'TOML' +[package] +name = "own" +version = "0.1.0" + +[dependencies] +openkal-musl = { version = "0.22.2", default-features = false, features = ["posix"] } +TOML +cp "$TMP/app/src/main.cpp" src/main.cpp +if "$MCPP" build --target "$T" > group.log 2>&1; then + fail "F: a version outside the platform's group was built" group.log +fi +grep -q "version group" group.log || fail "F: the refusal does not name the group" group.log +grep -qE "names openkal-musl [0-9]+\.[0-9]+\.[0-9]+" group.log || fail "F: the refusal does not name the group's version" group.log +echo "ok: a version outside the platform's group is refused" + +# G +"$MCPP" self env --target "$T" --platform none --format json > none.json 2> none.err || fail "G: self env --platform none" none.err +python3 - none.json <<'PY' || fail "G: --platform none still selected a platform" none.json +import json, sys +d = json.load(open(sys.argv[1]))["data"] +assert d["platform"] is None, d["platform"] +PY +echo "ok: --platform none selects no platform" + +# H +mkdir -p "$TMP/off/src" +cd "$TMP/off" +printf '[package]\nname = "off"\nversion = "0.1.0"\n' > mcpp.toml +printf 'import std;\nint main() { std::println("offline image"); }\n' > src/main.cpp +"$MCPP" toolchain install --target "$T" > install.log 2>&1 || fail "H: toolchain install --target" install.log +grep -q "Installed openkal-platform" install.log || fail "H: the install does not name the platform" install.log +"$MCPP" run --offline --target "$T" > off.log 2>&1 || fail "H: run --offline after the install" off.log +grep -q "offline image" off.log || fail "H: the offline run's output" off.log +echo "ok: toolchain install --target prepares the target for --offline" + +echo "PASS: 951 an ok image needs no declaration" diff --git a/tests/e2e/952_a_platform_package_states_a_class_of_targets.sh b/tests/e2e/952_a_platform_package_states_a_class_of_targets.sh new file mode 100755 index 000000000..1e2d1027d --- /dev/null +++ b/tests/e2e/952_a_platform_package_states_a_class_of_targets.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# requires: unix-shell python3 +# 952 -- a platform package that has nothing to do with openkal states the +# configuration of a class of targets, and the engine carries every group of it +# (SPEC-013 §4; design 2026-10-28 §12.1, the board-support row). +# +# The platform here is a local directory for this machine's own triples. It +# states compile flags, the name of a program, a seal step and a runner; the +# project selects it by path for its system and overrides one field under +# `cfg(platform = "demo")`. +# +# Criteria: +# A. The platform's compile flag reaches the project's units; the program is +# named by the platform's pattern; the seal step ran on it before it was +# published; `mcpp run` starts it through the platform's runner. +# B. The report names the platform and the source of its selection. +# C. An override under `cfg(platform = "demo")` takes the field's place +# (`link.exe`), and the report's JSON says where each field came from. +# D. `--platform none` builds the project without the platform: the default +# name, no flag, no seal, no runner. +# E. A platform package without `[platform]` is refused, naming the package. +# F. A flag with `$`, quotes and a space reaches the compile as written. +# G. With the objects removed, the program is linked and sealed again, once. +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; } + +mkdir -p "$TMP/plat" "$TMP/app/src" +cat > "$TMP/plat/mcpp.toml" <<'TOML' +[package] +name = "demo-platform" +version = "0.1.0" + +[platform] +targets = ["x86_64-linux-gnu", "aarch64-linux-gnu", "aarch64-macos", "x86_64-macos"] +runner = ["env", "DEMO_PLATFORM_RUNNER=yes", "{}"] + +[platform.build] +cxxflags = ["-DDEMO_PLATFORM=1", "-DDEMO_TAG=\"a$b c\""] + +[platform.link] +exe = "{name}.demo" +seal = [["sh", "-c", "printf DEMO-SEALED >> \"$0\"", "{}"]] +TOML + +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "app" +version = "0.1.0" + +[target.'cfg(any(linux, macos))'] +platform = { path = "../plat" } +TOML +cat > src/main.cpp <<'CPP' +#include +#include +int main() { +#ifdef DEMO_PLATFORM + const char* flag = "flag"; +#else + const char* flag = "no-flag"; +#endif +#ifdef DEMO_TAG + const char* tag = DEMO_TAG; +#else + const char* tag = "none"; +#endif + const char* runner = std::getenv("DEMO_PLATFORM_RUNNER"); + std::printf("demo: %s, runner %s, tag [%s]\n", flag, runner ? runner : "none", tag); + return 0; +} +CPP + +# A +"$MCPP" build > build.log 2>&1 || fail "A: build" build.log +prog=$(ls target/*/*/bin/app.demo 2>/dev/null | head -1) +[ -n "$prog" ] || fail "A: no program named by the platform's pattern (app.demo)" build.log +[ "$(tail -c 11 "$prog")" = "DEMO-SEALED" ] || fail "A: the seal step did not run before publishing" build.log +"$MCPP" run > run.log 2>&1 || fail "A: run" run.log +grep -q "demo: flag, runner yes" run.log || fail "A: the flag or the runner did not reach the program" run.log +echo "ok: the platform's flag, name, seal and runner reach the program" + +# B +grep -qE "Platform +demo-platform 0\.1\.0 \(mcpp\.toml" build.log || fail "B: the report does not name the platform and its source" build.log +echo "ok: the report names the platform and where it was selected" + +# C +cat >> mcpp.toml <<'TOML' + +[target.'cfg(platform = "demo")'.link] +exe = "{name}.over" +TOML +"$MCPP" build > over.log 2>&1 || fail "C: build with the override" over.log +ls target/*/*/bin/app.over > /dev/null 2>&1 || fail "C: the override did not take the field's place" over.log +host=$(python3 - <<'PY' +import platform +m = platform.machine().lower() +arch = "aarch64" if m in ("arm64", "aarch64") else "x86_64" +print(f"{arch}-macos" if platform.system() == "Darwin" else f"{arch}-linux-gnu") +PY +) +"$MCPP" self env --target "$host" --format json > env.json 2> env.err || fail "C: self env" env.err env.json +python3 - env.json <<'PY' || fail "C: the JSON's sources" env.json +import json, sys +p = json.load(open(sys.argv[1]))["data"]["platform"] +assert p and p["name"].endswith("demo-platform"), p +f = p["fields"] +assert f["link.exe"]["source"].startswith("mcpp.toml"), f["link.exe"] +assert f["runner"]["source"] == "platform", f["runner"] +PY +echo "ok: an override under cfg(platform) takes the field's place, and the JSON says where" + +# D +"$MCPP" run --platform none > none.log 2>&1 || fail "D: run --platform none" none.log +grep -q "demo: no-flag, runner none" none.log || fail "D: --platform none kept a field of the platform" none.log +ls target/*/*/bin/app > /dev/null 2>&1 || fail "D: the program is not under its default name" none.log +echo "ok: --platform none builds without the platform" + +# E +mkdir -p "$TMP/notplat" +printf '[package]\nname = "notplat"\nversion = "0.1.0"\n' > "$TMP/notplat/mcpp.toml" +if "$MCPP" build --platform "path:$TMP/notplat" > notplat.log 2>&1; then + fail "E: a package without [platform] was accepted as a platform" notplat.log +fi +grep -q "has no \[platform\] section" notplat.log || fail "E: the refusal does not say why" notplat.log +grep -q "notplat" notplat.log || fail "E: the refusal does not name the package" notplat.log +echo "ok: a package without [platform] is refused as a platform" + +# F +grep -qF 'tag [a$b c]' run.log || fail "F: the flag with \$ did not reach the compile as written" run.log +echo "ok: a flag with \$, quotes and a space reaches the compile as written" + +# G +"$MCPP" build > g0.log 2>&1 || fail "G: build" g0.log +prog=$(ls target/*/*/bin/app.over | head -1) +[ -n "$prog" ] || fail "G: no program" g0.log +rm -rf "$(dirname "$(dirname "$prog")")/obj" +"$MCPP" build > g.log 2>&1 || fail "G: build after the objects were removed" g.log +[ "$(tail -c 11 "$prog")" = "DEMO-SEALED" ] || fail "G: the program was not sealed again" g.log +[ "$(tail -c 22 "$prog")" = "DEMO-SEALEDDEMO-SEALED" ] && fail "G: the program was sealed twice" g.log +"$MCPP" run > g-run.log 2>&1 || fail "G: run" g-run.log +echo "ok: a program linked again is sealed again, once" + +echo "PASS: 952 a platform package states a class of targets" diff --git a/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh b/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh new file mode 100755 index 000000000..48e64ae2d --- /dev/null +++ b/tests/e2e/953_the_host_graph_runs_build_programs_as_images.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# requires: llvm python3 +# 953 -- the host graph's platform (SPEC-013 §6): a project whose `[host]` +# selects openkal-platform builds its build program as an ok image of this +# machine's architecture and runs it through the platform's runner; the build +# program's library is an image of the same graph; the platform's own tools are +# built for this machine as before (the bootstrap boundary). +# +# Criteria: +# A. The build program runs as an image: the report names the host platform, +# and what the program defines reaches the project's unit. +# B. A library of `[build-dependencies]` the build program imports is built +# into the same image graph, and its answer reaches the program. +# C. The runner of the host graph is a program of this machine (the platform's +# `okrun`, built natively): the bootstrap boundary holds. +# D. `--host-platform none` runs the build program as a program of this +# machine: no host platform is reported, and the answer is the same. +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|aarch64|arm64) ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac + +mkdir -p "$TMP/app/src" "$TMP/app/helper/src" +cd "$TMP/app" +cat > mcpp.toml <<'TOML' +[package] +name = "hostgraph" +version = "0.1.0" + +[host] +platform = "openkal-platform@0.17.3" + +[build-dependencies] +helper = { path = "helper" } +TOML +printf '[package]\nname = "helper"\nversion = "0.1.0"\n\n[targets.helper]\nkind = "lib"\n' > helper/mcpp.toml +cat > helper/src/helper.cppm <<'CPP' +export module helper; +import std; +export namespace helper { +int answer() { return static_cast(std::string_view("forty-two").size()) * 4 + 6; } +} +CPP +cat > build.mcpp <<'CPP' +import mcpp; +import std; +import helper; +int main() { + mcpp::define("BUILT_BY_AN_IMAGE=1"); + mcpp::define(std::format("HELPER_ANSWER={}", helper::answer()).c_str()); + return 0; +} +CPP +cat > src/main.cpp <<'CPP' +#include +int main() { std::printf("host graph: %d, answer %d\n", BUILT_BY_AN_IMAGE, HELPER_ANSWER); return 0; } +CPP + +# A, B +"$MCPP" run > run.log 2>&1 || fail "A: the project did not build or run" run.log +grep -qE "Host platform +openkal-platform" run.log || fail "A: the report does not name the host platform" run.log +grep -q "host graph: 1, answer 42" run.log || fail "A/B: the build program's definitions did not arrive" run.log +echo "ok: the build program and its library run as an image" + +# C +"$MCPP" self env --host --format json > host.json 2> host.err || fail "C: self env --host" host.err host.json +python3 - host.json <<'PY' || fail "C: the host graph's runner" host.json +import json, sys +d = json.load(open(sys.argv[1]))["data"] +p = d["platform"] +assert p and p["name"].endswith("openkal-platform"), p +okrun = d["tools"].get("okrun") +assert okrun, d["tools"] +assert d["runner"] and d["runner"]["program"] == okrun, d["runner"] +# A program of this machine, not an image: an executable's magic, and no +# image name. +magic = open(okrun, "rb").read(4) +assert magic in (b"\x7fELF", b"\xcf\xfa\xed\xfe", b"MZ\x90\x00") or magic[:2] == b"MZ", magic +assert not okrun.endswith(".oke"), okrun +PY +echo "ok: the host graph's runner is a program of this machine" + +# D +rm -rf target +"$MCPP" run --host-platform none > none.log 2>&1 || fail "D: --host-platform none" none.log +grep -q "Host platform" none.log && fail "D: a host platform is reported under --host-platform none" none.log +grep -q "host graph: 1, answer 42" none.log || fail "D: the answer differs without the host platform" none.log +echo "ok: --host-platform none runs the build program natively" + +echo "PASS: 953 the host graph runs build programs as images" diff --git a/tests/e2e/954_pack_has_three_axes.sh b/tests/e2e/954_pack_has_three_axes.sh new file mode 100755 index 000000000..339246a41 --- /dev/null +++ b/tests/e2e/954_pack_has_three_axes.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# requires: unix-shell python3 +# 954 -- `mcpp pack` has three axes (SPEC-014 §1): the linkage of the program, +# the closure the package carries (`--mode`), the container it is delivered in +# (`--pkg-format`, compressed as `--compress` says); every package describes +# itself in `mcpp-package.json`; impossible combinations are refused before +# anything is built. +# +# Criteria: +# A. `--compress zstd | xz | none` writes `.tar.zst`, `.tar.xz`, `.tar`, each +# holding one top-level directory; `--pkg-format zip` a zip. +# B. The tree's `mcpp-package.json` names the package, its legs and its three +# axes, and lists every file with its role and a digest that matches it. +# C. `--message-format json` states linkage, mode, pkgFormat and legs, and the +# sha256 of the archive. +# D. `[pack] format` and `[pack] compress` are the project's defaults. +# E. Refused before a build, naming both choices: single-file with --mode +# system; --compress on a directory; zstd in a zip; --systems for a package +# that carries no loader. +# F. `--format` is the earlier spelling of `--pkg-format`, with a warning. +# G. `[pack] include` stages the files it names at their paths, less what +# `[pack] exclude` names, and the metadata lists them as resources. +# H. `[pack] sign` runs on each program before the container, and the +# metadata's digests are those of the signed files. +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; } + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +printf '[package]\nname = "axes"\nversion = "0.1.0"\n' > mcpp.toml +printf 'int main() { return 0; }\n' > src/main.cpp + +# A +for c in zstd xz none; do + "$MCPP" pack --compress "$c" > "pack-$c.log" 2>&1 || fail "A: --compress $c" "pack-$c.log" +done +"$MCPP" pack --pkg-format zip > pack-zip.log 2>&1 || fail "A: --pkg-format zip" pack-zip.log +python3 - <<'PY' || fail "A: the archives" +import glob, io, lzma, tarfile, zipfile +from pathlib import Path +d = Path("target/dist") +def one_top(names): + tops = {n.split("/")[0] for n in names} + assert len(tops) == 1, tops +xz = glob.glob(str(d / "*.tar.xz")); assert xz, list(d.iterdir()) +with tarfile.open(xz[0], "r:xz") as t: one_top(t.getnames()) +plain = glob.glob(str(d / "*.tar")); assert plain +with tarfile.open(plain[0], "r:") as t: one_top(t.getnames()) +zst = glob.glob(str(d / "*.tar.zst")); assert zst +assert open(zst[0], "rb").read(4) == b"\x28\xb5\x2f\xfd", "not a zstd frame" +z = glob.glob(str(d / "*.zip")); assert z +with zipfile.ZipFile(z[0]) as f: + one_top(f.namelist()) + assert any(i.compress_type == zipfile.ZIP_DEFLATED for i in f.infolist()), "nothing deflated" +PY +echo "ok: tar compressions and zip" + +# B +python3 - <<'PY' || fail "B: mcpp-package.json" +import glob, hashlib, json, os +root = sorted(p for p in glob.glob("target/dist/*") if os.path.isdir(p))[0] +d = json.load(open(os.path.join(root, "mcpp-package.json"))) +assert d["schema"] == 1 and d["name"] == "axes" and d["version"] == "0.1.0", d +assert d["mode"] == "vendored" and d["platform"] is None and len(d["legs"]) == 1, d +listed = {f["path"]: f for f in d["files"]} +on_disk = set() +for dirpath, _, files in os.walk(root): + for n in files: + rel = os.path.relpath(os.path.join(dirpath, n), root).replace(os.sep, "/") + if rel != "mcpp-package.json" and not os.path.islink(os.path.join(dirpath, n)): + on_disk.add(rel) +assert on_disk == set(listed), (on_disk, set(listed)) +for rel, f in listed.items(): + assert f["sha256"] == hashlib.sha256(open(os.path.join(root, rel), "rb").read()).hexdigest(), rel +assert any(f["role"] == "program" for f in d["files"]), d["files"] +PY +echo "ok: mcpp-package.json describes every file" + +# C +"$MCPP" pack --compress zstd --message-format json > env.json 2> env.err || fail "C: json" env.err +python3 - env.json <<'PY' || fail "C: the envelope" env.json +import hashlib, json, sys +d = json.load(open(sys.argv[1]))["data"] +assert d["pkgFormat"] == "tar" and d["mode"] == "vendored" and d["linkage"], d +assert len(d["legs"]) == 1, d +a = d["artifacts"][0] +assert a["sha256"] == hashlib.sha256(open(a["path"], "rb").read()).hexdigest(), a +PY +echo "ok: the envelope states the axes and the digest" + +# D +cat >> mcpp.toml <<'TOML' + +[pack] +format = "tar" +compress = "xz:9" +TOML +rm -rf target/dist +"$MCPP" pack > d.log 2>&1 || fail "D: pack with [pack] defaults" d.log +ls target/dist/*.tar.xz > /dev/null 2>&1 || fail "D: [pack] compress was not the default" d.log +echo "ok: [pack] format and compress are the project's defaults" + +# E +refuse() { # label, expected text, args... + local label=$1 want=$2; shift 2 + if "$MCPP" pack "$@" > refuse.log 2>&1; then fail "E: $label was accepted" refuse.log; fi + grep -q -- "$want" refuse.log || fail "E: $label: the refusal does not say '$want'" refuse.log + grep -q "Compiling" refuse.log && fail "E: $label was refused after compiling" refuse.log + return 0 +} +refuse "single-file with --mode system" "single-file" --pkg-format single-file --mode system +refuse "--compress on a directory" "writes none" --pkg-format dir --compress zstd +refuse "zstd in a zip" "deflated" --pkg-format zip --compress zstd +refuse "--systems without loaders" "systems" --systems linux +echo "ok: impossible combinations are refused before a build" + +# F +"$MCPP" pack --format dir > f.log 2>&1 || fail "F: --format dir" f.log +grep -q "earlier spelling of --pkg-format" f.log || fail "F: no warning for the earlier spelling" f.log +echo "ok: --format is the earlier spelling, with a warning" + +# G +mkdir -p share/data +printf 'kept\n' > share/data/keep.txt +printf 'dropped\n' > share/data/drop.tmp +cat >> mcpp.toml <<'TOML' +include = ["share/**"] +exclude = ["share/**/*.tmp"] +TOML +"$MCPP" pack --pkg-format dir -o inc > g.log 2>&1 || fail "G: pack with [pack] include" g.log +[ -f target/dist/inc/share/data/keep.txt ] || fail "G: the included file is not staged" g.log +[ -f target/dist/inc/share/data/drop.tmp ] && fail "G: the excluded file was staged" g.log +python3 - <<'PY' || fail "G: the metadata" target/dist/inc/mcpp-package.json +import json +d = json.load(open("target/dist/inc/mcpp-package.json")) +roles = {f["path"]: f["role"] for f in d["files"]} +assert roles.get("share/data/keep.txt") == "resource", roles +PY +echo "ok: [pack] include stages what it names, less what exclude names" + +# H +cat >> mcpp.toml <<'TOML' +sign = ["sh", "-c", "printf SIGNED >> \"$0\"", "{}"] +TOML +"$MCPP" pack --pkg-format dir -o signed > h.log 2>&1 || fail "H: pack with [pack] sign" h.log +python3 - <<'PY' || fail "H: the signed tree" target/dist/signed/mcpp-package.json +import hashlib, json, os +root = "target/dist/signed" +d = json.load(open(os.path.join(root, "mcpp-package.json"))) +programs = [f for f in d["files"] if f["role"] == "program"] +assert programs, d["files"] +for f in programs: + data = open(os.path.join(root, f["path"]), "rb").read() + assert data.endswith(b"SIGNED"), f["path"] + " was not signed" + assert hashlib.sha256(data).hexdigest() == f["sha256"], f["path"] + ": the digest is the unsigned file's" +for f in d["files"]: + if f["role"] in ("resource", "metadata"): + assert not open(os.path.join(root, f["path"]), "rb").read().endswith(b"SIGNED"), f["path"] +PY +echo "ok: [pack] sign signs each program, and the digests are the signed files'" +"$MCPP" pack --pkg-format dir -o axes-0.1.0-named > h2.log 2>&1 || fail "H: pack -o with dots" h2.log +[ -d target/dist/axes-0.1.0-named ] || fail "H: -o axes-0.1.0-named did not name the directory" h2.log +echo "ok: a directory is named as -o writes it" + +echo "PASS: 954 pack has three axes" diff --git a/tests/e2e/955_a_universal_package_holds_every_architecture.sh b/tests/e2e/955_a_universal_package_holds_every_architecture.sh new file mode 100755 index 000000000..d8cf44513 --- /dev/null +++ b/tests/e2e/955_a_universal_package_holds_every_architecture.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# requires: llvm python3 unix-shell +# 955 -- a universal package (SPEC-014 §3, §4.2): the platform of the ok-image +# targets states `universal = true`, so `mcpp pack` of either target packs both +# into one package, named after the platform, with a launcher that picks this +# machine's architecture and starts the platform's runner. +# +# Criteria: +# A. `--openkal/` holds `lib/x86_64/` and `lib/aarch64/` (each +# the image of its architecture), `bin/` and `bin/.cmd`, and +# `mcpp-package.json` naming both legs, the runner and each entry. +# B. `bin/` runs the image of this machine's architecture through the +# runner found on PATH, with its arguments. +# C. `--mode self-contained` adds the runner's program for this system and +# each architecture under `libexec//-/`, and the launcher +# runs with no runner on PATH. +# D. `--no-universal` packs the selected target alone. +# E. (Linux) `--systems linux,macos,windows` carries the runner of each +# system this mcpp builds one for, made here over the platform's hosted +# arrangement: a Mach-O for macos-aarch64, a PE for windows-x86_64, and +# none of them with the runner's own package description 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; } + +case "$(uname -m)" in + x86_64|amd64) HOST_ARCH=x86_64; OTHER=aarch64 ;; + aarch64|arm64) HOST_ARCH=aarch64; OTHER=x86_64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac +case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) echo "SKIP: a POSIX launcher"; exit 0 ;; esac +T="$HOST_ARCH-openkal" + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +printf '[package]\nname = "uni"\nversion = "0.1.0"\n' > mcpp.toml +cat > src/main.cpp <<'CPP' +import std; +int main(int argc, char** argv) { + try { throw std::runtime_error("universal"); } + catch (const std::exception& e) { std::println("{} {} args", e.what(), argc); } + return 0; +} +CPP + +# A +"$MCPP" pack --target "$T" --pkg-format dir > a.log 2>&1 || fail "A: pack" a.log +P=target/dist/uni-0.1.0-openkal +for f in "lib/x86_64/uni.oke" "lib/aarch64/uni.oke" "bin/uni" "bin/uni.cmd" "mcpp-package.json"; do + [ -f "$P/$f" ] || fail "A: the package has no $f" a.log +done +python3 - "$P" <<'PY' || fail "A: the package's metadata" "$P/mcpp-package.json" +import json, sys +root = sys.argv[1] +d = json.load(open(root + "/mcpp-package.json")) +assert sorted(d["legs"]) == ["aarch64-openkal", "x86_64-openkal"], d["legs"] +assert d["launch"]["runner"][0].startswith("okrun"), d["launch"] +assert d["launch"]["entry"] == {"x86_64": "lib/x86_64/uni.oke", "aarch64": "lib/aarch64/uni.oke"}, d["launch"] +# Each image is of its architecture (e_machine 62 x86-64, 183 AArch64). +import struct +for arch, machine in (("x86_64", 62), ("aarch64", 183)): + head = open(f"{root}/lib/{arch}/uni.oke", "rb").read(20) + assert head[:4] == b"\x7fELF" and struct.unpack_from("/dev/null \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["runner"]["program"])') +[ -x "$okrun" ] || fail "B: the platform's runner was not located ($okrun)" +PATH="$(dirname "$okrun"):$PATH" "$P/bin/uni" one two > b.log 2>&1 || fail "B: the launcher" b.log +grep -q "universal 3 args" b.log || fail "B: the image's output through the launcher" b.log +echo "ok: the launcher runs this machine's image through the runner" + +# C +"$MCPP" pack --target "$T" --mode self-contained --pkg-format dir -o sc > c.log 2>&1 || fail "C: pack --mode self-contained" c.log +# This machine's architecture's runner is always there; the other one is there +# where this mcpp builds programs for it (not x86_64-macos, a planned row). +[ -f "target/dist/sc/libexec/okrun/$OS-$HOST_ARCH/okrun" ] || fail "C: no runner for $OS-$HOST_ARCH" c.log +if [ ! -f "target/dist/sc/libexec/okrun/$OS-$OTHER/okrun" ]; then + grep -q "no runner for $OS-$OTHER" c.log || fail "C: the runner for $OS-$OTHER is missing and not said" c.log +fi +env -i HOME="$HOME" PATH=/usr/bin:/bin target/dist/sc/bin/uni x > c-run.log 2>&1 || fail "C: the self-contained launcher" c-run.log +grep -q "universal 2 args" c-run.log || fail "C: the self-contained package's output" c-run.log +python3 - < d.log 2>&1 || fail "D: --no-universal" d.log +[ -f target/dist/one/bin/uni.oke ] || fail "D: --no-universal did not pack the target alone" d.log +[ -d target/dist/one/lib/$OTHER ] && fail "D: --no-universal packed the other architecture" d.log +echo "ok: --no-universal packs the selected target alone" + +# E +if [ "$OS" = linux ]; then + "$MCPP" pack --target "$T" --mode self-contained --systems linux,macos,windows --pkg-format dir -o uni-0.1.0-all \ + > e.log 2>&1 || fail "E: --systems linux,macos,windows" e.log + python3 - target/dist/uni-0.1.0-all <<'PY' || fail "E: the runners" e.log +import os, sys +root = os.path.join(sys.argv[1], "libexec", "okrun") +def head(p): return open(p, "rb").read(4) +assert head(os.path.join(root, "macos-aarch64", "okrun")) == b"\xcf\xfa\xed\xfe", "macos-aarch64 is not a Mach-O" +assert head(os.path.join(root, "windows-x86_64", "okrun.exe"))[:2] == b"MZ", "windows-x86_64 is not a PE" +for d, _, files in os.walk(root): + assert "mcpp-package.json" not in files, d +PY + grep -q "no runner for macos-x86_64" e.log || fail "E: the planned row is not said" e.log + echo "ok: the runners of other systems are made here, over the hosted arrangement" +fi + +echo "PASS: 955 a universal package holds every architecture" diff --git a/tests/e2e/956_a_single_file_runs_without_unpacking.sh b/tests/e2e/956_a_single_file_runs_without_unpacking.sh new file mode 100755 index 000000000..d27706ad2 --- /dev/null +++ b/tests/e2e/956_a_single_file_runs_without_unpacking.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# requires: unix-shell python3 +# 956 -- a single file (SPEC-014 §5.2): `--pkg-format single-file` writes one +# executable. A native program's file is the engine's launcher carrying the +# staged tree as a zstd frame, extracted once into the user's cache under the +# payload's sha256; an image's file is the platform's carrier, which runs the +# image from memory. +# +# Criteria: +# A. The native file runs the program with its arguments and exit status; +# the first run extracts the tree once, a second run extracts nothing. +# B. On Linux, `--linkage static` makes a file with no interpreter. +# C. Refused before a build: `--compress xz` for a single file. +# D. (llvm, an ok-image row) An image's single file runs, and writes nothing +# into the cache. +# E. (an ok-image row) `-o` with several `--systems` is refused before a +# build: one file is made per system; so is a system whose row this mcpp +# builds no program for (`x86_64-macos`, a planned row). +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; } +# The user's cache, isolated: `$XDG_CACHE_HOME` on Linux; on macOS the cache is +# `~/Library/Caches`, so the files run with a home of their own (mcpp keeps +# the real one). +if [ "$(uname -s)" = Darwin ]; then + CACHE="$TMP/home/Library/Caches" + mkdir -p "$CACHE" + run_file() { HOME="$TMP/home" "$@"; } +else + export XDG_CACHE_HOME="$TMP/cache" + CACHE="$XDG_CACHE_HOME" + run_file() { "$@"; } +fi + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +printf '[package]\nname = "one"\nversion = "0.1.0"\n' > mcpp.toml +cat > src/main.cpp <<'CPP' +#include +int main(int argc, char** argv) { std::printf("one file: %d args, last %s\n", argc, argv[argc - 1]); return 3; } +CPP + +# A +"$MCPP" pack --pkg-format single-file > a.log 2>&1 || fail "A: pack" a.log +f=$(python3 - <<'PY' +import glob, os +c = [p for p in glob.glob("target/dist/one-0.1.0-*") if os.path.isfile(p) and os.access(p, os.X_OK)] +print(c[0] if c else "") +PY +) +[ -n "$f" ] || fail "A: no single file under target/dist" a.log +set +e; run_file "$f" alpha beta > run1.log 2>&1; rc=$?; set -e +[ "$rc" = 3 ] || fail "A: the exit status is $rc, not the program's 3" run1.log +grep -q "one file: 3 args, last beta" run1.log || fail "A: the program's output" run1.log +dirs=$(ls "$CACHE/mcpp/single-file" | grep -v '^\.' | wc -l | tr -d ' ') +[ "$dirs" = 1 ] || fail "A: the first run left $dirs trees in the cache" +stamp=$(ls --full-time "$CACHE/mcpp/single-file"/*/.mcpp-complete 2>/dev/null || stat "$CACHE"/mcpp/single-file/*/.mcpp-complete) +set +e; run_file "$f" gamma > run2.log 2>&1; set -e +grep -q "last gamma" run2.log || fail "A: the second run" run2.log +stamp2=$(ls --full-time "$CACHE/mcpp/single-file"/*/.mcpp-complete 2>/dev/null || stat "$CACHE"/mcpp/single-file/*/.mcpp-complete) +[ "$stamp" = "$stamp2" ] || fail "A: the second run extracted the tree again" +echo "ok: a native single file runs, extracting once" + +# B +if [ "$(uname -s)" = Linux ]; then + "$MCPP" pack --pkg-format single-file --linkage static -o one-static > b.log 2>&1 || fail "B: --linkage static" b.log + python3 - target/dist/one-static <<'PY' || fail "B: the static file has an interpreter" b.log +import struct, sys +data = open(sys.argv[1], "rb").read() +assert data[:4] == b"\x7fELF" +phoff, = struct.unpack_from(" b-run.log 2>&1; set -e + grep -q "last z" b-run.log || fail "B: the static single file" b-run.log + echo "ok: --linkage static makes a file with no interpreter" +fi + +# C +if "$MCPP" pack --pkg-format single-file --compress xz > c.log 2>&1; then fail "C: xz was accepted" c.log; fi +grep -q "zstd" c.log || fail "C: the refusal does not name the codec" c.log +echo "ok: a single file's payload is zstd" + +# D +case "$(uname -m)" in + x86_64|amd64) T=x86_64-openkal ;; + aarch64|arm64) T=aarch64-openkal ;; + *) T= ;; +esac +if [ -n "$T" ] && command -v python3 > /dev/null && "$MCPP" toolchain list --format json 2>/dev/null | grep -q llvm; then + mkdir -p "$TMP/img/src" + cd "$TMP/img" + printf '[package]\nname = "img"\nversion = "0.1.0"\n' > mcpp.toml + printf 'import std;\nint main(int argc, char**) { std::println("image file: {} args", argc); }\n' > src/main.cpp + "$MCPP" pack --target "$T" --pkg-format single-file > d.log 2>&1 || fail "D: pack" d.log + g=$(ls target/dist/img-0.1.0-* | head -1) + before=$(find "$CACHE" -type f | wc -l) + run_file "$g" x y > d-run.log 2>&1 || fail "D: the image's single file" d-run.log + grep -q "image file: 3 args" d-run.log || fail "D: the output" d-run.log + after=$(find "$CACHE" -type f | wc -l) + [ "$before" = "$after" ] || fail "D: the image's single file wrote into the cache" + echo "ok: an image's single file runs from memory" + + # E + if "$MCPP" pack --target "$T" --pkg-format single-file --systems linux,macos -o one-file > e.log 2>&1; then + fail "E: -o with two systems was accepted" e.log + fi + grep -q "one single file per system" e.log || fail "E: the refusal" e.log + grep -q "Compiling" e.log && fail "E: refused after compiling" e.log + if "$MCPP" pack --target x86_64-openkal --pkg-format single-file --systems linux,macos > e2.log 2>&1; then + fail "E: a single file for x86_64-macos was accepted" e2.log + fi + grep -q "x86_64-macos" e2.log || fail "E: the refusal does not name the row" e2.log + grep -q -- "--systems linux" e2.log || fail "E: the refusal does not give the systems that are built" e2.log + echo "ok: -o with several systems, and a system of an unbuilt row, are refused before a build" +fi + +echo "PASS: 956 a single file runs without unpacking" diff --git a/tests/e2e/957_packages_merge_and_describe_themselves.sh b/tests/e2e/957_packages_merge_and_describe_themselves.sh new file mode 100755 index 000000000..275d63262 --- /dev/null +++ b/tests/e2e/957_packages_merge_and_describe_themselves.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# requires: llvm python3 unix-shell +# 957 -- packages written on several machines merge into one (SPEC-014 §4.6), +# and a package describes its own distribution entry (§8). +# +# A self-contained universal package carries the runner of the systems it was +# packed for. One packed on Linux and one packed on macOS hold the same images +# and different runners; `--merge` makes the package for both. Here the second +# is the first with its runner moved to another system's directory, which is +# what a package packed there holds. +# +# Criteria: +# A. `--merge a --merge b` writes one package whose legs and systems are the +# union, every file of both, and a `mcpp-package.json` that describes it. +# B. A file both packages hold with different digests is refused, naming the +# file and both digests; packages of different versions are refused. +# C. `mcpp emit xpkg --package` of a universal package is one asset for +# linux, macosx and windows, with the runner as a dependency when the +# package does not carry it. +# D. An archive merges as the directory it holds does. +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) T=x86_64-openkal ;; + aarch64|arm64) T=aarch64-openkal ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +printf '[package]\nname = "mrg"\nversion = "0.1.0"\n' > mcpp.toml +printf 'import std;\nint main() { std::println("merged"); }\n' > src/main.cpp +"$MCPP" pack --target "$T" --mode self-contained --pkg-format dir -o here > pack.log 2>&1 || fail "pack" pack.log + +# The same package as another system packs it: its runner in that system's +# directory, and the metadata saying so. +python3 - target/dist/here "$TMP/there" <<'PY' || fail "could not shape the second package" +import json, os, shutil, sys +src, dst = sys.argv[1], sys.argv[2] +shutil.copytree(src, dst, symlinks=True) +d = json.load(open(os.path.join(dst, "mcpp-package.json"))) +here = os.path.join(dst, "libexec", "okrun") +moved = {} +for sysdir in os.listdir(here): + other = sysdir.replace("linux-", "other-").replace("macos-", "other-") + os.rename(os.path.join(here, sysdir), os.path.join(here, other)) + moved[sysdir] = other +for f in d["files"]: + for a, b in moved.items(): + f["path"] = f["path"].replace(f"libexec/okrun/{a}/", f"libexec/okrun/{b}/") +d["systems"] = sorted(moved.values()) +json.dump(d, open(os.path.join(dst, "mcpp-package.json"), "w"), indent=2) +PY + +# A +"$MCPP" pack --merge target/dist/here --merge "$TMP/there" -o "$TMP/merged.zip" > a.log 2>&1 || fail "A: --merge" a.log +python3 - "$TMP/merged.zip" <<'PY' || fail "A: the merged package" a.log +import hashlib, json, sys, zipfile +z = zipfile.ZipFile(sys.argv[1]) +names = z.namelist() +top = names[0].split("/")[0] +d = json.loads(z.read(f"{top}/mcpp-package.json")) +assert len(d["systems"]) == 4 and any(s.startswith("other-") for s in d["systems"]), d["systems"] +assert sorted(d["legs"]) == ["aarch64-openkal", "x86_64-openkal"], d["legs"] +for f in d["files"]: + data = z.read(f"{top}/{f['path']}") + assert hashlib.sha256(data).hexdigest() == f["sha256"], f["path"] +PY +echo "ok: --merge writes the union of the packages" + +# D (before B changes the second package) +python3 - "$TMP/there" "$TMP/there.tar.gz" <<'PY' +import sys, tarfile +with tarfile.open(sys.argv[2], "w:gz") as t: + t.add(sys.argv[1], arcname="mrg-0.1.0-openkal") +PY +"$MCPP" pack --merge target/dist/here --merge "$TMP/there.tar.gz" --pkg-format dir -o "$TMP/merged-dir" > d.log 2>&1 \ + || fail "D: --merge of a directory and an archive" d.log +python3 - "$TMP/merged-dir" <<'PY' || fail "D: the merged tree" d.log +import hashlib, json, os, sys +root = sys.argv[1] +d = json.load(open(os.path.join(root, "mcpp-package.json"))) +assert len(d["systems"]) == 4, d["systems"] +for f in d["files"]: + assert hashlib.sha256(open(os.path.join(root, f["path"]), "rb").read()).hexdigest() == f["sha256"], f["path"] +PY +echo "ok: an archive merges as its directory does" + +# B +python3 - "$TMP/there" <<'PY' +import hashlib, json, os, sys +root = sys.argv[1] +img = os.path.join(root, "lib", "x86_64", "mrg.oke") +open(img, "ab").write(b"\0") +d = json.load(open(os.path.join(root, "mcpp-package.json"))) +for f in d["files"]: + if f["path"] == "lib/x86_64/mrg.oke": + f["sha256"] = hashlib.sha256(open(img, "rb").read()).hexdigest() +json.dump(d, open(os.path.join(root, "mcpp-package.json"), "w"), indent=2) +PY +if "$MCPP" pack --merge target/dist/here --merge "$TMP/there" -o "$TMP/bad.zip" > b.log 2>&1; then + fail "B: two digests of one image were merged" b.log +fi +grep -q "lib/x86_64/mrg.oke" b.log || fail "B: the refusal does not name the file" b.log +python3 - "$TMP/there" <<'PY' +import json, os, sys +p = os.path.join(sys.argv[1], "mcpp-package.json") +d = json.load(open(p)); d["version"] = "0.2.0"; json.dump(d, open(p, "w")) +PY +if "$MCPP" pack --merge target/dist/here --merge "$TMP/there" -o "$TMP/bad2.zip" > b2.log 2>&1; then + fail "B: two versions were merged" b2.log +fi +grep -q "version" b2.log || fail "B: the refusal does not name the version" b2.log +echo "ok: --merge refuses what is not one package" + +# C +"$MCPP" pack --target "$T" --pkg-format zip -o mrg-0.1.0-openkal.zip > c.log 2>&1 || fail "C: pack" c.log +"$MCPP" emit xpkg --package target/dist/mrg-0.1.0-openkal.zip --url https://example.org/v0.1.0 > xpkg.lua 2> c.err \ + || fail "C: emit xpkg" c.err +python3 - xpkg.lua <<'PY' || fail "C: the entry" xpkg.lua +import hashlib, re, sys +text = open(sys.argv[1]).read() +sha = hashlib.sha256(open("target/dist/mrg-0.1.0-openkal.zip", "rb").read()).hexdigest() +for system in ("linux", "macosx", "windows"): + block = re.search(system + r" = \{(.*?)\n \},", text, re.S) + assert block, system + b = block.group(1) + assert "https://example.org/v0.1.0/mrg-0.1.0-openkal.zip" in b and sha in b, (system, b) + assert 'deps = { "okrun" }' in b, (system, b) +assert 'xvm.add("mrg"' in text +PY +echo "ok: a universal package is one asset for every system" + +echo "PASS: 957 packages merge and describe themselves" diff --git a/tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh b/tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh new file mode 100755 index 000000000..fb14b4339 --- /dev/null +++ b/tests/e2e/958_a_build_program_runs_again_when_its_output_is_gone.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# requires: gcc +# 958 -- a build program's cached run is replayed only while what it wrote is +# there. The program writes a header below `MCPP_OUT_DIR` and names the +# directory with `mcpp::include_dir`; once that directory is deleted, a replay +# would hand the compile an include directory with nothing in it. The program +# runs again instead (measured 2026-10-28 on openkal-image's build program: a +# deleted output directory, then generated includes missing). +# +# Criteria: +# A. A second build replays the program (cached) and compiles nothing. +# B. With the program's output deleted, the build runs the program again and +# the unit finds its header. +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; } + +mkdir -p "$TMP/app/src" +cd "$TMP/app" +cat > build.mcpp <<'CPP' +#include +#include +#include +import mcpp; +int main() { + const std::string inc = std::string(mcpp::out_dir()) + "/inc"; + std::filesystem::create_directories(inc); + std::ofstream(inc + "/generated.h") << "#define GENERATED 7\n"; + mcpp::include_dir(inc.c_str()); + return 0; +} +CPP +printf '#include \n#include \nint main() { std::printf("generated %%d\\n", GENERATED); return 0; }\n' > src/main.cpp +printf '[package]\nname = "regen"\nversion = "0.1.0"\n' > mcpp.toml + +"$MCPP" run > a1.log 2>&1 || fail "A: the first build" a1.log +grep -q "generated 7" a1.log || fail "A: the header did not arrive" a1.log +"$MCPP" build -v > a2.log 2>&1 || fail "A: the second build" a2.log +grep -q "Compiling regen" a2.log && fail "A: the second build compiled" a2.log +echo "ok: a second build replays the program" + +# B +out=$(find target -type f -name generated.h | head -1) +[ -n "$out" ] || fail "B: the generated header is not under target/" +rm -rf "$(dirname "$out")" +rm -rf target/*/*/bin target/*/*/obj +"$MCPP" run -v > b.log 2>&1 || fail "B: the build after the output was deleted" b.log +grep -q "generated 7" b.log || fail "B: the program did not run again" b.log +echo "ok: the program runs again when its output is gone" + +echo "PASS: 958 a build program runs again when its output is gone" diff --git a/tests/e2e/timings/linux.tsv b/tests/e2e/timings/linux.tsv index 770bdb79a..fbbc90fea 100644 --- a/tests/e2e/timings/linux.tsv +++ b/tests/e2e/timings/linux.tsv @@ -1,515 +1,589 @@ -00_fixture_path_hygiene.sh 2543 -01_help_and_version.sh 12 -02_new_build_run.sh 5529 -03_multi_module.sh 5933 -04_incremental.sh 3342 -05_errors.sh 1223 -06_emit_xpkg.sh 15 -07_static_library.sh 133 -08_shared_library.sh 189 -09_path_dependency.sh 2437 -100_cppfly_reflection.sh 12254 -100_feature_sources_test_mode.sh 3887 -101_cppfly_llvm_soft.sh 3836 -103_target_vocabulary.sh 11073 -104_asm_sources_gas.sh 2741 -105_asm_sources_nasm.sh 8738 -106_feature_gated_sources_toml.sh 6781 -107_generated_files_toml.sh 1598 -108_cfg_conditional_sources.sh 1198 -109_per_glob_flags.sh 1218 -10_env_command.sh 5999 -110_build_mcpp_env_contract.sh 8839 -111_dep_build_mcpp.sh 2142 -112_build_mcpp_cross.sh 7194 -113_scanner_mcpp_dir_prune.sh 179 -114_run_scan_scope.sh 1563 -115_manifest_syntax_aot_brace.sh 1308 -116_include_family_and_define_spaces.sh 1322 -117_object_path_collision.sh 1311 -118_purview_include_rebuild.sh 8825 -119_dep_cfg_sources.sh 1351 -11_index_list.sh 7507 -120_ws_root_indices.sh 722 -121_default_ns_redirect.sh 491 -122_run_member.sh 2372 -123_same_named_main_across_dep.sh 1290 -124_gen_source_out_of_root_object_path.sh 1698 -125_build_mcpp_dep_dir.sh 1814 -126_default_features_opt_out.sh 391 -127_transitive_default_features_optout.sh 2577 -128_feature_forwarding.sh 279 -12_add_command.sh 450 -134_target_sysroot_and_queries.sh 4462 -135_diagnostic_promise_resolves.sh 7996 -136_aarch64_zero_libc_target.sh 577 -137_x86_64_zero_libc_target.sh 539 -138_feature_sources_gate_vs_provider.sh 601 -139_build_program_advisory.sh 637 -13_toolchain_pin.sh 8 -141_include_dirs_after.sh 1319 -142_generated_files_incremental.sh 2431 -143_build_mcpp_source_directive.sh 2716 -144_build_mcpp_include_dir.sh 3689 -145_root_build_mcpp_dep_dirs.sh 1621 -146_feature_flags.sh 8190 -147_per_os_features_xpkg.sh 504 -148_wide_include_list.sh 1408 -149_conditional_glob_flags.sh 1264 -14_toolchain_fallback.sh 7356 -150_clang_module_operator_template.sh 207 -151_index_url_artifact_migration.sh 5984 -152_test_subdir_names.sh 141 -153_test_isolation.sh 222 -154_test_filter.sh 150 -155_test_json.sh 188 -156_nested_ld_library_path.sh 124 -157_test_glob_flags.sh 140 -158_test_signal_and_duration.sh 308 -159_test_list.sh 24 -15_test_passing.sh 2176 -160_test_timeout.sh 4651 +00_fixture_path_hygiene.sh 2480 +01_help_and_version.sh 11 +02_new_build_run.sh 1310 +03_multi_module.sh 1960 +04_incremental.sh 3680 +05_errors.sh 1430 +06_emit_xpkg.sh 17 +07_static_library.sh 145 +08_shared_library.sh 288 +09_path_dependency.sh 2410 +100_cppfly_reflection.sh 8130 +100_feature_sources_test_mode.sh 3330 +101_cppfly_llvm_soft.sh 3210 +103_target_vocabulary.sh 5780 +104_asm_sources_gas.sh 2780 +105_asm_sources_nasm.sh 6700 +106_feature_gated_sources_toml.sh 7850 +107_generated_files_toml.sh 1610 +108_cfg_conditional_sources.sh 1110 +109_per_glob_flags.sh 1300 +10_env_command.sh 94 +110_build_mcpp_env_contract.sh 6020 +111_dep_build_mcpp.sh 2350 +112_build_mcpp_cross.sh 6820 +113_scanner_mcpp_dir_prune.sh 151 +114_run_scan_scope.sh 1550 +115_manifest_syntax_aot_brace.sh 1120 +116_include_family_and_define_spaces.sh 1480 +117_object_path_collision.sh 1290 +118_purview_include_rebuild.sh 7220 +119_dep_cfg_sources.sh 1230 +11_index_list.sh 9700 +120_ws_root_indices.sh 680 +121_default_ns_redirect.sh 619 +122_run_member.sh 1700 +123_same_named_main_across_dep.sh 939 +124_gen_source_out_of_root_object_path.sh 2420 +125_build_mcpp_dep_dir.sh 1340 +126_default_features_opt_out.sh 496 +127_transitive_default_features_optout.sh 2310 +128_feature_forwarding.sh 507 +12_add_command.sh 558 +134_target_sysroot_and_queries.sh 5130 +135_diagnostic_promise_resolves.sh 5640 +136_aarch64_zero_libc_target.sh 1150 +137_x86_64_zero_libc_target.sh 1390 +138_feature_sources_gate_vs_provider.sh 640 +139_build_program_advisory.sh 925 +13_toolchain_pin.sh 5 +141_include_dirs_after.sh 1030 +142_generated_files_incremental.sh 2470 +143_build_mcpp_source_directive.sh 2420 +144_build_mcpp_include_dir.sh 3840 +145_root_build_mcpp_dep_dirs.sh 1740 +146_feature_flags.sh 9390 +147_per_os_features_xpkg.sh 540 +148_wide_include_list.sh 1230 +149_conditional_glob_flags.sh 1370 +14_toolchain_fallback.sh 8130 +150_clang_module_operator_template.sh 390 +151_index_url_artifact_migration.sh 107 +152_test_subdir_names.sh 147 +153_test_isolation.sh 377 +154_test_filter.sh 173 +155_test_json.sh 202 +156_nested_ld_library_path.sh 112 +157_test_glob_flags.sh 136 +158_test_signal_and_duration.sh 277 +159_test_list.sh 17 +15_test_passing.sh 2160 +160_test_timeout.sh 4650 161_xpkg_name_form.sh 63 -162_bare_name_namespace_scope.sh 5879 -163_identity_first_resolution.sh 25198 -164_ninja_goal_command_length.sh 529 -165_bare_name_cross_namespace_wire_address.sh 6126 -166_run_env_no_private_glibc.sh 353 -167_build_defines_module_scan.sh 813 -168_build_mcpp_musl_host_static.sh 5623 -169_semver_project_index.sh 511 -16_test_failing.sh 2361 -170_bmi_staging_no_cascade.sh 1320 -171_bmi_staging_locked_dest.sh 2585 -172_build_cache_cross_project.sh 9478 -173_build_profile_isolation.sh 1114 -173_index_refresh_policy.sh 791 -174_cache_modes_and_commands.sh 17259 -175_cpp20_import_std.sh 7801 -176_cpp20_std_bmi_isolation.sh 8248 -178_test_observability.sh 10866 -179_spaced_paths.sh 1435 -17_test_no_tests.sh 16 -181_build_mcpp_import_std.sh 5332 -183_cross_no_relink.sh 1167 -183_cxx_runtime_contract.sh 4291 -184_build_cache_object_layout.sh 12353 -185_index_floor_degrades.sh 5647 -186_build_mcpp_protocol_and_bound.sh 7594 -187_dep_host_tool.sh 3472 -188_build_actions.sh 8591 -189_host_module_rules.sh 1893 -18_devdeps_isolation.sh 1181 -190_link_rspfile_newlines.sh 1278 -191_link_scale.sh 20371 -192_install_path_namespace.sh 3686 -193_provision_reexport.sh 8603 -194_build_mcpp_glob_input.sh 2471 -195_target_cfg_feature_deps.sh 410 -196_version_identity_and_lock.sh 15118 -198_windows_resources_cross.sh 3729 -199_resources_validation.sh 222 -19_bmi_cache_reuse.sh 6699 -200_subos_env_reaches_program.sh 1778 -201_gcc_no_specs_pollution.sh 8564 -202_machine_output_contract.sh 2576 -203_exact_selector_lock_migration.sh 358 -204_new_transactional_scaffold.sh 9698 -205_root_local_subos.sh 732 -206_runtime_binding_physics.sh 1177 -207_runtime_contract_provenance.sh 200 -208_private_libc_stays_in_the_binary.sh 314 -209_runtime_verdict_never_reruns_green.sh 723 -20_p1689_scanner.sh 4818 -210_local_index_addressing_on_every_host.sh 248 -211_configure_only_cdb.sh 486 -212_cached_dep_std_is_ordered.sh 5431 -213_build_after_test_is_not_the_test_graph.sh 396 -214_executable_carries_dt_rpath.sh 1506 -215_pack_has_no_build_machine_paths.sh 3483 -216_selfcontained_refuses_host_capability.sh 545 -217_module_extensions.sh 9988 -218_module_extensions_graph_shape.sh 5117 -219_runtime_search_farm_is_last.sh 345 -21_ninja_dyndep.sh 4670 -220_farm_only_library_runs.sh 880 -221_subos_without_info_still_builds.sh 157 -222_shared_library_cxx_runtime_contract.sh 2362 -22_doctor_cache_publish.sh 47292 -230_bench_harness.sh 55578 -231_jobs_option.sh 2197 -232_workflow_syntax.sh 325 -233_bench_matrix.sh 224 -234_bmi_schedule_on.sh 6695 -235_std_object_only_when_needed.sh 1071 -236_module_extensions_default_toolchain.sh 3494 -237_default_subos_without_info.sh 287 -238_c_only_unit_links_with_c_driver.sh 3775 -23_remove_update.sh 12842 -240_pack_pe_zip_cross.sh 414 -242_pack_library_interface_and_headers.sh 779 -243_pack_library_interface_closure.sh 681 -244_pack_library_gate.sh 536 -245_pack_library_fat_target_selection.sh 763 -246_explicit_empty_sources.sh 261 -247_bare_triple_conditional_native.sh 389 -248_pack_library_fat_pe_leg.sh 513 -249_pack_workspace_root_unchanged.sh 217 -24_git_dependency.sh 10859 -250_pack_names_the_target.sh 261 -251_pack_library_shared.sh 375 -252_pack_library_old_client.sh 308 -253_pack_library_undetermined_partition.sh 532 -254_pack_library_platform_coverage.sh 503 -25_convention_mode.sh 4566 -260_module_extension_is_configuration.sh 198 -261_pack_follows_the_module_extension.sh 465 -263_lib_root_follows_the_extension.sh 1252 -264_pack_library_is_relocatable.sh 424 -265_pack_strips_but_stays_usable.sh 916 -267_target_requested_is_empty_for_a_native_build.sh 744 -268_target_side_resolution.sh 242 -269_openkal_llvm_spelling_still_resolves.sh 485 -26_c_language_support.sh 1293 -26_toolchain_management.sh 40215 -27_namespace_dependencies.sh 8849 -27_self_contained_home.sh 16869 -280_target_side_layers.sh 2345 -281_target_side_rules.sh 223 -282_target_is_a_request.sh 67 -283_run_target_flag_owns_its_cache_slot.sh 323 -284_env_segment_is_optional_everywhere.sh 3132 -285_kernel_abi_from_graph_keeps_the_payload_c_library.sh 4622 -286_the_openkal_stack_still_builds.sh 40171 -287_the_openkal_stack_crosses_to_aarch64.sh 38940 -289_one_host_reaches_every_openkal_target.sh 77137 -28_target_static.sh 4 -290_the_declaration_puts_an_environment_in_front.sh 1998 -291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh 30147 -292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh 355 -293_the_requested_target_and_the_resolved_one_name_one_os.sh 9515 -294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh 23 -295_naming_the_host_target_changes_nothing.sh 1370 -296_what_the_report_names_is_what_the_link_line_uses.sh 26315 -297_a_capability_pin_is_not_a_preference.sh 251 -298_overriding_a_convention_requires_replacing_it.sh 28550 -299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh 9159 -29_toolchain_partial_versions.sh 5553 -300_a_registered_family_is_not_reported_unknown.sh 43 -301_the_graphs_compiler_is_taken_and_nothing_is_written.sh 298 -302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh 85 -303_the_query_gives_one_answer_for_the_c_library.sh 107 -304_the_target_side_reaches_every_unit.sh 188 -305_built_from_is_not_published.sh 228 -306_dependency_linkage_axis.sh 376 -307_symbol_provision.sh 551 -308_dependency_required_features.sh 213 -309_host_module_identity.sh 987 -30_dev_binary_home.sh 20 -30_pack_modes.sh 5699 -310_build_dependencies.sh 749 -311_rule_build_dependencies.sh 3019 -312_build_rules_example.sh 3353 -313_check_stamp_on_every_platform.sh 639 -314_dependency_action_generated_header.sh 2676 -315_blocking_check_gates_compilation.sh 4923 -316_link_unit_with_no_inputs_is_refused.sh 144 -317_project_build_hooks.sh 18666 -31_transitive_deps.sh 142 -321_workspace_inheritance.sh 687 -322_dialect_flag_refusal.sh 10582 -323_post_link_record_durability.sh 422 -324_fast_path_sees_path_deps.sh 3532 -325_system_toolchain_refused.sh 222 -326_dependency_standard_floor.sh 740 -327_declared_xlings_deps_report_their_own_failure.sh 7813 -328_layer_predicates_apply_and_unknown_ones_say_so.sh 808 -329_exit_codes_match_the_contract.sh 20 -32_semver_merge.sh 9628 -330_runner_hosted_targets.sh 1428 -331_xlings_workspace_one_table.sh 1855 -333_named_runners_locked_and_sbom.sh 7286 -334_dependency_declared_tool_is_found.sh 251 -335_xlings_tool_tiers.sh 37 -337_run_takes_features_and_profile.sh 961 -339_run_reports_the_program_status.sh 956 -33_multi_version_mangling.sh 15107 -340_no_orphan_survives_a_killed_mcpp.sh 11268 -35_workspace.sh 1807 -36_llvm_toolchain.sh 1246 -37_llvm_import_std.sh 3445 -38_llvm_modules.sh 4184 -38_self_config_mirror.sh 258 -39_llvm_incremental.sh 5127 -39_xlings_index_migration.sh 220 -40_llvm_bmi_cache.sh 5557 -41_llvm_std_compat.sh 2913 -42_custom_local_index.sh 8323 -43_indices_lockfile.sh 8452 -44_indices_e2e_integration.sh 7633 -45_install_platform_mapping.sh 12 -46_self_config_mirror_no_bootstrap.sh 6953 -47_cdb_prebuilt_module_path_abs.sh 1236 -47_llvm_atomic_link.sh 669 -48_build_error_output.sh 296 -49_bmi_cache_nested_custom_index.sh 6825 -50_package_owned_build_flags.sh 182 -51_package_generated_files.sh 351 -52_local_path_namespaced_index.sh 486 -53_namespaced_cache_label.sh 454 -54_package_owned_ldflags.sh 262 -55_dependency_shared_artifact.sh 206 -56_transitive_shared_artifact.sh 237 -57_static_dep_shared_artifact.sh 208 -58_preinstall_mcpp_deps_for_hooks.sh 295 -59_cpp_standard_config.sh 6668 -600_accel_variant_selection.sh 415 -601_exclusive_capability.sh 172 -603_version_floor.sh 303 -604_offline_skips_sandbox_bootstrap.sh 434 -605_probe_channel_and_accel_reach_the_build_program.sh 1349 -606_constrained_source_globs_narrow_to_the_build.sh 1145 -607_chained_actions_form_the_device_link.sh 1132 -608_object_actions_reach_a_static_library.sh 532 -609_clean_stale.sh 4532 -609_shader_sources_are_device_sources.sh 1679 -60_stale_xpkg_cache_reinstall.sh 330 -610_feature_controlled_rule_collection.sh 4584 -611_variant_switch_is_not_replayed.sh 601 -612_build_program_file_input_fast_path.sh 1232 -613_sycl_sources_are_device_sources.sh 1808 -614_feature_xlings_reaches_xpkg_dir.sh 1043 -615_feature_deps_shared_library_is_linked.sh 481 -616_examples_and_their_index_agree.sh 292 -617_build_program_knows_the_cxx_stdlib.sh 566 -618_dependency_feature_tools_reach_its_build_program.sh 45430 -619_a_tool_may_not_be_conditioned_on_a_layer.sh 29 -61_dependency_visibility.sh 476 -620_link_flag_reaches_the_link_line.sh 579 -621_exports_narrows_the_symbol_surface.sh 570 -622_rule_declared_payload_reaches_the_consumers_build_program.sh 50647 -623_device_sources_must_reach_an_action.sh 292 -624_a_missing_host_module_names_the_key.sh 304 +162_bare_name_namespace_scope.sh 769 +163_identity_first_resolution.sh 18500 +164_ninja_goal_command_length.sh 659 +165_bare_name_cross_namespace_wire_address.sh 5590 +166_run_env_no_private_glibc.sh 365 +167_build_defines_module_scan.sh 551 +168_build_mcpp_musl_host_static.sh 4970 +169_semver_project_index.sh 516 +16_test_failing.sh 2470 +170_bmi_staging_no_cascade.sh 1100 +171_bmi_staging_locked_dest.sh 2420 +172_build_cache_cross_project.sh 6820 +173_build_profile_isolation.sh 981 +173_index_refresh_policy.sh 920 +174_cache_modes_and_commands.sh 13830 +175_cpp20_import_std.sh 6700 +176_cpp20_std_bmi_isolation.sh 7490 +178_test_observability.sh 9480 +179_spaced_paths.sh 1390 +17_test_no_tests.sh 14 +181_build_mcpp_import_std.sh 5940 +183_cross_no_relink.sh 1230 +183_cxx_runtime_contract.sh 3570 +184_build_cache_object_layout.sh 11480 +185_index_floor_degrades.sh 5430 +186_build_mcpp_protocol_and_bound.sh 8029 +187_dep_host_tool.sh 4070 +188_build_actions.sh 8710 +189_host_module_rules.sh 2820 +18_devdeps_isolation.sh 1120 +190_link_rspfile_newlines.sh 686 +191_link_scale.sh 16149 +192_install_path_namespace.sh 3370 +193_provision_reexport.sh 9050 +194_build_mcpp_glob_input.sh 1780 +195_target_cfg_feature_deps.sh 437 +196_version_identity_and_lock.sh 13010 +198_windows_resources_cross.sh 3810 +199_resources_validation.sh 509 +19_bmi_cache_reuse.sh 6260 +200_subos_env_reaches_program.sh 1700 +201_gcc_no_specs_pollution.sh 10430 +202_machine_output_contract.sh 7620 +203_exact_selector_lock_migration.sh 482 +204_new_transactional_scaffold.sh 6840 +205_root_local_subos.sh 709 +206_runtime_binding_physics.sh 2850 +207_runtime_contract_provenance.sh 380 +208_private_libc_stays_in_the_binary.sh 246 +209_runtime_verdict_never_reruns_green.sh 4560 +20_p1689_scanner.sh 4420 +210_local_index_addressing_on_every_host.sh 262 +211_configure_only_cdb.sh 612 +212_cached_dep_std_is_ordered.sh 5090 +213_build_after_test_is_not_the_test_graph.sh 451 +214_executable_carries_dt_rpath.sh 1460 +215_pack_has_no_build_machine_paths.sh 2660 +216_selfcontained_refuses_host_capability.sh 521 +217_module_extensions.sh 5440 +218_module_extensions_graph_shape.sh 5000 +219_runtime_search_farm_is_last.sh 320 +21_ninja_dyndep.sh 5310 +220_farm_only_library_runs.sh 908 +221_subos_without_info_still_builds.sh 189 +222_shared_library_cxx_runtime_contract.sh 1810 +22_doctor_cache_publish.sh 4850 +230_bench_harness.sh 53150 +231_jobs_option.sh 2630 +232_workflow_syntax.sh 202 +233_bench_matrix.sh 161 +234_bmi_schedule_on.sh 4950 +235_std_object_only_when_needed.sh 968 +236_module_extensions_default_toolchain.sh 2520 +237_default_subos_without_info.sh 218 +238_c_only_unit_links_with_c_driver.sh 3660 +23_remove_update.sh 16010 +240_pack_pe_zip_cross.sh 379 +242_pack_library_interface_and_headers.sh 923 +243_pack_library_interface_closure.sh 808 +244_pack_library_gate.sh 555 +245_pack_library_fat_target_selection.sh 794 +246_explicit_empty_sources.sh 307 +247_bare_triple_conditional_native.sh 455 +248_pack_library_fat_pe_leg.sh 609 +249_pack_workspace_root_unchanged.sh 186 +24_git_dependency.sh 9150 +250_pack_names_the_target.sh 239 +251_pack_library_shared.sh 597 +252_pack_library_old_client.sh 346 +253_pack_library_undetermined_partition.sh 662 +254_pack_library_platform_coverage.sh 501 +25_convention_mode.sh 4770 +260_module_extension_is_configuration.sh 223 +261_pack_follows_the_module_extension.sh 445 +263_lib_root_follows_the_extension.sh 1470 +264_pack_library_is_relocatable.sh 494 +265_pack_strips_but_stays_usable.sh 832 +267_target_requested_is_empty_for_a_native_build.sh 554 +268_target_side_resolution.sh 312 +269_openkal_llvm_spelling_still_resolves.sh 1080 +26_c_language_support.sh 1290 +26_toolchain_management.sh 28430 +27_namespace_dependencies.sh 8430 +27_self_contained_home.sh 8290 +280_target_side_layers.sh 2750 +281_target_side_rules.sh 294 +282_target_is_a_request.sh 138 +283_run_target_flag_owns_its_cache_slot.sh 361 +284_env_segment_is_optional_everywhere.sh 2690 +285_kernel_abi_from_graph_keeps_the_payload_c_library.sh 4110 +286_the_openkal_stack_still_builds.sh 41110 +287_the_openkal_stack_crosses_to_aarch64.sh 26910 +289_one_host_reaches_every_openkal_target.sh 65430 +28_target_static.sh 3 +290_the_declaration_puts_an_environment_in_front.sh 2000 +291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh 30790 +292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh 1080 +293_the_requested_target_and_the_resolved_one_name_one_os.sh 4430 +294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh 53 +295_naming_the_host_target_changes_nothing.sh 1850 +296_what_the_report_names_is_what_the_link_line_uses.sh 21920 +297_a_capability_pin_is_not_a_preference.sh 414 +298_overriding_a_convention_requires_replacing_it.sh 20860 +299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh 295 +29_toolchain_partial_versions.sh 9020 +300_a_registered_family_is_not_reported_unknown.sh 56 +301_the_graphs_compiler_is_taken_and_nothing_is_written.sh 544 +302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh 131 +303_the_query_gives_one_answer_for_the_c_library.sh 351 +304_the_target_side_reaches_every_unit.sh 167 +305_built_from_is_not_published.sh 224 +306_dependency_linkage_axis.sh 498 +307_symbol_provision.sh 595 +308_dependency_required_features.sh 258 +309_host_module_identity.sh 1610 +30_dev_binary_home.sh 16 +30_pack_modes.sh 5360 +310_build_dependencies.sh 1050 +311_rule_build_dependencies.sh 2400 +312_build_rules_example.sh 2810 +313_check_stamp_on_every_platform.sh 807 +314_dependency_action_generated_header.sh 2710 +315_blocking_check_gates_compilation.sh 4860 +316_link_unit_with_no_inputs_is_refused.sh 225 +317_project_build_hooks.sh 26560 +31_transitive_deps.sh 118 +321_workspace_inheritance.sh 812 +322_dialect_flag_refusal.sh 10330 +323_post_link_record_durability.sh 684 +324_fast_path_sees_path_deps.sh 3510 +325_system_toolchain_refused.sh 193 +326_dependency_standard_floor.sh 782 +327_declared_xlings_deps_report_their_own_failure.sh 4620 +328_layer_predicates_apply_and_unknown_ones_say_so.sh 1190 +329_exit_codes_match_the_contract.sh 25 +32_semver_merge.sh 5420 +330_runner_hosted_targets.sh 1680 +331_xlings_workspace_one_table.sh 1380 +333_named_runners_locked_and_sbom.sh 4290 +334_dependency_declared_tool_is_found.sh 764 +335_xlings_tool_tiers.sh 59 +337_run_takes_features_and_profile.sh 1790 +339_run_reports_the_program_status.sh 1380 +33_multi_version_mangling.sh 10440 +340_no_orphan_survives_a_killed_mcpp.sh 11170 +35_workspace.sh 1930 +36_llvm_toolchain.sh 873 +37_llvm_import_std.sh 2650 +38_llvm_modules.sh 4010 +38_self_config_mirror.sh 20 +39_llvm_incremental.sh 4740 +39_xlings_index_migration.sh 169 +40_llvm_bmi_cache.sh 3790 +41_llvm_std_compat.sh 1980 +42_custom_local_index.sh 9900 +43_indices_lockfile.sh 9400 +44_indices_e2e_integration.sh 7680 +45_install_platform_mapping.sh 11 +46_self_config_mirror_no_bootstrap.sh 13 +47_cdb_prebuilt_module_path_abs.sh 1400 +47_llvm_atomic_link.sh 1000 +48_build_error_output.sh 971 +49_bmi_cache_nested_custom_index.sh 5970 +50_package_owned_build_flags.sh 575 +51_package_generated_files.sh 375 +52_local_path_namespaced_index.sh 468 +53_namespaced_cache_label.sh 713 +54_package_owned_ldflags.sh 907 +55_dependency_shared_artifact.sh 440 +56_transitive_shared_artifact.sh 416 +57_static_dep_shared_artifact.sh 153 +58_preinstall_mcpp_deps_for_hooks.sh 523 +59_cpp_standard_config.sh 6250 +600_accel_variant_selection.sh 392 +601_exclusive_capability.sh 175 +603_version_floor.sh 293 +604_offline_skips_sandbox_bootstrap.sh 131 +605_probe_channel_and_accel_reach_the_build_program.sh 1560 +606_constrained_source_globs_narrow_to_the_build.sh 805 +607_chained_actions_form_the_device_link.sh 917 +608_object_actions_reach_a_static_library.sh 547 +609_clean_stale.sh 4230 +609_shader_sources_are_device_sources.sh 1150 +60_stale_xpkg_cache_reinstall.sh 722 +610_feature_controlled_rule_collection.sh 3770 +611_variant_switch_is_not_replayed.sh 511 +612_build_program_file_input_fast_path.sh 704 +613_sycl_sources_are_device_sources.sh 1180 +614_feature_xlings_reaches_xpkg_dir.sh 705 +615_feature_deps_shared_library_is_linked.sh 1140 +616_examples_and_their_index_agree.sh 246 +617_build_program_knows_the_cxx_stdlib.sh 942 +618_dependency_feature_tools_reach_its_build_program.sh 26010 +619_a_tool_may_not_be_conditioned_on_a_layer.sh 44 +61_dependency_visibility.sh 600 +620_link_flag_reaches_the_link_line.sh 600 +621_exports_narrows_the_symbol_surface.sh 12360 +622_rule_declared_payload_reaches_the_consumers_build_program.sh 54630 +623_device_sources_must_reach_an_action.sh 651 +624_a_missing_host_module_names_the_key.sh 585 625_a_payload_can_be_gated_on_the_accelerator.sh 44636 -626_naming_a_subset_of_backends_is_not_a_mismatch.sh 434 -627_one_package_one_version.sh 46552 -628_a_pin_below_a_stated_floor_is_refused.sh 47993 -629_a_version_range_is_answered_by_xpkg_dir.sh 47222 -62_dotted_dependency_selector_priority.sh 420 -62_runtime_library_dirs.sh 414 -630_a_rule_brings_its_own_environment.sh 44172 -631_action_depfile_tracks_a_runtime_discovered_include.sh 944 -632_a_build_program_is_told_its_package_name.sh 550 -633_host_modules_are_ordered_by_what_they_import.sh 1288 -634_a_private_module_fragment_is_not_a_partition.sh 130 -635_a_byte_order_mark_does_not_hide_a_module.sh 305 -636_a_module_extension_file_need_not_provide_a_module.sh 165 -637_a_conditional_runtime_table_alone_is_applied.sh 519 -638_pack_format_dispatch.sh 5742 -639_the_scanner_does_not_read_inside_a_comment.sh 790 -63_bare_dependency_peer_root_priority.sh 378 -640_a_capability_pin_explains_its_own_row.sh 9920 -641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh 81543 -643_windows_subsystem_cross.sh 1455 -644_windows_keys_are_inert_elsewhere.sh 598 -645_the_fast_path_compares_the_toolchain_request.sh 684 -646_runtime_deploy_places_files_in_a_directory.sh 1881 -647_abi_threads_is_one_switch_for_the_graph.sh 642 -648_an_install_hook_sees_the_build_target.sh 868 -649_pack_carries_runtime_deploy_files.sh 3179 -64_shared_soname_runtime_alias.sh 404 -650_wasm_row_names_its_launcher.sh 21 -651_a_build_program_deploys_what_it_generated.sh 1872 -652_an_application_is_a_kind.sh 514 -653_exceptions_is_the_second_abi_member.sh 6 -654_a_requirement_on_the_target_axis.sh 715 -656_run_hands_the_distributable_to_the_runner.sh 2616 +626_naming_a_subset_of_backends_is_not_a_mismatch.sh 838 +627_one_package_one_version.sh 53560 +628_a_pin_below_a_stated_floor_is_refused.sh 26940 +629_a_version_range_is_answered_by_xpkg_dir.sh 27460 +62_dotted_dependency_selector_priority.sh 669 +62_runtime_library_dirs.sh 2160 +630_a_rule_brings_its_own_environment.sh 50180 +631_action_depfile_tracks_a_runtime_discovered_include.sh 2020 +632_a_build_program_is_told_its_package_name.sh 638 +633_host_modules_are_ordered_by_what_they_import.sh 1250 +634_a_private_module_fragment_is_not_a_partition.sh 167 +635_a_byte_order_mark_does_not_hide_a_module.sh 415 +636_a_module_extension_file_need_not_provide_a_module.sh 192 +637_a_conditional_runtime_table_alone_is_applied.sh 617 +638_pack_format_dispatch.sh 3900 +639_the_scanner_does_not_read_inside_a_comment.sh 1640 +63_bare_dependency_peer_root_priority.sh 822 +640_a_capability_pin_explains_its_own_row.sh 5820 +641_the_android_rows_are_wired_and_the_simulator_is_a_row.sh 35480 +643_windows_subsystem_cross.sh 1270 +644_windows_keys_are_inert_elsewhere.sh 995 +645_the_fast_path_compares_the_toolchain_request.sh 498 +646_runtime_deploy_places_files_in_a_directory.sh 1680 +647_abi_threads_is_one_switch_for_the_graph.sh 620 +648_an_install_hook_sees_the_build_target.sh 547 +649_pack_carries_runtime_deploy_files.sh 2820 +64_shared_soname_runtime_alias.sh 792 +650_wasm_row_names_its_launcher.sh 25 +651_a_build_program_deploys_what_it_generated.sh 1370 +652_an_application_is_a_kind.sh 579 +653_exceptions_is_the_second_abi_member.sh 5 +654_a_requirement_on_the_target_axis.sh 875 +656_run_hands_the_distributable_to_the_runner.sh 1020 657_a_build_program_under_the_web_row_uses_the_host_compiler.sh 3 -659_an_action_declaration_has_no_size_limit.sh 1262 -65_toolchain_runtime_dirs_for_run.sh 661 -660_an_action_names_the_engine_through_mcpp_self.sh 1358 -661_one_identity_two_declarations.sh 754 -662_pack_stages_declared_files_before_the_closure.sh 7113 -663_a_graph_libcxx_over_the_payloads_c_library.sh 17317 -665_a_host_tool_follows_its_source.sh 9337 -669_an_elf_host_pack_states_its_closure.sh 1078 -66_runtime_provides.sh 1271 -670_an_rpath_loader_token_is_left_as_written.sh 189 -671_toolchain_is_an_option_of_run_test_and_pack.sh 4156 -672_a_runner_receives_the_files_the_artifact_carries.sh 9538 -673_test_discover_names_where_tests_are.sh 1177 -674_a_format_is_run_by_the_runner_named_after_it.sh 8008 -677_a_conditional_dependency_replaces_the_unconditional_one.sh 728 -678_a_row_states_a_library_form.sh 860 -679_a_path_dependency_takes_its_manifests_identity.sh 669 -67_features_strict.sh 7264 -681_index_repos_reach_an_existing_home.sh 9473 -682_why_deps_prints_the_resolved_graph.sh 213 -683_a_build_program_reads_the_payload_pkg_config_view.sh 514 -684_a_runner_supplied_twice_is_refused.sh 518 -685_a_nested_test_loads_a_graph_shared_library.sh 405 -686_an_activated_xlings_subos_does_not_redirect_the_registry.sh 7823 -687_the_vendored_xlings_probe_is_an_argument_vector.sh 629 -688_emit_build_database.sh 4886 -689_pack_takes_features.sh 2163 -68_profile_passthrough.sh 8118 -690_a_shared_library_over_a_graph_cxx_runtime.sh 29859 -692_a_package_states_its_default_link_form.sh 1247 -693_the_root_build_program_reads_dependency_link_forms.sh 4248 -696_a_cxx_layer_provider_keeps_its_own_standard.sh 33280 -698_a_tool_source_under_its_dependency_has_a_bounded_address.sh 4322 -69_package_templates.sh 9948 -700_a_program_over_a_cxx_shared_library_has_one_cxx_runtime.sh 1123 -701_symbol_provision_does_not_report_what_the_build_shares_by_construction.sh 1437 -702_a_static_package_under_a_shared_library_is_linked_into_it.sh 747 -70_per_target_flags.sh 231 -710_a_forward_reaches_every_dependency_table.sh 454 -711_a_feature_deps_restatement_names_one_source.sh 5820 -712_a_tool_package_contributes_nothing_to_its_consumer.sh 6191 -713_a_git_dependency_selects_a_repository_member.sh 6173 -714_features_names_a_dependency_feature.sh 5951 -71_required_features_gate.sh 253 -720_the_root_build_program_reads_the_resolved_graph.sh 2778 -722_pack_strips_what_the_graph_built.sh 3741 -723_pack_reports_what_it_produced.sh 857 -72_test_profile_features.sh 337 -730_bare_compat_dependency_is_not_a_refresh_miss.sh 97 -731_planning_children_do_not_inherit_the_callers_pipe.sh 438 -732_xlings_refresh_is_bounded_and_owned.sh 4999 -733_envelope_reports_observed_network_access.sh 344 -734_auto_refresh_false_governs_first_index_sync.sh 159 -735_offline_missing_package_has_its_own_code.sh 118 -736_compile_flag_words_reach_the_compiler_and_the_databases.sh 414 -737_the_declared_runtime_payload_is_installed.sh 72189 -739_a_private_feature_dep_does_not_reach_the_consumer.sh 296 -73_issue131_per_target_cxxflag.sh 177 -740_gcc_over_a_graph_c_library_is_degraded_not_refused.sh 123 -741_c_abi_environment_declared_by_the_c_library.sh 177 -742_gcc_accepted_when_the_realisation_is_empty.sh 112 -743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh 205 -744_a_declared_absence_explains_the_link_that_asked_for_it.sh 90 -745_no_run_builds_the_tests_and_says_so.sh 367 -746_macos_deployment_target_is_target_keyed_not_host_keyed.sh 233 -74_run_no_loader_env_leak.sh 1306 -75_index_status_offline.sh 8087 -765_a_consumer_include_directory_stays_in_the_consumer.sh 675 -76_compile_commands_generated.sh 1245 -76_qualified_custom_ns_noncanonical_filename.sh 458 -770_workspace_member_as_dependency.sh 1139 -772_a_published_member_is_self_contained.sh 5544 -773_commands_outside_the_build_read_the_effective_manifest.sh 349 -774_an_index_member_inherits_its_archive_workspace.sh 324 -775_a_host_tool_merges_its_conditional_sections_once.sh 5157 -776_a_path_with_no_utf8_spelling_is_named.sh 314 -777_c_standard_applies_to_the_package_that_declares_it.sh 187 -778_a_graph_link_searches_no_host_directory.sh 99076 -779_a_build_program_declares_a_runtime_search_dir.sh 1953 -77_cdb_preserves_test_entries.sh 4460 -780_a_passing_check_moves_its_stamp_past_a_changed_input.sh 1689 -781_deleted_root_cdb_is_restored.sh 2308 -782_foreign_root_cdb_entries_are_replaced.sh 3587 -783_cdb_switches_whole_with_the_configuration.sh 3204 -784_cdb_replays_from_its_directory.sh 445 -785_cdb_interface_flag_module_extensions.sh 221 -786_std_unit_in_the_database_and_build_id.sh 1191 -787_emit_plans_every_member_on_its_own.sh 165 -788_emit_defers_an_unbuilt_host_tool.sh 1183 -789_emit_describes_a_member_past_a_failed_build_program.sh 327 -78_test_main_combinations.sh 11880 -790_a_prepare_action_populates_an_unknown_directory.sh 1823 -791_a_prepare_action_that_creates_nothing_fails.sh 955 -792_a_rerun_input_inside_a_prepare_directory_is_warned.sh 1743 -793_an_unknown_action_role_is_refused.sh 2595 -795_a_link_flag_reaches_the_linker_as_written.sh 960 -798_a_rule_applies_through_a_source_it_claims.sh 13286 -799_an_action_runs_with_its_env_and_cwd.sh 1024 -79_gtest_regular_dep_feature_main.sh 14959 -800_a_feature_provides_its_host_tools.sh 1031 -801_a_dependency_program_is_shipped_with_the_consumer.sh 1851 -802_a_host_build_applies_its_host_row.sh 1695 -803_a_recorded_payload_that_is_gone_is_refused_offline.sh 111 -804_a_path_host_tool_builds_with_its_chosen_toolchain.sh 1148 -805_rooted_workspace_reaches_its_own_path_dependency.sh 481 -806_dash_p_resolves_the_package_first.sh 755 -807_a_lib_root_imports_its_own_package.sh 508 -808_device_source_is_not_a_compile_unit.sh 15188 -809_failed_build_program_hides_no_device_source.sh 89 -80_feature_defines.sh 259 -810_two_packages_deploy_the_same_bytes_to_one_name.sh 1941 -812_an_index_floor_is_a_closing_tip.sh 252 -813_conditional_dialect_cxxflags_root_only.sh 23819 -815_the_database_describes_what_a_rule_generates.sh 970 -816_a_git_dependency_reports_its_fetch.sh 221 -817_emit_writes_no_xlings_json_under_private_work_dir.sh 283 -818_a_declared_deploy_outranks_a_search_dir_dll_cross.sh 949 -819_the_crt_is_placed_by_its_rule_not_by_search_order.sh 377 -81_capability_binding.sh 573 -821_an_edge_states_its_advice_on_both_paths.sh 1614 -822_a_package_states_the_oldest_mcpp_it_supports.sh 288 -823_mcpp_core_and_mcpp_name_one_interface.sh 765 -824_a_build_program_reads_the_resolved_toolchain.sh 1076 -826_plugin_diagnostics_features_and_names.sh 726 -827_many_placements_run_as_one_process.sh 2327 -828_a_library_states_its_interface.sh 638 -829_pack_takes_a_member.sh 278 -82_feature_optional_deps.sh 264 -831_a_path_dependency_host_module_outside_src.sh 4402 -832_the_fast_path_resumes_after_an_edit.sh 2677 -833_a_workspace_is_one_graph_per_configuration.sh 1703 -834_a_workspace_plans_once_and_replays_in_one_check.sh 1180 -835_a_shared_library_is_placed_by_a_link.sh 319 -836_a_members_own_statements_reach_its_build.sh 1367 -837_a_members_resources_reach_its_image.sh 170 -838_a_program_shipped_through_artifacts_links_its_own_closure.sh 366 -839_a_workspaces_build_programs_are_named_ordered_and_cached.sh 743 -83_feature_defines_propagate.sh 141 -840_a_workspace_database_describes_each_package_once.sh 602 -841_pack_summarizes_many_outputs.sh 3206 -842_the_build_reports_each_step_once.sh 7211 -843_a_terminal_names_the_action_that_runs.sh 4684 -844_a_planned_build_keeps_one_lock_entry_per_identity.sh 794 -845_play_game_restores_the_terminal.sh 12308 -846_the_vendored_xlings_is_replaced_from_the_newest_source.sh 758 -847_a_module_name_is_unique_within_a_program.sh 1960 -849_a_staged_bmi_waits_for_a_module_compiled_here.sh 1607 -84_runtime_dll_deploy.sh 161 -850_a_selected_member_declares_as_the_root.sh 760 -851_a_shared_member_is_compiled_once_per_configuration.sh 536 -852_a_repeated_dash_p_selects_every_member_it_names.sh 273 -853_run_keeps_one_member_and_exclude_removes_members.sh 764 -854_a_test_over_several_members_plans_and_builds_once.sh 1333 -855_a_shared_test_build_is_reported_once_per_group.sh 346 -856_a_test_over_several_members_continues_past_a_failing_one.sh 1528 -857_a_host_module_several_members_import_is_compiled_once.sh 807 -858_the_engines_module_is_compiled_once_per_home.sh 4172 -859_a_host_module_entry_is_reused_while_its_inputs_agree.sh 3220 -85_target_cfg_build_flags.sh 129 -860_a_workspaces_programs_compile_at_the_same_time.sh 16698 -861_concurrent_compiles_plan_what_serial_compiles_plan.sh 1228 -862_run_q_writes_exactly_the_programs_stdout.sh 199 -863_a_run_whose_build_failed_exits_101.sh 662 -864_a_builds_status_is_on_standard_error.sh 753 -865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh 575 -866_both_streams_on_one_pipe_keep_their_order_and_json_stays_clean.sh 3940 -867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh 10859 -868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh 5486 -869_a_pack_over_several_members_names_each_member_in_its_report.sh 1582 -86_llvm_hermetic_link.sh 3796 -86_target_cfg_dependencies.sh 156 -870_a_pack_over_several_members_resolves_and_fails_per_member.sh 6416 -871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh 11625 -872_a_member_compiles_once_whatever_the_selection.sh 1966 -873_payload_overrides.sh 1826 -874_payload_on_request.sh 2050 -875_toolchain_by_path.sh 665 -876_toolchain_phase.sh 1138 -877_why_sources.sh 83 -87_build_default_profile.sh 502 -88_xlings_environment.sh 9400 -89_build_mcpp.sh 3422 -90_workspace_test.sh 636 -91_target_bare_alias.sh 1021 -92_build_mcpp_import.sh 269 -93_xpkg_parse.sh 84 +659_an_action_declaration_has_no_size_limit.sh 1590 +65_toolchain_runtime_dirs_for_run.sh 922 +660_an_action_names_the_engine_through_mcpp_self.sh 1400 +661_one_identity_two_declarations.sh 707 +662_pack_stages_declared_files_before_the_closure.sh 5200 +663_a_graph_libcxx_over_the_payloads_c_library.sh 13860 +665_a_host_tool_follows_its_source.sh 8580 +669_an_elf_host_pack_states_its_closure.sh 1130 +66_runtime_provides.sh 1080 +670_an_rpath_loader_token_is_left_as_written.sh 181 +671_toolchain_is_an_option_of_run_test_and_pack.sh 3890 +672_a_runner_receives_the_files_the_artifact_carries.sh 8109 +673_test_discover_names_where_tests_are.sh 1440 +674_a_format_is_run_by_the_runner_named_after_it.sh 1600 +677_a_conditional_dependency_replaces_the_unconditional_one.sh 816 +678_a_row_states_a_library_form.sh 890 +679_a_path_dependency_takes_its_manifests_identity.sh 633 +67_features_strict.sh 7740 +681_index_repos_reach_an_existing_home.sh 283 +682_why_deps_prints_the_resolved_graph.sh 232 +683_a_build_program_reads_the_payload_pkg_config_view.sh 705 +684_a_runner_supplied_twice_is_refused.sh 838 +685_a_nested_test_loads_a_graph_shared_library.sh 438 +686_an_activated_xlings_subos_does_not_redirect_the_registry.sh 10470 +687_the_vendored_xlings_probe_is_an_argument_vector.sh 343 +688_emit_build_database.sh 5420 +689_pack_takes_features.sh 2370 +68_profile_passthrough.sh 7130 +690_a_shared_library_over_a_graph_cxx_runtime.sh 18760 +692_a_package_states_its_default_link_form.sh 1800 +693_the_root_build_program_reads_dependency_link_forms.sh 4470 +696_a_cxx_layer_provider_keeps_its_own_standard.sh 25180 +698_a_tool_source_under_its_dependency_has_a_bounded_address.sh 4590 +69_package_templates.sh 11350 +700_a_program_over_a_cxx_shared_library_has_one_cxx_runtime.sh 928 +701_symbol_provision_does_not_report_what_the_build_shares_by_construction.sh 1360 +702_a_static_package_under_a_shared_library_is_linked_into_it.sh 752 +70_per_target_flags.sh 176 +710_a_forward_reaches_every_dependency_table.sh 580 +711_a_feature_deps_restatement_names_one_source.sh 4610 +712_a_tool_package_contributes_nothing_to_its_consumer.sh 6270 +713_a_git_dependency_selects_a_repository_member.sh 5270 +714_features_names_a_dependency_feature.sh 6070 +71_required_features_gate.sh 509 +720_the_root_build_program_reads_the_resolved_graph.sh 1670 +722_pack_strips_what_the_graph_built.sh 5890 +723_pack_reports_what_it_produced.sh 1110 +72_test_profile_features.sh 612 +730_bare_compat_dependency_is_not_a_refresh_miss.sh 105 +731_planning_children_do_not_inherit_the_callers_pipe.sh 583 +732_xlings_refresh_is_bounded_and_owned.sh 4180 +733_envelope_reports_observed_network_access.sh 312 +734_auto_refresh_false_governs_first_index_sync.sh 148 +735_offline_missing_package_has_its_own_code.sh 82 +736_compile_flag_words_reach_the_compiler_and_the_databases.sh 436 +737_the_declared_runtime_payload_is_installed.sh 58840 +739_a_private_feature_dep_does_not_reach_the_consumer.sh 328 +73_issue131_per_target_cxxflag.sh 252 +740_gcc_over_a_graph_c_library_is_degraded_not_refused.sh 155 +741_c_abi_environment_declared_by_the_c_library.sh 462 +742_gcc_accepted_when_the_realisation_is_empty.sh 178 +743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh 285 +744_a_declared_absence_explains_the_link_that_asked_for_it.sh 176 +745_no_run_builds_the_tests_and_says_so.sh 536 +746_macos_deployment_target_is_target_keyed_not_host_keyed.sh 397 +74_run_no_loader_env_leak.sh 1140 +75_index_status_offline.sh 8119 +765_a_consumer_include_directory_stays_in_the_consumer.sh 694 +76_compile_commands_generated.sh 1310 +76_qualified_custom_ns_noncanonical_filename.sh 490 +770_workspace_member_as_dependency.sh 1040 +772_a_published_member_is_self_contained.sh 4520 +773_commands_outside_the_build_read_the_effective_manifest.sh 938 +774_an_index_member_inherits_its_archive_workspace.sh 685 +775_a_host_tool_merges_its_conditional_sections_once.sh 4260 +776_a_path_with_no_utf8_spelling_is_named.sh 389 +777_c_standard_applies_to_the_package_that_declares_it.sh 186 +778_a_graph_link_searches_no_host_directory.sh 31510 +779_a_build_program_declares_a_runtime_search_dir.sh 1840 +77_cdb_preserves_test_entries.sh 4330 +780_a_passing_check_moves_its_stamp_past_a_changed_input.sh 1720 +781_deleted_root_cdb_is_restored.sh 2200 +782_foreign_root_cdb_entries_are_replaced.sh 3300 +783_cdb_switches_whole_with_the_configuration.sh 3270 +784_cdb_replays_from_its_directory.sh 443 +785_cdb_interface_flag_module_extensions.sh 368 +786_std_unit_in_the_database_and_build_id.sh 786 +787_emit_plans_every_member_on_its_own.sh 216 +788_emit_defers_an_unbuilt_host_tool.sh 1520 +789_emit_describes_a_member_past_a_failed_build_program.sh 655 +78_test_main_combinations.sh 10910 +790_a_prepare_action_populates_an_unknown_directory.sh 2080 +791_a_prepare_action_that_creates_nothing_fails.sh 1030 +792_a_rerun_input_inside_a_prepare_directory_is_warned.sh 1680 +793_an_unknown_action_role_is_refused.sh 2530 +795_a_link_flag_reaches_the_linker_as_written.sh 1050 +798_a_rule_applies_through_a_source_it_claims.sh 10570 +799_an_action_runs_with_its_env_and_cwd.sh 1090 +79_gtest_regular_dep_feature_main.sh 11370 +800_a_feature_provides_its_host_tools.sh 1050 +801_a_dependency_program_is_shipped_with_the_consumer.sh 1090 +802_a_host_build_applies_its_host_row.sh 1400 +803_a_recorded_payload_that_is_gone_is_refused_offline.sh 80 +804_a_path_host_tool_builds_with_its_chosen_toolchain.sh 2140 +805_rooted_workspace_reaches_its_own_path_dependency.sh 598 +806_dash_p_resolves_the_package_first.sh 861 +807_a_lib_root_imports_its_own_package.sh 777 +808_device_source_is_not_a_compile_unit.sh 13580 +809_failed_build_program_hides_no_device_source.sh 433 +80_feature_defines.sh 270 +810_two_packages_deploy_the_same_bytes_to_one_name.sh 1100 +812_an_index_floor_is_a_closing_tip.sh 214 +813_conditional_dialect_cxxflags_root_only.sh 22660 +815_the_database_describes_what_a_rule_generates.sh 1200 +816_a_git_dependency_reports_its_fetch.sh 197 +817_emit_writes_no_xlings_json_under_private_work_dir.sh 252 +818_a_declared_deploy_outranks_a_search_dir_dll_cross.sh 1570 +819_the_crt_is_placed_by_its_rule_not_by_search_order.sh 303 +81_capability_binding.sh 593 +821_an_edge_states_its_advice_on_both_paths.sh 1620 +822_a_package_states_the_oldest_mcpp_it_supports.sh 266 +823_mcpp_core_and_mcpp_name_one_interface.sh 856 +824_a_build_program_reads_the_resolved_toolchain.sh 791 +826_plugin_diagnostics_features_and_names.sh 1020 +827_many_placements_run_as_one_process.sh 2650 +828_a_library_states_its_interface.sh 697 +829_pack_takes_a_member.sh 341 +82_feature_optional_deps.sh 316 +831_a_path_dependency_host_module_outside_src.sh 5000 +832_the_fast_path_resumes_after_an_edit.sh 2630 +833_a_workspace_is_one_graph_per_configuration.sh 1780 +834_a_workspace_plans_once_and_replays_in_one_check.sh 872 +835_a_shared_library_is_placed_by_a_link.sh 297 +836_a_members_own_statements_reach_its_build.sh 1440 +837_a_members_resources_reach_its_image.sh 155 +838_a_program_shipped_through_artifacts_links_its_own_closure.sh 425 +839_a_workspaces_build_programs_are_named_ordered_and_cached.sh 1610 +83_feature_defines_propagate.sh 166 +840_a_workspace_database_describes_each_package_once.sh 850 +841_pack_summarizes_many_outputs.sh 1510 +842_the_build_reports_each_step_once.sh 7560 +843_a_terminal_names_the_action_that_runs.sh 4550 +844_a_planned_build_keeps_one_lock_entry_per_identity.sh 943 +845_play_game_restores_the_terminal.sh 11710 +846_the_vendored_xlings_is_replaced_from_the_newest_source.sh 380 +847_a_module_name_is_unique_within_a_program.sh 1980 +849_a_staged_bmi_waits_for_a_module_compiled_here.sh 1870 +84_runtime_dll_deploy.sh 322 +850_a_selected_member_declares_as_the_root.sh 1000 +851_a_shared_member_is_compiled_once_per_configuration.sh 744 +852_a_repeated_dash_p_selects_every_member_it_names.sh 256 +853_run_keeps_one_member_and_exclude_removes_members.sh 1330 +854_a_test_over_several_members_plans_and_builds_once.sh 1760 +855_a_shared_test_build_is_reported_once_per_group.sh 329 +856_a_test_over_several_members_continues_past_a_failing_one.sh 1540 +857_a_host_module_several_members_import_is_compiled_once.sh 878 +858_the_engines_module_is_compiled_once_per_home.sh 4400 +859_a_host_module_entry_is_reused_while_its_inputs_agree.sh 4110 +85_target_cfg_build_flags.sh 155 +860_a_workspaces_programs_compile_at_the_same_time.sh 13460 +861_concurrent_compiles_plan_what_serial_compiles_plan.sh 1390 +862_run_q_writes_exactly_the_programs_stdout.sh 399 +863_a_run_whose_build_failed_exits_101.sh 992 +864_a_builds_status_is_on_standard_error.sh 888 +865_mcpp_test_reports_its_steps_on_stderr_and_its_results_on_stdout.sh 742 +866_both_streams_on_one_pipe_keep_their_order_and_json_stays_clean.sh 4080 +867_a_pack_over_several_members_plans_builds_and_runs_programs_once.sh 3340 +868_a_pack_over_several_members_is_refused_before_anything_is_compiled.sh 4040 +869_a_pack_over_several_members_names_each_member_in_its_report.sh 1470 +86_llvm_hermetic_link.sh 2690 +86_target_cfg_dependencies.sh 285 +870_a_pack_over_several_members_resolves_and_fails_per_member.sh 2700 +871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh 10380 +872_a_member_compiles_once_whatever_the_selection.sh 2570 +873_payload_overrides.sh 1700 +874_payload_on_request.sh 2890 +875_toolchain_by_path.sh 634 +876_toolchain_phase.sh 1610 +877_why_sources.sh 133 +878_an_upgraded_engine_does_not_replay_the_old_graph.sh 2110 +879_a_providers_module_extension_reaches_the_fast_path.sh 4900 +87_build_default_profile.sh 631 +880_a_members_executable_keeps_its_own_implementation.sh 2210 +882_parent_directory_glob_inputs.sh 7430 +883_run_hands_the_terminal_to_the_program.sh 3380 +884_a_shared_library_links_with_its_own_closure.sh 1370 +885_workspace_profiles_and_the_root_package.sh 977 +886_std_compat_is_built_when_imported.sh 6050 +88_xlings_environment.sh 259 +890_a_toolchain_declared_for_an_unservable_target_is_still_installed.sh 22 +891_a_host_header_is_written_not_searched.sh 558 +893_the_workspace_prefix_carries_the_meaning.sh 819 +894_the_position_reading_is_kept_and_reported.sh 898 +895_conditional_configuration_rows_count_as_they_evaluate.sh 821 +896_an_action_output_has_one_writer.sh 5630 +897_each_configuration_has_its_own_generated_output.sh 4980 +898_module_units_in_cpp_files.sh 10390 +899_toolchain_flag_replacing_a_declared_toolchain_says_so.sh 894 +89_build_mcpp.sh 2680 +900_publish_writes_the_workspace_layer_into_the_member.sh 342 +901_the_toolchain_library_path_stays_with_the_tools.sh 3020 +902_a_build_dependency_library_is_compiled_as_its_package.sh 5490 +903_one_package_in_both_tables_has_two_instances.sh 9580 +904_a_build_dependency_feature_stays_out_of_the_target.sh 4850 +905_a_build_program_links_its_libraries_whole.sh 5230 +906_a_build_program_is_compiled_when_its_graph_changes.sh 8070 +907_the_library_interface_is_stated_once.sh 728 +908_what_a_build_dependency_asks_for.sh 5000 +909_deprecated_readings_are_registered_and_kept.sh 5660 +90_workspace_test.sh 789 +910_a_build_plugin_is_a_package_of_the_build_program.sh 8590 +911_a_workspace_states_each_table_once.sh 971 +912_two_configuration_groups_share_the_build_programs_graph.sh 5780 +913_the_build_database_describes_the_build_programs.sh 6370 +914_an_upstream_home_is_linked_never_written.sh 15410 +915_the_system_home_is_maintained_and_read.sh 169 +916_a_home_from_before_keeps_working.sh 228 +918_an_older_packaging_is_used_not_replaced.sh 12410 +919_a_home_selection_is_remembered_once.sh 144 +91_target_bare_alias.sh 365 +920_the_doctor_checks_resources_without_bootstrap.sh 2160 +921_emit_watches_inputs_not_host_outputs.sh 1020 +923_cross_dependency_images_keep_their_resources.sh 802 +924_visible_imports_resolve_transitive_modules.sh 517 +925_a_stated_sysroot_is_the_c_library.sh 21350 +926_the_hosts_own_root_is_a_base.sh 8260 +927_the_runner_contract.sh 677 +928_an_apple_sdk_is_located_on_any_host.sh 298 +929_release_trims_paths_and_defines_ndebug.sh 3230 +92_build_mcpp_import.sh 285 +930_a_toolchain_phase_states_the_base.sh 3900 +931_a_suggested_command_quotes_its_path.sh 17310 +932_a_fresh_home_with_a_space_and_non_ascii_builds.sh 40640 +933_one_cxx_runtime_per_process.sh 2460 +934_llvm_with_a_graph_supplied_libstdcxx.sh 3680 +935_two_builds_of_one_configuration.sh 31770 +936_the_control_contract.sh 17950 +937_the_root_compile_database_is_opt_in.sh 4400 +938_a_relink_never_exposes_a_partial_file.sh 2009 +939_a_lock_written_meanwhile_is_not_overwritten.sh 1270 +93_xpkg_parse.sh 66 +940_an_indirect_import_is_found_whatever_map_its_importer_has.sh 599 +941_a_closed_reader_ends_mcpp_and_no_child_inherits_the_ignore.sh 365 +942_an_ok_image_is_built_once_and_run_by_okrun.sh 61130 +943_a_transitive_git_branch_is_locked.sh 623 +944_a_root_checkout_answers_a_version_the_index_lacks.sh 432 +945_a_miss_anywhere_in_the_graph_asks_for_a_refresh.sh 390 +946_a_program_begins_in_a_dependency_entry.sh 7450 +947_an_image_is_entered_at_one_stated_symbol.sh 2230 +948_an_upgraded_dependency_is_restaged_at_the_same_size.sh 6990 +949_a_version_the_index_lacks_refreshes_at_install.sh 4220 +950_a_kernel_abi_package_over_the_system_c_library_is_in_the_shared_image.sh 397 +951_an_ok_image_needs_no_declaration.sh 157570 +952_a_platform_package_states_a_class_of_targets.sh 1840 +953_the_host_graph_runs_build_programs_as_images.sh 30120 +954_pack_has_three_axes.sh 910 +955_a_universal_package_holds_every_architecture.sh 360000 +956_a_single_file_runs_without_unpacking.sh 117250 +957_packages_merge_and_describe_themselves.sh 93760 +958_a_build_program_runs_again_when_its_output_is_gone.sh 1260 96_msvc_unavailable_nonwindows.sh 38 -98_reflection_import_std.sh 6868 +98_reflection_import_std.sh 7170 diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index e8b671ebf..44a21b15c 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -16,7 +16,8 @@ # unsupported / host-tool-toolchain 交叉目标下的 build.mcpp 没有可解析的宿主工具链 # unsupported / lld-required-absent 这一行直接经 lld 链接,而本机没有 # unsupported / std-module-precompile std 模块预编译不过 -# unsupported / image-tool-absent 链接 ok 映像而图里没有 okld(扫描工程不声明 openkal-tools) +# unsupported / platform-version-group 图里有平台版本组之外的同族版本(graph 模式钉的 openkal-musl 0.3.5 与 +# openkal-llvm-runtime 0.1.3,ok 映像行的平台是 openkal-platform) # # 这张表里**没有 `other`**,而这是一条不变量而不是巧合。`other` 在 # `src/build/refusal.cppm` 里的定义是「拒绝了而这一处分支还没有名字」,所以一行 @@ -62,7 +63,7 @@ graph linux-aarch64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu payl graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-aarch64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-aarch64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-aarch64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph linux-aarch64 aarch64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned graph linux-aarch64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned graph linux-aarch64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin @@ -74,7 +75,7 @@ graph linux-aarch64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl payl graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned graph linux-aarch64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned graph linux-aarch64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-aarch64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph linux-aarch64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve graph linux-aarch64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-aarch64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin @@ -86,7 +87,7 @@ graph linux-x86_64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu paylo graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-x86_64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-x86_64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph linux-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned graph linux-x86_64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned graph linux-x86_64 wasm32-emscripten gcc@16.1.0 - - - - - unsupported capability-pin @@ -100,7 +101,7 @@ graph linux-x86_64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl paylo graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned graph linux-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned graph linux-x86_64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph linux-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement graph linux-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin @@ -109,13 +110,13 @@ graph macos-arm64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capabi graph macos-arm64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph macos-arm64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph macos-arm64 aarch64-macos llvm@23.1.3 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph macos-arm64 aarch64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned graph macos-arm64 wasm32-emscripten llvm@23.1.3 - - - - - unsupported capability-pin graph macos-arm64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin graph macos-arm64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph macos-arm64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned -graph macos-arm64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph macos-arm64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph macos-arm64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph macos-arm64 x86_64-windows-musl llvm@23.1.3 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph windows-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin @@ -124,7 +125,7 @@ graph windows-x86_64 aarch64-linux-gnu llvm@23.1.3 aarch64-unknown-linux-gnu non graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported host-tool-toolchain graph windows-x86_64 aarch64-linux-musl llvm@23.1.3 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph windows-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 aarch64-openkal msvc@system - - - - - unsupported capability-pin graph windows-x86_64 riscv64-linux-musl llvm@23.1.3 - - - - - unsupported tier-planned graph windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned @@ -136,7 +137,7 @@ graph windows-x86_64 x86_64-linux-musl llvm@23.1.3 x86_64-unknown-linux-musl non graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain graph windows-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +graph windows-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported platform-version-group graph windows-x86_64 x86_64-openkal msvc@system - - - - - unsupported capability-pin graph windows-x86_64 x86_64-windows-gnu llvm@23.1.3 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain @@ -153,7 +154,7 @@ payload linux-aarch64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conve payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf payload - - - ok none payload linux-aarch64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload linux-aarch64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload linux-aarch64 armv7a-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi payload - - - ok none payload linux-aarch64 armv7a-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin @@ -189,7 +190,7 @@ payload linux-aarch64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planne payload linux-aarch64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf payload - - - ok none payload linux-aarch64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-aarch64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload linux-aarch64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-aarch64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced payload linux-aarch64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin @@ -203,7 +204,7 @@ payload linux-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conven payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf payload - - - ok none payload linux-x86_64 aarch64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload linux-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload linux-x86_64 armv7a-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi payload - - - ok none payload linux-x86_64 armv7a-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin @@ -241,7 +242,7 @@ payload linux-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf payload - - - ok none payload linux-x86_64 x86_64-openkal gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload linux-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none ucrt(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin @@ -253,7 +254,7 @@ payload macos-arm64 aarch64-linux-gnu llvm@23.1.3 - - - - - unsupported host-can payload macos-arm64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload macos-arm64 aarch64-macos llvm@23.1.3 arm64-apple-macos14.0 host libSystem(payload) libc++(payload) - ok none payload macos-arm64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf host - - - ok none -payload macos-arm64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload macos-arm64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload macos-arm64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi host - - - ok none payload macos-arm64 armv7a-none-eabihf llvm@23.1.3 armv7a-none-eabihf host - - - ok none payload macos-arm64 riscv32-none-elf llvm@23.1.3 riscv32-none-elf host - - - ok none @@ -272,7 +273,7 @@ payload macos-arm64 x86_64-linux-android llvm@23.1.3 - - - - - unsupported capab payload macos-arm64 x86_64-linux-musl llvm@23.1.3 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-planned payload macos-arm64 x86_64-none-elf llvm@23.1.3 x86_64-none-elf host - - - ok none -payload macos-arm64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload macos-arm64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload macos-arm64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-windows-musl llvm@23.1.3 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-android llvm@23.1.3 - - - - - unsupported capability-pin @@ -283,7 +284,7 @@ payload windows-x86_64 aarch64-linux-musl llvm@23.1.3 - - - - - unsupported conv payload windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported convention-unreplaced payload windows-x86_64 aarch64-none-elf llvm@23.1.3 aarch64-none-elf none - - - ok none payload windows-x86_64 aarch64-none-elf msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 aarch64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload windows-x86_64 aarch64-openkal llvm@23.1.3 aarch64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload windows-x86_64 aarch64-openkal msvc@system - - - - - unsupported capability-pin payload windows-x86_64 armv7a-none-eabi llvm@23.1.3 armv7a-none-eabi none - - - ok none payload windows-x86_64 armv7a-none-eabi msvc@system - - - - - unsupported capability-pin @@ -319,7 +320,7 @@ payload windows-x86_64 x86_64-macos llvm@23.1.3 - - - - - unsupported tier-plann payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned payload windows-x86_64 x86_64-none-elf llvm@23.1.3 - - - - - unsupported lld-required-absent payload windows-x86_64 x86_64-none-elf msvc@system - - - - - unsupported capability-pin -payload windows-x86_64 x86_64-openkal llvm@23.1.3 - - - - - unsupported image-tool-absent +payload windows-x86_64 x86_64-openkal llvm@23.1.3 x86_64-unknown-none-elf payload musl(platform) libc++(platform) openkal-musl@0.22.3 ok none payload windows-x86_64 x86_64-openkal msvc@system - - - - - unsupported capability-pin payload windows-x86_64 x86_64-windows-gnu llvm@23.1.3 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported convention-unreplaced diff --git a/tests/unit/test_build_directives.cpp b/tests/unit/test_build_directives.cpp index 1cfc561ce..df38f3fb2 100644 --- a/tests/unit/test_build_directives.cpp +++ b/tests/unit/test_build_directives.cpp @@ -1062,16 +1062,56 @@ TEST(BuildDirectives, DeployRowIsProtocolElevenWithLinkGlobalScopeAndATag) { EXPECT_EQ(def->scope, dirs::Scope::LinkGlobal); EXPECT_EQ(def->sinceProtocol, 11); EXPECT_FALSE(def->tag.empty()); - EXPECT_EQ(dirs::kProtocolVersion, 16); + EXPECT_EQ(dirs::kProtocolVersion, 17); } -TEST(BuildDirectives, ProtocolSixteenIsAcceptedAndSeventeenIsNot) { - auto ok = parse("mcpp:protocol=16\n"); +TEST(BuildDirectives, ProtocolSeventeenIsAcceptedAndEighteenIsNot) { + auto ok = parse("mcpp:protocol=17\n"); EXPECT_FALSE(dirs::protocol_error(ok).has_value()); - auto no = parse("mcpp:protocol=17\n"); + auto no = parse("mcpp:protocol=18\n"); EXPECT_TRUE(dirs::protocol_error(no).has_value()); } +// SPEC-013 §4.1: the platform's fields a program states (protocol 17) become a +// platform statement at the build-program tier; malformed lines are refused. +TEST(BuildDirectives, PlatformFieldsAreProtocolSeventeenAndReachTheStatement) { + for (auto wire : {"link-driver", "link-input", "artifact-name", "seal", "run-host"}) { + auto def = dirs::find_by_wire(wire); + ASSERT_NE(def, nullptr) << wire; + EXPECT_EQ(def->sinceProtocol, 17) << wire; + EXPECT_FALSE(def->tag.empty()) << wire; + } + mcpp::manifest::Manifest m; + auto d = parse("mcpp:link-driver=lld\n" + "mcpp:link-input=from:notes.S:okld\n" + "mcpp:link-input=from:notes.S:note-source\n" + "mcpp:link-input=file:/opt/crt0.o\n" + "mcpp:artifact-name=exe:{name}.oke\n" + "mcpp:artifact-name=shared:lib{name}.okso\n" + "mcpp:seal=0:okld\nmcpp:seal=0:note\nmcpp:seal=0:{}\n" + "mcpp:seal=1:okld\nmcpp:seal=1:check\nmcpp:seal=1:{}\n" + "mcpp:run-host=same-arch\n"); + EXPECT_EQ(dirs::target_directive_error(m, d), ""); + dirs::apply(m, d); + auto const& s = m.buildConfig.programPlatform; + EXPECT_EQ(s.link.driver.value, "lld"); + ASSERT_EQ(s.link.inputs.size(), 2u); + EXPECT_EQ(s.link.inputs[0].as, "notes.S"); + EXPECT_EQ(s.link.inputs[0].from, (std::vector{"okld", "note-source"})); + EXPECT_EQ(s.link.inputs[1].file, "/opt/crt0.o"); + EXPECT_EQ(s.link.exe.value, "{name}.oke"); + EXPECT_EQ(s.link.shared.value, "lib{name}.okso"); + ASSERT_EQ(s.link.seal.size(), 2u); + EXPECT_EQ(s.link.seal[1], (std::vector{"okld", "check", "{}"})); + EXPECT_EQ(s.runner.host, "same-arch"); + EXPECT_EQ(s.link.driver.origin.tier, mcpp::manifest::platform::Tier::Program); + for (auto bad : {"mcpp:link-driver=gold\n", "mcpp:link-input=from:x\n", + "mcpp:artifact-name=exe:lib{name}\n", "mcpp:artifact-name=dll:{name}\n", + "mcpp:artifact-name=shared:lib/{name}.okso\n", + "mcpp:seal=x:okld\n", "mcpp:run-host=far\n"}) + EXPECT_NE(dirs::target_directive_error(m, parse(bad)), "") << bad; +} + // Design 2026-10-26 §24.3: a board's image entry symbol reaches the manifest's // link intent; a non-symbol and two different symbols are refused. TEST(BuildDirectives, LinkStartIsProtocolSixteenAndReachesTheLinkIntent) { diff --git a/tests/unit/test_build_progress.cpp b/tests/unit/test_build_progress.cpp index 3b80e38af..f0fc2d0b9 100644 --- a/tests/unit/test_build_progress.cpp +++ b/tests/unit/test_build_progress.cpp @@ -458,7 +458,9 @@ TEST(ProgressModel, AStepWhoseEntriesAreReadInTwoPiecesCountsOnce) { mcpp::ui::disable_color(); Tmp tmp; Record rec; - rec.packages = {{"lib", true, "lib", 0, 2, "v0.1.0 (lib)", "project"}}; + // The report is the process's, as it is the command's: a name no other + // test of this binary announced. + rec.packages = {{"mlib", true, "mlib", 0, 2, "v0.1.0 (mlib)", "project"}}; rec.owner = {{"obj/lib.m.o", 0}, {"pcm.cache/lib.pcm", 0}, {"bin/lib.a", 0}}; rec.step = {{"obj/lib.m.o", 0}, {"pcm.cache/lib.pcm", 0}, {"bin/lib.a", 1}}; Build b(tmp.path); @@ -473,7 +475,30 @@ TEST(ProgressModel, AStepWhoseEntriesAreReadInTwoPiecesCountsOnce) { b.pass_end(); b.finish(true); auto out = testing::internal::GetCapturedStderr(); - EXPECT_EQ(count(out, "Compiling lib"), 1u) << out; + EXPECT_EQ(count(out, "Compiling mlib"), 1u) << out; +} + +TEST(ProgressModel, APackageTwoConfigurationsShareIsAnnouncedOncePerCommand) { + // A workspace builds one plan per configuration; a dependency they share + // is the same line in each, and the command writes it once (L6). + mcpp::ui::disable_color(); + testing::internal::CaptureStderr(); + for (int config = 0; config < 2; ++config) { + Tmp tmp; + Record rec; + rec.packages = {{"shared", true, "shared", 0, 1, "v0.2.0 (shared)", "project"}}; + rec.owner = {{"obj/s.o", 0}}; + rec.step = {{"obj/s.o", 0}}; + Build b(tmp.path); + b.set_record(rec); + b.pass_begin(); + append(tmp.path / ".ninja_log", "# ninja log v6\n1\t10\t0\tobj/s.o\taa\n"); + b.status({1, 1, 10, {}, {}}); + b.pass_end(); + b.finish(true); + } + auto out = testing::internal::GetCapturedStderr(); + EXPECT_EQ(count(out, "Compiling shared v0.2.0 (shared)"), 1u) << out; } // `Finished` is written once per command. `mcpp run --format ` packs, and diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index acafd8eeb..cd10ee218 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -5052,8 +5052,8 @@ runnerX = ["qemu-aarch64-static"] // tests/unit/test_target_scalar_keys.cpp for the test that keeps this // string (and the array beside it) from drifting from the parser again. EXPECT_NE(m->schemaWarnings[0].find( - "Supported keys: cxx_runtime, linkage, min_api_level, run_under, runner, " - "sysroot, toolchain"), + "Supported keys: cxx_runtime, linkage, min_api_level, platform, run_under, " + "runner, sysroot, toolchain"), std::string::npos) << m->schemaWarnings[0]; } diff --git a/tests/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp new file mode 100644 index 000000000..175dca5bf --- /dev/null +++ b/tests/unit/test_pack_distribution.cpp @@ -0,0 +1,658 @@ +// The distribution half of `mcpp pack` (SPEC-014): the codecs, the zip writer's +// deflated and aligned members, SHA-256, the tar mcpp writes, what `unpack` +// refuses to create, the launchers, the package metadata, `--merge` and the +// single file's payload. +#include + +import std; +import mcpp.pack.compress; +import mcpp.pack.distribution; +import mcpp.pack.zip; + +namespace fs = std::filesystem; +namespace c = mcpp::pack::compress; +namespace d = mcpp::pack::distribution; + +namespace { + +struct Tmp { + fs::path path; + Tmp() { + path = fs::temp_directory_path() / std::format("mcpp-dist-{}", + std::chrono::steady_clock::now().time_since_epoch().count()); + fs::create_directories(path); + } + ~Tmp() { std::error_code ec; fs::remove_all(path, ec); } +}; + +void write(const fs::path& p, std::string_view text) { + fs::create_directories(p.parent_path()); + std::ofstream(p, std::ios::binary) << text; +} + +std::string slurp(const fs::path& p) { + std::ifstream in(p, std::ios::binary); + return std::string(std::istreambuf_iterator(in), {}); +} + +std::uint32_t le32(std::string_view b, std::size_t at) { + return static_cast(static_cast(b[at])) + | static_cast(static_cast(b[at + 1])) << 8 + | static_cast(static_cast(b[at + 2])) << 16 + | static_cast(static_cast(b[at + 3])) << 24; +} + +// A zip of `entries` at `dir/name`, members stored. +fs::path zip_of(const fs::path& dir, std::string_view name, const std::vector& entries) { + const auto out = dir / name; + EXPECT_TRUE(mcpp::pack::zip::write(out, entries)); + return out; +} + +} // namespace + +TEST(PackCompress, ParsesAlgorithmsAndLevels) { + auto z = c::parse("zstd:19"); + ASSERT_TRUE(z); + EXPECT_EQ(z->algorithm, c::Algorithm::Zstd); + EXPECT_EQ(z->level, 19); + EXPECT_TRUE(z->stated); + EXPECT_TRUE(c::parse("none")); + EXPECT_FALSE(c::parse("none:3")); + EXPECT_FALSE(c::parse("gzip:10")); + EXPECT_FALSE(c::parse("brotli")); + EXPECT_EQ(c::tar_extension(c::Algorithm::Xz), ".tar.xz"); + EXPECT_EQ(c::tar_extension(c::Algorithm::None), ".tar"); +} + +// An unstated level is -1, the codec's default; every stated one is honoured, +// `xz:0` and `zstd:1` too. +TEST(PackCompress, AStatedLevelIsHonoured) { + EXPECT_EQ(c::Choice{}.level, -1); + EXPECT_EQ(c::parse("xz")->level, -1); + EXPECT_EQ(c::parse("xz:0")->level, 0); + EXPECT_EQ(c::parse("zstd:1")->level, 1); + std::string text; + for (int i = 0; i < 4000; ++i) text += std::format("{} ", i * 7919 % 10007); + auto fastest = c::bytes(text, *c::parse("xz:0")); + auto preset = c::bytes(text, *c::parse("xz")); + ASSERT_TRUE(fastest && preset); + EXPECT_NE(*fastest, *preset) << "xz:0 was written at the default preset"; + auto z1 = c::bytes(text, *c::parse("zstd:1")); + auto z19 = c::bytes(text, *c::parse("zstd")); + ASSERT_TRUE(z1 && z19); + EXPECT_NE(*z1, *z19) << "zstd:1 was written at the default level"; +} + +// A gzip member names no file, no time and the system 255, whatever host +// wrote it. +TEST(PackCompress, AGzipHeaderIsTheSameOnEveryHost) { + auto gz = c::bytes("the same bytes", c::Choice{}); + ASSERT_TRUE(gz); + ASSERT_GE(gz->size(), 10u); + EXPECT_EQ(static_cast((*gz)[0]), 0x1f); + EXPECT_EQ(static_cast((*gz)[3]), 0) << "a name, a comment or an extra field"; + EXPECT_EQ(le32(*gz, 4), 0u) << "a modification time"; + EXPECT_EQ(static_cast((*gz)[9]), 255) << "the system byte"; +} + +TEST(PackCompress, EveryCodecRoundTrips) { + std::string text; + for (int i = 0; i < 4000; ++i) text += std::format("line {} of a package\n", i % 37); + for (auto a : { c::Algorithm::Gzip, c::Algorithm::Zstd, c::Algorithm::Xz, c::Algorithm::None }) { + c::Choice ch; + ch.algorithm = a; + auto packed = c::bytes(text, ch); + ASSERT_TRUE(packed) << c::name(a); + if (a != c::Algorithm::None) EXPECT_LT(packed->size(), text.size()) << c::name(a); + auto back = c::decompress(*packed, a); + ASSERT_TRUE(back) << c::name(a); + EXPECT_EQ(*back, text) << c::name(a); + } + auto raw = c::deflate_raw(text, -1); + ASSERT_TRUE(raw); + auto back = c::inflate_raw(*raw, text.size()); + ASSERT_TRUE(back); + EXPECT_EQ(*back, text); +} + +TEST(PackDistribution, Sha256OfKnownVectors) { + EXPECT_EQ(d::sha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + EXPECT_EQ(d::sha256("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + EXPECT_EQ(d::sha256(std::string(1000, 'a')), + "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"); +} + +TEST(PackZip, AnAlignedMemberIsStoredOnItsBoundary) { + Tmp t; + write(t.path / "a.oke", std::string(3000, 'x')); + write(t.path / "libb.okso", std::string(70000, 'y')); + std::vector entries{ + { "a.oke", t.path / "a.oke", false, {} }, + { "libb.okso", t.path / "libb.okso", false, {} }, + }; + mcpp::pack::zip::WriteOptions o; + o.align = 65536; + ASSERT_TRUE(mcpp::pack::zip::write(t.path / "out.zip", entries, o)); + std::ifstream in(t.path / "out.zip", std::ios::binary); + std::string b{std::istreambuf_iterator(in), {}}; + // Each member's data, found through the central directory, starts on a + // 64 KiB boundary and is stored. + std::size_t eocd = b.rfind(std::string("PK\x05\x06", 4)); + ASSERT_NE(eocd, std::string::npos); + std::size_t at = le32(b, eocd + 16); + for (int n = 0; n < 2; ++n) { + ASSERT_EQ(le32(b, at), 0x02014b50u); + const auto method = static_cast(b[at + 10]); + const auto nameLen = static_cast(static_cast(b[at + 28])) + | static_cast(static_cast(b[at + 29])) << 8; + const auto local = le32(b, at + 42); + const auto lname = static_cast(static_cast(b[local + 26])) + | static_cast(static_cast(b[local + 27])) << 8; + const auto lextra = static_cast(static_cast(b[local + 28])) + | static_cast(static_cast(b[local + 29])) << 8; + EXPECT_EQ(method, 0); + EXPECT_EQ((local + 30 + lname + lextra) % 65536, 0u); + at += 46 + nameLen; + } +} + +TEST(PackZip, DeflateWhenItShrinks) { + Tmp t; + write(t.path / "text", std::string(10000, 'z')); + mcpp::pack::zip::WriteOptions o; + o.deflate = true; + ASSERT_TRUE(mcpp::pack::zip::write(t.path / "out.zip", { { "text", t.path / "text", false, {} } }, o)); + EXPECT_LT(fs::file_size(t.path / "out.zip"), 1000u); + // And it reads back. + auto root = d::unpack(t.path / "out.zip", t.path / "x"); + ASSERT_FALSE(root) << "a zip without one top-level directory is not a package"; +} + +TEST(PackDistribution, LaunchersPickTheArchitectureAndTheRunner) { + d::Launch l; + l.name = "hello"; + l.runner = { "okrun", "{}" }; + l.entry = { { "x86_64", "hello.oke" }, { "aarch64", "hello.oke" } }; + l.loader = "okrun"; + ASSERT_TRUE(d::launchable(l)); + const auto sh = d::posix_launcher(l); + EXPECT_TRUE(sh.starts_with("#!/bin/sh\n")); + EXPECT_NE(sh.find("x86_64|amd64|AMD64) arch=x86_64"), std::string::npos); + EXPECT_NE(sh.find("'aarch64') entry='hello.oke' ;;"), std::string::npos); + EXPECT_NE(sh.find("image=\"$top/lib/$arch/$entry\""), std::string::npos); + EXPECT_NE(sh.find("runner=\"$top/libexec/$loader/$os-$arch/$loader\""), std::string::npos); + EXPECT_NE(sh.find("exec \"$runner\" \"$image\" \"$@\""), std::string::npos); + // The package is found through a link to the launcher, without readlink -f. + EXPECT_NE(sh.find("readlink -- \"$self\""), std::string::npos); + EXPECT_EQ(sh.find("readlink -f"), std::string::npos); + const auto cmd = d::windows_launcher(l); + EXPECT_NE(cmd.find("PROCESSOR_ARCHITEW6432"), std::string::npos); + EXPECT_NE(cmd.find("libexec\\okrun\\windows-%arch%\\okrun.exe"), std::string::npos); + EXPECT_NE(cmd.find("\"%runner%\" \"%image%\" %*"), std::string::npos); +} + +// The runner's words expand as `mcpp run` expands them: a word's `{}` is the +// image inside the word as well as alone; with no `{}` the image is appended. +TEST(PackDistribution, LaunchersExpandTheRunnerAsMcppRunDoes) { + d::Launch l; + l.name = "hello"; + l.entry = { { "x86_64", "hello.oke" } }; + l.runner = { "okrun", "--image={}", "it's" }; + EXPECT_NE(d::posix_launcher(l).find("exec \"$runner\" '--image='\"$image\" 'it'\\''s' \"$@\""), + std::string::npos) << d::posix_launcher(l); + EXPECT_NE(d::windows_launcher(l).find("\"%runner%\" \"--image=%image%\" \"it's\" %*"), std::string::npos) + << d::windows_launcher(l); + l.runner = { "okrun", "--quiet" }; + EXPECT_NE(d::posix_launcher(l).find("exec \"$runner\" '--quiet' \"$image\" \"$@\""), std::string::npos) + << d::posix_launcher(l); + EXPECT_NE(d::windows_launcher(l).find("\"%runner%\" \"--quiet\" \"%image%\" %*"), std::string::npos) + << d::windows_launcher(l); +} + +// A .cmd doubles `%` in what it states literally, and a word it cannot state +// (a double quote) is refused: the .cmd written for it runs nothing. +TEST(PackDistribution, AWindowsLauncherEscapesWhatItStates) { + d::Launch l; + l.name = "hello"; + l.entry = { { "x86_64", "hello.oke" } }; + l.runner = { "ok%run", "--load=50%", "{}" }; + const auto cmd = d::windows_launcher(l); + EXPECT_NE(cmd.find("set \"runner=ok%%run\""), std::string::npos) << cmd; + EXPECT_NE(cmd.find("\"%runner%\" \"--load=50%%\" \"%image%\" %*"), std::string::npos) << cmd; + l.runner = { "okrun", "--say=\"hi\"", "{}" }; + auto ok = d::launchable(l); + ASSERT_FALSE(ok); + EXPECT_NE(ok.error().find("double quote"), std::string::npos) << ok.error(); + const auto refused = d::windows_launcher(l); + EXPECT_EQ(refused.find("%runner%"), std::string::npos) << refused; + EXPECT_NE(refused.find("exit /b 126"), std::string::npos) << refused; +} + +// The POSIX launcher, run through a link to it from another directory, finds +// its package and starts the runner with the image spliced into its word. +TEST(PackDistribution, APosixLauncherRunsThroughALink) { + if (!fs::exists("/bin/sh")) GTEST_SKIP() << "no /bin/sh"; + std::string arch; + { + Tmp probe; + if (std::system(std::format("uname -m > '{}'", (probe.path / "m").string()).c_str()) != 0) + GTEST_SKIP() << "no uname"; + arch = slurp(probe.path / "m"); + } + arch = arch.starts_with("x86_64") || arch.starts_with("amd64") ? "x86_64" + : arch.starts_with("aarch64") || arch.starts_with("arm64") ? "aarch64" : ""; + if (arch.empty()) GTEST_SKIP() << "no launcher row for this machine"; + Tmp t; + const auto pkg = t.path / "my pkg"; + d::Launch l; + l.name = "hello"; + l.entry = { { "x86_64", "hello.oke" }, { "aarch64", "hello.oke" } }; + l.runner = { "fakerun", "--image={}" }; + write(pkg / "bin/hello", d::posix_launcher(l)); + write(pkg / "lib" / arch / "hello.oke", "image"); + write(t.path / "path/fakerun", "#!/bin/sh\nprintf '%s|' \"$@\"\n"); + for (auto f : { pkg / "bin/hello", t.path / "path/fakerun" }) + fs::permissions(f, fs::perms::owner_all, fs::perm_options::add); + fs::create_directories(t.path / "elsewhere"); + fs::create_symlink(pkg / "bin/hello", t.path / "elsewhere/hello"); + const auto out = t.path / "out"; + const auto rc = std::system(std::format("PATH='{}':\"$PATH\" '{}' 'a b' c > '{}' 2>&1", + (t.path / "path").string(), (t.path / "elsewhere/hello").string(), + out.string()).c_str()); + EXPECT_EQ(rc, 0) << slurp(out); + // The physical path: the launcher follows links to the directories too + // (macOS's temporary directory is under a link, /var -> /private/var). + EXPECT_EQ(slurp(out), std::format("--image={}|a b|c|", + (fs::canonical(pkg) / "lib" / arch / "hello.oke").string())); +} + +// A shared object is `.so` or `.so.`; a name that merely holds `.so` +// is not one. +TEST(PackDistribution, ALibraryIsNamedSoOrSoVersion) { + Tmp t; + const auto root = t.path / "app-1.0"; + for (auto f : { "lib/libz.so", "lib/libz.so.1.3", "share/notes.sol", "share/a.so.x", "share/b.socket" }) + write(root / f, "x"); + d::Package p; + p.name = "app"; + p.version = "1.0"; + auto described = d::describe(root, p, {}); + ASSERT_TRUE(described) << described.error(); + std::map roles; + for (auto const& f : described->files) roles[f.path] = f.role; + EXPECT_EQ(roles["lib/libz.so"], "library"); + EXPECT_EQ(roles["lib/libz.so.1.3"], "library"); + EXPECT_EQ(roles["share/notes.sol"], "resource"); + EXPECT_EQ(roles["share/a.so.x"], "resource"); + EXPECT_EQ(roles["share/b.socket"], "resource"); +} + +// `read` refuses a document that names a path outside the package. +TEST(PackDistribution, ReadRefusesAPathOutsideThePackage) { + Tmp t; + for (auto bad : { "../outside", "/etc/passwd", "a/../../b", "./bin/x", "bin\\\\x", "" }) { + write(t.path / "mcpp-package.json", + std::format(R"({{"schema": 1, "name": "x", "version": "1", "files": [{{"path": "{}", "role": "resource", "sha256": ""}}]}})", + bad)); + auto r = d::read(t.path); + ASSERT_FALSE(r) << bad; + EXPECT_NE(r.error().find("not a path inside the package"), std::string::npos) << r.error(); + } +} + +TEST(PackDistribution, PackageMetadataDescribesEveryFile) { + Tmp t; + const auto root = t.path / "app-1.0-openkal"; + write(root / "bin/app", "#!/bin/sh\n"); + write(root / "lib/x86_64/app.oke", "image"); + write(root / "share/data.txt", "data"); + write(root / "LICENSE", "license"); + d::Package p; + p.name = "app"; + p.version = "1.0"; + p.legs = { "x86_64-openkal" }; + p.runner = { "okrun", "{}" }; + p.entry = { { "x86_64", "lib/x86_64/app.oke" } }; + d::Stated s; + s.role["bin/app"] = "launcher"; + auto described = d::describe(root, p, s); + ASSERT_TRUE(described); + auto back = d::read(root); + ASSERT_TRUE(back) << back.error(); + std::map roles; + for (auto const& f : back->files) roles[f.path] = f.role; + EXPECT_EQ(roles["bin/app"], "launcher"); + EXPECT_EQ(roles["lib/x86_64/app.oke"], "image"); + EXPECT_EQ(roles["share/data.txt"], "resource"); + EXPECT_EQ(roles["LICENSE"], "metadata"); + EXPECT_EQ(back->entry.at("x86_64"), "lib/x86_64/app.oke"); + for (auto const& f : back->files) + EXPECT_EQ(f.sha256, d::sha256_file(root / f.path)) << f.path; +} + +TEST(PackDistribution, TreePayloadIsOrderedRecords) { + Tmp t; + write(t.path / "b/x", "12"); + write(t.path / "a", "3"); + auto payload = d::tree_payload(t.path); + ASSERT_TRUE(payload); + EXPECT_TRUE(payload->starts_with("MCPPTREE1\n")); + // `a` before `b` before `b/x`. + const auto a = payload->find("a"), b = payload->find("b/x"); + EXPECT_LT(a, b); +} + +// ─── the tar mcpp writes ────────────────────────────────────────────────── + +namespace { + +// A tree with a directory, an executable, a link, and a name and a link target +// past the 100 bytes of a ustar field. +fs::path sample_tree(const fs::path& dir) { + const auto root = dir / "app-1.0"; + write(root / "bin/app", "#!/bin/sh\necho app\n"); + fs::permissions(root / "bin/app", fs::perms::owner_exec, fs::perm_options::add); + write(root / "share/data.txt", "data"); + write(root / "share" / std::string(60, 'd') / (std::string(60, 'e') + ".txt"), "deep"); + fs::create_directories(root / "empty"); + fs::create_symlink("data.txt", root / "share/link"); + fs::create_symlink(std::string(60, 'd') + "/" + std::string(60, 'e') + ".txt", root / "share/far"); + return root; +} + +const c::Choice kPlain{ c::Algorithm::None, -1, false }; + +} // namespace + +// Two writes of one tree are the same bytes, its times and modes changed in +// between: no owner, no time, the modes normalised. +TEST(PackTar, TheSameTreeIsTheSameBytes) { + Tmp t; + const auto root = sample_tree(t.path); + for (auto ch : { kPlain, c::Choice{} }) { + ASSERT_TRUE(d::write_tar(root, t.path / "one", ch)); + fs::last_write_time(root / "share/data.txt", fs::file_time_type::clock::now() - std::chrono::hours(48)); + fs::permissions(root / "share/data.txt", fs::perms::group_write, fs::perm_options::add); + ASSERT_TRUE(d::write_tar(root, t.path / "two", ch)); + EXPECT_EQ(slurp(t.path / "one"), slurp(t.path / "two")) << c::name(ch.algorithm); + } + ASSERT_TRUE(d::write_tar(root, t.path / "plain.tar", kPlain)); + const auto tar = slurp(t.path / "plain.tar"); + ASSERT_GE(tar.size(), 1024u); + EXPECT_EQ(tar.substr(0, 9), std::string("app-1.0/\0", 9)); + EXPECT_EQ(tar.substr(108, 8), std::string("0000000\0", 8)) << "uid"; + EXPECT_EQ(tar.substr(116, 8), std::string("0000000\0", 8)) << "gid"; + EXPECT_EQ(tar.substr(136, 12), std::string("00000000000\0", 12)) << "mtime"; + EXPECT_EQ(tar.substr(257, 6), std::string("ustar\0", 6)); + EXPECT_EQ(tar.substr(265, 32), std::string(32, '\0')) << "uname"; + EXPECT_EQ(tar.size() % 512, 0u); +} + +// What mcpp writes, `unpack` reads back: files, modes, links, long names. +TEST(PackTar, UnpackReadsWhatWriteTarWrote) { + Tmp t; + const auto root = sample_tree(t.path); + for (auto ch : { kPlain, c::Choice{}, c::Choice{ c::Algorithm::Zstd, -1, false }, + c::Choice{ c::Algorithm::Xz, -1, false } }) { + const auto archive = t.path / ("app" + std::string(c::tar_extension(ch.algorithm))); + ASSERT_TRUE(d::write_tar(root, archive, ch)); + const auto into = t.path / ("x-" + std::string(c::name(ch.algorithm))); + auto back = d::unpack(archive, into); + ASSERT_TRUE(back) << back.error(); + EXPECT_EQ(back->filename(), "app-1.0"); + EXPECT_EQ(slurp(*back / "share/data.txt"), "data"); + EXPECT_EQ(slurp(*back / "share" / std::string(60, 'd') / (std::string(60, 'e') + ".txt")), "deep"); + EXPECT_NE(fs::status(*back / "bin/app").permissions() & fs::perms::owner_exec, fs::perms::none); + EXPECT_TRUE(fs::is_directory(*back / "empty")); + EXPECT_TRUE(fs::is_symlink(*back / "share/link")); + EXPECT_EQ(fs::read_symlink(*back / "share/far").generic_string(), + std::string(60, 'd') + "/" + std::string(60, 'e') + ".txt"); + EXPECT_EQ(slurp(*back / "share/far"), "deep"); + } +} + +// A tar holding a link out of its tree is refused, and nothing is written. +TEST(PackTar, UnpackRefusesALinkOutOfTheTree) { + Tmp t; + const auto root = t.path / "app-1.0"; + write(root / "a", "a"); + fs::create_symlink("../../outside", root / "escape"); + ASSERT_TRUE(d::write_tar(root, t.path / "app.tar", kPlain)); + auto r = d::unpack(t.path / "app.tar", t.path / "x"); + ASSERT_FALSE(r); + EXPECT_NE(r.error().find("outside the package"), std::string::npos) << r.error(); + EXPECT_TRUE(fs::is_empty(t.path / "x")); +} + +// ─── what unpack refuses ────────────────────────────────────────────────── + +// A member that would land outside the extraction root, or be written through +// a link member, is refused before anything is written. +TEST(PackUnpack, AZipCreatesNothingOutsideItsRoot) { + Tmp t; + write(t.path / "f", "content"); + using E = mcpp::pack::zip::Entry; + const auto f = t.path / "f"; + struct Case { std::string_view label, why; std::vector entries; }; + const std::vector cases{ + { "dot-dot", "not a path inside", { { "pkg/a", f, false, {} }, { "pkg/../evil", f, false, {} } } }, + { "absolute", "not a path inside", { { "/tmp/mcpp-evil", f, false, {} } } }, + { "drive", "not a path inside", { { "C:/evil", f, false, {} } } }, + { "the root as a file", "not a path inside", { { "pkg/.", f, false, {} }, { ".", f, false, {} } } }, + { "backslash", "not a path inside", { { "pkg\\..\\evil", f, false, {} } } }, + { "link out", "outside the package", { { "pkg/a", f, false, {} }, { "pkg/l", {}, false, "../../outside" } } }, + { "absolute link", "outside the package", { { "pkg/a", f, false, {} }, { "pkg/l", {}, false, "/etc" } } }, + { "link through", "lies under the link", { { "pkg/l", {}, false, "sub" }, { "pkg/l/x", f, false, {} } } }, + { "link chain out", "outside the package", + { { "pkg/d/up", {}, false, ".." }, { "pkg/l", {}, false, "d/up/../.." }, { "pkg/a", f, false, {} } } }, + { "twice", "twice", { { "pkg/a", f, false, {} }, { "pkg/a", {}, false, "b" } } }, + }; + for (std::size_t i = 0; i < cases.size(); ++i) { + const auto into = t.path / std::format("x{}", i); + auto r = d::unpack(zip_of(t.path, std::format("c{}.zip", i), cases[i].entries), into); + ASSERT_FALSE(r) << cases[i].label; + EXPECT_NE(r.error().find(cases[i].why), std::string::npos) << cases[i].label << ": " << r.error(); + EXPECT_TRUE(fs::is_empty(into)) << cases[i].label << ": something was written"; + } + EXPECT_FALSE(fs::exists(t.path / "evil")); + // A link inside the tree is made, after the files. + auto ok = d::unpack(zip_of(t.path, "ok.zip", { { "pkg/l", {}, false, "sub" }, { "pkg/sub/x", f, false, {} } }), + t.path / "ok"); + ASSERT_TRUE(ok) << ok.error(); + EXPECT_TRUE(fs::is_symlink(*ok / "l")); + EXPECT_EQ(slurp(*ok / "l/x"), "content"); +} + +// Every truncation of a zip, and every byte of it changed, is an error or a +// tree, never a read past the archive. +TEST(PackUnpack, ATruncatedOrCorruptZipIsAnError) { + Tmp t; + write(t.path / "f", std::string(300, 'q')); + write(t.path / "g", "small"); + mcpp::pack::zip::WriteOptions o; + o.deflate = true; + ASSERT_TRUE(mcpp::pack::zip::write(t.path / "a.zip", { { "pkg/f", t.path / "f", false, {} }, + { "pkg/g", t.path / "g", true, {} } }, o)); + const auto whole = slurp(t.path / "a.zip"); + for (std::size_t n = 0; n < whole.size(); ++n) { + write(t.path / "cut.zip", whole.substr(0, n)); + const auto into = t.path / std::format("cut{}", n); + EXPECT_FALSE(d::unpack(t.path / "cut.zip", into)) << n << " bytes"; + fs::remove_all(into); + } + for (std::size_t n = 0; n < whole.size(); ++n) { + auto bad = whole; + bad[n] = static_cast(bad[n] ^ 0x5a); + write(t.path / "bad.zip", bad); + const auto into = t.path / std::format("bad{}", n); + (void)d::unpack(t.path / "bad.zip", into); + fs::remove_all(into); + } +} + +// A member whose bytes do not match its CRC-32 is refused. +TEST(PackUnpack, ACrcMismatchIsRefused) { + Tmp t; + write(t.path / "f", "the member's content"); + auto zip = slurp(zip_of(t.path, "a.zip", { { "pkg/f", t.path / "f", false, {} } })); + const auto at = zip.find("the member's content"); + ASSERT_NE(at, std::string::npos); + zip[at] = 'T'; + write(t.path / "a.zip", zip); + auto r = d::unpack(t.path / "a.zip", t.path / "x"); + ASSERT_FALSE(r); + EXPECT_NE(r.error().find("CRC-32"), std::string::npos) << r.error(); +} + +// ─── --merge ────────────────────────────────────────────────────────────── + +namespace { + +// A described package of one leg under `dir`. +fs::path leg_package(const fs::path& dir, std::string_view leg, std::string_view arch) { + const auto root = dir / "app-1.0"; + write(root / "lib" / arch / "app.oke", std::format("image of {}", arch)); + write(root / "README.md", "readme"); + d::Package p; + p.name = "app"; + p.version = "1.0"; + p.legs = { std::string(leg) }; + p.runner = { "okrun", "{}" }; + p.entry = { { std::string(arch), std::format("lib/{}/app.oke", arch) } }; + EXPECT_TRUE(d::describe(root, p, {})); + return root; +} + +} // namespace + +// A file changed after its package was written is refused, named with both +// digests. +TEST(PackMerge, AFileThatIsNotItsDigestIsRefused) { + Tmp t; + const auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); + const auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); + write(b / "lib/aarch64/app.oke", "changed"); + d::MergeRequest req; + req.inputs = { a, b }; + req.output = t.path / "out"; + auto r = d::merge(req); + ASSERT_FALSE(r); + EXPECT_NE(r.error().find("lib/aarch64/app.oke"), std::string::npos) << r.error(); + EXPECT_NE(r.error().find(d::sha256("changed")), std::string::npos) << r.error(); + EXPECT_NE(r.error().find(d::sha256("image of aarch64")), std::string::npos) << r.error(); + EXPECT_FALSE(fs::exists(t.path / "out")); +} + +// The legs merge into a new directory; an output that exists, or is an input, +// is refused, and an input written with a trailing separator is its directory. +TEST(PackMerge, TheOutputIsNewOrAPackage) { + Tmp t; + const auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); + const auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); + d::MergeRequest req; + req.inputs = { fs::path(a.string() + "/"), b }; + req.output = t.path / "out"; + auto r = d::merge(req); + ASSERT_TRUE(r) << r.error(); + auto merged = d::read(*r); + ASSERT_TRUE(merged) << merged.error(); + EXPECT_EQ(merged->legs, (std::vector{ "aarch64-openkal", "x86_64-openkal" })); + EXPECT_EQ(slurp(*r / "lib/aarch64/app.oke"), "image of aarch64"); + + // Again into the directory it wrote: a directory is never replaced. + auto again = d::merge(req); + ASSERT_FALSE(again); + EXPECT_NE(again.error().find("exists"), std::string::npos) << again.error(); + // Onto an input. + req.output = a; + ASSERT_FALSE(d::merge(req)); + EXPECT_TRUE(fs::exists(a / "mcpp-package.json")); + // Over a file that is not a package. + write(t.path / "notes.tar.gz", "my notes"); + req.output = t.path / "notes.tar.gz"; + auto over = d::merge(req); + ASSERT_FALSE(over); + EXPECT_NE(over.error().find("not a package"), std::string::npos) << over.error(); + EXPECT_EQ(slurp(t.path / "notes.tar.gz"), "my notes"); + // Over an archive that is one: replaced. + req.output = t.path / "app.tar.gz"; + ASSERT_TRUE(d::merge(req)); + auto replaced = d::merge(req); + ASSERT_TRUE(replaced) << replaced.error(); +} + +// What the metadata states reaches the generated Lua as strings: a quote or a +// line break in a name is escaped, never code. +TEST(PackDistribution, XpkgDescriptorWritesMetadataAsStrings) { + d::Asset a; + a.file = "evil-1.0-openkal.zip"; + a.sha256 = std::string(64, 'a'); + a.pkg.name = "ev\"il\nos.exit()"; + a.pkg.version = "1.0"; + a.pkg.runner = { "okrun", "{}" }; + a.pkg.entry = { { "x86_64", "lib/x86_64/evil.oke" } }; + auto text = d::xpkg_descriptor({ a }, "https://example.org/v1.0"); + ASSERT_TRUE(text) << text.error(); + EXPECT_NE(text->find("description = \"ev\\\"il\\010os.exit() 1.0"), std::string::npos) << *text; + EXPECT_EQ(text->find("\nos.exit()"), std::string::npos) << *text; + EXPECT_NE(text->find("deps = { \"okrun\" }"), std::string::npos) << *text; +} + +// The compression fits the container the merge writes and the name it is +// given: zstd in a .tar.gz, or in a zip the first input decided, is refused. +TEST(PackMerge, TheCompressionFitsTheContainer) { + Tmp t; + const auto a = leg_package(t.path / "a", "x86_64-openkal", "x86_64"); + const auto b = leg_package(t.path / "b", "aarch64-openkal", "aarch64"); + d::MergeRequest req; + req.inputs = { a, b }; + req.compression = *c::parse("zstd"); + req.output = t.path / "out.tar.gz"; + auto named = d::merge(req); + ASSERT_FALSE(named); + EXPECT_NE(named.error().find(".tar.zst"), std::string::npos) << named.error(); + req.output = t.path / "out.zip"; + ASSERT_FALSE(d::merge(req)); + req.output = t.path / "out.tar.zst"; + auto fits = d::merge(req); + ASSERT_TRUE(fits) << fits.error(); +} + +// The entry is named after the command a person runs, not the package, and +// moves the archive's own top-level directory into place. +TEST(PackDistribution, XpkgDescriptorNamesTheCommandAndTheArchiveTop) { + d::Asset uni; + uni.file = "mcppls-0.0.18-openkal.zip"; + uni.sha256 = std::string(64, 'a'); + uni.top = "mcppls-0.0.18-openkal"; + uni.pkg.name = "mcpp-language-server"; + uni.pkg.version = "0.0.18"; + uni.pkg.runner = { "okrun", "{}" }; + uni.pkg.entry = { { "x86_64", "lib/x86_64/mcppls.oke" }, { "aarch64", "lib/aarch64/mcppls.oke" } }; + auto u = d::xpkg_descriptor({ uni }, "https://example.org/v0.0.18"); + ASSERT_TRUE(u) << u.error(); + EXPECT_NE(u->find("name = \"mcppls\""), std::string::npos) << *u; + EXPECT_NE(u->find("xvm.add(\"mcppls\", opts)"), std::string::npos) << *u; + EXPECT_NE(u->find("opts.alias = \"mcppls.cmd\""), std::string::npos) << *u; + EXPECT_NE(u->find("os.dirs(\"mcppls-0.0.18-openkal\")"), std::string::npos) << *u; + + std::vector per; + for (auto [triple, top] : { std::pair{ "x86_64-linux-musl", "okrun-0.1.3-linux-x86_64" }, + std::pair{ "aarch64-macos", "okrun-0.1.3-macos-aarch64" } }) { + d::Asset a; + a.file = std::string(top) + ".zip"; + a.sha256 = std::string(64, 'b'); + a.top = top; + a.pkg.name = "openkal-tools"; + a.pkg.version = "0.1.3"; + a.pkg.legs = { triple }; + a.pkg.program = "bin/okrun"; + per.push_back(a); + } + auto r = d::xpkg_descriptor(per, "https://example.org/0.1.3"); + ASSERT_TRUE(r) << r.error(); + EXPECT_NE(r->find("name = \"okrun\""), std::string::npos) << *r; + EXPECT_NE(r->find("os.dirs(\"okrun-0.1.3-*\")"), std::string::npos) << *r; + EXPECT_NE(r->find("description = \"openkal-tools 0.1.3"), std::string::npos) << *r; +} diff --git a/tests/unit/test_pack_stage_tree.cpp b/tests/unit/test_pack_stage_tree.cpp index f750d6c58..f8973118f 100644 --- a/tests/unit/test_pack_stage_tree.cpp +++ b/tests/unit/test_pack_stage_tree.cpp @@ -115,19 +115,20 @@ TEST(PackStageTree, TheOrderOfADirectoryWalkIsNotAPromise) { read_file(mcpp::pack::stage_manifest_path(b.path / "s"))); } -TEST(PackStageTree, TheEngineOwnsExactlyTwoFormatNames) { - // `tar` and `dir` are the archive shapes `mcpp pack` owns; every other - // value of `--format` is a name a package provides. The list lives beside - // the staged tree because two layers need the same answer -- the parser, - // which decides whether a value is a built-in or a dispatch, and the - // declaration check, which refuses a package that claims one of them and - // would therefore be silently unreachable. - EXPECT_TRUE(mcpp::pack::is_builtin_pack_format("tar")); - EXPECT_TRUE(mcpp::pack::is_builtin_pack_format("dir")); +TEST(PackStageTree, TheEngineOwnsFourFormatNames) { + // `tar`, `zip`, `dir` and `single-file` are the shapes `mcpp pack` owns + // (SPEC-014 §1.3); every other value of `--pkg-format` is a name a + // package provides. The list lives beside the staged tree because two + // layers need the same answer -- the listing and the refusal of an + // unknown name, and the declaration check, which warns of a package that + // claims one of them and would therefore be silently unreachable. + for (auto name : { "tar", "zip", "dir", "single-file" }) + EXPECT_TRUE(mcpp::pack::is_builtin_pack_format(name)) << name; EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("appimage")); + EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("okb")); EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("msi")); EXPECT_FALSE(mcpp::pack::is_builtin_pack_format("")); - EXPECT_EQ(mcpp::pack::kBuiltinPackFormats.size(), 2u); + EXPECT_EQ(mcpp::pack::kBuiltinPackFormats.size(), 4u); } // ── #630 §3: the `closure` field ───────────────────────────────────────── diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp new file mode 100644 index 000000000..b84b7e6e3 --- /dev/null +++ b/tests/unit/test_platform.cpp @@ -0,0 +1,453 @@ +#include + +import std; +import mcpp.manifest; +import mcpp.platform; +import mcpp.build.prepare_inputs; +import mcpp.toolchain.triple; + +// SPEC-013 (design 2026-10-28): what a platform states, how it is read in its +// three places, which platform a graph selects, how the tiers merge, and the +// two templates of the link (artifact names, seal steps). Tables, so each rule +// is one row. + +namespace mm = mcpp::manifest; +namespace mp = mcpp::manifest::platform; +namespace pf = mcpp::platform; +namespace cfgpred = mcpp::build::cfgpred; + +namespace { + +mm::Manifest parse(std::string_view text) { + auto m = mm::parse_string(text); + if (!m) ADD_FAILURE() << m.error().format(); + return m ? *m : mm::Manifest{}; +} + +std::string parse_error(std::string_view text) { + auto m = mm::parse_string(text); + return m ? std::string{} : m.error().message; +} + +pf::Matches matcher(std::string_view triple, std::string_view platformPackage = {}) { + auto c = cfgpred::context_for(triple, platformPackage); + return [c](std::string_view sel) { return cfgpred::matches(std::string(sel), c); }; +} + +constexpr std::string_view kPlatform = R"( +[package] +name = "board-platform" +version = "1.2.0" + +[platform] +targets = ["x86_64-openkal", "aarch64-openkal"] +toolchain = "llvm@23.1.3" +runner = { cmd = ["okrun", "{}"], host = "same-arch" } +carrier = { package = "openkal-run@0.1.1", section = ".oke", main = "carrier/main.cpp" } + +[platform.layers] +"c++-abi" = "openkal-llvm-runtime@0.16.3" +c-abi = { name = "openkal-musl", version = "0.22.3", default-features = false, features = ["posix"] } +kernel-abi = false + +[platform.tools] +okld = "openkal-tools@0.1.1" +probe = { program = "probe-rs" } + +[platform.build] +cxxflags = ["-fPIC"] +[platform.build.'cfg(arch = "aarch64")'] +cxxflags = ["-ffixed-x18"] + +[platform.link] +driver = "lld" +start = "ok_main" +flags = ["-shared"] +inputs = [{ from = ["okld", "note-source"], as = "ok-notes.S" }, { file = "crt/x.o" }] +exe = "{name}.oke" +shared = "lib{name}.okso" +seal = [["okld", "note", "{}"], ["okld", "check", "{}", "--with", "{needed}"]] + +[platform.hosted] +toolchain = "llvm@23.1.3" +[platform.hosted.layers] +c-abi = "openkal-musl@0.22.3" + +[platform.versions] +openkal-musl = "0.22.3" +)"; + +} // namespace + +// ── The reader ────────────────────────────────────────────────────────────── + +TEST(PlatformManifest, EveryGroupIsRead) { + auto m = parse(kPlatform); + ASSERT_TRUE(m.platformDeclared); + auto const& p = m.platform; + EXPECT_EQ(p.targets.value, (std::vector{"x86_64-openkal", "aarch64-openkal"})); + EXPECT_EQ(p.toolchain.value, "llvm@23.1.3"); + EXPECT_EQ(p.runner.cmd, (std::vector{"okrun", "{}"})); + EXPECT_EQ(p.runner.host, "same-arch"); + EXPECT_EQ(p.carrier.package.spec.shortName, "openkal-run"); + EXPECT_EQ(p.carrier.section, ".oke"); + EXPECT_TRUE(p.carrier.main.ends_with("carrier/main.cpp")); + ASSERT_TRUE(p.layers.contains("c++-abi")); + EXPECT_EQ(p.layers.at("c++-abi").ref.spec.version, "0.16.3"); + EXPECT_EQ(p.layers.at("c-abi").ref.spec.shortName, "openkal-musl"); + EXPECT_FALSE(p.layers.at("c-abi").ref.spec.defaultFeatures); + EXPECT_EQ(p.layers.at("c-abi").ref.spec.features, (std::vector{"posix"})); + EXPECT_TRUE(p.layers.at("kernel-abi").ref.off); + EXPECT_EQ(p.tools.at("okld").ref.spec.shortName, "openkal-tools"); + EXPECT_EQ(p.tools.at("probe").ref.program, "probe-rs"); + EXPECT_EQ(p.build.cxxflags, (std::vector{"-fPIC"})); + ASSERT_EQ(p.buildCfg.size(), 1u); + EXPECT_EQ(p.link.driver.value, "lld"); + EXPECT_EQ(p.link.start.value, "ok_main"); + ASSERT_EQ(p.link.inputs.size(), 2u); + EXPECT_EQ(p.link.inputs[0].as, "ok-notes.S"); + EXPECT_TRUE(p.link.inputs[1].file.ends_with("crt/x.o")); + EXPECT_EQ(p.link.seal.size(), 2u); + EXPECT_EQ(p.hosted.toolchain.value, "llvm@23.1.3"); + EXPECT_TRUE(p.hosted.layers.contains("c-abi")); + EXPECT_EQ(p.versions.at("openkal-musl"), "0.22.3"); + EXPECT_EQ(p.runner.origin.tier, mp::Tier::Platform); + EXPECT_GT(p.runner.origin.line, 0); +} + +TEST(PlatformManifest, RefusalsNameTheKey) { + const std::string head = "[package]\nname = \"p\"\nversion = \"1.0.0\"\n"; + struct Row { std::string body; std::string_view says; }; + const Row rows[] = { + { "[platform]\ntoolchain = \"llvm@23.1.3\"\n", "names the targets it serves" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.layers]\nc-lib = \"x@1\"\n", + "not a target-side layer" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.link]\ndriver = \"gold\"\n", + "\"compiler\"" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.link]\nexe = \"x.oke\"\n", + "{name}" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.link]\nexe = \"lib{name}.oke\"\n", + "begins with the target's name" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\nrunner = []\n", "empty command" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\nrunner = { cmd = [\"x\"], host = \"far\" }\n", + "same-arch" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.layers]\nc-abi = { version = \"1\" }\n", + "names the package too" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.layers]\nc-abi = { program = \"x\" }\n", + "for a tool only" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.link]\nshared = \"{name}{name}\"\n", + "exactly once" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.link]\nshared = \"lib/{name}.okso\"\n", + "names no directory" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\nplugin = \"gen.mcpp\"\n", + "must be \"build.mcpp\"" }, + { "[platform]\ntargets = [\"x86_64-openkal\"]\n[platform.layers]\n" + "c-abi = { git = \"https://x/y.git\", tag = \"v1\" }\n", + "a git source names the package too" }, + { "[target.'cfg(platform = \"openkal\")']\nplatform = \"x@1.0.0\"\n", + "cannot be selected under a selector" }, + { "[target.x86_64-openkal]\nlayers = \"x\"\n", "must be a table" }, + }; + for (auto const& r : rows) { + auto e = parse_error(head + r.body); + EXPECT_NE(e.find(r.says), std::string::npos) << r.body << "\n=> " << e; + } +} + +// An unknown key, in `[platform]`, its tables or `[host]`, is a warning: a +// manifest written for a later mcpp still reads (SPEC-013 §18.1). +TEST(PlatformManifest, UnknownKeysWarn) { + auto m = parse(R"( +[package] +name = "p" +version = "1.0.0" + +[platform] +targets = ["x86_64-openkal"] +runner = { cmd = ["okrun", "{}"], later = true } +[platform.hosted] +runner = ["x"] +[platform.link] +exe = "{name}.oke" +later = "x" +[platform.build] +rustflags = ["-x"] + +[host] +runner = ["x"] +)"); + std::string all; + for (auto const& w : m.schemaWarnings) all += w + "\n"; + for (std::string_view key : { "'later' is not a key of a runner", "[platform.hosted] has unsupported key 'runner'", + "[platform.link] has unsupported key 'later'", + "[platform.build] has unsupported key 'rustflags'", + "[host] has unsupported key 'runner'" }) + EXPECT_NE(all.find(key), std::string::npos) << key << "\n=> " << all; + EXPECT_EQ(m.platform.link.exe.value, "{name}.oke"); +} + +TEST(PlatformManifest, OverridesAndHostAreRead) { + auto m = parse(R"( +[package] +name = "app" +version = "0.1.0" + +[host] +platform = "openkal-platform@0.17.1" + +[target.'cfg(platform = "openkal")'] +runner = { cmd = ["my-okrun", "{}"], exclusive = true } +[target.'cfg(platform = "openkal")'.layers] +c-abi = { path = "../openkal-musl" } +[target.'cfg(platform = "openkal")'.tools] +okld = { path = "../openkal-image/tools" } +[target.'cfg(platform = "openkal")'.link] +flags = ["--build-id"] + +[target.x86_64-openkal] +platform = "none" +)"); + for (auto const& w : m.schemaWarnings) ADD_FAILURE() << "unexpected warning: " << w; + EXPECT_TRUE(m.hostPlatform.platformSet); + EXPECT_EQ(m.hostPlatform.platform.spec.shortName, "openkal-platform"); + ASSERT_EQ(m.platformOverrides.size(), 2u); + auto const& o = m.platformOverrides[0]; + EXPECT_EQ(o.fields.runner.cmd.front(), "my-okrun"); + EXPECT_TRUE(o.fields.runner.exclusive); + EXPECT_TRUE(o.fields.layers.at("c-abi").ref.spec.path.ends_with("openkal-musl")); + EXPECT_TRUE(o.fields.tools.at("okld").ref.spec.path.ends_with("openkal-image/tools")); + EXPECT_EQ(o.fields.link.flags.value, (std::vector{"--build-id"})); + EXPECT_EQ(o.fields.runner.origin.tier, mp::Tier::Manifest); + EXPECT_TRUE(m.platformOverrides[1].platformNone); + // The runner's command is the row's template as well. + EXPECT_EQ(m.targetOverrides.at("cfg(platform = \"openkal\")").runner.front(), "my-okrun"); +} + +TEST(PlatformManifest, ReferencesInEveryForm) { + struct Row { std::string_view text; std::string_view ns, name, version; }; + const Row rows[] = { + { "openkal-platform@0.17.1", "mcpplibs", "openkal-platform", "0.17.1" }, + { "mcpplibs.openkal-platform@0.17.1", "mcpplibs", "openkal-platform", "0.17.1" }, + { "acme.board@1.0.0", "acme", "board", "1.0.0" }, + }; + for (auto const& r : rows) { + auto ref = mp::parse_ref_text(r.text); + ASSERT_TRUE(ref) << r.text; + EXPECT_EQ(ref->spec.namespace_, r.ns); + EXPECT_EQ(ref->spec.shortName, r.name); + EXPECT_EQ(ref->spec.version, r.version); + } + EXPECT_FALSE(mp::parse_ref_text("openkal-platform")); + EXPECT_FALSE(mp::parse_ref_text("@1.0")); + EXPECT_EQ(mp::platform_name("openkal-platform"), "openkal"); + EXPECT_EQ(mp::platform_name("stm32-bsp"), "stm32-bsp"); +} + +// ── Selection: the first tier that answers decides ───────────────────────── + +TEST(PlatformSelect, TheTiersInOrder) { + auto m = parse(R"( +[package] +name = "app" +version = "0.1.0" +[target.'cfg(os = "openkal")'] +platform = "project-platform@1.0.0" +)"); + auto cfg = parse(R"( +[package] +name = "cfg" +version = "0.1.0" +[target.x86_64-openkal] +platform = "machine-platform@1.0.0" +)"); + auto t = matcher("x86_64-openkal"); + auto pick = [&](std::string_view flag, std::string_view env, bool project, bool config, + std::string_view column) { + pf::ChoiceInputs in{ flag, env, "MCPP_TARGET_X86_64_OPENKAL_PLATFORM", + project ? std::span(m.platformOverrides) + : std::span{}, + config ? std::span(cfg.platformOverrides) + : std::span{}, + column }; + auto s = pf::select_target(in, t); + EXPECT_TRUE(s) << (s ? "" : s.error()); + return s ? (s->none ? std::string("none") : s->ref.spec.shortName) + "|" + + std::string(mp::tier_name(s->origin.tier)) + : std::string("error"); + }; + EXPECT_EQ(pick("flag-platform@1.0.0", "env-platform@1.0.0", true, true, "row-platform@1.0.0"), + "flag-platform|flag"); + EXPECT_EQ(pick("", "env-platform@1.0.0", true, true, "row-platform@1.0.0"), "env-platform|env"); + EXPECT_EQ(pick("", "", true, true, "row-platform@1.0.0"), "project-platform|mcpp.toml"); + EXPECT_EQ(pick("", "", false, true, "row-platform@1.0.0"), "machine-platform|mcpp.toml"); + EXPECT_EQ(pick("", "", false, false, "row-platform@1.0.0"), "row-platform|built-in"); + EXPECT_EQ(pick("", "", false, false, ""), "none|built-in"); + EXPECT_EQ(pick("none", "", true, true, "row-platform@1.0.0"), "none|flag"); +} + +TEST(PlatformSelect, TheMostSpecificSelectorDecides) { + auto m = parse(R"( +[package] +name = "app" +version = "0.1.0" +[target.x86_64-openkal] +platform = "triple@1.0.0" +[target.'cfg(arch = "x86_64")'] +platform = "arch@1.0.0" +)"); + pf::ChoiceInputs in{ "", "", "", m.platformOverrides, {}, "" }; + auto s = pf::select_target(in, matcher("x86_64-openkal")); + ASSERT_TRUE(s); + EXPECT_EQ(s->ref.spec.shortName, "triple"); + auto a = pf::select_target(in, matcher("aarch64-openkal")); + ASSERT_TRUE(a); + EXPECT_TRUE(a->none); +} + +TEST(PlatformSelect, TheHostGraph) { + mp::Override project, config; + config.platformSet = true; + config.platform = *mp::parse_ref_text("machine@1.0.0"); + auto s = pf::select_host("", false, project, config); + ASSERT_TRUE(s); + EXPECT_EQ(s->ref.spec.shortName, "machine"); + project.platformSet = true; + project.platformNone = true; + s = pf::select_host("", false, project, config); + ASSERT_TRUE(s); + EXPECT_TRUE(s->none); + s = pf::select_host("flag@2.0.0", true, project, config); + ASSERT_TRUE(s); + EXPECT_EQ(s->ref.spec.shortName, "flag"); + EXPECT_EQ(s->origin.tier, mp::Tier::Flag); +} + +TEST(PlatformSelect, ADirectoryIsAPath) { + auto s = pf::parse_choice("path:/opt/board", {}); + ASSERT_TRUE(s); + // Absolute on this machine: `D:/opt/board` on a Windows one. + EXPECT_TRUE(s->ref.spec.path.ends_with("/opt/board")) << s->ref.spec.path; + s = pf::parse_choice("../board", {}); + ASSERT_TRUE(s); + EXPECT_FALSE(s->ref.spec.path.empty()); + EXPECT_FALSE(pf::parse_choice("not-a-reference", {})); +} + +// ── Merge: per field, flags appended, sequences replaced ─────────────────── + +TEST(PlatformMerge, TheRulesOfSection45) { + auto base = parse(kPlatform).platform; + mp::Statement over; + over.runner.cmd = {"my-okrun", "{}"}; + over.runner.set = true; // argv form: keeps host + over.build.cxxflags = {"-DUSER"}; + over.link.flags = { {"--build-id"}, {}, true }; + over.link.seal = { {"sign", "{}"} }; + over.link.sealSet = true; + over.layers["c-abi"] = { mp::PackageRef{ .off = true }, {} }; + pf::merge(base, over); + EXPECT_EQ(base.runner.cmd.front(), "my-okrun"); + EXPECT_EQ(base.runner.host, "same-arch"); + EXPECT_EQ(base.build.cxxflags, (std::vector{"-fPIC", "-DUSER"})); + EXPECT_EQ(base.link.flags.value, (std::vector{"-shared", "--build-id"})); + ASSERT_EQ(base.link.seal.size(), 1u); + EXPECT_EQ(base.link.seal.front().front(), "sign"); + EXPECT_EQ(base.link.inputs.size(), 2u); // not stated: kept + EXPECT_TRUE(base.layers.at("c-abi").ref.off); + EXPECT_FALSE(base.layers.at("c++-abi").ref.off); + EXPECT_EQ(base.link.start.value, "ok_main"); +} + +TEST(PlatformMerge, AHostTripleTakesTheHostedPartOnly) { + auto p = parse(kPlatform).platform; + auto h = pf::hosted_view(p); + EXPECT_EQ(h.toolchain.value, "llvm@23.1.3"); + EXPECT_TRUE(h.layers.contains("c-abi")); + EXPECT_FALSE(h.layers.contains("c++-abi")); + EXPECT_FALSE(h.runner.set); + EXPECT_FALSE(h.link.driver.set); + EXPECT_TRUE(h.build.empty()); + auto served = pf::serves(p, "board-platform", *mcpp::toolchain::triple::parse("x86_64-linux-gnu")); + ASSERT_TRUE(served); + EXPECT_TRUE(*served); + auto target = pf::serves(p, "board-platform", *mcpp::toolchain::triple::parse("x86_64-openkal")); + ASSERT_TRUE(target); + EXPECT_FALSE(*target); + p.hosted = {}; + auto refused = pf::serves(p, "board-platform", *mcpp::toolchain::triple::parse("x86_64-linux-gnu")); + ASSERT_FALSE(refused); + EXPECT_NE(refused.error().find("[platform.hosted]"), std::string::npos); +} + +TEST(PlatformMerge, CfgTablesFollowTheTarget) { + auto p = parse(kPlatform).platform; + auto x = pf::evaluate_flags(p, matcher("x86_64-openkal")); + auto a = pf::evaluate_flags(p, matcher("aarch64-openkal")); + EXPECT_EQ(x.cxxflags, (std::vector{"-fPIC"})); + EXPECT_EQ(a.cxxflags, (std::vector{"-fPIC", "-ffixed-x18"})); +} + +TEST(PlatformMerge, CfgPlatformMatchesTheNameOrThePackage) { + EXPECT_TRUE(cfgpred::matches("cfg(platform = \"openkal\")", + cfgpred::context_for("x86_64-openkal", "openkal-platform"))); + EXPECT_TRUE(cfgpred::matches("cfg(platform = \"openkal-platform\")", + cfgpred::context_for("x86_64-openkal", "openkal-platform"))); + EXPECT_FALSE(cfgpred::matches("cfg(platform = \"openkal\")", + cfgpred::context_for("x86_64-openkal"))); + EXPECT_GT(mcpp::manifest::cfg::specificity("cfg(os = \"openkal\")"), + mcpp::manifest::cfg::specificity("cfg(platform = \"openkal\")")); +} + +// ── The link's templates ──────────────────────────────────────────────────── + +TEST(PlatformLink, NamePatterns) { + auto e = pf::split_name_pattern("{name}.oke"); + ASSERT_TRUE(e); + EXPECT_EQ(e->first, ""); + EXPECT_EQ(e->second, ".oke"); + auto s = pf::split_name_pattern("lib{name}.okso"); + ASSERT_TRUE(s); + EXPECT_EQ(s->first, "lib"); + EXPECT_EQ(s->second, ".okso"); + EXPECT_FALSE(pf::split_name_pattern("name.oke")); + EXPECT_FALSE(pf::split_name_pattern("{name}{name}")); + EXPECT_FALSE(pf::split_name_pattern("bin/{name}")); +} + +TEST(PlatformLink, SealSteps) { + const std::vector check{"okld", "check", "{}", "--with", "{needed}"}; + const std::vector two{"a.okso", "b.okso"}; + EXPECT_EQ(pf::expand_step(check, "x.oke.tmp", two), + (std::vector{"okld", "check", "x.oke.tmp", "--with", "a.okso", + "--with", "b.okso"})); + EXPECT_EQ(pf::expand_step(check, "x.oke.tmp", {}), + (std::vector{"okld", "check", "x.oke.tmp"})); + EXPECT_EQ(pf::expand_step({"sign", "--image={}", "{needed}"}, "x", two), + (std::vector{"sign", "--image=x", "a.okso", "b.okso"})); +} + +// ── The report ────────────────────────────────────────────────────────────── + +TEST(PlatformReport, SourcesAndJson) { + pf::Resolved r; + r.active = true; + r.package = "mcpplibs.openkal-platform"; + r.version = "0.17.1"; + r.fields = parse(kPlatform).platform; + r.selection.origin = { mp::Tier::BuiltIn, {}, 0, "the target table" }; + r.layers["c-abi"] = { "openkal-musl@0.22.3", mp::Tier::Platform }; + r.layers["c++-abi"] = { "acme-runtime@1.0.0", mp::Tier::Graph }; + EXPECT_EQ(r.display(), "openkal-platform 0.17.1"); + EXPECT_EQ(r.identity(), "mcpplibs.openkal-platform@0.17.1"); + auto rows = pf::report_rows(r); + ASSERT_FALSE(rows.empty()); + bool graph = false; + for (auto const& [k, v] : rows) + if (k == "c++-abi") graph = v.find("(graph)") != std::string::npos; + EXPECT_TRUE(graph); + auto json = pf::to_json(r); + EXPECT_NE(json.find("\"name\":\"mcpplibs.openkal-platform\""), std::string::npos); + EXPECT_NE(json.find("\"source\":\"built-in\""), std::string::npos); + EXPECT_NE(json.find("\"layers.c++-abi\""), std::string::npos); + EXPECT_EQ(pf::to_json(pf::Resolved{}), "null"); +} diff --git a/tests/unit/test_pm_lock_io.cpp b/tests/unit/test_pm_lock_io.cpp index b1ac74f58..d0d3c146d 100644 --- a/tests/unit/test_pm_lock_io.cpp +++ b/tests/unit/test_pm_lock_io.cpp @@ -213,3 +213,34 @@ TEST(PmLockIo, UpdatingALinkedLockKeepsTheLinkAndUpdatesItsTarget) { ASSERT_EQ(loaded->packages.size(), 1u); EXPECT_EQ(loaded->packages[0].version, "2.0.0"); } + +// An entry names the configurations that resolve it, and reads back as written +// (SPEC-013 §15.1); a lock written before the field reads with none. +TEST(PmLockIo, TargetsRoundTrip) { + const auto dir = std::filesystem::temp_directory_path() / std::format("mcpp-lock-{}", + std::chrono::steady_clock::now().time_since_epoch().count()); + std::filesystem::create_directories(dir); + mcpp::pm::Lockfile lock; + lock.schemaVersion = 2; + mcpp::pm::LockedPackage p; + p.name = "openkal-musl"; + p.namespace_ = "mcpplibs"; + p.version = "0.22.6"; + p.source = "index+mcpplibs@0.22.6"; + p.hash = "fnv1a:0"; + p.origin = "platform"; + p.targets = { "aarch64-openkal", "x86_64-openkal" }; + lock.packages.push_back(p); + ASSERT_TRUE(mcpp::pm::write(lock, dir / "mcpp.lock")); + auto back = mcpp::pm::load(dir / "mcpp.lock"); + ASSERT_TRUE(back); + ASSERT_EQ(back->packages.size(), 1u); + EXPECT_EQ(back->packages[0].targets, p.targets); + EXPECT_EQ(back->packages[0].origin, "platform"); + std::ofstream(dir / "old.lock") << "version = 2\n\n[package.\"x\"]\nnamespace = \"mcpplibs\"\nversion = \"1\"\n"; + auto old = mcpp::pm::load(dir / "old.lock"); + ASSERT_TRUE(old); + EXPECT_TRUE(old->packages.at(0).targets.empty()); + std::error_code ec; + std::filesystem::remove_all(dir, ec); +}