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/vendor and unsets PERL6LIB, so the bundle's repository is the only one. A RAKULIB left 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 own vendor prefix, 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 not NOTCURSES_NATIVE_LIB_DIR, which suppresses that module's own TERMINFO_DIRS setup and leaves a TUI without terminfo.

  • On Windows, puts the exact tag-selected Notcurses lib/ directory first on PATH. NativeCall can find libnotcurses.dll by absolute path from NOTCURSES_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_PATH and points DBIISH_SQLCIPHER_LIB at the library. macOS needs neither: the library is staged inside rakudo/lib, which the bundled interpreter's own LC_RPATH already covers.

  • Prints a one-line first-run notice to STDERR, once, guarded by a .first-run sentinel 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 TERM is empty, dumb or unknown. A launcher that second-guesses the user's terminal is worse than one that mentions it.

  • execs the bundled raku on the app's installed script, passing every argument through.

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 on PATH. It does the whole launch with no cmd.exe anywhere 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-down ExecutionPolicy) refuses to run a .cmd or a .ps1 at 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.

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.