Native

E

<gt> path> lines are read. The two other shapes ldd #| emits — linux-vdso.so.1 (0x…), and the loader's own absolute path — #| carry no name/path pair to act on, and a NEEDED entry that is #| itself an absolute path (which prints in that second shape) is caught #| by elf-strays instead, where it belongs: nothing can be copied #| to fix it. our sub ldd-deps(Str:D $ldd-output --> List) is export { my @deps; for $ldd-output.lines -> $line { # " libcrypto.so.3 => /lib64/libcrypto.so.3 (0x00007f…)" my $entry = $line.trim.subst(/ \s* '(' <-[)]>* ')' $ /, ''); next unless $entry.contains(' => '); my ($name, $path) = $entry.split(' => ', 2).map(*.trim); next unless $name.chars; @deps.push($name => ($path.chars && $path ne 'not found' ?? $path !! Str)); } @deps.unique(as => *.key).List }

FILE

holding" ~ " a $slug build, or point {SQLCIPHER-DIR-ENV} at a directory with one." unless $host-slug.defined && $host-slug eq $slug;

FILE

holding a prebuilt library, or point" ~ " {SQLCIPHER-DIR-ENV} at a directory containing $lib-name." ~ (($err || $out).trim ?? "\n (brew --prefix sqlcipher: " ~ ($err || $out).trim.lines.head ~ ')' !! '') unless $code == 0;

FILE

.\n" ~ ($fetch-err.trim ?? $fetch-err.trim.lines.map({ " $_" }).join("\n") !! '') unless $fetch-code == 0;

FILE

." ~ ($cache-err.trim ?? "\n ({$cache-err.trim.lines.head})" !! '') unless $bottle.chars && $bottle.IO.f;

FILE

," ~ " or point {SQLCIPHER-DIR-ENV} at a directory containing it.\n" ~ " Looked in: {@dirs.join(', ')}"; }

FILE

.\n" ~ " Looked in: {@dirs.map(*.value).join(', ')}"; }

FILE

holding a library that is already" ~ " self-contained."; return (); }

NAME

App::Ariza::Native - stage SQLCipher into a bundle, and prove nothing loads from outside it

SYNOPSIS


use App::Ariza::Native;
use App::Ariza::Versions;

my %sql = App::Ariza::Native.stage-sqlcipher(
    :bundle-dir($work),
    :slug<macos-arm64>,
    :versions(App::Ariza::Versions.load),
);

say %sql<rel>;              # rakudo/lib/libsqlcipher.0.dylib
say %sql<version>;          # 4.14.0   (what was staged)
say %sql<pinned>;           # 4.14.0   (what versions.toml expected)
say %sql<origin>;           # homebrew keg: /opt/homebrew/opt/sqlcipher/lib/...
say %sql<sha256>;           # digest of the library as it came off the machine

# Everything that had to travel with it. On Linux this is the library
# plus the OpenSSL a bare NEEDED would otherwise have borrowed from the
# user's machine, each with its rpath set to $ORIGIN:
say %sql<staged>.map(*.basename);   # (libsqlcipher.so.0 libcrypto.so.3 libz.so.1)

# Where it would come from, without staging anything:
say App::Ariza::Native.sqlcipher-source(:slug<macos-arm64>)<origin>;

# Stage from a file instead — an air-gapped build, or a cross-build:
App::Ariza::Native.stage-sqlcipher(..., :archive('/tmp/libsqlcipher-linux-x86_64.tar.gz'));

# Everything ariza put in the bundle must be self-contained. For PE this
# proves closure and adjacency; the launcher still supplies the live PATH:
my %audit = App::Ariza::Native.audit(
    :bundle-dir($work), :slug<macos-arm64>, :extra(%sql<staged>));
say %audit<checked>;        # 31

# The verdict functions are pure, and take tool output as text:
say macho-strays(qq:to/OUT/);
    libfoo.dylib:
    	/opt/homebrew/lib/libcrypto.3.dylib (compatibility version 3.0.0)
    OUT
# (/opt/homebrew/lib/libcrypto.3.dylib)

# Windows needs no tool: a PE's import table is read out of its bytes,
# from any host.
say pe-imports('sqlcipher.dll'.IO);
# (libcrypto-3-x64.dll KERNEL32.dll api-ms-win-crt-heap-l1-1-0.dll)
say pe-imports('sqlcipher.dll'.IO).grep({ !pe-system-dll($_) });
# (libcrypto-3-x64.dll)

# ...with one family on that skiplist the audit judges anyway, because
# Windows does not actually ship it:
say pe-redist-dll('vcruntime140.dll');      # True
say pe-redist-dll('ucrtbase.dll');          # False — that one is the OS

DESCRIPTION

Two jobs, deliberately together: putting native libraries into a bundle, and refusing to ship one whose native dependency closure is not contained. Mach-O and ELF additionally prove loader resolution; PE proves the static closure staged together, while the generated Windows launchers put that directory on the live loader's PATH.

The notcurses side needs no work here — App::Ariza::Site stages it as a side effect of the zef install, by pointing NOTCURSES_NATIVE_DATA_DIR at the bundle. SQLCipher has no such hook, so this module finds a copy on the build machine and places it.

SQLCIPHER

Where it goes, and why macOS is different

  • macOS — < <bundle>/rakudo/lib >.

  • Linux — < <bundle>/native/sqlcipher >.

  • Windows — < <bundle>/native/sqlcipher >.

macOS is the odd one out because two separate things resolve there for free. The bundled moar carries LC_RPATH @executable_path/../lib, so is native('sqlcipher') — which is how DBDish::SQLCipher binds every one of its functions, with a bare library name and no environment variable in sight — finds the library with no launcher involvement at all. And it is exactly the directory App::Moneymoor's entry script probes before deciding whether to pay for MacOS::NativeLib's brew config round-trip, so staging there also removes about a second from every launch.

That is native('sqlcipher') binding is worth dwelling on, because it is why DBIISH_SQLCIPHER_LIB alone is not enough anywhere: DBDish::SQLCipher uses that variable to pre-dlopen a library by absolute path, but its actual FFI declarations still name sqlcipher by leaf name, so the leaf name has to resolve too. Linux and Windows get both — LD_LIBRARY_PATH/PATH for the leaf name and DBIISH_SQLCIPHER_LIB for the pre-load — from the launcher.

Each platform also gets the second name the loader may ask for (libsqlcipher.dylib beside libsqlcipher.0.dylib) as a symlink, so one image serves both, falling back to a copy where symlinks are not available.

Where it comes from: the machine's own package manager

There is no ariza-operated SQLCipher mirror, and there is not going to be one. SQLCipher's ABI does not move often enough to justify a second piece of release infrastructure with its own signing story, its own staleness and its own outage; CI installs the distribution package before it calls ariza bundle, exactly as a developer does on a laptop. What makes a package-manager library safe to redistribute is the self-containment pass and the audit below, not where it was downloaded from.

So sourcing resolves in this order:

  • :archive (--sqlcipher-archive=FILE) — a tarball or zip to unpack and search. It beats everything, and is the answer for an air-gapped build or a cross-build.

  • SQLCIPHER_LIB_DIR — a directory holding the library. Named by a human, so it also beats the package managers. If the directory does not contain the library, that is fatal rather than a silent fall-through: someone who names a directory has ruled out every other source.

  • The platform's package manager:

    • macOS — brew --prefix sqlcipher, and the keg's lib/libsqlcipher.0.dylib (symlinks chased, so what is copied is bytes rather than a link into a Cellar that will not exist on the user's machine). Not installed but Homebrew present: brew fetch --formula sqlcipher and the bottle out of brew --cache, which is the same binary brew install would have placed. No Homebrew at all is a die naming brew install sqlcipher and --sqlcipher-archive.

    • Linux — ldconfig -p (tried as both ldconfig and /sbin/ldconfig, which is not on every user's PATH), then the Debian multiarch, Red Hat and Alpine library directories. The exact canonical name — libsqlcipher.so.0 — wins outright when it is there; where it is not, EPEL's sqlcipher package renames the soname per version and ships nothing literally called that, so any libsqlcipher*.so* from the same two sources is considered too, and the newest of them — by a numeric-aware comparison, not a plain string sort — is taken, with origin naming what was actually found. That rename is safe: is native('sqlcipher') dlopens the staged copy by leaf name through LD_LIBRARY_PATH, which resolves by filename and never consults the file's own DT_SONAME, so a distro library staged under the canonical name loads exactly as if it had been called that on the machine that built it. Absent is a die naming dnf install sqlcipher, apt install libsqlcipher0 and apk add sqlcipher-libs, and --sqlcipher-archive.

    • Windows — an MSYS2 environment's bin (MSYSTEM_PREFIX, then C:\msys64\ucrt64\bin and C:\msys64\mingw64\bin) and a vcpkg tree under VCPKG_ROOT, in that order. Absent is a die naming pacman -S mingw-w64-ucrt-x86_64-sqlcipher, vcpkg, SQLCIPHER_LIB_DIR and --sqlcipher-archive. Whichever directory answers, it is more than where the DLL is found: it is also the search space for everything that DLL imports (see below), because both package managers install a port's whole runtime closure into one directory (installed/<triplet>/bin, ucrt64/bin).

MSYS2 is preferred, and the reason is vcruntime140.dll. vcpkg builds with MSVC, so its sqlcipher.dll imports the Visual C++ runtime, which is not part of Windows: it arrives with the Visual C++ Redistributable, which every CI runner and every developer machine has and a clean install has not. MSYS2's mingw-w64-ucrt-x86_64-* packages import ucrtbase.dll instead, which Windows 10 and later ship in System32. They are also prebuilt, which removes the fifteen-minute source build the vcpkg lane needed and the cache it needed to avoid it. "The redistributable gate" below is what happens if an MSVC-built library is staged anyway.

The two package managers do not agree on what the library is called, either: vcpkg's port produces sqlcipher.dll and MSYS2's produces libsqlcipher-0.dll. So the canonical name wins where it is found — per directory, in priority order, matched case-insensitively as the loader matches it — and where no directory holds it the search widens to libsqlcipher*.dll, newest first by the same numeric-aware comparison the Linux pass uses (-3.34. above -3.9., which a string sort gets backwards). origin names what was actually found.

Whatever is found is staged under the canonical sqlcipher.dll, and that rename is safe for exactly the reason the Linux one is: LoadLibrary resolves a DLL by the leaf name on disk and never consults the module's internal name in its export directory, just as dlopen never consults DT_SONAME. A libsqlcipher-0.dll staged as sqlcipher.dll loads as if it had been built under that name.

A system library only fits the system it came from

Taking macos-arm64's library off this machine while building a macos-x86_64 or linux-x86_64-glibc bundle would produce an artefact that fails at dlopen on every machine it was built for, so the package-manager strategies refuse to run unless the requested slug is this machine's slug. The two explicit forms — :archive and SQLCIPHER_LIB_DIR — bypass that check, because a human naming a file has already said which platform it is for.

That is the whole cross-build story: build linux-x86_64-glibc on Linux (which is what CI does), or hand ariza a Linux library.

The pinned version is advisory

versions.toml keeps sqlcipher = "4.14.0", but it is now the version ariza expects, not one it can enforce: the machine's package manager decides what is actually installed. A mismatch prints


ariza: sqlcipher 4.17.0 staged, pin says 4.14.0

and the build continues. The manifest records what was staged, never the pin — a manifest that quotes a number nobody verified is worse than no number at all — with the pin alongside it as pinned.

The version is read out of the staged library: SQLCipher keeps CIPHER_VERSION as a NUL-terminated X.Y.Z constant, and sqlcipher-version-of looks for exactly that, reporting the undefined Str unless it finds exactly one candidate. Filenames are no use here — a Homebrew keg's real file is libsqlcipher.3.51.3.dylib (SQLite's version, inherited) and Debian's is libsqlcipher.so.0.8.6 (libtool's current.revision.age); neither is the 4.x number anyone means.

