Site
NAME
App::Ariza::Site - install the app and its closure into the bundle's own module repository
SYNOPSIS
use App::Ariza::Site;
my %site = App::Ariza::Site.build-site(
:bundle-dir($work),
:app-source('/path/to/App-Moneymoor'),
:config($cfg),
:zef(%rt<zef>), # from App::Ariza::Rakudo.provision
:raku(%rt<raku>),
);
say %site<site-rel>; # rakudo/share/perl6/vendor
say %site<target-rel>; # rakudo/share/perl6/vendor/bin/moneymoor.raku
say %site<app-version>; # 0.2.0
say %site<notcurses-tag>; # binaries-notcurses-3.0.17-r11
say %site<notcurses-lib-rel>; # native/Notcurses-Native/β¦-r11/lib
say +%site<dists>; # 8
say +%site<warmed>; # 50
DESCRIPTION
The bundle's Raku code lives in one CompUnit::Repository::Installation
β < <bundle>/rakudo/share/perl6/vendor >, the vendor repository of
the runtime the bundle carries β and nothing else: no user repository,
no system repository, no ~/.raku. This module fills it.
Why the app goes in the runtime's vendor repository
Because a repository Rakudo has no name for cannot ship warm bytecode.
Rakudo records what each precompiled unit depends on. For a repository
the registry knows by name it records
< <name>#sources/<id> > β core#sources/β¦, vendor#sources/β¦ β
which is resolved against wherever that repository turns out to be. For
any other repository it records the absolute path of the source file
on the machine that compiled it.
The registry knows exactly four names: core, vendor and site
under the running interpreter's own prefix, and home under $HOME.
A repository reached through RAKULIB=inst#/some/path β which is what
< <bundle>/site > was β gets none of them.
So a bundle built at /__w/App-Moneymoor/dist-out/β¦ and installed at
~/.local/share/moneymoor/versions/0.3.0 would, on its first launch,
look for its own modules where the build machine had kept them, find
nothing, declare all 52 compiled units outdated and recompile the entire
closure: 58 seconds on a warm laptop, for a store that shipped
complete and correct. Every later launch was fast, so it looked like a
one-off cost of installing rather than the bug it was.
< <bundle>/rakudo/share/perl6/vendor > is the bundled runtime's
own vendor prefix. The registry names it, the store records
vendor#sources/β¦, and the bytecode survives being moved β the same
mechanism that lets the zef inside the runtime's site repository
run from a bundle unpacked anywhere. It is also in that interpreter's
default chain, so it resolves even for a command that runs the bundled
raku directly and never sets RAKULIB.
vendor and not site because the runtime's site is where that
zef lives: keeping the app's closure out of it means "what this
bundle installed" and "what the runtime shipped with" stay two
separate, separately reportable sets β which is what
App::Ariza::Licensing walks and what the manifest lists.
build-site asks the bundled runtime what it calls the repository
before warming a single module, and refuses to build if the answer is
nothing. The failure this prevents is invisible on the machine that
builds it: the store is present, the right size, and worthless.
The install has to run under the bundled runtime
On POSIX, zef is invoked as
< <bundle>/rakudo/share/perl6/site/bin/zef >: the copy that came down
inside the runtime archive. That wrapper is a shell script which
locates its sibling raku relocatably and execs it, so it is run
directly β feeding it to bin/raku as if it were a Raku script is a
syntax error.
The official Windows zip ships no equivalent zef.bat: only zef.exe
and zef.raku (plus -m/-j/-js variants). zef-cmd handles
that by running zef.raku β which App::Ariza::Rakudo.zef-bin
returns on Windows for this exact reason β under the bundled raku,
rather than exec'ing it: < <bundle>\rakudo\bin\raku.exe >
< <bundle>\rakudo\share\perl6\site\bin\zef.raku > install β¦.
zef.raku is byte-identical on every platform zef ships for
(sub MAIN(*@, *%) { CompUnit::RepositoryRegistry.run-script("zef") }),
so this depends only on the raku it is handed, not on any assumption
about a wrapper's format. zef.exe would work too, but a compiled
wrapper's own raku-discovery behaviour inside a relocated bundle is
unverified, so it is left alone.
Using the system zef instead would work, and would produce a bundle that recompiles its entire closure on first launch, because precompiled bytecode is tied to the exact Rakudo that produced it.
RAKULIB during the install, not just after it
The site repository is named in RAKULIB for the zef child as well
as --to. zef installs into a repository that is not in the chain
perfectly happily β and precompiles nothing at all when it does. With
the target in the chain some precompilation happens during install; the
rest is done explicitly (below).
PERL6LIB is removed from the child environment rather than set
empty, because an empty-but-set PERL6LIB makes Rakudo print a v6.e
deprecation warning across the whole run.
Warming the precompilation store
Even with the repository in the chain, zef install leaves most of the
closure uncompiled: the first process to use a module is what
compiles it. Shipping that state means the user's first launch compiles
everything β measured at ~55 seconds for Moneymoor, against ~0.5s
once warm β and pays it again on every launch if the bundle lives
somewhere unwritable, which for a downloaded archive is entirely normal.
So build-site loads every module the app distribution provides,
under the bundled runtime, with the bundle's environment. That pulls the
transitive closure (Selkie, DBIish, Notcurses::Nativeβ¦) into the store
as a side effect of loading what actually uses it.
The store is content-addressed, and β because the repository it belongs
to is one the runtime has a name for (above) β every dependency in it is
recorded relative to that repository. So warming it at build time and
unpacking the bundle somewhere else entirely, including a path with
spaces, loads the same bytecode. That is verified rather than assumed:
xt/05-relocation.rakutest builds a bundle, unpacks it at a second
path, deletes the first, and fails on a single recompilation.
Loading happens in a child that reports each module as it finishes, so a module that takes the process down rather than throwing something catchable costs only itself; the parent resumes at the next one. A module that fails to load is fatal: at this point every dependency is supposed to be in the bundle, so a load failure is the closure being incomplete, which is exactly the bug a bundle exists to prevent.
What is checked before the bundle is called good
The app distribution is in the site repository, by the name declared in
ariza.toml(which is also where its version comes from β the app's ownMETA6.jsonas installed).Every installed distribution's
dependsnames another installed distribution. A gap means zef satisfied something from a repository outside the bundle: the classic bundle that runs only on the machine that built it.The bundled runtime has a name for the repository, so the warm store it is about to be handed will still be valid on somebody else's machine.
Every module the app provides loads.
The precomp store holds at least one artefact per module warmed, and is not empty.
Notcurses-Native staged a non-empty library directory, if the app asked for notcurses in
bundle.native.
The notcurses tag is read, never written
Notcurses::Native's Build.rakumod stages its prebuilt libraries to
< <NOTCURSES_NATIVE_DATA_DIR>/Notcurses-Native/<BINARY_TAG>/lib >.
Pointing that variable at < <bundle>/native > during the install puts
them straight into the bundle, and pointing it at the same place at run
time is how they are found again β which is the launcher's job.
The tag (binaries-notcurses-3.0.17-r11) names a notcurses build, not
anything of ariza's, and changes whenever that distribution rebuilds. It
is read back off disk after the install and carried into the manifest.
Exactly one staged tag is expected; more than one means a stale build is
in the bundle and is an error rather than a coin toss.
Note NOTCURSES_NATIVE_DATA_DIR and not NOTCURSES_NATIVE_LIB_DIR:
the latter suppresses the module's own TERMINFO_DIRS setup, and a TUI
without terminfo is a blank screen.
METHODS
build-site(:$bundle-dir!, :$app-source!, :$config!, :$zef!, :$raku!, :$attempts, :$verbose --> Hash)
Install, warm, check. Returns site, site-rel, native, dists,
app, app-version, target, target-rel, notcurses-tag,
notcurses-lib, notcurses-lib-rel and warmed. The relative form
is the exact tag-selected directory launchers and smoke put on Windows
PATH.
:$attempts (default 2) retries the zef run, but only when the
failure looks like a fetch or resolution problem β a CDN hiccup aborts
the whole install with "Aborting due to fetch failure", and everything
zef completed before that point is already installed, so the retry costs
only the failed leg. A compile error is not retried.
The launcher's target
target-rel is the path, relative to the bundle root, that the
launcher hands to the bundled raku:
rakudo/share/perl6/vendor/bin/<exec>.raku. site-rel is the
repository it lives in, rakudo/share/perl6/vendor, which is what the
launcher puts in RAKULIB. Both are relative because nothing a bundle
ships may name the machine that built it.
zef installs two files per bin/ script β a sh (or .bat) wrapper
named after the executable, and a <name>.raku stub calling
CompUnit::RepositoryRegistry.run-script. The wrapper is useless in a
bundle: it execs a bare rakudo off PATH, which on a user's machine
is absent or is a different Rakudo. The stub, run by the bundled
interpreter with RAKULIB pointing at the bundle, is the correct
target.
exec-target(IO() $site, :$config! --> IO::Path)
The script the launcher hands to the interpreter: the .raku stub if
zef installed one, else the plain bin/<exec> file. Dies
listing what bin/ does contain when neither exists.
zef-cmd(IO() $zef, IO() $raku, *@args --> List)
The argv to run zef with. On POSIX, $zef (the shell wrapper) is
exec'd directly with @args. On Windows, $zef is zef.raku β the
run-script stub, not the wrapper the zip does not ship β so the command
is $raku, $zef, @args: run under the bundled interpreter rather
than exec'd. Which shape applies is read off $zef itself (its
extension), not off a platform slug.
closure-gaps(@dists --> List)
Every "A needs B" where B is not installed in the bundle. Returned
rather than thrown so all of them can be reported at once β and so the
rule is testable without building anything.
The dependency name is taken by stripping from the first :adverb<>,
not by splitting on :, which would turn a dependency on
JSON::Fast into one on JSON.
installed-dists(IO() $bundle-dir --> List)
{ name, version, auth, license, authors, provides, depends } for every
distribution in the site repository, sorted by name. Read from the
repository's own dist/ metadata, so it describes what is in the
bundle rather than what the app asked for.
dists-in(IO() $site-dir --> List)
The same, for any installation repository directory. A bundle has two:
the runtime's vendor repository, which is the app and its closure,
and the runtime's site repository, where the zef that came down
with Rakudo lives β and which is therefore redistributed like everything
else. App::Ariza::Licensing walks both.
license and authors come straight out of each distribution's
metadata and are undefined where it declares none. What an absent
licence field means is decided in one place, and it is not this one.
child-env(IO() $bundle-dir --> Hash) / site-dir / site-rel / native-dir
The environment a bundled-runtime child needs, and the two directories
it names: < <bundle>/rakudo/share/perl6/vendor > for modules (see
above for why it is that and not < <bundle>/site >) and
< <bundle>/native > for native payloads. site-rel is the first of
those relative to the bundle root, with forward slashes, for the
launcher templates and the manifest.
SEE ALSO
App::Ariza::Rakudo for the runtime this installs into,
App::Ariza::Launcher for the run-time twin of child-env.
AUTHOR
Matt Doughty
COPYRIGHT AND LICENSE
Copyright 2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.