Smoke

NAME

App::Ariza::Smoke - prove a built bundle works somewhere it has never been

SYNOPSIS


use App::Ariza::Smoke;

my %r = App::Ariza::Smoke.smoke(:archive($tarball));

say %r<passed>;                     # True
for %r<checks> -> %c {
    say "{%c<ok> ?? 'ok  ' !! 'FAIL'} {%c<name>}: {%c<detail>}";
}
# ok   unpack: extracted to moneymoor-0.2.0-macos-arm64
# ok   manifest: App::Moneymoor 0.2.0 for macos-arm64
# ok   launcher: bin/moneymoor
# ok   runtime: rakudo 2026.07-01
# ok   target: rakudo/share/perl6/vendor/bin/moneymoor.raku
# ok   precomp: 395 precompiled artefacts ship with the bundle
# ok   precomp-relocatable: 2938 dependency records, all repository-relative
# ok   native-audit: 32 native binaries have bundled dependency closure
# ok   notcurses-load: {raku} → exit 0: full libnotcurses resolver and core visual passed
# ok   smoke[0]: {exec} → exit 0: App::Moneymoor 0.2.0
# ok   smoke[1]: {raku} → exit 0: ok: encrypted database created …

DESCRIPTION

A build that finishes is not a build that works. smoke takes the archive, unpacks it somewhere the build never touched, and runs it the way a stranger would.

The scratch directory has a space in its name

On purpose. ariza smoke 51234-882931/ is where the bundle goes, and if the launcher's quoting slips anywhere, that is what notices — before a user with ~/Application Support/ or C:\Program Files\ does.

The environment is replaced, not filtered

run's :env substitutes the child's whole environment, which is env -i without needing env. Commands get PATH=/usr/bin:/bin, HOME and TERM. Windows gets only SystemRoot\System32 on PATH, plus SystemRoot, ComSpec, PATHEXT and the other small set without which processes cannot start — never the runner image's MSYS2/vcpkg/Visual Studio toolchain PATH.

That is the entire point. The failure this catches is a bundle that quietly relies on the developer's shell: a system Rakudo on PATH, a DBIISH_SQLCIPHER_LIB pointing into Homebrew, a RAKULIB that already has the app installed in it. Any of those makes a broken bundle look perfect on the machine that built it.

Two environments, for two kinds of command

A command that starts with {exec} goes through the launcher, so it gets the bare environment above: that run is a test of the launcher's ability to set up its own world.

A command that starts with {raku} drives the bundled interpreter directly. It is standing in for code running inside the app, so it additionally gets exactly what the launcher would have exported — RAKULIB, NOTCURSES_NATIVE_DATA_DIR, and the native-library variables: LD_LIBRARY_PATH on Linux, and on Windows a PATH prepended with the exact Notcurses tag/lib directory followed by SQLCipher's directory, since that is how their dependent DLLs are found. DBIISH_SQLCIPHER_LIB is set on both, exactly as the launcher templates set it. Without any of that it would be testing the absence of a launcher rather than the bundle.

Placeholders

Smoke commands come from the bundle's own ariza-manifest.json, so an archive can be checked with no access to the app's repository. Each argv word is expanded against:

  • {exec} — the launcher

  • {raku} — the bundled interpreter

  • {site} — the module repository, wherever the manifest says the bundle put it

  • {native} — the staged native libraries

  • {bundle} — the bundle root

  • {tmp} — a writable scratch directory beside the unpacked bundle

Nothing goes through a shell, so a smoke command can contain an entire Raku program without a quoting layer to get wrong — which is what makes it reasonable for an app to smoke-test its database engine and not just --version. See App::Ariza::Config for how to write one.

