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" \ "/$INDEX_REPO/actions/workflows/xpkgindex-update.yml" >&2 exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9d2df60..7328463f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [Unreleased] + +### Fixed + +- **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 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. + ## [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/specs/packaging.md b/docs/specs/packaging.md index d016911e8..6630cc734 100644 --- a/docs/specs/packaging.md +++ b/docs/specs/packaging.md @@ -4,8 +4,8 @@ |---|---| | 规范编号 | SPEC-014 | | 标题 | 打包与分发:链接、闭包、容器三个维度与构建腿,暂存树与包元数据,通用包、单文件、签名、压缩、合并与分发描述 | -| 状态 | 评审中 v1.1 | -| 版本 | 1.1 | +| 状态 | 评审中 v1.2 | +| 版本 | 1.2 | | 最后修改 | 2026-10-28 | | 对应实现 | mcpp >= 2026.10.27.1 | | 相关设计文档 | `.agents/docs/2026-10-28-platforms-part2-packaging-and-distribution-design.md`(§2–§7、§14)、`.agents/docs/2026-10-28-platforms-design.md`(§4.7) | @@ -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,7 @@ WebAssembly 的同名文件族,ok 映像的 `bin/`),规定见 docs/10。 与两个摘要;同一路径的文件摘要**必须**相同,每个文件只取一份;合并后的树重新描述。`mcpp-package.json` 中的每个 路径**必须**是包内的规范相对路径。输出**必须**是新的路径,或一个已有的、不是输入之一的本命令写出的归档;目录输出 只写新目录。 -容器取 `--pkg-format`,否则取 `-o` 的扩展名,否则取第一个输入的容器;`-o` 缺省为当前目录下与树同名的文件。 +容器取 `--pkg-format`,否则取 `[pack] format`,否则取 `-o` 的扩展名,否则取第一个输入的容器;`-o` 缺省为当前目录下与树同名的文件。 不进行任何构建。 ## 5. 容器 @@ -267,7 +269,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 +298,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 +344,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/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..e0553e5e7 100644 --- a/src/pack/distribution.cppm +++ b/src/pack/distribution.cppm @@ -1486,9 +1486,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 +1547,54 @@ 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. + constexpr std::array kArchives{ ".tar.gz", ".tar.zst", ".tar.xz", ".tar", ".zip" }; + 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(kArchives, [&](std::string_view e) { return file.ends_with(e); }); + if (ext == kArchives.end()) + return std::unexpected(std::format("{} is not an archive xim unpacks (.tar.gz, .tar.zst, .tar.xz, " + ".tar, .zip)", file)); + if (file.substr(0, file.size() - ext->size()) != 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 - kArchives.begin())] = true; } - const auto wrapper = lua_text(assets.size() == 1 ? top : top + "*"); + std::string exts; + for (std::size_t i = 0; i < kArchives.size(); ++i) + if (used[i]) exts += std::format("{}\"{}\"", exts.empty() ? "" : ", ", kArchives[i]); return std::format( "-- Generated by `mcpp emit xpkg --package` from the packages' mcpp-package.json\n" "-- (SPEC-014 §8).\n" @@ -1578,15 +1611,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 +1634,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 ────────────────────────────────────────────────────────────── 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..ef78f2ed5 100644 --- a/src/run/run.cpp +++ b/src/run/run.cpp @@ -54,9 +54,12 @@ 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. std::atomic g_finishing{false}; -void finish() { - if (g_finishing.exchange(true)) return; +std::once_flag g_finished; +void finish_once() { + 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 +79,7 @@ void finish() { } for (auto& f : todo) f(); } +void finish() { std::call_once(g_finished, finish_once); } // ─── The launcher ──────────────────────────────────────────────────────── @@ -341,6 +345,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..1a8649ccb 100755 --- a/tests/e2e/957_packages_merge_and_describe_themselves.sh +++ b/tests/e2e/957_packages_merge_and_describe_themselves.sh @@ -16,7 +16,8 @@ # 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 @@ -130,9 +131,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/unit/test_pack_distribution.cpp b/tests/unit/test_pack_distribution.cpp index 175dca5bf..04eb192e0 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,66 @@ 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 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; }