diff --git a/.agents/docs/2026-11-02-ecosystem-part3-ok-image-performance-parity-design.md b/.agents/docs/2026-11-02-ecosystem-part3-ok-image-performance-parity-design.md index e8fb64253..43246540c 100644 --- a/.agents/docs/2026-11-02-ecosystem-part3-ok-image-performance-parity-design.md +++ b/.agents/docs/2026-11-02-ecosystem-part3-ok-image-performance-parity-design.md @@ -5,7 +5,7 @@ status: active # 生态设计 part 3:ok 映像的性能对等——引擎路径、openkal 运行时与观测 -日期:2026-11-02(rev 3,评审通过)。基线:mcppls-clangd #16(`6336bf1`,引擎 23.1.0-mcppls.8)、mcppls #75(0.0.20)、 +日期:2026-11-02(rev 4,实施记录)。基线:mcppls-clangd #16(`6336bf1`,引擎 23.1.0-mcppls.8)、mcppls #75(0.0.20)、 openkal-platform 0.20.2(openkal 0.19.0、openkal-linux 0.20.0、openkal-macos 0.17.1、openkal-windows 0.15.0、openkal-musl 0.25.0、 openkal-llvm-runtime 0.16.15、openkal-image 0.1.9)。前文:[part 2](2026-10-30-ecosystem-part2-ide-and-language-service-design.md) §25.3(B4–B8)、§25.9(H1–H8)、§25.10(openkal 的边界)。 @@ -24,6 +24,8 @@ openkal-llvm-runtime 0.16.15、openkal-image 0.1.9)。前文:[part 2](2026-1 在 openkal-musl。mcpp 增加依赖的补丁与嵌入(§14):打过补丁的库是一个新的库(变体),唯一性按程序而不按工作区判定;同一个 mcpp 版本修正 mcpp#875(外部工具链找不到 mcpp 安装的 Ninja)与 mcpp#881(`clean --stale` 的报告自相矛盾)及其附带的两处 不一致。第三轮自我评审 17 条(§15.3),待评审项见 §16.4;四项均采纳(§16.5),本文定稿,按 §12 实施。 +- rev 4:实施中的两项决定(§16.6)与修订(§17):冷阶段的差距来自装入时复制映像的全部段,openkal 0.21 增加从文件映射指令的 + 可选操作;L1 改为报告,门禁在 L2、L3。 ## 1. 问题与结论 @@ -435,7 +437,8 @@ C 库这一类包要用上游的源码而不拷贝它:在源码上加几处改 **嵌入(`embed = true`)。** 依赖的源文件(按它的构建描述与 feature 选出,再减去 `exclude`)以使用者的编译参数、include 目录与宏 (再加上依赖自己的 include 目录与宏)编入使用者,不另成一个库;依赖不为目标单独构建,因此不需要目标的 C 库——C 库可以嵌入 它所需的代码而不成环。嵌入者即为该变体在程序中的供给者:同一程序中其他边依赖同一变体时,只编译接口、不再链接实现;依赖 -另一变体时按上表报错。`exclude` 只在 `embed` 下允许(普通依赖去掉源文件会改变它的公开行为,那应当是一个补丁)。 +另一变体时按上表报错。`exclude` 只在 `embed` 下允许(普通依赖去掉源文件会改变它的公开行为,那应当是一个补丁)。依赖的链接 +参数与库不进入程序:定义由嵌入者提供,建立在嵌入者选定的操作系统层之上,链接什么由嵌入者陈述(rev 4,§17.2)。 **与已有机制的关系。** `provides` 的能力选择不变;变体是包身份的扩展,不是新的能力。xim 的安装钩子与描述中的 `generated_files` 在补丁之后执行。先例:vcpkg 的 portfile 补丁、Conan 的 `patches`、Nix 的 `patches`、Spack 的 `patch()`;Cargo 的 @@ -578,3 +581,75 @@ mcpp 在首次初始化时已把 Ninja 装进自己的 home(`ensure_ninja`,` 2. `embed` 的语义:采纳(§14.1)。 3. mcpp 的这一版本进入本轮、排在 openkal-musl 之前;musl 留在 openkal-musl:采纳(§12、§14)。 4. "新"的定义统一为 `[build] keep_days`:采纳(§14.3)。 + +### 16.6 实施中的待决项与决定(2026-11-02) + +1. warm-prime 的冷阶段(176 个模块,每个模块一个 worker)映像就绪为原生的 1.63 倍(7.82 s 对 4.80 s),每个 worker 多约 26 ms, + 与模块大小无关:okld 把映像的全部段复制进 `kal_exec_alloc` 的区域,每次启动写入并驻留 54 MB、约 14 000 次缺页。决定: + openkal 0.21 增加可选操作 `kal_exec_map_part`,实现提供,加载器从文件映射不可写的段(§17.1)。 +2. L1 在 Linux x64 上 69 行中 hosted 仍有 5 行、映像 16 行超过 1.15(§17.4),多为纳秒级的字符串函数;L2、L3 达标。决定:L1 在 + CI 中照常运行并报告比值,不作门禁;门禁在 L2、L3。 + +## 17. 实现中的修订 + +### 17.1 openkal 0.21 与三个实现 + +| 项 | 内容 | +|---|---| +| 规范 | `openkal.exec` 增加 `kal_exec_map_part(region, offset, size, file, at, how)`:把区域的一部分换成文件的字节,只读或只读且可执行(`KAL_EXEC_MAP_EXECUTE`),永不可写,随区域由 `kal_exec_free` 释放;文件偏移是 `KAL_MAP_ALIGNMENT` 的倍数,区域内的偏移与长度是 `kal_exec_granularity` 的倍数;失败时区域不变。属性位 `KAL_EXEC_PROP_FILE`(11 条 46、12.2、12.3)。结束的程序的 `cpu_ns` 在句柄释放前可读,与峰值内存相同。设计见 openkal 仓库 `.agents/docs/2026-11-02-openkal-0.21-design.md` | +| openkal-linux 0.22 | `mmap(MAP_FIXED)` 换入文件页;结束的程序的处理器时间取自 `waitid(WNOWAIT)` 的记录。另有三项实现修正:程序以 `CLONE_VM \| CLONE_VFORK` 在独立的 64 KiB 栈上启动,不复制启动者的地址空间,父进程在此期间屏蔽全部信号,子进程把已捕获的信号恢复为默认后再 `execve`;`kal_alloc` 按发出的块切分大块,对齐宽于页时直接映射;名字的校验与终止一次完成,卷属性缓存以序列锁无锁读取 | +| openkal-macos 0.19 | 文件映射在可执行内存可用时声明;处理器时间取自 `proc_pid_rusage`;`kal_alloc` 同 Linux。启动程序保持 `fork`:`vfork` 在 CI 的 macOS 上以 SIGSYS 结束子进程 | +| openkal-windows 0.17 | 回答 `kal_err_not_supported`,不声明该位:保留区中拆出的部分只有在整个保留以占位方式建立时才能换成文件视图,区域会成为需逐个释放的片段表(7.1)。加载器照旧复制 | +| openkal-image 0.3 | okld 从文件映射满足四个条件的段:不可写、文件大小等于内存大小、文件偏移与地址模 64 KiB 同余、独占所在粒度;其余段复制。每个重定位只写入可写段。okrun 在映像运行期间保持文件打开 | + +§5.2 的单元按 worker 建立:每个 worker 以 `kal_process_job_enter` 形成自己的单元,以 `kal_process_job_limit` 的内存限额取代 +`RLIMIT_AS`(openkal 不陈述地址空间上限);worker 组的预算仍按 `kal_process_props` 的回答选择。 + +### 17.2 openkal-musl 0.26 与 openkal-llvm-runtime 0.17 + +- **互斥锁。** 普通互斥锁按 glibc 的形式等待:`a_swap` 置为"已占用且有等待者"后 futex 等待,解锁时只在有等待者时唤醒。 +- **字符串。** AVX2 的 `strlen` 先单独检查第一个向量,再三个一组,之后每次 128 字节;`strcmp` 的逐向量比较以 `min(a, a == b)` + 合并两项检查,按左操作数对齐,在页尾逐字推进;`memcmp` 一次合并至多 256 字节的比较。 +- **stat。** 目录的 `fstat` 经 `okm_fs_info(".")` 回答目录的身份(设备号与节点号),引擎测定缓存目录时以它对照 `stat` 的结果 + (§17.3)。`fill_kstat` 先整体清零。描述符的上限等于 `OKM_MAX_FD`。 +- **页大小。** x86_64 不再定义常量 `PAGESIZE`:Windows 的分配粒度是 64 KiB,常量 4096 使"映射两页、释放第二页"失败;页大小 + 取自运行时。 +- **统计。** 计数器的名字表在共用文件中;hosted 的程序不引用 `openkal.prof` 的任何名字(Mach-O 不允许未定义符号),映像在 + 启动时注册。 +- **分配器的两个 fork 名字。** `__malloc_atfork` 与 `__bump_lockptr` 在 musl 中由 fork.c 以弱别名给出默认定义;本移植在 + Mach-O 与 PE 上不从占位定义生成名字,由提供分配器的源文件定义。mallocng 与 lite_malloc.c 定义了它们,mimalloc 的粘合层 + 原先没有,macOS 上链接 fork 的程序失败;现由粘合层按 musl 的默认定义给出。 +- **`thread_local` 的析构读到自己的变量。** PE 与 Mach-O 上变量是 compiler-rt emutls 的块,释放它们的 key 早于运行 + `thread_local` 析构的 key 创建,musl 按 key 的顺序调用析构,析构读的是已释放的内存:mallocng 不改已释放块的字节,mimalloc + 在其中写入空闲链表。openkal-llvm-runtime 0.17 的 emutls 在 musl 的最后一轮才释放块(ok 映像的 emutls 在 openkal-musl 中, + 同样处理)。 +- **裸机的分配器。** `os = "none"` 上 openkal-llvm-runtime 选 `malloc-mallocng`:mimalloc 按 arena 预留地址空间,在 OpenSBI 的 + 内存上返回 `ENOMEM`。目标依赖替换通用依赖,两条目标依赖各自列出 `posix`。 +- **嵌入的依赖的链接要求。** `compat.mimalloc` 为 Windows 声明 `psapi`、`user32`、`advapi32` 等库;嵌入 openkal-musl 并以 + `exclude` 去掉 `prim.c` 之后,这些库描述的是不再编译的操作系统层,Linux 上交叉构建 Windows 程序时链接失败。mcpp 2026.11.2.2 + 规定嵌入的依赖的链接参数与库属于嵌入者(§14.1 增补;docs/05)。 + +### 17.3 引擎 + +| 项 | 修订 | +|---|---| +| 能力的测定(§9.1、§9.2) | 不读 `kal_fs_props`,而在缓存目录经 C 库实测一次:创建、`flock`、按打开文件的锁、相对目录操作、打开时删除。映像中这些调用由 openkal-musl 落到 openkal,测到的就是 openkal 的回答;原生与映像共用一条代码路径。目录的身份以 `fstat(目录句柄)` 对照 `stat(路径)` | +| 根标记的发布 | 以不替换的改名(`renameat2(RENAME_NOREPLACE)`)发布,不建立硬链接:openkal 不为文件建立第二个名字 | +| 打开的缓存 | 同一个根的租约共用一个已打开的缓存(每租约一个缓存在 macOS 上超过 256 个打开文件,在映像上超过 64 个打开目录);进程可打开的文件数提高到系统上限 | +| 0040 | 自有缓存中的模块在其发布处、凭自身的租约读取,不再为每个读者复制 | +| 0041 | worker 结束即被看到:所有者监视以条件变量等待,worker 退出时通知;父进程查看运行中的 worker 前 50 次间隔 1 ms,此后 10 ms。冷阶段 176 个模块的就绪由 4.9 s 降为 3.4 s,worker 的中位时长由 20 ms 降为 9 ms | +| okgen | 工具链文件变化时删除 `CMakeCache.txt` 与 `CMakeFiles` 后重新配置 | + +### 17.4 验收的形态与结果 + +- **warm-prime 的工作区**(§9.4):由 ux-mcpp 的 `mcpp emit build-database` 生成,304 个单元、176 个模块(另有 std)。处理器时间 + 与缺页在 Linux 上取 `/proc//stat` 的 `utime`、`stime`、`cutime`、`cstime` 与对应的缺页字段,其他系统为引擎自身的时间 + 加 worker 日志中各 worker 的时间。门禁在 Linux 与 macOS(两个引擎都走自有缓存与 worker);Windows 原生走可移植路径,只记录。 +- **L3(Linux x64)**:映像对原生,冷阶段就绪 1.04、CPU 1.08、缺页 1.12;已有缓存的阶段 1.04、1.09、1.12。 +- **L2**:Linux x64 的 hosted 与映像全部 ≤ 1.15。CI 的映像对比交替运行两侧各五次、逐行取最小值:同一侧连续运行时,共享 + 机器的一段干扰只落在一侧(macOS arm64 的 options/command 一次为 1.22,重跑为 0.85)。 +- **L1(报告)**:Linux x64,对 glibc。hosted(GCC 编译)69 行中 5 行超过 1.15:线程私有数据 1.20、stat 1.16、clock 1.21(多一层 + openkal 调用,约 2 ns)、4 线程的 open-close 1.16、memcmp/64/7 1.19。映像(Clang 编译)16 行超过,集中在字符串函数 + (memchr/4096 1.46、memcpy/4096 1.34、strcmp/64 1.29),另有 clock、stat 与线程私有数据;同一份源码以 Clang 编成 hosted 时为 + 9 行,映像多出的部分大半来自编译器而不是映像。这些差距在 L2 的工作负载中不显现。 + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a1cc10c6..6f4cc7522 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -837,6 +837,15 @@ jobs: > "$AUDIT_ROOT/release.json" gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ --dir "$AUDIT_ROOT/assets" + # SHA256SUMS names every archive of the release, the four builds + # and the source: the Linux job wrote it before the others had + # uploaded theirs, so it named Linux x86_64 alone. + (cd "$AUDIT_ROOT/assets" && ls mcpp-*.tar.gz mcpp-*.zip 2>/dev/null | LC_ALL=C sort \ + | xargs sha256sum > "$AUDIT_ROOT/SHA256SUMS") + cat "$AUDIT_ROOT/SHA256SUMS" + if ! cmp -s "$AUDIT_ROOT/SHA256SUMS" "$AUDIT_ROOT/assets/SHA256SUMS"; then + gh release upload "$TAG" "$AUDIT_ROOT/SHA256SUMS" --clobber --repo "$GITHUB_REPOSITORY" + fi python3 tools/release/generate_manifest.py \ --release-json "$AUDIT_ROOT/release.json" \ --assets-dir "$AUDIT_ROOT/assets" \ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd5a11c9..ecffd20af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,46 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.2.2] - 2026-11-02 + +The built-in openkal platform is 0.21.0; an embedded dependency's link +requirements are its embedder's; a response file a link flag names is an input +of the link (design 2026-11-02, part 3 rev 4). + +### Changed + +- **The built-in `-openkal` platform is `mcpplibs.openkal-platform@0.21.0`** + (docs/35): openkal 0.21, whose loader maps an image's instructions from its + file (`kal_exec_map_part`); openkal-linux 0.22, openkal-macos 0.19, + openkal-windows 0.17; openkal-musl 0.26 with mimalloc; openkal-llvm-runtime + 0.17; openkal-image 0.3. The Windows launcher `mcpp pack` builds over it + depends on mcpplibs.os 0.2.7, the release written against openkal 0.21. +- **An embedded dependency's link flags and libraries are its embedder's** + (docs/05). The embedder supplies the definitions, over the operating-system + layer it chose, and states what they link against; the dependency's + `[build] ldflags` and `[runtime] libraries` described the library it builds + on its own. openkal-musl embeds `compat.mimalloc` without its `prim.c`, and a + Windows program built on Linux was linked against `psapi`, `user32` and + `advapi32`, which no sysroot of that build has. e2e 1002. + +### Fixed + +- **A response file a link flag names (`@`), and every file it names, + are inputs of the link.** A build program that lists the archives a + `prepare` action makes in one (`link_flag("@")`) left the link up to + date over rebuilt archives. The hermetic link check reads such a file in + place: with one on its command line the driver handed the linker a response + file of its own, which `-###` names and does not write, and the check refused + the link as taking the system's C library. e2e 1006. +- **The cache entry of a package whose units this build does not all need is + filled** (`bmi cache populate failed ... expected build output missing`). The + engine package's `mcpp.core` was compiled only where something imported it, + and a build plugin importing `mcpp` alone left its entry unfilled on every + build. A unit whose outputs fill an entry is built whether or not a link or + an importer of this build asks for it. e2e 910. +- **A release's `SHA256SUMS` names every archive**, the four builds and the + source; it named Linux x86_64 alone. + ## [2026.11.2.1] - 2026-11-02 A dependency's source is used without being copied: a dependency edge carries diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index de5c01984..23a16cf1d 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -1502,7 +1502,7 @@ the platform's own key names ([35 — Platforms](35-platforms.md)): ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.18.0" # or { path = "…" }, or "none" +platform = "openkal-platform@0.21.0" # or { path = "…" }, or "none" [target.'cfg(platform = "openkal")'.layers] c-abi = { path = "../openkal-musl" } diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md index b318d29ff..4c5a3cbe6 100644 --- a/docs/05-dependencies.md +++ b/docs/05-dependencies.md @@ -656,6 +656,9 @@ verified source. - The dependency builds nothing of its own in that configuration: every other edge to it takes its interface, and the embedder supplies its definitions. A program that reaches it without its embedder is refused, naming both. +- Its link flags and libraries are not the program's: the embedder supplies + the definitions, over the operating-system layer it chose, and states what + they link against. - `exclude` is allowed only with `embed`: a library built on its own that compiled fewer files would change for every consumer, which is a patch's to state. diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index b1bac4a50..7aa1d7bf7 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -952,7 +952,7 @@ and sha256. A provided format, `--merge` and `mcpp emit xpkg` read it ([SPEC-014](specs/packaging.md) §4.5): ```json -{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.18.0", +{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.21.0", "linkage": "shared", "mode": "vendored", "legs": ["x86_64-openkal", "aarch64-openkal"], "systems": [], "launch": {"runner": ["okrun", "{}"], "entry": {"x86_64": "lib/x86_64/hello.oke", "aarch64": "lib/aarch64/hello.oke"}}, "files": [{"path": "lib/x86_64/hello.oke", "role": "image", "leg": "x86_64-openkal", "sha256": "…"}]} diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index 04872c803..028ffda6b 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -111,7 +111,7 @@ practice. ```toml [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.18.0" +platform = "openkal-platform@0.21.0" ``` One table (2026.10.27.1+). It selects the openkal platform for the host triples, diff --git a/docs/35-platforms.md b/docs/35-platforms.md index a29981ea1..5407b39cb 100644 --- a/docs/35-platforms.md +++ b/docs/35-platforms.md @@ -113,9 +113,9 @@ The first of these that answers selects the target's platform: | Source | Spelling | |---|---| -| the command line | `--platform openkal-platform@0.18.0`, `--platform path:../openkal-platform`, `--platform none` | +| the command line | `--platform openkal-platform@0.21.0`, `--platform path:../openkal-platform`, `--platform none` | | the environment | `MCPP_TARGET_X86_64_OPENKAL_PLATFORM=…` | -| the project | `[target.] platform = "openkal-platform@0.18.0"`, or `{ path = "…" }` | +| the project | `[target.] platform = "openkal-platform@0.21.0"`, or `{ path = "…" }` | | this machine | the same key in `~/.mcpp/config.toml` | | the target table | the platform column (`mcpp toolchain list`) | @@ -129,7 +129,7 @@ its own conventions. `[host] platform` selects a platform for them instead: ```toml [host] -platform = "openkal-platform@0.18.0" +platform = "openkal-platform@0.21.0" ``` ``` @@ -151,7 +151,7 @@ implementation: ```toml [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.18.0" +platform = "openkal-platform@0.21.0" ``` On Windows the arrangement is the GNU ABI's row (`--target x86_64-windows-gnu`); diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index d137ae2a9..63f6ce2e1 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -527,7 +527,7 @@ installation, which is located on the machine rather than installed by mcpp. A target row is `{target, note, toolchain, pin, status, default}`, and `status` is one of `installed` / `available` / `via dependency graph` / `planned`. A target row also carries `platform` (2026.10.27.1+): the platform package the -target table selects for it (`mcpplibs.openkal-platform@0.18.0`), or `null`. +target table selects for it (`mcpplibs.openkal-platform@0.21.0`), or `null`. **`toolchain` and `pin` are not the same field twice.** `toolchain` is what the row is associated with — the installed payload on an installed row, the diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md index bbf72fc11..af9ed077c 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -237,7 +237,7 @@ openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` "schema": 1, "name": "hello", "version": "0.1.0", - "platform": "openkal-platform@0.18.0", + "platform": "openkal-platform@0.21.0", "linkage": "declared", "mode": "vendored", "legs": ["x86_64-openkal", "aarch64-openkal"], diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index 856c9e95b..1e85705f4 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -1384,7 +1384,7 @@ runner 与 `run_under`。 ```toml [target.'cfg(os = "openkal")'] -platform = "openkal-platform@0.18.0" # or { path = "…" }, or "none" +platform = "openkal-platform@0.21.0" # or { path = "…" }, or "none" [target.'cfg(platform = "openkal")'.layers] c-abi = { path = "../openkal-musl" } diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md index e474849b7..05d914ce1 100644 --- a/docs/zh/05-dependencies.md +++ b/docs/zh/05-dependencies.md @@ -566,6 +566,8 @@ updater = { path = "../updater", artifacts = ["updater"] } 使用使用者的编译参数,并且对这些文件另加依赖的 include 目录与编译参数。 - 在这一配置中,该依赖自己不编译任何东西:指向它的其他边只取它的接口,定义由嵌入者提供。一个程序 到达它而不含其嵌入者时被拒绝,消息点名两者。 +- 依赖的链接参数与库不进入程序:定义由嵌入者提供,建立在嵌入者选定的操作系统层之上,链接什么由 + 嵌入者陈述。 - `exclude` 只能与 `embed` 一起使用:一个单独构建的库若少编译一些文件,它对所有使用者都变了, 这应当由补丁陈述。 diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 5c0237586..3d562c391 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -799,7 +799,7 @@ Authenticode 可以签名它。Linux 与 macOS 经 shell 启动它:`sh pg-0.1. 提供的格式、`--merge` 与 `mcpp emit xpkg` 读取它([SPEC-014](../specs/packaging.md) §4.5): ```json -{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.18.0", +{"schema": 1, "name": "hello", "version": "0.1.0", "platform": "openkal-platform@0.21.0", "linkage": "shared", "mode": "vendored", "legs": ["x86_64-openkal", "aarch64-openkal"], "systems": [], "launch": {"runner": ["okrun", "{}"], "entry": {"x86_64": "lib/x86_64/hello.oke", "aarch64": "lib/aarch64/hello.oke"}}, "files": [{"path": "lib/x86_64/hello.oke", "role": "image", "leg": "x86_64-openkal", "sha256": "…"}]} diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 0f007ec0b..dfe8f33d2 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -93,7 +93,7 @@ SDK,或者区分 `linux`/`windows`/`macos`。那些是 C 环境层或平台层 ```toml [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.18.0" +platform = "openkal-platform@0.21.0" ``` 一张表(2026.10.27.1+)。它为宿主三元组选择 openkal 平台,平台的宿主部分命名编译器与目标侧三个层的包; diff --git a/docs/zh/35-platforms.md b/docs/zh/35-platforms.md index b8c0754f9..fa9a154f2 100644 --- a/docs/zh/35-platforms.md +++ b/docs/zh/35-platforms.md @@ -98,9 +98,9 @@ C++ 运行时的包同时提供编译器运行时,因此 `"c++-abi" = false` 只 | 来源 | 写法 | |---|---| -| 命令行 | `--platform openkal-platform@0.18.0`、`--platform path:../openkal-platform`、`--platform none` | +| 命令行 | `--platform openkal-platform@0.21.0`、`--platform path:../openkal-platform`、`--platform none` | | 环境变量 | `MCPP_TARGET_X86_64_OPENKAL_PLATFORM=…` | -| 工程 | `[target.] platform = "openkal-platform@0.18.0"`,或 `{ path = "…" }` | +| 工程 | `[target.] platform = "openkal-platform@0.21.0"`,或 `{ path = "…" }` | | 本机 | `~/.mcpp/config.toml` 中的同一个键 | | 目标表 | 平台列(`mcpp toolchain list`) | @@ -112,7 +112,7 @@ C++ 运行时的包同时提供编译器运行时,因此 `"c++-abi" = false` 只 ```toml [host] -platform = "openkal-platform@0.18.0" +platform = "openkal-platform@0.21.0" ``` ``` @@ -130,7 +130,7 @@ platform = "openkal-platform@0.18.0" ```toml [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.18.0" +platform = "openkal-platform@0.21.0" ``` Windows 上这一安排对应 GNU ABI 的行(`--target x86_64-windows-gnu`);`[platform.hosted] targets` 之外的 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 44cbecd04..fa1269f15 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -463,7 +463,7 @@ mcpp toolchain list --format json 的形状是 `{target, note, toolchain, pin, status, default}`,`status` 取 `installed` / `available` / `via dependency graph` / `planned` 之一。 目标行另有 `platform`(2026.10.27.1+):目标表为该目标选择的平台包 -(`mcpplibs.openkal-platform@0.18.0`),或 `null`。 +(`mcpplibs.openkal-platform@0.21.0`),或 `null`。 **`toolchain` 与 `pin` 不是把同一个字段写了两遍。** `toolchain` 表示这一行 关联到什么 —— 已装的行是装好的载荷,词表行是它自己的约定。`pin` 只承载目标 diff --git a/mcpp.toml b/mcpp.toml index dcbb7963f..01190acbb 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.2.1" +version = "2026.11.2.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index c0eb467b1..7c717900d 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -906,9 +906,9 @@ inline constexpr TargetInfo kKnownTargets[] = { // `openkal-platform` (SPEC-013), pinned here. The two architectures the // loader relocates. { "x86_64-openkal", "preview", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@0.18.0" }, + "mcpplibs.openkal-platform@0.21.0" }, { "aarch64-openkal", "preview", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@0.18.0" }, + "mcpplibs.openkal-platform@0.21.0" }, }; inline std::span known_targets() { return kKnownTargets; } diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 743076770..f248e7315 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.2.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.2.2"; } // namespace mcpp diff --git a/src/build/command_line.cpp b/src/build/command_line.cpp index 2ee1aca2b..a362df580 100644 --- a/src/build/command_line.cpp +++ b/src/build/command_line.cpp @@ -102,6 +102,47 @@ std::string quote_through_shell(std::string_view s) { } +namespace { + +std::vector split(std::string_view text, bool responseFile) { + std::vector words; + std::string cur; + bool any = false; + char quote = 0; + auto escapes = [&](char next) { + if (responseFile || quote == 0) return true; + return quote == '"' && (next == '$' || next == '`' || next == '"' || next == '\\' || next == '\n'); + }; + for (std::size_t i = 0; i < text.size(); ++i) { + const char c = text[i]; + if (c == '\\' && i + 1 < text.size() && escapes(text[i + 1])) { + // A shell removes a backslash and the newline after it. + const char next = text[++i]; + if (responseFile || next != '\n') { cur.push_back(next); any = true; } + } else if (quote) { + if (c == quote) quote = 0; + else cur.push_back(c); + } else if (c == '\'' || c == '"') { + quote = c; + any = true; + } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + if (any) words.push_back(std::move(cur)); + cur.clear(); + any = false; + } else { + cur.push_back(c); + any = true; + } + } + if (any) words.push_back(std::move(cur)); + return words; +} + +} // namespace + +std::vector shell_words(std::string_view text) { return split(text, false); } +std::vector response_file_words(std::string_view text) { return split(text, true); } + std::string display(const std::vector& argv) { std::string out; for (auto const& a : argv) { diff --git a/src/build/command_line.cppm b/src/build/command_line.cppm index 8a99f580d..90f5da43c 100644 --- a/src/build/command_line.cppm +++ b/src/build/command_line.cppm @@ -79,6 +79,17 @@ std::string quote_through_shell(std::string_view s); // for the host's shell, so the text can be pasted to run the same program. std::string display(const std::vector& argv); +// The words of a command as a POSIX shell reads them: separated by white +// space, a part in single quotes taken as it is, a backslash taking the next +// character outside quotes and, inside double quotes, before `$`, a backquote, +// `"`, `\` or a newline. +std::vector shell_words(std::string_view text); + +// The words of a response file (`@`) as GCC, Clang and their linkers +// read one: separated by white space, a part in single or double quotes taken +// as it is, and a backslash taking the next character, inside quotes or not. +std::vector response_file_words(std::string_view text); + // Silent redirect — stdout + stderr → /dev/null (or NUL on Windows), and the // error stream alone, for a command a shell reads. constexpr std::string_view silent_redirect = mcpplibs::os::is_windows ? ">nul 2>&1" : ">/dev/null 2>&1"; diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 73d0c97b2..9f3fa39c5 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -12,6 +12,7 @@ module; export module mcpp.build.flags; import std; +import mcpp.build.command_line; import mcpp.build.distribution; import mcpp.build.plan; import mcpp.build.runtime_placement; @@ -468,6 +469,21 @@ std::string normalize_ldflag(const std::filesystem::path& root, const std::strin } // namespace +// The absolute paths of existing files a response file names. +std::vector response_file_paths(const std::filesystem::path& rsp) { + std::vector out; + std::ifstream in(rsp, std::ios::binary); + if (!in) return out; + const std::string text { std::istreambuf_iterator(in), std::istreambuf_iterator() }; + for (auto const& word : mcpp::build::command_line::response_file_words(text)) { + std::error_code ec; + if (!word.starts_with('-') && std::filesystem::path(word).is_absolute() + && std::filesystem::is_regular_file(word, ec)) + out.emplace_back(word); + } + return out; +} + std::string atomic_link_flag(const std::vector& linkDirs, bool staticLink) { for (auto& dir : linkDirs) { @@ -1286,10 +1302,25 @@ CompileFlags compute_flags(const BuildPlan& plan) { // A word that names an existing file by absolute path is a link // input. A relative word is read by the linker against the build // directory, and an option is no file. - if (!word.starts_with('-') && std::filesystem::path(word).is_absolute()) { + if (!word.starts_with('-') && !word.starts_with('@') + && std::filesystem::path(word).is_absolute()) { std::error_code ec; if (std::filesystem::is_regular_file(word, ec)) f.ldFileInputs.emplace_back(word); } + // A RESPONSE FILE IS AN INPUT, AND SO IS EVERY FILE IT NAMES. A build + // program that lists the archives a `prepare` action makes in one + // (`link_flag("@")`) rewrites nothing when the action rebuilds + // them, so the link stayed up to date over new archives (clangd's ok + // image, 2026-11-02: a stale image after its libraries changed). + if (word.starts_with('@')) { + const std::filesystem::path rsp { word.substr(1) }; + std::error_code ec; + if (rsp.is_absolute() && std::filesystem::is_regular_file(rsp, ec)) { + f.ldFileInputs.push_back(rsp); + for (auto const& named : response_file_paths(rsp)) + f.ldFileInputs.push_back(named); + } + } } // C standard. The file-level `$cflags` carries the engine default, a diff --git a/src/build/hermetic.cppm b/src/build/hermetic.cppm index 20bddb846..e34d84110 100644 --- a/src/build/hermetic.cppm +++ b/src/build/hermetic.cppm @@ -113,6 +113,31 @@ std::vector tokenize(std::string_view out) { return toks; } +// THE WORDS OF EACH RESPONSE FILE THE FLAGS NAME, IN ITS PLACE. A response +// file on the driver's command line makes it hand the linker its arguments in +// a response file of its own, which `-###` names and does not write: the +// dynamic linker and the runtime objects were not seen, and a link that names +// its archives in one (`link_flag("@")`) was refused as taking the +// system's C library. +std::string expand_response_files(const std::string& flags) { + std::string out; + for (auto const& word : mcpp::build::command_line::shell_words(flags)) { + std::vector expanded { word }; + if (word.size() > 1 && word.front() == '@') { + std::ifstream in(word.substr(1), std::ios::binary); + if (in) { + const std::string text { std::istreambuf_iterator(in), std::istreambuf_iterator() }; + expanded = mcpp::build::command_line::response_file_words(text); + } + } + for (auto const& w : expanded) { + out += ' '; + out += mcpp::build::command_line::quote(w); + } + } + return out; +} + } // namespace std::expected verify_hermetic_link( @@ -165,7 +190,7 @@ std::expected verify_hermetic_link( auto cmd = std::format("{} {} -### -x c++ /dev/null -o /dev/null 2>&1", mcpp::build::command_line::quote(tc.binaryPath.string()), - ldflags); + expand_response_files(ldflags)); // The flags are the link's as ninja runs them, text a shell reads. auto r = mcpp::run::capture_shell(cmd); if (r.exit_code != 0) { diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 6b1942426..80d61974f 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -4036,6 +4036,13 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, } if (!actionOutputsByPackage.empty()) append("\n"); + // A unit whose outputs fill a cache entry is built whether or not a link + // or an importer here asks for it: the entry is its package's every + // artifact or nothing. + for (auto const& cu : plan.compileUnits) + if (cu.fillsCache && !cu.servedFromCache) + actionDefaults += " " + escape_ninja_path(cu.object); + if (!plan.linkUnits.empty() || !actionDefaults.empty()) { std::string defaults; for (auto& lu : plan.linkUnits) { diff --git a/src/build/plan.cppm b/src/build/plan.cppm index fd8c3930a..5b31d8881 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -110,6 +110,12 @@ struct CompileUnit { bool servedFromCache = false; std::filesystem::path cachedObject; // absolute, inside the cache std::filesystem::path cachedBmi; // absolute; empty if no module + // This unit's outputs fill a global-cache entry after the build: the + // entry holds its package's every BMI and object, so the backend builds + // the unit even where nothing in this build imports or links it (the + // engine package's `mcpp.core' in a build plugin's configuration, where + // the plugin imports `mcpp' alone). + bool fillsCache = false; // mcpp#344: this object's address INSIDE a global-cache entry — relative to // `/obj/`, and a pure function of the owning package (its source's // path relative to its own package root). Distinct from `object`, which is diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index faae728d6..5cb51e0e6 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -246,14 +246,8 @@ static void step6_instances(PrepareState& state) { target[d] ? "target" : "", host[d] ? (target[d] ? " + build-program" : "build-program") : "")); } - for (auto const& [key, rec] : state.resolved) { - const auto p = rec.depIndex + 1; - if (p >= n || !state.packages[p].buildTimeOnly) continue; - auto& ldflags = state.m->buildConfig.ldflags; - for (auto const& flag : rec.linkFlagsAdded) - if (auto pos = std::ranges::find(ldflags, flag); pos != ldflags.end()) - ldflags.erase(pos); - } + for (std::size_t d = 1; d < n; ++d) + if (state.packages[d].buildTimeOnly) withdraw_link_flags(state, d); } static void step6_check_version_floors_closure(PrepareState& state) { diff --git a/src/build/prepare/graph.cpp b/src/build/prepare/graph.cpp index 327e438c0..b50a52012 100644 --- a/src/build/prepare/graph.cpp +++ b/src/build/prepare/graph.cpp @@ -251,6 +251,11 @@ static void removeLinkFlags(PrepareState& state, const std::vector& } } +void withdraw_link_flags(PrepareState& state, std::size_t p) { + for (auto const& [key, rec] : state.resolved) + if (rec.depIndex + 1 == p) removeLinkFlags(state, rec.linkFlagsAdded); +} + static std::expected, std::string> package_source_files( const std::filesystem::path& srcRoot, const mcpp::manifest::Manifest& depManifest) diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index ef77dd1be..f2833dc85 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -1978,6 +1978,7 @@ static std::expected step13_dependency_cache(PrepareState& st arts.bmiFiles.size() + arts.objFiles.size(), probe.layoutMismatch.front())); } + for (auto u : unitIdx) ctx.plan.compileUnits[u].fillsCache = true; ctx.depsToPopulate.push_back({ std::move(key), std::move(arts) }); } } diff --git a/src/build/prepare/scan.cpp b/src/build/prepare/scan.cpp index a6bb59a64..6796cf397 100644 --- a/src/build/prepare/scan.cpp +++ b/src/build/prepare/scan.cpp @@ -119,6 +119,19 @@ static std::expected step11_scan_sources(PrepareState& state) dep.manifest.modules.sources.clear(); dep.manifest.modules.declaredUnits.clear(); dep.manifest.buildConfig.sourcesDeclared = true; + // ITS LINK REQUIREMENTS ARE THE EMBEDDER'S. The dependency's link + // flags and libraries describe the library it builds on its own; + // embedded, its definitions are the embedder's, over the + // operating-system layer the embedder chose (a C library excludes the + // dependency's and supplies its own), and what they link against is + // what the embedder states. + withdraw_link_flags(state, d); + dep.manifest.buildConfig.ldflags.clear(); + dep.linkUsage.ldflags.clear(); + auto& intent = dep.manifest.runtimeConfig.linkIntent; + intent.libraries.clear(); + intent.frameworks.clear(); + intent.linkLibraryDirs.clear(); } // Modgraph: regex scanner by default; opt-in to compiler-driven P1689 diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 12627dd76..e9cbacd11 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -946,6 +946,9 @@ void recordDependencyEdge(PrepareState& state, std::size_t consumerDepIndex, const mcpp::manifest::DependencySpec& spec, bool buildTable, const std::string& writtenKey); std::expected phase4b_graph_worklist(PrepareState& state); +// graph.cpp: takes the link flags package `p` added to the root's pooled list +// back out of it (a package that serves the build alone; an embedded one). +void withdraw_link_flags(PrepareState& state, std::size_t p); // The lookups the later phases ask of the graph (graph_closures.cpp). void step4b_define_lookup_closures(PrepareState& state); // Where the build program of package `i` keeps its files: its cache, its diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index 871557396..12b421016 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -1074,10 +1074,10 @@ std::string exe_launcher_manifest(std::string_view name, std::string_view versio "# Generated by `mcpp pack`: the Windows launcher of {0} (design 2026-10-29 §4.3).\n" "[package]\nname = \"{0}\"\nversion = \"{1}\"\n\n" "[targets.{0}]\nkind = \"bin\"\nmain = \"src/main.cpp\"\n\n" - "# 0.2.1: the release whose Windows half builds over a platform's hosted\n" - "# arrangement, which the launcher is built over; its openkal is the one\n" - "# the platform of mcpp's openkal rows names, the platform it is built over.\n" - "[dependencies.mcpplibs]\nos = \"0.2.1\"\n", + "# 0.2.7: the release written against openkal 0.21, the openkal the\n" + "# platform of mcpp's openkal rows names (openkal-platform 0.21.0), which\n" + "# the launcher is built over.\n" + "[dependencies.mcpplibs]\nos = \"0.2.7\"\n", name, version.empty() ? std::string_view("0.0.0") : version); } diff --git a/tests/e2e/1002_a_dependency_is_embedded.sh b/tests/e2e/1002_a_dependency_is_embedded.sh index 6d2bc33ee..ee3967875 100755 --- a/tests/e2e/1002_a_dependency_is_embedded.sh +++ b/tests/e2e/1002_a_dependency_is_embedded.sh @@ -17,6 +17,8 @@ # E. An embedder with a build program builds without a warning about the # flags `embed' gives the dependency's files: the build program's own plan # compiles none of them, and the entry is the engine's, not a typo. +# F. The embedded package's link flags and libraries are not the program's: +# a dependency naming libraries no system has links when embedded. set -e TMP=$(mktemp -d) @@ -106,4 +108,13 @@ if "$MCPP" build --workspace > d.log 2>&1; then fail "a program without the embe grep -q "which 'zip' embeds" d.log || fail "the refusal does not name the embedder" d.log cd .. +# F +mkdir -p lk/src lkapp/src && cd lkapp +printf '[package]\nname = "lk"\nversion = "0.1.0"\n\n[build]\nldflags = ["-lmcpp-no-such-library"]\n\n[runtime]\nlibraries = ["mcpp-no-such-runtime-library"]\n' > ../lk/mcpp.toml +printf 'int lk_answer(void) { return 42; }\n' > ../lk/src/lk.c +printf '[package]\nname = "lkapp"\nversion = "0.1.0"\n\n[dependencies]\nlk = { path = "../lk", embed = true }\n' > mcpp.toml +printf 'extern "C" int lk_answer();\nint main() { return lk_answer() == 42 ? 0 : 1; }\n' > src/main.cpp +"$MCPP" run > f.log 2>&1 || fail "the embedded package's link flags or libraries reached the program's link" f.log +cd .. + echo "OK" diff --git a/tests/e2e/1006_a_response_file_and_what_it_names_are_link_inputs.sh b/tests/e2e/1006_a_response_file_and_what_it_names_are_link_inputs.sh new file mode 100755 index 000000000..a1a05a3bf --- /dev/null +++ b/tests/e2e/1006_a_response_file_and_what_it_names_are_link_inputs.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell +# 1006 -- a response file a link flag names (`@`), and every file it +# names, are inputs of the link. +# +# clangd's ok image lists the archives a `prepare` action makes in one +# response file, named by the build program (`link_flag("@")`). When the +# action rebuilt an archive the response file stayed as it was, and the link, +# which depended on neither, stayed up to date over the new archive. +# +# Criteria: the program runs with the archive's first contents; after the +# archive the response file names is replaced (the response file unchanged), +# `mcpp build` relinks and the program runs with the new contents; a further +# build with nothing changed links nothing; a response file that names the +# archive in quotes is read the same way. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +mkdir -p "$work/val/src" "$work/prebuilt" +cat > "$work/val/mcpp.toml" <<'TOML' +[package] +name = "val" +version = "0.1.0" + +[targets.val] +kind = "lib" +TOML +make_archive() { # make_archive + printf 'extern "C" int val() { return %s; }\n' "$1" > "$work/val/src/val.cpp" + (cd "$work/val" && "$MCPP" build > "$work/val.log" 2>&1) || { cat "$work/val.log"; echo "FAIL: the library did not build"; exit 1; } + local a + a="$(find "$work/val/target" -name 'libval*.a' -type f | head -1)" + [[ -n "$a" ]] || { find "$work/val/target" -type f | head -40; echo "FAIL: no archive of the library"; exit 1; } + cp "$a" "$work/prebuilt/libval.a" +} +make_archive 41 +printf '"%s"\n' "$work/prebuilt/libval.a" > "$work/prebuilt/libs.rsp" + +mkdir -p "$work/app/src" +cat > "$work/app/mcpp.toml" < "$work/app/src/main.cpp" <<'CPP' +import std; +extern "C" int val(); +int main() { std::println("val {}", val()); } +CPP +cd "$work/app" + +"$MCPP" run > run1.log 2>&1 || { cat run1.log; echo "FAIL: the first run"; exit 1; } +grep -q 'val 41' run1.log || { cat run1.log; echo "FAIL: the first run did not print val 41"; exit 1; } + +make_archive 42 +"$MCPP" run > run2.log 2>&1 || { cat run2.log; echo "FAIL: the second run"; exit 1; } +grep -q 'val 42' run2.log || { + cat run2.log + echo "FAIL: a replaced archive a response file names did not relink the program" + exit 1 +} + +"$MCPP" build -v > build3.log 2>&1 || { cat build3.log; echo "FAIL: the third build"; exit 1; } +if grep -qE '^\[[0-9]+/[0-9]+\] .*bin/app' build3.log; then + cat build3.log + echo "FAIL: a build with nothing changed linked the program again" + exit 1 +fi + +echo "OK" diff --git a/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh b/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh index 7fbd6170d..b8b3371ea 100755 --- a/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh +++ b/tests/e2e/910_a_build_plugin_is_a_package_of_the_build_program.sh @@ -6,7 +6,9 @@ # A a plugin built on another plugin and on an ordinary library, each named # in its [dependencies], serves a consumer's build program (S1, S2); # B P3: the plugin builds as the root of a build, in the build-program -# configuration, and its tests run; +# configuration, and its tests run; the engine package's cache entry is +# filled there, its `mcpp.core` interface built though the plugin imports +# `mcpp` alone; # C P3: `--target` for another machine is refused, naming the plugin; # D P1: a plugin reached through [dependencies] of a target is refused, # naming the chain and [build-dependencies]; @@ -69,6 +71,9 @@ printf 'import mcpp;\nimport acme.rule;\nint main() { return acme::rule::value() cd "$TMP/rule" "$MCPP" build > b1.log 2>&1 || fail "B: the plugin did not build as the root of a build" b1.log grep -q "build-program" b1.log || fail "B: the build does not state its configuration" b1.log +! grep -q "populate failed" b1.log || fail "B: the engine package's cache entry was not filled" b1.log +ls "$MCPP_HOME"/build-cache/v1/pkg/_engine/*/*/bmi/mcpp.core.* > /dev/null 2>&1 \ + || fail "B: the engine package's entry lacks mcpp.core" b1.log "$MCPP" test > b2.log 2>&1 || fail "B: the plugin's tests did not run" b2.log grep -q "1 passed" b2.log || fail "B: the test did not pass" b2.log diff --git a/tests/unit/test_command_words.cpp b/tests/unit/test_command_words.cpp new file mode 100644 index 000000000..da91a5cb9 --- /dev/null +++ b/tests/unit/test_command_words.cpp @@ -0,0 +1,33 @@ +#include + +import std; +import mcpp.build.command_line; + +// The two readers of a command's words: a POSIX shell's, for the link flags as +// ninja runs them, and a response file's, as GCC, Clang and their linkers read +// one (`@`). + +using words = std::vector; +using mcpp::build::command_line::response_file_words; +using mcpp::build::command_line::shell_words; + +TEST(CommandWords, AShellReadsQuotesAndBackslashes) { + EXPECT_EQ(shell_words(" -o 'a b' \"c d\" e\\ f "), (words{"-o", "a b", "c d", "e f"})); + EXPECT_EQ(shell_words("'a\\b'"), (words{"a\\b"})); + EXPECT_EQ(shell_words("\"a\\b\\\"c\""), (words{"a\\b\"c"})); + EXPECT_EQ(shell_words("'it'\\''s'"), (words{"it's"})); + EXPECT_EQ(shell_words("a\\\nb"), (words{"ab"})); + EXPECT_EQ(shell_words("''"), (words{""})); + EXPECT_EQ(shell_words(" \t\n"), (words{})); +} + +TEST(CommandWords, AResponseFileTakesTheCharacterAfterEveryBackslash) { + EXPECT_EQ(response_file_words("/a/lib.a\n\"/b c/lib.a\"\r\n'x\\'y'"), + (words{"/a/lib.a", "/b c/lib.a", "x'y"})); + EXPECT_EQ(response_file_words("a\\ b \"c\\\\d\""), (words{"a b", "c\\d"})); +} + +TEST(CommandWords, AWordQuotedForAShellReadsBackAsItself) { + for (std::string w : {"plain", "a b", "it's", "a\\b", "$x", "\"q\"", ""}) + EXPECT_EQ(shell_words(mcpp::build::command_line::quote_posix(w)), words{w}) << w; +} diff --git a/tests/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp index 819f81c41..4e309d947 100644 --- a/tests/unit/test_pack_distribution.cpp +++ b/tests/unit/test_pack_distribution.cpp @@ -261,7 +261,7 @@ TEST(PackDistribution, TheWindowsLauncherIsAProgram) { EXPECT_NE(src.find("(\"windows-\" + arch)"), std::string::npos) << src; EXPECT_NE(src.find("os::process::exec"), std::string::npos) << src; const auto manifest = d::exe_launcher_manifest("hello", "1.0.0"); - EXPECT_NE(manifest.find("os = \"0.2.1\""), std::string::npos) << manifest; + EXPECT_NE(manifest.find("os = \"0.2.7\""), std::string::npos) << manifest; EXPECT_NE(manifest.find("[targets.hello]"), std::string::npos) << manifest; }