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

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:

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:

Smaller bundles

Three techniques keep slimpack bundle output compact:

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