The checks

  • unpack — the archive extracts to exactly one directory.

  • manifest — ariza-manifest.json is present, readable, and the right shape: it names an app, and every entry in dists is an object with a name. That last one is not paranoia. A Raku { } literal whose body starts with a .method call is parsed as a Block, and a Block of pairs serialises as an array of one-key objects — producing a manifest that parses, reads plausibly, and lists nothing at all.

  • launcher — < bin/<exec> > exists, and is executable where that is a question anyone can answer: it takes both a POSIX bundle to have an executable bit and a POSIX host to read one. The platform comes from the manifest rather than from the machine — that is what lets an archive be checked anywhere — so a Windows box inspecting a Linux bundle is ordinary, and there the bit is absent from the filesystem rather than absent from the bundle.

  • runner — Windows only: < bin/<exec>.exe > and its < bin/<exec>.ariza > sidecar. Their absence passes and says so — a bundle built before the first runner release was pinned launches from its .cmd and is not broken — but an executable with no sidecar to read is a bundle that cannot start, so the pair is checked together.

  • runtime — the bundled interpreter is there.

  • target — the script the launcher execs is there.

  • precomp — the precompilation store shipped warm. An empty one is not a crash, so nothing else would catch it; it just makes every launch slow, forever, on a read-only bundle.

  • precomp-relocatable — and that store is worth something somewhere else. Every dependency a compiled unit records has to name a repository (vendor#sources/…) rather than a place (/home/runner/work/…/sources/…), because Rakudo resolves the first wherever the bundle now is and throws the unit away when it cannot find the second. This is the one check here that cannot be replaced by running the thing: on the machine that built the bundle those paths still exist, so a bundle that will recompile its whole closure for every user passes every other check in this list, including the smoke commands.

  • native-audit — App::Ariza::Native's audit, re-run over the unpacked tree rather than the build directory, because that is the tree a user has.

  • notcurses-load — when the manifest declares Notcurses, first calls nc-lib. On Windows, Notcurses::Native's resolver eagerly loads the full library with LoadLibraryExW and its sibling dependency directory, so the FFmpeg-linked closure is genuinely exercised. It then creates and destroys a one-pixel visual through the core library as an operational check. A notcurses_version query would prove only a version binding; no terminal or Notcurses context is opened here.

  • smoke[n] — each declared command, in order.

  • smoke[n].exe — on Windows, each {exec} command again through < bin/<exec>.exe >, when the bundle carries one. In addition to the .cmd run, never instead of it: the script and the executable implement one contract by two completely different routes — batch set against SetEnvironmentVariableW, %* against a verbatim command-line tail — so a passing script says nothing at all about the executable, and the executable is the one a user will actually run.

Every check runs; nothing short-circuits. "The launcher failed" and "the launcher failed and the audit found a stray library" are different bug reports and should not require two runs to distinguish.

Failure leaves the evidence

On success the scratch directory is removed. On failure — or with :keep — it stays, and its path is printed. The whole value of a failed smoke is the state it failed in; deleting it means reproducing a build to see anything.

METHODS

smoke(:$archive!, :$work-dir, :$keep, :$verbose --> Hash)

{ passed, checks, dir, root, manifest, kept }. Each check is { name, ok, detail }, and command checks also carry output.

base-env(--> Hash) / runtime-env(IO() $root, %manifest --> Hash)

The replacement environment, and the launcher-equivalent additions.

notcurses-lib-dir(IO() $root, %manifest --> IO::Path)

The exact staged Notcurses library directory recorded by the manifest, with the tag-based derivation retained for older manifests.

notcurses-probe-program(--> Str)

The terminal-free Raku program used by notcurses-load. It explicitly calls nc-lib first, then creates and destroys a one-pixel visual through the core library without initializing a terminal or a Notcurses context. Exposed so call order and the no-terminal contract can be inspected without executing a native library in a unit test.

site-dir(IO() $root, %manifest --> IO::Path)

The module repository inside an unpacked bundle, taken from the manifest's launcher.site so that an archive is checked on its own terms — including one built before that repository moved into the runtime's vendor prefix, which recorded nothing and kept its modules in < <root>/site >.

scratch-dir(:$work-dir --> IO::Path)

A fresh directory with a space in its name.

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.