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

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:

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:

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