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
The default command runs the whole pipeline above; 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 module sources and the boot program
(default on; --no-compress embeds verbatim; see
"Smaller bundles")
--binary, --no-binary store the compressed sources as escaped binary
(default, smallest) or as base64 with --no-binary,
which keeps the bundle a plain text file
--shebang LINE shebang for the bundle (default: the boot script's own;
without one, #!/usr/bin/env perl)
--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 minified and printed, shebang preserved. -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, ...), ALL_CAPS lexicals included. Never renamed: Perl special variables (%ENV,@INC,%SIG,@ARGV,$VERSION,@ISA,$AUTOLOAD, ...), control variables ($^X,%^H,${^...}), single-character names, andlocal/ourdeclarations (they may be package globals read via$PKG::name). Interpolating strings and regexes are rewritten to match —"$name",_${name}, heredocs, readlines, backticks,m//,s///,qr//— with braced${name}kept braced, delimiters paired/nested and\Q...\Estill interpolating. Left alone: escaped dollars ("\$name"), non-interpolatingm'...', and regex code regions ((?{ ... }),s///ereplacements). Disable with--no-rename-vars(API:rename_vars => 0; the legacy--no-renamespelling still works);--no-minifyskips the whole pass. - renames sub and method names (on by default): definitions, plain calls,
->methodchains,&name/\&namecode-refs and qualifiedPkg::name(...)calls shrink in step. Names Perl dispatches by string stay verbatim: keywords and builtins, magic methods (new,DESTROY,AUTOLOAD,import,can,isa,tiemethods,TO_JSON,STORABLE_*), strings passed to->can()and friends, hash keys, label andgototargets. Methods reached only through$obj->can('method')plus a string stop matching after the rename; pack those with--no-rename-subs(API:rename_subs => 0).
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, renaming becomes bundle-aware — the whole dependency graph
is in one file, so one shared name => short map (from
collect_bundle_globals()) applies everywhere; single-file minify renames
each file independently and only the safe local forms. Bundle-wide:
--rewritealso renamesour-declared package globals consistently ($hitsand$App::Keeper::hitsbecome the same short name).$VERSION,$AUTOLOAD,@ISA, ALL_CAPS and single-character names are never renamed; a global is left alone if its name appears in a string or as a lexical anywhere in the bundle, and short names avoid every bundle lexical, string and reserved name.--rename-exports(on by default) shrinks the export/import protocol in step with thesubdefinitions:use Pkg qw(...),our @EXPORT_OK,can('method')et al. Safe only because the bundler has seen every module and its strings.--rename-modules(on by default) renames the bundled packages' namespace components:package App::Keeper::Configbecomespackage bk::bl::Config, withusestatements, qualified calls and interpolated globals following. The loader answers%INCrequests with the same map; a package never declared in the bundle is never renamed.
Smaller bundles
Three techniques keep slimpack bundle output compact:
-
Module sources and the boot program are deflate-compressed (best level, core
Compress::Raw::Zlib); the core-only loader inflates each module lazily when it isrequired, thenevals the boot program.--no-compressembeds the minified sources verbatim instead.The compressed bytes are stored as escaped binary: only CR, LF, ctrl-Z, backslash, the quote and the
\x01escape byte are escaped (~2% overhead), so the bundle runs everywhere, Windows text-mode reads included. The file itself is binary — move it byte-for-byte; don't re-save it from an editor or paste it.--no-binarystores base64 instead: ~25% bigger, but a plain text file that survives diff, mail and copy-paste. Neither form is readable; to inspect a bundle, use--no-compress.
- Verbatim (
--no-compress) sources are emitted with the exportedApp::SlimPacker::pack_string:'single-quoted'orq<delim>literals (whichever is cheaper) that leave$ @ % "raw, whereB::perlstringwould escape every$,"and\. Theqdelimiter is the rarest character in the text among^ ~ | ? , ! # & - + * / % :(;,=,<,>are excluded:q;...;is a syntax error before perl 5.18, and=>,>=,<<would break the literal). - The loader is a single-quoted heredoc template, so its own sigils are never
escaped; the module table is spliced in at a
__SLPACK_ENTRIES__placeholder.
A boot program's __DATA__/__END__ section can't survive string-eval, so it
is split off and bundled as its own compressed entry; the loader installs an
in-memory handle over it as *main::DATA before the boot program runs (an
already-open main::DATA is left alone, with a warning). Reads, seek,
tell and eof work as before; fileno(DATA) is -1, and tell counts from
the start of the data, not the file.
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-binary | slimpack --no-compress |
| --- | --- | --- | --- | --- |
| size | 110 KB | 25 KB | 32 KB | 55 KB |
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
minifies and compresses them (see "Smaller bundles"). --no-compress trades
size for readable sources. 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