README
raku-pm — a teaching Raku package manager
简体中文 | English Architecture & deep dive · Quick start
A minimum viable package manager in ~3700 lines of Raku (19 modules + 1 CLI, excluding blank lines and comments), modelled on the layered, pluggable architecture of zef, Raku's official package manager. Written to understand how package managers actually work.
Repository: https://gitee.com/skyter10086/raku-pm
Installing this project
raku-pm is itself a standard Raku distribution with a META6.json at its root,
so zef can install it:
git clone https://gitee.com/skyter10086/raku-pm.git
cd raku-pm
zef install . # `zef install . --dry` for a dry runThis gives you a raku-pm command (on Windows, zef generates raku-pm.exe
under site/bin):
raku-pm # no args: print the command index (one screen), exit 0
raku-pm help # same thing (recommended)
raku-pm help install # detailed usage of one command
raku-pm help all # full manual (lock file / concurrency / env vars…)
raku-pm install Web::App # install by module name (from the local repo)
RAKUPM_REPO=repo raku-pm install Web::App # explicit repo root
raku-pm install https://github.com/raku-community-modules/File-Temp.git
# install from a git repo (deps included)
zef uninstall RakuPM # uninstallGotcha: a legacy zef wrapper (
raku-pm.exe) eats--helpwith its*%named-parameter signature, leaving only Raku's default "Usage: ..." message. The wrapper raku-pm generates itself does not have this problem (measured:raku-pm --help/-hprint help and exit 0). Preferraku-pm help— it works under every wrapper flavour. The default output is a compact command index (one screen);raku-pm help <command>shows one command,raku-pm help <group>shows a whole group (e.g.raku-pm help authorfor the publishing commands), andraku-pm help allthe full manual.
Recommended: run from source, skip zef entirely
raku bin/raku-pm.raku listThis is actually more robust than zef install . — prefer it when:
you have several Raku installs on the box (rakubrew and friends), or
raku-pmdies at runtime withMissing or wrong version of dependency ...
Why: running from source writes precompilation into the project's local
lib/.precomp, produced by the very raku you're running — self-consistent by
construction. zef install instead puts the package into site/, and its
precompilation is produced by whichever raku zef happens to use. If those two
are different builds (common with version managers), the source fingerprints
don't match and loading fails.
Want the raku-pm command anyway? A wrapper does it without involving 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(Replace $HOME/raku-pm with wherever you cloned it.)
Two environment variables control the paths; both have defaults:
| Variable | Meaning | Default |
|---|---|---|
RAKUPM_REPO | local repo root (scans for META6.json) | cwd . |
RAKUPM_TARGET | install root (store / site / lockfile / git-cache) | ~/.raku-pm |
Installing from a git URL
raku-pm can install a distribution straight from a git repository and does
resolve its dependencies recursively, using exactly the same pipeline as a
regular repository install.
raku-pm install https://github.com/raku-community-modules/File-Temp.gitOutput:
==> 克隆到本地缓存:.../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-ad3445eThe two install paths differ in exactly one respect: where the target package's sources come from.
| repository install | git install | |
|---|---|---|
| where the target comes from | the ecosystem indexes | the git clone directory |
| where deps come from | the ecosystem indexes | the ecosystem indexes (same Resolver) |
| dependency resolution | recursive + topological sort | identical |
| per-package steps | build → store → test → install | identical |
| lockfile | the whole chain | the whole chain |
The implementation is aligned accordingly: Client factors the per-package
landing into !install-chain, which accepts a %source-override (keyed
"<name>\0<version>") meaning "this package's sources live outside any
repository — use the directory I'm handing you". Git mode simply puts the clone
directory in there.
Three details worth knowing:
Dependencies are a set of roots, not one root. Hence
Resolver.resolve-all(@specs): the whole batch shares one selection table, so a module is chosen once and cross-dependency version conflicts are caught (A wantsFoo>=2, B wantsFoo<2→ conflict). Callingresolverepeatedly and concatenating cannot do that.Modules the target itself provides are not external dependencies. Some META6 files list their own modules under
depends; conversely, if another distribution in the tree provides the same module name, the git one wins — the user explicitly asked for that copy.The target is always last. It depends on everything before it, and it lives in no repository, so the Resolver cannot see it.
Also:
Repos are
git clone --depth 1'd into$RAKUPM_TARGET/git-cache/<host>/<repo>.git/; reinstalling the same repo does agit pull --ff-onlyinstead of a fresh clone.The version is always
0.0.0-<git-sha7>and the auth isgit:<host>— we don't parse git tags into versions, we just take HEAD.META6.json(the Raku standard) is parsed. As of 0.37.0 we no longer fall back to our ownMETA.json— third-party packages always shipMETA6.jsononly (the store still keeps its ownMETA.jsonfor reverse-restore).Dependencies obey
--dry/--no-test/--locked/--no-native-check, and the lockfile records the whole chain.A repo without
META6.jsonis rejected with an error.Bare git URLs inside
dependsare honored (a non-standard form zef also accepts). Draku's META6.json, for instance, depends ongit://github.com/jkramer/p6-Text-Wrap.git— the URL is the dependency, so it must not be looked up in repositories as a module name. raku-pm collects such entries intogit-deps, and before the declaring distribution is processed it clones the URL and installs it as an ordinary distribution through the full chain (git://,git@…,https://…/xxx.gitare all recognized). ※ At clone time agit://URL is transparently upgraded tohttps://: GitHub shut down the git protocol (port 9418) in January 2022, so oldgit://specs like Draku's would otherwise always fail; the upgrade is semantically equivalent for public repositories.
What's in META6.json
META6.json is a distribution's identity card in the Raku world — it's how zef
learns the name, the modules, and the dependencies. This project declares:
name/version/auth/api→ the IdentityRakuPM:ver<1.0.3>:auth<zef:skyter10086>:api<1>, a package manager's primary keyprovides→ module name → file mapping; zef uses it to place.rakumodfiles on the module search pathdepends.runtime→JSON::Fast,HTTP::Tinyish(system curl, the only HTTP backend),URI(the MD5 fingerprint comes from the bundled pure-RakuRakuPM::MD5— no ecosystem dependency); zef installs these firstbin→bin/raku-pm.raku, which after install becomes a self-contained bash wrapper. It is placed into<target>/bin/raku-pmand also copied into the rakudosite/bin/on your PATH (zef-style: the command works immediately after install, no manualPATHedits). Cross-platform notes: Windows also gets a.batwrapper for cmd/PowerShell (zef does the same); macOS's BSDsorthas no-Vso the wrapper falls back to lexicographic order. Ifsite/bin/is read-only it falls back to<target>/binwith a hint fromraku-pm env.test-depends→Test, sozef installrunst/first and refuses to install if they fail
Design details (architecture / install pipeline / atomic install & generation rollback / lockfile / concurrency / cache cleanup / mapping onto zef) moved to docs/architecture.en.md.
Three traps when picking a version
The Resolver hit three real-world data traps. This is the least toy-like part of the project.
1. provides drifts between versions — you must check per version
A distribution's provides is not constant. Real case:
DBIish0.5.5–0.6.1 bundledNativeLibsinside itself; from 0.6.2 on it was split out into a standaloneNativeLibsdistribution.
So the module NativeLibs appears in the provides history of two different
distributions. If you only know "some version of this distribution provided it"
and then pick the highest version, you land on DBIish 0.6.8 — which does not
contain NativeLibs at all, and you end up with DB::SQLite installed but
use NativeLibs broken.
The fix is a versions-providing($module, $dist) method on the repository role:
return only the versions that actually provide the module.
Repository::Ecosystem scans index rows (fast path); the role carries a generic
fallback that materialises each version and checks provides.
2. Version numbers are not comparable across distributions
Even when both candidates are legitimate, you cannot just compare versions. The ranking is:
A distribution whose name equals the module name wins —
NativeLibsthe module belongs toNativeLibsthe distribution;then higher version (via real semver, not string
cmp—"0.9" > "0.10"is the wrong answer);then name, lexicographically, so results are stable and reproducible.
3. :from<native> is a system library, and it looks different on every OS
DB::SQLite depends on sqlite3:from<native>. That phaser is how zef marks a
system native library (libsqlite3 — not a Raku package). raku-pm does not
ship system libraries, so it:
strips such entries out of the Raku dependency table, never querying a repository for them;
probes the machine at install time and aborts the install if any is missing (lists which, with install commands);
--no-native-checkskips the check but the library will still fail to load at runtime.
Libraries loaded at runtime via NativeCall are also checked (not just :from<native>).
Many packages (e.g. Duckie's libduckdb) load the library at runtime with
is native('duckdb') in source and do not declare :from<native> in META6, so a
static dependency check cannot see them — the failure would only surface when tests
load the library (with a stack trace). raku-pm scans lib//bin/ for is native('...')
after fetching the source and before running tests, and aborts the install if any is
missing. Libraries bundled in resources/libraries/ are resolved by Rakudo via
%?RESOURCES and are not treated as system-missing (so packages like GDBM are not falsely rejected).
The crux is that one logical name maps to different files per OS:
| logical name | 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 |
unknown foo | foo.dll | libfoo.dylib | libfoo.so |
Note Windows is the branch without the lib prefix — copying the Unix rule
gets you nothing.
Search paths are per-OS too:
Windows:
PATH→System32/SysWOW64→ vcpkg'sinstalled/*/binmacOS:
DYLD_LIBRARY_PATH→/opt/homebrew/lib(Apple Silicon) →/usr/local/lib(Intel) →/opt/local/lib(MacPorts) →/usr/libLinux:
LD_LIBRARY_PATH→/usr/local/lib,/usr/lib, … → Debian-family multiarch dirs (/usr/lib/x86_64-linux-gnu; modern Debian moved everything there, so scanning only/usr/libmisses it) → versioned.so.Nmatched by prefix
Install commands are per-OS as well, and Linux is further split by distro family (package names differ across apt / dnf / pacman / apk / zypper — the wrong command is as good as none). When the distro can't be identified, all family commands are listed rather than degrading to one (and never to the Windows one).
$ 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
→ 全部就绪。When something is missing (Linux example) the install aborts immediately instead of only warning (otherwise it would blow up later at the test/load stage with a long internal backtrace):
本地库依赖检查(Linux/Unix,raku-pm 不打包系统库):
✗ sqlite3 未找到 —— 本系统需要 libsqlite3.so / libsqlite3.so.0
安装:apt install libsqlite3-dev
⚠ Linux/Unix 上缺少 1 个本地库,放弃安装:sqlite3
请先按上面的命令装好本地库,再重试安装。
(`raku-pm native <名字>` 可单独查询;如确需跳过本检查可加 --no-native-check,但运行时加载这些库仍会失败)Implemented in RakuPM::NativeLib, also queryable on its own:
raku-pm native sqlite3 # is it on this machine?
raku-pm native ssl curl --paths # several at once, --paths shows search paths
raku-pm native --installed # scan the lockfile for libs your dists declared
raku-pm native sqlite3 --os=linux --distro=ubuntu # what another machine would needThat last one works because os / distro can be forced — which is also what
lets t/native-lib.t cover all three rule sets on a single machine.
4. Two more "not a Raku package" cases: external commands and per-platform deps
Beyond :from<native>, the ecosystem uses two more forms that must never be
looked up as Raku modules:
:from<bin> / :from<Perl5> — external commands or another language
Doc::TypeGraph depends on dot:from<bin> (graphviz), Documentable on
node:from<bin>, and POSIX:from<Perl5> is a Perl 5 module. raku-pm treats
them like native libs: stripped from the Raku dependency table at parse time,
looked up in PATH at install time, and merely reported when missing —
installation is never blocked:
外部命令依赖检查(raku-pm 不负责安装系统命令):
✗ dot 未在 PATH 中找到
⚠ 缺少 1 个外部命令:dot
请先用系统包管理器装好(apt / brew / choco …);Raku 代码照常会装好。Per-platform deps: by-distro.name
zef lets a dependency be chosen by platform:
"depends": [ { "name": { "by-distro.name": { "": "", "mswin32": "Win32::Registry" } } } ]Meaning Win32::Registry is required only when $*DISTRO.name is mswin32;
other platforms take the fallback key "", i.e. nothing. The real-world sample
is File::Which 1.0.4. Without support for this syntax the whole Hash becomes a
"module name" (stringified as by-distro.name\nmswin32 Win32::Registry), so on
Linux/WSL every package depending on it dies with "module not found" —
raku-pm install Digest::SHA1::Native failed exactly this way
(LibraryMake → File::Which).
We now pick the key for the current platform ($*DISTRO.name, then *, then the
"" fallback); a miss means no dependency. Do not invert this: a
platform-conditional dep means "other platforms don't need it", so installing
every branch would make Linux fetch Win32::Registry and fail just as hard.
{"any": [...]}: alternatives — external commands and native libs are
skipped, the first Raku module wins.
Quick start
export RAKUPM_REPO=repo # repository directory
export RAKUPM_TARGET=~/.raku-pm # install root (store/ + site/ + lockfile)Installing the distribution in the current directory (install .)
The counterpart of zef install .:
raku-pm install . # the current directory
raku-pm install ./some-dist # a subdirectory
raku-pm install /abs/path/to/dist # an absolute pathIt reads META6.json out of the directory to identify the
distribution, then resolves dependencies recursively → installs them
bottom-up → installs this package last. No META6.json in the
directory is a hard error.
Don't substitute
RAKUPM_REPO=<dist root>for this. The Local repository is laid out as "one repo directory holding several distribution subdirectories" (repo/DistA/META6.json) and only scans subdirectories — a distribution'sMETA6.jsonsits at its own root and is never seen.
It shares one code path with git installs (!install-local-dist): in both cases
the target lives outside any repository, so the Resolver cannot see it and we
append it to the end of the dependency chain ourselves. Only the origin of the
source directory differs (clone directory vs local directory).
Pinning a version (zef-compatible syntax)
Install specs accept zef's phaser syntax. Parsing lives in a standalone
lightweight module, RakuPM::Spec (Distribution.parse-spec remains as a
delegate) — extracted because this syntax is no longer install-only:
search / installed / store / version / fetch understand it too, and the query
paths shouldn't have to drag in the whole Distribution module graph just to
parse a spec string. It is the exact inverse of identity():
| Spelling | Meaning |
|---|---|
Foo:ver<1.2.3> | exactly 1.2.3 |
[email protected] | @ shorthand, same as above |
Foo --version=1.2.3 | the traditional spelling |
Foo:ver<1.2+> | at least 1.2 (zef's + suffix) — highest matching version |
Foo:ver<1.2.3>:auth<zef:someone> | version + author |
Foo:ver<1.2.3>:auth<zef:x>:api<1> | version + author + api |
Precedence: --version > :ver<> in the spec > the positional constraint.
Two things worth knowing:
:auth<>actually filters — it isn't parsed and then ignored. A mismatched auth is a hard error, never a silent substitution. That matters more than it sounds: packages do change hands upstream.Concurrent::Stack1.1 iscpan:JNTHNwhile 1.3 iszef:raku-community-modules.The
::in a module name is never mistaken for a phaser (a phaser must carry<...>).
This syntax is not install-only — the query/source commands accept it too:
| Command | Effect |
|---|---|
search 'Foo:ver<1.2+>' | filters both repo rows and locally installed dists; ties resolve to the highest version among those satisfying the constraint — a package whose newest release fails the constraint but whose older one passes is still found |
installed 'Foo:ver<1.0>' | only versions satisfying the constraint; none match → an explicit "not installed (no version satisfies…)" message instead of silent empty output |
installed 'Foo:auth<zef:x>' | additionally filter by author (applies to repo rows and ledger entries) |
store '[email protected]' | only cached versions matching the constraint, full paths included |
version 'Foo:ver<1.0>' | filters installed versions (the active marker still follows the globally active version) |
fetch 'Foo:ver<1.2+>' | picks the source version by constraint (--version stays exact-match, same precedence as install) |
<>is a shell redirection operator. How to write it per platform:
bash / zsh: quote the whole string, e.g.
search 'Foo:ver<1.2+>'.Windows cmd: quoting is not enough —
raku-pm.batends inraku ... %*, and cmd re-parses%*while the quotes have already been stripped before the batch file sees them, sosearch "Foo:ver<1.2.0>"still fails with "The syntax of the command is incorrect." Use the bracket-free@form instead — exactly equivalent to:ver<>and safe in every shell:raku-pm search [email protected]+ raku-pm installed [email protected]
If the ver part is not a valid constraint (e.g. the bar in search foo@bar),
the whole input falls back to a plain keyword — identical to the old behavior.
A broken constraint must never silently filter out every version.
Installing an old version needs REA mounted — the zef index only carries current versions:
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>Installing
# install a real-world package and its deps from the default ecosystem (https://360.zef.pm)
raku -Ilib bin/raku-pm.raku install File::Temp
# install a module and its deps (highest version matching the constraint)
raku -Ilib bin/raku-pm.raku install Web::App
# pin an exact version (this is also how you downgrade) — three equivalent spellings
raku -Ilib bin/raku-pm.raku install 'Concurrent::Stack:ver<1.1>'
raku -Ilib bin/raku-pm.raku install 'Concurrent::[email protected]'
raku -Ilib bin/raku-pm.raku install Concurrent::Stack --version=1.1
# resolve only, don't touch disk
raku -Ilib bin/raku-pm.raku install Web::App --dry
# skip t/ tests (some packages don't pass on Windows)
raku -Ilib bin/raku-pm.raku install File::Temp --no-test
# strict mode: abort if the lockfile disagrees with the resolution (CI)
raku -Ilib bin/raku-pm.raku install Web::App --lockedAfterwards, put inst#<target>/site on the module search path (raku-pm env
prints exactly that string):
export RAKULIB="inst#$HOME/.raku-pm/site"
raku -e 'use File::Temp; say tempfile'
# pick an exact version (requires having installed that version with --version)
raku -e 'use File::Temp:ver<0.0.10>; say tempfile'Querying
raku -Ilib bin/raku-pm.raku list # packages installable from the repos
raku -Ilib bin/raku-pm.raku search Temp # searches repo indexes AND locally installed dists
# ([brackets] = which repo; installed-only dists
# are tagged with the literal label "[已装]")
raku -Ilib bin/raku-pm.raku search 'JSON::Fast:ver<1.0+>' # queries accept install specs: filter by version
raku -Ilib bin/raku-pm.raku repos # list configured repos
raku -Ilib bin/raku-pm.raku installed # installed packages and versions
raku -Ilib bin/raku-pm.raku installed Foo # just Foo's versions (* = active)
raku -Ilib bin/raku-pm.raku installed Web::App # module name accepted (exact): finds the Web-App dist
raku -Ilib bin/raku-pm.raku installed 'Foo:ver<1.0>' # only versions matching the constraint; :auth<> too
raku -Ilib bin/raku-pm.raku store # local store (* active; each version + full cache path)
raku -Ilib bin/raku-pm.raku store Foo # just Foo's cached versions and their full paths
raku -Ilib bin/raku-pm.raku store Web::App # module name accepted (exact), same as installed
raku -Ilib bin/raku-pm.raku store '[email protected]' # @ shorthand likewise; filter cached versions
raku -Ilib bin/raku-pm.raku version 'Foo:ver<1.0>' # filter installed versions by the constraint
raku -Ilib bin/raku-pm.raku which JSON::Fast # where this module actually loads from, and which version (lists shadowed copies)
raku -Ilib bin/raku-pm.raku which # cross-repo conflict overview: distributions installed in several repos
raku -Ilib bin/raku-pm.raku lock # lockfile contents
raku -Ilib bin/raku-pm.raku verify # check installed packages for tampering
raku -Ilib bin/raku-pm.raku native sqlite3 # is a system library present?
raku -Ilib bin/raku-pm.raku env # how to set RAKULIBOne matching rule, shared by search / store / installed / install (
RakuPM::Repository::Matching, a repository-layer role; bothRakuPM::RepositoryandRakuPM::Client::Querydoesit, so one edit keeps all four in sync). A keyword matches only the dist name + provided module names — description is not scanned, because descriptions often say "provides method xxx", which would drag method names into the results (that is whysearch methodused to return a pile of packages).
search: fuzzy match + relevance ranking (name exact < name prefix < name substring < module exact < module prefix < module substring);
store/installed: exact match (exact dist name or exact module name, soinstalled Web::Appfinds the distWeb-App, whileinstalled Webdoes not over-match);
install: indexed exact module-name lookup (find-providers, i.e. the "module exact" tier above).The rule is a pure function (it only takes name / module names / keyword), so
t/match.ttests it with no repo and no HTTP stub.
Repository management
raku -Ilib bin/raku-pm.raku repos add rea # add the ecosystem archive (alias)
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 # force-refresh all index caches (like `zef update`)
raku -Ilib bin/raku-pm.raku repos update rea # refresh only reaMaintenance
raku -Ilib bin/raku-pm.raku upgrade JSON # upgrade to latest
raku -Ilib bin/raku-pm.raku uninstall JSON # uninstallSample output for install 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.lockInstalling a real package from the ecosystem (download + test stages):
将安装 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
✓ 完成。Installing a package that only exists in REA (ADT, algebraic data types):
$ 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
✓ 完成。Producer side (authoring)
Every section above is the consumer side — "install what, and where". The other half of the lifecycle is the producer side: acting on the distribution directory you are working on, maintaining its metadata. raku-pm ships these producer commands itself:
raku-pm new <module>— scaffold a brand-new distribution (see below)raku-pm refresh— rebuild META6.json'sprovidesfrom diskraku-pm check— pre-flight check before publishing (the gate below)raku-pm dist [path]— package the sdist (required before publishing, below)raku-pm bump [path]— version orchestration (below; bump version + writeChanges)raku-pm publish [path]— deploy to a local directory repo (see below; requiresdistfirst)
Login / publishing: deploying to a local directory repo (publish) is
implemented. M2 ships producer-side credential management (login) plus
publish --remote --dry (build the request + preview); the real multipart PUT
upload lands in M3 (needs RakuPM::Net to grow an upload capability). Today the
actual upload can still be done with fez.
Scaffold: raku-pm new
new is the producer-side starting point: it generates a brand-new
distribution directory at the current location. Output:
META6.json— name/version/auth/api/provides/depends/test-depends/bin, all set (version defaults to0.1.0; auth defaults toRAKUPM_AUTHOR_AUTHorzef:skyter10086)lib/<module>.rakumod— main module stub (unit module+ anour sub hellosmoke)t/01-basic.rakutest—use-oksmoke testChanges— initial version header.gitignore— ignores.precompREADME.md— stub (only with--with-readme)bin/<x>.raku— executable (only with--bin=<x>); META6'sbinlists it too
Directory name defaults to the module name with :: → - (mi6 convention);
--into=<path> overrides it (. = current directory). If the target exists and
is non-empty and --force is not given, it errors out — never silently clobbers
your files.
raku-pm new Foo::Bar # create Foo-Bar/
raku-pm new Foo::Bar --into=MyProj # custom dir name
raku-pm new Foo::Bar --with-readme --bin=foo --auth=github:me --version=0.2.0
raku-pm new Foo::Bar --dry # preview onlyrefresh solves a frequent and nasty problem: after you add a new module under
lib/, or rename one, the provides in META6.json no longer matches disk.
Keeping it by hand is easy to get wrong — and when provides is wrong, other
people's installs cannot use the module (while everything looks fine
locally).
raku-pm refresh # rebuild provides for the current directory from disk
raku-pm refresh ./some-dist # target a specific directory
raku-pm refresh --dry # preview the changes only; write nothingExample output:
==> 重建 META6.json 的 provides:/path/to/Web-App
磁盘 lib/ 下扫到 3 个模块
· 加入(磁盘上有、声明里没有):
+ Web-App
+ Web::Legacy
+ Web::Router
· 剔除(声明里有、磁盘上没有,写了也装不上):
- Web::Ghost
✓ 已更新 /path/to/Web-App/META6.jsonDeliberately conservative scope — only what disk can uniquely determine:
| Field | What refresh does |
|---|---|
provides | Fully rebuilt: scan lib/ → module names → relative paths (.rakumod/.pm6 both recognised; .rakudoc is not a module) |
resources / bin | Untouched. Resources are managed by explicit declaration (mi6 likewise leaves them to fez resource); bin is a path array that disk cannot uniquely determine |
| Everything else | Preserved verbatim (name / version / auth / depends / tags / source-url …) |
| When nothing changed | No file write (no gratuitous mtime churn) |
It is not in the RakuPM::Client tree, and it takes no write lock and never
touches the install target — it only reads the directory you point it at.
That is why the CLI dispatches it before constructing Client: changing one
META6.json should not drag in the target / store / index cache.
Pre-flight check before publishing: raku-pm check
check is the gate before publishing: before actually connecting to the
server to upload, it runs every hard error the client can determine — equivalent
to fez review / the meta validation mi6 release does, but more thorough. By
default it is local-only, instant, and offline; add --remote to also hit the
network and check "is this version already published".
Checks:
META6.json valid: exists and is a valid JSON object
name: present and a valid module name (
Foo/Foo::Bar)version: present and ≠
*(*is the "any version" placeholder; not publishable)auth: present and matches
RAKUPM_AUTHOR_AUTH(auth must equal the uploader account or the server rejects it; the client can only pre-check, not replace the server. WithRAKUPM_AUTHOR_AUTHunset it only warns; the server enforces it)provides matches disk
lib/: declared-but-missing-on-disk → error (won't install); on-disk-but-undeclared → warning (runraku-pm refreshfirst)bin: every listed script file must exist
license / description (not TODO) / authors: recommended; warning, not blocking
depends / test-depends / build-depends: parseable (malformed → warning)
raku-pm check # check the current directory
raku-pm check ./some-dist # target a specific directory
raku-pm check --remote # also check online whether the version is already publishedThe report marks errors with [✗] and warnings with [!]; any hard error exits
non-zero, so it drops straight into a CI publish gate. --remote that cannot
resolve (offline / index fetch failed) only warns, never blocks — the definitive
"no re-upload of the same version" is enforced by the server.
Package the sdist: raku-pm dist
dist rolls the validated distribution into an uploadable source archive
<name>-<version>.tar.gz (the input to publish). The order: run the check
gate first (any hard error is rejected outright — it never packages a dist that
fails pre-flight) → optionally run the build phase → write the tar.gz.
Build phase is inlined: if the distribution directory has a
Build.pm(the conventionalclass Buildwith abuildmethod),distruns it in place (native-extension dists use it to compileresources/libraries/*.so, and the product lands in the tarball). A pure-Raku dist has noBuild.pmand is skipped automatically.--no-buildforces the build to be skipped.What gets packaged: META6 +
lib/+bin/+t/+resources/+LICENSEREADME, etc.;.git/.hg/.svn/blib/.precomp/.DS_Storeare excluded, as is any hidden entry (name starting with.) —.workbuddy/.workflow/.vscode/.gitignore… — so a.gitignored private dir never leaks into a published package. The tarball being generated is excluded automatically, so re-running never rolls an old package into the new one.
.distignorecustom exclude list: the hidden-entry filter cannot stop normally-named internal files (AI instructions, internal roadmap, internal handbook…). Drop a.distignorein the distribution root — one relative path per line (supports#comments, trailing comments,dir/entries covering a whole subtree; no globs) — and those files stay in the repo but never enter the published package. Entries that match nothing get reported on stderr, so a typo'd filename never silently ships a file you meant to exclude.The product is a source distribution: ecosystems ship source only and consumers compile on their own machine, so precompiled bytecode is not included.
The archive lands in the current directory by default;
--to=<dir>points it elsewhere;--drypreviews without writing.
raku-pm dist # package current dir → ./<name>-<version>.tar.gz
raku-pm dist ./some-dist # target a specific directory
raku-pm dist --no-build # skip the build phase
raku-pm dist --to=../out # place it under ../outDeploy to a local repo: raku-pm publish
publish deploys the sdist built by dist into a local directory repo, so
that other machines / CI can raku-pm install from there. This is the local /
teaching equivalent of fez publish: a real ecosystem uploads the sdist to a
remote index server (42.zef.pm, needs server support + account login), where
credential management (login) + request construction (publish --remote --dry
preview) already landed in M2 and the real multipart PUT upload lands in M3;
the local case drops the source into a directory repo (one already registered via
raku-pm repos add).
Target directory:
--to=<repo-dir>first; else the env varRAKUPM_PUBLISH_REPO; else it errors asking you to specify one. The target should be a local repo registered withrepos add, soinstallcan resolve the package from it.Drop location:
<repo>/<name>-<version>/(::in the name becomes-). One subdirectory per distribution, matching the Local repo layout; multiple versions coexist as separate directories.Reuse / rebuild: by default it rebuilds a sdist via
dist(which runs thecheckgate + optional build first);--from=<tar.gz>consumes an existing package and skips repackaging.Collision check: if the drop directory already exists it refuses (same version cannot be re-uploaded) —
raku-pm bumpfirst, or--forceto overwrite.--remotetakes the real-ecosystem upload path (M2: build the request +--drypreview; the real PUT lands in M3); without--dryit errors explicitly and defers to M3 — never silently sends anything over the network.Not dry-run by default —
publishdeploys immediately (extracts into the directory); use--dryto preview the drop location only. Deleting is irreversible, but publishing only adds a directory to the repo, so on a mistake justrmit.The internally built temporary sdist is cleaned up after a successful deploy; a package given via
--fromis never deleted.
raku-pm publish # package cwd and extract into RAKUPM_PUBLISH_REPO (or error asking --to)
raku-pm publish --to=../my-dists # extract into the named local repo
raku-pm publish --from=../out/Foo-1.0.0.tar.gz --to=../my-dists # reuse an existing package
raku-pm publish --dry # preview the drop location only, write nothing
raku-pm login --api-key=<key> # store the ecosystem api-key in ~/.raku-pm/credentials.json
raku-pm login --username=me --password=secret # online login to 42.zef.pm/login, get key, store it
raku-pm publish --remote --dry # preview the real-ecosystem upload request (no network)
raku-pm publish --remote # actually upload to 42.zef.pm/upload (needs login or RAKUPM_API_KEY first)After publishing, another machine does raku-pm repos add <repo> and can
raku-pm install <name> to get your copy — the producer flow
new → refresh → check → dist → publish closes here.
Version orchestration: raku-pm bump
bump increments META6.json's version by semver and prepends a
## X.Y.Z - DATE heading to Changes (creating it from a title if absent). The
convention matches the project rules: patch = bug fix / minor = new feature /
major = layout or incompatible change.
raku-pm bump # default --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 # a positional that looks like a version → exact: → 2.5.0
raku-pm bump --to=2.5.0 # same, explicit spelling
raku-pm bump --date=2026-09-15 # set the heading date (defaults to today)
raku-pm bump --dry # preview only, no META6 / Changes changeWhen the current version cannot be inferred (e.g. version: '*'), bump errors
and asks for an explicit --to=.
Tests
raku -Ilib t/version.t # version parsing & constraint matching (15)
raku -Ilib t/store-lock.t # store, version switching, lockfile (11)
raku -Ilib t/meta6-local.t # META6.json parsing & local repo (14)
raku -Ilib t/depends.t # normalising the many shapes of depends, incl. :from<native> (18)
raku -Ilib t/by-distro-depends.t # by-distro.name platform-conditional deps (13)
raku -Ilib t/external-deps.t # :from<bin>/:from<Perl5> external commands & {"any":...} alternatives (17)
raku -Ilib t/file-lock.t # cross-process lock: mutual exclusion, re-entrancy, auto-release on kill, timeout (20)
raku -Ilib t/client-write-lock.t # Client.with-write-lock: writes really serialise, nesting doesn't self-lock (9)
raku -Ilib t/clean.t # cache cleanup: protected versions are never deleted / dry-run default / --all --name --older-than (31)
raku -Ilib t/repos.t # multi-repo config CRUD & persistence + repos update (36)
raku -Ilib t/resolve-provides.t # the three version-picking traps (18)
raku -Ilib t/native-lib.t # native libs: per-OS file names, search paths, install commands (50)
raku -Ilib t/git-install.t # git install dependency closure (17)
raku -Ilib t/git-url-dep.t # bare git URLs inside depends: collected into git-deps, cloned & installed first, idempotent (13)
raku -Ilib t/git-version.t # install-from-git version convention: preserve-version keeps the real version / else 0.0.0-* (3)
raku -Ilib t/multi-version.t # multiple versions & use :ver<> (27)
raku -Ilib t/resources.t # resources/ copy + %?RESOURCES after install (12)
raku -Ilib t/conflict-msg.t # conflict errors with full requester chain (16)
raku -Ilib t/build.t # build stage: Build.pm, builder field, build-depends (28)
raku -Ilib t/skip-installed.t # skipping installed/stored dists + auto re-admit + install-chain ledger self-heal (39)
raku -Ilib t/bin-and-sort.t # bin wrapper + semver sort + self-wrapper + PATH scan + which/cygpath-failure fallback + site-bin promote + bat + raku-fallback + cross-platform e2e (46)
raku -Ilib t/query-installed-store.t # installed [mod] / store [mod]: versions & full paths (24)
raku -Ilib t/cli-version.t # `version` / `version <module>` (9)
raku -Ilib t/spec.t # spec-string parser RakuPM::Spec: parse forms + constraint validity + delegate equivalence (70)
raku -Ilib t/search-spec.t # query/source commands take install specs: installed/store/version/fetch/search filter by version & auth (61)816 assertions across 40 test files.
t/resolve-provides.t reproduces real ecosystem pathologies (DBIish bundling
NativeLibs, mutual dependencies, self-cycles, cross-distribution version
comparison) against an in-memory fake repository, and runs the same assertions
against both versions-providing implementations (the index-scan fast path and
the role's generic fallback) to keep them in sync.
t/native-lib.t covers file-name mapping, search paths and install commands for
win32 / darwin / linux. All three are testable on one machine because
RakuPM::NativeLib's os / distro can be forced (which is also what
raku-pm native --os=... is built on).
t/git-install.t uses an in-memory fake repository shaped like a diamond
(App → LibA/LibB → Deep/Helper) to check the git install dependency closure:
two levels deep, shared deps installed once, order correct, cross-root version
conflicts detected, modules provided by the target not installed twice.
It exercises Client.resolve-deps-of, so no real git clone is needed.
t/git-url-dep.t is the regression test for Draku-style distributions whose
depends list a bare git URL: it creates two local git repos (the main one
depends on the other) and writes https://…/GitDep.git into the main repo's
depends. A copy of the dependency repo is pre-seeded into git-cache so the
nested clone takes the "reuse the cached copy" branch — fully offline. It proves
the URL is collected into git-deps, the dependency is cloned and installed
before the declaring distribution, both land in the ledger, and re-installing is
idempotent. Pure-parsing assertions also cover the git://, git@… and
https://…/xxx.git URL shapes and confirm ordinary modules are unaffected.
t/spec.t pins the spec-string parser itself: the parse forms (:ver<> /
:auth<> / :api<> / @ shorthand / unknown phasers must not swallow the
module name), the valid/invalid boundaries of constraint-valid (a bare word
like "bar" must be rejected, otherwise queries would filter out every
version), and item-by-item equivalence between the Distribution.parse-spec
delegate and Spec.parse.
t/search-spec.t runs real CLI subprocesses to verify the query commands'
spec support: installed's version/auth filters, store's cache filter,
version's constraint filter, fetch picking a version by constraint (including
at-least 1.0+ and a non-zero exit when nothing matches), and search
filtering both the repo rows and the installed side. The subprocesses
pre-write a repositories.json containing only a local entry, so everything
runs offline.
t/multi-version.t spawns real raku subprocesses that use MultiVer:ver<...>,
asserting on the version the module reports about itself. That is the only way to
prove a specific version was actually loaded rather than silently replaced by the
newest — checking installed.json cannot show it.
t/build.t covers the build stage: the three outcomes of a legacy Build.pm
(success / returns False / dies), invoking the META6 builder class, installing
build-depends before building, and the most important end-to-end scenario —
a build generating resources/libraries/libdemo.dll, after which
%?RESOURCES<libraries/libdemo> reads back the built payload. (META6 declares the
name without an extension; Rakudo's CUR appends the platform suffix at install
time — its native-resource convention, identical under zef.)
t/skip-installed.t covers "don't redo work": dists already installed at the exact
version (via raku-pm's own ledger or the zef-side site/home/vendor repositories on
the repo chain) are skipped entirely; dists already in the store skip build and
test; legacy store entries (pre-0.2.x without META6.json, or pre-0.4.0 with an
empty resources declaration) are re-admitted automatically; --force re-runs
everything.
Implemented vs. not
Implemented: multi-index repositories (persisted config + aliases + multi-valued
env var), remote ecosystem repositories (index caching + HTTP download + tarball
unpacking), META6.json parsing, dependency-tree resolution (recursion / conflicts /
cycle handling / topological sort, filtered by "does this version actually provide
the module"), git installs (clone + recursive dependency resolution + bottom-up,
same pipeline as repository installs), installation into
CompUnit::Repository::Installation (multiple versions coexisting + exact
use :ver<> selection), :from<native> detection with cross-OS probing
(three sets of file names + three sets of search paths + per-distro install
commands), multi-version store, content-digest verification, lockfile (incl. strict
mode), pinned installs, upgrade, clean uninstall, test stage, precompilation,
build stage (Build.pm with a class Build build method, and the META6
"builder" field such as Distribution::Builder::MakeFromJSON, both executed in a
subprocess from the dist root with zef's semantics; build-depends are installed
automatically before the build; --no-build skips it),
resources/ copied alongside the package and declared in META6.json (so
%?RESOURCES works after install — native .so/.dll payloads are findable),
installing only missing dependencies (a dependency already satisfied by an
installed dist — including zef-installed ones — is no longer required to exist
in any repo, so locally-installed packages can serve as dependencies of other
local packages),
skipping already-processed work (installed dists at the exact version —
including zef-installed ones — skipped entirely; store hits skip build and test;
legacy store entries (missing META6.json or with stale empty resources) auto re-admitted; the ledger self-heals (on installed,
dists present in site/ but missing from the ledger are re-recorded, so a
self-installed package can no longer "disappear"); --force re-runs it all),
dependency-conflict errors showing the full chain of requesters (who asked for
which version, so you can see at a glance which side to pin),
query/source commands accepting install specs (search / installed / store /
version / fetch all understand Foo:ver<1.2+> / [email protected] / :auth<>; the
parser is the zero-dependency RakuPM::Spec, shared by install and the
queries; an invalid constraint falls back to treating the whole input as a
keyword),
four distribution-metadata query commands (aligned with zef): info <pkg> prints
version / description / dependencies / source URL and more; browse <pkg> opens the
project homepage or source URL in the default browser; locate <module> reports the
real on-disk file path of a module (resolved through the repo chain, flagging shadowed
copies when several exist); smoke [pkgs…] fetches source and runs tests (with no
spec it smokes every installed distribution, which can be slow).
Not implemented (real zef still does these):
Dependency reclamation on uninstall— implemented, see below.Dependency-tree visualisation: an— implemented, see below.rdependsreverse lookup.Incremental index updates: the whole index is cached; no ETag / If-Modified-Since.✅ Implemented (since 0.36.x): theHTTPfacade and both backends support conditional GET (If-None-Match→ 304 reuses the cache), with the ETag kept in a sibling<cache>.etagfile.RAKUPM_INDEX_TTLcontrols the reuse window; once it expires we still ask the server once (we just very likely get a 304).Switching the active version after a downgrade: see "the downgrade trap" above — Raku's
use(unconstrained) always picks the highest version, so today the only way is to remove the higher ones with--only, or to introduce raku-pm's own pinning (which would have to affect Rakudo's module loading — expensive, deliberately not done).
Producer side (authoring) — the other half of the lifecycle, alongside the consumer side above:
| Capability | Status | Who does it today |
|---|---|---|
Rebuild provides in META6.json | ✅ raku-pm refresh (0.37.7) | mi6's mi6 build |
| Scaffold a new distribution | ✅ raku-pm new (0.39.0) | mi6 new / fez init |
| Build & package (sdist) | ✅ raku-pm dist (0.41.0, Build.pm inlined) | mi6 build + dist.ini plugins |
| Version & changelog orchestration | ✅ raku-pm bump (0.41.0) | mi6 release |
| Pre-flight check before publishing | ✅ raku-pm check (0.40.0) | fez review |
Upload credential management (login) | ✅ raku-pm login (0.56.0) | fez login |
| Upload to the ecosystem | ✅ raku-pm publish --remote (0.57.0, multipart PUT to 42.zef.pm) | fez upload |
Design stance: publishing will talk to the ecosystem API directly over the same protocol (not wrap
fez), so packages published by raku-pm remain visible to, and manageable by,fez/zef.publishwill insist on dry-run first plus a second confirmation, because the ecosystem forbids re-uploading a version (authmust match the uploader,version=*is rejected).
Versioning
The version in META6.json is bumped on every change, following
semver: patch for fixes, minor for new capability,
major for breaking changes to the on-disk layout (for example the move from a
flat lib/ to site/, which requires reinstalling packages and repointing
RAKULIB).