Making the staged library self-contained (macOS)

A Homebrew library names its OpenSSL dependency by absolute path — /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib — which exists on the machine that built the bundle and on no user's machine at all. Rewriting the library's own install name does not touch that.

This pass, and the audit that follows it, are what make a package-manager-sourced library safe to ship at all; they are the reason the mirror was never needed.

So after placing the library, every dependency that is not bundle-relative or part of macOS is copied in beside it, rewritten to @loader_path/<name>, and recursed into. Then everything touched is re-signed: install_name_tool invalidates the linker's ad-hoc signature, and macOS on arm64 will not load an incorrectly signed library — a failure that surfaces as an unexplained dlopen error, not as a signature complaint.

A dependency that is missing from the build machine is fatal. There is nothing honest to do with it: the archive is not self-contained and cannot be made so from here.

Making the staged library self-contained (Linux)

ELF hides the same problem better, which is why it went unnoticed longer. A distribution's libsqlcipher.so.0 names its OpenSSL as a bare DT_NEEDED libcrypto.so.3 — no path, no RPATH, nothing an audit that only reads the file can object to. It is also, at run time, "whatever libcrypto.so.3 this machine has", which for a bundle whose entire promise is that it carries its own dependencies is the one thing it must not be: no OpenSSL 3 on the user's box and the database never opens; a different OpenSSL 3 and it opens with code nobody tested.

