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