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):
- trace the boot script (compile it, without running it) to discover which modules it loads,
- drop core modules with a
Module::CoreListfilter, - resolve the surviving modules'
.packlistfiles, - copy their sources into a temporary
fatlib/, - minify each reachable
.pmfromlib/plus every.pmfromfatlib/withApp::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
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:
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:
# 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:
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:
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
myvariables 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 likemy $STATE_FILEormy $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/ishrinks toqr/\Q$c\E/i— while\Q...\Estill interpolates, braced${name}stays braced and delimiters pair/nest (s{...}{...}). A\$nameis a literal dollar and is untouched, single-quotedm'...'forms don't interpolate and are left alone, and names inside regex code regions ((?{ ... }),s///ereplacements) are preserved so the code keeps compiling. Escaped dollars ("\$name") and literal-only tokens are left untouched.local/ourdeclarations are left untouched (they may be package globals read via$PKG::name). Passrename_vars => 0to disable variable renaming via the API, or--no-rename-vars/--rename-varson the CLI (the legacy--rename/--no-renamespelling still works); pass--no-minifyto skip the whole pass. - renames sub and method names to short names too (on by default): definitions,
plain calls,
->methodchains,&name/\&namecode-refs and qualifiedPkg::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,tiemethods,TO_JSON,STORABLE_*), strings passed to->can()and friends, and hashed-deref keys / label andgototargets — 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. Passrename_subs => 0via 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 bundleing, --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::Zlibmodule,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 isrequired, andevals the boot program right after the loader. This is on by default; pass--no-compressto embed the minified sources and boot program verbatim instead (--compresscan always turn it back on).
- Embedded module sources are emitted with the exported
App::SlimPacker::pack_stringinstead of B::perlstring's always-double-quoted form.pack_stringnever perlstrings: it returns'single-quoted'literals with the content's backslashes escaped in place (\\), so$ @ % "stay raw — aq<delim>literal when that is cheaper (2 + quote-count vs 3 + delimiter occurrences,qpreferred on ties). The delimiter is the rarest occurrence in the text among^ ~ | ? , ; ! # & - + * / % :, escaping its occurrences;=and<>are excluded (they breakq=...=/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 <DATA> 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:
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:
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:
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
perl Makefile.PL
make test