So Linux gets the same three steps macOS gets, spelled the way ELF spells them:

  • Walk. ldd — which is the loader, and therefore answers with what the machine will really do, including whatever the build exported in LD_LIBRARY_PATH — reports where each NEEDED soname resolves. Where there is no ldd, readelf -d plus ldconfig -p and the standard library directories ask the same question the long way round.

  • Copy in. Every dependency that is not on the skiplist is copied in beside the library under its soname, and recursed into. The skiplist is Notcurses-Native's scripts/ci/bundle-elf.sh list — the loader, libc, libm, libpthread, libdl, librt, libgcc_s, libstdc++ and friends, which must stay dynamic or two C libraries end up in one address space — with one deliberate difference: libcrypto and libssl are not on it. That script leaves them dynamic because ffmpeg's use of them is optional. SQLCipher without OpenSSL is SQLite.

  • Point at them. patchelf --set-rpath '$ORIGIN' on the library and on every copy, so each finds its siblings in its own directory wherever the bundle is unpacked. This runs after the walk, not during it: a file given $ORIGIN half way through would resolve against a directory that does not hold the answer yet. There is no || true on it — a silently unpatched library falls back to /lib64 on the user's machine, which is invisible on the machine that built it.

The launcher still exports LD_LIBRARY_PATH for the bundle's SQLCipher directory, and still should: $ORIGIN covers the library's own dependencies, and LD_LIBRARY_PATH covers is native('sqlcipher') asking for a bare leaf name with no NEEDED entry pointing anywhere. They answer different questions and each is load-bearing.

