# raku-pm **Repository Path**: skyter10086/raku-pm ## Basic Information - **Project Name**: raku-pm - **Description**: Raku的包管理器 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-02 - **Last Updated**: 2026-09-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # raku-pm — 一个教学用 Raku 包管理器 [English](README.en.md) | 简体中文 [架构与深度设计](docs/architecture.md) · [快速上手](docs/quickstart.md) 仿照 Raku 官方包管理器 [zef](https://github.com/ugexe/zef) 的分层可插拔架构, 用约 3700 行 Raku(19 个模块 + 1 个 CLI,不含空行与注释)实现了一个最小可用 (MVP)的包管理器,用于理解包管理器的核心原理。 > 仓库地址: ## 安装本项目 raku-pm 自己也是个标准的 Raku 发行版,根目录带 `META6.json`,所以能被 zef 安装: ```bash git clone https://gitee.com/skyter10086/raku-pm.git cd raku-pm zef install . # 先 --dry 干跑也行 ``` 装完会多出一个 `raku-pm` 命令(Windows 下 zef 生成 `raku-pm.exe`,在 `site/bin` 里): ```bash raku-pm # 无参数:打印命令索引(一屏内),退出码 0 raku-pm help # 同上(推荐) raku-pm help install # 看某个命令的详细用法 raku-pm help all # 完整手册(锁文件 / 并发 / 环境变量…) 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`。默认输出是紧凑命令索引(一屏内);`raku-pm help <命令>` 看单个命令,`raku-pm help all` 看完整手册。 ### 推荐:直接跑源码,不用 zef 装 ```bash 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: ```bash 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 仓库装一个发行版,**并且会递归解析它的依赖**, 走的是与仓库安装完全相同的一套流程。 ```bash 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 · File::Temp:ver<0.0.0-ad3445e>:auth ← 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`(键 `"\0"`)来指明 「这个包的源码目录不在仓库里,用我给的这份」。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//.git/`,**下次装同一仓库走 `git pull --ff-only`**,不重新下载。 - 版本号固定用 `0.0.0-`,auth 用 `git:`——因为我们不解析 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 里写了什么 `META6.json` 就是 Raku 世界的"身份证"——zef 靠它知道这个发行版叫什么、有哪些模块、依赖什么。本项目声明了: - `name/version/auth/api` → 组成 Identity `RakuPM:ver<0.79.1>:auth: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`,装完软链(或复制)到 `/bin/raku-pm`,把 `bin/` 加进 `PATH` 就能直接敲 `raku-pm` - `test-depends` → `Test`,所以 `zef install` 会先跑 `t/` 下的测试,挂了就不装 > 设计细节(架构 / 安装流程 / 原子安装与世代回滚 / 锁文件 / 并发 / 缓存回收 / 与 zef 对应等)已迁入 [docs/architecture.md](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. 不同发行版的版本号没有可比性 上例中即便两个候选都合法,也不该直接比版本。规则是: 1. **发行版名与模块名一致者优先** —— `NativeLibs` 模块的正主是 `NativeLibs` 发行版; 2. 其次比版本高低(走 semver,**不是字符串 `cmp`** —— `"0.9" > "0.10"` 是错的); 3. 最后按名字字典序,保证结果稳定可复现。 ### 3. `:from` 是系统本地库,而且各系统长得不一样 `DB::SQLite` 依赖 `sqlite3:from`,这是 zef 用来标记**系统本地库**的 phaser (要的是 libsqlite3,不是 Raku 包)。raku-pm 不打包系统库,处理方式是: - 解析时把这类依赖从 Raku 依赖表里摘出来,绝不拿它去查仓库; - 安装时探测本机有没有,**缺任何一个就直接放弃安装**(提示缺哪些、给安装命令; `--no-native-check` 可强行跳过,但运行时加载这些库仍会失败)。 **运行时通过 `NativeCall` 直接加载的库也查**(不只 `:from`)。很多包 (典型如 **Duckie** 的 `libduckdb`)是源码里 `is native('duckdb')` 运行时加载的, META6 里**不写** `:from`,静态依赖检查看不到它,只能等测试阶段加载库失败 才暴露(还附一大段调用栈)。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:**`/bin`(raku.exe 所在目录,Windows 找 DLL 的第一站)** → `System32`/`SysWOW64` → `PATH` 上的目录 → vcpkg 的 `installed/*/bin` ⚠ 第一条最容易漏:Windows 的 DLL 搜索顺序里,**可执行文件所在目录**排在 `PATH` 之前,所以 zef / 各发行版常把配套 DLL 直接放在 `/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`,也可以单独查: ```bash 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`,生态里还有两种写法,一样不能当 Raku 模块去查仓库: **`:from` / `:from`——外部命令或别的语言** `Doc::TypeGraph` 依赖 `dot:from`(graphviz 的 `dot`),`Documentable` 依赖 `node:from`,`POSIX:from` 则是 Perl 5 的模块。raku-pm 的处理与本地库 同构:解析时从 Raku 依赖表里摘出来,安装时在 `PATH` 里查一下,缺了就提示, **不阻断安装**(Raku 代码照常装好): ``` 外部命令依赖检查(raku-pm 不负责安装系统命令): ✗ dot 未在 PATH 中找到 ⚠ 缺少 1 个外部命令:dot 请先用系统包管理器装好(apt / brew / choco …);Raku 代码照常会装好。 ``` **平台条件依赖 `by-distro.name`** zef 允许把依赖写成「按平台取」: ```json "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 模块。 ## 快速开始 ```bash export RAKUPM_REPO=repo # 仓库目录 export RAKUPM_TARGET=~/.raku-pm # 安装目标(store/ + site/ + 锁文件) ``` ### 安装当前目录的包(`install .`) 对应 zef 的 `zef install .`: ```bash 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` | 版本 + 作者 | | `Foo:ver<1.2.3>:auth: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'` | 已装列表再按作者筛(auth 筛选作用于仓库行与账本条目) | | `store 'Foo@1.0'` | store 缓存只看满足约束的版本,附完整路径 | | `version 'Foo:ver<1.0>'` | 查已装版本时按约束筛(激活标记仍按全局激活版本算) | | `fetch 'Foo:ver<1.2+>'` | 取源码按约束挑版本(`--version` 仍为精确语义,优先级同 install) | > **`<>` 是 shell 重定向符**,各平台怎么写: > - **bash / zsh**:整串加引号即可,如 `search 'Foo:ver<1.2+>'`。 > - **Windows cmd**:**加引号也不够** —— `raku-pm.bat` 里是 `raku ... %*`, > cmd 会对 `%*` 二次解析,而引号在传给 .bat 之前就被剥掉了,于是 > `search "Foo:ver<1.2.0>"` 照样报「命令语法不正确」。改用不带尖括号的 > `@` 写法,与 `:ver<>` 完全等价、任何 shell 都不用转义: > ```bat > raku-pm search Foo@1.2+ > raku-pm installed Foo@1.0 > ``` 查询命令对「不是合法约束」的 ver(如 `search foo@bar` 里的 `bar`)有兜底: 整串当普通关键词,行为与不带版本写法的旧版完全一致 —— 坏约束不该把 所有版本悄悄筛没。 装旧版本需要先挂 REA —— zef 索引只收录当前版本: ```bash 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 ``` ### 安装 ```bash # 从默认生态(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#/site` 加进模块搜索路径即可使用(`raku-pm env` 可直接给你这串): ```bash 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' ``` ### 查询 ```bash 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 桩**。 ### 仓库管理 ```bash 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 这一个 ``` ### 维护 ```bash 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 · HTTP-Client:ver<1.2.0>:auth · Web-App:ver<2.0.0>:auth ==> 存入存储: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 · File::Temp:ver<0.0.12>:auth ==> 下载源码包 [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 ==> 下载源码包 [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 的 provides - `raku-pm check` —— 发布前预检(上架前安全闸门,见下) - `raku-pm dist [路径]` —— 打包 sdist(见下,发布前必需) - `raku-pm bump [路径]` —— 版本编排(见下,递增版本 + 写 Changes) - `raku-pm publish [路径]` —— 部署到本地目录仓库(见下,发布前必需先 dist) - `raku-pm login [--api-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` —— 忽略 `.precomp` - `README.md` —— 占位(仅 `--with-readme`) - `bin/.raku` —— 可执行(仅 `--bin=`),META6 的 `bin` 同步含之 目录名默认 = 模块名里 `::` → `-`(mi6 惯例);`--into=<路径>` 显式指定(`.` = 当前目录)。 目标目录若存在且非空、又没 `--force`,直接报错,绝不静默覆盖用户文件。 ```bash 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` 不到**(你自己本地却看着没事)。 ```bash 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**:形态可解析(畸形 → 警告) ```bash raku-pm check # 预检当前目录 raku-pm check ./some-dist # 指定目录 raku-pm check --remote # 额外联网查同版本是否已发布(同版本不可重传) ``` 报告用 `[✗]` 标错误、`[!]` 标警告;**任何硬错误都以非零退出**,可直接接进 CI 发布门禁。`--remote` 查不到(离线 / 索引拉取失败)只告警、不阻断——最终的 「同版本不可重传」由服务器强制。 ### 打包 sdist:raku-pm dist `dist` 把校验过的发行版卷成可上传的**源码**归档 `-.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` 只预览不改文件。 ```bash raku-pm dist # 打包当前目录 → ./-.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=` 优先;否则读环境变量 `RAKUPM_PUBLISH_REPO`; 都没有就报错,让你显式指定。**目标应是用 `repos add` 登记过的本地仓库**, `install` 才能从那里解析到这个包。 - **解包落点**:`/-/`(name 的 `::` 换成 `-`)。一个子目录 一个发行版,与 Local 仓库布局一致;多版本各占一目录,可并存。 - **复用 / 重打**:默认复用 `dist` 现打一份 sdist(内部先跑 check 门禁 + 可选构建); 用 `--from=` 直接吃现成包,跳过重新打包。 - **碰撞检查**:落点目录已存在会被拦(同版本不能重传)—— 先 `raku-pm bump` 升版本, 或加 `--force` 覆盖。`--remote` 走真实生态上传路径(M2:构造请求 + `--dry` 预览; 真实 PUT 在 M3);非 dry 会明确报错、延后到 M3,绝不静默发网络。 - **默认只试算?不**——publish 默认**直接部署**(解包写目录);只想知道会落到哪用 `--dry`。删文件不可撤销,但发布只是「往仓库加一个目录」,出错时 `rm` 掉即可。 - 内部构建的临时 sdist 在部署成功后自动清理;`--from` 给的包不会被删。 ```bash 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= # 把生态 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 ` 即可 `raku-pm install ` 拿到 你这一份——作者侧工作流 `new → refresh → check → dist → publish` 至此闭合。 ### 版本编排:raku-pm bump `bump` 按 semver 递增 `META6.json` 的 `version`,并在 `Changes` 顶部加一条 `## X.Y.Z - DATE` 版本头(没有 `Changes` 会自动从标题起创建)。约定与项目纪律一致: **patch=修 bug / minor=新能力 / major=磁盘布局或不兼容变更**。 ```bash 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=` 显式指定。 ## 测试 ```bash 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(18 项) raku -Ilib t/by-distro-depends.t # 平台条件依赖 by-distro.name:按当前平台取舍,不再把 Hash 当模块名(13 项) raku -Ilib t/external-deps.t # :from/:from 外部命令与 {"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` 读到的就是构建产物 (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](https://semver.org): - **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` 也要从 `/lib` 改成 `inst#/site`。 这类变更会在 release note 里明确写出来。 ## 已实现 vs 待实现 **已实现**:多索引仓库(配置持久化 + 别名 + 环境变量多值)、远程生态仓库 (索引缓存 + HTTP 下载 + tar 解压)、META6.json 解析、依赖树解析 (递归/冲突/环处理/拓扑排序,按「该版本是否真提供此模块」挑版本)、 git 仓库安装(clone + **递归解析依赖** + 自底向上,与仓库安装同一条流水线)、 装进 `CompUnit::Repository::Installation`(**多版本共存 + `use :ver<>` 精确选版本**)、 `:from` 本地库识别与**跨系统探测**(三套文件名 + 三套搜索路径 + 按发行版给安装命令)、 多版本共存存储、内容指纹校验、 **只装缺失的依赖**(依赖已被已装包满足——含 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 还有的能力): 1. ~~**卸载时的依赖回收**~~ ✅ 已实现,见下节。 2. ~~**依赖树可视化**:`rdepends` 反查谁依赖了我。~~ ✅ 已实现,见下节。 3. ~~**索引增量更新**:整份索引缓存,没做 ETag / If-Modified-Since。~~ ✅ **已实现**(0.36.x 起):`HTTP` 门面与两个后端都支持条件 GET (`If-None-Match` → 304 复用缓存),ETag 单独存 `.etag`。 注意 `RAKUPM_INDEX_TTL` 控制复用窗口,TTL 一过仍会去问一次(只是大概率拿 304)。 4. **降级后自动切换激活版本**:见上面「降级的坑」——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=*`)。