Launcher
NAME
App::Ariza::Launcher - the one script a user runs
SYNOPSIS
use App::Ariza::Launcher;
my %w = App::Ariza::Launcher.write(
:bundle-dir($work),
:config($cfg),
:slug<macos-arm64>,
:target<rakudo/share/perl6/vendor/bin/moneymoor.raku>,
:site<rakudo/share/perl6/vendor>,
:app-version<0.2.0>,
:sqlcipher-rel<rakudo/lib/libsqlcipher.0.dylib>,
);
say %w<written>; # (ā¦/bin/moneymoor)
say %w<runner>; # {} ā macOS stages no executable
# Just the text, for a diff or a test:
say App::Ariza::Launcher.render(
:template<launcher-posix.sh.j2>,
|App::Ariza::Launcher.context(:$cfg, :slug<linux-x86_64-glibc>,
:target<rakudo/share/perl6/vendor/bin/moneymoor.raku>),
);
DESCRIPTION
Everything else in a bundle is inert until something sets three or four environment variables and starts the right interpreter on the right script. This module writes the thing that does that ā the only file in the bundle a user is ever expected to touch.
What the launcher does
Resolves its own physical path, following symlinks, and takes the directory above it as the bundle root.
Refuses, with a readable message, if the interpreter is not where it should be ā the signature of a half-unpacked archive.
Exports
RAKULIB=inst#<root>/rakudo/share/perl6/vendorand unsetsPERL6LIB, so the bundle's repository is the only one. ARAKULIBleft in the user's environment would otherwise put modules compiled against a different Rakudo ahead of the bundle's. That path is the bundled runtime's ownvendorprefix, and not a directory of ariza's choosing, because it is the only kind Rakudo has a name for ā see App::Ariza::Site for what a nameless one costs the user.Exports
NOTCURSES_NATIVE_DATA_DIR=<root>/native, which is both where App::Ariza::Site staged the notcurses libraries and where Notcurses::Native looks for them ā and, crucially, is notNOTCURSES_NATIVE_LIB_DIR, which suppresses that module's ownTERMINFO_DIRSsetup and leaves a TUI without terminfo.On Windows, puts the exact tag-selected Notcurses
lib/directory first onPATH. NativeCall can findlibnotcurses.dllby absolute path fromNOTCURSES_NATIVE_DATA_DIR, but ordinary Win32 dependency search does not thereby search beside that top DLL. SQLCipher's directory follows Notcurses when both are staged, then the inherited PATH.On Linux, additionally puts the bundle's SQLCipher directory on
LD_LIBRARY_PATHand pointsDBIISH_SQLCIPHER_LIBat the library. macOS needs neither: the library is staged insiderakudo/lib, which the bundled interpreter's ownLC_RPATHalready covers.Prints a one-line first-run notice to
STDERR, once, guarded by a.first-runsentinel under${XDG_STATE_HOME:-$HOME/.local/state}/<exec>/. Nothing is written into the bundle itself, so a read-only or shared bundle behaves the same as a private one.Warns ā never refuses ā when
TERMis empty,dumborunknown. A launcher that second-guesses the user's terminal is worse than one that mentions it.execs the bundledrakuon the app's installed script, passing every argument through.
Why not `readlink -f`
readlink -f is a GNU extension. It is absent on macOS before Monterey
and on the BSDs, and a launcher that resolves symlinks only on Linux is
one that breaks the first time someone puts a link to it in ~/bin ā
which is the single most likely thing a user will do with a bundle. The
POSIX template loops over plain readlink instead.
Spaces
Every expansion in the script is quoted, every path is built from
$BUNDLE_ROOT rather than assumed, and nothing is passed through
eval. A bundle unpacked into /Users/someone/My Applications/ works,
and the test suite unpacks into a directory with spaces in the name
specifically to keep it that way.
Windows
A Windows bundle gets four files in bin/, and only one of them is the
documented entry point.
< <exec>.exe >ā the compiled launcher, staged by App::Ariza::Runner from a pinned, digest-verified release artefact. This is what users run and what the installers put onPATH. It does the whole launch with nocmd.exeanywhere in it, which is what keeps^,%VAR%,!x!and quote-heavy arguments intact on the way to the app ā a batch file re-parses%*and cannot not damage them ā and what keeps the bundle usable where script-execution policy (AppLocker, SRP, a locked-downExecutionPolicy) refuses to run a.cmdor a.ps1at all.< <exec>.ariza >ā the runner's sidecar. Rendered like any other launcher, from the same context, and readable by anyone who wants to know what the executable is about to do. It carries the target script, the exec and display names, and then the bundle's environment as ordered directives:
target rakudo\share\perl6\vendor\bin\moneymoor.raku
app-exec moneymoor
app-display Moneymoor
set RAKULIB=inst#{root}\rakudo\share\perl6\vendor
unset PERL6LIB
set NOTCURSES_NATIVE_DATA_DIR={root}\native
prepend-path {root}\native\sqlcipher
prepend-path {root}\native\Notcurses-Native\binaries-notcurses-3.0.17-r11\lib
set DBIISH_SQLCIPHER_LIB={root}\native\sqlcipher\sqlcipher.dll
{root} is the bundle root the executable works out at run time, so
nothing absolute is baked in. set, unset and prepend-path are
applied top to bottom, and the runner knows nothing whatever about what
the variables mean ā which is the point. Everything ariza knows about
Rakudo's repository, notcurses' data directory and where a bundled DLL
lives is here, in the renderer, exactly as it is in the .cmd
template. A bundle that grows a native dependency grows a line in this
file; it does not need a new executable, and an executable pinned
several releases ago still launches it.
Because each prepend-path takes effect immediately, the lower-priority
SQLCipher directory is written first above. The resulting PATH begins
with Notcurses, then SQLCipher, matching the .cmd, .ps1 and smoke
environments.
< <exec>.cmd >and< <exec>.ps1 >ā still written and still supported as transparent launchers. In an update-disabled bundle they carry the complete script implementation and are the bootstrap fallback before a runner is pinned. In an update-enabled Windows bundle they delegate to the required runner-v2 executable, because that process owns the authenticated handoff and the original argument tail.
$PSScriptRoot and %~dp0 already give a resolved directory, so
neither script needs the POSIX symlink loop; the executable asks Windows
where it is with GetModuleFileNameW and takes the directory above
bin/, which is the same rule spelled a third way.
Windows output is CRLF, sidecar included; cmd.exe mis-parses an
LF-only batch file in ways that look nothing like a line-ending problem,
and having one file in bin/ disagree with the others about line
endings is a difference nobody should have to think about.
METHODS
write(:$bundle-dir!, :$config!, :$slug!, :$target!, :$app-version, :$notcurses-rel, :$sqlcipher-rel, :&stage-runner --> Hash)
Render and write every launcher the slug needs into
< <bundle>/bin >, chmod 0755 for the POSIX one, stage the compiled
runner where there is one, and return { written, runner } ā every
path written, and App::Ariza::Runner's account of what it staged
(path, artifact, tag, url, sha256), which is what
ariza-manifest.json records so the executable in a bundle can be
traced back to a published, digest-verified artefact.
:&stage-runner replaces the call into App::Ariza::Runner ā the one
thing here that touches the network ā so a test can render a Windows
bundle's bin/ without one. It is expected to return that same hash,
or an empty one for a bundle that gets no executable: every non-Windows
platform, and a Windows platform built while
resources/runner-checksums.txt is still empty.
:$target is the bundle-relative script the launcher hands to the
interpreter ā App::Ariza::Site's target-rel. :$notcurses-rel is
the exact tag-selected library directory from that Site result, and
:$sqlcipher-rel is
the bundle-relative library path; omitting it renders a launcher with no
SQLCipher wiring at all, which is what an app that does not use it
should get.
render(:$template!, *%ctx --> Str)
One template, as text. Exposed separately because that is what a golden-file test compares, and a launcher is a thing worth diffing rather than merely running.
context(:$config!, :$slug!, :$target!, :$app-version, :$notcurses-rel, :$sqlcipher-rel --> Hash)
The render context. Every path in it is relative to the bundle root: absolute paths baked in at build time are exactly what makes an archive unmovable.
scripts-for(Str $slug --> List)
The { template, suffix, mode } entries a slug's bundle gets rendered,
in write order. Windows has three ā .ps1, .cmd and the runner's
.ariza sidecar; the .exe is not in this list because it is fetched
rather than rendered.
SEE ALSO
App::Ariza::Runner, which supplies the compiled Windows launcher this module stages and writes the sidecar for.
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.