This pass needs a Linux machine, and says so rather than pretending: it resolves sonames against this machine's libraries, with ldd and patchelf. Staging an ELF anywhere else — a Mac cross-building from --sqlcipher-archive — prints a warning naming what could not be done and carries on, because the archive may already be self-contained (one made by bundle-elf.sh is) and refusing would take away the only cross-build route there is. ariza having no patchelf on a Linux host is fatal, and names the package to install.

Making the staged library self-contained (Windows)

Windows hides the problem worst of all, because it does not record it anywhere. A vcpkg sqlcipher.dll imports libcrypto-3-x64.dll by bare name — no path, no rpath, no RUNPATH, nothing a file-reading audit had anything to say about. Staging the DLL on its own therefore produced a bundle whose SQLCipher could not load at all: not "loads the user's OpenSSL", which is the Unix failure, but LoadLibrary failing during import resolution, which surfaces as


DBDish::SQLCipher needs 'sqlcipher.dll', not found

— a message about the DLL that is there, produced by the DLL that is not. Two earlier fixes for that symptom (putting the bundle's native/sqlcipher on the smoke's PATH, and naming zef.raku rather than zef.bat) were both real bugs and neither could have made it go away, because there was no OpenSSL in the bundle for any PATH to point at.

So Windows gets the same walk-and-copy the other two get, spelled the way PE spells it:

  • Walk. pe-imports reads the import table out of the file: DOS header to PE\0\0, the optional header (PE32 or PE32+), data directory 1, and each descriptor's name RVA mapped back through the section table. No tool is involved, because there is no tool to involve — dumpbin ships with Visual Studio and objdump with neither — and a dependency walk that finds nothing when its helper is absent produces exactly the bundle this pass exists to prevent.

  • Copy in. Every import that is not on PE-SYSTEM-DLLS is copied in beside the library and recursed into. The skiplist is Windows' own DLLs, the API sets, and the Visual C++ redistributable; it is not guessed at, but measured — the core Win32 set plus exactly the names the 119 DLLs of the notcurses Windows pack import and do not carry, which xt/03-pe-imports.rakutest re-measures against the published pack. libcrypto is not on it, for the same reason it is not on the ELF one.

  • No binary rewrite. PE has no rpath to edit. Putting the DLLs in one directory establishes the closure, and the generated Windows launcher puts that directory on PATH before loading. Both halves are required: an absolute LoadLibrary of the top DLL does not make ordinary dependency search inspect its sibling directory.

