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