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 run

This 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               # uninstall

Gotcha: a legacy zef wrapper (raku-pm.exe) eats --help with 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 / -h print help and exit 0). Prefer raku-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 author for the publishing commands), and raku-pm help all the full manual.

Recommended: run from source, skip zef entirely

raku bin/raku-pm.raku list

This is actually more robust than zef install . — prefer it when:

  • you have several Raku installs on the box (rakubrew and friends), or

  • raku-pm dies at runtime with Missing 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:

VariableMeaningDefault
RAKUPM_REPOlocal repo root (scans for META6.json)cwd .
RAKUPM_TARGETinstall 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.git

Output:

==> 克隆到本地缓存:.../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

The two install paths differ in exactly one respect: where the target package's sources come from.

repository installgit install
where the target comes fromthe ecosystem indexesthe git clone directory
where deps come fromthe ecosystem indexesthe ecosystem indexes (same Resolver)
dependency resolutionrecursive + topological sortidentical
per-package stepsbuild → store → test → installidentical
lockfilethe whole chainthe 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 wants Foo>=2, B wants Foo<2 → conflict). Calling resolve repeatedly 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 a git pull --ff-only instead of a fresh clone.

  • The version is always 0.0.0-<git-sha7> and the auth is git:<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 own META.json — third-party packages always ship META6.json only (the store still keeps its own META.json for reverse-restore).

  • Dependencies obey --dry / --no-test / --locked / --no-native-check, and the lockfile records the whole chain.

  • A repo without META6.json is rejected with an error.

  • Bare git URLs inside depends are honored (a non-standard form zef also accepts). Draku's META6.json, for instance, depends on git://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 into git-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.git are all recognized). ※ At clone time a git:// URL is transparently upgraded to https://: GitHub shut down the git protocol (port 9418) in January 2022, so old git:// 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 Identity RakuPM:ver<1.0.3>:auth<zef:skyter10086>:api<1>, a package manager's primary key

  • provides → module name → file mapping; zef uses it to place .rakumod files on the module search path

  • depends.runtime → JSON::Fast, HTTP::Tinyish (system curl, the only HTTP backend), URI (the MD5 fingerprint comes from the bundled pure-Raku RakuPM::MD5 — no ecosystem dependency); zef installs these first

  • bin → bin/raku-pm.raku, which after install becomes a self-contained bash wrapper. It is placed into <target>/bin/raku-pm and also copied into the rakudo site/bin/ on your PATH (zef-style: the command works immediately after install, no manual PATH edits). Cross-platform notes: Windows also gets a .bat wrapper for cmd/PowerShell (zef does the same); macOS's BSD sort has no -V so the wrapper falls back to lexicographic order. If site/bin/ is read-only it falls back to <target>/bin with a hint from raku-pm env.

  • test-depends → Test, so zef install runs t/ 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:

DBIish 0.5.5–0.6.1 bundled NativeLibs inside itself; from 0.6.2 on it was split out into a standalone NativeLibs distribution.

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:

  1. A distribution whose name equals the module name wins — NativeLibs the module belongs to NativeLibs the distribution;

  2. then higher version (via real semver, not string cmp — "0.9" > "0.10" is the wrong answer);

  3. 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-check skips 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 nameWindowsmacOSLinux
sqlite3sqlite3.dlllibsqlite3.dyliblibsqlite3.so
ssllibssl-3-x64.dlllibssl.dyliblibssl.so
libffilibffi-8.dlllibffi.dyliblibffi.so
zlibzlib1.dlllibz.dyliblibz.so
unknown foofoo.dlllibfoo.dyliblibfoo.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's installed/*/bin

  • 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-family multiarch dirs (/usr/lib/x86_64-linux-gnu; modern Debian moved everything there, so scanning only /usr/lib misses it) → versioned .so.N matched 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 need

That 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 path

It 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's META6.json sits 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():

SpellingMeaning
Foo:ver<1.2.3>exactly 1.2.3
[email protected]@ shorthand, same as above
Foo --version=1.2.3the 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::Stack 1.1 is cpan:JNTHN while 1.3 is zef: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:

CommandEffect
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.bat ends in raku ... %*, and cmd re-parses %* while the quotes have already been stripped before the batch file sees them, so search "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 --locked

Afterwards, 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 RAKULIB

One matching rule, shared by search / store / installed / install (RakuPM::Repository::Matching, a repository-layer role; both RakuPM::Repository and RakuPM::Client::Query does it, 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 why search method used 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, so installed Web::App finds the dist Web-App, while installed Web does 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.t tests 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 rea

Maintenance

raku -Ilib bin/raku-pm.raku upgrade JSON    # upgrade to latest
raku -Ilib bin/raku-pm.raku uninstall JSON  # uninstall

Sample 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.lock

Installing 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's provides from disk

  • raku-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 + write Changes)

  • raku-pm publish [path] — deploy to a local directory repo (see below; requires dist first)

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 to 0.1.0; auth defaults to RAKUPM_AUTHOR_AUTH or zef:skyter10086)

  • lib/<module>.rakumod — main module stub (unit module + an our sub hello smoke)

  • t/01-basic.rakutest — use-ok smoke test

  • Changes — initial version header

  • .gitignore — ignores .precomp

  • README.md — stub (only with --with-readme)

  • bin/<x>.raku — executable (only with --bin=<x>); META6's bin lists 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 only

