karr-single-binary · git:20260927.09f2607 · 2026-09-27 · sha256 b3492be47fc6d761
karr-single-binary git:20260927.09f2607A
Immutable. This exact content never changes and is served at /api/v1/blob/b3492be47fc6d761.
---
name: karr-single-binary
description: Use when packaging the karr CLI into a single distributable binary, or benchmarking such a binary against the perl CLI — pp / PAR::Packer, staticperl, standalone/single-file build, "no perl on the target", cold-start pick/move/claim timing. Covers the libgit2 FFI-dlopen trap and the dynamically loaded App::karr::Cmd::* classes that a packer's scanner never sees.
---
# Packing karr into a single binary
Goal: one file you ship that runs `karr` on a box with no CPAN install. The Perl
side is easy. The hard part is that **libgit2 is opened by FFI at runtime**, not
linked at build time — so a naive pack builds fine and then dies on first git
access. Read the verdict, avoid the three traps, follow the runbook.
## Verdict: which packer
- **App::FatPacker — no.** It inlines only pure-Perl `.pm` files. karr pulls XS
(`YAML::XS`, a JSON XS backend, `FFI::Platypus`) and a C library (`libgit2`).
FatPacker cannot carry either. Dead on arrival — don't spend time here.
- **staticperl — no (for karr).** A true `--static` binary cannot reliably
`dlopen`, and libgit2 is loaded via FFI/`dlopen` at runtime → git dies. Without
`--static` you must ship the `.so`s anyway, so it is no longer one file. It
solves only the XS-perl part and adds nothing for the C lib. Timebox any
attempt and abort on the first `.so` error. Why, and the exact abort signals:
`references/staticperl-dead-end.md`.
- **PAR::Packer (`pp`) — yes.** It bundles the interpreter + XS `.so`s + the
Alien share dir into one executable that extracts to a runtime cache where FFI
finds libgit2. Full worked recipe, runtime-path mechanics, cache tuning, and
the ten failure modes: `references/pp-recipe.md`.
## Three traps that decide the night
### 1. The command classes load dynamically
`lib/App/karr.pm` `use`s only `App::karr::Cmd::Board`; `MooX::Cmd` resolves the
other 33 `App::karr::Cmd::*` from `@ARGV` at runtime, so `pp`'s static scanner
never sees them. Force the whole namespace with a glob:
```
-M 'App::karr::**' -M 'App::karr::Cmd::**'
```
`::**` = every module below the namespace at any depth (without the parent);
trailing `::` (e.g. `-M 'App::karr::Cmd::'`) includes the parent too. Quote the
glob so the shell does not expand it. A class left out only dies at runtime when
that subcommand is first called — hence the "exercise every subcommand" step.
### 2. libgit2 is opened by FFI, not linked
`Git::Libgit2::FFI` runs `FFI::Platypus->new(lib => [Alien::Libgit2->dynamic_libs])`
— a runtime `dlopen`. Two consequences:
- **Bundle the Alien share dir** or `dist_dir` croaks at startup. `pp` does not
carry File::ShareDir trees automatically:
`-a "$DISTDIR;lib/auto/share/dist/Alien-Libgit2"`. libgit2 itself needs no
`-l` — it resolves through `@INC`/File::ShareDir to the extracted copy.
- **Its transitive `.so`s** (this box: `libssl libcrypto libssh2 libz libzstd`
— always re-check with `ldd`) are resolved by the dynamic loader at `dlopen`.
On Linux, PAR's mid-process `LD_LIBRARY_PATH` patch is not reliably honored by
glibc, so do not count on `-l` for these. They are ordinary OS libraries
present on any normal Linux target — require them there, and bundle only for a
bare/musl container. The reliable options are in `references/pp-recipe.md`.
### 3. karr's own share/ is a File::ShareDir tree too
`App::karr::Role::SkillFile` finds the shipped skills with
`File::ShareDir::dist_dir('App-karr')`, falling back to `share/` next to the
`lib/` it was loaded from when the dist isn't installed — and inside a PAR
binary there is no checkout for that fallback to find. `pp` bundles no share
tree of its own, so pack karr's alongside Alien-Libgit2's:
```
-a "share;lib/auto/share/dist/App-karr"
```
Miss it and the binary builds clean and passes `--help`, including trap 1's
per-command `--help` sweep (nothing there reads the files) — then every `karr
skill` action and `karr init --claude-skill` die with `Could not find
<skill>/SKILL.md. Is App::karr properly installed?` (k308, shipped in 0.601).
`pp` only warns about a missing `-a` path and still exits 0, so it is
`scripts/verify-binary.sh`'s trap 3 — install the skills into a throwaway
project and byte-compare them against `share/` — that actually catches it.
## This machine — verified start state
- `Alien::Libgit2 install_type=share` **already** → build against the existing
`~/perl5`; do not reinstall karr's deps, so `~/perl5` stays untouched.
- `PAR::Packer` is **not** installed → install it first (a build tool, additive;
it is not a karr runtime dep).
- perl is `/usr/bin/perl` (5.40.x) with `PERL5LIB=~/perl5/lib/perl5` → run `pp`
in exactly this environment or it will miss deps.
## Build & verify runbook
1. **Isolate.** Work in a git worktree/branch of `~/dev/karr`. Never edit
`dist.ini` or the release machinery. Do not reinstall deps into `~/perl5`.
2. **Install** `PAR::Packer`.
3. **Build** with the recipe in `references/pp-recipe.md`.
4. **Verify in a fresh cache and clean cwd:**
`PAR_GLOBAL_TEMP="$(mktemp -d)" ./karr --help`, then run **every** subcommand
against a real board (trap 1). Ideally repeat in a minimal container with no
dev libraries to catch a transitive-`.so` break before deployment.
5. **Benchmark** against the perl CLI: run `scripts/benchmark.sh` (execute it) —
cold-start `--version`/`list` and mutating `create` ×N, binary vs perl CLI,
plus binary size and RSS.
6. **Report** the working tool, the exact command, binary size, the benchmark
table, and any target-machine requirement (which system `.so`s must exist).
## Done when
- `./karr` runs from a fresh `$PAR_GLOBAL_TEMP` with nothing on the target beyond
the documented system `.so`s, **and** every subcommand works against a real
board, **and** `scripts/benchmark.sh` produced numbers for binary vs perl CLI.
The headline number that answers the original question: cold-start `--version`
— is it under ~50 ms, and how does it compare to the perl CLI?