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 own META6.json as installed).

  • Every installed distribution's depends names 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.

App::Ariza v0.2.4

bundler and distribution tool for Raku terminal apps

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Config::TOML:ver<0.1.3+>:auth<zef:raku-community-modules>JSON::Fast:ver<0.19+>:auth<cpan:TIMOTIMO>Template::Jinja2:ver<0.3.0+>:auth<zef:apogee>

Test Dependencies

Provides

  • App::Ariza
  • App::Ariza::Bundle
  • App::Ariza::CI
  • App::Ariza::Config
  • App::Ariza::Installer
  • App::Ariza::Launcher
  • App::Ariza::Licensing
  • App::Ariza::Native
  • App::Ariza::Platform
  • App::Ariza::Rakudo
  • App::Ariza::Resources
  • App::Ariza::Runner
  • App::Ariza::Site
  • App::Ariza::Smoke
  • App::Ariza::Tools
  • App::Ariza::Update
  • App::Ariza::Versions

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.