add option to reset @INC to defaults at top of fatpacked script
[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
4373b37e 17our $VERSION = '0.010001'; # 0.10.1
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
a03c42fa 73 $args = $self->call_parser($args => [
74 'core-only' => \my $core_only,
75 ]);
76
cb3e6884 77 my @modules = split /\r?\n/, $self->trace(args => $args);
78 my @packlists = $self->packlists_containing(\@modules);
79
80 my $base = catdir(cwd, 'fatlib');
81 $self->packlists_to_tree($base, \@packlists);
82
83 my $file = shift @$args;
a03c42fa 84 print $self->fatpack_file($file, $core_only);
cb3e6884 85}
86
48af1939 87sub script_command_trace {
88 my ($self, $args) = @_;
24d68aa2 89
69667cc8 90 $args = $self->call_parser($args => [
48af1939 91 'to=s' => \my $file,
92 'to-stderr' => \my $to_stderr,
3fdf85ca 93 'use=s' => \my @additional_use
69667cc8 94 ]);
48af1939 95
96 die "Can't use to and to-stderr on same call" if $file && $to_stderr;
97
3fdf85ca 98 $file ||= 'fatpacker.trace';
69667cc8 99
3fdf85ca 100 if (!$to_stderr and -e $file) {
101 unlink $file or die "Couldn't remove old trace file: $!";
48af1939 102 }
103 my $arg = do {
3fdf85ca 104 if ($to_stderr) {
020d9b76 105 ">&STDERR"
3fdf85ca 106 } elsif ($file) {
276a30c9 107 ">>${file}"
48af1939 108 }
109 };
3fdf85ca 110
b4704b1a 111 $self->trace(
5e1de95d 112 use => \@additional_use,
113 args => $args,
7a3662c8 114 output => $arg,
b4704b1a 115 );
abd7cf01 116}
117
118sub trace {
b4704b1a 119 my ($self, %opts) = @_;
69667cc8 120
1ea23474 121 my $output = $opts{output};
69667cc8 122 my $trace_opts = join ',', $output||'>&STDOUT', @{$opts{use}||[]};
7a3662c8 123
a73e80c4 124 local $ENV{PERL5OPT} = join ' ',
125 ($ENV{PERL5OPT}||()), '-MApp::FatPacker::Trace='.$trace_opts;
abd7cf01 126
69667cc8 127 my @args = @{$opts{args}||[]};
e0cd89ab 128
69667cc8 129 if ($output) {
130 # user specified output target, JFDI
131 system $^X, @args;
132 return;
e0cd89ab 133 } else {
69667cc8 134 # no output target specified, slurp
9317190f 135 open my $out_fh, "$^X @args |";
69667cc8 136 return do { local $/; <$out_fh> };
137 }
48af1939 138}
139
140sub script_command_packlists_for {
141 my ($self, $args) = @_;
142 foreach my $pl ($self->packlists_containing($args)) {
143 print "${pl}\n";
144 }
145}
146
147sub packlists_containing {
148 my ($self, $targets) = @_;
149 my @targets = @$targets;
b4ff64e8 150 {
151 local @INC = ('lib', @INC);
152 foreach my $t (@targets) {
153 require $t;
154 }
a976705b 155 }
48af1939 156 my @search = grep -d $_, map catdir($_, 'auto'), @INC;
157 my %pack_rev;
0612cca1 158 find({
159 no_chdir => 1,
160 wanted => sub {
161 return unless /[\\\/]\.packlist$/ && -f $_;
162 $pack_rev{$_} = $File::Find::name for lines_of $File::Find::name;
163 },
48af1939 164 }, @search);
0612cca1 165 my %found; @found{map +($pack_rev{Cwd::abs_path($INC{$_})}||()), @targets} = ();
48af1939 166 sort keys %found;
167}
168
169sub script_command_tree {
170 my ($self, $args) = @_;
171 my $base = catdir(cwd,'fatlib');
172 $self->packlists_to_tree($base, $args);
173}
174
175sub packlists_to_tree {
176 my ($self, $where, $packlists) = @_;
4d5603b7 177 rmtree $where;
178 mkpath $where;
48af1939 179 foreach my $pl (@$packlists) {
180 my ($vol, $dirs, $file) = splitpath $pl;
181 my @dir_parts = splitdir $dirs;
182 my $pack_base;
183 PART: foreach my $p (0 .. $#dir_parts) {
184 if ($dir_parts[$p] eq 'auto') {
185 # $p-2 since it's <wanted path>/$Config{archname}/auto
186 $pack_base = catpath $vol, catdir @dir_parts[0..$p-2];
187 last PART;
188 }
189 }
190 die "Couldn't figure out base path of packlist ${pl}" unless $pack_base;
191 foreach my $source (lines_of $pl) {
192 # there is presumably a better way to do "is this under this base?"
193 # but if so, it's not obvious to me in File::Spec
194 next unless substr($source,0,length $pack_base) eq $pack_base;
195 my $target = rel2abs( abs2rel($source, $pack_base), $where );
196 my $target_dir = catpath((splitpath $target)[0,1]);
4d5603b7 197 mkpath $target_dir;
48af1939 198 copy $source => $target;
199 }
200 }
201}
202
203sub script_command_file {
204 my ($self, $args) = @_;
a03c42fa 205
206 $args = $self->call_parser($args => [
207 'core-only' => \my $core_only,
208 ]);
209
48af1939 210 my $file = shift @$args;
a03c42fa 211 print $self->fatpack_file($file, $core_only);
9be5f3c0 212}
213
214sub fatpack_file {
a03c42fa 215 my ($self, $file, $core_only) = @_;
30c64724 216
217 my $shebang = "";
218 my $script = "";
219 if ( defined $file and -r $file ) {
220 ($shebang, $script) = $self->load_main_script($file);
221 }
222
223 my @dirs = $self->collect_dirs();
48af1939 224 my %files;
30c64724 225 $self->collect_files($_, \%files) for @dirs;
226
a03c42fa 227 my $lib_reset =
228 "BEGIN { use Config; \@INC = \@Config{qw(privlibexp archlibexp sitelibexp sitearchexp)} }\n";
229
230 return join "\n", $shebang, ($core_only ? $lib_reset : ()), $self->fatpack_code(\%files), $script;
30c64724 231}
232
233# This method can be overload in sub classes
234# For example to skip POD
235sub load_file {
236 my ($self, $file) = @_;
237 my $content = do {
238 local (@ARGV, $/) = ($file);
239 <>
240 };
241 close ARGV;
242 return $content;
243}
244
245sub collect_dirs {
246 my ($self) = @_;
247 my $cwd = cwd;
248 return grep -d, map rel2abs($_, $cwd), ('lib','fatlib');
249}
250
251sub collect_files {
252 my ($self, $dir, $files) = @_;
253 find(sub {
254 return unless -f $_;
255 !/\.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;
256 $files->{File::Spec::Unix->abs2rel($File::Find::name,$dir)} =
257 $self->load_file($File::Find::name);
258 }, $dir);
259}
260
261sub load_main_script {
262 my ($self, $file) = @_;
263 open my $fh, "<", $file or die("Can't read $file: $!");
264 my $shebang = <$fh>;
265 my $script = join "", <$fh>;
266 close $fh;
267 unless ( index($shebang, '#!') == 0 ) {
268 $script = $shebang . $script;
269 $shebang = "";
48af1939 270 }
30c64724 271 return ($shebang, $script);
272}
273
274sub fatpack_start {
275 return stripspace <<' END_START';
48af1939 276 # This chunk of stuff was generated by App::FatPacker. To find the original
277 # file's code, look for the end of this BEGIN block or the string 'FATPACK'
278 BEGIN {
279 my %fatpacked;
280 END_START
30c64724 281}
282
283sub fatpack_end {
284 return stripspace <<' END_END';
48af1939 285 s/^ //mg for values %fatpacked;
286
f147c6f0 287 my $class = 'FatPacked::'.(0+\%fatpacked);
288 no strict 'refs';
33020868 289 *{"${class}::files"} = sub { keys %{$_[0]} };
e7051d24 290
291 if ($] < 5.008) {
33020868 292 *{"${class}::INC"} = sub {
f147c6f0 293 if (my $fat = $_[0]{$_[1]}) {
e7051d24 294 return sub {
295 return 0 unless length $fat;
296 $fat =~ s/^([^\n]*\n?)//;
297 $_ = $1;
298 return 1;
299 };
300 }
301 return;
15bd679e 302 };
e7051d24 303 }
304
305 else {
e7051d24 306 *{"${class}::INC"} = sub {
15bd679e 307 if (my $fat = $_[0]{$_[1]}) {
e7051d24 308 open my $fh, '<', \$fat
309 or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
310 return $fh;
311 }
312 return;
313 };
e7051d24 314 }
48af1939 315
33020868 316 unshift @INC, bless \%fatpacked, $class;
e7051d24 317 } # END OF FATPACK CODE
48af1939 318 END_END
30c64724 319}
320
321sub fatpack_code {
322 my ($self, $files) = @_;
48af1939 323 my @segments = map {
324 (my $stub = $_) =~ s/\.pm$//;
325 my $name = uc join '_', split '/', $stub;
30c64724 326 my $data = $files->{$_}; $data =~ s/^/ /mg; $data =~ s/(?<!\n)\z/\n/;
56a51caa 327 '$fatpacked{'.perlstring($_).qq!} = '#line '.(1+__LINE__).' "'.__FILE__."\\"\\n".<<'${name}';\n!
48af1939 328 .qq!${data}${name}\n!;
30c64724 329 } sort keys %$files;
330
331 return join "\n", $self->fatpack_start, @segments, $self->fatpack_end;
48af1939 332}
f5a54fa1 333
bb49414d 334=encoding UTF-8
335
f5a54fa1 336=head1 NAME
337
338App::FatPacker - pack your dependencies onto your script file
339
340=head1 SYNOPSIS
341
6da38a2d 342 $ fatpack pack myscript.pl >myscript.packed.pl
343
344Or, with more step-by-step control:
345
f5a54fa1 346 $ fatpack trace myscript.pl
62ceea28 347 $ fatpack packlists-for `cat fatpacker.trace` >packlists
348 $ fatpack tree `cat packlists`
7dabafaa 349 $ fatpack file myscript.pl >myscript.packed.pl
cb50b68f 350
f5a54fa1 351See the documentation for the L<fatpack> script itself for more information.
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
66a19d01 356=head1 SEE ALSO
357
358L<article for Perl Advent 2012|http://www.perladvent.org/2012/2012-12-14.html>
359
f5a54fa1 360=head1 SUPPORT
361
ef3dc772 362Your current best avenue is to come annoy mst on #toolchain on
f5a54fa1 363irc.perl.org. There should be a non-IRC means of support by 1.0.
364
365=head1 AUTHOR
366
367Matt S. Trout (mst) <mst@shadowcat.co.uk>
368
369=head2 CONTRIBUTORS
370
99b15200 371miyagawa - Tatsuhiko Miyagawa (cpan:MIYAGAWA) <miyagawa@bulknews.net>
372
373tokuhirom - MATSUNO★Tokuhiro (cpan:TOKUHIROM) <tokuhirom@gmail.com>
374
375dg - David Leadbeater (cpan:DGL) <dgl@dgl.cx>
376
377gugod - 劉康民 (cpan:GUGOD) <gugod@cpan.org>
378
379t0m - Tomas Doran (cpan:BOBTFISH) <bobtfish@bobtfish.net>
380
381sawyer - Sawyer X (cpan:XSAWYERX) <xsawyerx@cpan.org>
382
383ether - Karen Etheridge (cpan:ETHER) <ether@cpan.org>
384
cbd99f49 385Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
386
56a51caa 387dolmen - Olivier Mengué (cpan:DOLMEN) <dolmen@cpan.org>
388
ab7608ee 389djerius - Diab Jerius (cpan:DJERIUS) <djerius@cpan.org>
390
3c29415b 391haarg - Graham Knop (cpan:HAARG> <haarg@haarg.org>
392
99b15200 393Many more people are probably owed thanks for ideas. Yet
f5a54fa1 394another doc nit to fix.
395
396=head1 COPYRIGHT
397
398Copyright (c) 2010 the App::FatPacker L</AUTHOR> and L</CONTRIBUTORS>
399as listed above.
400
401=head1 LICENSE
402
403This library is free software and may be distributed under the same terms
404as perl itself.
405
406=cut
407
48af1939 4081;
24d68aa2 409