From 40fcdc6f5d980f6425ff2c3a7b69cadf565e9598 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 29 Oct 2026 10:39:01 +0900 Subject: [PATCH 1/2] 2026.10.29.1: an ok image of another architecture runs through its execution host; test, pack, list-formats and self env follow the target; WebAssembly packages are neither xlings entries nor single files The 2026.10.29.1 release: seven defects an audit of 2026.10.28.1 measured for aarch64-openkal and wasm32-emscripten on x86_64 Linux. Design record: .agents/docs/2026-10-29-openkal-run-test-pack-design.md. - A same-arch runner runs on a machine of the artifact's architecture. With an execution host named (run_under, MCPP_TARGET__RUN_UNDER) and the architectures apart, a runner that is the platform's tool is the platform's runner for that architecture: the carrier with nothing carried, built for this system (mcpp::pack::runner_program), the program a self-contained universal package carries. The rule is mcpp::platform::runner_reach, read by the refusal, mcpp run, mcpp test and self env. run_under = ["qemu-aarch64"] runs aarch64-openkal on x86_64 (qemu was handed the x86_64 okrun: "Invalid ELF image for this architecture"). A platform package's mcpp test lets run_tests decide by the same rule. - mcpp test prints the not-run reason in full once (its run_under line had never reached the output); the summary keeps the first line. - mcpp self env --target reports the refusal (runner.refusal), the runner program's architecture (runner.arch), whether another architecture's runner is built (runner.built) and the execution host's program (runUnderProgram). - mcpp test --target records the build of the package's programs under the target; the next bare mcpp run no longer warns "fingerprint changed". - A self-contained package of one ok image carries it in a program of the image's architecture (it was an x86-64 program carrying an aarch64 image); a row this mcpp builds no program for is refused before the generated project is packed. - mcpp pack --target --list-formats plans that target and lists okb for an ok-image target. - mcpp emit xpkg describes a one-leg package for the system of its triple and refuses a leg that is no Linux (other than Android), macOS or Windows program, naming the route (dist-web for a WebAssembly package, the universal package for an ok image); every other triple was written linux. --pkg-format single-file is refused on a WebAssembly target before any compile. - SPEC-013 1.2 and SPEC-014 1.3 state these rules and how ok images relate to the web; docs/10, 24, 35, 41, 50 and docs/zh follow. Unit tests PlatformRun.TheRunnerReachesTheArtifactsArchitecture and PackDistribution.XpkgDescriptorDescribesProgramsOfASystem; e2e 960, 961, 962 (each fails on 2026.10.28.1). - The [Unreleased] entry (#858) is folded into 2026.10.29.1. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- ...2026-10-29-openkal-run-test-pack-design.md | 115 ++++++++++++++ .agents/docs/README.md | 4 +- CHANGELOG.md | 71 ++++++++- docs/10-pack-and-release.md | 22 ++- docs/24-openkal-cross.md | 44 ++++++ docs/35-platforms.md | 9 +- docs/41-devices.md | 6 +- docs/50-machine-output.md | 20 ++- docs/90-build-from-source.md | 4 +- docs/specs/README.md | 4 +- docs/specs/packaging.md | 32 +++- docs/specs/platforms.md | 55 +++++-- docs/zh/10-pack-and-release.md | 16 +- docs/zh/24-openkal-cross.md | 35 +++++ docs/zh/35-platforms.md | 9 +- docs/zh/41-devices.md | 4 +- docs/zh/50-machine-output.md | 15 +- docs/zh/90-build-from-source.md | 4 +- mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/build/execute.cppm | 143 +++++++++++++++-- src/cli/cmd_build.cppm | 50 ++++-- src/cli/cmd_publish.cppm | 7 +- src/pack/distribution.cppm | 43 ++++-- src/pack/pack.cppm | 97 +++++++++++- src/pack/pipeline.cppm | 10 ++ src/platform/platform.cppm | 20 +++ ...tecture_runs_through_its_execution_host.sh | 144 ++++++++++++++++++ ..._image_in_a_program_of_its_architecture.sh | 75 +++++++++ ...ither_an_xlings_entry_nor_a_single_file.sh | 73 +++++++++ tests/e2e/timings/linux.tsv | 3 + tests/unit/test_pack_distribution.cpp | 40 +++++ tests/unit/test_platform.cpp | 36 +++++ 33 files changed, 1117 insertions(+), 97 deletions(-) create mode 100644 .agents/docs/2026-10-29-openkal-run-test-pack-design.md create mode 100755 tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh create mode 100755 tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh create mode 100755 tests/e2e/962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh diff --git a/.agents/docs/2026-10-29-openkal-run-test-pack-design.md b/.agents/docs/2026-10-29-openkal-run-test-pack-design.md new file mode 100644 index 000000000..7ce7c7c1f --- /dev/null +++ b/.agents/docs/2026-10-29-openkal-run-test-pack-design.md @@ -0,0 +1,115 @@ +--- +subject: platform +status: active +--- + +# 另一架构的 ok 映像的运行、测试与打包,以及 ok 映像与 Web 的关系(2026.10.29.1) + +日期:2026-10-29。基线:mcpp `7b5f10aa`(2026.10.28.1 + #858)。前文:[platforms design](2026-10-28-platforms-design.md)(SPEC-013)、 +[part 2](2026-10-28-platforms-part2-packaging-and-distribution-design.md)(SPEC-014)。相关规范:SPEC-002 §6(执行宿主)、SPEC-013、SPEC-014。 + +**修订记录** + +- rev 1(本稿):七项的实测与定案;一个发布 PR。 + +## 0. 摘要 + +2026.10.28.1 在 x86_64 Linux 上对 `aarch64-openkal` 的审计发现七处缺陷,全部源于同一个假设:运行器的程序、载体与列出的格式都按 +**本机**决定,而它们应当按**目标**决定。本批的规则:`same-arch` 的运行器在产物架构的机器上运行,给出执行宿主时其程序是目标架构的 +(平台载体的运行器形式);单目标的 self-contained 包的载体是映像架构的;`--list-formats` 与 `self env` 报告所选目标的配置; +`mcpp test` 的人类输出给出完整的拒绝原因一次。Web 不是 ok 映像的目标:ok 映像只有 x86_64 与 aarch64,Web 经 `wasm32-emscripten` 与 +`dist-web` 到达;`emit xpkg` 与 `--pkg-format single-file` 拒绝 WebAssembly 包。 + +## 1. 实测(本机构建的 2026.10.28.1,x86_64 Linux,`/usr/bin/qemu-aarch64` 8.2.2) + +| 编号 | 观察 | 位置 | +|---|---|---| +| F1 | `mcpp run`、`mcpp test --target x86_64-openkal`、`mcpp run` 依次运行,第三条打印 `warning: fingerprint changed (28312234d2e59c78 → 7ff323110ea05b87), full rebuild`:测试前构建包的程序时,缓存条目的 `[target=]` 为空 | `src/build/execute.cppm:4319`,`run_build_plan(*binCtx, …, false)` 缺第四个参数;`mcpp run` 在 3187–3205 修过同一缺陷 | +| F2 | `mcpp pack --target aarch64-openkal --no-universal --mode self-contained` 写出 `bin/app: ELF 64-bit LSB executable, x86-64`,携带 aarch64 映像,在任何机器上都不能运行 | `src/pack/pack.cppm:2517–2519` 传 `host_arch`;单文件(3258)取映像的架构 | +| F3 | `mcpp pack --target aarch64-openkal --list-formats` 列出 `tar, zip, dir, single-file`,没有 `okb` | `src/cli/cmd_publish.cppm:274–292` 只设 profile 与 features | +| F4 | `mcpp test --target aarch64-openkal` 的摘要只有原因的第一行,`run_under` 的写法没有出现;注释称全文已在确立时打印,而该原因(`invocationNotRunReason`)在 `test_resolve_runner` 中被静默记录 | `execute.cppm:3704–3705`、`4185–4189` | +| F5 | `MCPP_TARGET_AARCH64_OPENKAL_RUN_UNDER=qemu-aarch64 mcpp run --target aarch64-openkal`:`qemu-aarch64: …/openkal-tools@0.1.3/…/bin/okrun: Invalid ELF image for this architecture`,退出 255 | 运行器首词 `okrun` 总是解析到 `platform_tool`(`execute.cppm:1078–1084`,用于 2876、3716),即为本机构建的工具 | +| F6 | 同一环境下 `mcpp self env --target aarch64-openkal` 报告 `runner okrun {} (platform) -> …/okrun`(x86_64),不说明本机拒绝运行,也不说明给出执行宿主后用哪个程序 | `src/cli/cmd_build.cppm` 的 `cmd_env_configuration` | +| F7a | `mcpp emit xpkg --package web-0.1.0-wasm32-emscripten.tar.gz` 写出 `archs = {"wasm32"}`、只有 `linux` 一项 | `src/pack/distribution.cppm` 的 `xlings_system`:不是 Windows、macOS 的三元组一律为 `linux` | +| F7b | `--pkg-format single-file` 在 wasm 目标上没有设计也没有拒绝,代码会去构建 `wasm32-linux-musl` 的启动器 | `pack.cppm:2264–2268`、`3305–3309` | + +F5 的对照:把运行器手工改为 `cmd = ["qemu-aarch64", "", "{}"], host = "any"`,其中程序取 self-contained 通用包的 +`libexec/okrun/linux-aarch64/okrun`,`mcpp run` 与 `mcpp test` 通过。 + +## 2. 定案 + +### 2.1 运行器的程序按执行宿主的架构决定(F5、F6) + +`host = "same-arch"` 的含义是"产物在其架构的机器上运行"。给出执行宿主(`run_under`,含 `MCPP_TARGET__RUN_UNDER`)时,运行器在 +执行宿主上运行,执行宿主的架构即产物的架构,所以运行器的程序**必须**是该架构的。规则只看三件事:运行器是否 `same-arch`、目标架构 +是否等于本机架构、是否给出了执行宿主;`--no-runner` 声明三元组在本机是原生的,不经运行器。三者的组合只有三种结果——本机的程序、目标 +架构的程序、拒绝——由 `mcpp.platform` 的一个纯函数 `runner_reach` 回答,拒绝(`unreachable_execution`)、运行器程序的解析 +(`mcpp run`、`mcpp test`)与 `self env` 都读它,单元测试按表覆盖。 + +目标架构的程序是平台的载体以运行器形式(不携带任何内容,SPEC-013 §11)为**本机系统**与**目标架构**构建的程序,与 self-contained +通用包放在 `libexec//-/` 中的是同一个程序,由同一个函数构建(`carrier_build`,经新导出的 +`mcpp::pack::runner_program`)。系统取本机的:执行宿主由本机经 `run_under` 到达,mcpp 不知道它的系统;用户态模拟器(`qemu-aarch64`) +与同系统的远程机器两种情形都成立。只对运行器首词是平台工具的情形改用目标架构的程序:首词是别的程序时,它不是平台构建的,mcpp +没有它的其他架构的版本。平台没有 `carrier`、或本 mcpp 不为该系统与架构构建程序(`row_is_built`)时,运行被拒绝并写出原因,而不是 +退回本机的程序(退回即 F5)。`host = "any"` 的运行器不受影响:它从任何机器到达产物,其程序是本机的。 + +`self env --target` 报告运行器程序的架构;给出执行宿主时报告目标架构的程序与它是否已构建(第一次运行时构建)以及执行宿主的程序; +不给时报告拒绝的原因。`self env` 只准备不构建,不为报告而构建载体。 + +平台包中不带 `--target` 的 `mcpp test` 原本在 CLI 层对其他架构的目标设 `noRun`;它改为交给 `run_tests` 按同一规则判断:拒绝时 +报告为已构建未运行(原行为),给出执行宿主时运行。 + +### 2.2 单目标 self-contained 包的载体是映像架构的(F2) + +载体程序的架构取映像的(`arch_of(plan.triple)`),系统取本机的,与单文件一致。本 mcpp 不构建该行(如 Windows 主机上的 +`aarch64-windows-gnu`)时 `carrier_build` 直接拒绝并写出系统、架构与行,不再进入生成工程的打包后才失败。 + +### 2.3 `--list-formats` 准备所选目标(F3) + +`--target`(多次给出时取最后一个,与打包相同)进入准备;平台阶段声明的格式(`okb`)因此出现在列表中。 + +### 2.4 `mcpp test` 的拒绝原因给出一次全文(F4) + +原因在运行阶段开始、第一个测试被报告为未运行之前,以警告打印一次全文(每次构建一次,工作空间的多个成员共用同一次构建时也是一次); +摘要保留第一行。与内核拒绝 spawn 的情形(`hostCannotRunReason`)相同,于是摘要处的注释成立。 + +### 2.5 F1 + +`run_build_plan(*binCtx, …, overrides.target_triple)`。 + +### 2.6 WebAssembly 包(F7a、F7b) + +**`emit xpkg`:拒绝。** SPEC-014 §8 的条目有两种形状:通用包(启动器在包内,经平台运行器启动映像,三个系统同一资产)与按系统的 +包(`xpm.` 安装该系统的程序,`archs` 为机器架构)。`wasm32-emscripten` 的包两者都不是:它没有启动器,`bin/.js` 由 +JavaScript 引擎或浏览器执行,`wasm32` 不是任何系统的机器架构,xvm 注册的 `bin/` 不存在。把它描述成系统无关的条目需要为 wasm +生成启动器并在条目中写运行器前缀,即一个新形状,而 Web 的分发路径是 `dist-web` 产出的页面,不是 xlings。规则按模型写成一般形式: +单腿包按其腿的三元组的系统描述,三元组不是 Linux(非 Android)、macOS 或 Windows 程序时拒绝。同一条规则也拒绝其他不属于这三个系统 +的单腿包(Android、iOS、裸机,以及 `--no-universal` 的 ok 映像包——xlings 经通用包安装 ok 映像);此前它们都被写成 `linux` +或 `macosx`。 + +**`--pkg-format single-file`:拒绝。** 单文件是一个系统执行的可执行文件(原生启动器或载体);WebAssembly 模块不是。拒绝在 +`refused_combination` 中,任何构建之前,写出两个选择与出路(`tar`、`zip`、`dir` 或提供者的格式,页面为 `dist-web`)。 + +### 2.7 ok 映像与 Web(文档) + +ok 映像是 x86_64 与 aarch64 上位置无关的 ELF,只导入 `kal_*`,需要加载器发布的可执行内存;okb 只装 ok 映像。Web 经 +`wasm32-emscripten` 目标(打包为 `dist-web`)到达,使用 openkal API 的代码经 openkal-emscripten 实现到达。SPEC-013 §14、SPEC-014 §0 +与 docs/24 各写一句。 + +## 3. 测试 + +| 项 | 单元 | e2e | +|---|---|---| +| F1 | — | 960 A:`run`、`test --target`、`run` 不出现 fingerprint 警告,缓存含该目标的条目 | +| F4 | — | 960 B:完整原因(含 `run_under` 的写法)恰好一次 | +| F5 | `PlatformRun.TheRunnerReachesTheArtifactsArchitecture`(`runner_reach` 的表) | 960 D:记录 argv 的执行宿主收到的运行器程序是另一架构的 ELF;本机有该架构的 qemu 时真实运行 `run` 与 `test` | +| F6 | — | 960 C:人类输出给出拒绝;JSON 的 `runner.refusal`、`runner.arch`;给出执行宿主时为另一架构 | +| F2、F3 | — | 961:载体为映像架构的 ELF(有 qemu 时运行它);`--target -openkal --list-formats` 含 `okb`,本机目标不含 | +| F7a | `XpkgDescriptorDescribesProgramsOfASystem` | 962 A:合成的 wasm32-emscripten 包被拒绝,消息写出三元组与出路 | +| F7b | — | 962 B:有 emsdk 载荷时 `--pkg-format single-file --target wasm32-emscripten` 在编译前被拒绝 | + +## 4. 文档 + +SPEC-013 1.2(§10 规则、§11 单目标包的架构、§14 ok 映像与 Web、§16 `self env` 与平台包的 `mcpp test`、§18 判据 7 与 12); +SPEC-014 1.3(§0 ok 映像与 Web、§1.2、§1.5 两行、§5.2、§8 的系统规则、§10 判据 8、9);docs/10、24、35、41、50 与 docs/zh 对应章节; +docs/90 的计数;CHANGELOG 2026.10.29.1(并入 Unreleased 的 #858)。 diff --git a/.agents/docs/README.md b/.agents/docs/README.md index c42752893..4ac745e00 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 --- ``` -340 records. +341 records. ## By subject @@ -93,6 +93,7 @@ Records that declare one. Everything else is listed by date below. ### platform +- [另一架构的 ok 映像的运行、测试与打包,以及 ok 映像与 Web 的关系(2026.10.29.1)](2026-10-29-openkal-run-test-pack-design.md) — active - [平台 part 2:打包与分发——三维模型、通用包、单文件、okb、体积,以及 openkal 0.17.2 轮](2026-10-28-platforms-part2-packaging-and-distribution-design.md) — landed - [平台:目标体系的通用扩展;openkal 作为第一个平台包;IDE 一线与 mcppls 的同批升级](2026-10-28-platforms-design.md) — landed @@ -149,6 +150,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-10 +- [另一架构的 ok 映像的运行、测试与打包,以及 ok 映像与 Web 的关系(2026.10.29.1)](2026-10-29-openkal-run-test-pack-design.md) — active - [平台 part 2:打包与分发——三维模型、通用包、单文件、okb、体积,以及 openkal 0.17.2 轮](2026-10-28-platforms-part2-packaging-and-distribution-design.md) — landed - [平台:目标体系的通用扩展;openkal 作为第一个平台包;IDE 一线与 mcppls 的同批升级](2026-10-28-platforms-design.md) — landed - [生态设计 part 2:openkal 的最小原子与二进制映像、其上的 `mcpplibs.os`,以及 part 1 的收尾与回归记录](2026-10-26-ecosystem-part2-os-library-over-openkal-design.md) — active diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbb7528f..96a2ae881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,70 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. -## [Unreleased] +## [2026.10.29.1] - 2026-10-29 ### Fixed +- **An image of another architecture runs through its execution host.** + openkal-platform's runner is `{ cmd = ["okrun", "{}"], host = "same-arch" }`, + and its first word resolved to the `okrun` built for this machine whatever + the execution host: `run_under = ["qemu-aarch64"]` for `aarch64-openkal` on + x86_64 handed qemu the x86_64 okrun ("Invalid ELF image for this + architecture"). A `same-arch` runner runs on a machine of the artifact's + architecture, so with an execution host named (`[target.] run_under`, or + `MCPP_TARGET__RUN_UNDER`) a runner that is the platform's tool is now the + platform's runner for that architecture: the carrier with nothing carried, + built for this system, the program a self-contained universal package + carries under `libexec//-/` (SPEC-013 §10). `mcpp run` and + `mcpp test` of `aarch64-openkal` pass under `qemu-aarch64` on x86_64 Linux + (e2e 960). With nothing named the refusal stays; `--no-runner` keeps its + meaning; a platform with no carrier, or a system and architecture this mcpp + builds no program for, is refused rather than handed this machine's tool. In + a platform package, `mcpp test` with no `--target` runs a target of another + architecture an execution host is named for, and reports the others built + and not run, as before (measured on a copy of openkal-platform 0.17.3). +- **`mcpp test` prints the refusal in full, once.** The reason a target is + not run here was recorded silently and the summary printed its first line, + so the `run_under` line it carries never reached a person (SPEC-013 §18, + criterion 7). It is printed as a warning before the first test it leaves + not run, once per build; the summary keeps the first line (e2e 960). +- **`mcpp self env --target` states how the target runs here.** It reported + the runner resolving to this machine's `okrun` for `aarch64-openkal` and + said nothing of the refusal. It now prints the refusal on a `run` line + (JSON `runner.refusal`), the architecture of the runner's program + (`runner.arch`), and, with an execution host named, the other architecture's + runner, whether the first run has built it (`runner.built`; `self env` + builds nothing) and the execution host's program (`runUnderProgram`) + (e2e 960). +- **`mcpp test --target` records its program build under the target.** The + package's programs, built before its tests, were recorded in the build cache + as this machine's build, so the next bare `mcpp run` printed `fingerprint + changed (… → …), full rebuild`; `mcpp run --target` was fixed for the same + defect earlier (e2e 960). +- **A self-contained package of one ok image is a program of the image's + architecture.** `mcpp pack --target aarch64-openkal --no-universal --mode + self-contained` built the carrier for this machine's architecture: an x86-64 + program carrying an aarch64 image, which runs on no machine. The carrier is + now this system's program of the image's architecture, as a single file's + is; a row this mcpp builds no program for is refused before the generated + project is packed, naming the row (SPEC-014 §1.2; e2e 961). +- **`mcpp pack --target --list-formats` lists that target's formats.** The + plan ignored `--target` and listed this machine's; for an ok-image target + it now lists `okb`, which the platform's plugin provides (SPEC-014 §6; + e2e 961). +- **`mcpp emit xpkg` describes a package for the system its program runs on.** + Every triple that was not Windows or macOS was written `linux`: a + `wasm32-emscripten` package became a Linux entry with `archs = {"wasm32"}`. + An entry's `xpm.` installs a program of that system for machine + architectures, and a WebAssembly package has no launcher, no system and no + machine architecture, so a package of one leg that is no Linux (other than + Android), macOS or Windows program is refused, naming the route that + distributes it: a page through `dist-web`, or for an ok image its + platform's universal package (SPEC-014 §8; e2e 962). +- **`--pkg-format single-file` is refused on a WebAssembly target.** A single + file is an executable of one system; the pack tried a `wasm32-linux-musl` + launcher. The refusal comes before anything compiles and names `tar`, + `zip`, `dir` and a provider's format (SPEC-014 §1.5; e2e 962). - **The release requests its index bump with the permissions its token has.** `bump_index.sh` started the index's updater with a workflow_dispatch, which needs Actions: write; the release's token holds Contents and Pull requests @@ -16,6 +76,15 @@ index's `xpkgindex-update-request.yml` passes on to the updater (openxlings/xim-pkgindex#1087). +### Changed + +- **SPEC-013 1.2 and SPEC-014 1.3** state the rules above, and how ok images + relate to the web: an ok image is position-independent ELF for x86_64 and + aarch64 that imports only `kal_*` and runs in executable memory its loader + publishes, an okb holds ok images only, and the web is reached through the + `wasm32-emscripten` target and the `dist-web` provider, openkal API code + through openkal-emscripten. + ## [2026.10.28.1] - 2026-10-28 ### Fixed diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 2b1e50446..b03c35fc0 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -731,8 +731,8 @@ runs the package must provide: |---|---|---| | `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 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 | +| `self-contained` | `bin/` — a program of this machine's system and the image's architecture 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's system and the image's architecture that carries one static `.oke` | nothing | 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 @@ -740,7 +740,11 @@ 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. +(musl) on Linux, with only the system's libraries on macOS and Windows. The +program is of the image's architecture (2026.10.29.1+): on x86_64 Linux, +`--target aarch64-openkal` packs an aarch64 Linux program. A system and +architecture this mcpp builds no program for (`aarch64-windows-gnu`, on a +Windows machine) is refused, naming the row. ### Android: the application object and its closure under `lib/` @@ -855,6 +859,10 @@ 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). +A WebAssembly target has no single file: `--pkg-format single-file` with +`--target wasm32-emscripten` is refused before anything compiles +(2026.10.29.1+), and its page is the `dist-web` provider's format. + ### `mcpp-package.json` Every staged tree carries `mcpp-package.json`: the package, its platform, its @@ -887,7 +895,11 @@ sha256 per architecture. The install moves the archive's top directory into place and registers the program with xvm (the `.cmd` launcher on Windows), from each system's own directory when the packages place the program apart (a Windows program at the root, a POSIX one under `bin/`). Each archive is its -own `--package`. +own `--package`. A package of one leg is described for the system of its triple +(Linux, macOS or Windows); one of another target is refused, naming the route +that distributes it (2026.10.29.1+): a `wasm32-emscripten` package is published +as a page (`dist-web`), an ok image as its platform's universal package, and an +Android, iOS or bare-metal package has no entry. ### Compression and signing @@ -945,6 +957,8 @@ The `static` mode additionally requires a musl toolchain configured under and `single-file`. - One file that runs on several systems is not produced; a single-file package serves one system and one architecture. +- A WebAssembly package is neither a single file nor an xlings entry; its page is + a provider's format (`dist-web`). - 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/24-openkal-cross.md b/docs/24-openkal-cross.md index 4bc07a540..71c54480e 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -432,6 +432,50 @@ anything compiles, saying where the layer was switched off. requirements note would be refused by every loader; the refusal (`platform-tool-absent`) names the package that builds it. +### An Image Of Another Architecture (mcpp 2026.10.29.1) + +`okrun` runs an image of its own architecture (`host = "same-arch"`). On a +machine of another architecture `mcpp run` and `mcpp test` refuse the image and +name `run_under`, the key that states the execution host. With one named, the +runner runs there, and its program is the platform's runner for the image's +architecture: mcpp builds the platform's carrier with nothing carried, for this +system and that architecture, which is the program a self-contained universal +package carries under `libexec/okrun/-/`. A user-mode emulator is an +execution host; on an x86_64 Linux machine with `qemu-aarch64`: + +```toml +[package] +name = "app" +version = "0.1.0" + +[target.aarch64-openkal] +run_under = ["qemu-aarch64"] +``` + +``` +$ mcpp run --target aarch64-openkal + Packing okrun for linux-aarch64 (openkal-platform 0.17.3's carrier, the runner on the execution host) + Running `okrun … target/aarch64-openkal/…/bin/app.oke` + Via qemu-aarch64 [/…/app/mcpp.toml:6] +``` + +`mcpp test --target aarch64-openkal` runs the tests the same way. +`MCPP_TARGET_AARCH64_OPENKAL_RUN_UNDER=qemu-aarch64` names the execution host +for one run, and `[target.aarch64-openkal] run_under` in `~/.mcpp/config.toml` +for one machine. `mcpp self env --target aarch64-openkal` reports the runner's +program and its architecture and the execution host's program, or why runs +are refused on this machine. A self-contained package of one image +(`--no-universal --mode self-contained`) is a program of this system and the +image's architecture ([10](10-pack-and-release.md)). + +### ok Images And The Web + +An ok image is position-independent ELF for x86_64 and aarch64 that imports +only `kal_*` and runs in executable memory its loader publishes; an okb holds +ok images only. The web is reached through the `wasm32-emscripten` target, whose +package the `dist-web` provider writes as a page; code written against the +openkal API reaches it through `openkal-emscripten` (above). + ## Bare Metal A target with no operating system is the same model with the platform layer diff --git a/docs/35-platforms.md b/docs/35-platforms.md index 40246b781..fd852b66f 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -54,7 +54,9 @@ mcpp run --target x86_64-openkal ``` Every value is reported with its source. `mcpp test`, `mcpp pack` and -`--target aarch64-openkal` need nothing more. `mcpp.lock` records the packages +`--target aarch64-openkal` need nothing more; an image of another architecture +than this machine's runs through an execution host +([24](24-openkal-cross.md)). `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. @@ -245,12 +247,13 @@ 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. +architecture and those an execution host is named for (2026.10.29.1+), 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 # toolchain, platform, tools, runner, and why runs are refused here 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 diff --git a/docs/41-devices.md b/docs/41-devices.md index 4dcbb2672..db3222791 100644 --- a/docs/41-devices.md +++ b/docs/41-devices.md @@ -104,7 +104,11 @@ 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 +runner that reaches the artifact from any machine. With an execution host named +(`run_under`), a `same-arch` runner runs on it, and a runner that is the +platform's tool is given as the platform's runner for the artifact's +architecture, which mcpp builds from the platform's carrier (2026.10.29.1+; +`run_under = ["qemu-aarch64"]` in [24](24-openkal-cross.md)). 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 diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index e988d6cc4..5927273c6 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -353,14 +353,23 @@ select as they do for a build ([35](35-platforms.md)). "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": [] + "program": "/…/okrun", "arch": "x86_64", "built": true, "refusal": null, + "source": "platform"}, + "runUnder": [], + "runUnderProgram": null } ``` `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 +one, else the program found on this machine. For the target graph +(2026.10.29.1+) the program is the one `mcpp run` and `mcpp test` would start: +`runner.arch` is its architecture; with an execution host named for an artifact +of another architecture and a `same-arch` runner, it is the platform's runner +for that architecture, located where the first run builds it (`built: false` +until then; `self env` builds nothing). `runUnderProgram` is the execution +host's program, and `runner.refusal` the reason runs are refused on this +machine (`null` when they are not). 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 @@ -1029,7 +1038,10 @@ mcpp pack --list-formats `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. +nothing is written into the project. With `--target ` (2026.10.29.1+; +the last one when repeated) the graph is that target's, and the formats of its +platform are listed: `mcpp pack --target x86_64-openkal --list-formats` names +`okb`, provided by `mcpplibs.openkal-platform`. ## Current limitations diff --git a/docs/90-build-from-source.md b/docs/90-build-from-source.md index b49c167d0..2675a3e41 100644 --- a/docs/90-build-from-source.md +++ b/docs/90-build-from-source.md @@ -82,7 +82,7 @@ src/ tests/ ├── unit/ 156 C++ tests, discovered by `mcpp test` -└── e2e/ 646 shell scripts against a real binary +└── e2e/ 649 shell scripts against a real binary ``` ## Test Organization @@ -93,7 +93,7 @@ Two layers, and they answer different questions. discovers. They exercise a module's contract with no binary and no filesystem state. -**End-to-end tests** (`tests/e2e/NN_.sh`, 646 files) run a real `mcpp` +**End-to-end tests** (`tests/e2e/NN_.sh`, 649 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/specs/README.md b/docs/specs/README.md index 221143bea..8c35ca1d0 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -42,8 +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.2 | 2026-10-28 | mcpp >= 2026.10.28.1;逐条标注 | +| [SPEC-013](platforms.md) | 平台:`[platform]` 的七组键、版本组与 `hosted`,三种写法与层次,目标图与宿主图的选择,`cfg(platform)`,依赖图的补全,链接、命名与封装,运行器与载体,平台阶段与协议 17 | 评审中 v1.2 | 2026-10-29 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1 | +| [SPEC-014](packaging.md) | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与 `mcpp-package.json`,通用包、单文件、签名、压缩、合并与分发描述 | 评审中 v1.3 | 2026-10-29 | mcpp >= 2026.10.29.1;逐条标注 | ## 文档约定 diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md index 49c5fa6d7..547edde78 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -4,11 +4,11 @@ |---|---| | 规范编号 | SPEC-014 | | 标题 | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与包元数据,通用包、单文件、签名、压缩、合并与分发描述 | -| 状态 | 评审中 v1.2 | -| 版本 | 1.2 | -| 最后修改 | 2026-10-28 | -| 对应实现 | mcpp >= 2026.10.28.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) | +| 状态 | 评审中 v1.3 | +| 版本 | 1.3 | +| 最后修改 | 2026-10-29 | +| 对应实现 | mcpp >= 2026.10.29.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)、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md`(§2.2、§2.6) | | 相关 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) | @@ -20,6 +20,10 @@ 本规范规定 `mcpp pack` 为一个**程序**目标产出可分发物的模型、管线、暂存树、包元数据、容器与接口,以及 `mcpp run --pkg-format` 与 `mcpp emit xpkg` 读取的部分。库的预编译包(docs/12)不在本规范内。 +ok 映像是 x86_64 与 aarch64 上位置无关的 ELF,只导入 `kal_*`,运行时需要加载器发布的可执行内存(SPEC-013 §14); +通用包、okb 与映像的单文件只装 ok 映像。Web 经 `wasm32-emscripten` 目标到达,其包为 WebAssembly 的同名文件族 +(§4.1),发布为页面经 `dist-web` 提供者(§6);以 openkal API 写的代码经 openkal-emscripten 到达 Web。 + ## 1. 模型:三个维度与构建腿 一次打包的选择由三个正交维度与一组构建腿组成,每个维度回答一个问题: @@ -45,7 +49,7 @@ openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` |---|---|---|---| | `system` | 程序 | 映像 | 全部共享库;openkal 上为加载器与映像所需的 `.okso` | | `vendored`(缺省) | 程序与非系统共享库 | 映像与其 `.okso` | C 库、C++ 运行时与加载器;openkal 上为加载器 | -| `self-contained` | 程序、全部共享库、C 与 C++ 运行时、加载器 | 映像、`.okso` 与本机程序形式的加载器(载体,SPEC-013 §11) | 无 | +| `self-contained` | 程序、全部共享库、C 与 C++ 运行时、加载器 | 映像、`.okso` 与程序形式的加载器(载体,SPEC-013 §11):单腿包为本机系统、映像架构的程序(2026.10.29.1),通用包为 `--systems` 的每个系统与每个架构的运行器(§4.4) | 无 | `bundle-project` 与 `bundle-all` 是 `vendored` 与 `self-contained` 的永久别名。 @@ -82,6 +86,7 @@ openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` | `--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` | +| `--pkg-format single-file` 与 WebAssembly 目标(`wasm32-*`,不论目标来自 `--target`、`[build] target` 或其他来源) | 拒绝:单文件是一个系统的可执行文件,WebAssembly 模块由 JavaScript 引擎运行;出路 `--pkg-format tar`、`zip`、`dir` 或提供者的格式(页面为 `dist-web`)(2026.10.29.1) | | 需要宿主提供能力的程序(docs/04 §2.11)与 `--mode self-contained` 或 `static` | 拒绝:携带自己 C 库的包不能使用宿主提供的库;出路 `--mode vendored` | | `cxx_runtime = "toolchain-coupled"` 与 `--mode system` | 拒绝 | | 产物为可执行文件、非通用包,且多条 `--target` | 拒绝:一个可执行文件属于一个目标 | @@ -256,6 +261,7 @@ liblzma)压缩,不调用 `tar` 等外部程序;产物扩展名为 `.tar.gz`(缺 | 原生目标,`vendored` 或 `self-contained` | 引擎的启动器,携带暂存树的一个 zstd 帧 | 首次运行把树解到用户缓存,之后直接执行 | | 原生 Linux 目标,`--linkage static` | 静态启动器加静态程序 | 同上,不依赖任何系统库 | | openkal 目标 | 平台的载体,携带映像,或全部映像的一个 zip | 载体经加载器库从内存加载,不写盘 | +| WebAssembly 目标 | 不产出(§1.5) | — | 原生目标的启动器: @@ -291,6 +297,9 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 平台插件(SPEC-013 §12)**可以**在平台阶段声明包格式与 artifact action;平台阶段的指令表因此包含 `pack-format` 与 `role = "artifact"` 的 action(其他角色的 action 仍被拒绝)。该平台的目标打包时,这些格式与引擎的格式并列。 +`mcpp pack --list-formats` 规划这次打包会构建的目标(`--target`,多次给出时取最后一个;未给出时为工程的目标)而不构建, +列出引擎的格式与该目标的依赖图(含其平台)声明的格式(2026.10.29.1)。 + ## 7. 签名 `[pack] sign = ["", ..., "{}"]` 在写容器之前对暂存树中角色为 `program`、`image`、`library`、`loader` 的每个 @@ -308,7 +317,11 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 中同名的包;openkal 为 `okrun`)。依赖带命名空间:不带命名空间的名字在家目录列出的第二个索引也有同名包时有歧义, xim-pkgindex 拒绝它。 - 各系统各架构一个的包:每个系统一项,一个以 `${arch}` 代替架构的 URL 与每个架构的 sha256;文件名除架构外不同时 - 拒绝。 + 拒绝。单腿包的系统由其腿的三元组决定:Linux(Android 除外)为 `linux`,macOS 为 `macosx`,Windows 为 `windows`。 + 腿不是这三个系统之一的程序时(WebAssembly 包、Android 与 iOS 应用、裸机映像、单腿的 ok 映像包),条目**必须**被拒绝, + 消息写出包、三元组与出路:WebAssembly 包以 `dist-web` 发布为页面,ok 映像以平台的通用包(不带 `--no-universal` 的 + 打包)描述。原因:条目的 `xpm.<系统>` 安装该系统的程序,`archs` 为机器架构;WebAssembly 包没有启动器,其程序由 + JavaScript 引擎运行,`wasm32` 不是任何系统的机器架构(2026.10.29.1)。 - 安装把归档的唯一顶层目录移到安装目录。该目录以归档命名(`mcpp pack` 的写法),安装取下载文件的名字去掉扩展名, 对每个系统、架构与索引日后追加的版本都成立;打包后改名的归档与 xim 不解开的文件(非 `.tar.gz`、`.tar.zst`、 `.tar.xz`、`.tar`、`.zip`)在写出条目时被拒绝;配置以 xvm 注册程序(通用包为 `bin/`,Windows 上为 @@ -339,6 +352,10 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 5. 单文件首次运行解压一次,再次运行不解压;静态单文件不带解释器;映像的单文件运行时缓存目录中无新文件(e2e 956)。 6. `mcpp-package.json` 的 `files` 覆盖树中每个文件,摘要与文件一致(e2e 954、957)。 7. 通用包的 `mcpp emit xpkg` 条目在三个系统上为同一资产,并以运行器为依赖(e2e 957)。 +8. `mcpp emit xpkg` 拒绝 `wasm32-emscripten` 的包,写出包、三元组与 `dist-web`;`--pkg-format single-file` 在 WebAssembly + 目标上于任何编译之前被拒绝(e2e 962)。 +9. 单腿的 `self-contained` ok 映像包的程序是映像架构的;`mcpp pack --target --list-formats` 列出该目标的平台提供的格式 + (`okb`),本机目标的列表不含它(e2e 961)。 ## 变更记录 @@ -347,3 +364,4 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 | 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。 | | 1.2 | 2026-10-28 | 包名的模式后缀;单文件启动器自身失败的退出状态 127;`--merge` 取 `[pack] format`,合并后的目录以输出命名;`--linkage static` 改为 musl 的行;`mcpp emit xpkg --package` 的每系统程序目录、带命名空间的运行器依赖、以归档名定位的顶层目录与多余词的拒绝。 | +| 1.3 | 2026-10-29 | mcpp 2026.10.29.1:§0 ok 映像与 Web;§1.2 单腿 `self-contained` 包的载体为映像架构的程序;§1.5、§5.2 WebAssembly 目标上的单文件被拒绝;§6 `--list-formats` 规划所选目标;§8 单腿包的系统由三元组决定,不属于 Linux、macOS、Windows 的腿被拒绝;判据 8、9(e2e 961、962)。 | diff --git a/docs/specs/platforms.md b/docs/specs/platforms.md index 74a67fde0..4d0a35320 100644 --- a/docs/specs/platforms.md +++ b/docs/specs/platforms.md @@ -4,11 +4,11 @@ |---|---| | 规范编号 | 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` | +| 状态 | 评审中 v1.2 | +| 版本 | 1.2 | +| 最后修改 | 2026-10-29 | +| 对应实现 | mcpp >= 2026.10.27.1;§10 的执行宿主与 §11 的载体架构 mcpp >= 2026.10.29.1;逐条标注 | +| 相关设计文档 | `.agents/docs/2026-10-28-platforms-design.md`、`.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`、`.agents/docs/2026-10-29-openkal-run-test-pack-design.md` | | 相关 issue | mcpp#846、mcpp#847、mcpp#848 | | 相关规范 | [SPEC-002](target-side.md)(目标侧的层)、[SPEC-004](manifest-semantics.md)(条件化形状)、[SPEC-006](toolchain-management.md)(工具链选择)、[SPEC-007](build-plugins.md)(构建程序与协议)、[SPEC-011](concurrency.md)(原子发布)、[SPEC-014](packaging.md)(打包) | | 使用文档 | [35 —— 平台](../zh/35-platforms.md) | @@ -300,13 +300,35 @@ plugin = "build.mcpp" `[target.] run_under` 的写法;`run_under` 存在或给出 `--no-runner` 时不拒绝。运行器的合并顺序与 §6 相同;`mcpp run` 与 `mcpp test` 读合并后的值。**已实现** +运行器的程序是 `cmd` 首项所命名的程序;首项是平台的工具(§7.3)时,为构建为本机定位的该工具。`host = "same-arch"` +的运行器在产物架构的机器上运行,因此产物的架构不是本机架构、且给出了执行宿主(`run_under`,含 +`MCPP_TARGET_<三元组>_RUN_UNDER`;SPEC-002 §6)而未给 `--no-runner` 时,首项为平台工具的运行器的程序**必须**是产物架构的: +平台的载体以运行器形式(§11,不携带任何内容)为本机系统与产物架构构建的程序,构建于 +`target/carrier-pack/<首项>--/`,与 `self-contained` 通用包放在 `libexec/<首项>/-/` 中的程序相同。 +平台未声明 `carrier`,或本 mcpp 不为该系统与架构构建程序(SPEC-014 §4.4)时,运行**必须**被拒绝并写出原因,**禁止**改用 +本机的工具。`host = "any"` 的运行器从本机到达产物,其程序是本机的。`mcpp test` 为一次构建构建一次该程序,不运行测试时 +(`--no-run`、已确定的拒绝)不构建。**已实现**(2026.10.29.1) + +例:x86_64 Linux 上以用户态模拟器作为执行宿主运行 `aarch64-openkal` 的映像: + +```toml +[target.aarch64-openkal] +run_under = ["qemu-aarch64"] +``` + +`mcpp run --target aarch64-openkal` 执行 `qemu-aarch64 <映像> <参数>`。 + +`mcpp test` 的人类输出在第一个因拒绝而未运行的测试之前以警告写出一次完整的原因(每次构建一次),摘要行写出其第一行。 +**已实现**(2026.10.29.1) + ## 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` +§4.4)、映像的单文件(SPEC-014 §5.2);`mcpp run` 与 `mcpp test` 为执行宿主构建另一架构的运行器(§10)时亦然。 +单目标包与单文件的程序的架构为产物的架构,系统为本机系统(单文件另按 `--systems`)。工程依赖 `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`(字符串)。 携带的字节**必须**是以下之一,载体的入口按内容区分: @@ -320,7 +342,8 @@ Mach-O 写作 `__<大写名>,__carried`),给出符号 `mcpp_carried_begin`、`mc 程序按其系统的约定构建(自举边界),不使用平台:Linux 上为 `-linux-musl` 行的静态程序,该行的工具链被 显式写出;macOS 上为 `-macos`、Windows 上为 `-windows-gnu` 行,系统与架构同本机时取本机的行,只依赖 系统库(映像的单文件在 Windows 上以 `system` 模式打包)。引擎不含任何加载器的调用。平台未声明 `carrier` 时, -需要载体的打包被拒绝并写出该键。 +需要载体的打包被拒绝并写出该键;本 mcpp 不构建程序的行(SPEC-014 §4.4)上的载体在生成工程之前被拒绝,写出系统、架构与行 +(2026.10.29.1)。 ## 12. 平台阶段与协议 17 @@ -361,6 +384,10 @@ Mach-O 写作 `__<大写名>,__carried`),给出符号 `mcpp_carried_begin`、`mc 选择来源报告为 `built-in`。项目可以不等引擎更新,以 `[target.] platform` 选择另一个版本。 **已实现** +ok 映像是 x86_64 与 aarch64 上位置无关的 ELF,只导入 `kal_*`,运行时需要加载器发布的可执行内存;okb 只装 ok 映像。 +openkal-platform 的 `targets` 因此不含 WebAssembly。Web 经 `wasm32-emscripten` 目标(宿主目标,无平台包)到达,打包为 +页面经 `dist-web` 提供者;以 openkal API 写的代码经 openkal 的 Emscripten 实现(openkal-emscripten)到达 Web。 + ## 15. 锁、身份与报告 ### 15.1 锁 @@ -411,11 +438,11 @@ Mach-O 写作 `__<大写名>,__carried`),给出符号 `mcpp_carried_begin`、`mc |---|---| | `--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 --target [--format json]` | 准备并报告一个配置:三元组、编译器与其来源、平台、工具、运行器与其程序、`run_under`;在工程中为该工程的配置,工程外为家目录缓存中的空程序;工程源码会被拒绝的原因(例:无 C++ 运行时而 `import std`)作为警告报告,配置照常报告。运行器的程序按 §10 的规则报告:其架构(JSON `runner.arch`),另一架构的运行器是否已构建(`runner.built`,不为报告而构建),执行宿主的程序(`runUnderProgram`),以及本机拒绝运行时的原因(人类输出的 `run` 行,JSON `runner.refusal`)(2026.10.29.1) | | `mcpp self env --host` | 宿主图的配置;其构建程序已构建过时,报告宿主平台的运行器与其程序、工具 | | `mcpp toolchain install --target `(不指定编译器) | 安装该目标的工具链后准备该配置:获取平台包,构建其工具 | | `mcpp toolchain list` | 目标行有 PLATFORM 列,JSON 有 `platform` 字段 | -| `mcpp test`(在平台包中,未给 `--target`) | 对 `targets` 中的每个目标以本包为平台构建测试;`host = "same-arch"` 时只运行本机架构的目标,其余报告为已构建未运行 | +| `mcpp test`(在平台包中,未给 `--target`) | 对 `targets` 中的每个目标以本包为平台构建测试;`host = "same-arch"` 时运行本机架构的目标与给出执行宿主的目标(§10),其余报告为已构建未运行 | **已实现** @@ -442,13 +469,16 @@ Mach-O 写作 `__<大写名>,__carried`),给出符号 `mcpp_carried_begin`、`mc 4. 图已提供某层时平台不加入该层的包,报告为 `graph`;`false` 不加入。 5. 版本组冲突在任何链接之前拒绝,消息给出两条出路。 6. 封装失败不发布产物,保留上一次的产物。 -7. `same-arch` 的运行器不运行其他架构的产物,并给出 `run_under` 的写法。 +7. `same-arch` 的运行器不运行其他架构的产物,并给出 `run_under` 的写法;`mcpp test` 的人类输出给出完整的原因恰好一次(e2e 960)。 8. 平台的工具在任何图中按宿主约定构建。 9. 锁中平台加入的包带 `origin = "platform"`;报告与 JSON 给出每个字段的来源。 10. 一个与 openkal 无关的平台包的编译标志、命名、封装与运行器到达程序,`cfg(platform)` 下的覆盖取代对应字段, `--platform none` 不使用平台,没有 `[platform]` 的包被拒绝为平台(e2e 952)。 11. 宿主图选用平台时构建程序与其库作为映像构建并经平台的运行器运行,平台的工具按宿主约定构建, `--host-platform none` 时构建程序为本机程序(e2e 953)。 +12. 给出执行宿主时,`mcpp run` 与 `mcpp test` 交给它的运行器程序是产物架构的 ELF;本机有该架构的 qemu 时映像经它运行、 + 测试通过;`mcpp self env` 报告拒绝的原因,给出执行宿主时报告另一架构的运行器(e2e 960)。 +13. `--mode self-contained` 的单目标包的载体是映像架构的程序(e2e 961)。 ## 变更记录 @@ -456,3 +486,4 @@ Mach-O 写作 `__<大写名>,__carried`),给出符号 `mcpp_carried_begin`、`mc |---|---|---| | 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)。 | +| 1.2 | 2026-10-29 | mcpp 2026.10.29.1:§10 给出执行宿主时 `same-arch` 运行器的程序为产物架构的载体运行器形式,`mcpp test` 写出完整的拒绝原因一次;§11 单目标包与单文件的程序为产物架构的,不构建的行在生成工程之前被拒绝;§14 ok 映像与 Web;§16 `self env` 报告运行器程序的架构、执行宿主与拒绝,平台包的 `mcpp test` 运行给出执行宿主的目标;判据 7、12、13(e2e 960、961)。 | diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 5957bbc57..933f91ca0 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -638,13 +638,15 @@ ok 映像经 `okrun` 在其架构的每个系统上运行(见 [24](24-openkal- |---|---|---| | `system` | `bin/.oke` | `okrun` 与它所需的每个映像 | | `vendored`(默认) | `bin/.oke` 与它所需的 `.okso` 映像 | `okrun` | -| `self-contained` | `bin/`:本机的程序,以平台载体的段(ELF 与 COFF 上为 `.oke`,Mach-O 上为 `__OKE,__carried`)携带映像并经 libokrun 运行,旁边是所需的 `.okso` 映像 | 无 | -| `static`(`--linkage static --mode self-contained`) | `bin/`:本机的程序,携带一个静态 `.oke` | 无 | +| `self-contained` | `bin/`:本机系统、映像架构的程序,以平台载体的段(ELF 与 COFF 上为 `.oke`,Mach-O 上为 `__OKE,__carried`)携带映像并经 libokrun 运行,旁边是所需的 `.okso` 映像 | 无 | +| `static`(`--linkage static --mode self-contained`) | `bin/`:本机系统、映像架构的程序,携带一个静态 `.oke` | 无 | 以上是单个目标的包(`--no-universal`,或平台不声明通用包)。`--linkage static` 构建不带 `.okso` 的映像。 `self-contained` 不重新编译映像:mcpp 把平台的载体(`[platform] carrier`,一个包与它的入口源码)生成为 `target/carrier-pack/--/` 下的工程,把映像的字节放进载体的段,再按任何程序的方式打包该程序 —— -Linux 上为静态(musl),macOS 与 Windows 上只引用系统自带的库。 +Linux 上为静态(musl),macOS 与 Windows 上只引用系统自带的库。该程序的架构为映像的架构(2026.10.29.1+):在 +x86_64 Linux 上,`--target aarch64-openkal` 打出 aarch64 的 Linux 程序。本 mcpp 不为其构建程序的系统与架构(在 +Windows 机器上为 `aarch64-windows-gnu`)被拒绝,并写出该行。 ### Android:应用目标文件与它的闭包在 `lib/` 下 @@ -730,6 +732,9 @@ ok 映像的文件为 `target/dist/---`(Windows 上 (缺省为本机系统)各一个。映像不引用其他映像时载体只携带它,否则携带全部映像的一个 stored zip(每个映像对齐到 64 KiB),载体的入口经 openkal-run 的 bundle 接口读取(openkal-platform 0.17.2 起)。 +WebAssembly 目标没有单文件:`--pkg-format single-file` 与 `--target wasm32-emscripten` 在任何编译之前被拒绝 +(2026.10.29.1+),它的页面是 `dist-web` 提供者的格式。 + ### `mcpp-package.json` 每棵暂存树都带 `mcpp-package.json`:包、它的平台、三个选择、腿与系统、启动方式,以及每个文件的角色与 sha256。 @@ -752,7 +757,9 @@ ok 映像的文件为 `target/dist/---`(Windows 上 包不携带运行器时以运行器为依赖(`deps = { "xim:okrun" }`);各系统各架构一个的包,每个系统一项,一个带 `${arch}` 的 URL 与每个架构的 sha256。安装把归档的顶层目录移到位,并向 xvm 注册程序(Windows 上为 `.cmd` 启动器);各系统的包 把程序放在不同目录时(Windows 的程序在包根,POSIX 的程序在 `bin/`),按系统注册各自的目录。每个归档各以一个 -`--package` 给出。 +`--package` 给出。单腿包按其三元组的系统(Linux、macOS 或 Windows)描述;其他目标的包被拒绝,并写出分发它的 +途径(2026.10.29.1+):`wasm32-emscripten` 的包发布为页面(`dist-web`),ok 映像以平台的通用包发布,Android、iOS +与裸机的包没有条目。 ### 压缩与签名 @@ -802,4 +809,5 @@ force_bundle = ["libfoo.so"] # bundle even if matched by the PEP 600 list - `.deb`、`.rpm`、AppImage、`.msi` 等分发格式经 `--pkg-format ` 由包提供;引擎提供 `tar`、`dir`、 `zip` 与 `single-file`。 - 不产出在多个系统上运行的单个文件;一个单文件包服务一个系统与一个架构。 +- WebAssembly 包既不是单文件,也不是 xlings 条目;它的页面是提供者的格式(`dist-web`)。 - ELF 程序的闭包由其动态链接器解析,因此不能在 Windows 上打包 Linux 程序。 diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 9f1f3de4e..88969b52f 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -377,6 +377,41 @@ mcpp run --target x86_64-openkal # okrun bin/app.oke **缺少 `okld` 是拒绝而非警告。** 没有需求 note 的映像会被每个加载器拒绝;拒绝(`platform-tool-absent`) 指名构建它的包。 +### 另一架构的映像(mcpp 2026.10.29.1) + +`okrun` 运行其自身架构的映像(`host = "same-arch"`)。在另一架构的机器上,`mcpp run` 与 `mcpp test` +拒绝该映像,并指出 `run_under` —— 陈述执行宿主的键。给出执行宿主后,runner 在那里运行,其程序是平台为映像 +架构准备的 runner:mcpp 为本机系统与该架构构建不携带任何内容的平台载体,即 self-contained 通用包放在 +`libexec/okrun/-/` 中的程序。用户态模拟器就是一种执行宿主;在装有 `qemu-aarch64` 的 x86_64 Linux 上: + +```toml +[package] +name = "app" +version = "0.1.0" + +[target.aarch64-openkal] +run_under = ["qemu-aarch64"] +``` + +``` +$ mcpp run --target aarch64-openkal + Packing okrun for linux-aarch64 (openkal-platform 0.17.3's carrier, the runner on the execution host) + Running `okrun … target/aarch64-openkal/…/bin/app.oke` + Via qemu-aarch64 [/…/app/mcpp.toml:6] +``` + +`mcpp test --target aarch64-openkal` 以同样的方式运行测试。`MCPP_TARGET_AARCH64_OPENKAL_RUN_UNDER=qemu-aarch64` +为一次运行指定执行宿主,`~/.mcpp/config.toml` 中的 `[target.aarch64-openkal] run_under` 为一台机器指定。 +`mcpp self env --target aarch64-openkal` 报告 runner 的程序与其架构、执行宿主的程序,或本机拒绝运行的原因。 +一个映像的 self-contained 包(`--no-universal --mode self-contained`)是本机系统、映像架构的程序 +([10](10-pack-and-release.md))。 + +### ok 映像与 Web + +ok 映像是 x86_64 与 aarch64 上位置无关的 ELF,只导入 `kal_*`,在其加载器发布的可执行内存中运行;okb 只装 ok 映像。 +Web 经 `wasm32-emscripten` 目标到达,其包由 `dist-web` 提供者写成页面;以 openkal API 写的代码经 +`openkal-emscripten`(见上文)到达 Web。 + ## 裸机 一个没有操作系统的 target,是同一个模型,只是平台层由固件而非内核供给。 diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index 2a0b2c5a4..282739ff7 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -45,7 +45,8 @@ mcpp run --target x86_64-openkal Running `okrun … target/x86_64-openkal/…/bin/app.oke` ``` -每个值都带来源报告。`mcpp test`、`mcpp pack` 与 `--target aarch64-openkal` 无需更多声明。`mcpp.lock` 以 +每个值都带来源报告。`mcpp test`、`mcpp pack` 与 `--target aarch64-openkal` 无需更多声明;与本机架构不同的 +映像经执行宿主运行([24](24-openkal-cross.md))。`mcpp.lock` 以 `origin = "platform"` 记录平台加入的包,并以 `targets` 写出每个条目的配置(只属于本机原生配置的条目不写), 因此为本机的构建与为 ok 映像的构建不互相改写对方的条目。 @@ -209,13 +210,13 @@ seal = [["okld", "note", "{}"], ["okld", "check", "{}", "--with", "{needed}"]] 64 KiB)([SPEC-013](../specs/platforms.md) §11)。 开发期间,工程以 `--platform path:` 或 `platform = { path = "" }` 选择工作树。在平台包中执行 -`mcpp test` 而不给 `--target` 时,以该包为平台为其每个 `targets` 构建测试,运行本机架构的那些,其余报告为 -已构建、未运行。 +`mcpp test` 而不给 `--target` 时,以该包为平台为其每个 `targets` 构建测试,运行本机架构的那些与给出了执行宿主的 +那些(2026.10.29.1+),其余报告为已构建、未运行。 ## 查看一个配置 ```sh -mcpp self env --target x86_64-openkal # toolchain, platform, tools, runner +mcpp self env --target x86_64-openkal # toolchain, platform, tools, runner, and why runs are refused here 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 diff --git a/docs/zh/41-devices.md b/docs/zh/41-devices.md index b6a29a4c5..76ab42c4e 100644 --- a/docs/zh/41-devices.md +++ b/docs/zh/41-devices.md @@ -92,7 +92,9 @@ runner = { cmd = ["okrun", "{}"], host = "same-arch", exclusive = false } ``` `host = "same-arch"` 只在产物架构的机器上运行它,其他架构的产物被拒绝并点名 `run_under`;`"any"` 表示 -runner 能从任何机器抵达产物。目标的平台提供它的缺省 runner([35](35-platforms.md));依赖的构建程序、本机的 +runner 能从任何机器抵达产物。给出执行宿主(`run_under`)时,`same-arch` 的 runner 在执行宿主上运行;runner +是平台的工具时,交给执行宿主的是平台为产物架构准备的 runner,由 mcpp 从平台的载体构建(2026.10.29.1+; +[24](24-openkal-cross.md) 中的 `run_under = ["qemu-aarch64"]`)。目标的平台提供它的缺省 runner([35](35-platforms.md));依赖的构建程序、本机的 `config.toml`、工程与 `MCPP_TARGET__RUNNER` 依次替换它,只写 argv 时保留平台的 `host` 与 `exclusive`。 ## runner 收到的内容 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index c7c0acb35..1b0169dad 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -311,13 +311,18 @@ mcpp self env --format json "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": [] + "program": "/…/okrun", "arch": "x86_64", "built": true, "refusal": null, + "source": "platform"}, + "runUnder": [], + "runUnderProgram": null } ``` 未选择平台时 `platform` 为 `null`;其形状与 `plan` 事件的相同(§9)。运行器指名平台自己的工具时 -`runner.program` 为该工具,否则为本机找到的程序。`--host` 时,工程的构建程序已在宿主平台下构建过,才报告宿主平台 +`runner.program` 为该工具,否则为本机找到的程序。对目标图(2026.10.29.1+),该程序就是 `mcpp run` 与 +`mcpp test` 会启动的程序:`runner.arch` 为其架构;产物属于另一架构、运行器为 `same-arch` 且给出了执行宿主时, +它是平台为该架构准备的运行器,报告第一次运行构建它的位置(此前 `built: false`;`self env` 不构建任何东西)。 +`runUnderProgram` 为执行宿主的程序,`runner.refusal` 为本机拒绝运行的原因(不拒绝时为 `null`)。`--host` 时,工程的构建程序已在宿主平台下构建过,才报告宿主平台 的 `runner` 与 `tools`。工程源码会被拒绝的原因 (图中没有 C++ 运行时而 `import std`)是一条代码为 `MCPP_CONFIGURATION_SOURCES_REFUSED` 的 `warning` 诊断,配置照常报告。无法准备的配置带 `MCPP_CONFIGURATION_FAILED`,没有 `data`。 @@ -919,7 +924,9 @@ mcpp pack --list-formats `data.formats` 列出引擎自己的格式(`tar`、`zip`、`dir`、`single-file`;`builtin: true`)以及已解析依赖 图中某个包提供的每种格式(`builtin: false`,带 `providers`)。依赖图在 mcpp home 的 -一个目录中规划,因此不向工程写入任何东西。 +一个目录中规划,因此不向工程写入任何东西。带 `--target ` 时(2026.10.29.1+;多次给出时取最后一个), +规划的是该目标的依赖图,并列出其平台的格式:`mcpp pack --target x86_64-openkal --list-formats` 列出 +`okb`,由 `mcpplibs.openkal-platform` 提供。 ## 当前边界 diff --git a/docs/zh/90-build-from-source.md b/docs/zh/90-build-from-source.md index 68a69521f..a2fa4019d 100644 --- a/docs/zh/90-build-from-source.md +++ b/docs/zh/90-build-from-source.md @@ -84,7 +84,7 @@ src/ tests/ ├── unit/ 156 C++ tests, discovered by `mcpp test` -└── e2e/ 646 shell scripts against a real binary +└── e2e/ 649 shell scripts against a real binary ``` ## 测试组织 @@ -94,7 +94,7 @@ tests/ **单元测试**(`tests/unit/`,156 个文件)是 `mcpp test` 发现的 C++ 程序。它们检验一个模块的契约,不需要二进制、也不需要文件系统状态。 -**端到端测试**(`tests/e2e/NN_.sh`,646 个文件)拿真实的 `mcpp` +**端到端测试**(`tests/e2e/NN_.sh`,649 个文件)拿真实的 `mcpp` 二进制跑真实工程。`run_all.sh` 是 CI 的入口。`mcpp test` **不**运行它们。 ```bash diff --git a/mcpp.toml b/mcpp.toml index 9010ae15d..20130043b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.28.1" +version = "2026.10.29.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 763adf613..14086adb5 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.28.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.29.1"; } // namespace mcpp diff --git a/src/build/execute.cppm b/src/build/execute.cppm index c0dcb70cd..b9e67d880 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -1083,6 +1083,37 @@ std::optional platform_tool(const mcpp::build::BuildConte return std::nullopt; } +// THE RUNNER'S PROGRAM WHEN IT IS THE PLATFORM'S TOOL (SPEC-013 §10). The tool +// the build located is a program of this machine. A `same-arch` runner reached +// through an execution host on another architecture runs THERE, on a machine +// of the artifact's architecture, so its program is the platform's runner for +// that architecture: the carrier with nothing carried (§11), for this +// machine's system -- the program a self-contained universal package carries +// under `libexec//-/`. Measured on 2026.10.28.1 before this: +// `run_under = ["qemu-aarch64"]` was handed the x86_64 okrun, and qemu said +// "Invalid ELF image for this architecture". `build = false` locates it +// without building it (`mcpp self env`). nullopt: the word is no platform +// tool, and is looked up as any runner's program is. +struct RunnerTool { + std::filesystem::path program; + std::string arch; // the architecture it is built for +}; +std::expected, std::string> +runner_tool(const mcpp::build::BuildContext& ctx, std::string_view word, std::string_view tripleKey, + bool noRunner, bool build = true) { + auto tool = platform_tool(ctx, word); + if (!tool) return std::nullopt; + auto t = mcpp::toolchain::triple::parse(tripleKey); + if (!t || mcpp::platform::runner_reach(ctx.plan.platform.fields.runner, t->arch, + mcpp::toolchain::platform::host_arch, + !ctx.runUnder.empty(), noRunner) + != mcpp::platform::RunnerReach::TargetArchitecture) + return RunnerTool{ *tool, std::string(mcpp::toolchain::platform::host_arch) }; + auto program = mcpp::pack::runner_program(ctx.plan.platform, ctx.projectRoot, word, t->arch, build); + if (!program) return std::unexpected(program.error().message); + return RunnerTool{ std::move(*program), t->arch }; +} + ExecutionHost locate_run_under(const mcpp::build::BuildContext& ctx, std::string_view tripleKey) { ExecutionHost h; if (ctx.runUnder.empty()) return h; @@ -1122,8 +1153,10 @@ std::optional unreachable_execution(const mcpp::build::BuildContext // (`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) + if (auto const& r = ctx.plan.platform.fields.runner; t + && mcpp::platform::runner_reach(r, t->arch, mcpp::toolchain::platform::host_arch, + !ctx.runUnder.empty(), noRunner) + == mcpp::platform::RunnerReach::Refused) return std::format( "cannot run {} artifacts on this {} host: the runner `{}` ({}) runs an artifact on a " "machine of its architecture.\n" @@ -1264,6 +1297,48 @@ RunnerChoice choose_runner(const BuildContext& ctx) { return choose_device_action(ctx, std::string_view{}, false); } +// HOW `mcpp run` AND `mcpp test` WOULD START THE TARGET'S ARTIFACT HERE, as +// `mcpp self env --target` reports it (SPEC-013 §10, §16), read from the +// helpers the two commands use and building nothing: the program the runner's +// first word resolves to and its architecture (another architecture's runner, +// for an execution host, is located where the first run builds it), the +// execution host's program, and the reason runs are refused on this machine. +export struct RunnerOnThisMachine { + std::string program; // empty: not found + std::string arch; // the architecture `program` is built for + bool built = true; // false: built by the first run + std::string runUnderProgram; // `run_under`'s program; empty: none, or not found + std::string refusal; // non-empty: runs are refused here, and why +}; + +export RunnerOnThisMachine runner_on_this_machine(const BuildContext& ctx) { + RunnerOnThisMachine out; + const auto choice = choose_runner(ctx); + out.arch = std::string(mcpp::toolchain::platform::host_arch); + if (auto why = unreachable_execution(ctx, choice.tripleKey, false)) out.refusal = *why; + if (!ctx.runUnder.empty()) { + auto host = locate_run_under(ctx, choice.tripleKey); + if (!host.error.empty()) out.refusal = host.error; + else out.runUnderProgram = host.prefix.front(); + } + if (choice.tmpl.empty()) return out; + auto found = mcpp::build::runner_lookup::locate( + choice.tmpl.front(), ctx.xlingsDepBinDirs, mcpp::invocation::get("PATH").value_or("")); + auto tool = runner_tool(ctx, choice.tmpl.front(), choice.tripleKey, false, /*build=*/false); + if (!tool) { + out.refusal = tool.error(); + return out; + } + if (*tool) { + found.program = (*tool)->program; + out.arch = (*tool)->arch; + std::error_code ec; + out.built = std::filesystem::is_regular_file((*tool)->program, ec); + } + if (found.program) out.program = found.program->generic_string(); + return out; +} + // The capacity number, printed because capacity is the constraint. // // After `Finished`, not instead of it: the build succeeded either way, and a @@ -2873,7 +2948,12 @@ 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; + auto tool = runner_tool(ctx, tmpl.front(), choice.tripleKey, no_runner); + if (!tool) { + std::println(stderr, "error: {}", tool.error()); + return 2; + } + if (*tool) found.program = (*tool)->program; if (!found.program) { std::println(stderr, "error: {}", mcpp::build::runner_lookup::not_found_message( @@ -3280,6 +3360,13 @@ export struct TestOptions { // `--no-run` makes the narrower claim available as its own answer: every // selected test compiled and linked for the target, and none was executed. bool noRun = false; + // A target this machine's runner cannot reach -- a `same-arch` runner on + // another architecture with no execution host named -- is reported built + // and not run, as `--no-run` reports, rather than not run: `mcpp test` in + // a platform package tests every target the package serves (SPEC-013 + // §16), and one of another architecture runs where an execution host is + // named for it. + bool unreachableIsBuilt = false; // Per-test RUN deadline. The default is deliberately non-zero: `mcpp test` // is something CI runs unattended, and an unbounded default makes a single // hung test able to consume the whole job with nothing to show for it. @@ -3496,9 +3583,14 @@ struct TestBuild { std::vector runUnderPrefix; // Non-empty ⇒ no test is spawned; every one is reported NotRun with it. std::string invocationNotRunReason; + // The reason is printed once per build, in full, before the first test it + // leaves not run; the summary of each member repeats its first line. + bool invocationReasonPrinted = false; // Non-zero: there is nothing to execute the tests with, and this is the // exit status every member of the build returns. int runnerFatal = 0; + // The tests are reported built and not run (`unreachableIsBuilt`). + bool builtNotRunHere = false; // Set by the first worker whose spawn the kernel refused; every worker // checks it before spawning. Workers already past the check may be // refused the same way — harmless, a refused spawn has no side effects — @@ -3700,9 +3792,16 @@ static void test_resolve_runner(TestBuild& tb, const TestOptions& testOpts, bool tb.runnerFatal = 2; return; } - // Not run, with the reason, as a missing runner is: the tests built. - if (auto why = unreachable_execution(*ctx, runnerChoice.tripleKey, testOpts.noRunner)) + // Not run, with the reason, as a missing runner is: the tests built. Or, + // where the caller asks, built and not run (a platform package's targets). + if (auto why = unreachable_execution(*ctx, runnerChoice.tripleKey, testOpts.noRunner)) { + if (testOpts.unreachableIsBuilt) { + tb.builtNotRunHere = true; + if (!json) mcpp::ui::info("note", std::format("built, not run on this host: {}", *why)); + return; + } tb.invocationNotRunReason = *why; + } if (!testOpts.noRunner) { auto host = locate_run_under(*ctx, runnerChoice.tripleKey); if (!host.error.empty()) tb.invocationNotRunReason = host.error; @@ -3713,10 +3812,18 @@ 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); + // The program the execution host runs is built once for the build + // when it is another architecture's, and only when the tests run. + auto tool = runner_tool(*ctx, runnerTmpl.front(), runnerChoice.tripleKey, testOpts.noRunner, + /*build=*/tb.invocationNotRunReason.empty() && !testOpts.noRun); + if (!tool) { + tb.invocationNotRunReason = tool.error(); + } else { + if (*tool) found.program = (*tool)->program; + if (found.program) runnerTmpl.front() = found.program->string(); + else tb.invocationNotRunReason = mcpp::build::runner_lookup::not_found_message( + runnerChoice.tripleKey, runnerTmpl.front(), found.searched); + } } } @@ -4114,11 +4221,18 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // `runnable` compiled and linked; a test that did not is already a // `CompileFail` in `results` and keeps that status, so this path reports // what was built without also reporting anything about what it does. - if (testOpts.noRun) { + if (testOpts.noRun || tb.builtNotRunHere) { for (auto& r : runnable) results.push_back({r.name, TestResult::St::Built, 0, {}, {}, 0, false, {}, r.buildMs}); } else { + // THE FULL REASON, ONCE (SPEC-013 §18, criterion 7). It carries the key + // that changes it (`run_under`); it was recorded silently and the + // summary printed its first line alone, so the hint never reached a + // person (measured on 2026.10.28.1). `mcpp run` prints the same text. + if (!json && !invocationNotRunReason.empty() && !runnable.empty() + && !std::exchange(tb.invocationReasonPrinted, true)) + mcpp::ui::warning(invocationNotRunReason); run_tests_now(runnable); } summary.elapsedMs = carriedMs + std::chrono::duration_cast( @@ -4181,7 +4295,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, // The count is in the summary line at the same weight as failures, with // its reason: a quiet skip is read as a pass. First line of the reason - // only — the full text was printed when it was established. + // only — the full text was printed once, before the tests it left not run. auto counts = std::format("{} passed; {} failed", passed, failed); if (built) counts += std::format("; {} built, not run", built); if (notRun) { @@ -4316,7 +4430,12 @@ export int run_tests(std::span passthrough, 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) + // The target is the cache entry's `[target=]` key, as for + // `mcpp run` (see `build_run_target`): without it this build + // of another target was recorded as the host's, and the next + // bare `mcpp run` warned "fingerprint changed" and rebuilt. + if (int rc = run_build_plan(*binCtx, mcpp::log::is_verbose(), false, + overrides.target_triple); rc != 0) return rc; for (auto const& lu : binCtx->plan.linkUnits) { if (lu.kind != LinkUnit::Binary) continue; diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index 940eb604f..cb7c09d55 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -1294,11 +1294,12 @@ 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): + // A PLATFORM PACKAGE IS TESTED ON THE TARGETS IT SERVES (SPEC-013 §16): // 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. + // (`host = "same-arch"`: the target of this machine's architecture, or one + // an execution host is named for); the others are built and reported as + // not run, as `run_tests` decides by the rule of §10. 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)); @@ -1312,11 +1313,8 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, 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)" : "")); + tto.unreachableIsBuilt = true; + mcpp::ui::info("Testing", std::format("platform {} on {}", m->package.name, t)); if (int r = mcpp::build::run_tests(passthrough, tov, tto); r != 0 && rc == 0) rc = r; } return rc; @@ -1683,7 +1681,15 @@ export int cmd_env_configuration(const mcpplibs::cmdline::ParsedArgs& parsed) { 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 target's: what `mcpp run` and `mcpp test` would start here, by + // their own rule (SPEC-013 §10) -- the runner's program and its + // architecture, the execution host's program, or why runs are refused. + mcpp::build::RunnerOnThisMachine reach; + reach.arch = std::string(mcpp::toolchain::platform::host_arch); + if (!host) { + reach = mcpp::build::runner_on_this_machine(*ctx); + runnerProgram = reach.program; + } else 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()) { @@ -1695,6 +1701,7 @@ export int cmd_env_configuration(const mcpplibs::cmdline::ParsedArgs& parsed) { if (found.program) runnerProgram = found.program->generic_string(); } } + auto or_null = [](const std::string& v) { return v.empty() ? nlohmann::json(nullptr) : nlohmann::json(v); }; if (json) { nlohmann::json tools = nlohmann::json::object(); for (auto const& [name, path] : P.tools) tools[name] = path; @@ -1706,9 +1713,12 @@ export int cmd_env_configuration(const mcpplibs::cmdline::ParsedArgs& parsed) { {"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)}, + {"program", or_null(runnerProgram)}, + {"arch", reach.arch}, {"built", reach.built}, + {"refusal", or_null(reach.refusal)}, {"source", mcpp::platform::origin_text(runner.origin)}}}, {"runUnder", ctx->runUnder}, + {"runUnderProgram", or_null(reach.runUnderProgram)}, }; mcpp::wire::Envelope env{.kind = "mcpp.env", .effects = {}}; env.data = std::move(data); @@ -1730,14 +1740,28 @@ export int cmd_env_configuration(const mcpplibs::cmdline::ParsedArgs& parsed) { 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), + // Another architecture's program (for an execution host) says so. + std::string arch; + if (reach.arch != mcpp::toolchain::platform::host_arch) + arch = std::format(" ({}{})", reach.arch, reach.built ? "" : "; built by the first run"); + std::println("{:<10} {} ({}){}{}", "runner", argv, mcpp::platform::origin_text(runner.origin), runnerProgram.empty() ? std::string(" -- not found on this machine") - : " -> " + runnerProgram); + : " -> " + runnerProgram, arch); } 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); + std::println("{:<10} {} ({}){}", "run_under", argv, ctx->runUnderOrigin, + reach.runUnderProgram.empty() ? std::string() + : " -> " + reach.runUnderProgram); + } + // Runs refused on this machine are said here, with the key that changes + // it, as `mcpp run` would say it. + if (!reach.refusal.empty()) { + std::string text = reach.refusal; + for (std::size_t at = text.find('\n'); at != std::string::npos; at = text.find('\n', at + 12)) + text.insert(at + 1, std::string(11, ' ')); + std::println("{:<10} refused on this machine: {}", "run", text); } return 0; } diff --git a/src/cli/cmd_publish.cppm b/src/cli/cmd_publish.cppm index 6406cb911..bea6b7144 100644 --- a/src/cli/cmd_publish.cppm +++ b/src/cli/cmd_publish.cppm @@ -270,7 +270,10 @@ int cmd_pack_body(const mcpplibs::cmdline::ParsedArgs& parsed, // packages that provide it -- as the `mcpp.pack.formats` envelope. A plan // that describes (plan_only) in a directory of the mcpp home, as `emit // build-database` plans: the build programs run, because they are what -// declares a format, and nothing is written into the project. +// declares a format, and nothing is written into the project. The plan is +// the target's the pack would build (`--target`, the last when repeated, as +// the pack takes it): a platform's plugin declares its formats for its own +// targets (`okb` of `-openkal`), and the host's plan lists none of them. int cmd_pack_list_formats(const mcpplibs::cmdline::ParsedArgs& parsed) { auto root = mcpp::project::find_manifest_root(std::filesystem::current_path()); if (!root) { @@ -282,6 +285,8 @@ int cmd_pack_list_formats(const mcpplibs::cmdline::ParsedArgs& parsed) { parsed.value("profile").value_or(""), parsed.is_flag_set("release"), parsed.is_flag_set("dev")); if (auto v = parsed.value("features")) ov.features = *v; + if (auto o = parsed.option("target"); o && !o->get().values.empty()) + ov.target_triple = o->get().values.back(); ov.plan_only = true; ov.work_dir = mcpp::home::root() / "cache" / "pack-formats" / mcpp::toolchain::hash_string(root->lexically_normal().generic_string()); diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index 8e98339cd..a94a87026 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -15,6 +15,7 @@ import std; import mcpp.libs.json; import mcpp.pack.compress; import mcpp.pack.zip; +import mcpp.toolchain.triple; // the system of a package's leg (§8) import mcpplibs.os; export namespace mcpp::pack::distribution { @@ -1445,13 +1446,31 @@ int main(int argc, char** argv) { 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"; +// The xlings system a package of one leg installs on: the system its program +// runs on (SPEC-014 §8). An entry's `xpm.` installs a program of that +// system and its `archs` are machine architectures; a leg that is no program +// of Linux, macOS or Windows -- a WebAssembly module, an Android or iOS +// application, a bare-metal image, an ok image -- has neither. Every triple +// that was not Windows or macOS used to be written `linux` (a wasm32-emscripten +// package: `archs = {"wasm32"}` under `linux`). +std::optional xlings_system(std::string_view triple) { + auto t = mcpp::toolchain::triple::parse(triple); + if (!t) return std::nullopt; + if (t->os == "windows") return "windows"; + if (t->os == "macos") return "macosx"; + if (t->os == "linux" && t->env != "android") return "linux"; + return std::nullopt; +} + +// The route that distributes a package of `triple` when no entry describes it. +std::string distributed_instead(std::string_view triple) { + auto t = mcpp::toolchain::triple::parse(triple); + if (t && t->is_wasm()) + return "a page for the web, mcpp pack --pkg-format dist-web (mcpp-plugins)"; + if (t && t->os == "openkal") + return "the platform's universal package (mcpp pack without --no-universal), which every " + "system installs"; + return "the package of a Linux, macOS or Windows target"; } // A text inside a Lua double-quoted literal: what the metadata states is @@ -1527,8 +1546,14 @@ std::expected xpkg_descriptor(const std::vector "packages of one leg", a.file.filename().string(), a.pkg.legs.size())); const auto& triple = a.pkg.legs.front(); + const auto system = xlings_system(triple); + if (!system) + return std::unexpected(std::format( + "{} is a package of {}, which is no program of Linux, macOS or Windows: an " + "xlings entry installs a program of the system it runs on.\n use: {}", + a.file.filename().string(), triple, distributed_instead(triple))); const auto arch = triple.substr(0, triple.find('-')); - bySystem[xlings_system(triple)][arch] = &a; + bySystem[*system][arch] = &a; if (std::ranges::find(archs, arch) == archs.end()) archs.push_back(arch); } for (auto const& [system, byArch] : bySystem) { @@ -1578,7 +1603,7 @@ std::expected xpkg_descriptor(const std::vector std::map dirOf; // system -> the program's directory for (auto const& a : assets) { const auto program = std::filesystem::path(a.pkg.program); - dirOf[xlings_system(a.pkg.legs.front())] = + dirOf[*xlings_system(a.pkg.legs.front())] = // each leg has one: checked above program.has_parent_path() ? program.parent_path().generic_string() : std::string("."); } const auto& one = dirOf.begin()->second; diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index dfbb62d84..bf79dc4f6 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -545,6 +545,19 @@ std::expected, Error> single_file(const Plan& // 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 PLATFORM'S RUNNER FOR ANOTHER ARCHITECTURE (SPEC-013 §10, §11): the +// carrier built with nothing carried, for this machine's system and `arch`, +// named `name` -- the program a self-contained universal package carries under +// `libexec//-/`, and the one `mcpp run` and `mcpp test` hand an +// execution host of that architecture. Built under +// `/target/carrier-pack/--/`; returns the program. +// Refused when the platform states no carrier or this mcpp builds no program +// for that system and architecture. `build = false` builds nothing and returns +// where the program is placed (what `mcpp self env` reports). +std::expected +runner_program(const mcpp::platform::Resolved& platform, const std::filesystem::path& projectRoot, + std::string_view name, std::string_view arch, bool build = true); + // 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. @@ -2191,7 +2204,8 @@ run_wasm(const Plan& plan) // 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 +// self-contained a program of THIS machine's system and the artifact's +// architecture 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 @@ -2310,6 +2324,24 @@ std::string carried_assembly(std::string_view system, std::string_view sectionNa return text; } +// Where a program generated under `dir` is packed: the tree, and the program +// in it -- under the tree's `bin/`, or, for a Windows program, whose tree is +// flat, at its root. +std::filesystem::path packed_tree(const std::filesystem::path& dir) { + return dir / "target" / "dist" / "packed"; +} +std::filesystem::path packed_program(const std::filesystem::path& dir, std::string_view stem, + std::string_view system) { + if (system == "windows") return packed_tree(dir) / (std::string(stem) + ".exe"); + return packed_tree(dir) / "bin" / std::string(stem); +} + +// The generated project of the carrier named `stem` for `system` and `arch`. +std::filesystem::path carrier_dir(const std::filesystem::path& projectRoot, std::string_view stem, + std::string_view system, std::string_view arch) { + return projectRoot / "target" / "carrier-pack" / std::format("{}-{}-{}", stem, system, arch); +} + // 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 @@ -2318,7 +2350,7 @@ std::expected pack_generated_program(const std::filesystem::path& dir, const ProgramRow& row) { namespace fs = std::filesystem; std::error_code ec; - const auto out = dir / "target" / "dist" / "packed"; + const auto out = packed_tree(dir); fs::remove_all(out, ec); std::vector argv{ mcpp::invocation::self_exe_path().string(), "pack", "--pkg-format", "dir", "-o", "packed", @@ -2384,8 +2416,23 @@ struct CarrierBuild { std::string system; // linux | macos | windows std::string arch; bool single = false; + // What the runner's form is for, as the `Packing` line says it. + std::string_view role = "the runner of the package"; }; +// A carrier for a row this mcpp builds no program for, refused before a +// project is generated for it and naming the row: a Windows machine packing an +// aarch64 image's carrier (`aarch64-windows-gnu`) otherwise failed inside the +// generated project's pack. nullopt: the row is built. +std::optional unbuilt_carrier(const mcpp::platform::Resolved& platform, + std::string_view system, std::string_view arch, + bool single) { + const auto row = program_row(system, arch, single, &platform); + if (row_is_built(row)) return std::nullopt; + return std::format("{}'s carrier is built as a program of {}-{} ({}), and this mcpp builds " + "no program for that row", platform.display(), system, arch, row.triple); +} + std::expected carrier_build(const Plan& plan, const CarrierBuild& b) { @@ -2404,8 +2451,9 @@ carrier_build(const Plan& plan, const CarrierBuild& b) 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); + if (auto why = unbuilt_carrier(plan.platform, b.system, b.arch, b.single)) + return std::unexpected(Error{*why}); + const auto dir = carrier_dir(plan.projectRoot, 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 ARTIFACT RESOLVED ITS PLATFORM. The @@ -2448,8 +2496,8 @@ carrier_build(const Plan& plan, const CarrierBuild& b) // keeps for a program -- no build-machine path, its closure beside it. On // 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("{} for {}-{} ({}'s carrier, {})", b.stem, + b.system, b.arch, plan.platform.display(), b.role) : std::format("{} into a program of {}-{} ({}'s carrier, M3)", plan.binaryName, b.system, b.arch, plan.platform.display())); return pack_generated_program(dir, row); @@ -2512,11 +2560,15 @@ run_loaded(const Plan& plan) } fs::path staged; // A single file is made from the images (`single_file`); the tree - // holds them as `vendored` does. + // holds them as `vendored` does. The carrier is a program of this + // machine's system and the IMAGE'S architecture, as a single file's + // is: one of this machine's architecture cannot map an image of + // another (measured on 2026.10.28.1: an x86-64 program carrying an + // aarch64 image, runnable on no machine). 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 }); + arch_of(plan.triple), false }); if (!packed) return std::unexpected(packed.error()); fs::copy(*packed, plan.stagingRoot / "bin", fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec); @@ -3220,6 +3272,35 @@ std::string unbuilt_row(std::string_view system, std::string_view arch) { return detail::row_is_built(row) ? std::string() : row.triple; } +std::expected +runner_program(const mcpp::platform::Resolved& platform, const std::filesystem::path& projectRoot, + std::string_view name, std::string_view arch, bool build) { + if (!platform.active || !platform.fields.carrier.set) + return std::unexpected(Error{std::format( + "the runner `{}` of {} runs on a machine of the artifact's architecture, {}, and its " + "program for that architecture is the platform's carrier, which {} does not state " + "([platform] carrier = {{ package, section, main }})", + name, platform.display(), arch, platform.display())}); + const std::string system(detail::host_system()); + if (auto why = detail::unbuilt_carrier(platform, system, arch, false)) + return std::unexpected(Error{*why}); + const auto program = detail::packed_program(detail::carrier_dir(projectRoot, name, system, arch), + name, system); + if (!build) return program; + Plan plan; + plan.platform = platform; + plan.projectRoot = projectRoot; + plan.binaryName = std::string(name); + auto packed = detail::carrier_build(plan, { {}, {}, std::string(name), system, std::string(arch), + false, "the runner on the execution host" }); + if (!packed) return std::unexpected(packed.error()); + std::error_code ec; + if (!std::filesystem::is_regular_file(program, ec)) + return std::unexpected(Error{std::format("the carrier built for {}-{} has no {}", system, arch, + program.filename().string())}); + return program; +} + std::expected, Error> single_file(const Plan& plan) { namespace fs = std::filesystem; namespace d = mcpp::pack::distribution; diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index b5a1e03e8..da73763a5 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -422,6 +422,16 @@ std::optional refused_combination(const mcpp::build::BuildContext& 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"); + // A single file is an executable of one system (the engine's launcher, or + // the platform's carrier); a WebAssembly module is no program of a system, + // and no launcher of `wasm32-linux-musl` (the row this would have tried) + // exists. The web is reached through a provider's page. + if (auto t = mcpp::toolchain::triple::parse(ctx.tc.targetTriple); + opts.format == mcpp::pack::Format::SingleFile && t && t->is_wasm()) + return std::format("--pkg-format single-file makes an executable of one system, and {} " + "builds a WebAssembly module a JavaScript engine runs.\n" + " use: --pkg-format tar | zip | dir, or a provider's format " + "(dist-web of mcpp-plugins writes a page)", t->str()); 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" diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm index a00609753..2034f5d9e 100644 --- a/src/platform/platform.cppm +++ b/src/platform/platform.cppm @@ -117,6 +117,19 @@ std::vector expand_step(const std::vector& step, std::string_view artifact, std::span needed); +// ── The run (§10) ─────────────────────────────────────────────────────────── + +// Where the runner's program comes from when `mcpp run` and `mcpp test` start +// an artifact of `targetArch` on a machine of `hostArch`. A `same-arch` runner +// runs the artifact on a machine of its architecture: here when the two +// agree; on the execution host (`run_under`) otherwise, whose architecture is +// the artifact's, so the program is that architecture's; refused when no +// execution host is named. `--no-runner` states the triple is native here. +// Every other runner reaches the artifact from this machine. +enum class RunnerReach { Here, TargetArchitecture, Refused }; +RunnerReach runner_reach(const mcpp::manifest::platform::Runner& runner, std::string_view targetArch, + std::string_view hostArch, bool executionHostNamed, bool noRunner); + // ── What the build uses (§9) ──────────────────────────────────────────────── // The platform of one graph after every tier has spoken: what the flags, the @@ -462,6 +475,13 @@ std::vector expand_step(const std::vector& step, return out; } +RunnerReach runner_reach(const mp::Runner& runner, std::string_view targetArch, + std::string_view hostArch, bool executionHostNamed, bool noRunner) { + if (noRunner || !runner.set || runner.host != "same-arch" || targetArch == hostArch) + return RunnerReach::Here; + return executionHostNamed ? RunnerReach::TargetArchitecture : RunnerReach::Refused; +} + std::string Resolved::identity() const { if (!active) return {}; std::string id = std::format("{}@{}", package, version); diff --git a/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh b/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh new file mode 100755 index 000000000..664b0f781 --- /dev/null +++ b/tests/e2e/960_an_image_of_another_architecture_runs_through_its_execution_host.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# requires: llvm python3 +# 960 -- an ok image of another architecture runs through its execution host +# (SPEC-013 §10; design 2026-10-29). +# +# openkal-platform's runner is `{ cmd = ["okrun", "{}"], host = "same-arch" }`: +# it runs an image on a machine of the image's architecture. On this machine +# an image of the other architecture is refused, naming `run_under`. With an +# execution host named, the runner runs there, so its program is the +# platform's runner for the image's architecture -- the carrier with nothing +# carried, built for this system -- and a user-mode emulator +# (`run_under = ["qemu-aarch64"]`) runs an aarch64 image on x86_64. Measured on +# 2026.10.28.1: qemu was handed the x86_64 okrun ("Invalid ELF image for this +# architecture"). +# +# Criteria: +# A. `mcpp run`, `mcpp test --target -openkal`, `mcpp run`: the +# third prints no "fingerprint changed": the build of the package's +# program before its tests is recorded under its target, not as this +# machine's. +# B. `mcpp test --target -openkal` exits 2 and prints the refusal in +# full, its `run_under` line included, exactly once. +# C. `mcpp self env --target -openkal` says runs are refused here +# (`runner.refusal` in JSON); with an execution host named, the runner's +# program is the other architecture's and the execution host's program +# is reported. +# D. An execution host that records what it is handed receives, from +# `mcpp run` and `mcpp test`, the platform's runner of the other +# architecture (an ELF of that machine); where this machine has that +# architecture's qemu, the image runs and the test passes through it. +set -e +source "$(dirname "$0")/_host_path.sh" + +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; } +TMP_HOST="$(host_path "$TMP")" + +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 +HT="$HOST_ARCH-openkal" +OT="$OTHER-openkal" +RUN_UNDER_KEY="MCPP_TARGET_$(printf '%s' "$OT" | tr 'a-z-' 'A-Z_')_RUN_UNDER" + +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 + +# The architecture of an ELF file (e_machine). +elf_arch() { + python3 - "$1" <<'PY' +import struct, sys +d = open(sys.argv[1], "rb").read(20) +assert d[:4] == b"\x7fELF", "not an ELF file" +m = struct.unpack(" a1.log 2>&1 || fail "A: mcpp run" a1.log +"$MCPP" test --target "$HT" > a2.log 2>&1 || fail "A: mcpp test --target $HT" a2.log +"$MCPP" run > a3.log 2>&1 || fail "A: mcpp run after the test" a3.log +grep -q "fingerprint changed" a3.log && fail "A: the test's build was recorded as this machine's" a3.log +grep -qx "\[target=$HT\]" target/.build_cache || fail "A: no cache entry for $HT" target/.build_cache +echo "ok: the build before a target's tests is recorded under the target" + +# B +rc=0 +"$MCPP" test --target "$OT" > b.log 2>&1 || rc=$? +[ "$rc" = 2 ] || fail "B: mcpp test --target $OT exited $rc, not 2" b.log +grep -q "smoke ... not run" b.log || fail "B: the test is not reported not run" b.log +[ "$(grep -c "run_under = " b.log)" = 1 ] || fail "B: the run_under line is not printed exactly once" b.log +grep -q "cannot run $OT artifacts on this $HOST_ARCH host" b.log || fail "B: the reason" b.log +echo "ok: the refusal is printed in full, once" + +# C +"$MCPP" self env --target "$OT" > c.log 2>&1 || fail "C: self env" c.log +grep -q "refused on this machine: cannot run $OT artifacts" c.log || fail "C: self env does not say runs are refused" c.log +"$MCPP" self env --target "$OT" --format json > c.json 2> c.err || fail "C: self env --format json" c.err c.json +python3 - c.json "$HOST_ARCH" <<'PY' || fail "C: the refusal in JSON" c.json +import json, sys +r = json.load(open(sys.argv[1]))["data"]["runner"] +assert r["refusal"] and "run_under" in r["refusal"], r +assert r["arch"] == sys.argv[2], r +PY +printf '#!/bin/sh\nprintf "%%s\\n" "$@" > "$VIA_LOG"\n[ -n "$VIA_QEMU" ] && exec "$VIA_QEMU" "$@"\nexit 0\n' > "$TMP/via" +chmod +x "$TMP/via" +env "$RUN_UNDER_KEY=$TMP_HOST/via" "$MCPP" self env --target "$OT" --format json > c2.json 2> c2.err \ + || fail "C: self env with an execution host" c2.err c2.json +python3 - c2.json "$OTHER" "$TMP/via" <<'PY' || fail "C: self env with an execution host" c2.json +import json, os, sys +d = json.load(open(sys.argv[1]))["data"] +r = d["runner"] +assert r["refusal"] is None, r +assert r["arch"] == sys.argv[2], r +assert "/carrier-pack/okrun-" in r["program"].replace("\\", "/") and r["program"].replace("\\", "/").endswith(sys.argv[2] + "/target/dist/packed/bin/okrun"), r +assert os.path.samefile(d["runUnderProgram"], sys.argv[3]), d["runUnderProgram"] +PY +echo "ok: self env states the refusal, and the other architecture's runner for an execution host" + +# D +printf '\n[target.%s]\nrun_under = ["%s/via"]\n' "$OT" "$TMP_HOST" >> mcpp.toml +VIA_LOG="$TMP/via.run" "$MCPP" run --target "$OT" -- one > d1.log 2>&1 || fail "D: mcpp run through the execution host" d1.log +grep -q "okrun for linux-$OTHER" d1.log || grep -q "okrun for macos-$OTHER" d1.log \ + || fail "D: the other architecture's runner was not built" d1.log +runner=$(head -1 "$TMP/via.run") +[ -f "$runner" ] || fail "D: the execution host was handed no program" "$TMP/via.run" +[ "$(elf_arch "$runner")" = "$OTHER" ] || fail "D: the runner handed over is $(elf_arch "$runner"), not $OTHER" "$TMP/via.run" +grep -q "/bin/app.oke$" "$TMP/via.run" || fail "D: the image is not the runner's argument" "$TMP/via.run" +VIA_LOG="$TMP/via.test" "$MCPP" test --target "$OT" > d2.log 2>&1 || fail "D: mcpp test through the execution host" d2.log +grep -q "smoke ... ok" d2.log || fail "D: the test did not pass through the execution host" d2.log +[ "$(head -1 "$TMP/via.test")" = "$runner" ] || fail "D: the test's runner is not the run's" "$TMP/via.test" +echo "ok: the execution host runs the image with the runner of its architecture" + +QEMU=$(command -v "qemu-$OTHER" || command -v "qemu-$OTHER-static" || true) +if [ -z "$QEMU" ]; then + echo " (no qemu-$OTHER on this machine: the image is not executed)" +else + VIA_LOG="$TMP/via.q" VIA_QEMU="$QEMU" "$MCPP" run --target "$OT" -- one > d3.log 2>&1 \ + || fail "D: mcpp run under $QEMU" d3.log + grep -q "image: caught (2 args)" d3.log || fail "D: the image's output under $QEMU" d3.log + VIA_LOG="$TMP/via.qt" VIA_QEMU="$QEMU" "$MCPP" test --target "$OT" > d4.log 2>&1 \ + || fail "D: mcpp test under $QEMU" d4.log + grep -q "smoke ... ok" d4.log || fail "D: the test under $QEMU" d4.log + echo "ok: $QEMU runs the $OTHER image and its test" +fi + +echo "PASS: 960 an image of another architecture runs through its execution host" diff --git a/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh b/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh new file mode 100755 index 000000000..6cfa4d4c7 --- /dev/null +++ b/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# requires: llvm python3 +# 961 -- a self-contained package of one ok-image target is a program of the +# image's architecture, and `--list-formats` lists what the target offers +# (SPEC-014 §1.2, §6; SPEC-013 §11; design 2026-10-29). +# +# Measured on 2026.10.28.1, on x86_64: `mcpp pack --target aarch64-openkal +# --no-universal --mode self-contained` wrote an x86-64 program carrying an +# aarch64 image, which runs on no machine; `mcpp pack --target aarch64-openkal +# --list-formats` planned this machine and left out the platform's `okb`. +# +# Criteria: +# A. The package's `bin/app` is an ELF of the image's architecture; where +# this machine has that architecture's qemu, it runs the image. +# B. `mcpp pack --target -openkal --list-formats` lists `okb`, provided by +# the platform; `mcpp pack --list-formats` for this machine does not. +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) OTHER=aarch64 ;; + aarch64|arm64) OTHER=x86_64 ;; + *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; +esac +OT="$OTHER-openkal" + +mkdir -p "$TMP/app/src" +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 + +# A +"$MCPP" pack --target "$OT" --no-universal --mode self-contained --pkg-format dir -o m3 > a.log 2>&1 \ + || fail "A: pack" a.log +[ -f target/dist/m3/bin/app ] || fail "A: the package has no program" a.log +python3 - target/dist/m3/bin/app "$OTHER" <<'PY' || fail "A: the program's architecture" a.log +import struct, sys +d = open(sys.argv[1], "rb").read(20) +assert d[:4] == b"\x7fELF", "not an ELF file" +m = {62: "x86_64", 183: "aarch64"}.get(struct.unpack(" a-run.log 2>&1 || fail "A: the program under $QEMU" a-run.log + grep -q "image: caught (2 args)" a-run.log || fail "A: the program's image under $QEMU" a-run.log + echo " ok: $QEMU runs it" +fi +echo "ok: a one-target self-contained package is a program of the image's architecture" + +# B +"$MCPP" pack --target "$OT" --list-formats > b.json 2> b.err || fail "B: --list-formats --target $OT" b.err b.json +"$MCPP" pack --list-formats > b-host.json 2> b-host.err || fail "B: --list-formats" b-host.err b-host.json +python3 - b.json b-host.json <<'PY' || fail "B: the formats" b.json b-host.json +import json, sys +target = {f["name"]: f for f in json.load(open(sys.argv[1]))["data"]["formats"]} +host = {f["name"] for f in json.load(open(sys.argv[2]))["data"]["formats"]} +assert "okb" in target, sorted(target) +assert any(p.endswith("openkal-platform") for p in target["okb"]["providers"]), target["okb"] +assert "okb" not in host, sorted(host) +PY +echo "ok: --list-formats lists the target's formats" + +echo "PASS: 961 a one-target package carries the image in a program of its architecture" diff --git a/tests/e2e/962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh b/tests/e2e/962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh new file mode 100755 index 000000000..4011a7cb2 --- /dev/null +++ b/tests/e2e/962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# requires: unix-shell +# 962 -- a WebAssembly package is neither an xlings entry nor a single file +# (SPEC-014 §1.5, §8; design 2026-10-29). +# +# An xlings entry installs a program of the system it runs on, under that +# system's key and for machine architectures; a `wasm32-emscripten` package +# is a module a JavaScript engine runs (`bin/.js`, `bin/.wasm`). +# `mcpp emit xpkg` described one under `linux` with `archs = {"wasm32"}` +# (measured on 2026.10.28.1). A single file is an executable of one system, +# and `--pkg-format single-file` on the wasm row tried a `wasm32-linux-musl` +# launcher. Both are refused, naming the route that serves the web. +# +# Criteria: +# A. `mcpp emit xpkg --package` of a wasm32-emscripten package exits 2, +# naming the package, its triple and `dist-web`, and writes no entry. +# B. Where an emsdk payload is installed, `mcpp pack --target +# wasm32-emscripten --pkg-format single-file` is refused before anything +# compiles, naming both choices and the formats that serve the 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; } + +# A -- the package `mcpp pack --target wasm32-emscripten` writes, as its +# mcpp-package.json describes it. +top=web-0.1.0-wasm32-emscripten +mkdir -p "$TMP/$top/bin" +cd "$TMP" +printf 'console.log("web");\n' > "$top/bin/web.js" +printf '\0asm\1\0\0\0' > "$top/bin/web.wasm" +cat > "$top/mcpp-package.json" <<'JSON' +{"schema": 1, "name": "web", "version": "0.1.0", "platform": null, "linkage": "declared", + "mode": "vendored", "legs": ["wasm32-emscripten"], "systems": [], + "launch": {"program": "bin/web.js"}, + "files": [{"path": "bin/web.js", "role": "program", "leg": "wasm32-emscripten", "sha256": "0"}, + {"path": "bin/web.wasm", "role": "program", "leg": "wasm32-emscripten", "sha256": "0"}]} +JSON +tar czf "$top.tar.gz" "$top" +rc=0 +"$MCPP" emit xpkg --package "$top.tar.gz" --url https://example.org/v0.1.0 > a.lua 2> a.err || rc=$? +[ "$rc" = 2 ] || fail "A: emit xpkg of a wasm package exited $rc, not 2" a.err a.lua +grep -q "$top.tar.gz is a package of wasm32-emscripten, which is no program of Linux, macOS or Windows" a.err \ + || fail "A: the refusal does not name the package and its triple" a.err +grep -q "dist-web" a.err || fail "A: the refusal does not name the route of the web" a.err +[ -s a.lua ] && fail "A: an entry was written" a.lua +echo "ok: emit xpkg refuses a WebAssembly package" + +# B +have_emsdk=0 +for d in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-emsdk/*/emscripten \ + "$HOME"/.xlings/data/xpkgs/xim-x-emsdk/*/emscripten; do + [[ -x "$d/em++" ]] && have_emsdk=1 +done +if [ "$have_emsdk" -ne 1 ]; then + echo " (no emsdk payload installed: B is not run here)" +else + mkdir -p "$TMP/web/src" + cd "$TMP/web" + printf '[package]\nname = "web"\nversion = "0.1.0"\n' > mcpp.toml + printf 'import std;\nint main() { std::println("web"); }\n' > src/main.cpp + rc=0 + "$MCPP" pack --target wasm32-emscripten --pkg-format single-file > b.log 2>&1 || rc=$? + [ "$rc" = 2 ] || fail "B: single-file of a wasm program exited $rc, not 2" b.log + grep -q "single-file makes an executable of one system, and wasm32-emscripten builds a WebAssembly module" b.log \ + || fail "B: the refusal does not name both choices" b.log + grep -q "use: --pkg-format tar | zip | dir" b.log || fail "B: the refusal names no way out" b.log + grep -q "Compiling" b.log && fail "B: refused after compiling" b.log + echo "ok: --pkg-format single-file refuses a WebAssembly program before a build" +fi + +echo "PASS: 962 a WebAssembly package is neither an xlings entry nor a single file" diff --git a/tests/e2e/timings/linux.tsv b/tests/e2e/timings/linux.tsv index 38a1c59da..2b513fb91 100644 --- a/tests/e2e/timings/linux.tsv +++ b/tests/e2e/timings/linux.tsv @@ -586,5 +586,8 @@ 957_packages_merge_and_describe_themselves.sh 93760 958_a_build_program_runs_again_when_its_output_is_gone.sh 1260 959_a_package_is_keyed_by_every_module_it_imports.sh 25000 +960_an_image_of_another_architecture_runs_through_its_execution_host.sh 150000 +961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh 120000 +962_a_webassembly_package_is_neither_an_xlings_entry_nor_a_single_file.sh 1000 96_msvc_unavailable_nonwindows.sh 38 98_reflection_import_std.sh 7170 diff --git a/tests/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp index 58860e1a1..b8c40ec98 100644 --- a/tests/unit/test_pack_distribution.cpp +++ b/tests/unit/test_pack_distribution.cpp @@ -727,3 +727,43 @@ TEST(PackDistribution, XpkgDescriptorNamesEachSystemsProgramDirectory) { EXPECT_NE(r->find("path.join(pkginfo.install_dir(), bindir[os.host()])"), std::string::npos) << *r; EXPECT_NE(r->find("name = \"okrun\""), std::string::npos) << *r; } + +// An entry describes packages of one leg for the system their program runs on +// (SPEC-014 §8): a Linux, macOS or Windows program. A leg of no such system -- +// a WebAssembly module, an Android application, a bare-metal image, an ok image +// of one leg -- is refused, naming the package, the triple and the route that +// distributes it; it was written `linux` before. +TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { + auto one = [](std::string triple, std::string top, std::string program) { + d::Asset a; + a.file = top + ".tar.gz"; + a.top = top; + a.sha256 = std::string(64, 'e'); + a.pkg.name = "web"; + a.pkg.version = "0.1.0"; + a.pkg.legs = { triple }; + a.pkg.program = program; + return a; + }; + struct Row { const char* triple; const char* says; }; + for (auto [triple, says] : { Row{ "wasm32-emscripten", "dist-web" }, + Row{ "aarch64-linux-android", "Linux, macOS or Windows target" }, + Row{ "riscv64-none-elf", "Linux, macOS or Windows target" }, + Row{ "x86_64-openkal", "universal package" } }) { + auto r = d::xpkg_descriptor({ one(triple, std::format("web-0.1.0-{}", triple), "bin/web.js") }, + "https://example.org/0.1.0"); + ASSERT_FALSE(r) << triple << ": " << *r; + EXPECT_NE(r.error().find(std::format("is a package of {}, which is no program of Linux, " + "macOS or Windows", triple)), std::string::npos) << r.error(); + EXPECT_NE(r.error().find(says), std::string::npos) << r.error(); + } + for (auto [triple, system] : { std::pair{ "x86_64-linux-gnu", "linux" }, + std::pair{ "aarch64-linux-musl", "linux" }, + std::pair{ "aarch64-macos", "macosx" }, + std::pair{ "x86_64-windows-msvc", "windows" } }) { + auto r = d::xpkg_descriptor({ one(triple, std::format("tool-0.1.0-{}", triple), "bin/tool") }, + "https://example.org/0.1.0"); + ASSERT_TRUE(r) << triple << ": " << r.error(); + EXPECT_NE(r->find(std::format(" {} = {{", system)), std::string::npos) << *r; + } +} diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp index b84b7e6e3..94bed6001 100644 --- a/tests/unit/test_platform.cpp +++ b/tests/unit/test_platform.cpp @@ -426,6 +426,42 @@ TEST(PlatformLink, SealSteps) { (std::vector{"sign", "--image=x", "a.okso", "b.okso"})); } +// ── The run (§10) ─────────────────────────────────────────────────────────── + +// A `same-arch` runner runs an artifact on a machine of its architecture: +// this one when they agree; the execution host, with that architecture's +// program, when one is named; refused otherwise. `--no-runner` and every +// other runner are this machine's. +TEST(PlatformRun, TheRunnerReachesTheArtifactsArchitecture) { + using R = pf::RunnerReach; + mp::Runner same; + same.set = true; + same.cmd = {"okrun", "{}"}; + same.host = "same-arch"; + mp::Runner any = same; + any.host = "any"; + mp::Runner bare = same; + bare.host.clear(); + mp::Runner none; + struct Row { const mp::Runner* runner; const char* target; bool executionHost; bool noRunner; R want; }; + const Row rows[] = { + { &same, "x86_64", false, false, R::Here }, + { &same, "x86_64", true, false, R::Here }, + { &same, "aarch64", false, false, R::Refused }, + { &same, "aarch64", true, false, R::TargetArchitecture }, + { &same, "aarch64", true, true, R::Here }, + { &same, "aarch64", false, true, R::Here }, + { &any, "aarch64", true, false, R::Here }, + { &any, "aarch64", false, false, R::Here }, + { &bare, "aarch64", true, false, R::Here }, + { &none, "aarch64", true, false, R::Here }, + }; + for (auto const& r : rows) + EXPECT_EQ(pf::runner_reach(*r.runner, r.target, "x86_64", r.executionHost, r.noRunner), r.want) + << r.runner->host << " " << r.target << " executionHost=" << r.executionHost + << " noRunner=" << r.noRunner; +} + // ── The report ────────────────────────────────────────────────────────────── TEST(PlatformReport, SourcesAndJson) { From ce0aeb77e05726ba8099895dab876afebd628f20 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 29 Oct 2026 10:57:43 +0900 Subject: [PATCH 2/2] emit xpkg: a one-leg package is described for the system its program runs on; a carrier package records its carrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #859: the rule refused every one-leg ok-image package, and a package that carries its image in the carrier (--no-universal --mode self-contained or static) is a program of the carrier's system: on Linux, a static Linux program of the image's architecture. It was described under linux before, and refusing it was a regression. - mcpp-package.json `systems` (and the mcpp.pack envelope's, through one function, mcpp::pack::carried_systems) records the carrier of a one-leg self-contained ok-image package as the loader it carries: `-`, e.g. ["linux-aarch64"]. It was empty, so the system was not a fact a reader could take. - emit xpkg describes a one-leg package for the system and architecture of its program (program_host): the triple's for a Linux (not Android), macOS or Windows target, the recorded carrier's for an ok image. It refuses only packages whose program is no program of a system: wasm, Android and iOS applications, bare metal, and an ok image without its carrier (route: the universal package, or a package that carries it). - SPEC-014 §4.5 and §8, criterion 9; docs/10 and docs/50 with docs/zh; CHANGELOG; the design record's rev 2. - Unit XpkgDescriptorDescribesProgramsOfASystem: a one-leg x86_64-openkal carrier package packed on Linux is described under linux with arch x86_64, an aarch64 one packed on macOS under macosx, a carrier-less one is refused. e2e 961 C: the package's systems, its entry under this system for the image's architecture, and the refusal of the vendored image package. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- ...2026-10-29-openkal-run-test-pack-design.md | 14 +++-- CHANGELOG.md | 15 +++-- docs/10-pack-and-release.md | 14 +++-- docs/50-machine-output.md | 2 +- docs/specs/packaging.md | 19 ++++--- docs/zh/10-pack-and-release.md | 6 +- docs/zh/50-machine-output.md | 2 +- src/pack/distribution.cppm | 57 ++++++++++++------- src/pack/pack.cppm | 20 +++++++ src/pack/pipeline.cppm | 2 +- ..._image_in_a_program_of_its_architecture.sh | 31 ++++++++++ tests/unit/test_pack_distribution.cpp | 32 +++++++++-- 12 files changed, 163 insertions(+), 51 deletions(-) diff --git a/.agents/docs/2026-10-29-openkal-run-test-pack-design.md b/.agents/docs/2026-10-29-openkal-run-test-pack-design.md index 7ce7c7c1f..0d3c03e3c 100644 --- a/.agents/docs/2026-10-29-openkal-run-test-pack-design.md +++ b/.agents/docs/2026-10-29-openkal-run-test-pack-design.md @@ -10,7 +10,10 @@ status: active **修订记录** -- rev 1(本稿):七项的实测与定案;一个发布 PR。 +- rev 1:七项的实测与定案;一个发布 PR(#859)。 +- rev 2(本稿,评审):§2.6 的 `emit xpkg` 规则改为按**程序**运行的系统描述。带载体的单腿 ok 映像包(M3)是载体所属系统的 + 程序,rev 1 把它一并拒绝,对在 Linux 上打出、此前可用的条目是回归;包现在在 `systems` 中记录其载体的 `-`,条目 + 读这一事实,只拒绝不带载体的 ok 映像包。 ## 0. 摘要 @@ -83,9 +86,10 @@ F5 的对照:把运行器手工改为 `cmd = ["qemu-aarch64", "` 安装该系统的程序,`archs` 为机器架构)。`wasm32-emscripten` 的包两者都不是:它没有启动器,`bin/.js` 由 JavaScript 引擎或浏览器执行,`wasm32` 不是任何系统的机器架构,xvm 注册的 `bin/` 不存在。把它描述成系统无关的条目需要为 wasm 生成启动器并在条目中写运行器前缀,即一个新形状,而 Web 的分发路径是 `dist-web` 产出的页面,不是 xlings。规则按模型写成一般形式: -单腿包按其腿的三元组的系统描述,三元组不是 Linux(非 Android)、macOS 或 Windows 程序时拒绝。同一条规则也拒绝其他不属于这三个系统 -的单腿包(Android、iOS、裸机,以及 `--no-universal` 的 ok 映像包——xlings 经通用包安装 ok 映像);此前它们都被写成 `linux` -或 `macosx`。 +单腿包按其**程序**运行的系统与架构描述。腿为 Linux(非 Android)、macOS 或 Windows 的三元组时取三元组的;腿为 ok 映像而包携带 +载体(`self-contained`、`static`)时,程序是载体,为本机系统、映像架构的程序,包在 `systems` 中记录它(`-`,与通用包 +记录 `libexec/` 中加载器的字段相同),条目读这一记录而不推测。程序不是任何系统的程序时拒绝:WebAssembly、Android、iOS、裸机, +以及不带载体的 ok 映像包(一个 `.oke` 需要运行器,出路为通用包);此前它们都被写成 `linux` 或 `macosx`。 **`--pkg-format single-file`:拒绝。** 单文件是一个系统执行的可执行文件(原生启动器或载体);WebAssembly 模块不是。拒绝在 `refused_combination` 中,任何构建之前,写出两个选择与出路(`tar`、`zip`、`dir` 或提供者的格式,页面为 `dist-web`)。 @@ -105,7 +109,7 @@ ok 映像是 x86_64 与 aarch64 上位置无关的 ELF,只导入 `kal_*`,需 | F5 | `PlatformRun.TheRunnerReachesTheArtifactsArchitecture`(`runner_reach` 的表) | 960 D:记录 argv 的执行宿主收到的运行器程序是另一架构的 ELF;本机有该架构的 qemu 时真实运行 `run` 与 `test` | | F6 | — | 960 C:人类输出给出拒绝;JSON 的 `runner.refusal`、`runner.arch`;给出执行宿主时为另一架构 | | F2、F3 | — | 961:载体为映像架构的 ELF(有 qemu 时运行它);`--target -openkal --list-formats` 含 `okb`,本机目标不含 | -| F7a | `XpkgDescriptorDescribesProgramsOfASystem` | 962 A:合成的 wasm32-emscripten 包被拒绝,消息写出三元组与出路 | +| F7a | `XpkgDescriptorDescribesProgramsOfASystem`(含 Linux 与 macOS 上打出的带载体包、不带载体的包) | 962 A:合成的 wasm32-emscripten 包被拒绝,消息写出三元组与出路;961 C:带载体的包的 `systems` 与条目,不带载体的包被拒绝 | | F7b | — | 962 B:有 emsdk 载荷时 `--pkg-format single-file --target wasm32-emscripten` 在编译前被拒绝 | ## 4. 文档 diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a2ae881..5bf63c335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,11 +59,16 @@ Every triple that was not Windows or macOS was written `linux`: a `wasm32-emscripten` package became a Linux entry with `archs = {"wasm32"}`. An entry's `xpm.` installs a program of that system for machine - architectures, and a WebAssembly package has no launcher, no system and no - machine architecture, so a package of one leg that is no Linux (other than - Android), macOS or Windows program is refused, naming the route that - distributes it: a page through `dist-web`, or for an ok image its - platform's universal package (SPEC-014 §8; e2e 962). + architectures. A package of one leg is now described for the system and + architecture of its program: the triple's for a Linux (other than Android), + macOS or Windows target, and for an ok image packed with its carrier + (`--no-universal --mode self-contained` or `static`) the carrier's, which + the package now records in `mcpp-package.json` `systems` (and the `mcpp.pack` + envelope's), e.g. `["linux-aarch64"]`. A package whose program is no program + of a system -- a WebAssembly module, an Android or iOS application, a + bare-metal image, an ok image without its carrier -- is refused, naming the + route that distributes it: a page through `dist-web`, or the platform's + universal package (SPEC-014 §4.5, §8; e2e 961, 962). - **`--pkg-format single-file` is refused on a WebAssembly target.** A single file is an executable of one system; the pack tried a `wasm32-linux-musl` launcher. The refusal comes before anything compiles and names `tar`, diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index b03c35fc0..ab3c726ec 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -895,11 +895,15 @@ sha256 per architecture. The install moves the archive's top directory into place and registers the program with xvm (the `.cmd` launcher on Windows), from each system's own directory when the packages place the program apart (a Windows program at the root, a POSIX one under `bin/`). Each archive is its -own `--package`. A package of one leg is described for the system of its triple -(Linux, macOS or Windows); one of another target is refused, naming the route -that distributes it (2026.10.29.1+): a `wasm32-emscripten` package is published -as a page (`dist-web`), an ok image as its platform's universal package, and an -Android, iOS or bare-metal package has no entry. +own `--package`. A package of one leg is described for the system and +architecture its program runs on (2026.10.29.1+): the triple's for a Linux, +macOS or Windows target, and for an ok image packed with its carrier +(`--no-universal --mode self-contained`) the carrier's, which the package +records in `systems` (`"systems": ["linux-aarch64"]` for an aarch64 image packed +on Linux). A package whose program is no program of a system is refused, naming +the route that distributes it: a `wasm32-emscripten` package is published as a +page (`dist-web`), an ok image without its carrier as its platform's universal +package, and an Android, iOS or bare-metal package has no entry. ### Compression and signing diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 5927273c6..b559f79b5 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -730,7 +730,7 @@ to stderr, including what the build programs and tools the pack starts print. | `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 | +| `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, or of the carrier that is the program of a self-contained package of one ok image (2026.10.29.1+) | | `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 diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md index 547edde78..2e64683a7 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -215,7 +215,7 @@ WebAssembly 的同名文件族,ok 映像的 `bin/`),规定见 docs/10。 | `linkage` | `static`、`shared`,或 `declared`(每个库按自己的声明) | | `mode` | §1.2 的模式:`system`、`vendored`、`self-contained`;`--mode static` 的原生包为 `static`,映像为 `self-contained` | | `legs` | 腿的规范三元组 | -| `systems` | `libexec//` 中加载器的 `-`;无加载器时为空 | +| `systems` | 包携带的加载器的 `-`:通用包为 `libexec//` 中的;单腿 `self-contained` 的 ok 映像包为其载体,即程序所属的系统与映像的架构(2026.10.29.1);无加载器时为空 | | `launch` | 通用包为 `runner`(平台的 argv)与每个架构的 `entry`;单腿原生包为 `program`(程序的相对路径) | | `files` | 树中每个文件(符号链接除外):`path`(相对于树根,`/` 分隔)、`role`、`leg`(所属的腿,共用文件为 `null`)、`sha256` | @@ -317,11 +317,13 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 中同名的包;openkal 为 `okrun`)。依赖带命名空间:不带命名空间的名字在家目录列出的第二个索引也有同名包时有歧义, xim-pkgindex 拒绝它。 - 各系统各架构一个的包:每个系统一项,一个以 `${arch}` 代替架构的 URL 与每个架构的 sha256;文件名除架构外不同时 - 拒绝。单腿包的系统由其腿的三元组决定:Linux(Android 除外)为 `linux`,macOS 为 `macosx`,Windows 为 `windows`。 - 腿不是这三个系统之一的程序时(WebAssembly 包、Android 与 iOS 应用、裸机映像、单腿的 ok 映像包),条目**必须**被拒绝, - 消息写出包、三元组与出路:WebAssembly 包以 `dist-web` 发布为页面,ok 映像以平台的通用包(不带 `--no-universal` 的 - 打包)描述。原因:条目的 `xpm.<系统>` 安装该系统的程序,`archs` 为机器架构;WebAssembly 包没有启动器,其程序由 - JavaScript 引擎运行,`wasm32` 不是任何系统的机器架构(2026.10.29.1)。 + 拒绝。单腿包按其**程序**运行的系统与架构描述:腿为 Linux(Android 除外)、macOS 或 Windows 的三元组时,为该三元组的 + 系统(`linux`、`macosx`、`windows`)与架构;腿为 ok 映像而包携带其载体(`self-contained`,含 `--mode static`)时, + 程序是载体,为包在 `systems` 中记录的唯一 `-`(§4.5)。程序不是任何系统的程序时(WebAssembly 包、Android + 与 iOS 应用、裸机映像、不带载体的 ok 映像包),条目**必须**被拒绝,消息写出包、三元组与出路:WebAssembly 包以 + `dist-web` 发布为页面,不带载体的 ok 映像以平台的通用包(不带 `--no-universal` 的打包)或带载体的包描述。原因:条目的 + `xpm.<系统>` 安装该系统的程序,`archs` 为机器架构;WebAssembly 包没有启动器,其程序由 JavaScript 引擎运行, + `wasm32` 不是任何系统的机器架构;一个 `.oke` 需要运行器(2026.10.29.1)。 - 安装把归档的唯一顶层目录移到安装目录。该目录以归档命名(`mcpp pack` 的写法),安装取下载文件的名字去掉扩展名, 对每个系统、架构与索引日后追加的版本都成立;打包后改名的归档与 xim 不解开的文件(非 `.tar.gz`、`.tar.zst`、 `.tar.xz`、`.tar`、`.zip`)在写出条目时被拒绝;配置以 xvm 注册程序(通用包为 `bin/`,Windows 上为 @@ -354,7 +356,8 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 7. 通用包的 `mcpp emit xpkg` 条目在三个系统上为同一资产,并以运行器为依赖(e2e 957)。 8. `mcpp emit xpkg` 拒绝 `wasm32-emscripten` 的包,写出包、三元组与 `dist-web`;`--pkg-format single-file` 在 WebAssembly 目标上于任何编译之前被拒绝(e2e 962)。 -9. 单腿的 `self-contained` ok 映像包的程序是映像架构的;`mcpp pack --target --list-formats` 列出该目标的平台提供的格式 +9. 单腿的 `self-contained` ok 映像包的程序是映像架构的,`systems` 记录其载体的 `-`,`mcpp emit xpkg` 以该系统 + 与架构描述它,不带载体的单腿 ok 映像包被拒绝;`mcpp pack --target --list-formats` 列出该目标的平台提供的格式 (`okb`),本机目标的列表不含它(e2e 961)。 ## 变更记录 @@ -364,4 +367,4 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 | 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。 | | 1.2 | 2026-10-28 | 包名的模式后缀;单文件启动器自身失败的退出状态 127;`--merge` 取 `[pack] format`,合并后的目录以输出命名;`--linkage static` 改为 musl 的行;`mcpp emit xpkg --package` 的每系统程序目录、带命名空间的运行器依赖、以归档名定位的顶层目录与多余词的拒绝。 | -| 1.3 | 2026-10-29 | mcpp 2026.10.29.1:§0 ok 映像与 Web;§1.2 单腿 `self-contained` 包的载体为映像架构的程序;§1.5、§5.2 WebAssembly 目标上的单文件被拒绝;§6 `--list-formats` 规划所选目标;§8 单腿包的系统由三元组决定,不属于 Linux、macOS、Windows 的腿被拒绝;判据 8、9(e2e 961、962)。 | +| 1.3 | 2026-10-29 | mcpp 2026.10.29.1:§0 ok 映像与 Web;§1.2 单腿 `self-contained` 包的载体为映像架构的程序;§1.5、§5.2 WebAssembly 目标上的单文件被拒绝;§4.5 单腿 `self-contained` ok 映像包的 `systems` 记录其载体;§6 `--list-formats` 规划所选目标;§8 单腿包按其程序运行的系统与架构描述(系统的三元组,或 ok 映像的载体),程序不是任何系统的程序的被拒绝;判据 8、9(e2e 961、962)。 | diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 933f91ca0..d5b236b8e 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -757,8 +757,10 @@ WebAssembly 目标没有单文件:`--pkg-format single-file` 与 `--target was 包不携带运行器时以运行器为依赖(`deps = { "xim:okrun" }`);各系统各架构一个的包,每个系统一项,一个带 `${arch}` 的 URL 与每个架构的 sha256。安装把归档的顶层目录移到位,并向 xvm 注册程序(Windows 上为 `.cmd` 启动器);各系统的包 把程序放在不同目录时(Windows 的程序在包根,POSIX 的程序在 `bin/`),按系统注册各自的目录。每个归档各以一个 -`--package` 给出。单腿包按其三元组的系统(Linux、macOS 或 Windows)描述;其他目标的包被拒绝,并写出分发它的 -途径(2026.10.29.1+):`wasm32-emscripten` 的包发布为页面(`dist-web`),ok 映像以平台的通用包发布,Android、iOS +`--package` 给出。单腿包按其程序运行的系统与架构描述(2026.10.29.1+):Linux、macOS 或 Windows 目标为三元组的 +系统与架构;带载体打包的 ok 映像(`--no-universal --mode self-contained`)为载体的系统与架构,包把它记录在 `systems` +中(在 Linux 上打包的 aarch64 映像为 `"systems": ["linux-aarch64"]`)。程序不是任何系统的程序的包被拒绝,并写出 +分发它的途径:`wasm32-emscripten` 的包发布为页面(`dist-web`),不带载体的 ok 映像以平台的通用包发布,Android、iOS 与裸机的包没有条目。 ### 压缩与签名 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 1b0169dad..fea4072c4 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -651,7 +651,7 @@ mcpp pack [target] [--pkg-format ] [--target ...] --message-format js | `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+)* | 每条腿的规范三元组(通用包列出其平台的每个目标),以及自带加载器的通用包所携带的每个运行器的 `-` | +| `legs`、`systems` *(2026.10.27.1+)* | 每条腿的规范三元组(通用包列出其平台的每个目标),以及自带加载器的通用包所携带的每个运行器的 `-`,或单个 ok 映像的 self-contained 包中作为程序的载体的 `-`(2026.10.29.1+) | | `stages` *(2026.10.1.1+)* | 只在对多个成员的打包(`--workspace`,或重复的 `-p`)中出现:每个成员一条记录,按 `[workspace] members` 的顺序,带 `member`(限定的包名),以及与 `stage` 相同的 `dir`、`manifest`、`closure` | 对多个成员的打包,`artifacts` 按同样的成员顺序列出每个成员的产物,每条记录多出 diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index a94a87026..0f85bf267 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -1446,19 +1446,38 @@ int main(int argc, char** argv) { namespace { -// The xlings system a package of one leg installs on: the system its program -// runs on (SPEC-014 §8). An entry's `xpm.` installs a program of that -// system and its `archs` are machine architectures; a leg that is no program -// of Linux, macOS or Windows -- a WebAssembly module, an Android or iOS -// application, a bare-metal image, an ok image -- has neither. Every triple -// that was not Windows or macOS used to be written `linux` (a wasm32-emscripten -// package: `archs = {"wasm32"}` under `linux`). -std::optional xlings_system(std::string_view triple) { - auto t = mcpp::toolchain::triple::parse(triple); +// The xlings key of a system as a package names it (linux | macos | windows). +std::optional xlings_system_of(std::string_view os) { + if (os == "linux") return "linux"; + if (os == "macos") return "macosx"; + if (os == "windows") return "windows"; + return std::nullopt; +} + +// THE SYSTEM AND ARCHITECTURE A PACKAGE OF ONE LEG INSTALLS ON: those its +// program runs on (SPEC-014 §8). An entry's `xpm.` installs a program +// of that system and its `archs` are machine architectures. A leg of a Linux +// (not Android), macOS or Windows triple is a program of that system. An ok +// image packed with its carrier (`--mode self-contained`, `static`) is a +// program of the carrier's system and architecture, which the package records +// as the one loader it carries (`systems`). Every other leg -- a WebAssembly +// module, an Android or iOS application, a bare-metal image, an ok image +// without a carrier -- is no program of a system. Every triple that was not +// Windows or macOS used to be written `linux` (a wasm32-emscripten package: +// `archs = {"wasm32"}` under `linux`). +struct ProgramHost { std::string system, arch; }; +std::optional program_host(const Package& pkg) { + auto t = mcpp::toolchain::triple::parse(pkg.legs.front()); if (!t) return std::nullopt; - if (t->os == "windows") return "windows"; - if (t->os == "macos") return "macosx"; - if (t->os == "linux" && t->env != "android") return "linux"; + if (t->os != "linux" || t->env != "android") + if (auto system = xlings_system_of(t->os)) return ProgramHost{ *system, t->arch }; + if (t->os == "openkal" && pkg.systems.size() == 1) { + const auto& carrier = pkg.systems.front(); // `-` + const auto dash = carrier.find('-'); + if (dash != std::string::npos) + if (auto system = xlings_system_of(std::string_view(carrier).substr(0, dash))) + return ProgramHost{ *system, carrier.substr(dash + 1) }; + } return std::nullopt; } @@ -1469,7 +1488,8 @@ std::string distributed_instead(std::string_view triple) { return "a page for the web, mcpp pack --pkg-format dist-web (mcpp-plugins)"; if (t && t->os == "openkal") return "the platform's universal package (mcpp pack without --no-universal), which every " - "system installs"; + "system installs, or a package that carries the image in its carrier (--mode " + "self-contained)"; return "the package of a Linux, macOS or Windows target"; } @@ -1546,15 +1566,14 @@ std::expected xpkg_descriptor(const std::vector "packages of one leg", a.file.filename().string(), a.pkg.legs.size())); const auto& triple = a.pkg.legs.front(); - const auto system = xlings_system(triple); - if (!system) + const auto host = program_host(a.pkg); + if (!host) return std::unexpected(std::format( "{} is a package of {}, which is no program of Linux, macOS or Windows: an " "xlings entry installs a program of the system it runs on.\n use: {}", a.file.filename().string(), triple, distributed_instead(triple))); - const auto arch = triple.substr(0, triple.find('-')); - bySystem[*system][arch] = &a; - if (std::ranges::find(archs, arch) == archs.end()) archs.push_back(arch); + bySystem[host->system][host->arch] = &a; + if (std::ranges::find(archs, host->arch) == archs.end()) archs.push_back(host->arch); } for (auto const& [system, byArch] : bySystem) { // One URL with `${arch}`: the file names may differ by the @@ -1603,7 +1622,7 @@ std::expected xpkg_descriptor(const std::vector std::map dirOf; // system -> the program's directory for (auto const& a : assets) { const auto program = std::filesystem::path(a.pkg.program); - dirOf[*xlings_system(a.pkg.legs.front())] = // each leg has one: checked above + dirOf[program_host(a.pkg)->system] = // each package has one: checked above program.has_parent_path() ? program.parent_path().generic_string() : std::string("."); } const auto& one = dirOf.begin()->second; diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index bf79dc4f6..2823894a3 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -545,6 +545,14 @@ std::expected, Error> single_file(const Plan& // 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 `-` of the loaders a package carries (`systems` of its +// `mcpp-package.json` and of the `mcpp.pack` envelope, SPEC-014 §4.5): a +// self-contained universal package's under `libexec//`, and for a +// self-contained package of one image its carrier, which is the program -- of +// this machine's system and the image's architecture. `mcpp emit xpkg` reads +// it as the system the carrier runs on (§8). +std::vector carried_systems(const Plan& plan); + // THE PLATFORM'S RUNNER FOR ANOTHER ARCHITECTURE (SPEC-013 §10, §11): the // carrier built with nothing carried, for this machine's system and `arch`, // named `name` -- the program a self-contained universal package carries under @@ -1625,6 +1633,9 @@ std::expected finalize_tree(const Plan& plan) { pkg.program = "bin/" + fs::path(plan.binaryName).stem().string() + std::string(mcpplibs::os::exe_suffix); stated.role[pkg.program] = "program"; + // The carrier that is the program says which system it is a program + // of; an index entry reads it there (SPEC-014 §8). + pkg.systems = carried_systems(plan); } // What `[pack] include` named is the package's resource, whatever its name. for (auto const& rel : *included) stated.role[rel] = "resource"; @@ -3272,6 +3283,15 @@ std::string unbuilt_row(std::string_view system, std::string_view arch) { return detail::row_is_built(row) ? std::string() : row.triple; } +std::vector carried_systems(const Plan& plan) { + // The one-leg tree of a single file holds the images, not the carrier + // (`run_loaded`): the carrier is the file, made per system of `--systems`. + if (plan.opts.universalName.empty() && plan.targetIsLoaded && plan.opts.mode == Mode::BundleAll + && plan.opts.format != Format::SingleFile) + return { std::format("{}-{}", detail::host_system(), detail::arch_of(plan.triple)) }; + return mcpp::pack::distribution::loader_systems(plan.stagingRoot); +} + std::expected runner_program(const mcpp::platform::Resolved& platform, const std::filesystem::path& projectRoot, std::string_view name, std::string_view arch, bool build) { diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index da73763a5..c4c72432c 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -1113,7 +1113,7 @@ 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); + out.systems = mcpp::pack::carried_systems(*plan); if (m.stageFailure.empty()) { out.stageDir = plan->stagingRoot; out.stageManifest = mcpp::pack::stage_manifest_path(plan->stagingRoot); diff --git a/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh b/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh index 6cfa4d4c7..11fc086a4 100755 --- a/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh +++ b/tests/e2e/961_a_one_target_package_carries_the_image_in_a_program_of_its_architecture.sh @@ -14,6 +14,11 @@ # this machine has that architecture's qemu, it runs the image. # B. `mcpp pack --target -openkal --list-formats` lists `okb`, provided by # the platform; `mcpp pack --list-formats` for this machine does not. +# C. The package records its carrier as the loader it carries (`systems`: +# this system and the image's architecture), and `mcpp emit xpkg` +# describes it under this system for that architecture; a one-target +# package of the image without its carrier (`--mode vendored`) is no +# program of a system and is refused, naming the universal package. set -e TMP=$(mktemp -d) @@ -25,6 +30,11 @@ case "$(uname -m)" in aarch64|arm64) OTHER=x86_64 ;; *) echo "SKIP: no ok-image row for $(uname -m)"; exit 0 ;; esac +case "$(uname -s)" in + Darwin) SYSTEM=macos; XSYSTEM=macosx ;; + MINGW*|MSYS*|CYGWIN*) SYSTEM=windows; XSYSTEM=windows ;; + *) SYSTEM=linux; XSYSTEM=linux ;; +esac OT="$OTHER-openkal" mkdir -p "$TMP/app/src" @@ -72,4 +82,25 @@ assert "okb" not in host, sorted(host) PY echo "ok: --list-formats lists the target's formats" +# C +python3 - target/dist/m3/mcpp-package.json "$SYSTEM-$OTHER" <<'PY' || fail "C: the package's systems" target/dist/m3/mcpp-package.json +import json, sys +d = json.load(open(sys.argv[1])) +assert d["systems"] == [sys.argv[2]], d["systems"] +assert d["launch"] == {"program": "bin/app"}, d["launch"] +PY +tar czf m3.tar.gz -C target/dist m3 +"$MCPP" emit xpkg --package m3.tar.gz --url https://example.org/v0.1.0 > c.lua 2> c.err \ + || fail "C: emit xpkg of the carrier package" c.err +grep -q "^ $XSYSTEM = {" c.lua || fail "C: the entry is not under $XSYSTEM" c.lua +grep -qF "archs = {\"$OTHER\"}" c.lua || fail "C: the entry's architecture is not $OTHER" c.lua +[ "$(grep -cE '^ (linux|macosx|windows) = [{]' c.lua)" = 1 ] || fail "C: the entry names more than one system" c.lua +"$MCPP" pack --target "$OT" --no-universal -o bare.tar.gz > c2.log 2>&1 || fail "C: the image's vendored package" c2.log +rc=0 +"$MCPP" emit xpkg --package target/dist/bare.tar.gz --url https://example.org/v0.1.0 > c2.lua 2> c2.err || rc=$? +[ "$rc" = 2 ] || fail "C: emit xpkg of an image without its carrier exited $rc, not 2" c2.err c2.lua +grep -q "is a package of $OT, which is no program of Linux, macOS or Windows" c2.err || fail "C: the refusal" c2.err +grep -q "universal package" c2.err || fail "C: the refusal names no route" c2.err +echo "ok: the carrier package is described for its system and architecture; the bare image is refused" + echo "PASS: 961 a one-target package carries the image in a program of its architecture" diff --git a/tests/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp index b8c40ec98..b6d7cc0e3 100644 --- a/tests/unit/test_pack_distribution.cpp +++ b/tests/unit/test_pack_distribution.cpp @@ -729,10 +729,12 @@ TEST(PackDistribution, XpkgDescriptorNamesEachSystemsProgramDirectory) { } // An entry describes packages of one leg for the system their program runs on -// (SPEC-014 §8): a Linux, macOS or Windows program. A leg of no such system -- -// a WebAssembly module, an Android application, a bare-metal image, an ok image -// of one leg -- is refused, naming the package, the triple and the route that -// distributes it; it was written `linux` before. +// (SPEC-014 §8): a Linux, macOS or Windows program, or the carrier of an ok +// image, of the system and architecture the package records. A leg whose +// program is no program of a system -- a WebAssembly module, an Android +// application, a bare-metal image, an ok image without its carrier -- is +// refused, naming the package, the triple and the route that distributes it; +// it was written `linux` before. TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { auto one = [](std::string triple, std::string top, std::string program) { d::Asset a; @@ -766,4 +768,26 @@ TEST(PackDistribution, XpkgDescriptorDescribesProgramsOfASystem) { ASSERT_TRUE(r) << triple << ": " << r.error(); EXPECT_NE(r->find(std::format(" {} = {{", system)), std::string::npos) << *r; } + // A one-leg ok image packed with its carrier on Linux: a Linux program of + // the image's architecture, as `systems` records it. + auto carried = one("x86_64-openkal", "app-0.1.0-x86_64-openkal-bundle-all", "bin/app"); + carried.pkg.mode = "self-contained"; + carried.pkg.systems = { "linux-x86_64" }; + auto c = d::xpkg_descriptor({ carried }, "https://example.org/0.1.0"); + ASSERT_TRUE(c) << c.error(); + EXPECT_NE(c->find("archs = {\"x86_64\"}"), std::string::npos) << *c; + EXPECT_NE(c->find(" linux = {"), std::string::npos) << *c; + EXPECT_EQ(c->find("macosx = {"), std::string::npos) << *c; + EXPECT_NE(c->find("app-0.1.0-${arch}-openkal-bundle-all.tar.gz"), std::string::npos) << *c; + EXPECT_NE(c->find("path.join(pkginfo.install_dir(), \"bin\")"), std::string::npos) << *c; + // ...and on a macOS machine of the other architecture, a macOS program. + carried.pkg.systems = { "macos-aarch64" }; + carried.pkg.legs = { "aarch64-openkal" }; + carried.file = "app-0.1.0-aarch64-openkal-bundle-all.tar.gz"; + carried.top = "app-0.1.0-aarch64-openkal-bundle-all"; + auto m = d::xpkg_descriptor({ carried }, "https://example.org/0.1.0"); + ASSERT_TRUE(m) << m.error(); + EXPECT_NE(m->find("archs = {\"aarch64\"}"), std::string::npos) << *m; + EXPECT_NE(m->find(" macosx = {"), std::string::npos) << *m; + EXPECT_EQ(m->find("linux = {"), std::string::npos) << *m; }