The search space for the copy is one directory: the one the staged library itself came from. That is a contract with the source rather than a shortcut. Both Windows package managers install a port's whole runtime closure into a single bin — vcpkg's installed/<triplet>/bin, MSYS2's ucrt64/bin — so sqlcipher.dll's OpenSSL is the libcrypto-3-x64.dll sitting next to it, and an MSYS2 build's libgcc_s_seh-1.dll and libwinpthread-1.dll are there too. Searching wider (PATH, System32) would find a same-named DLL from a different build of a different version, which is the bug a bundle exists to avoid rather than a fallback worth having. An import that is not in that directory is fatal, and the death names the import, the directory, and the two things it could mean.

Unlike the ELF pass, this static closure work needs no host of its own. Reading a PE's imports needs no Windows, so a Windows bundle assembled from --sqlcipher-archive on a Mac gets the same walk, the same copies and the same closure audit as one built on Windows. It does not get the target-platform loader proof; ariza smoke supplies that separately.

THE AUDIT

audit walks every file ariza put into the bundle's native directories, plus :@extra (the macOS SQLCipher library, which lives among Rakudo's own files and so cannot be found by walking a directory), and fails the build if its native closure is not contained. Mach-O and ELF inspect loader resolution. PE inspects import-table closure and adjacency; the launcher is responsible for making that directory searchable at run time.

This is not a formality. It is the difference between a bundle that works on the machine that built it and one that works anywhere, and it is a regression guard: the notcurses pack is already clean, so what the audit really watches is the next library someone adds.

Mach-O

Every non-@loader_path/@rpath/@executable_path//usr/lib/ /System entry in otool -L is a finding. The LC_ID_DYLIB line is checked too, not just the dependencies: an absolute install name is copied into everything that links against the library later, which is the same bug one step removed.

ELF

Two static checks, on any host. readelf -d: a NEEDED soname containing a slash is an absolute path baked into the binary, and an RPATH/RUNPATH entry that is not $ORIGIN-relative points at the build machine.

Two host-only checks

Those two are necessary and nowhere near sufficient, and this is the exact shape of the bug they missed: a bare NEEDED libcrypto.so.3 with no RPATH at all passes both, and loads the build machine's OpenSSL. Nothing in the file says so. You have to ask the loader.

So when the audit runs on Linux it adds bundle-elf.sh's two:

  • patchelf --print-rpath — every entry must be $ORIGIN-relative, and a file with a non-system NEEDED must have at least one, or nothing tells the loader to look beside it. (A file whose only dependency is libc is allowed no rpath: there is nothing beside it to find. And $ORIGIN:$ORIGIN/../lib passes — it is relocatable, which is the property being checked, not a spelling.)

  • ldd with the environment replaced by a bare PATH (env -i, in one call): every non-system dependency must resolve to a path inside the bundle, and not found is a finding. The clean environment is the point — a plain ldd in a build that exported LD_LIBRARY_PATH paints a picture no user will ever see.

