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'slib/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 sqlcipherand the bottle out ofbrew --cache, which is the same binarybrew installwould have placed. No Homebrew at all is a die namingbrew install sqlcipherand--sqlcipher-archive.Linux —
ldconfig -p(tried as bothldconfigand/sbin/ldconfig, which is not on every user'sPATH), 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'ssqlcipherpackage renames the soname per version and ships nothing literally called that, so anylibsqlcipher*.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, withoriginnaming what was actually found. That rename is safe:is native('sqlcipher')dlopens the staged copy by leaf name throughLD_LIBRARY_PATH, which resolves by filename and never consults the file's ownDT_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 namingdnf install sqlcipher,apt install libsqlcipher0andapk add sqlcipher-libs, and--sqlcipher-archive.Windows — an MSYS2 environment's
bin(MSYSTEM_PREFIX, thenC:\msys64\ucrt64\binandC:\msys64\mingw64\bin) and a vcpkg tree underVCPKG_ROOT, in that order. Absent is a die namingpacman -S mingw-w64-ucrt-x86_64-sqlcipher, vcpkg,SQLCIPHER_LIB_DIRand--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 inLD_LIBRARY_PATH— reports where eachNEEDEDsoname resolves. Where there is noldd,readelf -dplusldconfig -pand 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.shlist — 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:libcryptoandlibsslare 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$ORIGINhalf way through would resolve against a directory that does not hold the answer yet. There is no|| trueon it — a silently unpatched library falls back to/lib64on 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-importsreads the import table out of the file: DOS header toPE\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 —dumpbinships with Visual Studio andobjdumpwith 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-DLLSis 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, whichxt/03-pe-imports.rakutestre-measures against the published pack.libcryptois 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
PATHbefore loading. Both halves are required: an absoluteLoadLibraryof 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-systemNEEDEDmust have at least one, or nothing tells the loader to look beside it. (A file whose only dependency islibcis allowed no rpath: there is nothing beside it to find. And$ORIGIN:$ORIGIN/../libpasses — it is relocatable, which is the property being checked, not a spelling.)lddwith the environment replaced by a barePATH(env -i, in one call): every non-system dependency must resolve to a path inside the bundle, andnot foundis a finding. The clean environment is the point — a plainlddin a build that exportedLD_LIBRARY_PATHpaints 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.