Release commit for 0.010008
[p5sagit/App-FatPacker.git] / lib / App / FatPacker.pm
CommitLineData
48af1939 1package App::FatPacker;
2
3use strict;
4use warnings FATAL => 'all';
8572221e 5use 5.008001;
48af1939 6use Getopt::Long;
7use Cwd qw(cwd);
8use File::Find qw(find);
9use File::Spec::Functions qw(
10 catdir splitpath splitdir catpath rel2abs abs2rel
11);
cbd99f49 12use File::Spec::Unix;
48af1939 13use File::Copy qw(copy);
4d5603b7 14use File::Path qw(mkpath rmtree);
48af1939 15use B qw(perlstring);
16
5f837726 17our $VERSION = '0.010008'; # v0.10.8
f5a54fa1 18
19$VERSION = eval $VERSION;
20
48af1939 21sub call_parser {
24d68aa2 22 my $self = shift;
69667cc8 23 my ($args, $options) = @_;
24d68aa2 24
25 local *ARGV = [ @{$args} ];
69667cc8 26 $self->{option_parser}->getoptions(@$options);
24d68aa2 27
28 return [ @ARGV ];
48af1939 29}
30
31sub lines_of {
32 map +(chomp,$_)[1], do { local @ARGV = ($_[0]); <> };
33}
34
35sub stripspace {
36 my ($text) = @_;
37 $text =~ /^(\s+)/ && $text =~ s/^$1//mg;
38 $text;
39}
40
48af1939 41sub import {
24d68aa2 42 $_[1] && $_[1] eq '-run_script'
48af1939 43 and return shift->new->run_script;
44}
45
24d68aa2 46sub new {
47 bless {
48 option_parser => Getopt::Long::Parser->new(
49 config => [ qw(require_order pass_through bundling no_auto_abbrev) ]
50 ),
51 }, $_[0];
52}
48af1939 53
54sub run_script {
55 my ($self, $args) = @_;
56 my @args = $args ? @$args : @ARGV;
3e4dadce 57 (my $cmd = shift @args || 'help') =~ s/-/_/g;
24d68aa2 58
48af1939 59 if (my $meth = $self->can("script_command_${cmd}")) {
60 $self->$meth(\@args);
61 } else {
62 die "No such command ${cmd}";
63 }
64}
65
3e4dadce 66sub script_command_help {
67 print "Try `perldoc fatpack` for how to use me\n";
68}
69
cb3e6884 70sub script_command_pack {
71 my ($self, $args) = @_;
72
73 my @modules = split /\r?\n/, $self->trace(args => $args);
74 my @packlists = $self->packlists_containing(\@modules);
75
76 my $base = catdir(cwd, 'fatlib');
77 $self->packlists_to_tree($base, \@packlists);
78
79 my $file = shift @$args;
7dabafaa 80 print $self->fatpack_file($file);
cb3e6884 81}
82
48af1939 83sub script_command_trace {
84 my ($self, $args) = @_;
24d68aa2 85
69667cc8 86 $args = $self->call_parser($args => [
48af1939 87 'to=s' => \my $file,
88 'to-stderr' => \my $to_stderr,
3fdf85ca 89 'use=s' => \my @additional_use
69667cc8 90 ]);
48af1939 91
92 die "Can't use to and to-stderr on same call" if $file && $to_stderr;
93
3fdf85ca 94 $file ||= 'fatpacker.trace';
69667cc8 95
3fdf85ca 96 if (!$to_stderr and -e $file) {
97 unlink $file or die "Couldn't remove old trace file: $!";
48af1939 98 }
99 my $arg = do {
3fdf85ca 100 if ($to_stderr) {
020d9b76 101 ">&STDERR"
3fdf85ca 102 } elsif ($file) {
276a30c9 103 ">>${file}"
48af1939 104 }
105 };
3fdf85ca 106
b4704b1a 107 $self->trace(
5e1de95d 108 use => \@additional_use,
109 args => $args,
7a3662c8 110 output => $arg,
b4704b1a 111 );
abd7cf01 112}
113
114sub trace {
b4704b1a 115 my ($self, %opts) = @_;
69667cc8 116
1ea23474 117 my $output = $opts{output};
69667cc8 118 my $trace_opts = join ',', $output||'>&STDOUT', @{$opts{use}||[]};
7a3662c8 119
a73e80c4 120 local $ENV{PERL5OPT} = join ' ',
121 ($ENV{PERL5OPT}||()), '-MApp::FatPacker::Trace='.$trace_opts;
abd7cf01 122
69667cc8 123 my @args = @{$opts{args}||[]};
e0cd89ab 124
69667cc8 125 if ($output) {
126 # user specified output target, JFDI
127 system $^X, @args;
128 return;
e0cd89ab 129 } else {
69667cc8 130 # no output target specified, slurp
9317190f 131 open my $out_fh, "$^X @args |";
69667cc8 132 return do { local $/; <$out_fh> };
133 }
48af1939 134}
135
136sub script_command_packlists_for {
137 my ($self, $args) = @_;
138 foreach my $pl ($self->packlists_containing($args)) {
139 print "${pl}\n";
140 }
141}
142
143sub packlists_containing {
144 my ($self, $targets) = @_;
de0c2ef7 145 my @targets;
b4ff64e8 146 {
147 local @INC = ('lib', @INC);
de0c2ef7 148 foreach my $t (@$targets) {
149 unless (eval { require $t; 1}) {
150 warn "Failed to load ${t}: $@\n"
151 ."Make sure you're not missing a packlist as a result\n";
152 next;
153 }
154 push @targets, $t;
b4ff64e8 155 }
a976705b 156 }
48af1939 157 my @search = grep -d $_, map catdir($_, 'auto'), @INC;
158 my %pack_rev;
0612cca1 159 find({
160 no_chdir => 1,
161 wanted => sub {
162 return unless /[\\\/]\.packlist$/ && -f $_;
163 $pack_rev{$_} = $File::Find::name for lines_of $File::Find::name;
164 },
48af1939 165 }, @search);
0612cca1 166 my %found; @found{map +($pack_rev{Cwd::abs_path($INC{$_})}||()), @targets} = ();
48af1939 167 sort keys %found;
168}
169
170sub script_command_tree {
171 my ($self, $args) = @_;
172 my $base = catdir(cwd,'fatlib');
173 $self->packlists_to_tree($base, $args);
174}
175
176sub packlists_to_tree {
177 my ($self, $where, $packlists) = @_;
4d5603b7 178 rmtree $where;
179 mkpath $where;
48af1939 180 foreach my $pl (@$packlists) {
181 my ($vol, $dirs, $file) = splitpath $pl;
182 my @dir_parts = splitdir $dirs;
183 my $pack_base;
184 PART: foreach my $p (0 .. $#dir_parts) {
185 if ($dir_parts[$p] eq 'auto') {
9fecf348 186 # $p-2 normally since it's <wanted path>/$Config{archname}/auto but
187 # if the last bit is a number it's $Config{archname}/$version/auto
188 # so use $p-3 in that case
189 my $version_lib = 0+!!($dir_parts[$p-1] =~ /^[0-9.]+$/);
190 $pack_base = catpath $vol, catdir @dir_parts[0..$p-(2+$version_lib)];
48af1939 191 last PART;
192 }
193 }
194 die "Couldn't figure out base path of packlist ${pl}" unless $pack_base;
195 foreach my $source (lines_of $pl) {
196 # there is presumably a better way to do "is this under this base?"
197 # but if so, it's not obvious to me in File::Spec
198 next unless substr($source,0,length $pack_base) eq $pack_base;
199 my $target = rel2abs( abs2rel($source, $pack_base), $where );
200 my $target_dir = catpath((splitpath $target)[0,1]);
4d5603b7 201 mkpath $target_dir;
48af1939 202 copy $source => $target;
203 }
204 }
205}
206
207sub script_command_file {
208 my ($self, $args) = @_;
209 my $file = shift @$args;
9be5f3c0 210 print $self->fatpack_file($file);
211}
212
213sub fatpack_file {
214 my ($self, $file) = @_;
30c64724 215
216 my $shebang = "";
217 my $script = "";
218 if ( defined $file and -r $file ) {
219 ($shebang, $script) = $self->load_main_script($file);
220 }
221
222 my @dirs = $self->collect_dirs();
48af1939 223 my %files;
30c64724 224 $self->collect_files($_, \%files) for @dirs;
225
226 return join "\n", $shebang, $self->fatpack_code(\%files), $script;
227}
228
229# This method can be overload in sub classes
230# For example to skip POD
231sub load_file {
232 my ($self, $file) = @_;
233 my $content = do {
234 local (@ARGV, $/) = ($file);
235 <>
236 };
237 close ARGV;
238 return $content;
239}
240
241sub collect_dirs {
242 my ($self) = @_;
243 my $cwd = cwd;
244 return grep -d, map rel2abs($_, $cwd), ('lib','fatlib');
245}
246
247sub collect_files {
248 my ($self, $dir, $files) = @_;
249 find(sub {
250 return unless -f $_;
251 !/\.pm$/ and warn "File ${File::Find::name} isn't a .pm file - can't pack this -- if you hoped we were going to, things may not be what you expected later\n" and return;
252 $files->{File::Spec::Unix->abs2rel($File::Find::name,$dir)} =
253 $self->load_file($File::Find::name);
254 }, $dir);
255}
256
257sub load_main_script {
258 my ($self, $file) = @_;
259 open my $fh, "<", $file or die("Can't read $file: $!");
260 my $shebang = <$fh>;
261 my $script = join "", <$fh>;
262 close $fh;
263 unless ( index($shebang, '#!') == 0 ) {
264 $script = $shebang . $script;
265 $shebang = "";
48af1939 266 }
30c64724 267 return ($shebang, $script);
268}
269
270sub fatpack_start {
271 return stripspace <<' END_START';
48af1939 272 # This chunk of stuff was generated by App::FatPacker. To find the original
273 # file's code, look for the end of this BEGIN block or the string 'FATPACK'
274 BEGIN {
275 my %fatpacked;
276 END_START
30c64724 277}
278
279sub fatpack_end {
280 return stripspace <<' END_END';
48af1939 281 s/^ //mg for values %fatpacked;
282
f147c6f0 283 my $class = 'FatPacked::'.(0+\%fatpacked);
284 no strict 'refs';
33020868 285 *{"${class}::files"} = sub { keys %{$_[0]} };
e7051d24 286
287 if ($] < 5.008) {
33020868 288 *{"${class}::INC"} = sub {
11416666 289 if (my $fat = $_[0]{$_[1]}) {
290 my $pos = 0;
291 my $last = length $fat;
292 return (sub {
293 return 0 if $pos == $last;
294 my $next = (1 + index $fat, "\n", $pos) || $last;
295 $_ .= substr $fat, $pos, $next - $pos;
296 $pos = $next;
297 return 1;
298 });
299 }
15bd679e 300 };
e7051d24 301 }
302
303 else {
e7051d24 304 *{"${class}::INC"} = sub {
15bd679e 305 if (my $fat = $_[0]{$_[1]}) {
e7051d24 306 open my $fh, '<', \$fat
307 or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
308 return $fh;
309 }
310 return;
311 };
e7051d24 312 }
48af1939 313
33020868 314 unshift @INC, bless \%fatpacked, $class;
e7051d24 315 } # END OF FATPACK CODE
48af1939 316 END_END
30c64724 317}
318
319sub fatpack_code {
320 my ($self, $files) = @_;
48af1939 321 my @segments = map {
322 (my $stub = $_) =~ s/\.pm$//;
323 my $name = uc join '_', split '/', $stub;
30c64724 324 my $data = $files->{$_}; $data =~ s/^/ /mg; $data =~ s/(?<!\n)\z/\n/;
56a51caa 325 '$fatpacked{'.perlstring($_).qq!} = '#line '.(1+__LINE__).' "'.__FILE__."\\"\\n".<<'${name}';\n!
48af1939 326 .qq!${data}${name}\n!;
30c64724 327 } sort keys %$files;
328
329 return join "\n", $self->fatpack_start, @segments, $self->fatpack_end;
48af1939 330}
f5a54fa1 331
bb49414d 332=encoding UTF-8
333
f5a54fa1 334=head1 NAME
335
336App::FatPacker - pack your dependencies onto your script file
337
338=head1 SYNOPSIS
339
6da38a2d 340 $ fatpack pack myscript.pl >myscript.packed.pl
341
342Or, with more step-by-step control:
343
f5a54fa1 344 $ fatpack trace myscript.pl
62ceea28 345 $ fatpack packlists-for `cat fatpacker.trace` >packlists
346 $ fatpack tree `cat packlists`
7dabafaa 347 $ fatpack file myscript.pl >myscript.packed.pl
cb50b68f 348
f21949d5 349Each command is designed to be simple and self-contained so that you can modify
350the input/output of each step as needed. See the documentation for the
351L<fatpack> script itself for more information.
f5a54fa1 352
3326f144 353The programmatic API for this code is not yet fully decided, hence the 0.x
f5a54fa1 354release version. Expect that to be cleaned up for 1.0.
355
f21949d5 356=head1 CAVEATS
357
358As dependency module code is copied into the resulting file as text, only
359pure-perl dependencies can be packed, not compiled XS code.
360
361The currently-installed dependencies to pack are found via F<.packlist> files,
362which are generally only included in non-core distributions that were installed
363by a CPAN installer. This is a feature; see L<fatpack/packlists-for> for
364details. (a notable exception to this is FreeBSD, which, since its packaging
365system is designed to work equivalently to a source install, does preserve
366the packlist files)
367
66a19d01 368=head1 SEE ALSO
369
370L<article for Perl Advent 2012|http://www.perladvent.org/2012/2012-12-14.html>
371
f21949d5 372L<pp> - PAR Packager, a much more complex architecture-dependent packer that
373can pack compiled code and even a Perl interpreter
374
f5a54fa1 375=head1 SUPPORT
376
23b0c283 377Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=App-FatPacker>
378(or L<bug-App-FatPacker@rt.cpan.org|mailto:bug-App-FatPacker@rt.cpan.org>).
379
380You can normally also obtain assistance on irc, in #toolchain on irc.perl.org.
f5a54fa1 381
382=head1 AUTHOR
383
384Matt S. Trout (mst) <mst@shadowcat.co.uk>
385
386=head2 CONTRIBUTORS
387
99b15200 388miyagawa - Tatsuhiko Miyagawa (cpan:MIYAGAWA) <miyagawa@bulknews.net>
389
390tokuhirom - MATSUNO★Tokuhiro (cpan:TOKUHIROM) <tokuhirom@gmail.com>
391
392dg - David Leadbeater (cpan:DGL) <dgl@dgl.cx>
393
394gugod - 劉康民 (cpan:GUGOD) <gugod@cpan.org>
395
396t0m - Tomas Doran (cpan:BOBTFISH) <bobtfish@bobtfish.net>
397
398sawyer - Sawyer X (cpan:XSAWYERX) <xsawyerx@cpan.org>
399
400ether - Karen Etheridge (cpan:ETHER) <ether@cpan.org>
401
cbd99f49 402Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
403
56a51caa 404dolmen - Olivier Mengué (cpan:DOLMEN) <dolmen@cpan.org>
405
ab7608ee 406djerius - Diab Jerius (cpan:DJERIUS) <djerius@cpan.org>
407
64941bce 408haarg - Graham Knop (cpan:HAARG) <haarg@haarg.org>
409
410grinnz - Dan Book (cpan:DBOOK) <dbook@cpan.org>
3c29415b 411
99b15200 412Many more people are probably owed thanks for ideas. Yet
f5a54fa1 413another doc nit to fix.
414
415=head1 COPYRIGHT
416
417Copyright (c) 2010 the App::FatPacker L</AUTHOR> and L</CONTRIBUTORS>
418as listed above.
419
420=head1 LICENSE
421
422This library is free software and may be distributed under the same terms
423as perl itself.
424
425=cut
426
48af1939 4271;
24d68aa2 428