diff --git a/.github/tools/bump_index.sh b/.github/tools/bump_index.sh index 77d2bef56..b51dc4e3e 100755 --- a/.github/tools/bump_index.sh +++ b/.github/tools/bump_index.sh @@ -5,8 +5,10 @@ # updater does it for every package (`xpkgindex-update.yml`, pinned # openxlings/xpkgindex-update): it discovers the release from the recipe's # `extensions.autoupdate.source` (xlings-res/ releases), requires the -# GLOBAL and CN resources, computes each sha256 and opens -# `auto/bump-xim--`. The script that used to do this here, +# GLOBAL and CN resources, computes each sha256 and opens a PR whose body +# starts with `` +# (its branch, `auto/bump-xim-`, does not name the version, so the +# marker is what identifies the proposal). The script that used to do this here, # `.github/scripts/version-check.py`, was removed from the index by that # change; 2026.10.24.1 is the release that found out (publish-ecosystem # failed on the missing file). Asking the index's own updater keeps one writer @@ -17,27 +19,38 @@ # # Usage: bump_index.sh # e.g. mcpp 2026.10.24.1 # Auth: -# PKGINDEX_TOKEN (required) -- may dispatch workflows of openxlings/xim-pkgindex +# PKGINDEX_TOKEN (required) -- a token with Actions: write on +# openxlings/xim-pkgindex (a workflow dispatch needs that permission; +# Contents and Pull requests do not grant it) set -euo pipefail PROJ="${1:?usage: bump_index.sh }" VER="${2:?usage: bump_index.sh }" INDEX_REPO="${INDEX_REPO:-openxlings/xim-pkgindex}" -BRANCH="auto/bump-xim-${PROJ}-${VER}" +export MARKER="" WAIT_S="${BUMP_WAIT_S:-1200}" export GH_TOKEN="${PKGINDEX_TOKEN:?PKGINDEX_TOKEN required (dispatch on $INDEX_REPO)}" info() { echo "[bump] $*"; } -pr_url() { gh pr view "$BRANCH" -R "$INDEX_REPO" --json url --jq .url 2>/dev/null || true; } +# The updater's PR for this version, open or merged. +pr_url() { + gh pr list -R "$INDEX_REPO" --state all --limit 50 --json url,body,state \ + --jq '[.[] | select(.state != "CLOSED" and (.body | startswith(env.MARKER)))][0].url // empty' \ + 2>/dev/null || true +} if url="$(pr_url)" && [[ -n "$url" ]]; then - info "PR already open: $url" + info "PR already proposed: $url" exit 0 fi info "dispatching xpkgindex-update.yml for xim:$PROJ" -gh workflow run xpkgindex-update.yml -R "$INDEX_REPO" --ref main -f "package=xim:$PROJ" +if ! gh workflow run xpkgindex-update.yml -R "$INDEX_REPO" --ref main -f "package=xim:$PROJ"; then + echo "[bump] PKGINDEX_TOKEN cannot dispatch on $INDEX_REPO (it needs Actions: write);" \ + "a maintainer runs: gh workflow run xpkgindex-update.yml -R $INDEX_REPO --ref main -f package=xim:$PROJ" >&2 + exit 1 +fi deadline=$(( $(date +%s) + WAIT_S )) while (( $(date +%s) < deadline )); do @@ -50,6 +63,6 @@ done # BLOCKING: without the PR the index never learns the release exists. The # updater's run says why (a resource missing on one host, a policy refusal). -echo "[bump] no $BRANCH after ${WAIT_S}s; see" \ +echo "[bump] no PR for xim:$PROJ $VER after ${WAIT_S}s; see" \ "https://github.com/$INDEX_REPO/actions/workflows/xpkgindex-update.yml" >&2 exit 1 diff --git a/.github/tools/upgrade_lane.sh b/.github/tools/upgrade_lane.sh index e2a0bb811..b079d65d0 100644 --- a/.github/tools/upgrade_lane.sh +++ b/.github/tools/upgrade_lane.sh @@ -17,6 +17,13 @@ # revisions (llvm revision 0, gcc 16.1.0 before -r1). # orphan every payload recorded at another revision, and `c++` active at # gcc's version whose registration is gone (a reported home). +# modules a module dependency the previous release compiled is served from +# its cache when the spelling of its BMI has not moved. +# programs the previous release builds an ok image, whose platform's build +# program imports mcpp.plugins over the engine interface (`mcpp`); +# this commit's mcpp builds it again over that cache, and no +# library compiled against another interface is served ("CRC +# mismatch"). # # Asserted, every variant: the first `mcpp new && mcpp run` of the new mcpp # succeeds and downloads nothing; a second run likewise; `mcpp self doctor` @@ -158,6 +165,12 @@ if [ "$VARIANT" = modules ]; then fi new_project p-old "$OLD" run old-run "$W/p-old" "$OLD" run || fail "the previous release cannot run a new project" +case "$PLAT" in linux-x86_64) OK_TARGET=x86_64-openkal ;; *) OK_TARGET=aarch64-openkal ;; esac +if [ "$VARIANT" = programs ]; then + new_project k-old "$OLD" + run old-programs "$W/k-old" "$OLD" build --target "$OK_TARGET" \ + || fail "the previous release cannot build an ok image" +fi if [ "$VARIANT" = orphan ]; then "$PY" - "$REG" <<'PY' || fail "could not shape the orphan home" @@ -238,6 +251,12 @@ if [ "$VARIANT" = modules ]; then grep -q "STAGE gcm.cache/mcpplibs.openkal/" "$W/new-modules2.log" \ || fail "the rebuilt dependency was not served from the cache" fi +if [ "$VARIANT" = programs ]; then + run new-programs "$W/k-old" "$NEW" build --target "$OK_TARGET" \ + || fail "the ok image the previous release built does not build over its cache" + grep -q "CRC mismatch" "$W/new-programs.log" \ + && fail "a build program's library compiled against the previous engine interface was served" +fi if [ "$VARIANT" = orphan ]; then run new-provision "$W" "$NEW" toolchain install "$TC" || fail "toolchain install could not replace the payload" grep -q "installed asset registration missing" "$W/new-provision.log" \ diff --git a/.github/workflows/ci-upgrade.yml b/.github/workflows/ci-upgrade.yml index 3aa0b0a78..ea3da0222 100644 --- a/.github/workflows/ci-upgrade.yml +++ b/.github/workflows/ci-upgrade.yml @@ -30,6 +30,7 @@ jobs: - { host: linux-x86_64, runs-on: ubuntu-24.04, variant: snapshot } - { host: linux-x86_64, runs-on: ubuntu-24.04, variant: orphan } - { host: linux-x86_64, runs-on: ubuntu-24.04, variant: modules } + - { host: linux-x86_64, runs-on: ubuntu-24.04, variant: programs } - { host: linux-aarch64, runs-on: ubuntu-24.04-arm, variant: plain } - { host: macos-arm64, runs-on: macos-15, variant: plain } - { host: windows-x86_64, runs-on: windows-latest, variant: plain } diff --git a/.gitignore b/.gitignore index 020f29095..8c6cac6e6 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,7 @@ examples/*/target/ # Local review reports (not part of the design record). /.agents/reviews/ + +# The logs .github/tools/upgrade_lane.sh copies beside the workspace (CI uploads +# them as an artifact; a local run leaves them here). +/upgrade-lane-logs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9d2df60..d3050459a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,73 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.10.28.1] - 2026-10-28 + +### Fixed + +- **A package's cache key covers every package whose module it imports.** + A BMI is bound to the BMIs it read, and the key folded in only the + dependencies a manifest states. A package can import a module its manifest + does not name: the libraries of a build program import the engine + interface (`mcpp`), which the engine adds to the build program's graph. + Such a package was served an entry compiled against another BMI of that + module and failed with "CRC mismatch" (measured: `mcpp.plugins` in a build + of an ok image, over a cache earlier builds had filled). The provider of + each imported module is now an upstream of the importer's key, a declared + edge or not; a module this build generates for a dependency below its + consumer's root is not one (e2e 849). e2e 959 serves a package an + interface it does not declare at two versions; the upgrade lane gains a + `programs` variant that builds an ok image over the previous release's + cache. +- **A build ended by SIGTERM exits 143 however the race falls.** The watcher + that takes a request to end over ends the programs mcpp runs, writes the + cancelled record, runs the cleanups and then ends mcpp as asked; the main + thread, woken by its compiler's end, could return from `main` first with + the build's own failure (exit 1, seen once on CI in e2e 936 H). The end of + `main` now ends mcpp as the request asks, after the watcher has finished. +- **`mcpp emit xpkg --package` names each system's program directory.** The + packages of one program for several systems may place it apart: a Windows + package holds the program at its root, a POSIX one under `bin/` beside the + root's convenience script. The entry registered the first package's + directory for every system, so on Windows xvm pointed at a `bin/` the + package does not have. It now writes `bindir[os.host()]` from a table of + each system's directory when they differ (SPEC-014 §8). +- **`mcpp emit xpkg --package` states the runner dependency with its + namespace** (`deps = { "xim:okrun" }`). A bare name turns ambiguous once a + second index the home lists has a package of that name, and xim-pkgindex's + `check-dep-namespace` refuses it for a published package. +- **The install of an `emit xpkg --package` entry finds the package.** It + looked the archive's directory up with `os.dirs("-0.1.3-*")`, and + libxpkg's `os.dirs` lists `/*` without matching names: no xlings + install of such an entry succeeded. The directory is now the downloaded + archive's name without its extension, which is what `mcpp pack` names it + after, so it holds for every system, architecture and later version; an + archive renamed since it was packed, or a file xim does not unpack, is + refused when the entry is written. `mcpp pack --merge -o ` names + the merged package's directory after its output as `mcpp pack -o` does, + where it kept the first input's, so a merged package is described as + every other is. +- **`mcpp emit xpkg` refuses a word that is not an option.** Each archive is + its own `--package`; `--package a.zip b.zip` (a shell glob's expansion) + described `a.zip` alone and left `b.zip` out without a word. +- **The release proposes its index bump again.** `bump_index.sh` waited for + `auto/bump-xim-mcpp-`, and the index's updater opens + `auto/bump-xim-mcpp`; the proposal is now found by the marker in its body. + A dispatch the token is refused for (it needs Actions: write on + openxlings/xim-pkgindex) names the permission and the command a maintainer + runs. + +### Changed + +- **`upgrade-lane-logs/` is no longer in the tree.** A local run's logs were + committed with 2026.10.17.1; the directory is ignored, as the lane writes + it beside the workspace for CI to upload. +- **SPEC-014 1.2** states what 2026.10.27.1 already does: a package's name + carries its mode's suffix, a single file's launcher exits 127 on its own + failure, `--merge` takes its container from `[pack] format` before `-o`, and + `--linkage static` moves this machine's row and each `-linux-gnu` `--target` + to musl while a project's own `[build] target` is packed as it is. + ## [2026.10.27.1] - 2026-10-27 ### Added diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 99deef270..2b1e50446 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -881,10 +881,13 @@ package. writes the xim-pkgindex entry (spec 2) of packages this command wrote, from their `mcpp-package.json`. A universal package is one asset that `linux`, `macosx` and `windows` install alike, with the runner as a dependency -(`deps = { "okrun" }`) unless the package carries it; packages of one system +(`deps = { "xim:okrun" }`) unless the package carries it; packages of one system and architecture each make that system's entry, one URL with `${arch}` and a sha256 per architecture. The install moves the archive's top directory into -place and registers the program with xvm (the `.cmd` launcher on Windows). +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`. ### Compression and signing diff --git a/docs/90-build-from-source.md b/docs/90-build-from-source.md index 4892ab520..b49c167d0 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/ 645 shell scripts against a real binary +└── e2e/ 646 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`, 645 files) run a real `mcpp` +**End-to-end tests** (`tests/e2e/NN_.sh`, 646 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 a5f49f390..221143bea 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -43,7 +43,7 @@ | [SPEC-011](concurrency.md) | 并发运行:三段、构建目录锁、原子发布、`mcpp.lock` 的乐观写入、取消 | 评审中 v1.0 | 2026-10-26 | mcpp >= 2026.10.26.1 | | [SPEC-012](human-output.md) | 面向人的输出:两条流、两种介质、行的种类、颜色 | 评审中 v1.0 | 2026-10-26 | mcpp >= 2026.10.26.1 | | [SPEC-013](platforms.md) | 平台:`[platform]` 的七组键、版本组与 `hosted`,三种写法与层次,目标图与宿主图的选择,`cfg(platform)`,依赖图的补全,链接、命名与封装,运行器与载体,平台阶段与协议 17 | 评审中 v1.0 | 2026-10-28 | mcpp >= 2026.10.27.1 | -| [SPEC-014](packaging.md) | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与 `mcpp-package.json`,通用包、单文件、签名、压缩、合并与分发描述 | 评审中 v1.0 | 2026-10-28 | mcpp >= 2026.10.27.1;逐条标注 | +| [SPEC-014](packaging.md) | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与 `mcpp-package.json`,通用包、单文件、签名、压缩、合并与分发描述 | 评审中 v1.2 | 2026-10-28 | mcpp >= 2026.10.28.1;逐条标注 | ## 文档约定 diff --git a/docs/specs/packaging.md b/docs/specs/packaging.md index d016911e8..49c5fa6d7 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -4,10 +4,10 @@ |---|---| | 规范编号 | SPEC-014 | | 标题 | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与包元数据,通用包、单文件、签名、压缩、合并与分发描述 | -| 状态 | 评审中 v1.1 | -| 版本 | 1.1 | +| 状态 | 评审中 v1.2 | +| 版本 | 1.2 | | 最后修改 | 2026-10-28 | -| 对应实现 | mcpp >= 2026.10.27.1 | +| 对应实现 | 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) | | 相关 issue | mcpp#847、mcpp#849 | | 相关规范 | [SPEC-007](build-plugins.md)(包格式提供者、artifact action)、[SPEC-011](concurrency.md)(原子发布)、[SPEC-013](platforms.md)(平台、载体) | @@ -35,7 +35,8 @@ `--linkage`(未给出时取 `[pack] linkage`)是这次打包的构建的依赖链接,优先于 `[build] dependency_linkage` 与构建配置中的值;都未给出时每个库按它自己的声明链接,包元数据记为 `declared`。原生 Linux 目标上 `static` -选择 `-linux-musl` 行(`-linux-gnu` 目标换成对应的 musl 行),全静态程序不论 `--mode` 为何都整体打包; +选择 `-linux-musl` 行:未给出目标且工程没有 `[build] target` 时为本机架构的 musl 行,`--target` 的 `-linux-gnu` +目标换成对应的 musl 行,工程自己的 `[build] target` 照原样打包;全静态程序不论 `--mode` 为何都整体打包; openkal 目标上 `static` 为一个 `.oke`、无 `.okso`,`shared` 为 `.oke` 加其所需的 `.okso`。 ### 1.2 闭包 @@ -154,7 +155,8 @@ WebAssembly 的同名文件族,ok 映像的 `bin/`),规定见 docs/10。 mcpp-package.json ``` -包名带平台名(平台包名去掉 `-platform`),不带系统与架构。`[runtime] deploy` 放置的文件位于每条腿的映像旁边。 +包名带平台名(平台包名去掉 `-platform`),不带系统与架构;模式不是 `vendored` 时带模式的后缀(`-system`、`-static`、 +`self-contained` 为 `-bundle-all`),与原生包的命名相同。`[runtime] deploy` 放置的文件位于每条腿的映像旁边。 ### 4.3 启动器 @@ -227,7 +229,9 @@ WebAssembly 的同名文件族,ok 映像的 `bin/`),规定见 docs/10。 与两个摘要;同一路径的文件摘要**必须**相同,每个文件只取一份;合并后的树重新描述。`mcpp-package.json` 中的每个 路径**必须**是包内的规范相对路径。输出**必须**是新的路径,或一个已有的、不是输入之一的本命令写出的归档;目录输出 只写新目录。 -容器取 `--pkg-format`,否则取 `-o` 的扩展名,否则取第一个输入的容器;`-o` 缺省为当前目录下与树同名的文件。 +容器取 `--pkg-format`,否则取 `[pack] format`,否则取 `-o` 的扩展名,否则取第一个输入的容器;`-o` 缺省为当前目录下与树同名的文件。 +合并后的包的目录以输出命名,与 `mcpp pack -o` 相同:归档名去掉其扩展名(`-o app-1.0-openkal.zip` 含 +`app-1.0-openkal/`);不带 `-o` 时取第一个输入的目录名。 不进行任何构建。 ## 5. 容器 @@ -267,7 +271,7 @@ liblzma)压缩,不调用 `tar` 等外部程序;产物扩展名为 `.tar.gz`(缺 使用它的结果;缓存目录在 POSIX 上只对当前用户可写,`mcpp/`、`single-file/` 或摘要目录为符号链接或属于其他 用户时启动器拒绝运行。 5. 以原有的参数执行树中的程序(`bin/`;self-contained 的 Linux 树为根部的包装脚本;Windows 树为根部的 - `.exe`),退出状态即程序的退出状态。 + `.exe`),退出状态即程序的退出状态;启动器自身失败(载荷不可读、校验不符、无法执行)时写出原因并以 127 退出。 openkal 目标的单文件:载体携带映像本身(映像不引用其他映像时),否则携带全部映像的一个 stored zip,各映像数据按 64 KiB 对齐,`mcpp_carried_name` 为入口映像(SPEC-013 §11);`--systems` 的每个系统(缺省为本机系统)各写一个文件 @@ -296,15 +300,20 @@ openkal 目标的单文件:载体携带映像本身(映像不引用其他映像 ## 8. 分发描述 `mcpp emit xpkg --package <归档> [--package ...] --url <前缀> [-o <文件>]` 由本命令写出的包的 `mcpp-package.json` -生成 xim-pkgindex 条目(spec 2),资产地址为 `<前缀>/<文件名>`: +生成 xim-pkgindex 条目(spec 2),资产地址为 `<前缀>/<文件名>`。每个归档各以一个 `--package` 给出;其后不带选项的 +词(例如 shell 通配展开的第二个文件)被拒绝,而不是不加描述。 - 通用包是一个资产,`linux`、`macosx`、`windows` 三项的 URL 与 sha256 相同;包不携带运行器(非 `self-contained`) - 时三项各以运行器为依赖(`deps = { "" }`,`` 为平台运行器 argv 首项的程序名,即 xim-pkgindex - 中同名的包;openkal 为 `okrun`)。 + 时三项各以运行器为依赖(`deps = { "xim:" }`,`` 为平台运行器 argv 首项的程序名,即 xim-pkgindex + 中同名的包;openkal 为 `okrun`)。依赖带命名空间:不带命名空间的名字在家目录列出的第二个索引也有同名包时有歧义, + xim-pkgindex 拒绝它。 - 各系统各架构一个的包:每个系统一项,一个以 `${arch}` 代替架构的 URL 与每个架构的 sha256;文件名除架构外不同时 拒绝。 -- 安装把归档的唯一顶层目录移到安装目录;配置以 xvm 注册程序(通用包为 `bin/`,Windows 上为 - `bin/.cmd`;单腿原生包为程序所在的目录)。 +- 安装把归档的唯一顶层目录移到安装目录。该目录以归档命名(`mcpp pack` 的写法),安装取下载文件的名字去掉扩展名, + 对每个系统、架构与索引日后追加的版本都成立;打包后改名的归档与 xim 不解开的文件(非 `.tar.gz`、`.tar.zst`、 + `.tar.xz`、`.tar`、`.zip`)在写出条目时被拒绝;配置以 xvm 注册程序(通用包为 `bin/`,Windows 上为 + `bin/.cmd`;单腿原生包为程序所在的目录)。各系统的包把程序放在不同目录时(Windows 的程序在包根,POSIX + 的程序在 `bin/`,包根为便利脚本),条目按系统写出各自的目录(`bindir[os.host()]`)。 不带 `--package` 时 `mcpp emit xpkg` 照旧生成源码包的描述。 @@ -337,3 +346,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` 的每系统程序目录、带命名空间的运行器依赖、以归档名定位的顶层目录与多余词的拒绝。 | diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 088ad8c4d..5957bbc57 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -749,8 +749,10 @@ ok 映像的文件为 `target/dist/---`(Windows 上 `mcpp emit xpkg --package <归档> [--package …] --url <前缀> [-o <文件>]` 由本命令写出的包的 `mcpp-package.json` 生成 xim-pkgindex 条目(spec 2)。通用包是 `linux`、`macosx`、`windows` 同样安装的一个资产, -包不携带运行器时以运行器为依赖(`deps = { "okrun" }`);各系统各架构一个的包,每个系统一项,一个带 `${arch}` 的 -URL 与每个架构的 sha256。安装把归档的顶层目录移到位,并向 xvm 注册程序(Windows 上为 `.cmd` 启动器)。 +包不携带运行器时以运行器为依赖(`deps = { "xim:okrun" }`);各系统各架构一个的包,每个系统一项,一个带 `${arch}` 的 +URL 与每个架构的 sha256。安装把归档的顶层目录移到位,并向 xvm 注册程序(Windows 上为 `.cmd` 启动器);各系统的包 +把程序放在不同目录时(Windows 的程序在包根,POSIX 的程序在 `bin/`),按系统注册各自的目录。每个归档各以一个 +`--package` 给出。 ### 压缩与签名 diff --git a/docs/zh/90-build-from-source.md b/docs/zh/90-build-from-source.md index 7d6c0097e..68a69521f 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/ 645 shell scripts against a real binary +└── e2e/ 646 shell scripts against a real binary ``` ## 测试组织 @@ -94,7 +94,7 @@ tests/ **单元测试**(`tests/unit/`,156 个文件)是 `mcpp test` 发现的 C++ 程序。它们检验一个模块的契约,不需要二进制、也不需要文件系统状态。 -**端到端测试**(`tests/e2e/NN_.sh`,645 个文件)拿真实的 `mcpp` +**端到端测试**(`tests/e2e/NN_.sh`,646 个文件)拿真实的 `mcpp` 二进制跑真实工程。`run_all.sh` 是 CI 的入口。`mcpp test` **不**运行它们。 ```bash diff --git a/mcpp.toml b/mcpp.toml index 8afd1bfe8..9010ae15d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.27.1" +version = "2026.10.28.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 eb50e20fb..763adf613 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.27.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.28.1"; } // namespace mcpp diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index 2e3047fd8..2c757c27a 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -25,7 +25,10 @@ // D identity index name, package FQN, version // E own config features, cflags/cxxflags/asmflags/ldflags, per-glob flags, // defines, generated files, include dirs, source list -// F upstream the KEY of each direct dependency, recursively (Merkle) +// F upstream the KEY of each direct dependency and of each package whose +// module the package imports, a declared edge or not (the +// engine interface a build program's libraries import), +// recursively (Merkle) // G epoch kCacheEpoch — cache-format compatibility, decoupled from the // mcpp release number // diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 2b71e9912..03fedb74e 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -1643,6 +1643,10 @@ static std::expected step13_dependency_cache(PrepareState& st // Sources belonging to each package, package-root-relative and sorted. std::vector> pkgSources(state.packages.size()); std::vector pkgProvidesModule(state.packages.size(), 0); + // The package each unit is filed under, and the package that provides + // each module: the import edges below read both. + std::vector unitPackage; + std::map moduleProvider; for (auto& cu : ctx.plan.compileUnits) { // Longest matching root wins. Package roots can nest — a workspace // member lives under the workspace root — and taking the first match @@ -1665,13 +1669,33 @@ static std::expected step13_dependency_cache(PrepareState& st best = p; bestLen = len; bestRel = std::move(rels); } } + unitPackage.push_back(best); if (best != state.packages.size()) { - if (!cu.providesModule.empty()) pkgProvidesModule[best] = 1; + if (!cu.providesModule.empty()) { + pkgProvidesModule[best] = 1; + moduleProvider.emplace(cu.providesModule, best); + } pkgSources[best].push_back(std::move(bestRel)); } } for (auto& v : pkgSources) std::ranges::sort(v); + // A package whose unit imports another package's module reads that + // package's BMI, whether a manifest states the edge or not: the engine + // interface (`mcpp`) the libraries of a build program import is named + // by none of theirs. Its key is an upstream as a declared + // dependency's is; without it an entry compiled against another mcpp + // version's interface was served and failed with "import 'mcpp' has + // CRC mismatch" (2026.10.27.1 -> 2026.10.28.1, mcpp.plugins). + std::vector> importedFrom(state.packages.size()); + for (std::size_t u = 0; u < unitPackage.size(); ++u) { + const auto p = unitPackage[u]; + if (p == state.packages.size()) continue; + for (auto const& m : ctx.plan.compileUnits[u].imports) + if (auto it = moduleProvider.find(m); it != moduleProvider.end() && it->second != p) + importedFrom[p].insert(it->second); + } + std::vector pkgKeys(state.packages.size()); std::vector pkgInputs(state.packages.size(), nlohmann::json::object()); @@ -1701,6 +1725,19 @@ static std::expected step13_dependency_cache(PrepareState& st for (auto& e : state.dependencyEdges) if (e.consumerPackageIndex < pkgDeps.size()) pkgDeps[e.consumerPackageIndex].push_back(e.dependencyPackageIndex); + // An import a declared edge already covers adds nothing. One that a + // declared path runs the other way is a module this build generates + // for the importer below its consumer's root, filed under the + // consumer by its path (e2e 849): not an upstream, and left out + // rather than closing a cycle. + for (std::size_t p = 0; p < importedFrom.size(); ++p) + std::erase_if(importedFrom[p], [&](std::size_t q) { + if (std::ranges::find(pkgDeps[p], q) != pkgDeps[p].end()) return true; + const auto above = mcpp::graph::closure(pkgDeps, std::array{q}, /*include_roots=*/false); + return std::ranges::find(above, p) != above.end(); + }); + for (std::size_t p = 0; p < importedFrom.size(); ++p) + pkgDeps[p].insert(pkgDeps[p].end(), importedFrom[p].begin(), importedFrom[p].end()); auto keyOrder = mcpp::graph::topological_order(pkgDeps); if (!keyOrder) { keyCycleError = std::format( @@ -1760,6 +1797,10 @@ static std::expected step13_dependency_cache(PrepareState& st if (localTaint[e.dependencyPackageIndex]) localTaint[idx] = 1; for (auto& f : e.requestedFeatures) pa.features.push_back(f); } + for (auto q : importedFrom[idx]) { + if (!pkgKeys[q].empty()) pa.upstreamKeys.push_back(pkgKeys[q]); + if (localTaint[q]) localTaint[idx] = 1; + } std::ranges::sort(pa.upstreamKeys); pa.upstreamKeys.erase(std::unique(pa.upstreamKeys.begin(), pa.upstreamKeys.end()), diff --git a/src/cli/cmd_publish.cppm b/src/cli/cmd_publish.cppm index d57e15048..6406cb911 100644 --- a/src/cli/cmd_publish.cppm +++ b/src/cli/cmd_publish.cppm @@ -39,6 +39,13 @@ namespace mcpp::cli { // `mcpp emit xpkg ...` — only one subcommand defined, so the action sits // directly on the `emit xpkg` nested subcommand and receives its ParsedArgs. export int cmd_emit_xpkg(const mcpplibs::cmdline::ParsedArgs& parsed) { + // Each archive is its own `--package`: a word past one (a shell glob's + // second file) would otherwise go undescribed. + if (parsed.positional_count() > 0) { + mcpp::ui::error(std::format("emit xpkg takes no argument '{}'; each archive is its own --package " + "(--package a.zip --package b.zip)", parsed.positional(0))); + return 2; + } // `--package`: the distribution description of packages `mcpp pack` wrote // (SPEC-014 §8), read from their mcpp-package.json. if (parsed.option("package")) { diff --git a/src/main.cpp b/src/main.cpp index 5147635ac..8c18d33ad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -66,6 +66,10 @@ int main(int argc, char* argv[]) { // status line. mcpp::ui::close_region(); if (mcpp::ui::output_closed()) mcpp::run::end_for_closed_output(); + // A request to end that the command ran into ends mcpp as it asks; the + // watcher may still be ending it, and the command's status is not the one + // a shell is owed. + mcpp::run::end_if_requested(); // With statically linked libc++ (the macOS release linkage since // 0.0.50), static destruction can SIGABRT on exit — same issue xlings // guards against. A CLI tool needs no destructor-based cleanup; skip diff --git a/src/pack/distribution.cppm b/src/pack/distribution.cppm index 4cb8979e7..8e98339cd 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -140,6 +140,18 @@ std::string_view launcher_main(); // ─── the distribution description (SPEC-014 §8) ─────────────────────────── +// The suffixes of the archives a package is written as, the longer of two +// that end alike first. +inline constexpr std::array kArchiveSuffixes{ ".tar.gz", ".tgz", ".tar.zst", ".tar.xz", + ".tar", ".zip" }; + +// The name of a package's one directory: its archive's name without the +// suffix (`app-1.0-openkal.zip` holds `app-1.0-openkal/`), and a name with no +// archive's suffix as written (`-o app-1.0-openkal` with `--pkg-format dir`; +// a stem would cut at the version's last dot). `mcpp pack -o` and `--merge -o` +// name the directory so, and an index entry finds it so. +std::string archive_directory(const std::filesystem::path& archive); + // A package written by `mcpp pack`, as an index entry reads it. struct Asset { std::filesystem::path file; @@ -1462,6 +1474,13 @@ std::string lua_list(const std::vector& v) { } // namespace +std::string archive_directory(const fs::path& archive) { + const auto name = archive.filename().string(); + for (auto suffix : kArchiveSuffixes) + if (name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); + return name; +} + std::expected xpkg_descriptor(const std::vector& assets, std::string_view urlPrefix) { if (assets.empty()) return std::unexpected(std::string("no package to describe")); @@ -1486,9 +1505,12 @@ std::expected xpkg_descriptor(const std::vector "(mcpp pack --merge)")); auto const& a = assets.front(); for (auto const& [arch, _] : first.entry) archs.push_back(arch); + // The runner is a package of the index's own namespace; a bare name + // turns ambiguous as soon as a second index the home lists has one + // (xim-pkgindex refuses it: check-dep-namespace). std::string deps; if (first.mode != "self-contained") - deps = std::format(" deps = {{ \"{}\" }},\n", + deps = std::format(" deps = {{ \"xim:{}\" }},\n", lua_text(std::filesystem::path(first.runner.front()).stem().string())); for (auto system : { "linux", "macosx", "windows" }) blocks[system] = std::format( @@ -1544,24 +1566,53 @@ std::expected xpkg_descriptor(const std::vector else if (!first.program.empty()) command = std::filesystem::path(first.program).stem().string(); if (command.empty()) command = first.name; - // Where the program lies in the installed tree. - std::string bindir = "\"bin\"", alias; + // Where the program lies in the installed tree. The packages of two + // systems may place it apart (a Windows program at the root, a POSIX one + // under bin/ beside the root's convenience script); the entry then names + // each system's directory. + std::string bindir = "\"bin\"", alias, bindirs; if (universal) { alias = std::format("if os.host() == \"windows\" then opts.alias = \"{}.cmd\" end\n ", lua_text(command)); } else { - const auto program = std::filesystem::path(first.program); - bindir = program.has_parent_path() ? std::format("\"{}\"", lua_text(program.parent_path().generic_string())) - : std::string("\".\""); + 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())] = + program.has_parent_path() ? program.parent_path().generic_string() : std::string("."); + } + const auto& one = dirOf.begin()->second; + if (std::ranges::all_of(dirOf, [&](auto const& e) { return e.second == one; })) { + bindir = std::format("\"{}\"", lua_text(one)); + } else { + bindirs = " local bindir = {"; + for (auto const& [system, dir] : dirOf) + bindirs += std::format(" {} = \"{}\",", system, lua_text(dir)); + bindirs.back() = ' '; + bindirs += "}\n"; + bindir = "bindir[os.host()]"; + } } - // The archives' top-level directory: the one of a single asset, else what - // the assets' names share (they differ by system and architecture). - std::string top = assets.front().top; + // xim unpacks the archive beside the file it downloaded, and `mcpp pack` + // names the archive's one top-level directory after the archive; the + // install takes the directory from that file's name, which holds for + // every system, architecture and later version of the entry. + std::array used{}; for (auto const& a : assets) { - const auto [p, q] = std::ranges::mismatch(top, a.top); - top.erase(static_cast(p - top.begin())); + const auto file = a.file.filename().string(); + const auto ext = std::ranges::find_if(kArchiveSuffixes, [&](std::string_view e) { return file.ends_with(e); }); + if (ext == kArchiveSuffixes.end()) + return std::unexpected(std::format("{} is not an archive xim unpacks (.tar.gz, .tgz, .tar.zst, " + ".tar.xz, .tar, .zip)", file)); + if (archive_directory(a.file) != a.top) + return std::unexpected(std::format( + "{} holds the directory {}/: an entry installs the directory named after the archive, as " + "mcpp pack writes it (name the archive {}{})", file, a.top, a.top, *ext)); + used[static_cast(ext - kArchiveSuffixes.begin())] = true; } - const auto wrapper = lua_text(assets.size() == 1 ? top : top + "*"); + std::string exts; + for (std::size_t i = 0; i < kArchiveSuffixes.size(); ++i) + if (used[i]) exts += std::format("{}\"{}\"", exts.empty() ? "" : ", ", kArchiveSuffixes[i]); return std::format( "-- Generated by `mcpp emit xpkg --package` from the packages' mcpp-package.json\n" "-- (SPEC-014 §8).\n" @@ -1578,15 +1629,19 @@ std::expected xpkg_descriptor(const std::vector "import(\"xim.libxpkg.pkginfo\")\n" "import(\"xim.libxpkg.xvm\")\n\n" "function install()\n" - " -- The archive's one top-level directory is the package.\n" - " os.tryrm(pkginfo.install_dir())\n" - " for _, d in ipairs(os.dirs(\"{5}\")) do\n" - " os.mv(d, pkginfo.install_dir())\n" - " return true\n" + " -- The archive's one top-level directory is the package; it is named\n" + " -- after the archive, which xim unpacks beside it.\n" + " local top = pkginfo.install_file()\n" + " for _, ext in ipairs({{ {5} }}) do\n" + " if top:sub(-#ext) == ext then top = top:sub(1, #top - #ext) break end\n" " end\n" - " return false\n" + " if not os.isdir(top) then return false end\n" + " os.tryrm(pkginfo.install_dir())\n" + " os.mv(top, pkginfo.install_dir())\n" + " return os.isdir(pkginfo.install_dir())\n" "end\n\n" "function config()\n" + "{9}" " local opts = {{ bindir = path.join(pkginfo.install_dir(), {6}) }}\n" " {7}xvm.add(\"{0}\", opts)\n" " return true\n" @@ -1597,7 +1652,7 @@ std::expected xpkg_descriptor(const std::vector "end\n", lua_text(command), lua_text(first.version), universal ? std::format(", one package for every system ({})", lua_text(first.platform)) : std::string(), - lua_list(archs), xpm, wrapper, bindir, alias, lua_text(first.name)); + lua_list(archs), xpm, exts, bindir, alias, lua_text(first.name), bindirs); } // ─── --merge ────────────────────────────────────────────────────────────── @@ -1665,7 +1720,10 @@ std::expected merge(const MergeRequest& req) { differ("linkage", first.linkage, p.linkage), differ("mode", first.mode, p.mode) }) if (r) return std::unexpected(*r); } - const auto wrapper = trees.front().first.filename(); + // The merged package's directory: named after the output, as `mcpp pack + // -o` names it, else the first input's. + const fs::path wrapper = req.output.empty() ? trees.front().first.filename() + : fs::path(archive_directory(req.output)); // The container: as asked, else as the output's name says, else as the // first input was written. diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 9e1762444..dfbb62d84 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -629,16 +629,7 @@ std::string default_archive_name(std::string_view name, std::string_view version // archive — keeping the two in lock-step makes click-to-extract behave the // way users expect (and on Windows, Explorer's "extract here" too). std::string wrapper_dirname_from_archive(const std::filesystem::path& archive) { - auto name = archive.filename().string(); - for (auto suffix : {std::string_view{".tar.gz"}, std::string_view{".tgz"}, - std::string_view{".tar.zst"}, std::string_view{".tar.xz"}, - std::string_view{".tar"}, std::string_view{".zip"}}) { - if (name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); - } - // No archive's suffix: the name as written (`-o app-0.1.0-openkal` with - // `--pkg-format dir` is that directory; a stem would cut at the version's - // last dot and write `app-0.1`). - return name; + return mcpp::pack::distribution::archive_directory(archive); } } // namespace detail diff --git a/src/run.cppm b/src/run.cppm index ecc08dd04..b979f7ee6 100644 --- a/src/run.cppm +++ b/src/run.cppm @@ -50,6 +50,12 @@ void set_interrupt_files(const std::vector cleanup); diff --git a/src/run/run.cpp b/src/run/run.cpp index 350be835f..e34ecaa4c 100644 --- a/src/run/run.cpp +++ b/src/run/run.cpp @@ -54,9 +54,15 @@ struct Registered { }; // Everything mcpp owes before it ends at a request or a closed output, once. +// A second caller returns when the first has finished: what it ends then is +// the state the request left. A cleanup that reaches it again from within (a +// terminal's mode restored onto an output with no reader) returns at once. std::atomic g_finishing{false}; -void finish() { - if (g_finishing.exchange(true)) return; +std::once_flag g_finished; +thread_local bool t_finishing = false; +void finish_once() { + t_finishing = true; + g_finishing.store(true); // The files first: what the request leaves is the state the build was in // when it arrived. A program ended below wakes the thread that waited on // it, and what that thread then records is not written (`ending`). @@ -76,6 +82,10 @@ void finish() { } for (auto& f : todo) f(); } +void finish() { + if (t_finishing) return; + std::call_once(g_finished, finish_once); +} // ─── The launcher ──────────────────────────────────────────────────────── @@ -341,6 +351,12 @@ void install() { bool ending() { return g_finishing.load(); } +void end_if_requested() { + if (!os::interrupt::requested()) return; + finish(); + os::interrupt::end_as_requested(); +} + void set_interrupt_files(const std::vector>& files) { std::lock_guard g(g_files); interrupt_files() = files; diff --git a/tests/e2e/957_packages_merge_and_describe_themselves.sh b/tests/e2e/957_packages_merge_and_describe_themselves.sh index 275d63262..75c3aa18c 100755 --- a/tests/e2e/957_packages_merge_and_describe_themselves.sh +++ b/tests/e2e/957_packages_merge_and_describe_themselves.sh @@ -11,12 +11,14 @@ # # Criteria: # A. `--merge a --merge b` writes one package whose legs and systems are the -# union, every file of both, and a `mcpp-package.json` that describes it. +# union, every file of both, and a `mcpp-package.json` that describes it; +# its directory is named after the output, as `mcpp pack -o` names one. # B. A file both packages hold with different digests is refused, naming the # file and both digests; packages of different versions are refused. # C. `mcpp emit xpkg --package` of a universal package is one asset for # linux, macosx and windows, with the runner as a dependency when the -# package does not carry it. +# package does not carry it; an archive named without its own --package +# (a shell glob's second file) is refused, not left undescribed. # D. An archive merges as the directory it holds does. set -e @@ -63,6 +65,8 @@ import hashlib, json, sys, zipfile z = zipfile.ZipFile(sys.argv[1]) names = z.namelist() top = names[0].split("/")[0] +# Named after the output, as `mcpp pack -o` names a package's directory. +assert top == "merged" and all(n.startswith("merged/") for n in names), names[:3] d = json.loads(z.read(f"{top}/mcpp-package.json")) assert len(d["systems"]) == 4 and any(s.startswith("other-") for s in d["systems"]), d["systems"] assert sorted(d["legs"]) == ["aarch64-openkal", "x86_64-openkal"], d["legs"] @@ -130,9 +134,15 @@ for system in ("linux", "macosx", "windows"): assert block, system b = block.group(1) assert "https://example.org/v0.1.0/mrg-0.1.0-openkal.zip" in b and sha in b, (system, b) - assert 'deps = { "okrun" }' in b, (system, b) + assert 'deps = { "xim:okrun" }' in b, (system, b) assert 'xvm.add("mrg"' in text +assert 'local top = pkginfo.install_file()' in text and '{ ".zip" }' in text, text PY +if "$MCPP" emit xpkg --package target/dist/mrg-0.1.0-openkal.zip target/dist/mrg-0.1.0-openkal.zip \ + --url https://example.org/v0.1.0 > c2.lua 2> c2.err; then + fail "C: an archive without its own --package was taken" c2.lua +fi +grep -q "each archive is its own --package" c2.err || fail "C: the refusal does not say how" c2.err echo "ok: a universal package is one asset for every system" echo "PASS: 957 packages merge and describe themselves" diff --git a/tests/e2e/959_a_package_is_keyed_by_every_module_it_imports.sh b/tests/e2e/959_a_package_is_keyed_by_every_module_it_imports.sh new file mode 100755 index 000000000..c1cf74b74 --- /dev/null +++ b/tests/e2e/959_a_package_is_keyed_by_every_module_it_imports.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# requires: gcc fresh-sandbox +# 959 -- a cached package's key covers every package whose module it imports, +# a declared edge or not. +# +# A BMI is bound to the BMIs it read. The key of a cached package folded in the +# keys of the dependencies its manifest states, and a package can import a +# module its manifest does not name: the libraries of a build program import +# the engine interface (`import mcpp;`), which the engine adds to the build +# program's graph. Served under another interface, such an entry failed with +# "import 'mcpp' has CRC mismatch" (mcpp.plugins, 2026.10.28.1 over a cache +# 2026.10.27.1 builds had filled). +# +# Here the engine's part is played by an index package the importer does not +# declare: `ex.lib` imports `ex.api`, and the project depends on both. +# +# Criteria: +# A. With ex.api 0.1.0 the project builds, and ex.lib is cached. +# B. With ex.api 0.2.0 (another interface) and the same ex.lib, the project +# builds: ex.lib is compiled again against the interface it imports, and +# the entry compiled against 0.1.0 is not served. +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; } + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" +TMP_HOST="$(host_path "$TMP")" + +descriptor() { # : a package served from a seeded payload + local name=$1; shift + local rows="" + for v in "$@"; do + rows+=" [\"$v\"] = { url = \"https://example.invalid/$name-$v.tar.gz\", sha256 = \"$(printf '0%.0s' {1..64})\" },"$'\n' + done + cat > "$TMP/index/pkgs/e/ex.$name.lua" < "$store/ex.api/0.1.0/src/api.cppm" <<'EOF' +export module ex.api; +export namespace api { inline int value() { return 1; } } +EOF +cat > "$store/ex.api/0.2.0/src/api.cppm" <<'EOF' +export module ex.api; +export namespace api { struct Box { int v; }; inline int value() { return Box{2}.v; } } +EOF +# ex.lib imports ex.api and states no dependency on it. +cat > "$store/ex.lib/0.1.0/src/lib.cppm" <<'EOF' +export module ex.lib; +import ex.api; +export namespace lib { inline int twice() { return 2 * api::value(); } } +EOF +cat > src/main.cpp <<'EOF' +import ex.lib; +int main() { return lib::twice() == EXPECT ? 0 : 1; } +EOF +manifest() { # + cat > mcpp.toml < a.log 2>&1 || fail "A: the project over ex.api 0.1.0" a.log +ls "$MCPP_HOME"/build-cache/v1/pkg/*/ex.lib@0.1.0/ > /dev/null 2>&1 || fail "A: ex.lib was not cached" a.log +echo "ok: ex.lib is cached over ex.api 0.1.0" + +# B +manifest 0.2.0 4 +"$MCPP" run > b.log 2>&1 || fail "B: the project over ex.api 0.2.0 with the same ex.lib" b.log +grep -q "CRC mismatch" b.log && fail "B: ex.lib compiled against ex.api 0.1.0 was served" b.log +[ "$(ls -d "$MCPP_HOME"/build-cache/v1/pkg/*/ex.lib@0.1.0/*/ | wc -l)" -eq 2 ] \ + || fail "B: ex.lib has not one entry per interface it imported" b.log +echo "ok: ex.lib is compiled again against the interface it imports" + +echo "PASS: 959 a package is keyed by every module it imports" diff --git a/tests/e2e/timings/linux.tsv b/tests/e2e/timings/linux.tsv index fbbc90fea..38a1c59da 100644 --- a/tests/e2e/timings/linux.tsv +++ b/tests/e2e/timings/linux.tsv @@ -585,5 +585,6 @@ 956_a_single_file_runs_without_unpacking.sh 117250 957_packages_merge_and_describe_themselves.sh 93760 958_a_build_program_runs_again_when_its_output_is_gone.sh 1260 +959_a_package_is_keyed_by_every_module_it_imports.sh 25000 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 175dca5bf..58860e1a1 100644 --- a/tests/unit/test_pack_distribution.cpp +++ b/tests/unit/test_pack_distribution.cpp @@ -587,6 +587,7 @@ TEST(PackMerge, TheOutputIsNewOrAPackage) { TEST(PackDistribution, XpkgDescriptorWritesMetadataAsStrings) { d::Asset a; a.file = "evil-1.0-openkal.zip"; + a.top = "evil-1.0-openkal"; a.sha256 = std::string(64, 'a'); a.pkg.name = "ev\"il\nos.exit()"; a.pkg.version = "1.0"; @@ -596,7 +597,7 @@ TEST(PackDistribution, XpkgDescriptorWritesMetadataAsStrings) { ASSERT_TRUE(text) << text.error(); EXPECT_NE(text->find("description = \"ev\\\"il\\010os.exit() 1.0"), std::string::npos) << *text; EXPECT_EQ(text->find("\nos.exit()"), std::string::npos) << *text; - EXPECT_NE(text->find("deps = { \"okrun\" }"), std::string::npos) << *text; + EXPECT_NE(text->find("deps = { \"xim:okrun\" }"), std::string::npos) << *text; } // The compression fits the container the merge writes and the name it is @@ -635,7 +636,9 @@ TEST(PackDistribution, XpkgDescriptorNamesTheCommandAndTheArchiveTop) { EXPECT_NE(u->find("name = \"mcppls\""), std::string::npos) << *u; EXPECT_NE(u->find("xvm.add(\"mcppls\", opts)"), std::string::npos) << *u; EXPECT_NE(u->find("opts.alias = \"mcppls.cmd\""), std::string::npos) << *u; - EXPECT_NE(u->find("os.dirs(\"mcppls-0.0.18-openkal\")"), std::string::npos) << *u; + EXPECT_NE(u->find("for _, ext in ipairs({ \".zip\" }) do"), std::string::npos) << *u; + EXPECT_NE(u->find("local top = pkginfo.install_file()"), std::string::npos) << *u; + EXPECT_NE(u->find("os.mv(top, pkginfo.install_dir())"), std::string::npos) << *u; std::vector per; for (auto [triple, top] : { std::pair{ "x86_64-linux-musl", "okrun-0.1.3-linux-x86_64" }, @@ -653,6 +656,74 @@ TEST(PackDistribution, XpkgDescriptorNamesTheCommandAndTheArchiveTop) { auto r = d::xpkg_descriptor(per, "https://example.org/0.1.3"); ASSERT_TRUE(r) << r.error(); EXPECT_NE(r->find("name = \"okrun\""), std::string::npos) << *r; - EXPECT_NE(r->find("os.dirs(\"okrun-0.1.3-*\")"), std::string::npos) << *r; - EXPECT_NE(r->find("description = \"openkal-tools 0.1.3"), std::string::npos) << *r; + EXPECT_NE(r->find("for _, ext in ipairs({ \".zip\" }) do"), std::string::npos) << *r; + EXPECT_NE(r->find("path.join(pkginfo.install_dir(), \"bin\")"), std::string::npos) << *r; +} + +// The install takes the package's directory from the downloaded archive's +// name, which is what `mcpp pack` names it after; an archive renamed since +// is refused, and so is a file xim does not unpack. +TEST(PackDistribution, XpkgDescriptorInstallsTheDirectoryNamedAfterTheArchive) { + d::Asset a; + a.file = "tool-1.0-linux-x86_64.tar.gz"; + a.top = "tool-1.0-linux-x86_64"; + a.sha256 = std::string(64, 'd'); + a.pkg.name = "tool"; + a.pkg.version = "1.0"; + a.pkg.legs = { "x86_64-linux-gnu" }; + a.pkg.program = "bin/tool"; + d::Asset w = a; + w.file = "tool-1.0-windows-x86_64.zip"; + w.top = "tool-1.0-windows-x86_64"; + w.pkg.legs = { "x86_64-windows-gnu" }; + w.pkg.program = "tool.exe"; + auto r = d::xpkg_descriptor({ a, w }, "https://example.org/1.0"); + ASSERT_TRUE(r) << r.error(); + EXPECT_NE(r->find("for _, ext in ipairs({ \".tar.gz\", \".zip\" }) do"), std::string::npos) << *r; + + d::Asset renamed = a; + renamed.file = "tool.tar.gz"; + auto refused = d::xpkg_descriptor({ renamed }, "https://example.org/1.0"); + ASSERT_FALSE(refused); + EXPECT_NE(refused.error().find("named after the archive"), std::string::npos) << refused.error(); + + d::Asset tgz = a; + tgz.file = "tool-1.0-linux-x86_64.tgz"; + auto t = d::xpkg_descriptor({ tgz }, "https://example.org/1.0"); + ASSERT_TRUE(t) << t.error(); + EXPECT_NE(t->find("for _, ext in ipairs({ \".tgz\" }) do"), std::string::npos) << *t; + EXPECT_EQ(d::archive_directory("app-1.0-openkal.tar.zst"), "app-1.0-openkal"); + EXPECT_EQ(d::archive_directory("app-1.0-openkal"), "app-1.0-openkal"); + + d::Asset okb = a; + okb.file = "tool-1.0-linux-x86_64.okb"; + auto notArchive = d::xpkg_descriptor({ okb }, "https://example.org/1.0"); + ASSERT_FALSE(notArchive); + EXPECT_NE(notArchive.error().find("not an archive xim unpacks"), std::string::npos) << notArchive.error(); +} + +// Packages of two systems that place their program apart -- a Windows one at +// the root, a POSIX one under bin/ -- give each system its own directory. +TEST(PackDistribution, XpkgDescriptorNamesEachSystemsProgramDirectory) { + std::vector per; + for (auto [triple, top, program] : + { std::tuple{ "x86_64-linux-musl", "okrun-0.1.3-linux-x86_64", "bin/okrun" }, + std::tuple{ "aarch64-macos", "okrun-0.1.3-macos-aarch64", "bin/okrun" }, + std::tuple{ "x86_64-windows-gnu", "okrun-0.1.3-windows-x86_64", "okrun.exe" } }) { + d::Asset a; + a.file = std::string(top) + ".zip"; + a.sha256 = std::string(64, 'c'); + a.top = top; + a.pkg.name = "openkal-tools"; + a.pkg.version = "0.1.3"; + a.pkg.legs = { triple }; + a.pkg.program = program; + per.push_back(a); + } + auto r = d::xpkg_descriptor(per, "https://example.org/0.1.3"); + ASSERT_TRUE(r) << r.error(); + EXPECT_NE(r->find("local bindir = { linux = \"bin\", macosx = \"bin\", windows = \".\" }"), + std::string::npos) << *r; + 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; } diff --git a/upgrade-lane-logs/new-doctor.log b/upgrade-lane-logs/new-doctor.log deleted file mode 100644 index 243d393b0..000000000 --- a/upgrade-lane-logs/new-doctor.log +++ /dev/null @@ -1,34 +0,0 @@ - Checking toolchain - ok clang 23.1.3 (x86_64-unknown-linux-gnu) at /tmp/lane/upgrade-lane/home/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ (what a build here uses) - ok payload llvm@23.1.3 (packaging revision 0 → 1) (builds keep it; `mcpp toolchain install` takes the new one) - Checking package home - ok consistent - Checking std module - ok /tmp/lane/upgrade-lane/home/build-cache/v1/std/d74c7107fd501d77 (entry 36.8 MiB) - Checking registry - ok xlings at /tmp/lane/upgrade-lane/home/registry/bin/xlings - ok default index = 'mcpplibs' - Checking package indexes - ok index 'mcpplibs' requires mcpp >= 2026.9.18.3 - ok index 'xim-pkgindex' declares no mcpp floor - Checking home - ok self-contained: no upstream ([xlings] upstream) - ok /tmp/lane/upgrade-lane/home on ext4: every write is the filesystem's own - Checking cache health - ok build cache size = 36.8 MiB - Checking last runtime closure verdict - ok /tmp/lane/upgrade-lane/p-new/target/x86_64-linux-gnu/6fca180a97c5f98c/bin/p-new (RuntimeBinding f3b2c2d035013bc2): pass - Checking runtime capabilities - ok no host runtime capabilities required - Checking toolchain runtime deps - ok all installed toolchain RUNPATH dirs present - ok vendored xlings 2026.10.17.1 (pinned 2026.10.17.1) - ok subos lib symlinks all resolve (/tmp/lane/upgrade-lane/home/registry/subos/default/lib) - Checking gcc include-fixed headers - ok no installed gcc-family payloads to check - Checking build policy - ok module interfaces: .cppm (built-in only) - ok build.mcpp run bound: 600s (from built-in default) - ok process deadlines: enforced (POSIX SIGKILL / Windows job object) - -Doctor result: all checks passed diff --git a/upgrade-lane-logs/new-index-update.log b/upgrade-lane-logs/new-index-update.log deleted file mode 100644 index dd00904c6..000000000 --- a/upgrade-lane-logs/new-index-update.log +++ /dev/null @@ -1,15 +0,0 @@ - Updating all index repos - Updating package index xim done in 1.8s - Downloading xim done, 1.5 MB in 0.6s - Updating package index awesome done in 1.2s - Downloading awesome done, 10 KB in 0.0s - Updating package index scode done in 1.1s - Downloading scode done, 11 KB in 0.0s - Updating package index d2x done in 1.1s - Downloading d2x done, 9 KB in 0.0s - Updating package index (rebuild) done in 0.2s - Note the index has newer packagings of installed payloads: - llvm@23.1.3 (packaging revision 0 → 1) - glibc@2.44.3 (packaging revision 2 → 4) - builds keep the installed ones; `mcpp toolchain install ` takes the new one - Updated index refresh complete diff --git a/upgrade-lane-logs/new-provision.log b/upgrade-lane-logs/new-provision.log deleted file mode 100644 index dac05bba7..000000000 --- a/upgrade-lane-logs/new-provision.log +++ /dev/null @@ -1,10 +0,0 @@ - Installing llvm@23.1.3 via mcpp's xlings - Updating llvm@23.1.3 packaging revision 0 → 1 - Downloading xim:glibc@2.44.3 done in 0.3s - Downloading xim:patchelf@0.18.0 done in 1.3s - Downloading xim:llvm@23.1.3 done in 0.7s - Repaired c++@16.1.0 in mcpp's package home had no registration; deactivated - Repaired c++@16.1.0 in mcpp's package home had no registration; dropped - Installed llvm@23.1.3 → /tmp/lane/upgrade-lane/home/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ - -Tip: `mcpp toolchain default llvm@23.1.3` to make this the default. diff --git a/upgrade-lane-logs/new-run-after-provision.log b/upgrade-lane-logs/new-run-after-provision.log deleted file mode 100644 index 3cc595b69..000000000 --- a/upgrade-lane-logs/new-run-after-provision.log +++ /dev/null @@ -1,13 +0,0 @@ - Resolving toolchain - Resolved llvm@23.1.3 → @mcpp/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ - Target x86_64-unknown-linux-gnu - Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}] - Inferred target p-new (bin from src/main.cpp) - Compiling p-new v0.1.0 (.) -warning: fingerprint changed (a1e8361286e00ca7 → 35bfb92762f36cca), full rebuild; `mcpp clean --stale` drops the directories no build still uses - - Finished dev [unoptimized + debuginfo] in 2.12s - Running `target/x86_64-linux-gnu/35bfb92762f36cca/bin/p-new` - -Hello from p-new! -Built with import std + std::println on modular C++23. diff --git a/upgrade-lane-logs/new-run-old-project.log b/upgrade-lane-logs/new-run-old-project.log deleted file mode 100644 index ef5a3ea58..000000000 --- a/upgrade-lane-logs/new-run-old-project.log +++ /dev/null @@ -1,4 +0,0 @@ - Running `target/x86_64-linux-gnu/6fca180a97c5f98c/bin/p-old` - -Hello from p-old! -Built with import std + std::println on modular C++23. diff --git a/upgrade-lane-logs/new-run-on-old-index.log b/upgrade-lane-logs/new-run-on-old-index.log deleted file mode 100644 index cb90de84a..000000000 --- a/upgrade-lane-logs/new-run-on-old-index.log +++ /dev/null @@ -1,14 +0,0 @@ - Updating vendored xlings 2026.10.16.2 -> 2026.10.17.1 from the release of this mcpp (pinned 2026.10.17.1) - Resolving toolchain - Resolved llvm@23.1.3 → @mcpp/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ - Target x86_64-unknown-linux-gnu - Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}] - Inferred target p-old (bin from src/main.cpp) - Compiling p-old v0.1.0 (.) -warning: fingerprint changed (f24dd8c55f72810d → 6fca180a97c5f98c), full rebuild; `mcpp clean --stale` drops the directories no build still uses - - Finished dev [unoptimized + debuginfo] in 1.72s - Running `target/x86_64-linux-gnu/6fca180a97c5f98c/bin/p-old` - -Hello from p-old! -Built with import std + std::println on modular C++23. diff --git a/upgrade-lane-logs/new-run1.log b/upgrade-lane-logs/new-run1.log deleted file mode 100644 index b5a4c43b4..000000000 --- a/upgrade-lane-logs/new-run1.log +++ /dev/null @@ -1,12 +0,0 @@ - Resolving toolchain - Resolved llvm@23.1.3 → @mcpp/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ - Target x86_64-unknown-linux-gnu - Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}] - Inferred target p-new (bin from src/main.cpp) - Compiling p-new v0.1.0 (.) - - Finished dev [unoptimized + debuginfo] in 1.14s - Running `target/x86_64-linux-gnu/6fca180a97c5f98c/bin/p-new` - -Hello from p-new! -Built with import std + std::println on modular C++23. diff --git a/upgrade-lane-logs/new-run2.log b/upgrade-lane-logs/new-run2.log deleted file mode 100644 index 3756ef791..000000000 --- a/upgrade-lane-logs/new-run2.log +++ /dev/null @@ -1,4 +0,0 @@ - Running `target/x86_64-linux-gnu/6fca180a97c5f98c/bin/p-new` - -Hello from p-new! -Built with import std + std::println on modular C++23. diff --git a/upgrade-lane-logs/new-toolchain-list.log b/upgrade-lane-logs/new-toolchain-list.log deleted file mode 100644 index 0c835ad87..000000000 --- a/upgrade-lane-logs/new-toolchain-list.log +++ /dev/null @@ -1,33 +0,0 @@ -Toolchains: - llvm 23.1.3 (packaging revision 1 available: `mcpp toolchain install llvm 23.1.3`) - -Targets: - TARGET NOTE TOOLCHAIN STATUS - x86_64-linux-gnu host llvm 23.1.3 installed - aarch64-linux-android cross android-ndk 30.0.16248370available - aarch64-linux-gnu cross llvm 23.1.3 via dependency graph - aarch64-linux-musl static, cross gcc 16.1.0 available - aarch64-none-elf bare, static, cross llvm 23.1.3 available - armv7a-none-eabi bare, static, cross llvm 23.1.3 available - armv7a-none-eabihf bare, static, cross llvm 23.1.3 available - riscv32-none-elf bare, static, cross llvm 23.1.3 available - riscv64-none-elf bare, static, cross llvm 23.1.3 available - thumbv6m-none-eabi bare, static, cross llvm 23.1.3 available - thumbv7em-none-eabi bare, static, cross llvm 23.1.3 available - thumbv7em-none-eabihf bare, static, cross llvm 23.1.3 available - thumbv7m-none-eabi bare, static, cross llvm 23.1.3 available - thumbv8m.base-none-eabi bare, static, cross llvm 23.1.3 available - thumbv8m.main-none-eabi bare, static, cross llvm 23.1.3 available - thumbv8m.main-none-eabihfbare, static, cross llvm 23.1.3 available - wasm32-emscripten wasm, static, cross emsdk 6.0.9 available - x86_64-linux-android cross android-ndk 30.0.16248370available - x86_64-linux-musl static gcc 16.1.0 available - x86_64-none-elf bare, static, cross llvm 23.1.3 available - x86_64-windows-gnu PE, static, cross gcc 16.1.0 available - x86_64-windows-musl PE, static, cross llvm 23.1.3 via dependency graph - riscv64-linux-musl static, cross — planned - x86_64-macos cross — planned - -Available toolchains (run `mcpp toolchain install `): - gcc 16.1.0 / 15.1.0 / 13.3.0 / 11.5.0 / 9.4.0 - llvm 22.1.8 / 20.1.7 diff --git a/upgrade-lane-logs/old-env.log b/upgrade-lane-logs/old-env.log deleted file mode 100644 index 6b47703c6..000000000 --- a/upgrade-lane-logs/old-env.log +++ /dev/null @@ -1,21 +0,0 @@ - Bundled xlings 2026.10.16.2 (from the release of this mcpp: /tmp/lane/upgrade-lane/old/registry/bin/xlings) - Initialize mcpp sandbox layout (one-time) - Fetching package index (one-time) - Bootstrap patchelf into mcpp sandbox (one-time) - Installing xim:patchelf@0.18.0 done in 5.6s - Bootstrap ninja into mcpp sandbox (one-time) - Installing xim:ninja@1.12.1 done in 6.6s -MCPP_HOME = /tmp/lane/upgrade-lane/home -xlings binary = /tmp/lane/upgrade-lane/home/registry/bin/xlings -xlings pinned = 2026.10.16.2 -xlings home = /tmp/lane/upgrade-lane/home/registry -config = /tmp/lane/upgrade-lane/home/config.toml -build cache = /tmp/lane/upgrade-lane/home/build-cache/v1 -meta cache = /tmp/lane/upgrade-lane/home/cache -default toolchain = (none — run `mcpp toolchain install gcc 16.1.0`) - -Index repos: - mcpplibs https://github.com/mcpplibs/mcpp-index.git [artifact] (default) - -Toolchain = gcc 13.3.0 (x86_64-linux-gnu) -std module src = diff --git a/upgrade-lane-logs/old-run.log b/upgrade-lane-logs/old-run.log deleted file mode 100644 index 4f9d189be..000000000 --- a/upgrade-lane-logs/old-run.log +++ /dev/null @@ -1,12 +0,0 @@ - Resolving toolchain - Resolved llvm@23.1.3 → @mcpp/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ - Target x86_64-unknown-linux-gnu - Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}] - Inferred target p-old (bin from src/main.cpp) - Compiling p-old v0.1.0 (.) - - Finished dev [unoptimized + debuginfo] in 2.09s - Running `target/x86_64-linux-gnu/f24dd8c55f72810d/bin/p-old` - -Hello from p-old! -Built with import std + std::println on modular C++23. diff --git a/upgrade-lane-logs/old-tc-gcc.log b/upgrade-lane-logs/old-tc-gcc.log deleted file mode 100644 index 822563f55..000000000 --- a/upgrade-lane-logs/old-tc-gcc.log +++ /dev/null @@ -1,6 +0,0 @@ - Installing gcc@16.1.0 via mcpp's xlings - Downloading xim:binutils@2.42.1 done, 27.4 MB in 24.0s - Downloading xim:gcc@16.1.0 done, 101.7 MB in 9.9s - Installed gcc@16.1.0 → /tmp/lane/upgrade-lane/home/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++ - -Tip: `mcpp toolchain default gcc@16.1.0` to make this the default. diff --git a/upgrade-lane-logs/old-tc.log b/upgrade-lane-logs/old-tc.log deleted file mode 100644 index 3d4a6fa91..000000000 --- a/upgrade-lane-logs/old-tc.log +++ /dev/null @@ -1,5 +0,0 @@ - Installing llvm@23.1.3 via mcpp's xlings - Downloading xim:glibc@2.44.3 done, 36.4 MB in 69.0s - Installed llvm@23.1.3 → /tmp/lane/upgrade-lane/home/registry/data/xpkgs/xim-x-llvm/23.1.3/bin/clang++ - -Tip: `mcpp toolchain default llvm@23.1.3` to make this the default.