# Changes
## 1.1.3 - 2026-10-08
- **修复 `normalize` 的归并缺陷(自装 raku-pm 在 OpenSSL 3.0 系统上仍误报缺失 `libcrypto.so.1.1`)**:
1.1.2 引入的 `NativeLib.normalize` 有两个 bug,导致 `libcrypto.so` / `.so.3` /
`.so.1.1` 等**同库候选未能归并到逻辑名 `crypto`**,自装时依旧把不存在的
`libcrypto.so.1.1` 判成硬缺失而回滚(用户实测:`raku-pm.raku install . --force`
在只有 OpenSSL 3.0 的机器上必然失败):
- `%rev` 反向表用 `$v.values.flat` 构建,平台文件数组被「条目化
(itemized)」而未被展平,实际只生成了「每平台一整个数组当成一个键」的错误映射;
- 版本号剥离正则 `^(.+?)\.(\d[\w.]*)$` 只能处理**尾随**版本(如 `libcrypto.so.1.1`),
无法处理**中缀**版本(如 `libcrypto.3.dylib`),且对 Windows Git 的绝对路径候选
无能为力。
现改为两层遍历构建 `%rev`,并新增 `core-name` 同时覆盖尾随 / 中缀两种版本位置、
对绝对路径取 basename 再归并。`t/native-normalize.t` 已覆盖三平台 + 未知库 +
绝对路径场景(并修正原 `plan 10` 与实际 8 条断言不符导致该测试永远不过的问题)。
## 1.1.2 - 2026-10-08
- **修复运行时本地库检查的跨平台误报(自装 raku-pm 不再因此失败)**:
`runtime-native-libs` 会把源码里每个 `is native('...')` 当成独立硬依赖,而
raku-pm 自己的 `MD5` 为跨平台 + 纯 Raku 回退写了**一组多平台候选**
(`libcrypto` / `.so` / `.so.3` / `.so.1.1` / `.dylib` / `.3.dylib` / `-3-x64.dll`)。
旧逻辑用「全部都要存在」的 AND 语义,于是 Linux 上把 macOS / Windows 候选以及
旧的 `libcrypto.so.1.1`(OpenSSL 1.1 ABI)全判成「缺失」而放弃安装,导致
`raku-pm.raku install .` 在 OpenSSL 3.0 系统上必然失败。新增 `NativeLib.normalize`
归并:丢弃非本平台的候选名、把同库多平台候选反向归并到逻辑名(如
`libcrypto.so.1.1 → crypto`),从而只算逻辑库、走本平台候选列表「任一存在即满足」
的 OR 语义。该修复同时惠及任何声明多平台 native 候选的包。
## 1.1.1 - 2026-10-08
- **自装 raku-pm 时自动跳过自身回归测试**:`raku-pm.raku install .`(用源码脚本自装 /
自升级)此前会走通用安装流程,在「自己正在安装的事务里」跑 raku-pm 自己的回归套件,
而该套件会重入写文件锁 / 真实 HTTP / 拉起 raku-pm CLI 做真实安装,导致死锁或超时、
整链回滚、install 被判失败。现对名为 `RakuPM` 的发行版(`Client.rakumod` 的
`!install-chain-inner`)强制跳过自身测试,依赖仍照常测试;装完用 `raku-pm test .`
(tools/run-suite.raku)即可独立、安全地验证。
## 1.1.0 - 2026-10-08
- **help 横幅(title)旁显示当前程序版本号**:`raku-pm help` / `help <命令>` / `help all` /
`help <组名>` 在终端(富文本)模式下,横幅提示行 `raku-pm help` 之后追加 `vX.Y.Z`
(取自 raku-pm 自身的 META6.json / 已装发行版元数据,取不到时静默不显示)。
改动集中在 `UI.rakumod` 的 `ui-banner` 增加可选 `:$version` 参数,由 `CLI.rakumod` 的
`print-help` 一次性取 `raku-pm-version()` 后传给四处横幅调用;非富文本(管道/重定向)
横幅本就为空串,故对纯文本输出与测试零影响。
## 1.0.4 - 2026-09-29
- **修复 Rakudo 2026.09 下 `unhandled Failure detected in DESTROY` 警告**(`install`/`repos`/`list` 等命令)。
Rakudo 2026.09 起,`.d`/`.e` 等文件测试符对【不存在】的路径不再稳定返回 `Bool::False`
(本机 WSL 2026.09 实测 `.d` 对缺失路径返回未处理 `Failure`),该 Failure 若不被显式消费
会在对象销毁时报警。根因在 `CLI.rakumod` 的 `looks-like-path`,旧写法
`return True if try { $s.IO.d }` 的 `if` 修饰符未能可靠消费缺失路径产生的 Failure。
现改为「先 `.e` 再 `.d`、且都用 `.so` 显式消费」的短路写法:缺失路径时 `.e` 已为 False,
短路后根本不会调用 `.d`,从源头杜绝未处理 Failure。
同步审查并加固其余文件测试符用法(`Repositories.add/remove`、`CLI` 的 `raku-pm-version`/
`raku-pm-git-root`/`list --paths`/`install` 的 git-URL 判断、`Author.logout`、`Store.has`、
`Ledger.exists`、`Lock.exists`),凡可能落在缺失路径上的 `.d` 一律加 `.e` 短路或 `.so` 消费。
## 1.0.3 - 2026-09-24
- 修复 `ui-wrap` 无断点文本(URL 等)折行退化为逐字符竖排的 bug(1.0.2 引入):
回退到「最近可断点」时,若整段剥到 1 字符仍没找到断点,旧判断条件照样成立,
导致每行只推 1 个字符(实测 `raku-pm info DB::SQLite` 的源码地址竖排成 6 行)。
现在只有真的找到断点才回退,否则整段硬断——每段占满列宽、绝不丢字。
回归测试 3 条钉住(含「第一段不得短于 60 列」的竖排探针),变异验证通过。
## 1.0.2 - 2026-09-24
- **修复 `raku-pm info` 长描述「错行」**(UI 展示层)。`ui-table` 此前把整段长描述当成一列、
不按终端宽度折行,终端被迫在任意位置硬折,把表格边框 `│` 打断,看着很别扭。
现在按终端宽度(`RAKUPM_COLUMNS`/`COLUMNS` 可覆盖,默认 80)截断列宽,长文本列承担压缩,
并按【显示宽度】折行(CJK 友好:优先在 `、` `:` 等标点后断开,无词边界时按字符硬断),
续行各列空白对齐、边框完整。配套 `ui-term-width` / `ui-wrap` 两个辅助子,
及 `t/ui.t` 的折行回归测试。
## 1.0.1 - 2026-09-24
**修复:部分 Windows 机器上 `zef install` 因随包测试误报而中止。**
- 现象:`t/author-remote.t` 场景 1b「当前用户 X 拥有显式完全控制」在个别 Windows 机器上失败
(13 项仅挂这 1 项),zef 跑测试即中止安装。根因有二:
① 生产侧按 `whoami` 字面串(`主机\用户`)做 icacls 授权,而 icacls 显示的账户名
与该串可能**大小写不同或主机前缀不同**(NetBIOS 名显示形式随机器而异);
② 测试侧 `contains` 大小写敏感地匹配整串,且 ACE 集为空时「无继承」断言**空真**。
- 生产侧(`Author.save-credentials`):授权主体改用 **SID**(icacls 的 `*S-1-...:F` 语法,
机器无关、不受账户名显示形式影响;`whoami /user` 的表头本地化但 SID 恒为 ASCII),
SID 拿不到再退回账户名。
- 测试侧(`t/author-remote.t`):账户匹配改为**用户名段 + 大小写不敏感**(icacls 显示
可能不带 `主机\` 前缀),并新增「ACE 行非空」断言堵住空真;失败时 `diag` 打印
icacls 原始输出便于定位。
- WSL/Linux 不受影响(走 POSIX chmod 600 分支)。
- 已做变异验证:跳过 ACL 加固后「去掉继承权限」断言变红,防线真实。
## 1.0.0 - 2026-09-24
**首个稳定版。** 0.88.x 这一路是「1.0 前审查与加固」专场:消费侧与生产侧功能在更早的版本已完整,
1.0 的重心是把**质量门禁、路径安全、凭据安全**做到经得起第三方审计。
- **门禁提速与可信度**(0.88.2–0.88.5):套件加每用例墙钟 + 最慢排行小结;`t/` 与 `xt/`
合成单池、按历史耗时降序做 LPT 分桶、各条道**独立连续跑(去掉层屏障)**,
全量墙钟从 **699.8s 压到 494.4s(加速比 7.12x)**;并加**防漏跑断言**(计划集 vs 执行集比对,
漏跑用例不再假绿)。
- **路径安全收口(第三方审计 AUDIT.md 的 S-1 / M-1,0.88.6)**:修 `Client::Git.!git-cache-key`
的整条路径直拼穿越(含恶意 `git-deps` 入口,用户无感);把「含 `..` 成员的 tar 包不会写出
目标目录」钉进 `xt/tar-member-escape.t`(变异验证:加 `-P` 立刻红)。
- **凭据安全(审计 L-1,0.88.7)**:Windows 上 `Author.save-credentials` 改用 `icacls` 把
`credentials.json` 的 ACL 收敛到**仅当前用户**(去掉继承 + 显式完全控制),不再因跳过
`chmod` 而在多用户机器上暴露 api-key。
- **第三方审计全条目核实**(0.88.6–0.88.7):M-2(`RAKUPM_CURL` 不存在)/ M-3(curl 默认校验证书)/
L-3(`%!last-conflict` 已自带重置)/ L-4(`!tarball-url` 失败路径不存在)经核实不成立或不可利用,
均记录在案、未做无效改动。
- 配套不变式修正与文档同步:`assert-under` 对 `..` 穿越**天然失效**,净化须在生成路径时做
(`docs/architecture{,.en}.md`、`nav-map.md` 的净化点清单已从 6 处更新到 7 处);
双语 `README` 与 `Changes` 随版本同步。
语义上 1.0.0 遵循本项目约定:**major = 磁盘布局或行为的不兼容变更**——本版本未引入不兼容变更,
跳到 1.0 是**成熟度信号**(完整功能 + 已通过安全审计 + 门禁可信),API 与 0.88.x 保持兼容。
## 0.88.7 - 2026-09-24
- **凭据文件在 Windows 上的权限收紧(第三方审计报告 AUDIT.md 的 L-1,防御性加固)。**
`Author.save-credentials` 原先在 Windows 上直接跳过 POSIX `chmod`(Windows 无此语义),
凭据文件 `credentials.json`(含 api-key,类 ssh key)在多用户机器上存在被别的账号读到的风险。
修法:`is-windows` 时改用 `icacls` 把 ACL 收敛到**仅当前用户**——`/inheritance:r` 去掉继承权限、
再 `/grant:r <当前用户>:F` 显式只授当前用户完全控制(best-effort,失败不致命、不喷噪声)。
当前用户取自已 `whoami`(按字节读,避开中文 Windows 的 GBK 编码坑)。
守卫 `t/author-remote.t` 场景 1b:Windows 断言 ACE 无 `(I)` 继承标记且当前用户持 `(F)`;
POSIX 断言权限位为 `0600`。
- **核实并关闭审计 AUDIT.md 的 L-3 / L-4(无需改代码,仅作记录):**
- L-3:`Resolver.explain-all` 与 `resolve-all` 开头都已 `%!last-conflict = %()` 重置,
审计担心的「共享状态跨命令串味」在当前调用模式下不会发生,已自带防御。
- L-4:审计称 `!derive-base` 对非 http URL 返回 Nil 会在 `!tarball-url` 里字符串化拼错 URL,
但实测 `!tarball-url` 拼相对路径用的是 `$!index-url.subst(...)` 而非 `base-url`,
该失败路径不存在;`base-url` 全项目无消费者,故不加显式 die(避免误伤 file:// 本地索引测试)。
## 0.88.6 - 2026-09-23
- **修 git 缓存目录的路径穿越(第三方审计报告 AUDIT.md 的 S-1,1.0 前最后一处漏网)。**
`Client::Git.!git-cache-key` 是全项目**唯一"整条路径直接拼"**的注入点:它把 git 地址
规整成缓存目录名后直接 `$cache-root.add($key)`,**没过 `path-component` / `assert-under`**。
于是 `https://host/x/../../../tmp/evil.git` 能让 clone 落到 `target/git-cache` 之外 ——
实测:`git-cache/github.com/foo/../../../escaped-dir` 最终建在 git-cache 的**上一层**。
三条入口都汇聚到它:CLI 直接输入、`self-upgrade --from`、**以及恶意发行版 META6.json
里的 `git-deps`**(这条用户完全无感)。
修法:对 key 的每一段做**栈式规范化**(`..` 上溯一层、已出界则丢弃;`.` 跳过;其余段
过 `path-component`)。刻意不用"见到 `..` 就 die" —— 远程 URL 里 `..` 非法,但
`self-upgrade --from ../repo` 这类**本地路径**里它完全合法,一刀切会把正常用法打死。
**★ 顺带写明一条对不变式的修正**:`assert-under` 对 `..` 穿越**天然失效** —— 它比的是
**字面前缀**(`$child.absolute` 以 `$root.absolute ~ sep` 开头),而含 `..` 的路径字面上
确实"在下面";再加上 **Raku 的 `IO::Path` 不折叠 `..`**(`.absolute` 与 `.cleanup` 都原样
保留,真正解析 `..` 的是内核),这类检查**看起来完全正常**。
⇒ **净化必须在生成路径时做,事后断言补不上。**(`docs/architecture{,.en}.md` 与
`nav-map.md` 的净化点清单已从 6 处更新到 7 处。)
回归测试 `t/git-cache-key-safety.t`(12 条)刻意**不靠字符串断言**做核心判定 —— 它真的
让文件系统去建目录、再看落点(因为字符串层面的"在不在根下"对 `..` 恒为真)。变异验证:
把净化换成"原样返回" → 8 条红;第一版把逃逸落点写成 `$base/tmp`(实际上溯三层落在
`$target/tmp`),变异时暴露为假阴性,已改成与层数无关的兜底判据。
- **把"含穿越成员的 tar 包不会写出目标目录"钉进门禁**(同一份审计的 M-1)。
审计指出 `Ecosystem.source-dir-for` 解压源码包时是裸调 `tar -xzf`、没有安全标志。
**实测结论:当前不可利用** —— GNU tar / bsdtar 默认就拒绝含 `..` 的成员(打印
"Removing leading `../'" 与 "Member name contains '..'",非零退出,一个文件都不解出来)。
**没有改解压命令本身**:加安全标志在 GNU tar / bsdtar 之间语法不一致,而当前行为已经
安全,为它引入平台差异不划算。改为新增 `xt/tar-member-escape.t` 钉住这条不变量 ——
变异验证:给解压加上 `-P` 之后文件立刻落到目标目录之外、测试当场变红。
- **审计报告其余条目的核实结论(部分与源码事实不符,故未改动)**:
· **M-2「`RAKUPM_CURL` 可被替换成任意可执行文件」不成立**:源码里**没有** `RAKUPM_CURL`
这个变量(HTTP 层走 `HTTP::Tinyish`,没有开放 exe 替换)。`RAKUPM_TAR` 确实存在
(Author 打包 / Ecosystem 解包都认),但它是**用户自己设的环境变量**,与审计自己
"不是设计错误"的判断一致 —— 加 `--version` 校验反而会给合法的替代实现添堵。
· **M-3「TLS 未显式校验」**:curl 默认即校验证书,且没有明文降级路径,未改。
· **L-2 / L-5** 审计自己判定"不是 bug / 不是安全边界" → 未动。
· **L-1 / L-3 / L-4**(Windows 凭据 ACL / Resolver 状态重置 / 非 http(s) 索引基址显式报错)
属防御性加固,当前代码路径都不会触发,留作后续。
## 0.88.5 - 2026-09-23
- **门禁再降一截:去掉"按层同步"的屏障(各条道独立连续跑)。**
0.88.3 的合并调度把总墙钟从 699.8s 压到 544.8s,但离理论下界仍差得远 ——
下界 = `max(最长用例 ≈195s, 用例合计/8 ≈320s) ≈ 371s`。差距出在执行粒度上:当时是
「第 i 层 = 每条道的第 i 个元素,一起跑完再进下一层」,**每层末尾都是一道屏障** ——
某条道上排着 160s 的用例时,别的道跑完自己那层(可能只要 5s)就得干等 155s。
现在每条道**独立连续推进**:谁先跑空谁立刻接下一个,屏障消失。并发上限仍是 `$jobs`
(每条道同时只跑一个子进程),不会把机器压爆。线程安全上刻意保守:worker 只做两件事
—— ① 实时打印(经一把 `Lock` 串行化,保证每行完整不交错)② 把结果 push 进**自己的**
数组并作为返回值交回;共享容器(`@results` / `@timings` / `%est`)一律由主线程在
`await` 之后统一更新,因为多线程并发写 Array/Hash 在 Raku 里没有保证 —— 宁可多一轮
汇总,也不赌。
防漏跑断言(计划集 vs 实际执行集)**照旧生效**:变异验证把 `for @lane` 改成
`for @lane.head(*-1)`(每条道少跑最后一个)→ 立即中止并点名 3 个未执行用例。
## 0.88.4 - 2026-09-23
- **修 `raku-pm reinstall <没装过的包>`(不带 `--version`)直接报"找不到模块"。**
用户现场:`raku-pm reinstall CSV::Table` → 「在所有已配置的仓库里都找不到模块
'CSV::Table' (约束 = )」。对照实验:同一个包 `raku-pm install CSV::Table` 能正常
解析出 9 个发行版,说明只是 reinstall 这条路坏了。
**根因是三处叠加的"空串 vs 未定义"语义错位**:
① `Client.install` 的签名是 `Str :$version`(**没有默认值**),内部用
`$version.defined ?? "= $version" !! $constraint` 判断"有没有指定版本";
② CLI 在没给 `--version` 时把兜底值写成**空串** `''` —— 空串是【已定义】的,
于是 `$effective` 变成 `"= "`(等于空版本)→ 解析器谁都不匹配;
③ `reinstall` 的"未安装 → 退化为 install"分支又把这个空串原样传给了 `install`,
于是"本该装最新版"变成"找不到模块"。
修法:CLI 与 `install` 对齐,兜底值传**未定义的 `Str`**;`Client.reinstall` 再加一道
—— 只在真的给了版本时才透传 `:version`,空串一律丢弃。两处都改,因为它们各自都
可能被单独回退(回归测试里各配了一条断言)。
**为什么既有测试没抓住它**:`t/reinstall.t` 里早就有「包未装过 → 退化为 install」的
用例,但走的是 **API 直调**(`$client.reinstall('FreshPkg', '')`,version 是未定义
值)—— 与 CLI 的形态不同,而这个洞**只**在 CLI 传空串时出现。新用例**照 CLI 修复前
的形态**传 `:version('')`,所以抓得住;另加一条静态守卫扫 CLI 里传给 `install` /
`reinstall` 的 `:version` 兜底值,并按**被调方语义**限定范围:`upgrade` /
`uninstall` / `fetch` / `look` 的签名是 `Str :$version = ''`,它们**就是**用空串表示
"没指定",那几处 `!! ''` 是对的,一刀切禁掉反而会误报。
变异验证两组:去掉 `Client` 侧的加固 → 3 条红;把 CLI 兜底值改回空串 → 静态守卫红。
- 顺带记录一处 Raku 陷阱(本次自己踩到):`my Bool $x = $a.defined && $a.chars;` 会
`Type check failed … expected Bool but got Int (0)` —— **`&&` 返回的是操作数本身,
不是布尔值**。要写 `so (...)`。
## 0.88.3 - 2026-09-23
- **门禁把 700s 往下压:`t/` 与 `xt/` 合并调度 + LPT 分桶(首次用实测数据做优化)。**
0.88.2 的计时数据指出瓶颈不在进程启动(只占 2%),而在**分批方式**:旧实现按目录分开
跑、各自按文件名分批,长任务于是随机落在批次尾部 —— 批尾常只剩一两个还在跑、其余并发槽
空转(`xt/self-upgrade-bootstrap` 单个 158.7s,足以独占一条尾巴),8 并发实测只有 2.83x。
现在:① `t/` 与 `xt/` **合成一个池**,`t/` 里那些 1–8s 的短用例正好填进 `xt/` 长用例留出
的空槽;② 按**历史耗时降序**调度(LPT:最长处理时间优先 + 依次放进当前最空的桶),耗时
缓存在 `~/.raku-pm/suite-timings.json`(`RAKUPM_SUITE_TIMINGS` 可改路径;首次无历史时用
粗启发式,第二次起用实测);③ 预热从"每段各一次"变为**只预热一次**。
**刻意没选"谁先空谁抢下一个"的事件驱动调度**:LPT 只是排序 + 一次划分,没有异步状态机,
且每个用例恰好属于一个桶 —— 配合新增的**防漏跑断言**(计划里有用例没被执行到 →
立即中止门禁并点名),"调度自己出 bug、少测了几个文件却报绿"这种最危险的假绿在结构上就
不可能悄悄发生。宁可慢一点,也不要一个看起来很干净的绿灯。
判定口径不变:`t/` 强制安静、`xt/` 只判成败、失败用例仍串行复验一次。合并调度后日志里
的用例是**混着出现**的(这是必然),故末尾新增**按目录的分组小结**(`t/ 97 个:全过`)。
- **`t/message.t` 不再对"环境级 rakudo 提示"假红。** 该测试断言子进程 stderr **逐字节**
等于 `E\nW\nH\n`;可 rakudo 每个进程都会扫 RAKULIB 链上的 FileSystem 仓库,只要有
`.pm` 扩展名的模块就往 stderr 喷一段 deprecation 提示 —— 于是**在"用户自己维护 RAKULIB
模块目录"的机器上必然假红**(而这恰恰是 raku-pm 的目标场景:见 `Installer/ShellTemplates`
里那条注释),原因与 raku-pm 毫无关系。实测:用户 `~/raku-modules` 里 pull 出一个
`B-Raku/lib/B/Raku.pm` 之后,本文件立刻变红。
修法是**精确剥离**那块提示本身(首行 `Saw N occurrence … of deprecated code.` 到尾行
`… this message will disappear!`,含其中分隔线),其余一个字节都不动;并配**负面断言**
钉住边界(产品自己多写一行不许被当成噪音丢掉)。两处变异验证:完全不过滤 → 3 条红;
过度过滤(只留 E/W/H 行)→ 负面断言红。放宽类改动必须配负面断言,否则"过滤"会悄悄
退化成"什么都放过"。
## 0.88.2 - 2026-09-23
- **门禁首次能回答"为什么慢":`tools/run-suite.raku` 默认打印耗时小结。**
全量 `t/`+`xt/` 要十几分钟,此前只有一个总数 —— "慢在进程启动、还是慢在某几个
测试"只能靠猜。现在每个用例输出一行墙钟,结尾给出最慢 15 个排行与统计:
```
===== 用例耗时(单文件墙钟,含 raku 启动;最慢 15 个) =====
<秒>s xt/<最慢的那个>.t
…
用例耗时合计(= 若串行跑一遍的下限): <秒>s( 个用例,平均 <秒>s)
t/ 实际墙钟: <秒>s
并行加速比: x(用例合计 ÷ 墙钟;墙钟含预热/串行复验/自检)
↳ 其中 t/ 预热预编译: <秒>s(已含在墙钟内)
raku 启动基线(min of 3 × `raku -e ""`): <秒>s
→ 光启动就占用例合计的 %( × <秒>s = <秒>s);用例内容本身的净耗时约 <秒>s
```
关键是**启动基线及其占比**:它把"起了个进程"与"测试真在算"分开,否则任何优化都
无从判断收益。判据 —— 若各用例耗时都贴近基线,瓶颈就是**进程数**(合并用例文件 /
复用进程才有用);只有少数明显突出(网络、大量子进程)才值得去抠测试逻辑。
单文件耗时刻意覆盖"起进程 → 退出"整段(含 raku 启动与 precomp 检查),那才是门禁
真正付出的代价;只计时测试体会低估到失去意义。`--no-timings` 可关掉(CI 日志求干净)。
预热、串行复验的耗时单独列出且注明"已含在墙钟内",避免重复计数误判。
## 0.88.1 - 2026-09-23
- **外部数据的畸形形态:一律"人话报错 / 归一 / 滤除",不再甩裸 Raku 异常**
(1.0 审查 P2 批最后一项,外加全量数据复核实测出的两处)。共同点是:用户拿到的
要么是看不懂的内部异常,要么是莫名噪音 —— 而原因往往只是"少了一对引号"。
**① `name` / `version` / `description` / `builder` 类型错或缺失 → 明确报错。**
修复前直接把这些值塞进构造器,类型不对就抛:
```
Type check failed in assignment to $!version; expected Str but got Rat (1.0)
```
没有文件名、没有字段上下文、也没有"该怎么改"。现在:
```
META 文件 .../META6.json 的「version」字段类型不对:期望字符串,实际是 Rat(1)。
JSON 里不加引号就是数值类型,请给整个值加一对引号让它成为字符串。
注意不能靠程序自动转:数值解析成 Rat 时已丢失书写形式……
```
**为什么不"归一"而是报错**:`name` / `version` 是发行版的**身份**(要进 store 路径
与账本),而且没法无损转 —— `"version": 1.0` 到 Raku 是 Rat,`~1.0` 得到 `"1"`,
悄悄转会把用户装的东西记成另一个版本,比失败糟得多。(`description` / `builder`
同理按严格处理;`auth` 早就单独归一了。)
**★ 一处自我纠正**:初版提示会回显"改好的字面量" `"version": "1"` —— 而 Rat 把
`1.0` 归一成了 `1`,用户照抄当场把版本从 1.0 改成 1。这是"照解析结果反推原文"
的经典错误,已改为只给方法(加引号)、不回显值,并加了**防回退断言**钉住它。
**② 索引行 `auth` 是数组 → 归一(既有 bug,本轮实测发现)。**
`from-meta-file` 那条路早就在用 `!normalize-auth` 处理数组形态,`from-index-row`
这条路只判 `.defined && .chars` —— 数组**能通过判断却被原样返回**,撞上
`auth-of-row(--> Str)` 的返回约束:
```
Type check failed for return value; expected Str but got Array (["github:titsuki"])
```
cpan 索引里的 **`Text::Sift4`**(5 行)就这样带着裸异常倒下。两条路现已共用同一个
归一实现。全量索引实测:63 行 `auth` 非字符串(58 行是 `"auth": null`,那类不崩)。
**③ `resources` 数组里的 `null` → 滤除(既有 bug)。** 真实索引里 `Config`
1.3.0 / 1.3.1(cpan 与 rea 共 4 行)是 `"resources": [null]`,旧实现
`.map(*.Str)` 对 null 调 `~Nil`:
```
Use of uninitialized value @result of type Any in string context
```
除了噪音,还会把**空串当成一个资源名**收进列表。现在先滤掉未定义/空元素。
- 回归测试:新增 `t/meta-type-errors.t`(**51 条**)—— 4 个字符串字段的类型错矩阵
(断言报错点名文件 + 字段 + 期望/实际类型)、缺失与 `null`、**防回退**(不回显
有损字面量)、`from-index-row` 的可读报错、`resources` 含 null 不喷警告且不留空串、
`auth` 数组/null 两种形态归一,以及端到端(CLI rc=1 且 stderr 里能找到文件名与
字段名、不再出现 `Type check failed`)。
- 变异验证:不校验类型 → **15 红**;回显有损字面量 → **2 红**(正好那两条防回退
断言);缺失静默默认 → **9 红**;`resources` 的 null 不过滤 → **2 红**;
`auth` 退回只认字符串 → **2 红**(含 `expected Str but got Array` 原文)。
- 全量真实数据复核(这是本轮几个结论的来源):缓存里 **28 个已解包发行版**全部
解析成功;**全量索引 25117 行**全部通过,**0 误拒**;扫描过程中的
`Use of uninitialized value` 警告由 4 条降到 **0 条**。
- 文档:双语 `docs/architecture{,.en}.md` 的「发行版 = 元数据文件」一节补一张
「畸形外部数据按**能不能无损修复**分三类」的对照表(报错 / 归一 / 滤除)与两条
纪律(索引与 META 两条路共用同一归一实现;报错要给可执行的改法但不回显字面量);
`docs/nav-map.md` 同步 `Distribution` 行数。
- Bump to 0.88.1.
## 0.88.0 - 2026-09-23
- **Windows 保留字符改为「百分号编码」,不再放进路径**(1.0 审查 P2 批第三项)。
动手前我对它的定性**错过一次**,先更正:`* ? " < > |` **不含路径分隔符、也不含
`.`/`..`,拼不出目标之外的路径**,所以它**不是安全漏洞**(与 0.87.0 修的
`/` `\` `..` `:` 不是一类)。它真实的性质是**健壮性缺陷**,而且后果可复现:
| 平台 | 旧行为 |
| --- | --- |
| Windows | 缓存路径变成 `...//*.tar.gz` → 打开失败报 **`Invalid argument`**,重试 3 次后回滚 |
| 其它 | 不报错,但造出一个名字就是 `*` 的文件/目录(还会被 shell 的 glob 展开) |
两边报错都看不出真因 —— 用户只会觉得"这包有毒"。
- **影响面(全量索引 25117 条实测,逐分量扫 50234 个 name/version)**:
`*` 出现在 **109 条**(全是 `version` 占位符:`Add`、`Algorithm::Viterbi`、
`Apache::LogFormat`…);`?` `"` `<` `>` `|` `%` **各 0 条**。
另核实:这 109 条的 `source-url` 本身是**好的**(URL 里是 `%2A` 编码),
坏的只有本地路径 —— 所以修好它就能让它们真的装上。
- 修法:`Fs.path-component` 内部把输入分成**两类,处理方式刻意不同**
(这条线已写进函数注释与 `docs/architecture{,.en}.md`,别混起来):
· **穿越类**(`/` `\`、恰为 `.`/`..`、控制字符、结尾点)→ **继续 die**(有安全含义);
· **Windows 保留类**(`* ? " < > |`)→ **百分号编码**:`*`→`%2A`、`?`→`%3F`、
`"`→`%22`、`<`→`%3C`、`>`→`%3E`、`|`→`%7C`,且 **`%` 自身编成 `%25`**。
【为什么是编码,而不是"替换成 `_` 之类"】编码是**单射**的 —— 不同输入永远落到
不同输出,所以不会出现"两个分量撞进同一目录"(那正是 0.87.0 选择"拒绝而非静默
清理"的理由)。`%` 一起编码就是为单射服务的:字面量 `%2A` 必须与"编码后的 `*`"
落在不同目录。测试里专门放了 `_` / `a_` / `A_B` 样本,让"替换成 `_`"那类非单射
写法当场撞车。
- **端到端实证(本项最硬的凭据)**:造一个 `version = "*"` 的发行版 ——
```
装完 store/WinStar/%2A/ ← 落在编码目录,不是字面量 *
账本记 version=* ← 用户可见面仍是原始串
外部 raku 进程 use StarPkg → USE_OK 42 ← 真的能用
```
CUR::Installation 那层**接受** `*`,不会二次失败 —— 这点动手前专门验过,
否则"修了路径还是装不上"就白改了。
- 兼容性(全量真实数据复核):**0 个分量被误拒**(历史 store 条目照常可达);
映射发生变化的分量**恰好 109 个**,且只有 `*` → `%2A` 一种。
- 回归测试:`t/path-safety.t` 由 69 → **85 条**,新增编码矩阵(7 个字符)、
混合串、**单射性**(12 个输入两两不撞车)与 `version="*"` 的端到端安装。
- 变异验证:去掉编码 → **13 红**;把 `*` 换成 `_`(非单射)→ **6 红**,且正好含
那两条单射性断言;改成"拒绝"(方案 B)→ 测试硬中止、门禁判失败。
- 顺带修掉一处文档漂移:`xt/http-no-hang.t` 的头部注释仍称网络层是「纯 Raku 的
HTTP::Tiny(无外部进程)」—— 那是 0.82.0 之前的说法。现状是**单后端
`HTTP::Tinyish`,底层就是系统 curl,仍然是外部进程**(区别在"谁来管子进程")。
- 文档:双语 `docs/architecture{,.en}.md` 的路径安全不变式补"两类字符"的划分与
109 条的实测数据;`docs/nav-map.md` 同步 `Fs` 行数。
- Bump to 0.88.0.
## 0.87.3 - 2026-09-23
- **修掉两处「静默错答」**(1.0 发布前审查 P2 批里的前两项)。共同点是:不报错、
不崩,看起来一切正常,但给出的答案是错的。
**① 预发布版的比较** —— `Version.key` 原先把整个预发布段折成一个布尔
(`$!prerelease eq '' ?? 1 !! 0`),于是 `1.0.0-alpha` / `-beta` / `-rc1` 的 key
**完全相同**、`cmp` 一律判 `Same`。而 `cmp` 是 `pick-best` / `sort-versions` /
`installed` 星号标注共用的底座,于是答案**取决于输入顺序**:
| 调用 | 修复前 | 修复后 |
| --- | --- | --- |
| `pick-best("*", <1.0.0-alpha 1.0.0-beta 1.0.0-rc1>)` | `1.0.0-alpha` ❌ | `1.0.0-rc1` |
| `pick-best("*", <1.0.0-rc1 1.0.0-beta 1.0.0-alpha>)` | `1.0.0-rc1` ✅(纯属运气) | `1.0.0-rc1` |
修法:新增 `Version.!pre-key`,按 semver 2.0.0 §11 展开成可比较结构 ——
正式版 > 预发布;数字标识符 < 字母数字标识符;同位数字比**数值**(不是字典序);
前缀相同则字段少的更低。顺带在 `from-string` 里剥掉 build metadata
(`+build` 在 semver 里**不参与**优先级比较,不剥会把 `1.0.0-alpha+1` 与
`1.0.0-alpha` 排成两个不同版本)。
影响面(本机全量索引 25117 条实测):带预发布标记的只有 **16 条 / 3 个发行版
(0.06%)**;其中 1 个发行版的最佳版本会变 —— 生态里的 `Hey` 从
`1.0.0-beta.2`(错)变成 `1.0.0-beta.9`(对:它是 8 个 beta 里最高的)。
**② 卸载的「虚假成功」** —— `Client.uninstall` 此前不看 `Installer.uninstall`
的返回值,无条件打印「已卸载 Foo」。四类「其实什么都没发生」的情形全部报成功 +
**rc=0**:包根本没装 / 指定版本不存在 / 范围没匹配到任何版本 / 因反向依赖被拒绝
(最后这条走 `msg-warn` + `return`,同样 rc=0)。脚本靠退出码判断成败
(`set -e`、`uninstall X && …`)会被全部骗过;而且报的是「已卸载」——
比不报更糟,用户以为卸干净了。
修法:`Installer.uninstall` 改为返回「本次真的从 CUR 摘掉了哪些版本」
(以 CUR 为准 —— 它才是「装没装」的事实来源);`Client.uninstall` 据此判定,
一个都没摘掉就 `fail-die`(rc=1)并给出可执行的措辞(列出实际已装版本;
被依赖而拒绝时给出 `--recursive` / `--force` 两条出路)。反向依赖拒绝同样改成
`fail-die`。`--force` / `--recursive` / 正常卸载的路径行为不变。
- 回归测试:
· `t/version.t` 新增预发布版语义块 —— 10 组 `cmp` 三向断言、semver §11 示例序列
整条严格升序、`pick-best` / `sort-versions` **颠倒输入结果不变**、`satisfies`
侧一致性(`1.0.0-alpha` 不满足 `>= 1.0.0`)。**顺序无关性是关键**:
旧实现在同一集合上换个输入顺序就换答案,只断言「选出来了」根本抓不到。
· 新增 `t/uninstall-missing.t`(29 条):四类情形进程内断言「抛错」且
**断言没有打印「已卸载」**(那正是 bug 的症状)、断言失败后包仍在(无副作用)、
端到端子进程断言**真实退出码**、以及 `Installer.uninstall` 的返回值契约。
- 变异验证(门禁不会变红就等于没有门禁):
`key` 退回布尔 → `t/version.t` **9 红**(`t/uninstall-missing.t` 保持全绿,隔离正确);
Client 不看返回值 → **12 红**;拒绝路径退回静默 `return` → **16 红**。
- `xt/rdepends.t` 同步:拒绝卸载现在以 `die` 表达(rc=1),测试改用裸块 + `CATCH`
接住并断言「确实被拒绝」(`Mid` 仍在)。
- 文档:双语 `docs/architecture{,.en}.md` 的「版本管理」补两条不变式 ——
预发布定序**只有一处实现**(`Version.key`)且别退回布尔;卸载成没成是**判定的**
不是宣告的。`docs/MANUAL{,.en}.md` 的 `uninstall` 段补退出码说明与预发布定序速查。
`docs/nav-map.md` 行数表顺带校准(8 处漂移)。
- Bump to 0.87.3.
## 0.87.2 - 2026-09-23
- **修掉 semver 糖语法 `^` / `~` 的上界算错(1.0 发布前审查的 F4)—— 方向是「约束过宽」**。
根因很短但很典型:`Version` 里数「用户写了几段版本」用的是
`.comb('.').elems`,而 **`comb` 返回的是匹配到的分隔符本身**,不是被分隔的段:
`'1.2'.comb('.')` → `(.)` 只有 1 个元素,段数却是 2 —— 整整差一位,
于是「按最后指定的位 +1」那类分支全部错位:
| 约束 | 修复前上界 | 正确上界 |
| --- | --- | --- |
| `~1.2`(最常见的两段写法) | `<2.0.0` | `<1.3.0` |
| `~0.1` | `<1.0.0` | `<0.2.0` |
| `~0.0` | `<1.0.0` | `<0.1.0` |
| `^0.0` | `<1.0.0` | `<0.1.0` |
| `^0.0.0` | `<0.1.0` | `<0.0.1` |
影响面:`uninstall <包> --version='~1.2'` 会**连 1.3.0 / 1.9.0 一起卸掉**;
依赖解析里同类的 `^0.x` 约束会放进整个 1.x。比写严了危险得多 —— 用户以为锁住了
minor,实际放进来整个大版本。
- 修法:新增 `Version.!spec-parts`(`split('.')` 数段数,语义不歧义),
`!caret-upper` / `!tilde-upper` 共用它。**`~1.2.3` 与 `^1` 当年「看起来是对的」纯属
歪打正着**(差一位后恰好落进正确分支)—— 这也是这个 bug 能活这么久的原因。
- 回归测试:
· `t/version.t` 的糖语法块改成 **12 行矩阵**,每行三条断言 —— **精确展开串**(逐字)
+ **应含** + **应不含**(`t/version.t` 对 `~1.2` 原先只有正面断言「含 1.2.5」,
上界放宽到 2.0.0 后照样为真 → 整个 bug 在套件里隐形;**放宽类 bug 只能靠负面断言
与逐字展开串抓**)。共 80 条用例。
· `t/uninstall-range.t` 补 `~1.2` 的**用户可见面**端到端(这个文件的注释一直声称覆盖
`~1.2`,实际只测了 `^1.0`):版本集 `1.2.0 / 1.2.9 / 1.3.0 / 1.9.0` 正是为区分
「锁 minor(正确)」与「放到 2.0.0(错误)」而挑的。
- 变异验证:`!spec-parts` 复原成 `comb` 计数 → `t/version.t` 10 红、**`t/uninstall-range.t`
2 红**(正是新加的 `~1.2` 块,证明它对老 bug 有区分力);`tilde-upper` 忽略 minor → 8 红;
`caret` 全零分支不看段数 → 4 红。
- 文档:`MANUAL{,.en}.md` 里一条**过时的说法**顺带修掉 —— 它写「`--version` 仅接受精确
版本号,不支持范围」,而范围(`<1.0` / `>=1.0,<2.0` / `*` / `^1.0` / `~1.2`)早已支持,
CLI 的 help 文案里本来就写着;现在两份 MANUAL 都改成准确描述 + 糖语法上界速查。
- 顺带修一处**测试自身的环境脆弱点**(`t/ghost-provides.t` 的子进程校验):它用
`run(..., :in($空文件路径))` 给子进程喂 EOF,而实测在本机环境下**传路径**会让子进程
直接被杀(`exitcode -16`、零输出),只有传**已打开的 IO::Handle** 才正常。
改成传句柄(注释里写明原因与三种失败的写法,免得日后被"简化"回去)。
⇒ 教训:`run` 的 `:in` 用句柄比用路径稳。
- Bump to 0.87.2.
- Bump to 0.87.2.
## 0.87.1 - 2026-09-23
- **修掉「幽灵安装」:装完报成功、`use` 永远找不到(1.0 发布前审查的 F2 + F3)**。
症状最恶劣的一条链:`install` 返回 rc=0 → 账本记着已装 → `installed` 列得出来
→ 而 `use` 报 "Could not find",且 `verify` 还会说「✓ 没有不一致」,用户毫无线索。
两个独立成因,必须一起修:
① **`provides` 缺失**(META6 里它是可选字段)。旧代码照样写出一份 provides 为**空**
的标准 META6.json → CUR::Installation 一个模块都不挂。全量索引实测:zef 索引
8060 条**一条不缺**,缺的全在 **rea(138 条 / 53 个发行版)与 cpan(8 条,
真正的 Perl 5 包)** —— 正好是 `.pm` 时代的老包。provides 被写成字符串
(`"provides": "lib/Foo.rakumod"`)也会被归一成空,同一个坑。
② **`.pm` 不被认作模块源**。`provides-with-paths` / `scan-provides` 只试
`.rakumod` / `.pm6`;而 `.pm` 是 Rakudo 正式支持的模块扩展名(会打 deprecation
提示,`inst#` 下同样能加载),早期 Perl 6 时代是主流写法。实证 `Cache::Memcached`
的模块就是 `lib/Cache/Memcached.pm`,旧代码完全认不出。
- 修法:
· 新增 `RakuPM::Distribution.provides-from-disk` —— **有声明就用声明(且只保留磁盘上
真有文件的项,与旧行为逐字节一致);声明为空、或声明的模块一个都对不上磁盘,
就扫 `lib/` 反推**。`lib/` 是发行版自己的内容,比一份缺失/写坏的元数据字段更可信。
· 模块源扩展名收敛成**唯一一份** `@MODULE-EXTS`(`.rakumod` / `.pm6` / `.pm`),
跨模块经 `module-extensions` 取(`which` 也改成走它)——原先同一份清单在两处各写了
一遍、都漏了 `.pm`,正是这次漏的根。
· **幽灵安装护栏**:反推仍拿不到任何模块、而 `lib/` 里确有文件时,`Store.put`
**拒绝入库**(宁可不装,也不写一个 `use` 不到东西的空条目)。判定放在落盘**之前**,
所以拒绝时连目录都不会建。「只发 bin/资源、不含模块」的发行版仍按合法处理。
· `Store` 的 `META.json`(`Store.provides-of` / `show-store` 靠它按模块名反查发行版)
与账本条目都改记**生效的** provides,而不是索引行那份空声明——否则 `installed`
按模块名查、`autoremove` 的反向依赖图对这批包全部失灵。
· `verify` / `doctor` 新增第 ⑥ 段检查(issue kind = `ghost-provides`):揪出**已经装在
机器上**的历史幽灵条目(store 的标准 META6.json 零声明、而 `lib/` 里有模块源),
并给出可执行的修复建议。`--fix` 修不了这类,只能重装(重装会走上面的反推)。
- 回归守卫 `t/ghost-provides.t`(37 条):反推判定矩阵 + `.pm` 识别 + 三类畸形元数据的
**真装 + 外部 raku 进程 `use`** 端到端 + 负控(拒绝入库、零残留)+ 事务性(失败不毁
已有账本)+ verify 能查出历史幽灵。变异验证:去掉反推 → 20 红;去掉 `.pm` → 9 红;
在 `Query` 里重新内联扩展名清单 → 静态守卫 1 红。
- 实测(真实生态,均为索引里缺 `provides` 的 REA 老包):`Cache::Memcached`(模块是
`.pm`)、`Compress::Snappy`、`C::Parser`、`Automata::Cellular`、`Clifford`、`Chess`、
`App::P6Dx` 现在都装得上且 provides 正确,前四者经外部 `raku` 进程 `use` 验证通过;
修复前它们全是幽灵安装。
- **修掉门禁自身的一个死锁**(本轮顺带发现;不修就跑不完 `xt/` 那一半):
`tools/run-suite.raku` 的 `run-one` 在 `run(..., :out, :err)` 之后是**先 slurp
stdout、再 slurp stderr**。子进程若把某一条流写满管道缓冲(Windows 上约 64KB),
它会阻塞在 write 上,而父进程永远等不到另一条流的 EOF → **双向死锁**。表现极具
欺骗性:**CPU 0%、无子进程、无限等待**,看起来只是「套件跑得慢」。实测 `xt/http.t`
的 stderr 有 258KB(stdout 仅 1.4KB),`--only=xt` 因此卡在它上面 23 分钟一动不动,
而单独跑该测试只要 **1 秒**、18/18 全绿。修法:新增 `capture()` helper **并发**抽干
两条流,5 处捕获点全部改走它(预热/编译/静态检查那几处同款隐患一并消除)。
- Bump to 0.87.1.
## 0.87.0 - 2026-09-23
- **安全修复:发行版名 / 版本号未消毒导致任意路径穿越写文件(1.0 发布前审查的 P0)**。
`name` 与 `version` 来自**包自己的 META6.json / 生态索引行**(发布者可控),却被
直接当作路径分量拼进 `store//`、下载缓存、日志目录。修复前
`Store` 里只做 `subst('::','-')`(旧私有方法 `!safe-name` —— 名字叫 safe,其实
**只处理模块分隔符、不处理路径结构字符**),而同一条路径的 `$version` 连这层都没有。
实测载荷 `"name": "../../../../../../tmp/PWNED"` 会把整个 store 条目写到 target
**之外**(逃到 `C:\Users\\tmp\PWNED`)——即「发布一个包就能往用户机器任意位置
写文件」。`version` 同样可穿越。
- 修法:新增**唯一的净化入口** `RakuPM::Fs.path-component`,并在**全部 6 处**拼路径的
地方接入(不只 Store):
① `Store.!safe-name` / `path-for`(name **与** version);
② `Client::Tester` 的测试日志目录 `log//`;
③ `Repository::Ecosystem` 的**下载缓存**路径(索引行可控,发生在 store 之前);
④ `Installer` 的临时文件名 `rakupm-adopt---.json`;
⑤ `Author` 的分发归档名与发布落点目录;
⑥ `Client::Git.!fetch-source` 的 `fetch --to` 输出目录。
另在 `Author.new` 加一处**冗余**防御(那里的模块名已被正则校验,留着防日后放宽正则)。
另加**第二道防线** `RakuPM::Fs.assert-under`(落盘前断言路径在根之内;入口会随代码
演进增多,漏做一处就立刻变成明确报错,而不是一次静默的越界写入)。
净化规则:拒 `/` 与 `\`、恰为 `.` / `..`、控制字符、结尾点;模块分隔符 `::` 与单个
冒号 `:` 一律归一到 `-`。**选择 die 而不是静默清理**:静默把 `../x` 改成 `x` 会让不同
发行版撞进同一目录(互相覆盖),比拒绝更危险。
- **向后兼容**:对一切合法输入,`path-component` 与旧 `subst('::','-')` **输出逐字节相同**
(已用真实索引全量验证:25117 条条目 0 误拒;本机 33 个已装 store 条目 0 映射差异)
—— 历史 store 条目仍然可达,不会被判成"未入库"。唯一例外是名字含**单个冒号**的
5 个发行版(`App:Ralc`、`Slang:Date` 等),0.87.0 起也归一到 `-`:它们在 Windows 上
本来就无法作为目录名(`mkdir App:Ralc` 会写成文件 `App` 的备用数据流 ADS),归一等于修好。
- 新增 `t/path-safety.t`(68 条):净化函数接受/拒绝矩阵、`assert-under`(含"同前缀兄弟
目录"这个经典假绿点)、以及**端到端**——真装恶意发行版,断言被拒 + **按旧实现推算的
逃逸落点确实不存在**。另含**正控**(合法名必须确实装成),避免"安装链根本没走到拼路径
那一步"造成否定断言假绿。已做变异验证:去掉 version 净化 → 11 条红;让 `path-component`
变成空操作 → 45 条红。
- Bump to 0.87.0.
## 0.86.2 - 2026-09-23
- **Windows 上给原生 MD5 增加 Git 候选**:与 macOS / Linux 不同,**Windows 不自带
`libcrypto`**,全新装机常常一个候选都命中不了(那时就走纯 Raku + 清单兜底)。但
**Git for Windows 几乎人人都有**,且自带一份 `libcrypto-3-x64.dll` —— 故补三个候选:
默认安装目录的绝对路径(`C:\Program Files\Git\mingw64\bin\…` 与 `(x86)` 那一支)
与裸名 `libcrypto-3-x64.dll`(Git 的 `mingw64\bin` 在 PATH 上时命中)。
实测本机两条都能加载(`abc` 向量正确)。探测失败照旧跳过,对其它平台零影响。
- `.files.json` 清单缓存**保留**(按评估结论):它是「原生库不可用」时的兜底 ——
在那种机器上把 `verify` 从约 10.9s 压到约 1.9s。
- 文档:`MANUAL{,.en}.md` 的 verify 说明补充「Windows 上还会尝试 Git 自带的那份」。
## 0.86.1 - 2026-09-23
- 文档补充(无功能变更):说明**原生 MD5 不需要用户额外安装 OpenSSL**。
`libcrypto` 是**系统库**、不是 raku-pm 的新依赖(META6 没加依赖、zef 也不会装它);
设计是「有就用、没有就自动回退纯 Raku」,两边指纹逐位一致,只是快慢不同。
各平台实际情况写进 `MANUAL{,.en}.md` 与 `RakuPM::MD5` 头注释:
macOS 系统自带 `libcrypto.dylib`(LibreSSL)、Linux 多数发行版随 curl/git 依赖装好
`libcrypto.so.3`、Windows 不保证(开发机上那份实测是 LibreSSL 3.8.2,随某软件带进来)。
## 0.86.0 - 2026-09-23
- **MD5 改用原生实现(NativeCall 绑系统 libcrypto),并把「系统命令」那一层删掉**。
起因:用户提出「CPU 密集还是要系统程序来做」。实测证实:
- NativeCall 绑 `libcrypto` 的 `MD5()`:store 里 296 个文件(2.1 MB)**0.246s**
—— 比纯 Raku 的 10.97s 快 **44 倍**(RFC 向量与纯 Raku 逐位一致)。
- `verify` 强制全量哈希 **11.7s → 2.86s**(此时剩下的主要是 ~1s 的 CLI/Client 启动
与读文件,哈希已不再是瓶颈)。
- 这条路比「起系统命令」更快,而且**不需要**为每个文件起一个进程、也不需要处理
certutil / md5 -q / md5sum 三种输出格式与平台命令名差异。
- 实现:`RakuPM::MD5` 里写多个候选 sub(`libcrypto` / `libcrypto.so.3` / `.so.1.1` /
`libcrypto.so` / `libcrypto.3.dylib` / `libcrypto.dylib` —— 库名在 NativeCall 里必须是
编译期字面量),**首次调用时逐个试并用 RFC 1321 的 `abc` 向量自校验**,结果缓存;
全部不可用才落到纯 Raku。刻意不在 import 期探测(否则每条命令的启动都要付一次库加载)。
新增 `native-md5-available()` 供诊断 / 测试。
- **删除**:`RakuPM::MD5` 的 `system-md5` 与 `parse-md5-bytes`、`RakuPM::Platform` 的
`md5-command` 与 `parse-md5-output`、`md5-file` 的 64KB 阈值分支,以及
`t/platform.t` 的第 4 节与 `t/md5.t` 的两个系统命令场景。
连带消失的还有:中文 Windows 上 certutil 输出 GBK 导致整条路径静默失效那类坑。
- `md5-file` 现在只剩两条路:**原生 → 纯 Raku**,任何环境都算得出来,且没有平台分支。
`RAKUPM_MD5=raku` 仍可强制纯 Raku(调试 / 对照)。
- `t/md5.t` 改为钉住新不变量:`native-md5-available()` 返回 Bool 且不抛错;
**原生可用时其结果必须与纯 Raku 逐位一致**(6 组样本含空串 / 中文);`RAKUPM_MD5=raku`
能强制回纯 Raku。刻意**不**断言「必须有原生」—— 那会让没装库的机器假红。
- 一个 Raku 语法坑(记档):sub 名不能以 `-数字` 结尾(`md5-libcrypto-3` 会被解析成
减法);另外 `List.fmt('%02x')` 的**默认分隔符是 `", "`**,拼 hex 必须写
`.fmt('%02x', '')`,否则自校验会全部误判失败(这个坑让原生探测一开始全灭)。
- 文档:`MANUAL{,.en}` 的 verify 说明、`architecture{,.en}` 的「verify 内容校验」一节
都重写为三步迭代史(0.84.3 修静默失效 → 0.85.0 少算 → 0.86.0 换实现)。
- 说明:`.files.json` 清单缓存**保留**(原生库不是每台机器都有,它是那种情况下的兜底)。
## 0.85.0 - 2026-09-23
- **`verify` 的内容校验加「清单旁车」,少算而不是算得快**(首次 ~13s → 之后 **1.8s**)。
背景:内容校验要逐文件算 MD5,纯 Raku MD5 只有约 0.16 MB/s(store 296 个文件 10.7s),
而**并行 / 多进程实测都救不了**(8 线程仅 1.18x;多进程被每个 ~1s 的启动成本吃掉)。
做法:每个 store 条目在根目录存一份 `.files.json`,记下每个文件的 (size, mtime, md5)。
下次校验时 **size + mtime 都没变的文件直接复用上次算好的 md5**,只重算变过 / 新增的文件。
- 它是与 `.digest` / `build.json` 同类的「缓存旁车」:`!fingerprint` 跳过它(否则指纹自指),
Installer 布置 site 时只处理 lib/resources/bin 等子目录、不会把它带进安装点。
- **取舍明说**:「改了内容、又把 size 与 mtime 都保持原样」的改动查不出来。
要无条件全量哈希:`RAKUPM_VERIFY_NOCACHE=1`,或删掉条目里的 `.files.json`。
- 实测:`raku-pm verify` 首次 20.4s(建清单)→ 命中 **1.8s**;逃生门强制全量 11.7s。
- `t/store-verify.t` 新增 5 条场景(4 → 9):清单落盘且可解析、命中时确实复用旧 md5
(连同取舍一起断言)、`RAKUPM_VERIFY_NOCACHE=1` 强制全量后能正确判出「已修改」。
已做变异验证(让清单永不命中 → 第 8 条必红)。
- 文档:`MANUAL{,.en}.md` 的 `verify` 条目补清单缓存与逃生门说明;
`architecture{,.en}.md` 性能节补 0.85.0 的对策与实测数据。
## 0.84.3 - 2026-09-23
- **性能修复:中文 Windows 上「大文件走系统 md5」这条加速路径静默失效**(`verify` 19.3s → 13.5s)。
`certutil -hashfile x MD5` 在中文 Windows 上输出的是 **GBK**(实测原始字节:
`MD5 \xb5\xc4 C:\…\licenses.json \xb9\xfe\xcf\xa3:`),而 `$proc.out.slurp(:close)`
默认按 UTF-8 解码 → 抛 "Malformed UTF-8" → 被外层 `try` 吞掉 → 返回空 →
**静默回退纯 Raku**。后果:每个 ≥64KB 的文件都白起一次 certutil(0.25s)再跑一遍
慢速纯 Raku(331KB 实测 2.07s),合计 2.5s —— 比直接用纯 Raku 还慢。
修法:按字节读(`slurp(:close, :bin)`)+ latin-1 解码(字节↔字符一一对应,
非 UTF-8 字节不会抛错),抽成可单测的 `RakuPM::MD5.parse-md5-bytes(Blob, $os)`;
`system-md5` 外层仍包 `try` 兜底(解析出意外只意味着「这次系统命令不可用」,
回退纯 Raku,绝不把异常抛给 Store 的指纹计算)。
实测:331KB 文件 `md5-file` 2.5s → **0.25s**;store 全量 md5 17.6s → **10.7s**
(平均 59.5 → 36.1ms/文件,>300ms 的慢文件清零);`raku-pm verify` 19.3s → **13.5s**;
两条路径(系统命令 / 纯 Raku)哈希值一致。
- `t/md5.t` 新增场景:用 latin-1 造出真实的 GBK 字节(`chr(0xB5)` 等)断言
`parse-md5-bytes` 仍能解析出摘要。**原有的用例抓不到这个坑**——它只断言「不抛异常 +
结果一致」,而静默回退恰好两条都满足。已做变异验证(把 latin-1 改回 utf-8 → 必红)。
- **记档(实测否定的方向)**:**并行算 store 指纹没有收益**——8 线程只有 1.1x
(19.8s → 17.5s,结果与串行逐位一致)。纯 Raku 的哈希计算在 MoarVM 上不能真并行
(对照:并行解析索引 JSON 有效,4.32s → 3.31s,因为那条含 IO)。剩下的 10.7s 是
纯 Raku MD5 约 0.16 MB/s 的固有成本,加并发救不了。
- `architecture{,.en}.md` 性能章节新增「又一处:`verify` 的内容校验」小节。
## 0.84.2 - 2026-09-23
- **性能修复:索引缓存被重复解析一遍**(`search` / `info` 等查询命令快约 4.3s,约 −36%)。
`Ecosystem.!read-fetched-at` 原先 `slurp + from-json` **整个索引缓存**,只为读一个
`_rakupm_t` 时间戳;而紧接着 `!load-rows` 又会把同一份文件完整解析一遍 —— 每次 refresh
白花一倍解析时间。索引缓存实测最大 24MB(rea 23.6 / zef 12.6 / cpan 2.6 MB),三份
合计约 4.2s。
改为**只扫文件头尾各 4KB**(时间戳总落在最前或最后几个字节:rea / cpan 在偏移 5,
zef 在距末尾 24 字节 —— 两种键序都在用,故头尾都扫),解码用 latin-1
(截断多字节字符也不会抛错)。
实测(同机 min-of-3~5):`refresh(rea)` 5.86s → **3.19s**;
`raku-pm search sqlite --offline` 12.03s → **7.71s**;
`raku-pm info SQLite --offline` 11.12s → **6.64s**。
`version` / `list` / `installed`(不读索引)不受影响。
- 顺带记档:真正的**匹配**只占 0.36s(零命中与满命中的关键词耗时相同)—— 这类命令的
大头从来是「把 40MB 索引读进来解析」,不是搜索算法。
- `xt/http.t` 新增场景:把 `_rakupm_t` 写在缓存文件**末尾**(zef 那种键序)、且构造
> 8KB 的缓存(小了就没有区分力 —— 头部一次 read 就读完了),断言仍能识别新鲜度。
已做变异验证:临时禁用尾部扫描 → 该条必红。
- 架构文档(`architecture{,.en}.md`)新增「慢命令的真正大头:索引加载」小节,含
实测数据、修复方式,以及「查询用精简索引」方案**评估后不做**的理由。
## 0.84.1 - 2026-09-23
- 文档修订(无代码变更):`docs/nav-map.md` 的「哪些可砍」清单里有两条判断错了,已改正。
- `NativeLib`:原写「非 native 包用不到,可降级为按需」—— **错**。它不只是 `raku-pm native`
这个只读查询命令的后端,**安装链每次都调它**查待装发行版声明的 `:from` 本地库与
`:from` 外部命令依赖(`Client/Git.rakumod` 的 `!check-native-libs` /
`!check-external-deps`,除非 `--no-native-check`)。而且实测:
热 precomp 缓存下 `require RakuPM::NativeLib`(含 Platform + Fs)约 **276ms**,
`require` 任意轻量模块(`Fs`)约 **275ms**,空 raku 启动约 **4ms** ——
那 ~270ms 是「Raku 加载任意模块」的固定开销,NativeLib 并不额外付费;
故「改成按需 require」收益≈0。**撤销该建议**。
- `Author`:原写「建议 `raku-pm author ...` 独立命名空间」—— 模块面其实**早已独立**
(独立文件 + 独立命名空间 + CLI 懒加载 `(require RakuPM::Author)`),扎眼的只是命令面;
0.84.0 已按「导航档」处理(只加 `help <组名>` 分组总览,命令面一个没动)。
## 0.84.0 - 2026-09-23
- **help 增加「分组总览」这一层(纯导航;命令面零变化)**:新增 `raku-pm help <组名>`
(如 `raku-pm help author`,中文标题同样认:`raku-pm help 作者侧`),列出该组的定位
一句话 + 组内命令与各自用法行。此前 `help <命令>` 只能看单个命令、`help` 摘要只给
名字,中间缺「这组是干嘛的、我要不要看」——只装别人包的用户看到索引里 `作者侧` 那 8 个
发布相关命令平铺着,容易以为必须搞懂它们才能用。
- `raku-pm help` 摘要给 `作者侧` / `开发门禁` 两个重型组各加一句注记
(「发布/打包用,只装别人包不需要」),并在末尾提示 `raku-pm help <组名>`。
索引完备性不变:命令与别名照旧一个不漏(`t/help.t` + `xt/cli-output-stability.t` 守着)。
- 未知参数报错细化:「没有名为 'x' 的命令」→「没有名为 'x' 的命令或分组」,并多给
一行分组入口提示。
- 命令面**未动**:`raku-pm publish` / `bump` / `check` … 仍是一等公民,CliSpec 未改。
本次只在 Help 新增 `help-group` / `%GROUP-HELP` / `%GROUP-NOTE`,以及 CLI 的
`print-help` 里一条「不是命令就试分组名」的分支(命令优先)。
- 文档:README×2 / MANUAL×2 的 help 分级说明补上 `<组名>` 一层;nav-map 行数更新。
## 0.83.0 - 2026-09-23
- **self-upgrade 从三条路径收敛到两条(不兼容变更)**:删掉「当前这份在 git 仓库里 →
就地 `git pull` 你的工作树」那条(删 `!upgrade-from-git` 与 `self-upgrade` 的 `:$source`
形参)。它与远端路径本就是同一件事的两种取法,且是唯一会改动用户工作树的分支;删后
在源码仓库里跑也走默认远端(gitee),行为更可预期。现在只剩:① `--from-dir <目录>`
(本地源码目录,也是两阶段自举第二阶段的入口);② git 远端(`--from ` 或默认上游)。
注:原先同在这条路上的 `!in-git-repo` **没删**,只是搬到 `Client::Git` ——
`install-from-dir` 用它判断本地目录是不是 git 工作树以记录 HEAD 提交号(与 self-upgrade 无关)。
- 收尾消息去重:把原先在三个方法里各抄一遍的「跑 `raku-pm version` 确认 / Windows
PowerShell 的 PATHEXT 一次性提示」抽成 `!after-upgrade-hint(Str $how)`,供
`!install-self-from` 与远端回退分支共用。Windows PS1 提示原先只挂在被删的那条路上,
现在所有升级路径的 Windows 用户都能看到。
- CLI:`self-upgrade` 分支去掉 `my $src = raku-pm-git-root()` 与 `source => $src`
(`raku-pm-git-root()` 仍被 `preflight` 使用,保留);`Help` 的 self-upgrade 文案去掉
「在 git 仓库里跑则 pull 当前仓库」。
- 测试:`xt/self-mgmt.t` 删「--from 覆盖 source」与「checkout 走 git 分支」两个场景,
并去掉随之为死的 `capture` 助手与已不再使用的 `use RakuPM::Distribution`;
`t/named-arg-wiring.t` 删掉「② self-upgrade 的 source 接线」文本级断言(形参已不存在)。
- 文档:`MANUAL{,.en}.md` 的 self-upgrade 来源优先级改为两条;`nav-map.md` 更新行数与
「已简化」标注,并把 0.82.0 漏改的「双 HTTP 后端」一并更正为单后端;`architecture{,.en}.md`
的用法行补 `--from-dir`,并修正 role 列表(`Reporter` → `Query`、4 → 5 个 role,含 `Flusher`);
`Repository/Matching.rakumod` 与 `Client.rakumod` 里残留的 `Reporter` 注释改为 `Query`。
- 不兼容提示:`self-upgrade` 不再有 `source` 语义(该形参已删);在源码仓库里跑不再自动
`git pull` 你的工作树。
- Bump to 0.83.0.
## 0.82.0 - 2026-09-23
- **HTTP 层改成单后端,只保留 curl —— 移除可插拔后端机制(不兼容变更)。** 删掉纯 Raku 的
`HTTP::Tiny` 兜底后端(`lib/RakuPM/HTTP/HTTPTiny.rakumod`)与整套插件化:`%REGISTRY` /
`register-backend` / `backend-names` / 后端顺序 `@.order` / `RAKUPM_HTTP_BACKEND` 环境变量 /
「加载失败自动回退下一个」。`RakuPM::HTTP` 现在只持**一个**后端实例(默认首次用到时懒建
`RakuPM::HTTP::Tinyish`,底层系统 curl),外加指数退避重试 —— 与 `post-form` / `post-json`
(本就直调 curl)统一到 curl 一条路径。
- 测试替身改经构造器 `:backend` 注入(原先靠 `register-backend` + `RAKUPM_HTTP_BACKEND` 选后端)。
- 相应改测试:`t/http-wire.t` 删 HTTPTiny 用例与「回退」用例(只留 Tinyish 的条件 GET + 超时,
plan 10→4;并修掉会让进程**永久挂住**的假服务器 accept 线程 —— 收尾加哨兵连接唤醒 accept,
实测从挂死 10 分钟+ 降到 ~5s 退出);`t/http-debug.t` 改用 `:backend` 注入、
删后端顺序/覆盖来源/回退类断言(plan 13→9);`xt/http.t` 删 `backend-names` / `order` 断言、
改注入替身。
- 依赖与声明:`META6.json` 去掉 `HTTP::Tiny` 依赖与 `RakuPM::HTTP::HTTPTiny` provides,
description 的「HTTP 客户端插件化」改为「单后端 HTTP 客户端(curl)」。
- 具名实参白名单:`tools/NamedArgCheck.rakumod` 补 `:host` / `:port`
(`IO::Socket::INET.new(:host,:port)` 连客户端 —— `t/http-wire.t` 的哨兵连接用)。
- 文档:`README{,.en}.md` 依赖列表、`MANUAL{,.en}.md` §16 与依赖/环境变量表、
`architecture{,.en}.md` 的 HTTP 章节(删「双后端 / 回退 / HTTP::Tiny 调用侧加闸」段)、
`nav-map.md`、`Repository/Ecosystem.rakumod` 注释、内部 `CODEBUDDY.md` 同步。
- 不兼容提示:`RAKUPM_HTTP_BACKEND` 不再被识别;`RakuPM::HTTP.register-backend` 已移除。
- Bump to 0.82.0.
## 0.81.0 - 2026-09-23
- **测试进度改为「按当前文件的用例」显示(配合 0.80.0 的串行测试)。** 串行下同一时刻只跑
一个文件,故进度行从「第几个文件 / 共几个」改为当前文件的
「`测试 <文件号>/<文件数> <文件名> <用例完成数>/<用例总数> 用例 pct%`」——通过实时解析
该文件正在输出的 TAP(顶层 `ok`/`not ok` 计数 + `1..N` plan)持续刷新;plan 尚未出现时
(如 `done-testing` 在结尾才打印)只显示已完成用例数、不显示百分比。
- `Client::Tester`:`!progress-text`(文件级)→ `!progress-file-line`(用例级),新增
`!tap-counters`(只数顶层 TAP,缩进的子测试不计入);`!run-one-test-async` 增
`:$progress`/`:file-index`/`:file-total`/`:$on-case`,在 stdout tap 里按新 TAP 增量回调重画;
`!run-tests-serial` 经 `:on-case` 把「行状态 + 文件序号」传下去,跑完统一擦除。
- `xt/tester-progress.t`:夹具改用前置 `plan 1`,新增「`1/1 用例`」断言,注释改用例级口径。
- 文档:`architecture{,.en}.md` 的 ⑤「实时进度」描述改为用例级格式。
## 0.80.0 - 2026-09-23
- **测试改为串行;移除测试并发机制 —— `--test-jobs` 开关随之取消(CLI 不兼容变更)。**
此前默认按 CPU 核数并行跑 `t/`,而为了让并发不误伤安装,还得额外配「串行预热 precomp」+
「失败用例串行复验」两道安全网。这份复杂度对单机教学场景不划算(且 `self-upgrade` 早已
单独强制串行),故整体移除:`Client::Tester` 删掉 worker 池(Channel 信号量 + Lock)、
`!warmup-precomp` 与 `RAKUPM_TEST_JOBS`,退化为纯串行循环 `!run-tests-serial`;运行行由
「N 个文件,并发 J」改为「N 个文件,串行」。
**保留**「失败用例串行复验」(防上游 flaky,如 `Concurrent::Stack 1.3` stress 连跑 10 挂 3)、
超时看门狗与实时进度。
- 整条 CLI 链同步移除 `test-jobs`:`CliSpec`(install / reinstall / smoke / test 四处旗标集)、
`InstallOptions`(去掉 `$.test-jobs` 字段与 describe 行)、`CLI` / `Client` / `Git` /
`SelfManager`(去掉 `:test-jobs` 形参与透传)、`Help`(去掉 `[--test-jobs=N]`)。
- 测试调整:删 `xt/tester-warmup.t`(专测已移除的预热);`xt/tester-async.t` 删并发场景
(保留失败首行 + 日志 + 超时);`xt/tester-defaults.t` 场景1 改断言「3 个文件,串行」;
`t/install-options.t` 去掉 test-jobs 断言(plan 15→11)。
- 文档:`MANUAL{,.en}.md` §15 的「测试并发」小节改写为串行版、环境变量表去掉
`RAKUPM_TEST_JOBS` 与 `RAKUPM_NO_PRECOMP_WARMUP`;`architecture{,.en}.md` 的「测试并发」
章节改写并更正因果;`nav-map.md` 同步(并纠正其中「套件并集会死锁」的错误说法 —— 套件
现为默认并行 + 预热 + 复验)。
- 不兼容提示:`raku-pm install X --test-jobs=N` 现在报「未知选项」(原先被接受)。
## 0.79.4 - 2026-09-22
- **重命名 `Client::Reporter` → `Client::Query` 并拆分写命令(可读性重构)**:
原 `Reporter` 名字误导(里面其实装着 `uninstall` / `rollback` / `autoremove`
等写命令)。现在这三个写命令挪回 `Client`(编排器),`Query` role 从此只承载
只读查询 + 诊断(`list` / `which` / `why` / `depends` / `doctor` / `verify` 等)
+ native 库检查。所有 `use`/`does` 引用、测试、文档同步更新,`provides` 登记
同步改名。零功能损失(方法逐字搬移,已通过加载 + 三个相关测试 + CLI 冒烟)。
- **新增 `docs/nav-map.md`(贡献者速查)**:「改动 X → 去哪个文件」排障地图 +
端到端流程 + 模块分层与「哪些可砍」清单,并登记进发布包公开白名单。
## 0.79.3 - 2026-09-22
- **内部笔记迁移 + 脱敏**:把根目录的 `CODEBUDDY.md` / `REFACTOR-ISSUES.md` /
`COMMANDS-WIKI.md` 三份开发笔记搬进 `docs/internal/`,与公开文档(`MANUAL` /
`architecture` / `quickstart` …)分开;`.distignore` 相应改为排除 `docs/internal/`
整棵子树(继续不进发布包)。笔记里的 gitee 个人账号 `skyter10086` 统一替换为
`<发布者>`(包身份 `auth` 属已发布元数据,保持不变)。
CLI 行为与发布内容均不变。
- 版本:0.79.2 → 0.79.3(patch:内部文档 relocation + 脱敏;无功能变更)。
## 0.79.2 - 2026-09-22
- **修:发布包(sdist)里混进了内部文档** —— `docs/refactor-roadmap.md`(内部重构路线图)
与 `docs/github-issue-templates.md` 会随 `dist` / `publish --remote` 一起发到公开生态。
`.distignore` 的注释早就写明「内部开发文档不宜发到公开生态」,但它的匹配是**按路径**的:
根目录的 `REFACTOR-ISSUES.md` 一直被排除着,而它的 **docs/ 版本是后来新增/搬家**的 ——
没人补 `.distignore`,于是静默进包(实测 `tar -tzf RakuPM-0.79.1.tar.gz` 清单里确有其名)。
现已把两条补进 `.distignore`,并实测重打包后的 tar 清单只剩 8 份公开文档。
- **新增门禁:`check-shipped-docs`(挂进 `check-docs`,故 `--docs-only` / `--precommit` /
全量三种口径都跑)** —— 这类泄漏**不会让任何测试变红**(它只在发布那一刻生效),
所以必须前移到门禁:
- 「**哪些 .md 允许进发布包**」改成**显式白名单**:新增一个 .md 时二者必居其一 ——
登记进 `tools/run-suite.raku` 的 `@public`(=决定公开),或写进 `.distignore`
(=内部文件);否则门禁红。
- 白名单条目必须存在(防清单过期)。
- **刻意不复刻「`.distignore` 条目是否指向存在的文件」**:那条规则 `Author` 打包时
已经在对「写了却匹配不到任何文件」的条目喷 ⚠(见 `xt/dist-ignore.t` 场景 3),
同一规则实现两遍=日后改一处忘一处。
- **变异验证**(不会变红的门禁等于没有):① 去掉新排除项=复现原 bug → 门禁如期变红并
点名 `docs/refactor-roadmap.md`;② 新增一个未登记的 `docs/*.md` → 变红;
③ 删掉/注释掉门禁接线 → `t/run-suite-docs.t` 变红。为此在 `t/run-suite-docs.t`
加了一条**接线断言**(plan 3 → 4)—— 且断言写成「有【一整行】恰好是这个调用」而非
`.contains`:后者对**被注释掉**的接线同样成立,变异时实测漏判过。
- **文档纠错:`--help` 在 PowerShell 下是否可用**。此前 4 处措辞称「`raku-pm --help`
会被 zef wrapper 吞掉」,实测**已不准确**:本机 wrapper 是 raku-pm 自己生成的
`site/bin/raku-pm.ps1`,它把 `@args` 原样转发,`raku-pm --help` / `-h` 都正常进帮助
(退出码 0)。现改为明确区分**遗留 zef 包装器**(`raku-pm.exe`,其 `*%` 会吃掉
`--help`,且会在安装/升级时被改名 `.zef-old`)与 **raku-pm 自生成的包装器**,
并保留「推荐一律用 `raku-pm help`」这条跨形态都成立的建议。
- 版本:0.79.1 → 0.79.2(patch:修发布泄漏 + 加门禁 + 文档纠错;CLI 行为不变)。
## 0.79.1 - 2026-09-22
- **文档:把「安装时的测试并发会不会影响安装成败」写清**(`MANUAL{,.en}.md` §15)。
新增要问的三层:① **并发边界** —— 只有「跑测试」这一段并发,解析/下载/构建/入
store/进暂存/晋升**全串行**(`Client.!install-chain-inner` 是 `for @order -> $dist`),
跨进程靠文件锁;② **两道安全网** —— 串行预热 precomp(消掉并发首编竞态 → 假的
`===SORRY!===`)、失败串行复验一次(过滤假失败与上游 flaky),并写明复验日志
`<名字>.flake.log` 不覆盖首跑日志;③ **残余风险表**(如实列出)—— 复验也挂会
**整链回滚**、上游 flaky 与并发无关、**测试自己用共享资源时预热防不了**(这类包
`--test-jobs=1` 能过),以及超时误判;附四个旋钮与三个环境变量。
`architecture{,.en}.md` 的「测试并发」节同步补上这层因果(面向设计)。
- **文档:新增「启动耗时构成」与**「评估后**不做**」**的优化记录**
(`architecture{,.en}.md`)。实测(7 轮取最小值):raku 启动 **0.148s**、
加载**第一个** precomp 模块 **+0.20s(固定成本)**、之后**每多**一个仅
**+0.011s**;`RakuPM::CLI` 全量 +0.231s,`RakuPM::Client` 整条重模块图**只多 ~80ms**。
结论:**precomp 模块的加载成本与模块行数无关**(`RakuPM::Installer` 1205 行与零依赖的
`RakuPM::Net` 单独加载耗时几乎相同),贵的始终是**加载机制本身**。
- 据此**评估后决定不做**「拆开 / 惰性化 `RakuPM::Client` 重模块图」:收益上限仅
~80ms,且只对纯查询命令有意义(对 install 是 0.1% 噪声);而要真做到「读命令不加载
Installer/Builder/Tester」,必须**去掉 `Client` 的属性类型约束 + 改惰性访问器** ——
等于拿掉项目里最安全攸关那个类(整树事务化安装)的编译期保证;且它**本来就是惰性
`require` + 已 precomp**,与 0.79.0 那次「bin 从不可缓存变可缓存」的架构性收益不同。
- 顺带排除一个可疑项:CLI 顶层 `use JSON::Fast`(只为 `version` 读一次 META6)看似可省,
但 `RakuPM::Spec` 已 `use JSON::Fast`,**传递引入不可避免**,边际成本仅 **0.020s**。
- 无代码/行为变更(纯文档),版本 0.79.0 → 0.79.1(patch)。
## 0.79.0 - 2026-09-22
- **CLI 主体从 bin 搬进 `RakuPM::CLI`、bin 退化成薄壳 —— 启动实测量快 0.6~0.8s**。
根本原因:**bin 脚本不享受 precomp**,raku 每次运行都要把它整体编译一遍;实测
`raku -c bin/raku-pm.raku` 要 1.267s(空脚本基线才 0.516s),即光编译 bin 就 ~0.75s。
把逻辑搬进普通模块后,每次只编译那十几行薄壳,其余走模块的 precomp 缓存。
实测(同机同命令,Git Bash):`version` 1.72s → **1.14s**、`help` 1.91s → **1.10s**;
PowerShell 端重构后约 **1.00s**。raku 解释器自身启动那 ~0.55s 无法消除。
- **验收三条**:① 全量门禁 `--no-xt` 全绿;② **xt/ 全部 39 个文件全绿**;
③ **新旧 CLI 输出逐字节相同** —— 用 `git archive` 取 0.78.1 旧树,对 12 个纯输出场景
(version / help / help all / help <命令> / 4 种 shell 补全 / 裸调用 / --help / 未知命令)
比对 stdout+stderr+退出码的 md5:**12/12 完全相同**。
- 附带修复:入口刻意叫 `run-cli` 而**不是** `run` —— 叫 `run` 会把 Raku 内建的 `run()`
(本模块内多处用它起子进程)遮蔽掉,令 `preflight` 转调 run-suite 变成
`raku-pm raku tools/run-suite.raku …`。这个坑由 `xt/preflight.t` 抓到(`--no-xt` 口径
覆盖不到,说明大重构必须额外跑 xt/)。
- `t/named-arg-wiring.t` 的文本级断言改指 `lib/RakuPM/CLI.rakumod`(代码搬了家);
provides 新增 `RakuPM::CLI`;架构文档(中英)的层级图补上该模块与「薄壳」说明。
- 版本:0.78.1 → 0.79.0(minor:新增模块 / 磁盘布局变化;CLI 行为不变)。
## 0.78.1 - 2026-09-22
- **测试失败的病因不再被外壳藏住**:Raku 编译失败时 stderr 是嵌套的 ——
`===SORRY!=== Error while compiling <测试文件>` / `<模块.rakumod>` 两行外壳之后,
才是真正的病因(如 `Could not find Foo:ver<…>` / `Version must be specified`)。
旧实现前台**只印首行**,等于把病因完全藏起来;而日志目录在安装失败【回滚】时又会
被清空,用户事后无从查起。现在 `RakuPM::Client::Tester.!failure-lines` 会跳过开头
连续的 SORRY 外壳行,把最内层病因印最多 3 行;**普通失败仍只印首行**(契约不变)。
(起因:实测某个上游包安装失败时,前台只剩一句无信息量的
`Error while compiling t/…`,真正原因被两层外壳挡在后面。)
- 测试:`xt/tester-defaults.t` 新增场景 4(2 断言)锁住新行为,并做变异验证
(短路新逻辑后场景 4 如期变红);既有「只印首行」契约由场景 2 继续钉住。
## 0.78.0 - 2026-09-22
- **help 分级显示**:`raku-pm help` 默认只给紧凑命令索引(一屏内:命令按分组 +
别名速查),`raku-pm help <命令>` 看单个命令的用法,`raku-pm help all` 给完整手册。
原先 help 正文 358 行一次性刷屏、前面的内容被顶走,想看还得往回翻。
- **help 正文抽到新模块 `RakuPM::Help`**:命令清单 / 分组 / 别名一律从 CliSpec 现取
并自动排版 —— 新增命令时索引自动跟上,不会「加了命令忘了写 help」。模块可 precomp,
bin 从 1304 行瘦到 ~970 行;bin 侧惰性 require(只有 help 才加载)。
- **bash wrapper 启动优化**:入口路径改为安装时写死(与 .bat/.ps1 同款),不再每次
调用 `ls | sort -V | tail` 挑最新版本 —— 那一句在 Windows/Git Bash 下要 ~0.7s。
写死路径不在时(store 被 clean 清掉)才退回探测。
- 测试:新增 `t/help.t`(18 断言);`xt/cli-output-stability.t` 的 help 完备性断言
(首行含「用法」+ 全命令 + 全别名 + 幂等)继续成立;`t/installer-shell-templates.t`
补「入口写死 + fallback」断言。
## 0.77.0 - 2026-09-22
- 新增中文/英文用户手册 `docs/MANUAL.md` 与 `docs/MANUAL.en.md`:覆盖安装、命令参考、依赖解析、原子安装与世代回滚、HTTP 层、作者工作流与发布等。
- 修正手册中的两处不准确:`RAKUPM_AUTHOR_AUTH` 默认值应为 `zef:skyter10086`(非 `gitee:`);`RAKUPM_ECOSYSTEM` 实为「仅兜底覆盖、非默认单一索引」,默认内置 zef+cpan+rea+本地仓库。
## 0.76.0 - 2026-09-22
- **发布准备:auth 命名空间 `gitee:` → `zef:`,bump 0.75.2 → 0.76.0(minor)**:
为发布到 fez / 42.zef.pm 生态,META6 的 `auth` 须为 `zef:<用户名>`(官方要求),
原 `gitee:skyter10086` 会在上传时被拒或归属错误。故:
- `META6.json` auth → `zef:skyter10086`,version → 0.76.0;
- 两份 README 的 Identity 示例、help 文本、`Author.rakumod` 的 `RAKUPM_AUTHOR_AUTH`
默认值、相关 xt 测试 fixture 一并同步为 `zef:`;
- `check-preflight.t` 的 env auth 同步,门禁 auth 一致性检查恢复 0 警告。
(fez 用户名即 `skyter10086`;若注册时该名被占用需改用他名,发布前再改一次 auth 即可。)
## 0.75.2 - 2026-09-22
- **把「个人痕迹」钉成门禁**(`tools/run-suite.raku` 新增 `check-personal-traces`):
0.75.1 清掉痕迹后,我在写 0.75.1 的 Changes 时**又把那两个字符串原样写了回来**
(一次是 0.75.0 的条目说「已清理某用户名」、一次是 0.75.1 的条目写「本机用户名 →
中性占位」)—— 语义上是报告修复,效果上是换个地方把它发到公开生态。同类错误
连续复发两次,说明「发版前记得 grep」不可靠,故改为机器守:
- 三条规则:① 本机工作区绝对路径;② 历史遗留的短用户名;③ `Users\<名字>` 且名字
不在中性占位白名单(`user`/`foo`/`x`/`tester`/`example` …)里(疑似真实用户名)。
- **扫描范围 = 会进发布包的内容**:跳过 dot 目录与 `blib`,并跳过 `.distignore`
列出的条目 —— 否则内部文档里的路径会把门禁误报成红(不会误伤不发布的东西)。
- 在 `--docs-only` / `--precommit` / 全量门禁**三种口径下都会跑**:起初写成了
`xt/` 测试,但 `--precommit` 与 `--no-xt` 都不跑 xt/,等于日常提交路径上根本没生效,
故挪进门禁脚本本体。
- 门禁脚本里的黑名单**必须是拼接构造**(`'z' ~ 'by'`):写字面量的话,这个检查
文件自己就成了泄漏源(`tools/` 同样进 sdist)。
- 变异验证 4 项全通过:三条规则各注入一次都变红(命中哪条也精确对上),
另验证「被 `.distignore` 排除的文件不参与扫描」(不误报),之后全部还原。
- 修正 0.75.1 条目里对「门禁落在哪」的描述(当时写的是新增 xt 测试;实际落在门禁脚本)。
- 文档:`docs/architecture.md`(与 `.en.md`)在「跑测试只用 run-suite」那条补上它带的
三道静态闸门(具名实参 / 文档纪律 / 个人痕迹);门禁脚本自身的 `--help` 同步。
- 顺带记一个 Raku 坑(本版本首次踩到):**`Array.append` 不展平 `Seq`** —— `.map` 的
返回值会被当成**单个元素**塞进去(stringify 成内容拼接,空 Seq 即空串),
症状是「明明命中 3 处、打印出来却是 3 行空白」。必须显式写 `.append(@x.map({…}).List)`。
- 版本纪律:0.75.1 → **0.75.2**(patch:清理 + 门禁加固,无用户可见行为变更)。
## 0.75.1 - 2026-09-22
- **发布前审计补漏:清掉发布包里最后的个人痕迹**(0.75.0 的 sdist 内容级扫描发现):
- `t/builder-quote.t` / `t/known-test-failure.t` / `t/known-failure-nojson.t` /
`lib/RakuPM/Builder.rakumod` 里的本机 Windows 用户名 → 中性占位 `user`(共 10 处)。
**只换用户名、反斜杠数量一字不动** —— 那几个夹具是 `is-known-windows-test-failure`
的测试数据,其「单/双反斜杠形态」正是 0.36.16 线上漏匹配事故的回归锚点,
动它必须原样保留转义形态(改完 `t/known-test-failure.t` 等三个测试全绿即证)。
- `Changes` 里 0.74.1 那条「清理用户名痕迹」的说明本身又把待清理的原始用户名写了
出来 —— 等于换个方式把它发到公开生态。改写为中性描述(`/home/` 之类),
语义不变、不再出现该字符串。
- 复核:两类痕迹在全仓残留均为 **0 处**(0.75.2 起由门禁脚本自动守着,见下条)。
- 版本纪律:0.75.0 → **0.75.1**(patch:清理 / 无行为变更)。
## 0.75.0 - 2026-09-22
- **新增 `.distignore` 自定义排除清单**(`RakuPM::Author`):0.74.1 的隐藏项过滤只挡
「以 `.` 开头」的东西,挡不住【名字正常的内部文件】(AI 指令 / 内部路线图 / 内部手册
这类:文件本身没毛病,只是不该发到公开生态)。现在在发行版根放一个 `.distignore`
即可逐条排除 —— **文件保留在 git 仓库、只是不进 sdist**。
- 语法刻意做小、可预测:一行一条相对路径(`/` 分隔);空行与 `#` 开头的行忽略;
**行尾注释也认**(`#` 前须有空白,与 `.gitignore` 同款语义 —— 不剥的话
「`FOO.md # 内部文档`」会被当成含空格的长文件名而**静默失配**);行首 `./`、
行尾多余 `/` 剥掉;匹配 = 相对路径恰好相等或以 `<条目>/` 开头(目录条目覆盖整棵
子树);**不支持通配符**(避免「以为排除了、其实没有」的静默事故)。
- **安全网**:列了却匹配不到任何文件的条目会 `⚠` 提示到 stderr(不阻断打包)——
拼错文件名会立刻暴露,而不是静默把文件发出去。
- `.distignore` 自身以 `.` 开头,按既有规则天然不进包。未提供该文件时零行为变化。
- 本仓库据此排除 `CODEBUDDY.md` / `REFACTOR-ISSUES.md` / `COMMANDS-WIKI.md`
(**仍留在仓库**,仅不进发布包);`repo/` 是 `t/store-lock.t` 的夹具,明确保留。
- 测试:`t/author-dist.t` 新增场景 7(4 断言,plan 15→19:文件条目 / 行尾注释与空行 /
目录条目覆盖子树 / 常规文件照常进包且 `.distignore` 自身不进包);新增
`xt/dist-ignore.t`(6 断言,真实 CLI 子进程:排除生效、未列出的照常进包、
`.distignore` 不进包、**拼错条目会提示到 stderr**)。两条排除分支均做过变异验证
(摘掉过滤器 / 摘掉行尾注释剥离 → 测试变红)。
- 实测真实仓库:sdist 197 个成员,三份内部文档全部排除、无任何 `.` 开头条目,
LICENSE / repo / META6 / README 均在。
- 文档:`README.md` / `README.en.md` 的「打包内容」各补 `.distignore` 说明;
`dist` 的 `--help` 注明「排除 `.` 开头的隐藏项 + 可选 `.distignore` 清单」。
- 版本纪律:0.74.1 → **0.75.0**(minor:新能力)。
## 0.74.1 - 2026-09-22
- **修 `dist` 打包泄漏:不再把隐藏项(`.` 开头)打进 sdist。** `dist` 是按【目录】扫描
(走 `RakuPM::Fs.walk-files`,只跳 `.git/.hg/.svn/.precomp/blib`),**不读 `.gitignore`**
—— 于是仓库里被 gitignore 的私有目录(如 `.workbuddy/` 工作记忆、`.workflow/` CI 配置)
会被打进发布包发到公开生态(发布前审计实测到)。现于打包这一层额外排除「相对发行版根、
路径中含隐藏分量」的一切条目(`.workbuddy` / `.workflow` / `.vscode` / `.idea` /
`.gitignore` / `.gitattributes` …)。
【为什么在 dist 层过滤、而不改 `walk-files` 的默认跳过集】后者还用于内容指纹(`Store`)
与 provides 扫描;改默认集会让既有包指纹漂移,只在打包层收紧影响面最小。
另把 `raku-pm.lock` 也一并排除(作者在发行版目录里跑过 `raku-pm install` 时,cwd 会留下
这个本地锁文件,属于本地状态、不该进发布包)。
- **新增 `LICENSE`(Apache License 2.0 全文)**:`META6.json` 的 `license` 由 `Artistic-2.0`
改为 `Apache-2.0`(此前声明了 license 却一直没有 LICENSE 文件)。
- **清理个人用户名痕迹**:历史文档与注释里遗留的个人路径 / 用户名一律换成中性占位
(`/home/`、`/Home/`、` 的 X 现场` 之类),涉及
`Changes` / `docs/architecture.md`(与 `.en.md`) / `CODEBUDDY.md` /
`lib/RakuPM/{Builder,Installer}.rakumod` / `t/builder-quote.t` / `xt/rollback-restore.t`。
- 测试:`t/author-dist.t` 增场景 6(4 断言)——断言 `.workbuddy/` / `.workflow/` /
根级 dot 文件都不进包、而常规文件照常进包(plan 10 → 14)。
- 文档:两份 README 的「打包内容」补一句「排除任何 `.` 开头的隐藏项」。
- 版本纪律:0.74.0 → **0.74.1**(patch:修 bug);双语文档版本示例同步。
## 0.74.0 - 2026-09-22
- **测试阶段实时进度**(治「跑测试时长时间黑屏、像卡死」):`RakuPM::Client::Tester`
现在在**终端**上刷实时进度 —— 串行预热阶段逐文件 `预热 i/N … <文件>`;并发跑测试
阶段逐文件 `测试 i/N pct% ✓/✗ <文件>`。都用 `\r` 原地重画(不刷屏、跑完擦掉)。
- 那两段「长静默」正源于此:预热是逐文件串行 `-c` 编译(全程无输出),并发执行又是
「结果先收集、跑完才汇总打印」。现在终端始终有动静,一眼看出没卡死。
- **只碰 stderr,且仅当 stderr 是终端时才显示** —— 管道 / 重定向 / 测试捕获自动
静默,`stdout` 与日志文件**零影响**(既有断言不受影响)。`RAKUPM_TEST_PROGRESS`
设 `0` 强制关、设 `1` 强制开。
- 顺带修:`· 运行测试` / `· 预热预编译` 两行打印后补 `$*OUT.flush`,否则块缓冲会把
标题压到进度行之后(看着像「先跑后说」)。
- 新增 `xt/tester-progress.t`(9 断言):起子进程分别以 `RAKUPM_TEST_PROGRESS=1/0` 跑
一次真实安装,断言 =1 时 stderr 出现「预热 i/N」「测试 i/N pct%」且能到 100%、=0 时
完全静默;两种情况下 `stdout` 的 `✓` 汇总都不变。
- 文档:bin help 的测试阶段说明、`docs/architecture.md`(与 `.en.md`)测试要点各补一条
实时进度说明(含 `RAKUPM_TEST_PROGRESS`)。
- **修真 bug:发行版 wrapper 下敲裸 `raku-pm` 不显示帮助。** 三份 wrapper 模板都会隐式
注入 `--target=<前缀>`,而「无位置参数」分支把所有非 help 旗标一律当「未知选项」→
裸 `raku-pm` 实际得到的是 `未知选项:--target` 并退出 1(真人在终端里敲 `raku-pm`
就是这个形态)。现改为 **公共旗标(`target` / `no-lock` / `promote-bin` /
`no-promote-bin` / `offline` / `no-color`)一律放行** —— 它们都不蕴含某个命令;只有
真·拼错的旗标才报错(`--dry` 这类每命令旗标无命令时仍报错)。
`t/cli-flags.t` 补 5 条断言做回归(公共旗标无命令 → 帮助;`--dry` 无命令 → 仍报错)。
- 版本纪律:0.73.0 → **0.74.0**(minor:新能力);双语文档版本示例同步。
## 0.73.0 - 2026-09-22
- **UI 美化**(`RakuPM::UI` 新增三个能力,全部 **rich-gated**:非 TTY / 管道 / 测试捕获下
一律退化为原样文本,故 help 的纯文本输出与改动前**逐字节一致**,既有断言零影响):
- `ui-banner()`:`raku-pm help` / 无参数时在终端顶部打印 ANSI Shadow 艺术字标题
(`raku-pm`,6 行、每行 60 列等宽)+ 一行副标题;非 TTY 返回空串。
- `ui-colorize-help()`:help 正文**命令名染粗体绿、`--旗标` 染黄、`<占位符>` 染品红**;
并把 `--no-color` 的判定提前到 help 分支**之前**(`--no-color help` 与 `help --no-color` 都生效)。
- `ui-tree-branch()` / `ui-tree-child()`:树形连接符(rich=Unicode `├─ └─ │`;非 rich=ASCII `` |- `- | ``)。
- **依赖树显示改造**:`rdepends` 从扁平列表改为**反向依赖树**(自目标包向上长:谁依赖我 →
谁又依赖它们 …,每层标出「经 <模块> + 约束」,`%seen` 防环);`depends` 已有的树改用同一套
UI helper 并上色(已装=绿、待装=黄★、找不到=红、重复=暗)。树连接符随之 rich / 非 rich 自适配。
- **修 `bump --to=X.Y.Z` 不生效的真缺陷**:bin 的 bump 派发此前只认【位置参数】里的版本号,
`--to=` 被静默忽略、退化成 patch(而 help 明确写了 `--to=`;`Author.bump` 本就支持 `:to`)。
现从 `%opts` 读取,位置参数仍优先。
- 版本纪律:0.72.7 → **0.73.0**(minor:新能力);双语文档版本示例同步。
## 0.72.7 - 2026-09-21
- **Issue 1(文档拆分与快速上手)**:README 瘦身,深度设计内容迁到 `docs/`。
- `docs/architecture.md`(中英 `docs/architecture.en.md`):接收架构总览 / HTTP 双后端 /
安装五阶段 / 原子安装与世代回滚 / 反向依赖与回收 / 校验 / 仓库数组 / 本地目录布局 /
版本管理与多版本 / 锁文件 / 跨进程文件锁 / 测试并发 / 缓存回收 / flush / 与 zef 对应 /
核心设计思想——**仅位置迁移,内容未删减**。
- `docs/quickstart.md`(中英 `docs/quickstart.en.md`):新增 5 分钟最小上手
(install 一个包 + `use` 加载,复用真实输出)。
- README 只留介绍 / 安装 / 最小用法(快速开始)/ 生产侧 / 测试 / 版本约定 /
已实现 vs 待实现;顶部加文档导航,章节空缺处加单行回指 `docs/architecture.md`。
- 中英文两份 README 同步,顶部互链保持。
- **文档纪律门禁同步扩展**(`tools/run-suite.raku` 的 `check-docs`):版本示例
`RakuPM:ver<>` 的校验范围从 `README.md`/`README.en.md` 扩展到递归扫描 `docs/*.md`;
README 两文件维持「必须有版本示例且一致」,docs 下改为「若有则必须一致」(不强制必须有)。
**变异验证**:故意在 `docs/architecture.md` 注入 `RakuPM:ver<0.0.0>`,门禁变红并定位到该文件,
还原后恢复绿——符合 Issue 1「门禁仍能拦版本漂移」的验收。
## 0.72.6 - 2026-09-21
- **Issue 2(CLI 元数据独立模块验收)收口**:0.67.0 已把命令元数据抽成
`RakuPM::CliSpec`(旗标/别名/分组/写锁/联网维度一处定义,bin 只做解析与派发)。
本次补 `xt/cli-output-stability.t` 锁 `raku-pm help` 与
`raku-pm completions`(bash/zsh/fish/pwsh)的**稳定性不变量**:
· 幂等性——连跑两次字节一致(防 hash 顺序 / 日期 / 路径等非确定因素漂出),
· 完备性——CliSpec 的每一个规范命令、别名、旗标都出现在产物里(真正的回归风险
是「CliSpec 改了但补全脚本没跟上」,而非某次措辞变化),
· 结构片段——四种 shell 各自的关键引导串都在。
不冻结 84KB 易碎快照,改守不变量;以后动 CliSpec 或补全文案不会误红、动错才会红。
- **Issue 3(用户面消息层收口)收口**:0.68.0 已建 `RakuPM::Message`(warn/error/
hint/fail-die + 退出码)与 `RakuPM::UI`(排版/色彩/TTY 降级)分工、Message 不自带
ANSI。本次把 `lib/RakuPM/Client/Reporter.rakumod` 里**绕开** Message 层的 stderr
端提示收口:doctor 外部命令缺失、native 放弃安装、`uninstall` 反向依赖守卫、
verify 外部命令依赖、`browse` 打开失败等处,从裸 `note "⚠ …"` / `die "⚠ …"`
改为统一走 `msg-warn` / `msg-hint` / `fail-die`,与 bin 的 CLI 校验报错共用同一套
「措辞 + 退出码」出口。报告体里的 `⚠` 表格注释行属排版、保持走 stdout 不变。
## 0.72.5 - 2026-09-21
- 延续 0.72.4:installed META6.json 的 `build-depends` / `test-depends` 也做保真写回。
原先这两字段同样没写(raku-pm 自己的构建/测试依赖图读不到准确约束)。
现抽出 `!faithful-depends($dist-dep, $source, $field)` 助手,`depends` /
`build-depends` / `test-depends` 三档复用:优先照搬源 META6 对应字段
(数组 / 平铺 Hash / depends..requires 嵌套结构都原样),缺源时退回
`module:ver` 至少保版本。新增回归 t/store-depends-fidelity.t(12 例)。
## 0.72.4 - 2026-09-21
- 修 installed META6.json 的 `depends` 失真(md:Store.rakumod `!meta6-for`):
原先写 `depends => $dist.depends.keys.sort.List`,只写模块名、把版本/作者
约束全丢了,已安装包的 `depends` 退化成裸名(`String::Utils` 而非
`String::Utils:ver<0.0.35+>:auth`)。后果:① 读 installed 元数据的
包/测试拿到错误依赖——真实案例 Identity::Utils 0.0.29 的
"can we get dependencies from identity" 测试(test 176)因此 `not ok`;
② 污染 raku-pm 自己的反向依赖图(Installer.!depends-of 把约束读成 `*`)。
改为直接照搬源 META6 的 `depends`(zef 同款原样保留),`$source` 缺失时
退回「`module:ver`」至少保版本。新增回归测试 `t/store-depends-fidelity.t`。
## 0.72.3 - 2026-09-21
- 修 `upgrade` 的潜在崩溃(Issue 8 的 golden e2e 才暴露):`upgrade` 方法本身带有
`Str :$version` 形参,其内部写 `self.install($name, :$version($target), :$dry)`
时,`:$version($target)` 被 Raku 解析成「短配对 `(version => $version)`(取 upgrade
自己的空 `$version`)再拿 `($target)` 去调用它」,于是抛
`No such method 'CALL-ME' for invocant of type 'Pair'`,`upgrade` 整条链路直接死。
之前没有任何测试走过「带版本约束的升级」,故该隐患一直潜伏。改为显式 fat-comma
`self.install($name, version => $target, :$dry)` 即修。
- 新增两条跨阶段 golden e2e(`xt/`,Issue 8):
· `xt/golden-install-upgrade-rollback.t`:走 Client 命令层把
`install V1 → upgrade → rollback` 串起来,并在每个阶段用子进程真实 `use` 该包、
读出其版本常量,证明「安装状态」与「实际加载到的版本」一致(generations.t 没做的
硬验证);顺带抓出上面的 upgrade 崩溃。
· `xt/golden-verify-fix.t`:走 Client 命令层的 `verify-all(:fix)` / `verify-all()`,
制造「CUR 有、账本被清空」的半截状态,证明 `--fix` 真的把账本自动补记回来、而无
`--fix` 只报不动(引擎级 t/verify-consistency.t 不覆盖命令层这三段分支)。
## 0.72.2 - 2026-09-21
- 修 self-upgrade 自装时会跑本项目 `t/`,并行首跑偶发触发 lib 预编译缓存竞态,
前台先打出一个吓人但良性的 `✗ <某测试>.t`(Tester 的串行复验会吸收、升级仍成功)。
self-upgrade 很低频,故在 `!install-self-from` / `!upgrade-from-git` / 回退
`install-from-git` 三处给安装链传 `:test-jobs(1)`,让自装测试串行跑,从根上
消除该竞态、不再出现假失败噪音。正常安装行为不变。
## 0.72.1 - 2026-09-21
- 修 uninstall 反向依赖守卫的判定反转:`Reporter.uninstall` 里拦截「被依赖的包」的
逻辑原本写在 `unless $whole`(非整包卸载时才拦),但 `$whole=True` 才是「会删光
全部版本」该拦的情形,导致 `uninstall` 一个仍被其它包依赖的整包时不会拒绝、留
下坏掉的依赖。改为 `if $whole`,与注释语义一致;部分版本卸载(还有其它版本在)
仍跳过检查,行为不变。顺带让 `xt/rdepends.t`(孤儿回收)通过。
- 对齐 `xt/search-spec.t` 的 `installed` 断言到表格渲染格式(`| Dual | 0.9.2 |` 用
竖线分隔包名与版本,不再空格相连),与门禁内 `t/query-installed-store.t` 同款;
这是实现表格化后测试漂移,非实现 bug。
## 0.72.0 - 2026-09-21
- (Issue 5)新增 `why` 依赖解析诊断命令:把「为什么选了这个版本 / 冲突的双方约束」
讲清楚,不再黑盒。正常安装行为不受影响(仍走 `resolve-all`,会 die)。
- 引擎(`RakuPM::Resolver.explain-all`):**不 die** 的诊断入口 —— 跑解析 +
拓扑排序,冲突时把结构化冲突存进 `%!last-conflict` 并返回 `ok=False`,与
fatal 路径共用同一份数据,避免两套实现漂移。返回每模块的 chosen / 有效约束 /
全部候选(含来源仓库、是否满足约束、淘汰原因)/ 完整请求链(谁因什么要求过它)。
- 展示(`RakuPM::Client::Reporter.show-why`):冲突时先友好展开「双方约束 +
最后选定的版本」,再列冲突模块的候选细节(帮用户决定 pin 哪边);非冲突失败
(如找不到模块)转述 fatal 路径原始报错;普通解析逐模块打印选中版本、来源仓库、
有效约束、候选表与请求链。
- 协议面:CliSpec 登记 `why`(命令组 `packages`、别名 `wh`、支持 `--pin`、
需要网络);`bin/raku-pm.raku` 新增派发与 help 文本。
- 来源仓库可追溯:`%!chosen-repo` 与 `chosen-detail` 让「这个版本从哪个仓库
解析出来」可回答(Distribution 模型本身不带仓库信息)。
- 修复:`!find-best-satisfying-all` 返回类型声明漏改 —— `!materialize-first`
改为返回 `{dist, repo}` 哈希后,其声明仍是 `RakuPM::Distribution`,冲突时求
交集改选会触发类型检查失败(`Type check failed for return value`)。已改为
`Hash`,`t/conflict-msg.t` 的「'*' 与 '0.0.24' 交集非空不报冲突」场景恢复。
## 0.71.0 - 2026-09-21
- (Issue 6)HTTP 层可观察性增强:排障时不再黑盒。
- `RakuPM::HTTP` 新增 `:$debug` 构造器开关 + 环境变量 `RAKUPM_HTTP_DEBUG`
(1/true/yes/on 算开)双入口;**默认关**,关时本层零新增输出(不刷屏,
且排障行前缀 ` · http:` 刻意不含 ⚠ / [RakuPM],不会触发 zef install 的
噪音判定)。开时把排障信息打到 stderr。
- 可观测维度:① 后端顺序及来源(默认 `tinyish→httptiny` / 显式指定 /
`RAKUPM_HTTP_BACKEND` 覆盖 —— 覆盖来源一并报出,解决「为什么这次用的是
httptiny」查不清);② 实际使用的后端、落到哪个后端(加载失败 / 不可用 /
请求失败都报);③ 全部后端不可用时报「所有后端都失败:[…]」;④ 条件 GET
命中缓存统一为「304 命中本地缓存(Not Modified,不重新下载)」;⑤ 每个出口
入口报出 `离线=ON/OFF`,与 `RakuPM::Net.offline()` 口径对齐(HTTP / 生态索引
都经本门面被 `assert-online` 硬拦,git 走的是另一条子进程出口、不在本层)。
- curl 上传路径(post-form / post-json)同样报出离线状态与 `RAKUPM_CURL`
覆盖来源。
- 新增 `t/http-debug.t`(纯假后端、零网络)覆盖:默认不刷屏、各维度可观测、
离线仍被硬拦(行为不变)。
- 修一个隐藏事故:`RAKUPM_HTTP_DEBUG` 的「开」判定原写成 `eq any(<1 true yes on>)`
返回 Junction,赋给 `Bool` 属性会触发 Type check 失败 —— 会让**设了该环境变量的
每次 `RakuPM::HTTP.new` 都抛异常**;改为 `.first + eq` 返回普通 Bool。
## 0.70.0 - 2026-09-21
- (Issue 9)扩展 `RakuPM::InstallOptions` 收敛安装参数:把尚未纳入该类的零散策略
统一收入安装链单一载体,清除残余的逐层布尔/Int 抄写。
- 新增字段 `pins`(依赖钉版 Map)、`test-jobs`(并发测试数)、`test-timeout`(单测
看门狗秒数)、`recursive`(卸载递归)、`keep-store`(卸载保留 store 快照)。
- `test-jobs` / `test-timeout` 改由 `InstallOptions` 承载,经 `$opts` 进入
`Tester`(env `RAKUPM_TEST_JOBS` / `RAKUPM_TEST_TIMEOUT` 仍作默认值回退);
移除了 `Client` 上的 `has Int $.test-jobs` / `$.test-timeout` 实例属性及
`set-test-jobs` / `set-test-timeout` 访问器。
- 卸载侧 `keep-store` 经 `$opts` 进入 `Installer.uninstall`(`Reporter.uninstall`
与 `reinstall` 内部回滚均改用 `:opts(...)`);保留 `:$keep-store` 兼容形参以
兼容直调内部方法与 `bin` 入口。
- 新增 `t/install-options.t` 覆盖默认值 / `with()` 不可变派生 / `describe()` 仅列
非默认字段。验收:现有安装命令行为不变(`t/install-opts.t`、`xt/install-opts.t`、
`t/uninstall-*.t`、`xt/tester-async.t` 全绿),新增策略只改 `InstallOptions` 一处。
## 0.69.0 - 2026-09-21
- bump 顺带同步文档里的身份版本示例(`:ver<旧版>` → 新版):此前 bump 只改
META6 + Changes,README 里的示例会与 META6 漂移,而文档纪律门禁要求两者一致,
于是每次 bump 后都要手工补两份 README,漏了就一提交就红。现在扫描发行版目录下
所有 .md(递归,跳过隐藏目录),只替换【恰好等于旧版本】的实例 —— 历史引用
(如「0.60.0 起支持 X」)原样保留,改掉反而成了错误信息。新增 --no-sync-docs
可关闭该步(发布第三方包、其文档里的身份示例不属于本包时用)。
- 修 Changes 新条目的插入位置:文件以 `# 标题` 开头时插到标题【之后】。此前是
无条件前插,会把文件标题挤到新条目下面,形成颠倒结构(既有测试只断言
`contains('## X.Y.Z')`、不看位置,故长期未暴露)。
- 新增 `raku-pm preflight`:一条命令给出「能不能提交」的明确结论。内部转调
`tools/run-suite.raku --no-xt`(文档纪律 + 具名实参 + t/),与预提交钩子
【共用同一份门禁实现】,不另起一套以免漂移;`--filter` / `--jobs` 可透传。
- bin 帮助文案补 `preflight` 与本条新增旗标。
- 测试:新增 `t/author-bump-docs.t`(14 断言:同步 / 不动历史引用 / --no-sync-docs /
--dry 一个字节不改);`t/author-bump.t` 补 2 条钉住 Changes 插入位置;
新增 `xt/preflight.t`(4 断言:转调链路 + 退出码透传)。
## 0.68.0 - 2026-09-21
- **用户面消息层收口(新增 `RakuPM::Message`)**:把散在 `bin/raku-pm.raku` 里的
**24 处裸 `die`**、以及无参数分支 / 顶层 `CATCH` / 注销失败等处的裸 `note`,统一到
`usage-die`(用法错)/ `fail-die`(业务失败)/ `msg-error` / `msg-warn` / `msg-hint`
五个出口;裸数字退出码(9 处 `exit 0` + 4 处三目)改用 `EXIT-OK` / `EXIT-FAIL` 常量。
- **与 `RakuPM::UI` 划清边界**:Message 只管「说什么、以什么身份说、用什么退出码」,
排版与上色归 UI;**Message 不得自己拼 ANSI 转义** —— `t/message.t` 有断言钉住这一点,
否则「非 TTY / NO_COLOR 自动降级」会从这里漏出一个口子。
- **退出码语义显式化并写进文档**:`0` 成功 / `1` 失败(业务失败与用法错共用)。
历史上两者都是 1(`die` 的默认退出码),外部脚本已在依赖,**本轮刻意不拆成 2**;
将来真要区分,只改 `EXIT-*` 与 `usage-die` / `fail-die` 两处 —— 这正是把它们从
20 多处裸写法里显式化的意义。失败信息一律走 stderr,正常数据才走 stdout。
- **零行为变更(本轮是收口,不是改文案)**:用固定输入探针对重构前后比对,
24 个探针 × 3 类产物(stdout / stderr / 退出码)= **72 个文件全部 IDENTICAL**。
- **变异验证**:破坏三条契约(退出码改成 2 / 给消息加前缀 / 改写 die 的文本),
确认 `t/message.t` 每次都恰好在【预期的那条断言】上变红,再整份原文写回复原。
- 新增 `t/message.t`(8 断言):退出码常量、die 类消息逐字原样、msg-* 只写 stderr
且不加上前缀、输出不含 ANSI。
## 0.67.0 - 2026-09-21
- **CLI 命令元数据抽成 `RakuPM::CliSpec`**(重构:纯搬家,不改行为):原先埋在
`bin/raku-pm.raku` 里的 `%CMD_FLAGS` / `%CMD_ALIASES` / 公共旗标清单 /
`allowed-flags` / `completion-script` 全部迁入新模块,bin 只保留解析 / 校验 / 派发。
收益:新增一个旗标从「改三处」变成「改一处」,且这张表可以直接单测
(新增 `t/cli-spec.t`),不必再起子进程跑 CLI。
- **补上原先散落的三类声明**:`writes-prefix`(会写前缀的命令,原 bin 的
`%prefix-writer`)、`needs-network`(可能联网的命令,原 `%net-cmd`)、
`command-group`(命令分组,新增;`t/cli-spec.t` 断言每个命令都有分组,防止新增
命令时漏登)。
- **等价性验证(逐字节)**:用 `git archive` 导出重构前的代码树,对同一组固定输入
(help 族 / 四种 shell 补全 / 各类校验报错,17 个探针 × 3 类产物 = 51 个文件)
逐字节比对,**全部 IDENTICAL**。
- **变异验证**:故意破坏 `CliSpec` 三处(漏登命令分组 / 别名指向不存在的命令 /
白名单漏掉公共旗标),确认 `t/cli-spec.t` 每次都恰好在【预期的那条断言】上变红,再复原。
- 坑:`canonical` 这个名字与 `RakuPM::Prefix` 已导出的同名 sub 冲突 —— bin 同时
`use` 两者会直接编译失败(`Cannot import symbol '&canonical'`)→ 改名 `canonical-command`。
## 0.66.0 - 2026-09-21
- **self-upgrade 改为两阶段自举**:先把源码取到 git-cache 判断是否需要升级,再改用
【取到的新版代码】执行安装(子进程跑 `/bin/raku-pm.raku` 并 `-I /lib`),
旧版只负责「取源码 + 决定要不要升」。这样即便新版改了安装链行为(测试门禁、依赖解析、
wrapper 写法…),也不会出现「新版自己能装自己、旧版装它必失败」的升不上去死循环。
自举子进程凭父进程下发的锁凭证免锁放行(`RAKUPM_LOCK_TOKEN` + 锁旁 `.token`),
因此必须透传 `--target`(写锁按 target 定位)。自举不可用时(老源码没有 `bin/`、
或子进程失败)自动回退到原路径,不会把升级卡死。
- **新增 `self-upgrade --from-dir <目录>`**:直接用本地源码目录升级(不 clone、不联网);
它也是自举第二阶段的入口。
- **修 Windows 上 `--from <本地仓库路径>` 直接失败**:`git-cache-key` 过去只处理
`git@` / `https` / `http` / `git` 四种协议,本地路径原样当作目录名 —— Windows 盘符里的
冒号在文件名中非法,`git clone` 报 `Invalid argument`。现在本地路径统一落到 `local/`
前缀下并去掉冒号。
- 新增 `xt/self-upgrade-bootstrap.t`:覆盖自举命令行拼装(尤其 `--target` 透传)、
`--from-dir` 端到端、真自举端到端(本地 git 仓库 + 在新源码 bin 里埋标记,验证
「执行安装的确实是新版代码」,并验证父进程持锁时子进程不会自锁)。
## 0.65.0 - 2026-09-21
- **终端展示层 `RakuPM::UI`(零依赖)**:新增色彩与表格渲染,`installed` / `list` / `info`
三个命令改用表格 + 色彩输出。中文按【显示宽度】对齐(`uniprop('East_Asian_Width')`,
宽 / 全角 = 2 列),表格不再因中文而歪列。
- **自动降级**:非 TTY(管道 / 重定向 / 测试捕获)或设了 `NO_COLOR` / `RAKUPM_NO_COLOR`
时一律输出纯文本(无 ANSI 转义、边框退化为 ASCII)—— 测试断言不受影响;并新增
`--no-color` 公共旗标,供在终端里临时关闭。
- **刻意不引入第三方依赖**:评估过 `Terminal::Table`(功能最强,但带
ANSIcolor / WCWidth / Text::Tabs 依赖链)后仍选择自写 —— raku-pm 是包管理器,
界面美化不该变成它自己的安装负担,也不能让门禁依赖「本机 site 恰好装了某几个包」。
`RakuPM::UI` 只用 Raku 内置能力。
- 新增 `t/ui.t`(显示宽度 / 对齐 / 降级 / 富文本);`t/query-installed-store.t`
的断言随表格化更新。
## 0.64.6 - 2026-09-21
- **`rakulib-shadowing` 自检不再误报单仓库多版本**:`CliCheck::check-rakulib-shadowing`
改为只在「跨仓库出现不同 RakuPM 版本」(链序遮蔽风险)时报警;同一 `site` 仓库内的
CUR 多版本共存(Rakudo 永远按 semver 取最高,无害)现在静默。自装/升级后 site/ 残留
旧版不再触发噪音提示。`t/rakulib-shadow.t` 新增单仓库多版本静默用例。
## 0.64.5 - 2026-09-21
- **uninstall 支持 semver 版本范围**:`--version` 的版本筛选从「精确匹配」升级为「semver 约束匹配」,
除精确版本外,现支持区间(`<1.0`、`>=1.0, <2.0`)、通配(`*`)与 zef 风格 `0.2+`;并新增
npm/cargo 风格的 `^` / `~` 糖语法(`^1.0` → `>=1.0.0, <2.0.0`、`~1.2` → `>=1.2.0, <1.3.0`),
加进 `RakuPM::Version` 后依赖解析同样可用。承载点:`Installer.uninstall` 的 CUR/store 筛选与
`Ledger.remove` 的版本过滤统一走 `RakuPM::Version.satisfies`;`Reporter.uninstall` 的反向依赖守卫
改为「范围删光全部已装版本时」才拦截(单版本/部分范围跳过,与原整包语义一致)。向后兼容:
精确版本串行为不变。
### 针对外部审计文档(HARDWARE_FIX_SUGGESTIONS,H1–H12)的修复总览
某份第三方审计文档基于 **v0.52.1**(commit `55a3f83`)扫描 raku-pm,列了 H1–H12 共 12
条「硬伤 / 缺陷 / 建议」。我们在 0.52.2 → 0.54.3 之间逐条对照**当前源码**核对,并修复了其中
5 条(H3 / H6 / H10 / H11 为代码级新增;H7 为现有行为已满足)。下表是总览,
方便读者一眼看清「针对那份文档改了哪些、哪些是有意不修」。
| 文档 ID | 主题 | 状态 | 落点版本 / 说明 |
|---------|------|------|----------------|
| **H3** | `clean` / `gc` 会回收被保留世代引用的版本 | ✅ 已修 | 0.52.2(`Generations.protected-versions` + `Cleaner` 来源 4) |
| **H6** | Windows 已知测试失败豁免写死 `Log::Async` | ✅ 已修 | 0.54.2(规则外置 `known-windows-test-failures.json`) |
| **H7** | `uninstall` 无反向依赖检查 | ✅ 已缓解 | 现有行为:`Reporter.uninstall` 已算 `reverse-dependencies` 并报告下游伤害(非本轮新增) |
| **H10** | `install` 期不主动读 `build.json` | ✅ 已修 | 0.54.3(装完读构建痕迹,提前告警) |
| **H11** | 无 `RAKULIB` 遮蔽自检 | ✅ 已修 | 0.54.1(`RakuPM::CliCheck` 冷启动告警) |
| H1 | CUR 预编译不含 core 链 | ⬜ 不修(语言级) | Rakudo 限制,需上游配合 |
| H2 | HTTP 超时悬挂线程杀不掉 | ⬜ 不修(语言级) | Raku 无线程取消原语;超时闸已加,仅无法回收线程 |
| H4 | native 一行两 heredoc 只跟踪第一个 | ⬜ 不修(刻意边界) | 宁可漏报不误报,误报会让安装自锁死 |
| H5 | 版本排序非数字 → 0 | ⬜ 不修(刻意边界) | `pick-best` 返回原始串,选择时不混为同版本 |
| H8 | 缺显式 `--pin` | ✅ 已修 | 0.58.0(`install`/`reinstall` 支持 `--pin=Mod@ver[,Mod2@ver2]`,解析器按精确匹配钉版,优先级高于依赖方约束) |
| H9 | 无真实上传到远端索引 | ✅ 已修 | 0.57.0(`publish --remote` multipart PUT 到 `42.zef.pm/upload`;`login --username/--password` 联网换 key) |
| H12 | `.ps1` 优先于 `.bat` | ⬜ 部分缓解 | cmd 行为改不动;`.bat` 仍生成、`.ps1` 优先 |
> 注:该文档基于 0.52.1 扫描,其「当前源码实锤」对 0.52.2 之后的改动已过期——
> H3(0.52.2 已堵)、H7(下游报告现有代码已有)在我们这轮之前就已解决,并非漏看。
## 0.64.4 - 2026-09-20
- 性能:测试默认改为【按 CPU 核数并行】(上限 8),不再为躲预编译竞态而永远串行。
- 门禁 `tools/run-suite.raku`:`--jobs` 默认值从 1 改为 `min(CPU 核数, 8)`;并修复原并行分支的语义错误(旧逻辑把 `--jobs=N` 当成「批数」、实际并发 = 文件数/N);新增「跑前预热 lib 预编译缓存(-c 写热)+ 跑后失败用例串行复验」两道安全网吸收并发竞态假失败。
- 安装测试 `RakuPM::Client::Tester`:`--test-jobs=0`(auto)从「串行」改为「按 CPU 核数并行(上限 8)」;复用已有的预热 precomp + 串行复验兜底,故默认并行也稳定。
- 帮助文案(bin + COMMANDS-WIKI)同步:明确「默认并行、设 1 则串行」。
- 并发度读取收进 `RakuPM::Platform.cpu-cores`(新增导出函数):消除 `Tester` 里直接读 `$*KERNEL` 对「平台判断只许在 Platform」铁律的违反(原 `t/platform-centralized.t` 会因 `$*KERNEL.cpu-cores` 直接出现而红)。
- 并行默认改动引发的回归修复(同一轮):`uninstall` 默认清 store 后,`rollback-transaction` 与 `rollback-to` 内部「摘 CUR」的卸载必须 `:keep-store`——store 是世代回滚的还原源,不能随之删掉,否则 `xt/generations.t` / `xt/rollback-restore.t` 回滚时无源可装;`xt/rollback-restore.t` / `xt/skip-installed.t` 的 setup 卸载同步加 `:keep-store`(它们专门验证「store 还在时能恢复/重新入库」)。
## 0.64.3 - 2026-09-20
- 修复 `self-upgrade --force` 在原生 Windows 上因 `known-test-failure.t` 失败而回滚的事故:
- 根因:`is-known-windows-test-failure` 的规则只从 `lib/RakuPM/Client/known-windows-test-failures.json` 加载,而该 `.json` 不在 `META6` 的 `provides`(只列 `.rakumod`),安装到 `site/` 时不会随包拷过去;`self-upgrade` 跑测试时 `Tester` 可能从已安装的 `site/` 加载,`$?FILE` 兄弟文件缺失 → 规则集被掏空 → Windows 上本应软通过的已知失败误返 `False` → 安装被无辜回滚。
- 修复:在 `Tester` 内嵌一份【内置兜底规则】(Log::Async 12-context),文件缺失/解析失败时不再退化为「无规则」,文件存在时再叠加社区规则。新增回归测试 `t/known-failure-nojson.t`(强制 JSON 缺失 + 模拟 win32)锁死该路径。
- `uninstall` 默认把被卸版本的 **`store` 源码快照一并清掉**(CUR + 账本 + store 三处一致),普通用户「卸了就该没了」,不用卸完再跑 `clean`。新增 `--keep-store` 旗标保留快照(供秒级离线重装;`reinstall` 自动带此旗标)。`Installer.uninstall` / `Reporter.uninstall` / `repos uninstall` 分发同步:`keep-store => $keep-store`。新增回归测试 `t/uninstall-store.t` 锁死三态(整包清 / `--keep-store` 保留 / `--version` 只清那一版)。撤销了「新增 `remove`/`trim`/`prune` 命令做版本修剪」的提案——`uninstall Foo --version=X` 已能「保留包、只甩某旧版」,无需独立命令(`remove` 现只指 `repos remove`)。
## 0.64.2 - 2026-09-20
- 作者侧 `check --remote`(联网查同版本是否已发布)真实端到端验证 + 回归补全:
- **真实端到端跑通**:对真实生态索引(默认 zef + cpan + rea;验证时约束到 zef)
实测三种场景均正确——已发布(自测探针包 `RakuPMVerifyTmp:ver<0.1.0>` 与第三方
`JSON::Fast:ver<0.21.0>`)返回 `ok=False` 并提示「版本已发布…无法重传」;
未发布(随机名)返回 `ok=True` 放行。顺带证实 0.64.1 的 `publish --remote`
确实落进了 zef 索引(publish→索引→check 链路闭合)。
- **回归**:`t/author-check.t` 补「联网未能确认(缝=Nil)→ 警告但不阻断」分支
(plan 15→17),至此 `check --remote` 的「已发布 / 未发布 / 未能确认」三态分支
全部离线锁死(真网络路径仍靠手动验证,因依赖外网、不宜进门禁)。
- 纯测试/验证增强,**无源码行为变更**(patch)。
- 测试加固(同一批门禁修复,仍 patch):
- `t/known-test-failure.t`:判据从 `$*DISTRO.is-win` 改为 `is-windows()`。前者在
`$*DISTRO.name='win32'` 的 Windows 上返回 **False**(Rakudo 只认 `mswin32`),而识别函数
内部用项目统一的 `is-windows()`(兼容 `win32`)返回 True → 两端判据不一致致测试在
`win32` 命名 Windows 上误红,进而让 `self-upgrade` 在安装期跑 `t/` 时回滚。
- `xt/tester-async.t`:场景 1 把「并发证明」从脆弱的墙钟耗时阈值改为**进程起止标记重叠证明**
(每个测试起止各写标记文件,最晚启动 ≤ 最早结束 ⇒ 真并发;与计时无关,满负载不虚高);
另修正 `(now).posix` 误用(`Instant` 无此方法 → 生成的测试体编译失败),改 `(now).Num`,
并去掉并发下框架日志 flush 的 benign 竞态断言。场景 3 超时返回阈值放宽到 15s。
## 0.64.1 - 2026-09-20
- 作者侧真实端到端验证抓出的 wire 契约 bug 修复(patch):
- `login --username/--password` 此前用 multipart/form-data 打 42.zef.pm/login,
真实服务端 from-body 只认 application/json,导致服务端 500(「Odd number of
elements found where hash initializer expected」)。新增 `RakuPM::HTTP.post-json`
(发 JSON 体 + Authorization 头),`Author.login-remote` 改用它。
- 真实登录 / 发布已对 42.zef.pm 跑通:login 用裸用户名(如 `skyter10086`,不含
`zef:`/`gitee:` 前缀——前缀是包 identity 的 auth 命名空间,不是登录 handle)成功换得
真实 api-key;publish --remote 用该 key 真实上传返回 `{"success": true}`。
- `RakuPM::HTTP.post-form` / `post-json` 的状态码解析顺带加固:容忍 Windows CRLF
(子进程 stdout 的 \r),改用「提取数字串」而非尾随锚定正则。
- 回归:t/http-post-json.t(新增,验证 post-json 发 JSON 体、非 multipart、带 auth 头);
t/author-upload.t 场景 B 断言改为 JSON 体;t/mock-curl.raku 增加 -d JSON 体捕获。
## 0.64.0 - 2026-09-20
- A2:upgrade 不带包名默认升级全部前,新增一行醒目提示(非阻塞、不挡 CI;
--dry 试算时不提示),引导用户显式指定包名或改用 install --locked 做可复现升级。
- A5:移除 %net-cmd 中 refresh/check/publish 三个死配置(作者侧命令在更早的
`if $cmd eq` 分支已 exit 0,永不到达离线软提示块),离线 UX 逻辑收敛到单一来源。
- A7:help 别名速查表补列 rdeps(= rdepends)/ gens(= generations),与实际可用拼写一致。
- 回归:t/upgrade-mass-warning.t(A2)、t/p1-readonly-cmds.t 增 A7 断言。
(A6 已在 0.62.0 随别名补全一并修复,本次无改动。)
## 0.63.0 - 2026-09-20
UX 别扭点修复(A1 / A3 / A8 / A9,minor:提示增强 + 一处启发式误判修复)
- **A3:install 顶层参数的 git URL 探测误判本地路径**。旧逻辑:任何以 `.git`
结尾的字符串都被当成 git URL 去 `git clone`,于是本地目录 `./my.repo.git`
会被误判、clone 失败。`bin` 的分流判断从 `$spec ~~ /'.git' $/` 改为
`($spec ~~ /'.git' $/ && !$spec.IO.e)`——磁盘上确实存在的本地路径优先当
本地目录装(`install-from-dir`),不再误走 git。`t/install-git-url-heuristic.t`
端到端钉死(构造本地 `dist.git` 目录,断言不被拿去 clone、走 install-from-dir)。
- **A1:锁文件位置透明化 + 修正过时注释**。锁默认写在【当前目录】`raku-pm.lock`
(项目级、可提交,Cargo.lock 同款语义),包装在 ``(全局前缀)——两者
**有意分离**(锁记录「项目的依赖闭包」,包全局共享)。`Client.rakumod` 里那条
「锁文件放 target」的注释是过时的(bin 总是显式传 `:lock-path` 覆盖),已更正。
`doctor` 新增〔6/7〕锁文件小节,明确揭示锁的绝对路径、与包分离的意图、
以及「upgrade 不读锁」的提示,消除「取决于你在哪」的困惑。
- **A8:upgrade help 强调可复现**。升级提示加一句:upgrade 取【最新版】、不读锁文件
(`--locked` 才会钉死);要可复现请用 `install --locked`。`t/p1-readonly-cmds.t`
钉死 help 含该提示。
- **A9:doctor 新增〔7/7〕作者侧凭据小节**。检查 `/credentials.json`
是否存在,提示 `publish --remote` / `check --remote` 需先 `raku-pm login`,
仅本地发布不受影响。
- 文档纪律:META6 / README.md / README.en.md 的 `RakuPM:ver<>` 与 Changes 顶部
同步到 0.63.0。
## 0.62.0 - 2026-09-20
命令简写别名(minor:新增批量短别名能力)
- 新增 `%CMD_ALIASES` 单一事实来源,覆盖几乎所有命令的双/三字母简写(如 `i`→install、
`un`/`rm`→uninstall、`up`→upgrade、`ri`→reinstall、`ls`→list、`li`→installed、
`inf`→info、`dep`→depends、`rdep`→rdepends、`s`→search、`doc`→doctor、
`out`→outdated、`gen`→generations、`rb`→rollback、`ar`→autoremove 等 30+ 条)。
- 派发时一次性把别名归一化成规范命令名,`help` / `version` / 旗标校验 / `given` 派发
全部只看规范名,别名不会让任何分支重复或漂移。
- 补全脚本(`completions`)同步输出别名,`help` 文本加一行别名说明。
- 回归测试:`t/p1-readonly-cmds.t` 新增端到端别名断言(`e`→`env`、`doc`→`doctor`、
无参 `un` 走 uninstall 报错分支、bash 补全清单含 `i`/`rm`)。
## 0.61.1 - 2026-09-20
两个一致性 bug 修复(patch)
- **B1:`update` 命令漏进 `%CMD_FLAGS` 单一事实来源**。`raku-pm update` 已实现、
help 也写了,但 `%CMD_FLAGS` 里没有 `'update'` 键,导致 `completions` 生成的补全脚本
漏了 `update`(以及它的别名 `repos update`/`gens`/`rdeps` 也取不到),且以后给 `update`
加旗标会被「未知选项」静默拒掉。补上后补全脚本与命令清单自动同步。
- **B2:`reinstall Foo:ver<1.0>` 静默丢掉 `:ver<>` 约束**。原实现「已装」分支只用
`--version` 旗标筛版本,完全忽略规格串里的 `:ver<>`,于是 `reinstall Foo:ver<1.0>` 会把
Foo 的**所有已装版本**全重装一遍,与 `install`/`upgrade` 都认规格串约束的行为不一致。
现改为与 `version`/`installed` 同义:用 `RakuPM::Version.satisfies` 解释约束
(`'1.0'` 精确、`'1.0+'` 范围)。`--version` 仍优先。
- 回归测试:`t/reinstall.t` 增 B2 用例(同包双版本,断言只重装匹配版);
`t/p1-readonly-cmds.t` 增 B1 用例(bash 补全必须含 `update`)。
## 0.61.0 - 2026-09-20
self-upgrade 覆盖安装语义(minor:self-upgrade 新增「自动清理旧版本」能力)
- **`self-upgrade` 升级完自动清掉 RakuPM 的旧版本**(CUR / store / 账本三处),
等价于「覆盖安装」——升级后无需再手动 `uninstall` 旧版本。
- 根因:`install()` 只摘「同版本」、`!record` 把每个版本都累加进账本 `versions[]`,
于是升级后旧版本仍残留在 CUR / store / 账本里(wrapper 永远跑 store 里最新的那份,
故功能上新版在跑,但旧版占着空间、还出现在 `list` / `outdated` 里)。
- 实现:新增 `Installer.prune-other-versions($name, $keep)`,摘掉「除 keep 外的所有版本」;
`self-upgrade` 的两条路径(git 远端 / 本地仓库)安装成功后调用,只保留刚装的这一版。
- **刻意只用于 RakuPM 自身**:普通用户包仍然支持多版本共存,绝不调用本方法。
- `--dry` 时只报告、不实际删除(与 install 的 --dry 一致)。
## 0.60.2 - 2026-09-18
新增三个只读命令(均不进写锁、不改账本、不改 CUR;patch:纯能力扩展,对外事务化核心不变)
- **`outdated`**:列出 raku-pm 管理的、生态里有更高版本的已装包(`当前 → 最新`),
并标注「源码安装 / 生态里找不到」的跳过项。刻意**绕过** `reconcile-installed`
直接读账本,避免只读命令顺手改写账本。
- **`doctor`**:综合体检 5 小节——RAKULIB 指向、PATH 中 raku-pm 是否被遮蔽、
`target/stage` 残留暂存、账本↔CUR 一致性(`consistency-issues`)、运行环境
(Rakudo/VM/平台),最后给总评。契合「教学型」定位,排障友好。
- **`completions`**:生成 bash / zsh / fish / PowerShell 的补全脚本并打到 stdout
(`raku-pm completions `,或 `--shell=`),可直接 `eval` 或落地到
shell 的 completion 目录。
**命令清单 / 旗标单一事实来源重构**:把原来散落在 `allowed-flags` 内的局部
`%per` 提升为文件级 `my %CMD_FLAGS`(命令→旗标白名单的唯一来源);`allowed-flags`
改为从它读取;新增命令/选项只需改 `%CMD_FLAGS` 一处,`completions` 自动同步。
顺手修掉 bash 补全公共旗标重复追加的 bug。
## 0.60.1 - 2026-09-20
收敛旧的每包独立暂存进整树事务化路径(patch:内部清理,对外行为不变)
- **删除 per-package 暂存方法** `Installer.stage` / `cleanup-stage` / `purge-cur-entry`
(各自在 `target/stage/-` 建一个独立 CUR)。它们的职责已被
`install-pending`(写共享整树暂存 CUR `target/stage/pending`)+ `pending-cur-spec`
+ `raku-lib-spec`/`pending-lib-spec`(注入子进程 `-I`)完整覆盖;清理由
`cleanup-stage-all`(成功晋升后)与 `discard-pending`(失败路径)统一负责。
- 同版本重装的「去旧」逻辑改由 `install-pending` 内部对暂存 CUR 做 `uninstall`、
以及 `install()` 对真实 CUR 做 `uninstall` 承担——`purge-cur-entry`(针对真实
site 的整份摘除)已无调用方。
- 同步更新 `lib/RakuPM/Fs.rakumod` 与 `CODEBUDDY.md` 里对 `Installer.stage` 的引用。
## 0.60.0 - 2026-09-18
彻底事务化安装 + 修复整树暂存 CUR 未被注册为包含路径(minor:改变安装期间的落盘时序,对外行为等价)
- **整树事务化安装落地**:安装链期间所有包只装进共享的「整树暂存 CUR」(`target/stage/pending`),
测试也在那里跑;整条链全部通过后才由 `promote-pending` 一次性原子晋升到真实 CUR + 账本 + bin。
失败 / 进程中断时真实 CUR 与 `installed.json` 自始未被触碰,只需丢弃暂存(`discard-pending`),
无需逐个回滚——原子「全有或全无」语义真正落地(与 0.59.0 的逐包即时激活对比)。
- **修复 P0:`cleanup-stage-all` 从未定义**:0.59.0 的失败路径会因「找不到方法」先崩溃、且从不回滚。
0.60.0 补齐 `cleanup-stage-all` 等整树暂存方法(`pending-cur-spec` / `install-pending` /
`promote-pending` / `discard-pending` / `!reset-pending`),保证暂存目录不论成败都被清干净。
- **修复构建/测试子进程找不到暂存依赖**:原先把暂存 CUR 与真实 CUR 用空格拼成一个串塞进
`raku-lib-spec`,但 `Builder` 与 `Tester` 把它当【单条】`-I` 传入,Rakudo 不会拆分空格串,
导致暂存 CUR 从未生效——构建期 `use` 不到刚暂存的 `build-depends`(`t/build.t` 必现回归)。
现改为 `raku-lib-spec` 恒返回真实 CUR,新增独立的 `pending-lib-spec`,由 Builder / Tester
注入为【独立 `-I`】(排在真实 site 之前)。`t/build.t` 由此恢复全绿。
## 0.59.0 - 2026-09-18
安装健壮性与缓存回收(minor:改变清理/回收语义,无破坏性接口变更)
- **多版本清理根因修复**:账本 `installed.json` 的 `versions[]` 不再累加「安装历史」,
改为只镜像 CUR 里该发行版的当前激活集。此前每次 `self-upgrade` 都 append 一条版本,
导致 `clean` 的保护集合锁死、`store` 里 30 个旧版本永远回收不掉——现在只要旧版本
不在任何保留世代里就能被 `clean` 正常回收。
- **CUR 修枝**:`install` 激活新版后,顺手把 CUR(site/)里该发行版的其它版本摘掉,
避免升级后旧版本残留在 site/(多版本膨胀在 CUR 一侧的表现)。
- **安装失败必清 Mock**:测试失败/进程中断时,安装链在失败路径调用新增的
`cleanup-stage-all`,确保 `target/stage/` 暂存 CUR 不论成败都被清干净
(此前只有成功路径才清,失败的残留会累积)。
- 保留 `rollback-to` + `generations` + `clean`;`rollback-transaction` 的失败回滚仍保留
(依赖在构建/测试期就会被写进 site,链中段失败时它负责把现场还原)。
## 0.58.0 - 2026-09-17
H8 收尾 + 凭据管理闭环(minor:新增 `--pin` / `logout` 两个能力)
- **`--pin` 依赖钉版**(H8 正式修复):`install` / `reinstall` 支持
`--pin=Mod@ver[,Mod2@ver2]`,把指定模块(含传递依赖)精确钉到某个版本。
解析器在 `RakuPM::Resolver` 里新增 `%!pins`,`!resolve-one` 命中钉版时用
精确约束 `= ver` 覆盖任何来路约束(依赖方提出的、CLI 位置参数给的),优先级最高;
若钉版与某依赖约束无法同时满足,冲突报错仍会把钉版列进请求链,便于定位。
钉版沿 `install → !resolve → Resolver` 以及 git/本地目录路径的 `resolve-deps-of`、
构建/测试相依赖、`!ensure-git-deps` 全链路透传(`%pins` 命名参数)。
- **`logout` 命令**:与 `login` 配对,删除本地凭据文件
`~/.raku-pm/credentials.json`(或 `RAKUPM_TARGET` 下的)。凭据文件本来不存在时
只提示「没有已登录的会话」、不报错。返回结构 `(ok, existed, deleted, path, error?)`。
- 文档:两份 README 的 `RakuPM:ver<>` 示例同步到 0.58.0;Changes 总览表 H8 由
「已用 --only 缓解」改为「✅ 已修」。
## 0.57.0 - 2026-09-17
真实生态上传(**H9 修复的收尾里程碑 M3 → minor**)
把 M2 已构造好的上传请求真正发出去,并把账号登录换 key 也补上,H9 至此闭合:
- 新增 `RakuPM::HTTP.upload-multipart` / `post-form`:raku-pm 唯一的上行路径。
用系统 curl 的 `curl -F "dist=@" -H "Authorization: Zef " ` 形态
(与 fez/zef 服务端 `42.zef.pm/upload` 期望完全一致;HTTP::Tinyish 不原生支持
multipart 文件上传,故上行走 curl,下载仍走 HTTP::Tinyish 后端)。run 后
`slurp(:close)` 关掉两条管道(防 Windows 句柄泄漏),用 `-o`/`-w '%{http_code}'`
精确拿 HTTP 状态与响应体。首行 `assert-online` 离线硬拦。
- `login --username= --password=`:真实联网 `POST 42.zef.pm/login` 换 api-key
并存入 `~/.raku-pm/credentials.json`(密码不落盘,与 fez 一致);响应里健壮抽取
key(key / api-key / api_key / token,或裸串)。`--api-key` 仍是直接导入已有 key。
- `publish --remote`(非 `--dry`):先校验 api-key(环境变量 `RAKUPM_API_KEY` >
凭据文件),再 `upload-multipart` 真实上传;成功打印确认,失败给出含「同版本已
存在 / auth 不匹配」提示的友好报错,由服务端强制校验。`--dry` 仍只预览不发网。
- 版本纪律:META6 / 两份 README 示例 / Changes 顶部一致到 0.57.0;README 生产侧
「发布到生态」由 🟡 改为 ✅;H1–H12 总览表 H9 行由「不修」改为「已修」。
- `post-form` 韧性加固:子进程(curl)的 stdout / stderr 按二进制读回,先试 utf-8
解码、失败退回 latin-1。修复 Windows 上子进程 stderr 可能是系统代码页(GBK)文本、
严格 utf-8 解码直接抛「Malformed UTF-8」炸掉整个上传链路的真实缺陷(见 `t/author-upload.t`
的 mock curl 测试:本环境 `IO::Socket::INET :listen` 抛 "Invalid port (Int)"、
`IO::Socket::Async` 第 3 连接数据 Supply 不触发,本地 mock 服务器起不来,故用替换
curl 二进制的方式离线验证 wire 契约)。
### 测试夹具 `t/mock-curl.raku` 的最终收口(本环境 Rakudo/Windows 专属坑)
上传路径带 `-H "Authorization: Zef "`(含空格)与 `-F "dist=@"`,本环境
rakudo 的 `run()` 会把 `.bat` 参数包进 `cmd /c` 并**错引含空格的参数** → `.bat` 起不来 →
mock 根本没跑 → `post-form` 收到 exit 1(Windows `ERROR_INVALID_NAME`)。login 那条
只传 `-F username=… -F password=…`(无 `-H`、无 `@` 字段)所以能过,正好佐证是
空格参数引述问题。最终收口方式(向后兼容):`post-form` 检测 `RAKUPM_CURL` 以
`.raku` / `.rakumod` 结尾时,直接用 `$*EXECUTABLE`(真实 `.exe`,不经 `cmd /c`)
跑 `$*EXECUTABLE .raku …`;其它 curl 仍按原样直接调用。mock 自身三处加固:
① 日志目录保持 `IO::Path`(本环境 `IO::Path.absolute` 返回 **Str**,在其上 `.add`
会抛 "No such method 'add' for string"、让 mock 在启动期就崩);② 文件内容按二进制
读回(`slurp(:bin)` + `try .decode('utf-8') // .decode('latin-1')`)—— sdist 是真实
二进制 tar.gz,默认 UTF-8 `slurp` 会抛 "Malformed UTF-8 near byte …";③ 用 `.raku`
取代已弃用的 `.perl`,避免弃用告警污染 stderr 被 `post-form` 当成错误。
## 0.56.0 - 2026-09-17
作者侧凭据管理 + 真实上传请求构造(**H9 修复的第一里程碑 M2 → minor**)
基于 M1 对 fez/zef 生态上传协议的调研(`https://42.zef.pm` 是规范上传端点,
认证头 `Authorization: Zef `,multipart 字段 `dist`),把「生产侧发布到
真实生态」这条链路从「完全空缺」推进到「凭据 + 请求结构本地可验证」:
- 新增 `raku-pm login [--api-key=] [--username= --password=
]`:
把生态 api-key 存到 `~/.raku-pm/credentials.json`(权限收紧到仅属主可读,类 ssh key);
`--api-key` 直接导入一把已有 key(与 fez 共用同一把),`--username/--password`
实时换 key 的登录在 M3 实现(仅记 username、明确告知延迟)。
- `publish --remote` 重写:先校验 api-key(环境变量 `RAKUPM_API_KEY` > 凭据文件),
构造真实上传请求(`POST /upload`、`Authorization: Zef `、字段 `dist`);
`--dry` 只打印请求、不发网络、不本地部署;**非 dry 的真实 multipart PUT 在 M3
实现**(RakuPM::Net 目前只覆盖 GET 类请求,没有上传能力),M2 明确报错延后、不静默发网。
- `publish --remote` 不再要求配置本地目录仓库(`--to`/`RAKUPM_PUBLISH_REPO`)——
真实上传路径在本地部署之前短路。
- 凭据读写全部走 `RakuPM::Platform.is-windows` 单一平台入口(chmod 仅非 Windows 执行),
不破坏平台集中化门禁。
- 新增 `t/author-remote.t`(login 凭据读写 + publish --remote dry 预览 + 缺凭据/非 dry 报错)。
## 0.55.0 - 2026-09-17
新增 `shell` 命令:只打印可 eval 的环境赋值,把 raku-pm 环境灌进当前交互式 shell
(**缺口补齐:以前只有 `env` 打印、`look --shell` 看源码,没有真正注入环境的命令**)
- `raku-pm shell` → 打印 `export RAKULIB=…` / `export PATH=…`(bash/zsh/sh/Git Bash 默认)
- 自动识别 shell 类型,也支持 `--shell=fish|pwsh|cmd` 强制格式
- 与 `env`(带说明、给人看)不同,`shell` 只输出赋值、无注释,可直接 `eval` 安全生效
- `env` 输出里加了一句提示:`eval "$(raku-pm shell)"`
## 0.54.3 - 2026-09-17
install 期主动读 build.json 提示构建问题(**硬伤/H10 修复 → patch**)
`Installer.install` 装完会把 store 的 build.json 旁车读一遍,遇到「构建没跑成功 /
被 `--no-build` 跳过 / 声明了 resources 却没生成」这类【事后才暴露、运行期才崩】
的问题,立即在 stderr 打 `⚠ 构建痕迹:…` 提示(纯警告、不中断安装)。
原本这类只在 `raku-pm verify` 才查得到,现在安装时就前置暴露。
抽取 `!build-issues-for($name,$v)` 供 `consistency-issues` 与 `install` 共用,
行为不变、单一事实来源。
## 0.54.2 - 2026-09-17
Windows 已知测试失败豁免名单外置为 JSON(**硬伤/H6 修复 → patch**)
`Tester.is-known-windows-test-failure` 里写死的 `Log::Async` 专属逻辑改为查表:
规则数据放在 `lib/RakuPM/Client/known-windows-test-failures.json`(JSON 数组),
社区无需改代码即可贡献新规则。每条规则字段:`name`(包名精确匹配)、
`test-starts-with` / `test-contains`(测试文件过滤,可选)、`contains`(失败输出
子串列表,任一命中即匹配)。判定仍够窄(具体包+测试文件+失败特征+Windows),
真 bug 不会被掩盖;JSON 缺失/解析失败退化为「无已知规则」,不写死任何包名。
可用 `RAKUPM_KNOWN_FAILURES_JSON` 覆盖数据文件路径(测试/高级用户用)。
## 0.54.1 - 2026-09-17
CLI 冷启动自检 RAKULIB 遮蔽(**硬伤/H11 修复 → patch**)
加载链(`$*REPO.repo-chain`)上若出现【多个不同版本】的 RakuPM,raku-pm 启动时
会在 stderr 打印警告,提示用户「实际加载的未必是以为的那份」—— 典型坑:全局
RAKULIB / 多个 site(cur/home/vendor)指向旧版 raku-pm,导致跑着旧版却以为新版。
- 新增零依赖模块 `RakuPM::CliCheck`(承载 `check-rakulib-shadowing`);
- 只在「响应 `.installed` 的安装型仓库」上查,开发期 `use lib` 前置的 FileSystem
仓库自动排除,**开发期不误报**;单一版本(哪怕散落多仓库)静默,只有多版本才打扰;
- 纯警告、零代价,置于 MAIN 入口、`require RakuPM::Client` 之前,不拖 Client 模块图。
## 0.54.0 - 2026-09-16
可提交锁文件(**新能力 → minor**):把锁文件从「全局前缀内部」挪到「项目可提交」。
之前 `raku-pm.lock` 写在 `target/raku-pm.lock`(默认全局前缀 `~/.raku-pm`),是整机
共享的一份、没法按项目提交、也不能复现单个项目的依赖闭包。0.54.0 起:
- 默认位置改为**当前工作目录**的 `raku-pm.lock`(仿 Cargo.lock),可随项目提交,
实现可复现安装:`git add -f raku-pm.lock` → 队友 `raku-pm install --locked` 装出
完全一致的版本(`--locked` 下锁缺失 / 与解析结果不符直接报错)。
- 新增 `--lock-file=<路径>` 旗标(install / reinstall / self-upgrade / lock 都认),
可把锁改到任意位置(CI 常用)。
- 锁描述的是「项目的依赖闭包」,与安装位置(`--target`)无关:即使包装进独立前缀
`.raku-env`,锁仍写在项目根,提交的是锁不是前缀。
- 库层 `RakuPM::Lock` / `Client` 不变,仍是 `$.lock-path` 注入;CLI 负责推导默认路径
(无 `--lock-file` 时取 `$*CWD/raku-pm.lock`)。测试直接构造 `Client` 仍走 target
内锁,不污染仓库根。
- 默认 `.gitignore` 忽略 `raku-pm.lock`(避免开发残留),要提交可复现锁用 `git add -f`
或项目内 `!raku-pm.lock` 取消忽略。
- 文档:`README.md` / `README.en.md` 各加「可提交锁文件」小节,help 文本补
`--lock-file` 与锁文件说明。
## 0.53.0 - 2026-09-16
新增 `reinstall` 与 `update` 两条命令(**新能力 → minor**)。
- `reinstall <模块名>`:把当前已装版本卸掉再按**完全相同版本**装回(对应 zef
`reinstall`)。复用 `uninstall` 的反向依赖保护(被依赖时同样会拒绝,除非
`--force`/`--recursive`),`install` 传 `:version` 精确钉回避免被解析成升级;
完全没装过退化为一次普通 `install`;`--dry` 只预览(uninstall 无 --dry,故 dry
时跳过卸载只调 install :dry)。放在 `RakuPM::Client.reinstall`。
- `update [仓库名|URL]`:顶层别名,等价于 `repos update`,刷新仓库索引缓存
(不加名 = 全部)。进入 `%net-cmd` 离线提示集。注意它与 `upgrade`(升级已装包)
语义不同。
- 同步:META6 / README / README.en / Changes 版本升 0.52.2 → 0.53.0。
## 0.52.2 - 2026-09-16
修复 `clean` / `gc` 会回收「被保留世代引用的版本」的真实缺口(AI 审查提的 H3)。**修 bug → patch。**
- `RakuPM::Installer::Generations` 新增 `protected-versions`:扫所有保留世代的 `manifest.json`,
返回其中出现过的 `name\0version` 集合(空目录 / 解析失败都安全返回空)。
- `RakuPM::Cleaner.protected-versions` 新增来源 4:合并世代 manifest 引用的版本。
`rollback-to` 把账本恢复到某世代的 `manifest` 后,缺失版本从 `store` 装回;若被 clean
回收就装不回了(只能留空世代 + 警告)。现在这些版本纳入受保护集,clean 不会打断回滚。
- `t/clean.t` 增加隔离回归块(6 断言):账本/锁/site 都不认的版本,只要被某世代 manifest
引用就受保护、不进回收列表、试算不落盘。
## 0.52.1 - 2026-09-16
修复 `info` / `browse` 拿不到许可证与主页地址的真·生态联调问题(对 360.zef.pm 实测暴露)。**修 bug → patch。**
- `RakuPM::Repository::Ecosystem` / `RakuPM::Repository::Local` 新增 `raw-row($name, $version = '')`:
返回完整索引行 / META6(含 `source-url` / `license` / `support`),不再被 `search-rows`
的精简显示裁掉。找不到返回安全空 Hash(非 Nil)。
- `RakuPM::Client::Reporter.!raw-row-for` 改用 `raw-row` 取行(老仓库类型无 `raw-row` 时
退回 `search-rows` 精简行兜底)。
- 实测:`raku-pm info JSON::Fast` 现在显示「许可证:Artistic-2.0」「项目主页:git://github.com/timo/json_fast.git」;
`browse JSON::Fast` 打印并在默认浏览器打开该地址;`smoke JSON::Fast` 拉源码跑完 14 个测试文件全过。
- 新增 `t/query-commands.t` 中 MetaPkg 用例,离线锁定「info 把 license / source-url / support 打出来」。
## 0.52.0 - 2026-09-16
补齐与 zef 对齐的元信息查询四条命令(`info` / `browse` / `locate` / `smoke`)。**新能力 → minor。**
### 发行版元信息查询(对齐 zef)
- `info <包>` —— 显示发行版详情:身份(name:ver:auth)、描述、作者、提供的模块、
运行时/构建/测试依赖、本地库依赖、外部命令依赖、git 依赖、资源文件,以及
生态索引里的源码地址 / 许可证 / 项目主页。仓库里查不到的本地包(如 raku-pm 自身)
退回到已装账本也能看到。
- `browse <包>` —— 在默认浏览器打开主页或源码地址(地址优先级:META6 的
`support.source` > 索引行的 `source-url`;都没有则明确说无法打开)。平台分支集中在
新增的 `RakuPM::Platform.open-url`(Windows `start` / macOS `open` / Linux `xdg-open`)。
- `locate <模块>` —— 走仓库链解析模块,打印实际加载的那份 `.rakumod`/`.pm6` 文件路径;
多份共存时指出被遮蔽的那些(与 `which` 同源口径)。
- `smoke [包…]` —— 取源码并跑测试(复用 `test-dir`),统计通过/失败/跳过;
不给规格则测全部已装(best-effort,离线或取不到源码的会跳过,可能很慢)。
### 其它
- `RakuPM::Platform` 新增 `open-url`(集中浏览器打开的平台分支,可被测试遮蔽)。
- 版本纪律:META6 0.51.0→0.52.0;两份 README 示例同步;此条目已加(门禁 --docs-only 全过)。
### 离线模式(`--offline` / `RAKUPM_OFFLINE=1`)
- 唯一判定处 `RakuPM::Net`:三处网络出口(HTTP 门面 / 生态索引 / git 子进程)都读它,
于是「这一轮能不能联网」是结构性保证,不是靠每个调用点自觉。
- 索引无缓存、git 无副本时**明确报错**,绝不静默改成联网。
- 用途:网络受限环境、CI 要确定性、离线复现。
### 查询 / 单阶段命令
- `depends <模块>` —— 显示完整正向依赖树(已装 / 待装 / 找不到一眼看清,含环检测)。
- `test [路径]` —— 只跑测试、不安装(复用 Tester,默认当前目录)。
- `build [路径]` —— 只跑构建、不安装(默认当前目录)。
- `look <模块> [--shell]` —— 取源码进 `cache/look/` 目录看;默认只打印路径,
`--shell` 才进交互 shell。
### 生产侧:`raku-pm publish`(本地目录仓库的发布)
- 把 `dist` 打出的 sdist **解包部署**到本地目录仓库(`--to=` 或
`RAKUPM_PUBLISH_REPO`),使其立即可被 `raku-pm install` 解析。
- 复用 / 重打:`--from=` 吃现成包;默认复用 `dist` 现打(先跑 check 门禁)。
- 碰撞检查:`/-/` 已存在则拦(先 `bump` 或 `--force`);
`--remote` 额外联网查远端生态索引同名同版(离线硬拦)。
- 作者侧工作流至此闭合:`new → refresh → check → dist → publish`。
- 真实生态的上传(`login`)仍待做。
### 其它
- 新增模块 `RakuPM::Net`(零依赖,冷启动无负担)。
- 作者侧命令(`new`/`refresh`/`check`/`dist`/`bump`/`publish`)在构造 `Client` 之前
分发,不进写锁、不碰安装目标。
## 0.50.0 - 2026-09-16
新增命令 **`raku-pm flush`**:一键清空当前前缀(含 raku-pm 自己的入口),回到
「可以重新安装」的状态。**新能力 → minor。**
### 为什么单开一个命令,而不是加强 `clean`
三个命令的意图不同,`clean` 的每条规则都是「什么不能删」:
| 命令 | 意图 |
|------|------|
| `clean` / `gc` | **有保护**地回收(只动没被引用的版本,默认还留着索引与克隆缓存) |
| `self-remove` | 只卸 raku-pm 自己 |
| `flush` | **无保护**地全清,只为从零重来 |
想要「从零重来」,原先得 `self-remove` → `clean --all --yes` → 再手工收拾,还清不干净
(世代目录、日志、账本、前缀外的入口副本都留着)。合成一个命令迟早会有人把 `flush`
当成 `clean` 的加强版顺手敲出去,所以刻意分开。
### 做了什么
- 新增 role `RakuPM::Client::Flusher` + 命令 `flush [--yes|--force]`。
**默认只试算**(删文件不可撤销,与 `clean` 同一约定)。
- 清:前缀内的 `store/` `site/` `git-cache/` `cache/` `log/` `generations/` `stage/`
`bin/` `installed.json` `raku-pm.lock`,以及**前缀外**那几份自装入口
(全局 `site/bin` 下的 `raku-pm` / `.bat` / `.ps1`)。
- **两道护栏**(全删命令的护栏比功能重要):
1. 目录必须**看起来就是一份 raku-pm 前缀**(`store/`、`site/`、`installed.json`
至少一个),且不是 `$*HOME` 本身、不是文件系统根 —— 否则直接拒绝。
`--target` / `RAKUPM_TARGET` 打错一个字符时靠它兜住。
2. 只删**认得的条目名**,别的一律不动(并列出来)→ 前缀与别的文件共处一个目录
也不会误伤。
- **入口按内容判、不按名字判**:同一个目录里可能躺着别人放的入口,名字还就叫
`raku-pm`(Unix 上 zef 生成的就是无扩展名的 `<名>`)。按名字删会删掉别人的东西;
按内容只删自己写的那三份(它们都设 `RAKUPM_RAKU` 作启动兜底 —— 结构标记,改文案改不动它)。
- **不动别人的东西**:zef 装的 RakuPM 不在本前缀内(只提示 `zef uninstall RakuPM`);
`<名>.exe.zef-old`(zef 入口的唯一备份)只报告不删。
- 结尾提示如实说明「入口已一并清掉,所以此刻没有 `raku-pm` 命令了」,并给出两种
装回来的办法(写「raku-pm self-upgrade」是错的 —— 那时已经没有这个命令)。
- `flush` 也进了「会写前缀」的命令集,于是自定义前缀下会先提示前缀路径再动手。
- 新增 `RakuPM::Prefix.is-fs-root`:把护栏里「是不是文件系统根」抽成**可测谓词**,
否则那个分支没法在测试里触发(临时目录的 canonical 形式不可能是根)。
顺带修掉它的一处平台陷阱:`C:` 必须在 `normalize` **之前**判 —— 否则会被
`IO::Path.absolute` 当成相对路径变成 `/C:`,永远匹配不到根。
### 测试
`xt/flush.t`(19 例,**放 xt/**:会触发「发现外来入口」分支并打印 ⚠,而 t/ 有洁净度硬约束)。
三条纪律写在文件头部:
① **永远注入 `:entry-dir`** —— 否则测试会删掉开发机上真正的 `raku-pm` 命令;
② 每条断言都问两遍「该删的删了没 / 不该删的还在不在」(只验前者的话,
一个「把前缀整个 rm -rf」的实现也能全绿,而那正是最该挡住的实现);
③ 护栏必须有测试。
**变异验证 3 个全红**:拿掉条目白名单(→「不认得的条目仍在」红)、拿掉入口内容标记
(→「外来的同名入口保留」红)、拿掉护栏(→「像前缀/家目录拒绝」3 条红)。
`t/prefix.t` 补 `is-fs-root` 5 例(≥16→21)。
### 自己踩的坑(记下来)
- 我用临时前缀跑 CLI 冒烟测试时,**真的把本机全局 `site/bin` 里的 `raku-pm` 入口删掉了**
—— 生产行为是对的(flush 本就该清入口),但说明**冒烟测试也必须注入入口目录**。
已按原样复制恢复(`~/.raku-pm/bin/` 里那三份 → 全局 `site/bin`),`raku-pm version` 复验可用。
- 新增的 Raku 陷阱已进 CODEBUDDY:`try {…} CATCH {…}` 的 CATCH 是死代码(try 吞异常)、
`||` 链里夹 `~~` 会返回 Match(撞 `--> Bool`)、`Seq.sort({…})` 把 Seq 喂给比较块、
`<[a..Z]>` 是倒序区间。
## 0.49.5 - 2026-09-16
修两件事:让 `xt/fetch.t` 真正离线(它是门禁里最后那个红),并补全索引拉取的韧性
—— 后者是**真实产品缺陷**,用户在国内网络下必然撞上。
### ① `xt/fetch.t` 声称离线,其实会去拉三个远程索引
它自己的注释写着「离线、确定性:源用本地目录仓库模拟 … 不去网络」,但构造 Client 时
只传了 `:repo-root`、**没传 `:repos`** → Client 照常读 `repositories.json` / 默认值,
于是挂上 zef + cpan + rea 三个**远程**索引。解析 `Lib::A`(明明在本地仓库里)之前
先要拉三个网络索引,任一无缓存且不可达就整条崩:
```
# You planned 8 tests, but ran 0
生态索引拉取失败且本地无缓存(https://raw.githubusercontent.com/Raku/REA/main/META.json)
```
现在显式 `:repos([Local…])`,注释与行为一致,门禁也不再随网络抖动变红。
### ② 索引拉不动的韧性:无缓存时也不再让整条命令失败
`Ecosystem.!refetch-index` 的失败分支原本是:
| 情形 | 旧行为 |
|------|--------|
| 拉取失败 + 有旧缓存 | 用旧缓存继续 + 告警 ✅ |
| 拉取失败 + **无缓存** | **`die`** ❌ |
而本文件上方早就写着原则「**单点网络抖动不应升级成安装失败**」—— 只在「有缓存」那半
兑现了。解析器会把每个仓库都问一遍,所以任何一个索引不可达都能让**整条命令**失败,
哪怕答案本来就在可达的仓库里(本地目录仓库、或 zef 索引)。**用户现场(国内网络)**:
`raw.githubusercontent.com` 上的 cpan/rea 不可达且从未缓存过 →
`raku-pm fetch / install / search` 全都直接崩。
- 无缓存失败改为:**按空索引处理 + 一行 `⚠`(点名索引与 URL)**,不再 `die`。
- 新增公开属性 `Ecosystem.degraded`:两种降级(用旧缓存 / 按空索引)都置位。
照着 `Client.did-self-shadow-warn` 的既有做法 —— **警告要能被测试断言**,
而不是让测试去 grep stderr(那种写法一改文案就假绿)。
- README(中/英)补「某个索引拉不动时怎么办」:两种情形一张表 + 怎么读那几行 ⚠
(「找不到模块」紧挨着索引 ⚠ = 不是没有、是这次看不到)。
**测试**:`xt/http.t` 新增 7d(无缓存失败 → 不抛错 + `degraded` 置位 + 空索引,
并先断言「cache-root 里确实没有任何缓存」)。
**变异验证**:把 `die` 放回去 → 7d 两条断言变红;恢复后绿。
**回归**:`t/` 66/66、`xt/` **30/30**(`xt/fetch.t` 首次在离线环境下通过)。
## 0.49.4 - 2026-09-16
文档补充(无代码改动):补上「**全局 `RAKULIB` 对「开发 raku-pm 本身」是个陷阱**」的警告
—— README 的 PATH 模式一节本来就建议把 `RAKULIB` 写进 shell 配置,却没说清副作用。
**触发它的真实数据**(用户 WSL,裸 `raku` 的仓库链):
| 链序 | 仓库 | `RakuPM::Version` |
|------|------|-------------------|
| [0] | `/home/user/.raku-pm/site` | **0.36.14** ← 过期副本,且排在最前 |
| [1] | `/home/user/.raku` | 0.46.1 |
| [2] | `/home/user/rakudo/share/perl6/site` | 0.46.1 |
后果是**逐模块的版本混用**,比整份旧版更难认:`RakuPM::Author` 在 0.36.14 里还不存在
(作者侧是 0.41.0 才加的),于是 `use RakuPM::Author` 落到 [1] 的 0.46.1
(它第 57 行会调 `scan-provides`),而 `RakuPM::Distribution` 从 [0] 的 0.36.14 拿到
(那时还没这个方法)→
`No such method 'scan-provides' for invocant of type 'RakuPM::Distribution'`。
这与 0.49.3 修的那个假绿是同一件事的两面:**测的都不是工作树**。
- README(中/英)在「安装前缀」下新增小节:说清副作用、给三条自保办法
(只用 `raku tools/run-suite.raku`;单跑用 `raku -Ilib t/.t`;用
`raku-pm which RakuPM::Version` 看此刻裸 `raku` 会加载哪一份),
以及清理过期副本的**命令**步骤(`installed` → `self-upgrade` →
`uninstall --version=`,**别手删**)。
- CODEBUDDY 把这条症状写进排查清单:**「旧版的 bug 被当成当前代码的失败」先查链首**。
## 0.49.3 - 2026-09-16
修两个**测试基建**缺陷(产品逻辑无改动)。用户在 WSL 上跑 `raku tools/run-suite.raku`
一次报了 6 个红:其中 5 个是同一个原因,另 1 个是 0.49.2 同类问题的漏网。
### ① 门禁跑测试时漏传 `-Ilib` —— 5 个红的总因
`tools/run-suite.raku` 的 `run-one` 是
```raku
run($raku, $rel, :out, :err, :cwd($root)); # ← 没有 -Ilib
```
(同文件的「编译暂存文件」那处倒是传了 `-Ilib`,所以一直没被注意到。)
于是**任何没写 `use lib` 的测试都在测「系统里已安装的 RakuPM」,而不是工作树** ——
`t/author-check.t`、`t/store-lock.t`、`t/store-provides-ext.t`、`xt/repos.t`、
`xt/http.t` 正好都属于这一类。
**本机实测对照**(`raku -e 'say $*REPO.repo-chain.head.Str'`):
| 启动方式 | 仓库链首 | 结果 |
|----------|----------|------|
| `raku -Ilib …` | `<仓库>/lib` | ✅ 测的是工作树 |
| `raku …`(无 `-I`) | 用户 `RAKULIB` 目录 | ❌ 本仓库 `lib/` **根本不在链上** |
所以开发机上一直全绿,只因那份已安装副本恰好自洽;用户机器上它与当前代码版本不一致,
于是**旧版的 bug 被当成当前代码的失败报了出来**。最典型的一条:`xt/repos.t` 报
`123.foo.example.com → foo`,而 `foo` 正是**修复前**的返回值(同一组里
`360.zef.pm → zef` 旧版也能过,所以只有 4c 一条红 —— 与旧代码行为完全吻合)。
- `run-one` 改为带 `-I<绝对路径>/lib`;启动参数收敛成**唯一**的 `test-args`
(自检与真实运行共用它,否则自检只是自说自话:删掉 `run-one` 里的 `-I` 它照样绿)。
- 新增**门禁自检** `tests-see-worktree`:跑测试之前,用同一个启动方式验证
`$*REPO.repo-chain.head` 就是本仓库的 `lib/`;不是就**直接中止**并说明「再跑下去
只会得到测了已安装副本的假绿」。这类「门禁看着绿、其实测的不是这份代码」的假绿,
比没有门禁更危险。
- **变异验证**:把 `-I` 从 `test-args` 去掉 → 自检立刻中止(exit=1,并打印排查命令);恢复后正常。
**审计(范围扩到 `xt/`)**:0.49.2 只扫了 `t/`,所以漏掉了 `xt/entry-check.t`。这次两边一起扫,
**共 14 个测试文件没有 `use lib`**(`t/` 9 个:author-check / clean / known-test-failure /
resolver-core-modules / store-lock / store-precomp / store-provides-ext / test-depends / version;
`xt/` 5 个:check-preflight / http / new-scaffold / repos / self-upgrade)。
它们**完全依赖**运行器传 `-Ilib` —— 这正是自检要守的东西。
### ② `xt/entry-check.t` 也是「宿主依赖」(第 6 个红)
与 0.49.2 修的 `t/bin-path-order.t` 是同一次事故的第二个现场:场景 1-5 写的是
**Windows 语义**(靠 `.exe`/`.bat` 参与命令解析)却没注入平台 —— Linux 上
`pick-command-file` 只看无扩展名,一个都认不出来 → 不接管 → 断言全红。
(`t/` 那个会挡住 `install .`;`xt/` 这个只挡自己跑门禁,所以上一轮没暴露。)
- `Installer.ensure-bin-entry` 新增 `Str :$os` 注入口(与 `:@dirs` 同一惯例),
透传给 `pick-command-file`;生产路径不传,**行为不变**。
- `xt/entry-check.t` 的 Windows 场景统一写 `:os('win32')`,并新增场景 6 验 Unix 语义:
**Linux 下 `.exe` 根本不参与命令解析**,所以那种外来 `.exe` 不该被改名
(改名在 Linux 上毫无意义,还白改别人的文件)。
- **变异验证**:去掉 `$os` 透传 → 场景 6 两条断言变红(exit=2);恢复后 10/10 全绿。
## 0.49.2 - 2026-09-16
修「raku-pm 在 Linux 上装不起来」:`t/bin-path-order.t` 是**只在 Windows 上能过**的测试,
它在 t/ 里,于是 Linux 上 `bin/raku-pm.raku install .` 跑到它必红、整次安装被回滚。
**用户现场**(WSL):
```
✓ …(其余 65 个用例全过)
✗ bin-path-order.t
↻ 串行复验 1 个失败用例一次(并行编译竞态 / 上游 flaky 都可能)
⚠ 安装未全部成功,正在回滚到本次操作前的状态…
```
复验仍红 ⇒ **稳定失败,不是并发竞态**,是测试本身的问题。
**根因**:该文件 0.48.0 进来时把 **Windows 语义当成了宿主事实** —— 断言里放的是
`.bat` / `.exe`,而 Linux 上 `Platform.exec-suffixes` 只认无扩展名那一种(`: ('',)`),
一个候选都找不到。**被测逻辑本身没问题**:唯一判定处 `Platform.pick-command-file` 是对的,
`raku-pm env` 在 Linux 上工作正常;错在测试没有注入平台。
- **给 `Reporter.detect-bin-on-path` 加 `Str :$os` 注入口**(与既有的 `:@dirs` 同一惯例),
并透传给 `pick-command-file` / `exec-suffixes`。生产路径不传 `$os`,行为**完全不变**。
- **`t/bin-path-order.t` 改为两套语义都注入**:`:os('win32')` 验 Windows 那一节、
`:os('linux')` 验 Unix 那一节。于是任何宿主上都能验两个方向,Windows 分支不再只能靠
「恰好在这台机器上」验证。新增 Unix 侧 4 条断言,其中最关键的一条是
「只有 `.bat`/`.exe` 时零候选」—— 这正是宿主混淆时会红的那条。
- **顺带修掉测试报告的一个缺陷**:候选数组为空时 `@got[0]` 会**抛异常**而不是干净
失败,于是文件在第一条断言就中断、后面几条根本不跑(报告只剩半截,看不出别处有没有问题)。
加 `base-at(@got, $i)` 安全取值(空则返回 `'<缺>'`),失败时如实列出全部红项。
**审计**:全库扫 `t/` 里依赖宿主平台的地方,**只此一处** offender
(`t/build.t` / `t/build-trace.t` / `t/by-distro-depends.t` / `t/md5.t` /
`t/known-test-failure.t` / `t/native-search-path.t` 都走了 `$*DISTRO` 分支或遮蔽)。
**变异验证**:把 `detect-bin-on-path` 的 `$os` 透传去掉(= 修前状态),Unix 那一节
**四条断言全部变红**(7/8/9/10,exit=4);恢复后 11/11 全绿。这也从**代码侧**证明了
「宿主依赖」这一类问题现在会被测试抓到 —— 用户那次 Linux 运行是同一形态的实例。
**顺带记录一个 Raku 陷阱**(写上面那个安全取值助手时踩的):`A && B` 短路求值成
**`False`,而 `False` 是 defined 的** → `(... ) // '<缺>'` 兜不住 → 返回值是 Bool →
`--> Str` 当场类型检查失败。`//` 只兜未定义,与 `try` 那条约同一类坑。
## 0.49.1 - 2026-09-16
文档补充(无代码改动):把 **PATH 模式**写成「安装前缀」一节的推荐做法 ——
即完全不往前缀外写文件的那条路。
0.49.0 的参数已经够用(`--no-promote-bin` + 把 `<前缀>/bin` 放 PATH 最前),
但 README 只说了「有这个旗标」,没说清**怎么整体切换、切换后要注意什么**:
- 三步切换:① 先配 PATH 与 RAKULIB(顺序必须在前)→ ② `self-upgrade --no-promote-bin`
→ ③ 可选清掉此前写进全局 `site/bin` 的三份副本。
**顺序不能反**:先配好 PATH 再删副本,否则会短暂没有入口。
- 讲清三个好处(前缀外零文件 / 不动别人的文件 / 撤销只要改 PATH)与代价
(自己配一次那两行,`raku-pm env` 会打印)。
- 补一节「多版本 / 多项目共存」:一份前缀就是一份独立环境,
`--target=./projA/.raku-env`,各项目的同一个包可以有不同版本。
- 提醒 0.48.0 的「入口体检」改名只是补救手段,PATH 模式才是干净解。
中英两份 README 同步。
## 0.49.0 - 2026-09-16
把 `--target` 从「一个能传但会漏的开关」变成**真正的安装前缀**(插座),并让
`promote-bin` 的默认值随前缀走。**行为变更**:自定义前缀下不再默认往全局
`site/bin` 写 wrapper(要保留旧行为就加 `--promote-bin`)。
**为什么值得单独一版**:`--target` 与 `RAKUPM_TARGET` 一直都在,但插座是**断的** ——
1. 三份 wrapper(bash / `.bat` / `.ps1`)都**没有把前缀传给 CLI**,只设了 `RAKULIB`。
于是 `<前缀>/bin/raku-pm` 设的 RAKULIB 指向自己的 site,而 CLI 回落到默认前缀
`~/.raku-pm` —— **命令装进 A、模块却从 B 找**。实测:直接跑 store 里的 CLI,
`raku-pm env` 一律打印 `C:\Users\...\.raku-pm`,与 wrapper 所在前缀无关。
2. `promote-bin` 在 CLI 里**无条件为真** → 自定义前缀也会往全局 `site/bin`
(zef 用的那个、天然在 PATH 上的目录)写 wrapper。0.47.0 开发时用
`--target=_h9repro/target` 装了一次,全局 `raku-pm` 就被指向了那个临时目录。
- **新增 `RakuPM::Prefix`**:「默认前缀」的唯一出处。CLI 要在 `require RakuPM::Client`
**之前**就知道前缀(`version` / `help` 要零模块图冷启动),而 Client 又要按前缀决定
promote —— 两边各写一遍 `~/.raku-pm` 就是两个事实来源。零依赖,含 `default-target` /
`resolve` / `canonical` / `is-default-target` / `default-promote-bin`。
- **顺手修掉一个真实缺陷:`~` 不展开。** 实测 Rakudo **不会**展开 `IO::Path` 里的前导
`~` —— `'~/.raku-pm'.IO.absolute` 得到的是 `<当前目录>/~/.raku-pm`。所以修复前
`raku-pm --target=~/myenv install Foo` 会真的在当前目录下建一个字面叫 `~` 的目录。
现在在入口统一展开(认 `~/x` 与 `~\x`;不认 `~user`,跨平台语义不一致不猜)。
- **前缀自洽**:三份 wrapper 都把 `--target=<自己的前缀>` 传给 CLI —— 敲哪个前缀的
wrapper 就在哪个前缀里干活,与 venv 的 `/bin/python` 同一语义。
- **`promote-bin` 的默认值按前缀算**(`Prefix.default-promote-bin`):
默认前缀 → 写全局 site/bin(保持 zef 同款「装完即用」);
自定义前缀 → 不写(局部安装不该污染整机环境,多个前缀还会互相覆盖)。
新增 `--promote-bin` / `--no-promote-bin` 显式覆盖(互斥,同时给会报错)。
`Client.promote-bin` 改为**不写默认值**,用「未定义 = 未指定」区分「没传」与
「显式要求关」,于是自动策略只有一处实现。
- **自定义前缀下如实告知**:会写前缀的命令打印说明(前缀路径 + 该把哪个 bin 放进 PATH
+ 想回 zef 行为加什么旗标)。显式给了 `--no-promote-bin` / `--promote-bin` 时不再重复。
- **`--target` 不带值不再静默回落默认前缀**(以前会被当成「没给」,用户以为指定了)。
- **「是不是默认前缀」按路径值判**(不是按「有没有写 --target」):把
`RAKUPM_TARGET=~/.raku-pm` 写进 shell 配置的人,意图就是默认前缀;Windows 上折大小写。
**测试**:新增 `t/prefix.t`(16 例,前缀解析/判定的纯函数)、`t/prefix-isolation.t`
(20 例 = CLI 策略 14 + Client 策略 3 + **真装一次**并断言前缀外零写入 2)。
`t/installer-shell-templates.t` / `t/wrapper-rakulib.t` 补「显式传前缀」断言(3 处);
`xt/wrapper-rakulib-run.t` 真跑 Git Bash,断言 `--target` 确实落到了子进程。
**变异验证 4 个全红**:拿掉 bash 的 `--target`(3 处断言红)、拿掉 `.bat`/`.ps1` 的
(2 处红)、Client 改回无条件 promote(4 处红)、CLI 改回无条件 promote(3 处提示断言红)。
其中「Client 改回无条件 promote」那一次让 `t/prefix-isolation.t` 的泄漏断言真的变红 ——
它确实往全局 `site/bin` 写了探针文件,事后已清理并核对目录回到基线(也正因此,
「自定义前缀不污染全局」这条不是纸面承诺而是被测试守住的)。
**层3 的测试刻意带正控**:先断言「bin 确实落进了自己的前缀」,再断言「全局 site/bin 里
没有」—— 否则 `!install-bin` 根本没跑也会绿(假绿高发区,本项目反复踩过)。
**顺带记录两个 Raku 事实**(都实测确认):
`IO::Path.absolute` 返回的是 **Str** 而不是 IO::Path(`--> IO::Path` 会当场断言失败);
`Bool` 是原生类型 —— 属性**省略**是「未指定」,但显式传一个未定义的 `Any` 会直接类型
检查失败(`has Bool $.x;` + `:$x` 透传时要注意)。
## 0.48.0 - 2026-09-16
修「self-upgrade 成功了、敲 `raku-pm` 却还是旧版」,并补上「zef 与 raku-pm 都装了
同一个包时怎么查」的可观测性。**无破坏性变更**(新增 `raku-pm which`;`env` 输出多两节)。
**用户现场**:本机跑 `raku-pm self-upgrade`(0.46.1 → 0.47.0),流程全通 —— 入 store、
62 个测试文件全绿、三份 wrapper 重写、lock 更新 —— 但升级后敲 `raku-pm version`
仍显示 **0.46.1**。
根因(已实证):Windows 用 `PATHEXT` 解析命令名,**`.EXE` 优先于 `.BAT`**,也优先于
没有扩展名的文件。而 `PATH` 上 rakudo 的全局 `site/bin` 排在自装目录之前,那里留着
**zef 时代生成的 `raku-pm.exe`** —— 它会一直遮蔽 raku-pm 自己写的 bash/`.bat` wrapper。
self-upgrade 只维护自写的那三类 wrapper,从不处理这个 `.exe`。
(把该 `.exe` 临时移开 → 立刻变成 0.47.0;移回 → 0.46.1。)
- **入口体检 + 自动接管**(`Installer.ensure-bin-entry`):安装/升级结束时按平台解析
规则算出用户敲该命令名**实际会执行哪个文件**;若不是 raku-pm 写的那份
(`` / `.bat` / `.ps1`),把它改名备份为 `.zef-old`
(可逆、不删数据)并打印说明与还原方法。幂等;没有自写 wrapper 时不接管
(否则会制造「连命令都没有」,比遮蔽更糟)。判定与 `raku-pm env` 共用
`Platform.pick-command-file`,不再两处各写一套。
- **`exec-suffixes` 改为 PATHEXT 感知**:旧值是 `('', '.exe', '.bat', '.cmd', '.ps1')`
—— 把无扩展名排第一(Unix 语义),于是自查工具指错「实际执行的那份」。现在 Windows
读真实 `PATHEXT`,且**不把无扩展名列进去**(cmd/PowerShell 根本不会执行无扩展名
文件);Unix 仍是 `('',)`。`find-executable` / `has-executable` 一并受益。
- **`raku-pm env` 的候选排序改为「模拟系统解析」**:不再依赖 `which`/`where` 的输出
顺序(Windows 的 `where` 实测按「无扩展名 → .bat → .exe」列,与系统解析不符)。
现在按 PATH 目录顺序 × 目录内 PATHEXT 顺序算 —— 第一个即实际执行的那份;后续标注
「被前面那份遮蔽,不会被执行」,并点名「自装 wrapper 被遮蔽了」。
顺带:目录顺序**优先于**扩展名优先级(同一个目录内才轮到扩展名说话)。
- **新增 `raku-pm which [模块名]`**(回答「两个包管理器都有同一个包,程序从哪加载」):
- 不给参数:跨仓库冲突总览 —— 同一发行版存在于多个仓库的全部情况,按仓库链序号
标优先级。本机实测 **24 个**(其中 `HTTP::Tiny`/`JSON::Fast`/`MIME::Base64`
版本还不一样)。
- 给模块名:该模块实际从哪个仓库解析、版本多少;多份时全部列出并标出被遮蔽的。
要写**模块名**(如 `JSON::Fast`),不是发行版名。
- **wrapper 保留用户已有的 `RAKULIB`**(三个平台模板):旧 wrapper 直接
`export RAKULIB="inst#/site"` —— **整条替换**,把用户自己设的仓库
(用户机上 `RAKULIB` 指向 `~/raku-modules` 这个 FileSystem 仓库)挤掉,raku-pm
会话里那些模块突然看不见。现在:先存原值 → 设 raku-pm 的 site(排最前,保住
「raku-pm 装的包优先」)→ 原值非空则**追加在后面**。
- **promote 目标改为「全局 site/bin」**,并把 `.ps1` 一并推上去:`site-bin-dir` 取的是
「链上第一个 basename 为 `site` 的仓库」,而 raku-pm 自己的 `~/.raku-pm/site` 也叫
`site` —— 于是通过 raku-pm 的 wrapper(`RAKULIB` 已设)启动时,promote 会把 wrapper
写进那个**不在 PATH 上**的目录,用户永远敲不到。新增 `global-site-bin-dir`
(显式排除自己的 site),promote 与入口体检都用它。另外 `.ps1` 以前只生成不推广,
现在三份一致(输出也不再声称写了三个却只放上去两个)。
- 回归:新增 `t/wrapper-rakulib.t`(9)、`t/bin-path-order.t`(7)、`xt/entry-check.t`(8)、
`xt/wrapper-rakulib-run.t`(5,真跑 Git Bash 验证 RAKULIB 拼接语义)、`t/platform.t`
第 8 节(PATHEXT 顺序 + `pick-command-file` 三份并存场景)。
**变异验证**:4 个变异(bash 模板退回整条替换 / `exec-suffixes` 退回旧表 / 入口体检
去掉 own 判断 / env 目录顺序反转)全部按预期变红,源码逐份复原。
- 顺带:`tools/NamedArgCheck` 的 `%ALLOWED` 补 `short-name`
(`CompUnit::DependencySpecification.new(:short-name)` 的核心 API 参数)。
## 0.47.0 - 2026-09-16
落地审查报告遗留项 **H9(平台分支不可达)**,顺带修掉用户报的 `raku-pm version`
在 Linux 上喷 `Use of Nil in string context`。**无破坏性变更**;`--os=` 现在会校验取值。
### 新能力:`RakuPM::Platform` —— 平台差异的唯一出处
- 要解决的问题:`if $*DISTRO.is-win { ... } else { ... }` 在当前平台上是**单向短路**的,
非当前平台那一半代码在开发机上**永远跑不到**,错误也就永远测不出来。项目真实栽过:
`MD5.system-md5` 里写了 `$*DISTRO.is-macosx`(`Distro` 根本没这个方法),Windows 上
`is-win` 为真、`elsif` 直接短路,全套测试全绿;一到 Linux 就
「No such method 'is-macosx'」崩在 Store 的内容指纹计算上。
- 做法:把「这台机器是什么平台」收敛成一个模块。`os-key` / `is-windows` / `is-macos` /
`path-list-sep` / `env-dirs` / `exec-suffixes` / `find-executable` / `native-path`(MSYS
路径转换)/ `which-command` / `kill-tree`(taskkill vs SIGKILL)/ `library-file-names` /
`library-search-paths` / `linux-family` / `md5-command` / `parse-md5-output` 全在这里,
一律**调用时**读 `$*DISTRO`。
- 因此测试能用 `my $*DISTRO = Distro.new(:name('linux'))` 词法遮蔽,在任意一台机器上把
三个平台的分支**真跑一遍**(Raku 的动态变量查找是运行期的,能穿透进被调模块)。
- 消费方全部改为调用它(`MD5` / `NativeLib` / `Client` / `Client::Tester` /
`Client::Reporter` / `Client::SelfManager` / `Installer` / `Distribution`)。
`NativeLib` 的公开方法(`os-key` / `os-label` / `package-key` / `candidates` /
`search-paths`)签名不变,只把实现委托过去。
- 途中修掉两个真实缺陷:① `os-key` 若只信 `$*DISTRO.is-win`,遮蔽成 `win32` 会被误判成
linux(Rakudo 只把 name `mswin32` 判成 win)→ 补 name 判据;② `native-path` 原先**无
条件**做 MSYS 转换,在 Linux 上会把合法的绝对路径 `/c/foo` 篡改成 `C:\foo` → 改为按平台门控。
- `--os=` 现在**校验取值**:只认 win32/windows、darwin/macos/osx、linux/unix;拼错直接报错。
(`resolve-os` 对认不出的值会回落到本机 —— 静默判错平台比报错更糟。)
- 回归:新增 `t/platform.t`(94 例,三平台 × 每个函数 + 消费方跟随遮蔽 + 真实 md5 输出
样本解析 + kill-tree 契约与真杀 + 遮蔽不泄漏)、`t/platform-centralized.t`(12 例,
禁止 `lib/` 除 Platform 外再直接用 `$*DISTRO`/`$*KERNEL`,并反向要求 Platform 自己确实在读)。
### 修复:`raku-pm version` 在 Linux 上的 `Use of Nil in string context`
- 现场:0.46.1 在 Linux 上每次 `raku-pm version` 都在 stderr 多一行警告,报的行号指向
`bin/raku-pm.raku` 第 76 行 —— 而那行看起来毫无字符串插值。
- 根因:76-77 行是**一条跨行语句**
`return ($d.meta // ...) if $d.meta eq 'RakuPM';`。
`eq` 是**字符串比较**,会对左操作数调 `.Str`;某些已装发行版的 meta 里没有 `name`
字段 → `Nil` → 警告。Rakudo 报的是这条语句的**起始行**,所以指到 76。
- 改法:`next unless (($d.meta // '') eq 'RakuPM')`。项目里 20+ 处读 CUR 元数据的
地方本来就都写了 `// ''`,只有 CLI 这一处漏了。
- 新增 `t/meta-definedness.t`(8 例):同一份探针只差一行守卫,证明那行守卫是承重的;
外加全库静态检查(禁止 `meta<...>` 直接 `eq`/`ne`),并修掉 `xt/skip-installed.t`
里最后一处同类写法。
### 修复:Linux 上的测试超时看门狗从未生效(做 H9 时挖出来的)
- 现场:把 Tester 的超时杀进程抽成 `Platform.kill-tree` 后,`xt/tester-async.t` 场景 3
(`sleep 30` 的测试应被看门狗杀掉、安装应在远小于 30s 内返回)**跑了 60.95s**。
- 查下去发现两件事:① 我抽出来时把签名写成 `Proc $p`,而调用点传的是
`$proc.start()` 返回的 **Promise** → 类型检查失败 → 异常在 Promise 的 `.then`
回调里被静默吞掉 → 根本没杀;② 更要紧的是**回看基线**:0.46.1 的 Unix 分支写的是
`try $p.kill(9)`,而那里的 `$p` **本来就是 Promise**(`Promise` 没有 `.kill`)——
也就是说**超时看门狗在 Linux/macOS 上从来没有生效过**,只是被 `try` 吞得无声无息。
Windows 分支只用 `$pid`(走 taskkill)、碰都不碰这个参数,所以开发机上永远发现不了。
- 改法:`kill-tree` 的第一个参数改成 `$proc where *.can('kill')`(既在**平台分支之前**
拒绝传错对象,又允许测试传替身对象),调用点改传 `$proc`。
- 这条也顺手说明了 H9 的价值:它与 `is-macosx` 那次是同一类 —— **非当前平台的分支
没有任何验证**,错误就长期潜伏在用户那边。
- 回归:`t/platform.t` 新增三个断言(误传 Promise 立刻抛;Unix 分支调 `kill(9)`;
Windows 分支不调 `.kill`),并**真起一个睡 3600s 的子进程真杀一次**(0.2s 见效)。
变异验证:把 Unix 分支改成 `kill(0)` → 变红;把 Tester 调用点退回传 Promise →
`xt/tester-async.t` 场景 3 变红(即本次那个回归本身)。
## 0.46.1 - 2026-09-16
修 0.46.0 引入的检查器健壮性隐患:**heredoc 遮蔽改为两遍扫描**。
- 问题:`mask-heredocs` 原本是单遍扫描 —— 只要起始判据误把某行当成 heredoc 起始
(典型情形:**被扫描项目的文档注释里恰好列了 `q:to/END/` 这类写法**),就会一路
遮蔽到文件末尾、或下一次偶然出现该标记的行,**静默吞掉大片真实代码**,而检查器
照样输出「通过」。这正是作者第一版翻车的原因(NativeLib 的注释里列了各种 heredoc
写法 → 273 行的 `has Str $.os` 整行变空格,属性名一个都收不到)。
- 改法:**先扫描确认「结束标记真的存在」,再遮蔽**。找不到结束标记就**不当作 heredoc**
—— 「起始判据误判」于是自然退化为「没找到」,不再有吞掉整个文件的后果。另加 500 行
上限作防御。
- 回归:`t/named-arg-check.t` 新增第 ⑫ 例「有起始行、却无结束标记时不能吞掉后面的
代码」(单遍实现会在这一例变红),共 13 例。
## 0.46.0 - 2026-09-16
落地审查报告遗留项里优先级最高的一条:**具名实参名的静态检查**。做它的过程中又抓
出一类真实的跨平台缺陷(`mkdir` 的 `:parents`/`:p` 是无效参数)。**无破坏性变更**。
### 新能力:具名实参静态检查(`tools/check-named-args.raku`)
- 要抓的问题:Raku 的 **method 会静默忽略未知具名实参**(而 sub 会报错)——
`$o.m(:zzz(9))` 不报错、不警告,参数凭空失效。项目已因此栽过两次:
`uninstall($name, :$ver)`(形参其实叫 `:version`,整包版本被删)、
`self-upgrade(:$src)`(形参叫 `:source`,那条分支永不走到)。
- 做法:全项目收集「已声明的具名名字」(routine 形参 + `has` 属性名),再扫所有
colonpair,名字不在集合里就报。**全程字符级扫描** —— 不用正则配命名捕获,
那是 0.44.0 那次失败的死因(命名捕获配字符类会「匹配上了但捕获是 Nil」→
全体误判为通过 → 检查器变假绿)。
- 已接入回归门禁(`run-suite.raku` 的静态检查一节;`--docs-only` / `--precommit`
也会跑);新增 `t/named-arg-check.t`(12 例,每个场景都是「喂人造源码 →
断言报 / 不报」)与 `tools/check-named-args.raku --verbose`。
- 白名单 `%ALLOWED` 每条都写了理由;新增前先问「这个名字能不能在项目源码里找到
声明」,能就别加(说明是扫描器漏收)。
### 顺带修掉的两类「静默失效」(检查器第一次运行就报出来的)
- **`mkdir(:parents)` / `mkdir(:p)` 是无效参数**:Rakudo 2026.08 的
`IO::Path.mkdir` 实测签名是 `:(IO::Path:D $: Int(Any) $mode = 511, *%_)` ——
只有 `$mode`,`:parents` / `:p` 全被 `*%_` 吞掉(不报错、也不生效)。Windows 上
因 `CreateDirectoryW` 会自动建父目录而「看起来正常」,**Linux 上 `mkdir(2)` 不建
父目录**,`//` 这类深层路径会直接失败 —— 属于「只在
Linux/CI 上炸、本机 Windows 测不出来」的一类。
- **新增 `RakuPM::Fs.mkdir-p`**:逐级创建目录(等价 `mkdir -p`),返回 `$p` 以便
链式调用。lib/ 里 25 处 `.mkdir` 全部改用它(逐个判断父目录是否已存在不现实,
统一走逐级创建最稳)。
### 修掉一个既有测试的偶发失败(与本次改动无关,顺手治好)
`xt/new-scaffold.t` 的工作目录只用 `$*PID` 命名且**从不清理**:TMPDIR 里攒了十几个
`raku-pm-new-xt-*` 残骸后,一旦 PID 重用就会撞上上次的 `Foo-Bar`,`new` 因「目标目录
已存在」而退出 1(单独跑却总是绿的)。改为「PID + 时间戳」并把工作目录纳入 `LEAVE` 清理。
## 0.45.0 - 2026-09-16
审查报告里的 **H6**(`with-write-lock` 用环境变量广播绕过子进程锁 —— 设计脆弱)
落地。**无破坏性变更**(`--no-lock` / `RAKUPM_NO_LOCK` 的用法与语义不变)。
### 变更:锁的「继承」从广播开关改为校验凭证
- 问题:持锁进程原先设 `RAKUPM_NO_LOCK=1` 给子进程「开绿灯」(`Build.pm` 里再调
`raku-pm` 是常态,不这样做会自锁到 600s 超时)。那是个**设了就开门**的全局布尔
开关 —— 任何继承了该变量的进程都自动拿到免锁权限:用户随手 `export` 一下就绕过
了锁;而且**装 raku-pm 自己时会跑它自带的 `t/`**,子进程继承该变量让
`t/client-write-lock.t` 的真锁从未持起、3 条断言恒真(假绿)。
- 现在:持锁时下发一张随机**凭证**(锁旁 `.token` 文件 + `RAKUPM_LOCK_TOKEN`
环境变量)。子进程走 `FileLock.should-bypass`,要求三件事同时成立才免锁放行:
① 带着 token;② 盘上 token 与它一致;③ **锁此刻确实被占**。
伪造不了,且凭证随锁作废 —— 父进程一放锁,晚到的子进程照样排队。
- `RAKUPM_NO_LOCK` 保留但**只作**用户逃生舱;程序内部不再设置它。
同进程嵌套也不再依赖环境变量,交回 `FileLock` 的引用计数
(原先内层会完全绕过 `acquire`,连计数都不加)。
- 回归:`t/file-lock-token.t`(20 例)覆盖凭证生命周期、同进程重入,以及跨进程的
「正确凭证放行 / 伪造凭证被挡」;`t/client-write-lock.t` 场景 4 改为断言新机制。
两者都做过**变异验证**(弱化判定 / 退回广播 / 不校验锁被占 / 不删凭证文件,
各自令对应断言变红)。
## 0.44.0 - 2026-09-16
审查报告里的 **H5**(`verify` 补记构建痕迹)落地,并在过程中**顺带查出并修掉
两个真实的静默失效缺陷**。**无破坏性变更**(store 只多一个旁车文件,旧条目不受影响)。
### 新能力:构建痕迹 `build.json` 旁车(H5)
- 背景(此前 README 里被诚实标注为「查不出来」的一类):`Store` 只把**磁盘上真实
存在**的资源写进 store 的 `META6.json`(必须如此 —— 声明了却找不到会让
`CUR::Installation` 装不上)。于是「构建没跑成功 → 声明过的资源压根没生成」
在元数据里被**静默剔除**,事后看那份 `META6.json` 干干净净,`verify` 无从判断。
- 现在 `Store.put` 落一个 `build.json` 旁车,记下:构建阶段做了什么
(`mode` = `built` / `skipped-no-build` / `not-needed` / `not-recorded`、
成败、builder 名 / Build 文件名、用的 raku 与**编译器发行版号**),以及
`resources-declared` / `resources-present` / `resources-missing` ——
**被剔掉的是哪些,原样留证**。
- 它**不计入内容指纹**(`!fingerprint` 里与 `.digest` 同级跳过):是「条目怎么
产出的」的记录,不是发行版内容;带时间戳,计入指纹会让每次重算都漂移而误报
「内容已被修改」。已用测试钉死「改写 build.json 不影响 verify」「重新入库指纹不变」。
- `Store.!meta6-for` 补写 `builder` 字段:store 作为快照不该丢掉「这个包需要构建」
这个事实,否则 `verify` 判断不了「一个没有痕迹的条目本不该有痕迹」。
- `Installer.consistency-issues` 新增 4 类检查(全部 `fixable=False`,需重装):
`missing-build-resource`(构建后资源仍没生成)、`no-build-trace`(声明了 builder
却没有痕迹)、`build-skipped`(用 `--no-build` 装进去的)、
`build-failed`(构建失败却仍入库)。
- `Client.!install-chain-inner` 里**两处**重复的构建调用(readmit 分支与新装分支)
收敛成唯一的 `!build-phase`:加一个字段不再要改两处 —— 正是本项目最高产的漏传
模式。`ensure-git-deps` 的调用时机按原样保留不动。
### 顺带修掉的两个静默失效缺陷(都源于下面那条 Rakudo 行为)
- **① 按版本卸载会变成整包卸载**:`Reporter.uninstall` 写的是
`self.installer.uninstall($name, :$ver)`,而 `Installer.uninstall` 的形参叫
`:version` —— 具名参数被静默丢弃,版本约束凭空消失,
`raku-pm uninstall Foo --version=1.0.0` 会把 Foo 的**所有版本**都删掉,
却照样打印「已卸载 Foo 1.0.0」。既有测试没抓到是因为 `t/multi-version.t` 走的是
`$inst.uninstall(:version(...))`,**绕过了出问题的那一层**。
- **② self-upgrade 的「在仓库里跑则 pull 当前仓库」永远走不到**:CLI 传
`:$src`,而 `SelfManager.self-upgrade` 的形参叫 `:source` —— 同样被静默丢弃,
开发者的本地提交被无视、直接去拉上游(与帮助文案承诺不符)。
- 新增 `t/named-arg-wiring.t`:① 走 **Client 公开 API** 做行为断言(变异验证过:
退回 `:$ver` 时它确实变红,且复现了「两个版本全没」);② 对 self-upgrade 的
调用形式做文本级钉(真跑要联网 + 改本机安装,不适合放进 `t/`)。
### 那条 Rakudo 行为(本轮最重要的发现)
本机 Rakudo(v2026.08)实测:`sub` 传未知具名参数会报
`Unexpected named argument`,但 **`method` / 私有方法 / role 方法一律静默忽略**
未知具名参数 —— 不报错、不警告、直接取默认值。也就是说**方法调用点上的具名实参
名字拼错,编译器与运行时都不会告诉你**。上面两个缺陷都落在同一个盲区里。
- 已把这条写进 `CODEBUDDY.md` 踩坑清单,并定下写法约定:
方法调用一律用具名 `name => value`(而不是 `:%x` 简写),除非形参确实同名。
- 曾尝试写一个「全域静态检查」测试把整类风险挡住,**但没能做成可靠的实现**
(手工解析签名时形参表收集始终为空,成了「假绿」—— 比没有检查更糟),
已撤下、不发布半成品。**遗留项**:把「具名实参名必须存在」做成可靠的
lint(思路与踩到的坑记在 `CODEBUDDY.md`)。
### 测试
- 新增 `t/build-trace.t`(27 例):构建成功 / 资源缺失 / `--no-build` / 无需构建 /
旧条目无痕迹 / 旁车不参与指纹,六种情形全覆盖。
- 新增 `t/named-arg-wiring.t`(6 例,含变异验证)。
- 两条新测试都按要求做了**修复前必然变红**的验证:`t/build-trace.t` 在未改动的
0.43.0 代码树上跑,报 `No such method 'facts'` 退出码 1。
## 0.43.0 - 2026-09-16
健壮性加固第二轮(承接 0.42.0 的重构):把「回归门禁」真正建起来,并拆掉 Installer
最后两块越界职责。**无破坏性变更** —— 公开 API、命令行用法、磁盘布局、以及生成出来的
wrapper 文本全部与 0.42.0 逐字节兼容。
### 新增:回归门禁(本轮最重要的东西)
- `tools/run-suite.raku`:**唯一**的测试判定口径。t/(逐个取真实退出码 + 强制洁净 ——
任何 `⚠` / `[RakuPM]` 生产级警告即判失败,因为 zef install 会跑这一批)、xt/(只判
成败)、文档纪律校验。默认**串行**(对齐 zef;`--jobs=N` 才并行,理由见 0.41.3)。
`prove6` 不进这条链路:它会把「Subtest N doesn't have a plan」这类无害解析告警当失败。
- **文档纪律纳入门禁**:META6 版本 ↔ README.md / README.en.md 的 `RakuPM:ver<>` 示例 ↔
Changes 顶部条目三者必须一致;`provides` 与 `lib/` 下 `.rakumod` 必须互为全集;
`bin` 清单文件必须存在;`depends.runtime.requires` 不得有空项。这些此前全靠人肉自觉,
已漂移过多次。
- `tools/git-hooks/pre-commit` + `tools/install-hooks.raku`:预提交跑「文档纪律 + 编译本次
暂存的文件」,秒级(不跑测试 —— 钩子一慢就会被 `--no-verify` 绕过)。
- `.workflow/master-pipeline.yml` 改为直接调同一个脚本:本地跑什么,CI 就跑什么,
不再出现「本地绿、CI 红」而让人怀疑门禁本身。
- 门禁本身做了**变异验证**:故意改坏版本号 / 多加一个未声明的 lib 文件 / 让 provides 指向
不存在的文件 / 让用例失败 / 让 t/ 喷警告 —— 五种情况都如实变红之后才恢复。不会变红的
门禁等于没有门禁(这是本项目 5 起「假绿测试」的直接对策)。
### 架构:拆掉 Installer 最后两块越界职责(1029 → 923 行)
- 新增 `RakuPM::Installer::ShellTemplates`:bin wrapper 的三段平台 shell 模板
(bash / cmd `.bat` / PowerShell `.ps1`)。原先它们是混在安装逻辑里的约 150 行字符串
拼接,平台细节(cmd 的二次解析、PATHEXT、GNU vs BSD `sort -V`、`RAKUPM_RAKU` 兜底)
与「把发行版交给 CUR」纠缠在一起。提成**无状态函数**后可脱离文件系统与 CUR 单独单测。
- 新增 `RakuPM::Installer::Generations`:`/generations/` 的格式与生命周期
(id 分配 / `before.json` / `meta.json` / `manifest.json` / `current` / 裁剪),原先散在
Installer 的 6 个私有方法里。提成**值对象**(只吃一个 `$.dir`)。
- 为什么不用 role:Raku 的 role 只能访问自己在 role 内声明的属性,访问宿主的 `$!target`
会直接编译报错(`Attribute $!target not declared in role`)。拆共享私有状态的代码,
应当把状态提炼成值对象、把纯逻辑提炼成无状态函数。
- **验收标准是「输出逐字节相同」**:用 `git archive HEAD` 取 0.42.0 的代码树,在新旧两棵树
上用**固定的 target 路径**各生成一次 wrapper,比 md5 —— bash / `.bat` / `.ps1` 三轮全部
`IDENTICAL`。拆这种「生成用户可见文本」的代码,只看测试是否绿是不够的。
- `Generations.finish` 加 `// ''` 守卫:调用方若误把 Hash 展平后传进来,不再喷一屏
`Use of Nil in string context`,只写空。
### 测试
- 新增 `t/installer-generations.t`(23 例):id 分配、discard 后编号复用(刻意不留空洞)、
prune 只保留最新且永不裁掉当前世代。
- 新增 `t/installer-shell-templates.t`(19 例):钉住三个模板的关键结构(LF vs CRLF、
`inst#` 前缀、fallback 分支、`@args`、GNU/BSD sort 两条路)。
- 新增 `t/run-suite-docs.t`:门禁自身的守护 —— 文档纪律一旦漂移,`zef install` 的测试阶段
就会红,而不是等用户发现。
- 门禁接入后全量:t/ 55 个用例全绿,xt/ 全绿。
### 文档
- `CODEBUDDY.md`:新增「回归门禁」章节;把「新增/修改 if-else 或 fallback 分支,必须有人为
触发该分支的测试」写成规则(本项目 5 起假绿全部命中同一判据:bug 存在时那条测试也是绿的);
补 role 属性访问限制、逐字节等价验收、「`[%(...)]` 会摊平」等踩坑。
- 两份 README 的模块清单同步新增的两个模块。
- 模块数量与用例数量在文档里改为「以 `META6.json` / 目录为准」,不再硬编码,避免再次漂移。
## 0.42.0 - 2026-09-16
以全项目《架构与缺陷审查报告》为依据的重构 + 健壮性加固:新增 3 个模块、修 5 个
既有/潜在缺陷、补 5 个守护测试。**无破坏性变更** —— 公开 API、命令行用法与磁盘
布局都保持兼容。
### 架构:消除三类「根因级」重复
- 新增 `RakuPM::Fs`:递归删除 / 复制 / 遍历 / 体积的**单一实现处**。
原先 `rm-tree` 有 4 份(Installer / Store / Cleaner / SelfManager)、
`copy-tree` 2 份、目录遍历 5 份,各自的跳过规则与异常兜底互不一致;其中 3 份用
惰性 `for $d.dir`,会在整个递归期间持住目录句柄,Windows 上导致调用方随后
`rmdir` 失败(曾因此残留空目录)。现在一律**急切收集** + 统一跳过
`.git/.hg/.svn/.precomp/blib` + 按绝对路径稳定排序。
`copy-tree` 另加一道防呆:拒绝「目标目录在源目录内部」——那会自我复制到路径
长度爆炸(实测 "Failed to mkdir: name too long" 并留下几百层目录)。
- 新增 `RakuPM::InstallOptions`:安装链的选项载体。7+ 个布尔开关原先要在
install → !install-local-dist → !install-chain → !install-chain-inner →
!ensure-phase-deps / !ensure-git-deps 共 6 层签名里各抄一遍,
加一个开关要改 6 处、漏一处就出现「开关传了但不起作用」。
- 新增 `RakuPM::Ledger`:账本(installed.json)的**唯一**读写入口。原先同一套
「解析 → 守卫 → 改 → 序列化」样板在 Installer 里复制了 6 处,账本格式一变就要
同步 6 处。`Installer` 上原有公开方法保留为薄转发,外部调用方不受影响。
- `Distribution` 新增**单一构造入口** `clone-with` / `from-index-row`,取消
`Ecosystem.fetch-distribution` 与 `Git.!distribution-from-git`(两分支)的手工
挑字段重建 —— 那是本项目最高产的缺陷模式(resources → git-deps → builder →
build-depends → test-depends 依次漏传,每次都造成真实故障)。
### 修复
- **native 库扫描漏报(潜在缺陷,已实证)**:`NativeLib.runtime-native-libs` 的
逐行 POD 启发式,在 heredoc / 多行字符串正文含 `=` 行首内容时(内嵌 Markdown /
配置片段很常见)会进入「缩写 POD」模式并吞到下一个空行,把紧随其后的真实
`is native(...)` 声明一起漏掉 → 不报缺库 → 安装照跑 → 运行期才以
`Slip (Empty)` / `Cannot locate native library` 崩(正是这个检查要消灭的形态)。
修法:① POD 指令必须**顶格**(Raku 规定指令是行首第一个字符);② 识别 heredoc
起始(`q:to/…/`、`:to<…>`、裸 `to/…/`)并整段跳过正文。
- **两处开关漏传**:`!ensure-git-deps` 漏 `no-native-check`;
`!ensure-phase-deps` 漏 `no-native-check` 与 `allow-test-failure`。用户给 git /
构建 / 测试依赖加这些开关时子链不认,仍会跑本机库检查并可因此 die、把整条链
回滚(用户明明关了开关却照样被拦)。收成 InstallOptions 后结构性消灭。
- **串行模式下复验提示语错**:默认已改为串行,提示却仍写「并发首跑的失败判定为
假失败」,自相矛盾。改为与 jobs 无关的准确措辞。
(复验本身不能因默认串行而删:它还要对付**上游自己 flaky** 的测试 ——
实测 Concurrent::Stack 的 stress.rakutest 连跑 10 次挂 3 次,串行复验照样能翻盘;
xt/flaky-retry.t 会红。)
- **CLI 未知旗标静默忽略**:选项原先是开放式接收(任何 `--key` 都进 `%opts`),
于是 `raku-pm install Foo --no-tets` 会照常跑测试(用户以为跳过了)、
`--verison=1.2` 被静默丢弃。现在按命令白名单校验,未知旗标报错并列出可用选项。
顺带修掉:旗标写在**命令位之前**时,Raku MAIN 会抛一句莫名的内部错误
("rindex search target requires a concrete string, but got null")——
用 `*%early` 接住并并入 `%opts`,两个位置从此等价(原先只有命令之后才有效)。
- **既有 xt 测试失败**:`xt/dist-bump-cli.t` 硬编码 MSYS 的 `/c/…` 路径转换,但
Windows 自带 tar 是 bsdtar(认原生 `C:/…`)→ tar 报错、成员列表为空、断言假失败。
改用项目自己的 `Author.tar-archive-list`(自动适配 GNU / BSD 两种 tar)。
### 新增测试(守护)
- `t/distribution-fields.t`:**反射式**遍历 `Distribution` 的全部属性,断言
`clone-with` 逐个保留;并断言 `from-index-row` 对「每个字段都有值」的索引行全部
解析出来。以后新增字段却忘了同步这两个入口,测试立刻红。
- `t/ledger.t`(30 例):账本主键由账本自己保证、`modify`(upsert)与
`modify-existing` 的区别、版本 semver 排序(0.9.2 < 0.13.0)、老格式兼容、
按版本 / 整包删除、损坏账本的可读报错。
- `xt/install-opts.t`(9 例):用「构建依赖的源码声明了缺失的 native 库」
「测试依赖自带的测试必挂」两个真实场景,断言用户的开关**确实传到依赖子链**
(该文件在修复前是红的)。
- `t/cli-flags.t`(24 例):未知旗标两种位置都报错、合法旗标两种位置都生效、
5 个帮助入口都可用、未知命令报「未知命令」。
- `t/native-lib-scan.t` 扩到 12 例:新增 heredoc 夹具(修复前必红)。
### 文档
- 两份 README 的架构图补上 3 个新模块;版本示例同步 0.42.0。
- `CODEBUDDY.md` 补入本轮的新踩坑(heredoc/POD 扫描、InstallOptions 漏传、
账本主键、tar 形态差异)。
## 0.41.4 - 2026-09-15
- 修复 self-upgrade 的「永远升不上去」死循环:旧版 self-upgrade 跑【旧版】扫描器
去检查【全新】源码,而 raku-pm 自己的文档注释 / POD 里有 `is native('foo')` /
`is native('...')` 这样的【样例】,被误当成真实依赖、报「缺少 foo/... 」而放弃
自装,导致 0.41.3 永远装不上去。两处修正:
- `self-upgrade` 现在【默认跳过】运行时本地库检查(raku-pm 是纯 Raku、无
native 依赖,检查对它无意义且自我阻碍)。这是打破鸡生蛋死循环的关键。
- `NativeLib.runtime-native-libs` 进一步跳过 **Raku POD 文档块**(`=begin/=end`
定界块、`=for`/`=head` 等缩写块到空行结束),不再只剥 `#` 行注释;扫描器
对任意包的文档样例都免疫(t/native-lib-scan.t 新增 POD 用例)。
## 0.41.3 - 2026-09-15
- 修复 native 库扫描器误报:逐行匹配 `is native('...')` 前先剥离 `#` 行注释,
否则模块文档注释里的样板 `is native('foo')` / `is native('...')` 会被当成真实
依赖,导致 `self-upgrade` 在自身(及无 native 依赖的纯 Raku 包)上误报缺少
duckdb / foo / ... 而放弃安装。同时递归收集目录条目改为急切收集,避免 Windows
上目录句柄泄漏导致清理时 rmdir 失败。
- 测试执行默认改为串行,对齐 zef:zef 逐个跑测试文件、不并行;此前默认并发会让
多个 raku 子进程同时首次编译同一份 lib/*.rakumod,偶发触发 rakudo 预编译缓存
竞态(表现为 `===SORRY!=== Error while compiling` 假失败),这正是「zef 能装、
raku-pm 频频报错」的主因。需要速度的用户用 `--test-jobs=N`(N>1)显式并行。
## 0.41.2 - 2026-09-15
- 防御性预编译预热:并发测试前先串行 `raku -c` 写热 precomp 缓存,规避并发首跑
多进程同时首次编译同一份 lib/*.rakumod 的竞态(可用 RAKUPM_NO_PRECOMP_WARMUP=1 跳过)。
- 面向原生 Windows 重写 tar 路径处理:`!wsl` 废弃,改 `!tar-is-gnu` / `!tar-path`
探测原生 BSD libarchive tar 与 GNU tar 的差异(盘符路径转换),不再假设 Git Bash。
## 0.41.1
- 并发测试失败串行复验时,复验轮日志写 `.flake.log` 而非覆盖首跑
`.log`,保留首跑(可能是确定性编译错误)全文用于诊断;复验通过提示改为
“并发首跑的失败判定为假失败”,不再臆断用例本身不稳定。
## 0.41.0
- 作者侧新增 `raku-pm dist`(打包 sdist,内联可选 Build.pm)与 `bump`(版本编排)命令。