refresh 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 nothing

Example output:

==> 重建 META6.json 的 provides:/path/to/Web-App
    磁盘 lib/ 下扫到 3 个模块
  · 加入(磁盘上有、声明里没有):
      + Web-App
      + Web::Legacy
      + Web::Router
  · 剔除(声明里有、磁盘上没有,写了也装不上):
      - Web::Ghost
  ✓ 已更新 /path/to/Web-App/META6.json

Deliberately conservative scope — only what disk can uniquely determine:

FieldWhat refresh does
providesFully rebuilt: scan lib/ → module names → relative paths (.rakumod/.pm6 both recognised; .rakudoc is not a module)
resources / binUntouched. Resources are managed by explicit declaration (mi6 likewise leaves them to fez resource); bin is a path array that disk cannot uniquely determine
Everything elsePreserved verbatim (name / version / auth / depends / tags / source-url …)
When nothing changedNo 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. With RAKUPM_AUTHOR_AUTH unset 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 (run raku-pm refresh first)

  • 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 published

The 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 conventional class Build with a build method), dist runs it in place (native-extension dists use it to compile resources/libraries/*.so, and the product lands in the tarball). A pure-Raku dist has no Build.pm and is skipped automatically. --no-build forces the build to be skipped.

  • What gets packaged: META6 + lib/ + bin/ + t/ + resources/ + LICENSE

    • README, etc.; .git / .hg / .svn / blib / .precomp / .DS_Store are 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.

  • .distignore custom exclude list: the hidden-entry filter cannot stop normally-named internal files (AI instructions, internal roadmap, internal handbook…). Drop a .distignore in 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; --dry previews 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 ../out

Deploy 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 var RAKUPM_PUBLISH_REPO; else it errors asking you to specify one. The target should be a local repo registered with repos add, so install can 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 the check gate + 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 bump first, or --force to overwrite. --remote takes the real-ecosystem upload path (M2: build the request + --dry preview; the real PUT lands in M3); without --dry it errors explicitly and defers to M3 — never silently sends anything over the network.

  • Not dry-run by default — publish deploys immediately (extracts into the directory); use --dry to preview the drop location only. Deleting is irreversible, but publishing only adds a directory to the repo, so on a mistake just rm it.

  • The internally built temporary sdist is cleaned up after a successful deploy; a package given via --from is 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 change

When 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):

  1. Dependency reclamation on uninstall — implemented, see below.

  2. Dependency-tree visualisation: an rdepends reverse lookup. — implemented, see below.

  3. Incremental index updates: the whole index is cached; no ETag / If-Modified-Since. ✅ Implemented (since 0.36.x): the HTTP facade and both backends support conditional GET (If-None-Match → 304 reuses the cache), with the ETag kept in a sibling <cache>.etag file. RAKUPM_INDEX_TTL controls the reuse window; once it expires we still ask the server once (we just very likely get a 304).

  4. 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:

CapabilityStatusWho 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. publish will insist on dry-run first plus a second confirmation, because the ecosystem forbids re-uploading a version (auth must 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).

RakuPM v1.0.3

一个教学用的 Raku 包管理器:多索引仓库、依赖解析、跨系统本地库(:from<native>)探测、内容寻址存储、锁文件、单版本激活、单后端 HTTP 客户端(curl)、索引 TTL 与网络重试

Authors

  • skyter10086

License

Apache-2.0

Dependencies

Test Dependencies

Provides

  • RakuPM::Author
  • RakuPM::Builder
  • RakuPM::CLI
  • RakuPM::Cleaner
  • RakuPM::CliCheck
  • RakuPM::CliSpec
  • RakuPM::Client
  • RakuPM::Client::Flusher
  • RakuPM::Client::Git
  • RakuPM::Client::Query
  • RakuPM::Client::SelfManager
  • RakuPM::Client::Tester
  • RakuPM::Distribution
  • RakuPM::FileLock
  • RakuPM::Fs
  • RakuPM::HTTP
  • RakuPM::HTTP::Backend
  • RakuPM::HTTP::Tinyish
  • RakuPM::Help
  • RakuPM::InstallOptions
  • RakuPM::Installer
  • RakuPM::Installer::Generations
  • RakuPM::Installer::ShellTemplates
  • RakuPM::Ledger
  • RakuPM::Lock
  • RakuPM::MD5
  • RakuPM::Message
  • RakuPM::NativeLib
  • RakuPM::Net
  • RakuPM::Platform
  • RakuPM::Prefix
  • RakuPM::Repositories
  • RakuPM::Repository
  • RakuPM::Repository::Ecosystem
  • RakuPM::Repository::Local
  • RakuPM::Repository::Matching
  • RakuPM::Resolver
  • RakuPM::Spec
  • RakuPM::Store
  • RakuPM::UI
  • RakuPM::Version

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.