bump version
[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
ae852378 17our $VERSION = '0.010_006'; # 0.10.6
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) = @_;
145 my @targets = @$targets;
b4ff64e8 146 {
147 local @INC = ('lib', @INC);
148 foreach my $t (@targets) {
149 require $t;
150 }
a976705b 151 }
48af1939 152 my @search = grep -d $_, map catdir($_, 'auto'), @INC;
153 my %pack_rev;
0612cca1 154 find({
155 no_chdir => 1,
156 wanted => sub {
157 return unless /[\\\/]\.packlist$/ && -f $_;
158 $pack_rev{$_} = $File::Find::name for lines_of $File::Find::name;
159 },
48af1939 160 }, @search);
0612cca1 161 my %found; @found{map +($pack_rev{Cwd::abs_path($INC{$_})}||()), @targets} = ();
48af1939 162 sort keys %found;
163}
164
165sub script_command_tree {
166 my ($self, $args) = @_;
167 my $base = catdir(cwd,'fatlib');
168 $self->packlists_to_tree($base, $args);
169}
170
171sub packlists_to_tree {
172 my ($self, $where, $packlists) = @_;
4d5603b7 173 rmtree $where;
174 mkpath $where;
48af1939 175 foreach my $pl (@$packlists) {
176 my ($vol, $dirs, $file) = splitpath $pl;
177 my @dir_parts = splitdir $dirs;
178 my $pack_base;
179 PART: foreach my $p (0 .. $#dir_parts) {
180 if ($dir_parts[$p] eq 'auto') {
9fecf348 181 # $p-2 normally since it's <wanted path>/$Config{archname}/auto but
182 # if the last bit is a number it's $Config{archname}/$version/auto
183 # so use $p-3 in that case
184 my $version_lib = 0+!!($dir_parts[$p-1] =~ /^[0-9.]+$/);
185 $pack_base = catpath $vol, catdir @dir_parts[0..$p-(2+$version_lib)];
48af1939 186 last PART;
187 }
188 }
189 die "Couldn't figure out base path of packlist ${pl}" unless $pack_base;
190 foreach my $source (lines_of $pl) {
191 # there is presumably a better way to do "is this under this base?"
192 # but if so, it's not obvious to me in File::Spec
193 next unless substr($source,0,length $pack_base) eq $pack_base;
194 my $target = rel2abs( abs2rel($source, $pack_base), $where );
195 my $target_dir = catpath((splitpath $target)[0,1]);
4d5603b7 196 mkpath $target_dir;
48af1939 197 copy $source => $target;
198 }
199 }
200}
201
202sub script_command_file {
203 my ($self, $args) = @_;
204 my $file = shift @$args;
9be5f3c0 205 print $self->fatpack_file($file);
206}
207
208sub fatpack_file {
209 my ($self, $file) = @_;
30c64724 210
211 my $shebang = "";
212 my $script = "";
213 if ( defined $file and -r $file ) {
214 ($shebang, $script) = $self->load_main_script($file);
215 }
216
217 my @dirs = $self->collect_dirs();
48af1939 218 my %files;
30c64724 219 $self->collect_files($_, \%files) for @dirs;
220
221 return join "\n", $shebang, $self->fatpack_code(\%files), $script;
222}
223
224# This method can be overload in sub classes
225# For example to skip POD
226sub load_file {
227 my ($self, $file) = @_;
228 my $content = do {
229 local (@ARGV, $/) = ($file);
230 <>
231 };
232 close ARGV;
233 return $content;
234}
235
236sub collect_dirs {
237 my ($self) = @_;
238 my $cwd = cwd;
239 return grep -d, map rel2abs($_, $cwd), ('lib','fatlib');
240}
241
242sub collect_files {
243 my ($self, $dir, $files) = @_;
244 find(sub {
245 return unless -f $_;
246 !/\.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;
247 $files->{File::Spec::Unix->abs2rel($File::Find::name,$dir)} =
248 $self->load_file($File::Find::name);
249 }, $dir);
250}
251
252sub load_main_script {
253 my ($self, $file) = @_;
254 open my $fh, "<", $file or die("Can't read $file: $!");
255 my $shebang = <$fh>;
256 my $script = join "", <$fh>;
257 close $fh;
258 unless ( index($shebang, '#!') == 0 ) {
259 $script = $shebang . $script;
260 $shebang = "";
48af1939 261 }
30c64724 262 return ($shebang, $script);
263}
264
265sub fatpack_start {
266 return stripspace <<' END_START';
48af1939 267 # This chunk of stuff was generated by App::FatPacker. To find the original
268 # file's code, look for the end of this BEGIN block or the string 'FATPACK'
269 BEGIN {
270 my %fatpacked;
271 END_START
30c64724 272}
273
274sub fatpack_end {
275 return stripspace <<' END_END';
48af1939 276 s/^ //mg for values %fatpacked;
277
f147c6f0 278 my $class = 'FatPacked::'.(0+\%fatpacked);
279 no strict 'refs';
33020868 280 *{"${class}::files"} = sub { keys %{$_[0]} };
e7051d24 281
282 if ($] < 5.008) {
33020868 283 *{"${class}::INC"} = sub {
11416666 284 if (my $fat = $_[0]{$_[1]}) {
285 my $pos = 0;
286 my $last = length $fat;
287 return (sub {
288 return 0 if $pos == $last;
289 my $next = (1 + index $fat, "\n", $pos) || $last;
290 $_ .= substr $fat, $pos, $next - $pos;
291 $pos = $next;
292 return 1;
293 });
294 }
15bd679e 295 };
e7051d24 296 }
297
298 else {
e7051d24 299 *{"${class}::INC"} = sub {
15bd679e 300 if (my $fat = $_[0]{$_[1]}) {
e7051d24 301 open my $fh, '<', \$fat
302 or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
303 return $fh;
304 }
305 return;
306 };
e7051d24 307 }
48af1939 308
33020868 309 unshift @INC, bless \%fatpacked, $class;
e7051d24 310 } # END OF FATPACK CODE
48af1939 311 END_END
30c64724 312}
313
314sub fatpack_code {
315 my ($self, $files) = @_;
48af1939 316 my @segments = map {
317 (my $stub = $_) =~ s/\.pm$//;
318 my $name = uc join '_', split '/', $stub;
30c64724 319 my $data = $files->{$_}; $data =~ s/^/ /mg; $data =~ s/(?<!\n)\z/\n/;
56a51caa 320 '$fatpacked{'.perlstring($_).qq!} = '#line '.(1+__LINE__).' "'.__FILE__."\\"\\n".<<'${name}';\n!
48af1939 321 .qq!${data}${name}\n!;
30c64724 322 } sort keys %$files;
323
324 return join "\n", $self->fatpack_start, @segments, $self->fatpack_end;
48af1939 325}
f5a54fa1 326
bb49414d 327=encoding UTF-8
328
f5a54fa1 329=head1 NAME
330
331App::FatPacker - pack your dependencies onto your script file
332
333=head1 SYNOPSIS
334
6da38a2d 335 $ fatpack pack myscript.pl >myscript.packed.pl
336
337Or, with more step-by-step control:
338
f5a54fa1 339 $ fatpack trace myscript.pl
62ceea28 340 $ fatpack packlists-for `cat fatpacker.trace` >packlists
341 $ fatpack tree `cat packlists`
7dabafaa 342 $ fatpack file myscript.pl >myscript.packed.pl
cb50b68f 343
f5a54fa1 344See the documentation for the L<fatpack> script itself for more information.
345
3326f144 346The programmatic API for this code is not yet fully decided, hence the 0.x
f5a54fa1 347release version. Expect that to be cleaned up for 1.0.
348
66a19d01 349=head1 SEE ALSO
350
351L<article for Perl Advent 2012|http://www.perladvent.org/2012/2012-12-14.html>
352
f5a54fa1 353=head1 SUPPORT
354
23b0c283 355Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=App-FatPacker>
356(or L<bug-App-FatPacker@rt.cpan.org|mailto:bug-App-FatPacker@rt.cpan.org>).
357
358You can normally also obtain assistance on irc, in #toolchain on irc.perl.org.
f5a54fa1 359
360=head1 AUTHOR
361
362Matt S. Trout (mst) <mst@shadowcat.co.uk>
363
364=head2 CONTRIBUTORS
365
99b15200 366miyagawa - Tatsuhiko Miyagawa (cpan:MIYAGAWA) <miyagawa@bulknews.net>
367
368tokuhirom - MATSUNO★Tokuhiro (cpan:TOKUHIROM) <tokuhirom@gmail.com>
369
370dg - David Leadbeater (cpan:DGL) <dgl@dgl.cx>
371
372gugod - 劉康民 (cpan:GUGOD) <gugod@cpan.org>
373
374t0m - Tomas Doran (cpan:BOBTFISH) <bobtfish@bobtfish.net>
375
376sawyer - Sawyer X (cpan:XSAWYERX) <xsawyerx@cpan.org>
377
378ether - Karen Etheridge (cpan:ETHER) <ether@cpan.org>
379
cbd99f49 380Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
381
56a51caa 382dolmen - Olivier Mengué (cpan:DOLMEN) <dolmen@cpan.org>
383
ab7608ee 384djerius - Diab Jerius (cpan:DJERIUS) <djerius@cpan.org>
385
3c29415b 386haarg - Graham Knop (cpan:HAARG> <haarg@haarg.org>
387
99b15200 388Many more people are probably owed thanks for ideas. Yet
f5a54fa1 389another doc nit to fix.
390
391=head1 COPYRIGHT
392
393Copyright (c) 2010 the App::FatPacker L</AUTHOR> and L</CONTRIBUTORS>
394as listed above.
395
396=head1 LICENSE
397
398This library is free software and may be distributed under the same terms
399as perl itself.
400
401=cut
402
48af1939 4031;
24d68aa2 404