Both tools answer for the machine they are on, so from a Mac cross-inspecting an ELF the audit does the static half and says so rather than pretending to more. Which is another way of saying: the Linux bundle gets built on Linux, and xxt/linux-selfcontain-proof.sh runs the whole thing — staging, copy-in, rpath, audit, and three negative controls — in a manylinux container to prove it.

The two extra sections travel as text, under the ELF-RPATH-SECTION and ELF-LDD-SECTION markers, appended to the readelf output. That keeps :&inspect a seam that returns a report, and keeps elf-strays a pure function of text — so the Linux verdicts, including these two, are reachable from a test on a Mac.

PE

Every non-system DLL a staged .dll imports must be a file inside the bundle, and the audit resolves each one to find out.

This used to be a presence check — "the payload is there and is not empty" — on the reasoning that a PE records no rpath to inspect, so there was nothing to audit. That reasoning was wrong, and expensively: what a PE records is its import table, which is the entire question. A bundle holding sqlcipher.dll and nothing else passed the presence check with two files and no findings, and failed at LoadLibrary on the first Windows machine that ran it. An audit that cannot see the hole its own staging leaves is worse than no audit, because it is quoted in the release notes.

So the audit reads the same import table pe-imports reads for staging, drops the PE-SYSTEM-DLLS names, and matches what is left against the directory the file sits in — the closure ariza stages and whose exact path the launcher prepends to PATH. not found is a finding; so is a dependency that resolves outside :@inside (the bundle), by the same separator-normalised containment check the ELF audit uses. A file named like a DLL that is not a PE, or is empty, is a finding in those words, and a PE the parser cannot read stops the audit rather than passing through it as "checked".

It needs no Windows to do this static half: the file is read, not run. The closure verdict is as strong from a Mac as it is on the machine the bundle is for. It does not claim to validate the launcher's PATH or a live LoadLibrary; App::Ariza::Smoke's full-libnotcurses probe does that on the target platform.

The redistributable gate

One family of names is on the skiplist and audited anyway: vcruntime*.dll, msvcp*.dll, concrt*.dll and vcomp*.dll — PE-REDIST-DLLS, the Visual C++ Redistributable.

They look like Windows and they are not. kernel32.dll and ucrtbase.dll are in System32 on every Windows 10 machine ever installed; vcruntime140.dll arrives with Visual Studio, or with the redistributable installer, or dragged in by some other application that shipped it. A DLL importing one loads on the machine that built it, on every CI runner, and on most developer laptops — and fails on a clean install with the same wordless LoadLibrary refusal a missing OpenSSL gives. It is the exact shape of gap this audit exists to close, made worse by the fact that every machine likely to run the audit is a machine where the gap is invisible.

So an import from that family which is not present inside the bundle is a finding, and the finding carries the whole argument — consequence, and both ways out — rather than a bare path, because the reader has just watched the bundle work:


vcruntime140.dll => not found (Visual C++ Redistributable: not part of
Windows and not in the bundle, so this file loads only on a machine that
has installed it — which every CI runner has and a clean install does
not. Use a UCRT-built library instead: ...)

The fix ariza recommends is the source, not the copy: an MSYS2 mingw-w64-ucrt-x86_64-* library imports ucrtbase.dll and the question does not arise. Putting the runtime in the bundle by hand also satisfies the audit — the check is "not in the bundle", not "never imported" — but ariza will not copy it in on an author's behalf. It is Microsoft's to redistribute, on Microsoft's terms, and app-local deployment of it is a decision that belongs to whoever signs the release.

The UCRT itself is deliberately not in this family. ucrtbase.dll is part of the OS, which is the entire reason a UCRT build is the answer rather than another instance of the question.

The verdict functions are pure, and the tool call is a seam

macho-strays, elf-strays and pe-strays take report text and return the offending entries — no file, no tool, no platform.

audit itself takes :&inspect, which maps a file to its report text (or the undefined Str for a file of the wrong format). Between them, every branch of the audit — counting, skipping, the finding list, the wording of the failure — is reachable from a test on any machine, including the Linux and Windows verdicts from a Mac.

METHODS

stage-sqlcipher(:$bundle-dir!, :$slug!, :$versions!, :$archive, :$host-slug, :$host-kernel, :%env, :&run, :@search --> Hash)

