raku-pm — 一个教学用 Raku 包管理器
English | 简体中文
架构与深度设计 · 快速上手
仿照 Raku 官方包管理器 zef 的分层可插拔架构,
用约 3700 行 Raku(19 个模块 + 1 个 CLI,不含空行与注释)实现了一个最小可用
(MVP)的包管理器,用于理解包管理器的核心原理。
仓库地址:https://gitee.com/skyter10086/raku-pm
安装本项目
raku-pm 自己也是个标准的 Raku 发行版,根目录带 META6.json,所以能被 zef 安装:
git clone https://gitee.com/skyter10086/raku-pm.git
cd raku-pm
zef install . # 先 --dry 干跑也行
装完会多出一个 raku-pm 命令(Windows 下 zef 生成 raku-pm.exe,在 site/bin 里):
raku-pm # 无参数:打印帮助,退出码 0
raku-pm help # 同上(推荐)
raku-pm install Web::App # 按模块名装(从本地仓库)
RAKUPM_REPO=repo raku-pm install Web::App # 显式指定仓库根
raku-pm install https://github.com/raku-community-modules/File-Temp.git
# 从 git 仓库装(含依赖,与仓库安装同流程)
zef uninstall RakuPM # 卸载
小坑:raku-pm --help 在 PowerShell 下会被 zef wrapper 拦截成"Usage: ..."的默认提示(wrapper 用 *% 命名参数吃掉了 --help),所以推荐用 raku-pm help 或直接 raku-pm 看完整帮助。
推荐:直接跑源码,不用 zef 装
raku bin/raku-pm.raku list
这条路实际上比 zef install . 更稳,建议在下面这些情况优先用它:
- 机器上装了多个 raku(rakubrew / rakubrew 之类管多版本的)
- 装完包但运行时报
Missing or wrong version of dependency ...
原因是:直接跑源码时,预编译写在项目本地的 lib/.precomp,用的就是当前这个 raku,
天然自洽;而 zef install 会把包装进 site/,预编译由 zef 当时用的那个 raku 来编 ——
如果 zef 和运行时不是同一个 raku(多版本环境下很常见),指纹就对不上。
想要 raku-pm 这个命令,自己写个 wrapper 即可,不必惊动 zef:
cat > ~/.local/bin/raku-pm <<'EOF'
#!/usr/bin/env bash
exec raku "$HOME/raku-pm/bin/raku-pm.raku" "$@"
EOF
chmod +x ~/.local/bin/raku-pm
(把 $HOME/raku-pm 换成你实际 clone 的路径。)
两个环境变量控制路径,不设就用默认值:
| 变量 | 作用 | 默认值 |
|---|
RAKUPM_REPO | 本地仓库根(扫描其下 META6.json) | 当前目录 . |
RAKUPM_TARGET | 安装目标根(store / site / git-cache / 锁文件) | ~/.raku-pm |
从 git URL 安装
raku-pm 可以直接从 git 仓库装一个发行版,并且会递归解析它的依赖,
走的是与仓库安装完全相同的一套流程。
raku-pm install https://github.com/raku-community-modules/File-Temp.git
输出:
==> 克隆到本地缓存:.../git-cache/github.com/raku-community-modules/File-Temp.git
将安装 2 个发行版(自底向上,依赖优先):
· File::Directory::Tree:ver<0.2>:auth<zef:raku-community-modules>
· File::Temp:ver<0.0.0-ad3445e>:auth<git:github.com> ← git: https://github.com/...
==> 下载源码包 [zef]:https://360.zef.pm/F/IL/FILE_DIRECTORY_TREE/8f155b8a....tar.gz
==> 存入存储:File::Directory::Tree 0.2 (24be1140)
==> 已激活 File::Directory::Tree 0.2
==> 存入存储:File::Temp 0.0.0-ad3445e (be25c423)
==> 已激活 File::Temp 0.0.0-ad3445e
两条安装路径的差异只有一处:目标包的源码从哪来。
| 仓库安装 | git 安装 |
|---|
| 目标包从哪找 | 各生态索引 | git clone 出来的目录 |
| 依赖从哪找 | 各生态索引 | 各生态索引(同一套 Resolver) |
| 依赖解析 | 递归 + 拓扑排序 | 完全相同 |
| 逐包动作 | 构建 → 入 store → 跑测试 → 激活 | 完全相同 |
| 写锁 | 整条链 | 整条链 |
实现上就是这么对齐的:Client 把「逐包落地」抽成了 !install-chain,
接受一个 %source-override(键 "<name>\0<version>")来指明
「这个包的源码目录不在仓库里,用我给的这份」。git 模式只是往里塞了
clone 目录这一项。
三个细节:
- 依赖是一组根,不是一个根。所以给
Resolver 加了 resolve-all(@specs):
整批共享同一份已选表,因此同一模块只装一份、跨依赖的版本冲突能被检出
(A 要 Foo>=2 而 B 要 Foo<2 会报冲突);逐个调 resolve 再拼起来做不到这点。 - 目标包自己提供的模块不算外部依赖。有些包的 META6 会把自己提供的模块
也写进
depends;反过来,若依赖树里冒出另一个提供同名模块的发行版,
以 git 上这份为准(用户明确指定了要装它)。 - 目标包必然排最后——它依赖前面那些,它自己又不在任何仓库里,Resolver 看不见它。
要点:
- 仓库会被
git clone --depth 1 到 $RAKUPM_TARGET/git-cache/<host>/<repo>.git/,下次装同一仓库走 git pull --ff-only,不重新下载。 - 版本号固定用
0.0.0-<git-sha7>,auth 用 git:<host>——因为我们不解析 git tag 当版本,纯粹拿 HEAD。 - 解析
META6.json(Raku 标准格式);0.37.0 起只认 META6.json,不再回退我们自己的简化版 META.json(store 内部仍保留 META.json 作反向还原用)。 - 依赖同样受
--dry / --no-test / --locked / --no-native-check 控制,锁文件记录整条链。 - 仓库必须带
META6.json 才认;缺这个文件会报错。 depends 里写裸 git URL 也认(zef 同款非标准写法)。Draku 的 META6.json 就依赖
git://github.com/jkramer/p6-Text-Wrap.git —— URL 本身就是依赖,不能当模块名去仓库解析。
raku-pm 会把这类串收进 git-deps,在轮到声明它的包之前先 clone 下来、
当普通发行版走完整安装链(git:// / git@… / https://…/xxx.git 都认)。
※ clone 时 git:// 会自动升级成 https://:GitHub 已于 2022-01 关闭 git 协议
(9418 端口),Draku 这类老写法直接 clone 必失败,升级后语义等价(公开仓库)。
META6.json 就是 Raku 世界的"身份证"——zef 靠它知道这个发行版叫什么、有哪些模块、依赖什么。本项目声明了:
name/version/auth/api → 组成 Identity RakuPM:ver<0.76.0>:auth<zef:skyter10086>:api<1>,这是包管理器的主键provides → 模块名到文件的映射,zef 安装时据此把 .rakumod 放进模块搜索路径depends.runtime → JSON::Fast、HTTP::Tinyish(走系统 curl)、HTTP::Tiny(纯 Raku 兜底)、URI(MD5 指纹由内置的纯 Raku RakuPM::MD5 提供,零生态依赖)bin → bin/raku-pm.raku,装完软链(或复制)到 <target>/bin/raku-pm,把 bin/ 加进 PATH 就能直接敲 raku-pmtest-depends → Test,所以 zef install 会先跑 t/ 下的测试,挂了就不装
设计细节(架构 / 安装流程 / 原子安装与世代回滚 / 锁文件 / 并发 / 缓存回收 / 与 zef 对应等)已迁入 docs/architecture.md。
挑版本的三个坑
解析器(Resolver)在真实生态数据上踩过三个坑,这里的处理方式是本项目最"非玩具"的部分。
1. provides 会随版本漂移 —— 必须按版本校验
同一发行版的 provides 不是一成不变的。真实案例:
DBIish 0.5.5–0.6.1 曾把 NativeLibs 打在自己包里,0.6.2 之后拆分成了
独立的 NativeLibs 发行版。
于是模块 NativeLibs 同时出现在两个发行版的 provides 历史里。如果只按
"这个发行版某版本提供过"去挑最高版本,就会挑中 DBIish 0.6.8 —— 而它压根不含
NativeLibs,装完 DB::SQLite 却 use NativeLibs 失败。
解决办法是给仓库加了 versions-providing($module, $dist) 接口:只返回该版本确实
提供此模块的版本号。Repository::Ecosystem 直接扫索引行(快路径),
role 里还有一份通用兜底(逐个版本物化后检查 provides)。
2. 不同发行版的版本号没有可比性
上例中即便两个候选都合法,也不该直接比版本。规则是:
- 发行版名与模块名一致者优先 ——
NativeLibs 模块的正主是 NativeLibs 发行版; - 其次比版本高低(走 semver,不是字符串
cmp —— "0.9" > "0.10" 是错的); - 最后按名字字典序,保证结果稳定可复现。
3. :from<native> 是系统本地库,而且各系统长得不一样
DB::SQLite 依赖 sqlite3:from<native>,这是 zef 用来标记系统本地库的 phaser
(要的是 libsqlite3,不是 Raku 包)。raku-pm 不打包系统库,处理方式是:
- 解析时把这类依赖从 Raku 依赖表里摘出来,绝不拿它去查仓库;
- 安装时探测本机有没有,缺任何一个就直接放弃安装(提示缺哪些、给安装命令;
--no-native-check 可强行跳过,但运行时加载这些库仍会失败)。
运行时通过 NativeCall 直接加载的库也查(不只 :from<native>)。很多包
(典型如 Duckie 的 libduckdb)是源码里 is native('duckdb') 运行时加载的,
META6 里不写 :from<native>,静态依赖检查看不到它,只能等测试阶段加载库失败
才暴露(还附一大段调用栈)。raku-pm 在取到源码后、跑测试前扫描 lib//bin/
里的 is native('...'),把这些库名也纳入缺失检查,缺了同样直接放弃安装。
随包打进 resources/libraries/ 的库由 Rakudo 经 %?RESOURCES 解析,不算系统缺失,
不会被误杀。
关键在于同一个逻辑名在不同系统上是不同的文件:
| 逻辑名 | Windows | macOS | Linux |
|---|
sqlite3 | sqlite3.dll | libsqlite3.dylib | libsqlite3.so |
ssl | libssl-3-x64.dll | libssl.dylib | libssl.so |
libffi | libffi-8.dll | libffi.dylib | libffi.so |
zlib | zlib1.dll | libz.dylib | libz.so |
未知库 foo | foo.dll | libfoo.dylib | libfoo.so |
注意 Windows 是不需要 lib 前缀的那一支,直接照搬 Unix 规则会全军覆没。
搜索路径同样分系统:
Windows:<rakudo>/bin(raku.exe 所在目录,Windows 找 DLL 的第一站) →
System32/SysWOW64 → PATH 上的目录 → vcpkg 的 installed/*/bin
⚠ 第一条最容易漏:Windows 的 DLL 搜索顺序里,可执行文件所在目录排在 PATH
之前,所以 zef / 各发行版常把配套 DLL 直接放在 <rakudo>/bin 下
(moar.dll、readline.dll,以及拷进去的 sqlite3.dll 之类)。NativeCall 加载
得到,但只扫 PATH 的工具会把它报成「未找到」。raku-pm 已把这一路径放在最前。
macOS:DYLD_LIBRARY_PATH → /opt/homebrew/lib(Apple Silicon)→
/usr/local/lib(Intel)→ /opt/local/lib(MacPorts)→ /usr/lib
Linux:LD_LIBRARY_PATH → /usr/local/lib、/usr/lib →
Debian 系的多架构目录(/usr/lib/x86_64-linux-gnu 等,现代 Debian 把库全挪这儿了,
只搜 /usr/lib 会漏掉)→ 带版本号的 .so.N 用前缀匹配
安装命令也分系统,Linux 还细分发行版族(apt / dnf / pacman / apk / zypper 的
包名各不相同,给错命令等于没给)。认不出具体发行版时把各家族命令都列出来,
而不是退化成某一条(更不能退成 Windows 的命令)。
$ raku-pm install DB::SQLite
将安装 5 个发行版(自底向上,依赖优先):
· BitEnum:ver<0.5>
· Concurrent::Stack:ver<1.3>
· NativeLibs:ver<0.0.9>
· DB:ver<0.5>
· DB::SQLite:ver<0.7>
本地库依赖检查(Windows,raku-pm 不打包系统库):
✓ sqlite3 已在 D:\raptor\sqlite3.dll
→ 全部就绪。
缺失时(以 Linux 为例)会直接放弃安装,不再只警告了事(否则要跑到测试 /
加载阶段才炸,还附一大段内部调用栈):
本地库依赖检查(Linux/Unix,raku-pm 不打包系统库):
✗ sqlite3 未找到 —— 本系统需要 libsqlite3.so / libsqlite3.so.0
安装:apt install libsqlite3-dev
⚠ Linux/Unix 上缺少 1 个本地库,放弃安装:sqlite3
请先按上面的命令装好本地库,再重试安装。
(`raku-pm native <名字>` 可单独查询;如确需跳过本检查可加 --no-native-check,但运行时加载这些库仍会失败)
实现在 RakuPM::NativeLib,也可以单独查:
raku-pm native sqlite3 # 本机有没有
raku-pm native ssl curl --paths # 一次查多个,--paths 打印搜索路径
raku-pm native --installed # 扫锁文件,列出已装发行版声明的本地库
raku-pm native sqlite3 --os=linux --distro=ubuntu # 不切机器查另一台该装什么
最后一条能查别的系统,是因为 os / distro 可强制指定——这也让三套规则能在
同一台机器上被测试完整覆盖(t/native-lib.t)。
4. 还有两种「不是 Raku 包」的依赖:外部命令与平台条件依赖
除了 :from<native>,生态里还有两种写法,一样不能当 Raku 模块去查仓库:
:from<bin> / :from<Perl5>——外部命令或别的语言
Doc::TypeGraph 依赖 dot:from<bin>(graphviz 的 dot),Documentable 依赖
node:from<bin>,POSIX:from<Perl5> 则是 Perl 5 的模块。raku-pm 的处理与本地库
同构:解析时从 Raku 依赖表里摘出来,安装时在 PATH 里查一下,缺了就提示,
不阻断安装(Raku 代码照常装好):
外部命令依赖检查(raku-pm 不负责安装系统命令):
✗ dot 未在 PATH 中找到
⚠ 缺少 1 个外部命令:dot
请先用系统包管理器装好(apt / brew / choco …);Raku 代码照常会装好。
平台条件依赖 by-distro.name
zef 允许把依赖写成「按平台取」:
"depends": [ { "name": { "by-distro.name": { "": "", "mswin32": "Win32::Registry" } } } ]
意思是只有 $*DISTRO.name 为 mswin32 时才需要 Win32::Registry,其它平台取
兜底键 "" —— 也就是空。真实样本是 File::Which 1.0.4;不认这个语法时整块 Hash
会被当成模块名(字符串化后形如 by-distro.name\nmswin32 Win32::Registry),
于是 Linux / WSL 上装任何依赖它的包都会死在「找不到模块」——
raku-pm install Digest::SHA1::Native 就是这么挂的(LibraryMake → File::Which)。
现在的处理:按当前平台取键(优先 $*DISTRO.name,其次 *,最后 "" 兜底),
取不到就当没有这条依赖。注意方向不能反:平台条件依赖的含义是「别的平台
不用装」,把全部分支都装上会让 Linux 去装 Win32::Registry,同样必失败。
{"any": [...]} 多选一:逐个看,跳过外部命令与本地库,取第一个 Raku 模块。
快速开始
export RAKUPM_REPO=repo # 仓库目录
export RAKUPM_TARGET=~/.raku-pm # 安装目标(store/ + site/ + 锁文件)
安装当前目录的包(install .)
对应 zef 的 zef install .:
raku-pm install . # 装当前目录
raku-pm install ./some-dist # 装指定子目录
raku-pm install /abs/path/to/dist # 绝对路径也行
它读目录里的 META6.json 认出发行版,然后
递归解析依赖 → 自底向上装依赖 → 最后装这个包。目录里没有
META6.json 会直接报错。
别用 RAKUPM_REPO=<发行版根目录> 来替代。 Local 仓库的布局是
「一个仓库目录下挂多个发行版子目录」(repo/DistA/META6.json),它只扫子目录;
而一个发行版的 META6.json 就在自己的根目录上,扫不到。
实现上它和 git 安装共用同一段逻辑(!install-local-dist)—— 两者的本质相同:
目标包不在任何仓库里,Resolver 看不见它,只能由我们把它接到依赖链最后,
区别只是源码目录从哪来(clone 目录 vs 本地目录)。
指定版本(与 zef 语法一致)
安装规格串支持 zef 的那套 phaser 写法,解析器是独立的轻量模块 RakuPM::Spec
(Distribution.parse-spec 保留为委托)—— 独立出来是因为这套写法后来不止
install 在用:search / installed / store / version / fetch 这些查询与取源码
命令同样要认,而查询路径不该为了解析规格串把 Distribution 整条模块图拖起来。
它正是 identity() 的逆操作:
| 写法 | 含义 |
|---|
Foo:ver<1.2.3> | 精确 1.2.3 |
Foo@1.2.3 | @ 简写,等价上面 |
Foo --version=1.2.3 | 传统写法,等价于上面两种 |
Foo:ver<1.2+> | 至少 1.2(zef 的 + 后缀),挑满足约束的最高版 |
Foo:ver<1.2.3>:auth<zef:someone> | 版本 + 作者 |
Foo:ver<1.2.3>:auth<zef:x>:api<1> | 版本 + 作者 + api |
优先级:--version > 规格串里的 :ver<> > 位置参数约束。
两个要点:
:auth<> 真的会过滤,不是解析了就扔掉。auth 对不上会明确报错,不会偷偷
装另一份。这挺有用 —— 生态里同一个包换维护者的事并不少见,比如
Concurrent::Stack 的 1.1 是 cpan:JNTHN、1.3 是 zef:raku-community-modules。- 模块名里的
:: 不会被误判成 phaser(:ver<> 必须带 <...> 才算)。
这套写法不止 install 认 —— 查询/取源码命令同款:
| 命令 | 效果 |
|---|
search 'Foo:ver<1.2+>' | 仓库与已装两侧都按版本筛;同名在【满足约束】的版本里取最高 —— 最高版本不满足、低版本满足的包也能被搜出来 |
installed 'Foo:ver<1.0>' | 已装列表只看满足约束的版本;一个都不满足时明确提示(而不是悄悄空输出) |
installed 'Foo:auth<zef:x>' | 已装列表再按作者筛(auth 筛选作用于仓库行与账本条目) |
store 'Foo@1.0' | store 缓存只看满足约束的版本,附完整路径 |
version 'Foo:ver<1.0>' | 查已装版本时按约束筛(激活标记仍按全局激活版本算) |
fetch 'Foo:ver<1.2+>' | 取源码按约束挑版本(--version 仍为精确语义,优先级同 install) |
<> 是 shell 重定向符,各平台怎么写:
查询命令对「不是合法约束」的 ver(如 search foo@bar 里的 bar)有兜底:
整串当普通关键词,行为与不带版本写法的旧版完全一致 —— 坏约束不该把
所有版本悄悄筛没。
装旧版本需要先挂 REA —— zef 索引只收录当前版本:
raku -Ilib bin/raku-pm.raku repos add rea
raku -Ilib bin/raku-pm.raku install 'Concurrent::Stack:ver<1.1>'
将安装 1 个发行版(自底向上,依赖优先):
· Concurrent::Stack:ver<1.1>:auth<cpan:JNTHN>
安装
# 从默认生态(https://360.zef.pm)安装真实世界的包及其依赖
raku -Ilib bin/raku-pm.raku install File::Temp
# 安装模块及其依赖(取满足约束的最高版本)
raku -Ilib bin/raku-pm.raku install Web::App
# 指定精确版本(降级也是这个)—— 三种写法等价
raku -Ilib bin/raku-pm.raku install 'Concurrent::Stack:ver<1.1>'
raku -Ilib bin/raku-pm.raku install 'Concurrent::Stack@1.1'
raku -Ilib bin/raku-pm.raku install Concurrent::Stack --version=1.1
# 只解析不落地
raku -Ilib bin/raku-pm.raku install Web::App --dry
# 跳过 t/ 测试(某些包在 Windows 上测不过时用)
raku -Ilib bin/raku-pm.raku install File::Temp --no-test
# 严格模式:锁与解析结果不符就报错(CI 用)
raku -Ilib bin/raku-pm.raku install Web::App --locked
装完之后把 inst#<target>/site 加进模块搜索路径即可使用(raku-pm env 可直接给你这串):
export RAKULIB="inst#$HOME/.raku-pm/site"
raku -e 'use File::Temp; say tempfile'
# 精确选版本(前提是用 --version 装过旧版)
raku -e 'use File::Temp:ver<0.0.10>; say tempfile'
查询
raku -Ilib bin/raku-pm.raku list # 仓库里可安装的包
raku -Ilib bin/raku-pm.raku search Temp # 仓库索引 + 本地已装的包都搜([方括号] 标注来自哪个仓库)
# 已装但不在任何索引里的(如 RakuPM 自己)标 [已装]
raku -Ilib bin/raku-pm.raku search 'JSON::Fast:ver<1.0+>' # 查询词也认安装规格串:按版本筛
raku -Ilib bin/raku-pm.raku repos # 列出已配置的仓库
raku -Ilib bin/raku-pm.raku installed # 已安装及其版本
raku -Ilib bin/raku-pm.raku installed Foo # 只看 Foo 这个模块的所有版本(* 为激活)
raku -Ilib bin/raku-pm.raku installed Web::App # 模块名也认(精确):发行版叫 Web-App 也能找到
raku -Ilib bin/raku-pm.raku installed 'Foo:ver<1.0>' # 只看满足约束的版本;:auth<> 同理
raku -Ilib bin/raku-pm.raku store # 本地存储(* 为激活版本,附各版本完整路径)
raku -Ilib bin/raku-pm.raku store Foo # 只看 Foo 的缓存版本与各自完整路径
raku -Ilib bin/raku-pm.raku store Web::App # 模块名也认(精确),同 installed
raku -Ilib bin/raku-pm.raku store 'Foo@1.0' # @ 简写同款,按约束筛缓存版本
raku -Ilib bin/raku-pm.raku version 'Foo:ver<1.0>' # 查已装版本时按约束筛
raku -Ilib bin/raku-pm.raku which JSON::Fast # 这个模块实际从哪个仓库加载、版本多少(多份全列出)
raku -Ilib bin/raku-pm.raku which # 跨仓库冲突总览:哪些发行版装在多个仓库里
raku -Ilib bin/raku-pm.raku lock # 锁文件内容
raku -Ilib bin/raku-pm.raku verify # 校验已装包是否被篡改
匹配口径:search / store / installed / install 共用一份实现
(RakuPM::Repository::Matching,仓库层 role;RakuPM::Repository 与
RakuPM::Client::Reporter 都 does 它,改一处四处同步)。
关键词只匹配【发行版名 + provided 模块名】,不扫 description ——
描述里常写 "provides method xxx",扫它会把方法名也当成匹配项
(早先 search method 能搜到一堆包就是这个原因)。
search:模糊匹配 + 相关度排序(名精确 < 名前缀 < 名子串 <
模块名精确 < 模块名前缀 < 模块名子串);store / installed:精确匹配(精确发行名 或 精确模块名,
所以 installed Web::App 能找到发行版 Web-App,但 installed Web 不会误列);install:索引化的精确模块名反查(find-providers,等价上面的「模块名精确」档)。
这段判定是纯函数(只吃 名字 / 模块名 / 关键词),所以单测 t/match.t
不需要构造仓库、不需要 HTTP 桩。
仓库管理
raku -Ilib bin/raku-pm.raku repos add rea # 加 Raku 生态存档(别名)
raku -Ilib bin/raku-pm.raku repos add https://example.com/eco.json
raku -Ilib bin/raku-pm.raku repos remove rea
raku -Ilib bin/raku-pm.raku repos update # 强制刷新全部索引缓存(zef update 同款)
raku -Ilib bin/raku-pm.raku repos update rea # 只刷新 rea 这一个
维护
raku -Ilib bin/raku-pm.raku upgrade JSON # 升级到最新
raku -Ilib bin/raku-pm.raku uninstall JSON # 卸载
安装 Web::App 的输出示例:
将安装 3 个发行版:
· JSON:ver<2.0.0>:auth<demo:raku>
· HTTP-Client:ver<1.2.0>:auth<demo:raku>
· Web-App:ver<2.0.0>:auth<demo:raku>
==> 存入存储:JSON 2.0.0 (d47078e2)
==> 存入存储:HTTP-Client 1.2.0 (5732165b)
==> 存入存储:Web-App 2.0.0 (167f481e)
==> 已激活 JSON 2.0.0
==> 已激活 HTTP-Client 1.2.0
==> 已激活 Web-App 2.0.0
✓ 完成。锁文件已更新:./_demo/raku-pm.lock
从生态装真实包的输出(含下载与测试阶段):
将安装 2 个发行版(自底向上,依赖优先):
· File::Directory::Tree:ver<0.2>:auth<zef:raku-community-modules>
· File::Temp:ver<0.0.12>:auth<zef:raku-community-modules>
==> 下载源码包 [zef]:https://360.zef.pm/F/IL/FILE_DIRECTORY_TREE/8f155b8a....tar.gz
==> 存入存储:File::Directory::Tree 0.2 (24be1140)
· 运行测试:File::Directory::Tree 0.2
✓ 01-basic.rakutest
==> 已激活 File::Directory::Tree 0.2
==> 下载源码包 [zef]:https://360.zef.pm/F/IL/FILE_TEMP/9e97bc86....tar.gz
==> 存入存储:File::Temp 0.0.12 (3ad733c0)
· 运行测试:File::Temp 0.0.12
✓ 01-basic.rakutest
✓ 02-gc.rakutest
✓ 03-tempfile.rakutest
==> 已激活 File::Temp 0.0.12
✓ 完成。
装一个只在 REA 里有、zef 索引里没有的包(ADT,代数数据类型):
$ raku -Ilib bin/raku-pm.raku install ADT
在所有已配置的仓库里都找不到模块 'ADT'。
提示:· 用 `raku-pm repos add rea` 加上 Raku 生态存档(收录更全,含历史版本);
$ raku -Ilib bin/raku-pm.raku repos add rea
已添加仓库 'rea' → https://raw.githubusercontent.com/Raku/REA/main/META.json(优先级 #1)
$ raku -Ilib bin/raku-pm.raku install ADT
==> 拉取生态索引 [rea]:https://raw.githubusercontent.com/Raku/REA/main/META.json
将安装 1 个发行版(自底向上,依赖优先):
· ADT:ver<0.5>:auth<github:timo>
==> 下载源码包 [rea]:https://raw.githubusercontent.com/raku/REA/main/archive/A/ADT/...
==> 存入存储:ADT 0.5 (0cd38e3d)
· 运行测试:ADT 0.5
✓ 01-tree.t
✓ 02-EXPORT.t
✓ 03-positional.t
✓ 04-whitespace.t
==> 已激活 ADT 0.5
✓ 完成。
生产侧(作者侧)
前面各节都是消费侧——「装什么、装到哪」。还有一半生命周期是生产侧:
面向你自己那个发行版目录,维护它的元数据。raku-pm 自己提供的生产者命令:
raku-pm new <模块名> —— 创建新发行版骨架(见下)raku-pm refresh —— 以磁盘为准重建 META6.json 的 providesraku-pm check —— 发布前预检(上架前安全闸门,见下)raku-pm dist [路径] —— 打包 sdist(见下,发布前必需)raku-pm bump [路径] —— 版本编排(见下,递增版本 + 写 Changes)raku-pm publish [路径] —— 部署到本地目录仓库(见下,发布前必需先 dist)raku-pm login [--api-key=<key>] —— 生产侧凭据管理(见下,把生态 api-key 存本地;也可 --username/--password 联网登录换 key)
登录 / 发布:本地目录仓库的发布(publish)已实现;真实生态的上传在 0.57.0 完整
落地——login(凭据管理,含用户名+密码联网登录换 key)+ publish --remote
(multipart PUT 到 42.zef.pm/upload,与 fez upload 同端点同形态)。默认上传
端点 42.zef.pm(规范的 fez/zef 上传后端;raku-pm 默认用的 360.zef.pm 只是只读
镜像,无上传 API)。注:42.zef.pm 上传后端是私有服务,能否用取决于该生态服务端。
创建骨架:raku-pm new
new 是作者侧起点,在当前位置生成一个【全新】发行版目录。生成物:
META6.json —— name/version/auth/api/provides/depends/test-depends/bin 一应俱全
(version 默认 0.1.0,auth 默认 RAKUPM_AUTHOR_AUTH 或 zef:skyter10086)lib/<模块>.rakumod —— 主模块占位(unit module + 一个 our sub hello 冒烟)t/01-basic.rakutest —— use-ok 冒烟测试Changes —— 初始版本头.gitignore —— 忽略 .precompREADME.md —— 占位(仅 --with-readme)bin/<x>.raku —— 可执行(仅 --bin=<x>),META6 的 bin 同步含之
目录名默认 = 模块名里 :: → -(mi6 惯例);--into=<路径> 显式指定(. = 当前目录)。
目标目录若存在且非空、又没 --force,直接报错,绝不静默覆盖用户文件。
raku-pm new Foo::Bar # 创建 Foo-Bar/ 骨架
raku-pm new Foo::Bar --into=MyProj # 用指定目录名
raku-pm new Foo::Bar --with-readme --bin=foo --auth=github:me --version=0.2.0
raku-pm new Foo::Bar --dry # 只预览将要创建的文件
refresh 解决一个高频且很坑的问题:往 lib/ 里加了新模块、或改了模块文件名之后,
META6.json 里的 provides 就与磁盘不一致了。手工维护极易漏,而 provides 一旦
不对,别人装出来的包就 use 不到(你自己本地却看着没事)。
raku-pm refresh # 以磁盘为准重建当前目录的 provides
raku-pm refresh ./some-dist # 指定目录
raku-pm refresh --dry # 只预览将要发生的改动,不写文件
输出示例:
==> 重建 META6.json 的 provides:/path/to/Web-App
磁盘 lib/ 下扫到 3 个模块
· 加入(磁盘上有、声明里没有):
+ Web-App
+ Web::Legacy
+ Web::Router
· 剔除(声明里有、磁盘上没有,写了也装不上):
- Web::Ghost
✓ 已更新 /path/to/Web-App/META6.json
语义边界(刻意保守,只做能由磁盘唯一推导的事):
| 字段 | refresh 怎么处理 |
|---|
provides | 完全重建:扫 lib/ → 模块名 → 相对路径(.rakumod/.pm6 都认,.rakudoc 不算模块) |
resources / bin | 不动。资源靠显式声明管理(mi6 也是单用 fez resource);bin 是路径数组,无法从磁盘唯一确定 |
| 其余字段 | 原样保留(name / version / auth / depends / tags / source-url …) |
| 内容没变时 | 不写文件(不去白改 mtime) |
它不在 RakuPM::Client 树里,也不进写锁、不碰安装目标——只读你给的那个目录,
所以 CLI 在构造 Client 之前就把它分发出去了(不该为了改一个 META6.json 去初始化
target / store / 索引缓存)。
发布前预检:raku-pm check
check 是上架前安全闸门:在真去连服务器发布之前,先把「客户端能确定的硬错误」
全查一遍——等价于 fez review / mi6 release 前的 meta 校验,但覆盖更全。默认
纯本地、秒级、不联网;加 --remote 才联网查「同版本是否已发布」。
校验项:
- META6.json 合法:存在、是合法 JSON 对象
- name:存在且是合法模块名(
Foo / Foo::Bar) - version:存在且 ≠
*(* 是「任意版本」占位符,不能发布) - auth:存在,且与
RAKUPM_AUTHOR_AUTH 一致(auth 必须与上传账号相同,否则
服务器会拒;客户端只能预检,不能替代服务器校验。没设 RAKUPM_AUTHOR_AUTH
时只告警,由发布时服务器强制校验) - provides 与磁盘
lib/ 一致:声明里有、磁盘上没有 → 错误(装不上);
磁盘上有、声明里没有 → 警告(建议先 raku-pm refresh) - bin:列出的脚本文件都必须存在
- license / description(非 TODO)/ authors:建议项,警告不阻断
- depends / test-depends / build-depends:形态可解析(畸形 → 警告)
raku-pm check # 预检当前目录
raku-pm check ./some-dist # 指定目录
raku-pm check --remote # 额外联网查同版本是否已发布(同版本不可重传)
报告用 [✗] 标错误、[!] 标警告;任何硬错误都以非零退出,可直接接进
CI 发布门禁。--remote 查不到(离线 / 索引拉取失败)只告警、不阻断——最终的
「同版本不可重传」由服务器强制。
打包 sdist:raku-pm dist
dist 把校验过的发行版卷成可上传的源码归档 <name>-<version>.tar.gz
(这是 publish 的输入物)。顺序是:先跑 check 门禁(有硬错误直接拒,
绝不打包一个过不了预检的包)→ 可选地跑构建阶段 → 打 tar.gz。
- 构建阶段内联:发行版目录里若有
Build.pm(约定 class Build 的 build
方法),dist 会原地跑它一遍(原生扩展类靠它编出 resources/libraries/*.so,
产物会进 tar 包)。纯 Raku 发行版没有 Build.pm,自动跳过。用 --no-build
可强制跳过构建。 - 打包内容:META6 +
lib/ + bin/ + t/ + resources/ + LICENSE +
README 等;排除 .git / .hg / .svn / blib / .precomp / .DS_Store
等,并排除任何以 . 开头的隐藏项(.workbuddy / .workflow / .vscode /
.gitignore …)—— 免得被 .gitignore 掉的私有目录随发布包发出去。
自动排除正在生成的 tar 包自身,所以重复跑不会把旧包卷进新包。 .distignore 自定义排除清单:隐藏项过滤挡不住「名字正常的内部文件」
(AI 指令、内部路线图、内部手册…)。在发行版根放一个 .distignore,一行一条
相对路径(支持 # 注释、行尾注释、目录条目 dir/ 覆盖整棵子树、不支持通配符),
这些文件就保留在仓库、但不进发布包。列了却匹配不到任何文件的条目会提示到
stderr —— 防止拼错文件名导致「以为排除了、其实发了出去」。- 产物是源码分发:生态只传源码,消费者在自己机器上编译,因此不打预编译字节。
- 归档默认放当前目录,
--to=<目录> 指定别的落点;--dry 只预览不改文件。
raku-pm dist # 打包当前目录 → ./<name>-<version>.tar.gz
raku-pm dist ./some-dist # 指定目录
raku-pm dist --no-build # 跳过构建阶段
raku-pm dist --to=../out # 放到 ../out 下
部署到本地仓库:raku-pm publish
publish 把 dist 打出的 sdist 部署到一个本地目录仓库,使别人 / CI 能
raku-pm install 从那里装上它。这是 fez publish 在「本地 / 教学」场景下的等价物:
真实生态是把 sdist 上传到远端索引服务器(42.zef.pm,需要服务端支持 + 账号登录),
其中凭据管理(login)+ 请求构造(publish --remote --dry 预览)已在 M2 落地,
真实 multipart PUT 上传在 M3 实现;本地场景则是把源码放进一个目录仓库
(已用 raku-pm repos add 登记过的目录)。
- 目标目录:
--to=<repo-dir> 优先;否则读环境变量 RAKUPM_PUBLISH_REPO;
都没有就报错,让你显式指定。目标应是用 repos add 登记过的本地仓库,
install 才能从那里解析到这个包。 - 解包落点:
<repo>/<name>-<version>/(name 的 :: 换成 -)。一个子目录
一个发行版,与 Local 仓库布局一致;多版本各占一目录,可并存。 - 复用 / 重打:默认复用
dist 现打一份 sdist(内部先跑 check 门禁 + 可选构建);
用 --from=<tar.gz> 直接吃现成包,跳过重新打包。 - 碰撞检查:落点目录已存在会被拦(同版本不能重传)—— 先
raku-pm bump 升版本,
或加 --force 覆盖。--remote 走真实生态上传路径(M2:构造请求 + --dry 预览;
真实 PUT 在 M3);非 dry 会明确报错、延后到 M3,绝不静默发网络。 - 默认只试算?不——publish 默认直接部署(解包写目录);只想知道会落到哪用
--dry。删文件不可撤销,但发布只是「往仓库加一个目录」,出错时 rm 掉即可。 - 内部构建的临时 sdist 在部署成功后自动清理;
--from 给的包不会被删。
raku-pm publish # 打包当前目录并解包进 RAKUPM_PUBLISH_REPO(或报错提示 --to)
raku-pm publish --to=../my-dists # 解包进指定本地仓库
raku-pm publish --from=../out/Foo-1.0.0.tar.gz --to=../my-dists # 复用现成包
raku-pm publish --dry # 只打印落点,不写文件
raku-pm login --api-key=<key> # 把生态 api-key 存到 ~/.raku-pm/credentials.json
raku-pm login --username=me --password=secret # 联网登录 42.zef.pm/login 换 key 并保存
raku-pm publish --remote --dry # 预览真实生态上传请求(不联网)
raku-pm publish --remote # 真实上传到 42.zef.pm/upload(需先 login 或 RAKUPM_API_KEY)
发布之后,别的机器 raku-pm repos add <repo> 即可 raku-pm install <name> 拿到
你这一份——作者侧工作流 new → refresh → check → dist → publish 至此闭合。
版本编排:raku-pm bump
bump 按 semver 递增 META6.json 的 version,并在 Changes 顶部加一条
## X.Y.Z - DATE 版本头(没有 Changes 会自动从标题起创建)。约定与项目纪律一致:
patch=修 bug / minor=新能力 / major=磁盘布局或不兼容变更。
raku-pm bump # 默认 --patch:0.1.0 → 0.1.1
raku-pm bump --minor # 0.1.0 → 0.2.0
raku-pm bump --major # 0.1.0 → 1.0.0
raku-pm bump 2.5.0 # 位置参数像版本号 → 精确指定:→ 2.5.0
raku-pm bump --to=2.5.0 # 同上(显式写法)
raku-pm bump --date=2026-09-15 # 指定版本头日期(默认今天)
raku-pm bump --dry # 只预览,不改 META6 / Changes
当前版本无法推断(如 version: '*')时,bump 会报错要求用 --to= 显式指定。
测试
raku -Ilib t/version.t # 版本解析与约束匹配(15 项)
raku -Ilib t/store-lock.t # 存储、版本切换、锁文件(11 项)
raku -Ilib t/meta6-local.t # META6.json 解析与本地仓库(14 项)
raku -Ilib t/depends.t # depends 的多种写法归一化,含 :from<native>(18 项)
raku -Ilib t/by-distro-depends.t # 平台条件依赖 by-distro.name:按当前平台取舍,不再把 Hash 当模块名(13 项)
raku -Ilib t/external-deps.t # :from<bin>/:from<Perl5> 外部命令与 {"any":...} 多选一(17 项)
raku -Ilib t/file-lock.t # 跨进程文件锁:互斥 / 可重入 / 进程被杀自动释放 / 超时报错(20 项)
raku -Ilib t/client-write-lock.t # Client.with-write-lock:写命令真的串行、嵌套不自锁(9 项)
raku -Ilib t/clean.t # 缓存回收:受保护版本一个不删 / 默认试算 / --all 与 --name 与 --older-than(31 项)
raku -Ilib t/repos.t # 多仓库配置的增删改与持久化 + repos update 刷新索引缓存(36 项)
raku -Ilib t/resolve-provides.t # 挑版本的三个坑:provides 漂移 / 跨发行版比版本 / 环(18 项)
raku -Ilib t/native-lib.t # 本地库的跨系统文件名、搜索路径、安装命令(50 项)
raku -Ilib t/git-install.t # git 安装的依赖闭包:递归 / 去重 / 冲突 / 自提供模块(17 项)
raku -Ilib t/git-url-dep.t # depends 里的裸 git URL:识别进 git-deps + 先 clone 装完再装声明它的包 + 幂等(13 项)
raku -Ilib t/git-version.t # install-from-git 的版本约定:preserve-version 保留真版本 / 否则 0.0.0-*(3 项)
raku -Ilib t/multi-version.t # 多版本与 use :ver<>:真的挑到指定版本而不是静默给最新版(27 项)
raku -Ilib t/resources.t # resources/ 复制 + 装完 %?RESOURCES 可读(12 项)
raku -Ilib t/conflict-msg.t # 依赖冲突报错带完整请求链(16 项)
raku -Ilib t/build.t # 构建阶段:Build.pm / builder 字段 / build-depends(28 项)
raku -Ilib t/skip-installed.t # 跳过已装/已入库 + 老格式 store 自动重新入库 + 安装链账本自愈(39 项)
raku -Ilib t/bin-and-sort.t # bin wrapper + semver 排序 + 自包含 wrapper + PATH 扫描 + which/cygpath 失败自扫兜底 + site-bin promote + Windows .bat + raku 路径 fallback + 跨平台端到端(46 项)
raku -Ilib t/query-installed-store.t # installed [模块] / store [模块] 查询:版本与完整路径(24 项)
raku -Ilib t/cli-version.t # version / version <模块> 查已装与激活版本(9 项)
raku -Ilib t/spec.t # 规格串解析 RakuPM::Spec:各形态 parse + 约束合法性 + Distribution 委托等价(70 项)
raku -Ilib t/search-spec.t # 查询/取源码命令的规格串:installed/store/version/fetch/search 按版本与 auth 筛(61 项)
t/resolve-provides.t 用内存假仓库复现真实生态的畸形数据(DBIish 捆绑 NativeLibs、
互相依赖、自环、跨发行版版本比较),并对「扫索引快路径」与「role 通用兜底」
两种 versions-providing 实现跑同一组断言,确保二者行为一致。
t/native-lib.t 覆盖本地库在 win32 / darwin / linux 三套系统上的文件名映射、
搜索路径与安装命令。之所以能在同一台机器上测三套,是因为 RakuPM::NativeLib
的 os / distro 可以强制指定(同时也是 raku-pm native --os=... 的实现基础)。
t/git-install.t 用内存假仓库(菱形依赖图:App → LibA/LibB → Deep/Helper)
验证 git 安装的依赖闭包:递归到两层深、共享依赖只装一份、拓扑序正确、
跨根版本冲突能检出、目标包自己提供的模块不重复装。
它测的是 Client.resolve-deps-of,不需要真的 git clone。
t/git-url-dep.t 回归 Draku 那类 depends 写裸 git URL 的包:本地起两个 git
仓库(主包依赖另一个),主包 depends 里写 https://…/GitDep.git。依赖仓库的
git-cache 预置一份拷贝让嵌套 clone 走「复用缓存」分支,全程离线 —— 验证
URL 被收进 git-deps、轮到主包前先把它 clone 装完、账本同时记下两个包、
重复安装幂等。另有纯解析断言覆盖 git:// / git@… / https://…/xxx.git
等 URL 形态与「普通模块不受影响」。
t/spec.t 钉死规格串解析器本身:parse 的各形态(:ver<> / :auth<> /
:api<> / @ 简写 / 未知 phaser 不吞模块名)、constraint-valid 的合法与
非法边界(纯单词 "bar" 必须判非法,否则查询会把所有版本筛没),以及
Distribution.parse-spec 委托与 Spec.parse 的逐项等价。
t/search-spec.t 走真 CLI 子进程验证查询命令的规格串支持:installed 的
版本/auth 筛、store 的缓存筛、version 的约束筛、fetch 按约束挑版本取源码
(含 at-least 1.0+ 与取不到时的非 0 退出)、search 仓库与已装两侧筛。
子进程预写只含 local 条目的 repositories.json,全程离线。
t/multi-version.t 会真的起 raku 子进程去 use MultiVer:ver<...>,
用「模块自己声明的 :ver<> 里写的版本号」来断言到底加载了哪一份。
这是唯一能证明「没被静默换成最新版」的办法 —— 只查 installed.json 是不够的。
t/build.t 覆盖构建阶段:Legacy Build.pm(成功/返回假/抛异常三种出路)、
META6 builder 字段的类调用、build-depends 先装再构建,
以及最重要的端到端场景 —— Build 生成 resources/libraries/libdemo.dll,
装完后 %?RESOURCES<libraries/libdemo> 读到的就是构建产物
(META6 里声明【无扩展名】的名字,Rakudo 安装时按平台自动补后缀,
这是它对 native 资源的固有约定,zef 行为一致)。
t/skip-installed.t 覆盖「跳过已处理内容」:版本完全一致的已装包
(raku-pm 自己账本 + zef 的 site/home/vendor 仓库链)整个跳过、
已在 store 的包跳过构建与测试、老格式 store 条目(0.2.x 之前的,缺标准
META6.json)自动重新入库,以及 --force 强制全部重跑。
版本约定
META6.json 里的版本号每次改动都递增,遵循 semver:
- patch(0.2.1):修 bug,不动行为;
- minor(0.3.0):新增能力,向后兼容;
- major(1.0.0):磁盘布局或行为的不兼容变更,需要用户额外操作。
例如 0.1.0 → 0.2.0 就是把安装目录从平铺的 lib/ 换成了 site/
(CompUnit::Repository::Installation),属于不兼容变更:已装的包要重装,
RAKULIB 也要从 <target>/lib 改成 inst#<target>/site。
这类变更会在 release note 里明确写出来。
已实现 vs 待实现
已实现:多索引仓库(配置持久化 + 别名 + 环境变量多值)、远程生态仓库
(索引缓存 + HTTP 下载 + tar 解压)、META6.json 解析、依赖树解析
(递归/冲突/环处理/拓扑排序,按「该版本是否真提供此模块」挑版本)、
git 仓库安装(clone + 递归解析依赖 + 自底向上,与仓库安装同一条流水线)、
装进 CompUnit::Repository::Installation(多版本共存 + use :ver<> 精确选版本)、
:from<native> 本地库识别与跨系统探测(三套文件名 + 三套搜索路径 +
按发行版给安装命令)、
多版本共存存储、内容指纹校验、
只装缺失的依赖(依赖已被已装包满足——含 zef 装的——就不再要求仓库里有,
自装的本地包可以被其他本地包当依赖)、
跳过已处理的内容(版本完全一致的已装包 —— 包括 zef 装的 —— 整个跳过;
已在 store 的包跳过构建与测试;老格式 store 条目(缺 META6.json 或资源声明过时)自动重新入库;
账本自愈(installed 时对账 + install 跳过路径上也当场补记:site 里有而账本缺记的包自动补记,
自己装的包不会再"消失";--force 全部重跑)、
构建阶段(Build.pm:class Build 的 build 方法,与 META6 "builder" 字段
如 Distribution::Builder::MakeFromJSON,均按 zef 的语义在发行版根目录起子进程执行;
build-depends 会在构建前自动装好;--no-build 可跳过)、
resources/ 随包复制并在 META6.json 声明(装完 %?RESOURCES 可用,native 包的 .so/.dll 才能被找到)、
依赖冲突报错带完整请求链(谁要求了什么版本,一眼定位该 pin 哪边)、
锁文件(含严格模式)、指定版本安装、升级、卸载清理、测试阶段、预编译、
META6 bin 字段自动出 wrapper(zef 同款:装完就放到 rakudo 的 site/bin/——
那个目录默认在 PATH 上——所以 raku-pm 自装后直接敲 raku-pm 就能跑;
wrapper 是自包含 bash 脚本,不依赖软链解析/外部 RAKULIB;Windows 额外生成 .bat
wrapper 供 cmd/PowerShell 用;macOS 的 BSD sort 无 -V 时自动退回字典序兜底;
site/bin 只读时就回落到 ~/.raku-pm/bin/ + raku-pm env 提示)、多版本激活按 semver 取最高(不是字符串字典序)、
查询/取源码命令也认安装规格串(search / installed / store / version / fetch
都能写 Foo:ver<1.2+> / Foo@1.2.3 / :auth<>,解析器抽成零依赖的
RakuPM::Spec,install 与查询共用;非法约束整串当关键词兜底)、
发行版元信息查询四条命令(与 zef 对齐):info <包> 显示版本/描述/依赖/
源码地址等详情;browse <包> 在浏览器打开主页或源码地址;locate <模块> 定位
模块在磁盘上的真实文件位置(走仓库链解析,多份共存时指出被遮蔽的那些);
smoke [包…] 取源码并跑测试(不给规格则测全部已装,可能很慢)。
在 CompUnit::Repository::Installation(zef 装包用的就是它)里装包,多版本可并存,
use Foo:ver<1.2.3> 由 Rakudo 自己解析。
待实现(真实 zef 还有的能力):
卸载时的依赖回收 ✅ 已实现,见下节。依赖树可视化:rdepends 反查谁依赖了我。 ✅ 已实现,见下节。索引增量更新:整份索引缓存,没做 ETag / If-Modified-Since。
✅ 已实现(0.36.x 起):HTTP 门面与两个后端都支持条件 GET
(If-None-Match → 304 复用缓存),ETag 单独存 <cache>.etag。
注意 RAKUPM_INDEX_TTL 控制复用窗口,TTL 一过仍会去问一次(只是大概率拿 304)。- 降级后自动切换激活版本:见上面「降级的坑」——Raku 的
use(无约束)永远
取最高版本,所以只能靠 --only 卸掉更高的版本来实现,或者引入 raku-pm 自己的
pin 机制(需影响 Rakudo 的加载,成本高,暂不做)。
生产侧(作者侧)——与上面「消费侧」并列的另一半生命周期:
| 能力 | 状态 | 眼下谁在做 |
|---|
重建 META6.json 的 provides | ✅ raku-pm refresh(0.37.7) | mi6 的 mi6 build |
| 创建发行版骨架 | ✅ raku-pm new(0.39.0) | mi6 new / fez init |
| 构建与打包(sdist) | ✅ raku-pm dist(0.41.0,Build.pm 内联) | mi6 build + dist.ini 插件 |
版本与变更编排(bump / Changes) | ✅ raku-pm bump(0.41.0) | mi6 release |
| 上架前预检 | ✅ raku-pm check(0.40.0) | fez review |
| 上传凭据管理(login) | ✅ raku-pm login(0.56.0) | fez login |
| 上传到生态 | ✅ raku-pm publish --remote(0.57.0,multipart PUT 到 42.zef.pm) | fez upload |
设计取向:发布走同协议直连生态 API(不 wrap fez),保证 raku-pm 发的包
fez/zef 都能看到、都能管;publish 会坚持 dry-run 优先 + 二次确认,
因为生态侧规定同一版本不可重传(auth 必须与上传者匹配、拒绝 version=*)。