# App::SlimPacker Fatpack-style bundler with PPI-based minification for standalone Perl scripts. `slimpack` turns a boot script plus its module tree into a single self-contained, minified `#!/usr/bin/perl` script. By default it runs the whole `fatpack` pipeline internally (via `App::FatPacker`'s methods): 1. **trace** the boot script (compile it, without running it) to discover which modules it loads, 2. **drop** core modules with a `Module::CoreList` filter, 3. **resolve** the surviving modules' `.packlist` files, 4. **copy** their sources into a temporary `fatlib/`, 5. **minify** each reachable `.pm` from `lib/` plus every `.pm` from `fatlib/` with `App::SlimPacker::Minify`, and emit the final script — shebang, module loader, minified modules, boot script. Each step is also available as its own subcommand (see Usage). It is project-agnostic: no search paths or class layouts are baked in. ## Layout | File | Purpose | | --- | --- | | `lib/App/SlimPacker.pm` | Bundling helpers (`pack_string`, `inline_plugins`, `perl_switches`) | | `lib/App/SlimPacker/Minify.pm` | The minifier (`process`, `minify_file`, `collect_bundle_globals`, ...) | | `bin/slimpack` | CLI bundler that assembles the final script | | `bin/minify` | Standalone minify-only front-end (same minification pass, no bundling) | | `t/minify.t` | Tests for the minifier (whitespace rules, variable renaming) | | `t/minify-module.t` | Tests for the `App::SlimPacker::Minify` API | | `t/rewrite.t` | Tests for the `--rewrite` pass and bundle-wide renaming | | `t/syntax.t` | Compiles every module and script | | `t/deps.t` | Tests for dependency finding (`module_deps`, plugin inlining, `perl_switches`) | | `t/bundle.t` | End-to-end tests for the `bundle` subcommand | | `t/minify-cli.t` | Tests for minify-only mode (`slimpack minify`, `--minify`, `bin/minify`) | | `t/cli.t` | Tests for subcommand dispatch (`pack`, `trace`, `packlists-for`, `tree`, `bundle`) | ## Installation ```sh perl Makefile.PL make make test make install # installs bin/slimpack and bin/minify to your perl's bin dir ``` Requires `PPI` and `App::FatPacker` (neither is a core module); `Module::CoreList` ships with perl. ## Usage By default `slimpack` runs the whole fatpack pipeline internally (calling `App::FatPacker`'s methods, no shelling out): trace the boot script, drop core modules via `Module::CoreList`, resolve their `.packlist` files, copy the sources into a temporary `fatlib/`, then minify and emit the standalone script. Each step is also exposed as a subcommand with the same arguments as `fatpack`: ```sh slimpack [OPTIONS] [COMMAND] [ARGS] # default 'pack' — full pipeline slimpack [OPTIONS] script slimpack [OPTIONS] -e/-E 'code' [-m/-M module ...] # individual steps (fatpack-compatible args) slimpack trace [--to=FILE|--to-stderr] [--use=MODULE] script slimpack packlists-for MODULE... slimpack tree [PACKLIST ...] slimpack bundle [OPTIONS] script # minify+bundle only (--lib/--fatlib) script boot script to bundle (ignored when -e/-E is given) -m module use module with no imports (like perl -m) -M module[=list] use module, optional import list or version (like perl -M) -e CODE code to bundle; may be repeated (like perl -e) -E CODE same, but enables all features (like perl -E) -o, --output FILE write the bundled script to FILE and chmod +x (default a.out; use '-' for stdout) --lib DIR project .pm sources (default lib) --fatlib DIR fatpacked module tree (default fatlib; pack uses a temp one) --no-minify bundle modules and boot program verbatim --rename-vars / --no-rename-vars rename my variables (default on) --rename-subs / --no-rename-subs rename sub/method names (default on) --rename-modules / --no-rename-modules rename bundled package names (default on) --rename-exports / --no-rename-exports also rename names in import/export lists and can() strings (default on) --rewrite, --no-rewrite additionally shorten keywords/operators (opt-in, experimental; see "The minifier" below) --compress, --no-compress deflate+base64 module sources and the boot program (default on; --no-compress embeds verbatim; see "Smaller bundles") --no-inline-plugins keep Module::Pluggable as a runtime dependency --bundle-lib-all include every .pm under --lib, even if not referenced from the boot program (default: only statically-reachable lib modules are bundled; fatlib is always fully included) --include MODULE also bundle MODULE and what it loads (repeatable; * and ? are wildcards) --exclude MODULE never bundle MODULE (repeatable; * and ? are wildcards) ``` Module discovery resolves the static dependency tree from `--lib` and fully includes what `pack` collected into `fatlib`, so modules referenced by `-M` or by `use base`/`use parent` are only bundled if their `.pm` files are in those trees; like perl, counterparts already present on the target system work either way. Pass `--bundle-lib-all` to include every `.pm` under `--lib` regardless. Modules loaded in ways slimpack can't see, such as a plugin picked at runtime, can be added with `--include`. Optional parts of a dependency the program never uses can be left out with `--exclude`. Moo's Moose support is left out without it: `Moo::HandleMoose` is only bundled when Moose is in the bundle anyway. Examples: ```sh # from this repo root, bundling a host project's boot script (full pipeline) perl -Ilib bin/slimpack -o myapp bin/boot # a one-liner program, pp/perl style slimpack -o myapp -M List::Util=sum -e 'print sum(1..100)' slimpack -o myapp -E 'say reverse qw(b a c)' slimpack -o myapp -M strict bin/myapp # -M also applies to scripts ``` ### Minify only `slimpack` can run just the minification pass, without tracing or bundling — useful for stamping comments/POD/whitespace out of existing scripts: ```sh slimpack minify script.pl # minified script to STDOUT slimpack --minify script.pl # same, as a flag slimpack minify -o out.pl script.pl # write to a file slimpack minify --no-rename-vars script.pl # keep variable names slimpack minify --no-rename-subs script.pl # keep sub/method names slimpack minify --rewrite script.pl # also shorten keywords/operators (opt-in) slimpack bundle --no-rename-modules boot.pl # keep package/class names as-is slimpack bundle --no-rename-exports boot.pl # keep qw() import/export names ``` A standalone `minify` script with the same options is installed alongside `slimpack`: ```sh minify [ -o out.pl ] [ --rename-vars / --no-rename-vars ] [ --rename-subs / --no-rename-subs ] [ --rewrite / --no-rewrite ] script.pl ... ``` Each file is read, minified and printed; the `#!` shebang line is preserved so the output stays runnable. `-o` needs exactly one input file; otherwise results go to STDOUT. ### Module::Pluggable inlining If the boot script contains `use Module::Pluggable(...)`, `slimpack` reads the `search_path` from its arguments and inlines the matching plugin classes into the `plugins()` call, so `Module::Pluggable` never needs to load at runtime. This is on by default; disable with `--no-inline-plugins`. Boot scripts without `Module::Pluggable` are unaffected. ## The minifier `App::SlimPacker::Minify::process` does PPI-based minification: * strips comments and POD, * collapses blank lines and intra-line whitespace, * renames `my` variables to short names (`a`, `b`, ... `aa`, ...) to shrink the source — skipping the Perl special variables (`%ENV`, `@INC`, `%SIG`, `@ARGV`, `$VERSION`, `@ISA`, `$AUTOLOAD`, ...), control-variable names (`$^X`, `%^H`, `${^...}`) and single-character names. Everything else is fair game, including ALL_CAPS lexicals like `my $STATE_FILE` or `my $ROWS`. Interpolating strings are *not* skipped: `"$name"`, `_${name}`, `<<`EOF`>>` heredocs, `<...>` readlines and backticks are rewritten to match, and so are regexes (`m//`, `s///`, `qr//`) — `my $query; qr/\Q$query\E/i` shrinks to `qr/\Q$c\E/i` — while `\Q...\E` still interpolates, braced `${name}` stays braced and delimiters pair/nest (`s{...}{...}`). A `\$name` is a literal dollar and is untouched, single-quoted `m'...'` forms don't interpolate and are left alone, and names inside regex *code regions* (`(?{ ... })`, `s///e` replacements) are preserved so the code keeps compiling. Escaped dollars (`"\$name"`) and literal-only tokens are left untouched. `local`/`our` declarations are left untouched (they may be package globals read via `$PKG::name`). Pass `rename_vars => 0` to disable variable renaming via the API, or `--no-rename-vars` / `--rename-vars` on the CLI (the legacy `--rename`/`--no-rename` spelling still works); pass `--no-minify` to skip the whole pass. * renames sub and method names to short names too (on by default): definitions, plain calls, `->method` chains, `&name`/`\&name` code-refs and qualified `Pkg::name(...)` calls all shrink in step. Names Perl dispatches or reads by name stay verbatim — keywords and builtins, magic methods (`new`, `DESTROY`, `AUTOLOAD`, `import`, `can`, `isa`, `tie` methods, `TO_JSON`, `STORABLE_*`), strings passed to `->can()` and friends, and hashed-deref keys / label and `goto` targets — so runtime string dispatch (e.g. `tie $h, __PACKAGE__`) still works. Methods only reached through `$obj->can('method')` plus a string stop matching after the rename; pack those with `--no-rename-subs`. Pass `rename_subs => 0` via the API. An additional pass (`rewrite => 1` in the API, `--rewrite` / `--no-rewrite` on the CLI, off by default) shortens code into provably equivalent forms: `foreach` -> `for`; `m/.../` -> `/.../` right after `=~`/`!~`; trailing `;` before `}`; `$x += 1;` -> `$x++;` (only mid-block, where the value is discarded); `$x = $x OP $y;` -> `$x OP= $y;` for `. + - * / %`; and parens around statement-terminal builtin calls (`print($x)` -> `print $x`). The pass is a single self-contained method with one call site, so it can be switched off or deleted wholesale if it ever misbehaves. When `bundle`ing, `--rewrite` also becomes bundle-aware: because the whole dependency graph ends up in one file, `our`-declared package globals are renamed too, consistently across every bundled module and the boot program (`$hits` and `$App::Keeper::hits` become the same short name everywhere). `$VERSION`, `$AUTOLOAD`, `@ISA`, ALL_CAPS and single-character names are never renamed, and a global is left alone if its name appears inside a string or as a `my`/`state` lexical anywhere in the bundle; short names are chosen to avoid every bundle lexical, string and reserved name, so an unqualified reference can never become ambiguous. Single-file `minify` has no bundle knowledge and still renames only the safe local forms. Sub/method renaming is bundle-aware the same way: `bundle` collects one shared `name => short` map over the whole dependency graph (via `collect_bundle_globals()`, third return value) so every module renames the same names to the same shorts; the `minify` mode renames each file independently. With `--rename-exports` (on by default) the export/import protocol is included too: the words in `use Pkg qw(...)`, `our @EXPORT_OK`, `can('method')` et al. shrink in step with their `sub` definitions, which is only safe because the bundler has seen every module and its strings in one `collect_bundle_globals()` call. `bundle` also renames the bundled packages' namespace components (`--rename-modules`, on by default): `package App::Keeper::Config` becomes `package bk::bl::Config`, and `use App::Keeper::Config`, qualified calls and interpolated globals like `"$App::Saisons::VERSION"` follow along. Only build-time-visible names are touched, and every package's leaf component is renamed the same way — the bundle is closed and its loader answers `%INC` requests with the same component map, so nothing resolves by its original path at runtime. A package that was never declared in the bundle is never renamed. ## Smaller bundles Three techniques keep `slimpack bundle` output compact: - Each bundled module source, and the boot program itself, is deflate-compressed (best level) with the core `Compress::Raw::Zlib` module, `MIME::Base64`-encoded, and embedded as a literal. The embedded loader (also core-only: `Compress::Raw::Zlib::Inflate` + `MIME::Base64::decode_base64`) decompresses a module lazily exactly when it is `require`d, and `eval`s the boot program right after the loader. This is on by default; pass `--no-compress` to embed the minified sources and boot program verbatim instead (`--compress` can always turn it back on). * Embedded module sources are emitted with the exported `App::SlimPacker::pack_string` instead of B::perlstring's always-double-quoted form. `pack_string` never perlstrings: it returns `'single-quoted'` literals with the content's backslashes escaped in place (`\\`), so `$ @ % "` stay raw — a `q` literal when that is cheaper (2 + quote-count vs 3 + delimiter occurrences, `q` preferred on ties). The delimiter is the rarest occurrence in the text among `^ ~ | ? , ; ! # & - + * / % :`, escaping its occurrences; `=` and `<` `>` are excluded (they break `q=...=`/`q<...>` when the text contains `>=`/`=>`/`<<` etc.). Even modules with `\\` sequences embed without any sigil escaping, whereas perlstring would have escaped every `$`, `"` and `\`. * The tracer/injector prologue is a single-quoted heredoc template: its own sigils are never escaped, and the module table is spliced in at a `__SLPACK_ENTRIES__` placeholder. The old string-built prologue had to be written with `\$` everywhere; the template saves tens of bytes. A boot program containing a `__DATA__`/`__END__` section cannot be string-eval'd (a data section survives only in real files), so `slimpack` keeps that boot program verbatim and warns that it was left uncompressed — modules are still compressed. This also means a bundle's `__DATA__` stays at a line start in the output so `` keeps reading the bundled data. ## Fatpack vs SlimPacker Both tools pack a program plus its module tree into one self-contained script. The example below was run in a scratch directory on perl 5.40.1 / Debian; both tools produced a working "Hello, world!" — only the sizes differ. Save this as `helloworld.pl`: ```perl package MyGreeter; use Moo; has name => (is => 'ro', default => sub { 'world' }); sub greet { my $self = shift; return "Hello, " . $self->name . "!\n"; } package main; print MyGreeter->new->greet; ``` Pack it with both tools: ```sh fatpack pack helloworld.pl > helloworld.fatpack.pl slimpack -o helloworld.slimpack.pl helloworld.pl ``` | | `fatpack pack` | `slimpack` | `slimpack --no-compress` | | --- | --- | --- | --- | | size | 110 KB | 32 KB | 55 KB | | lines | 3,726 | 4 | 225 | Both bundles inline the modules the program loads (the `Moo` tree: `Role::Tiny`, `Sub::Quote`, `Sub::Defer`, `Class::Method::Modifiers`, the `Method::Generate::*` builders) and run standalone — no `Moo`, no `PERL5LIB`: ```sh perl helloworld.fatpack.pl # Hello, world! perl helloworld.slimpack.pl # Hello, world! ``` `fatpack` copies every module verbatim, comments and POD included; `slimpack` sends them through the PPI minifier and collapses the output onto a handful of long lines. The `slimpack` column is the new default: the minified sources and the boot program are deflate-compressed (core `Compress::Raw::Zlib`) and decompressed lazily by the embedded loader — hence 4 lines and 32 KB. `--no-compress` trades that for readable sources at 55 KB. Measured on perl 5.40.1 with Moo 2.005005; exact sizes vary with the module set. ## Running the tests ```sh perl Makefile.PL make test ```