Source it, place it, make it self-contained, alias it. Returns version (what was staged, possibly the undefined Str), pinned, library, rel, alias, staged (the library and everything copied in beside it), origin, source, kind and sha256.

:$host-kernel defaults to $*KERNEL.name.lc and decides whether the ELF self-containment pass can run at all — ldd and patchelf answer for the machine they are on. With :&run, it makes the Linux staging path reachable from a test on a Mac.

sqlcipher-source(:$slug!, :$archive, :$host-slug, :%env, :&run, :@search --> Hash)

The decision on its own, copying nothing: { kind, path, origin, version }. kind is 'library' or 'archive'.

:$host-slug is the machine's own slug (the cross-build guard), :%env the environment SQLCIPHER_LIB_DIR, MSYSTEM_PREFIX and VCPKG_ROOT are read from, :&run the process runner every brew and ldconfig call goes through, and :@search replaces the directories probed (on Linux and on Windows both). Together they make every platform's resolution — and every death message — reachable from a test on any machine.

audit(:$bundle-dir!, :$slug!, :@extra, :&inspect, :$host-kernel, :&run --> Hash)

{ checked, skipped, findings, family }, or a die naming every offending file and dependency. :&inspect replaces the tool call outright; :$host-kernel and :&run replace only the machine it is standing on, which is what the two host-only ELF checks are decided by.

sqlcipher-version-of(IO() --> Str)

The version a library reports about itself, read out of its bytes; the undefined Str when they do not say so unambiguously. Exported.

sqlcipher-dir / sqlcipher-rel / sqlcipher-layout / sqlcipher-slugs

The naming and placement rules, exposed individually so the manifest and the launcher template agree with what was actually staged.

macho-strays(Str --> List) / elf-strays(Str, :@inside --> List) / pe-strays(Str, :@inside --> List)

The pure verdict functions, exported. :@inside is the list of directory prefixes a dependency is allowed to resolve under — the bundle, spelled both as given and as resolved. With none, the containment half has nothing to judge against and only not found is reported. ELF and PE share the comparison, which normalises separators on both sides: a resolved target is spelled in the host's separators (backslashes, on Windows) and :@inside's prefixes may not be.

binary-format(IO() --> Str)

'Mach-O', 'ELF', 'PE' or the undefined Str, by magic number. Exported.

pe-imports(IO() --> List)

The DLLs a PE file imports, by name, in import-table order. Reads the bytes — DOS header, PE signature, COFF header, optional header (PE32 and PE32+ both), data directory 1, the import descriptors, and the section table that maps their RVAs back to file offsets — so it answers on any host, for any .dll or .exe, with no toolchain installed. Exported.

Anything that does not parse is a die naming the file and what was wrong with it: an empty answer for an unreadable file would pass both callers (staging and the audit) silently, which is the failure mode the whole pass exists to remove. Delay-load imports (data directory 13) are not read; the Pod above says why.

elf-system-lib(Str --> Bool) / pe-system-dll(Str --> Bool) / pe-redist-dll(Str --> Bool)

The skiplist membership tests: whether a soname or an imported DLL name is one the platform provides and a bundle must therefore not carry. pe-system-dll folds case, because the Windows loader does. All three take a name or a path and consider only the leaf. Exported.

pe-redist-dll is the narrower second question, asked of names that are already on the skiplist: whether this is the Visual C++ Redistributable, which is neither shipped by Windows nor copied in by ariza, and which the audit therefore reports when it does not resolve inside the bundle. "The redistributable gate" above says why.

elf-needed(Str --> List) / ldd-deps(Str --> List)

The DT_NEEDED sonames in readelf -d output, and the soname => path pairs in ldd output (the undefined Str for not found). Both pure, both exported.

SEE ALSO

App::Ariza::Site (which stages notcurses), App::Ariza::Launcher (which names the Linux and Windows library paths at run time), xxt/linux-selfcontain-proof.sh (which proves the Linux half of this module in a container, negative controls included), and xt/03-pe-imports.rakutest (which runs the PE parser and the skiplist over the 119 DLLs of the published notcurses Windows pack).

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.