Release commit for 0.009015
[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
6481e9bf 17our $VERSION = '0.009015'; # 0.009.015
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
cb3e6884 35sub maybe_shebang {
36 my ($file) = @_;
37 open my $in, "<", $file or die "$file: $!";
38 my $head = <$in>;
39 if ($head =~ m/^#\!/) {
40 ($head, do { local $/; <$in> });
41 } else {
42 ('', do { local $/; $head . <$in> });
43 }
44}
45
48af1939 46sub stripspace {
47 my ($text) = @_;
48 $text =~ /^(\s+)/ && $text =~ s/^$1//mg;
49 $text;
50}
51
48af1939 52sub import {
24d68aa2 53 $_[1] && $_[1] eq '-run_script'
48af1939 54 and return shift->new->run_script;
55}
56
24d68aa2 57sub new {
58 bless {
59 option_parser => Getopt::Long::Parser->new(
60 config => [ qw(require_order pass_through bundling no_auto_abbrev) ]
61 ),
62 }, $_[0];
63}
48af1939 64
65sub run_script {
66 my ($self, $args) = @_;
67 my @args = $args ? @$args : @ARGV;
3e4dadce 68 (my $cmd = shift @args || 'help') =~ s/-/_/g;
24d68aa2 69
48af1939 70 if (my $meth = $self->can("script_command_${cmd}")) {
71 $self->$meth(\@args);
72 } else {
73 die "No such command ${cmd}";
74 }
75}
76
3e4dadce 77sub script_command_help {
78 print "Try `perldoc fatpack` for how to use me\n";
79}
80
cb3e6884 81sub script_command_pack {
82 my ($self, $args) = @_;
83
84 my @modules = split /\r?\n/, $self->trace(args => $args);
85 my @packlists = $self->packlists_containing(\@modules);
86
87 my $base = catdir(cwd, 'fatlib');
88 $self->packlists_to_tree($base, \@packlists);
89
90 my $file = shift @$args;
91 my($head, $body) = maybe_shebang($file);
92 print $head, $self->fatpack_file($file), $body;
93}
94
48af1939 95sub script_command_trace {
96 my ($self, $args) = @_;
24d68aa2 97
69667cc8 98 $args = $self->call_parser($args => [
48af1939 99 'to=s' => \my $file,
100 'to-stderr' => \my $to_stderr,
3fdf85ca 101 'use=s' => \my @additional_use
69667cc8 102 ]);
48af1939 103
104 die "Can't use to and to-stderr on same call" if $file && $to_stderr;
105
3fdf85ca 106 $file ||= 'fatpacker.trace';
69667cc8 107
3fdf85ca 108 if (!$to_stderr and -e $file) {
109 unlink $file or die "Couldn't remove old trace file: $!";
48af1939 110 }
111 my $arg = do {
3fdf85ca 112 if ($to_stderr) {
020d9b76 113 ">&STDERR"
3fdf85ca 114 } elsif ($file) {
276a30c9 115 ">>${file}"
48af1939 116 }
117 };
3fdf85ca 118
b4704b1a 119 $self->trace(
5e1de95d 120 use => \@additional_use,
121 args => $args,
7a3662c8 122 output => $arg,
b4704b1a 123 );
abd7cf01 124}
125
126sub trace {
b4704b1a 127 my ($self, %opts) = @_;
69667cc8 128
1ea23474 129 my $output = $opts{output};
69667cc8 130 my $trace_opts = join ',', $output||'>&STDOUT', @{$opts{use}||[]};
7a3662c8 131
69667cc8 132 local $ENV{PERL5OPT} = '-MApp::FatPacker::Trace='.$trace_opts;
abd7cf01 133
69667cc8 134 my @args = @{$opts{args}||[]};
e0cd89ab 135
69667cc8 136 if ($output) {
137 # user specified output target, JFDI
138 system $^X, @args;
139 return;
e0cd89ab 140 } else {
69667cc8 141 # no output target specified, slurp
142 open my $out_fh, '-|', $^X, @args;
143 return do { local $/; <$out_fh> };
144 }
48af1939 145}
146
147sub script_command_packlists_for {
148 my ($self, $args) = @_;
149 foreach my $pl ($self->packlists_containing($args)) {
150 print "${pl}\n";
151 }
152}
153
154sub packlists_containing {
155 my ($self, $targets) = @_;
156 my @targets = @$targets;
a976705b 157 foreach my $t (@targets) {
158 require $t;
159 }
48af1939 160 my @search = grep -d $_, map catdir($_, 'auto'), @INC;
161 my %pack_rev;
162 my $cwd = cwd;
163 find(sub {
164 return unless $_ eq '.packlist' && -f $_;
165 $pack_rev{$_} = $File::Find::name for lines_of $File::Find::name;
166 }, @search);
167 chdir($cwd) or die "Couldn't chdir back to ${cwd} after find: $!";
f5a54fa1 168 my %found; @found{map +($pack_rev{$INC{$_}}||()), @targets} = ();
48af1939 169 sort keys %found;
170}
171
172sub script_command_tree {
173 my ($self, $args) = @_;
174 my $base = catdir(cwd,'fatlib');
175 $self->packlists_to_tree($base, $args);
176}
177
178sub packlists_to_tree {
179 my ($self, $where, $packlists) = @_;
4d5603b7 180 rmtree $where;
181 mkpath $where;
48af1939 182 foreach my $pl (@$packlists) {
183 my ($vol, $dirs, $file) = splitpath $pl;
184 my @dir_parts = splitdir $dirs;
185 my $pack_base;
186 PART: foreach my $p (0 .. $#dir_parts) {
187 if ($dir_parts[$p] eq 'auto') {
188 # $p-2 since it's <wanted path>/$Config{archname}/auto
189 $pack_base = catpath $vol, catdir @dir_parts[0..$p-2];
190 last PART;
191 }
192 }
193 die "Couldn't figure out base path of packlist ${pl}" unless $pack_base;
194 foreach my $source (lines_of $pl) {
195 # there is presumably a better way to do "is this under this base?"
196 # but if so, it's not obvious to me in File::Spec
197 next unless substr($source,0,length $pack_base) eq $pack_base;
198 my $target = rel2abs( abs2rel($source, $pack_base), $where );
199 my $target_dir = catpath((splitpath $target)[0,1]);
4d5603b7 200 mkpath $target_dir;
48af1939 201 copy $source => $target;
202 }
203 }
204}
205
206sub script_command_file {
207 my ($self, $args) = @_;
208 my $file = shift @$args;
9be5f3c0 209 print $self->fatpack_file($file);
210}
211
212sub fatpack_file {
213 my ($self, $file) = @_;
48af1939 214 my $cwd = cwd;
0c46b17a 215 my @dirs = grep -d, map rel2abs($_, $cwd), ('lib','fatlib');
48af1939 216 my %files;
217 foreach my $dir (@dirs) {
218 find(sub {
219 return unless -f $_;
220 !/\.pm$/ and warn "File ${File::Find::name} isn't a .pm file - can't pack this and if you hoped we were going to things may not be what you expected later\n" and return;
cbd99f49 221 $files{File::Spec::Unix->abs2rel($File::Find::name,$dir)} = do {
48af1939 222 local (@ARGV, $/) = ($File::Find::name); <>
223 };
0de38c11 224 close ARGV;
48af1939 225 }, $dir);
226 }
227 my $start = stripspace <<' END_START';
228 # This chunk of stuff was generated by App::FatPacker. To find the original
229 # file's code, look for the end of this BEGIN block or the string 'FATPACK'
230 BEGIN {
231 my %fatpacked;
232 END_START
233 my $end = stripspace <<' END_END';
234 s/^ //mg for values %fatpacked;
235
236 unshift @INC, sub {
237 if (my $fat = $fatpacked{$_[1]}) {
7bf1d4f2 238 if ($] < 5.008) {
239 return sub {
240 return 0 unless length $fat;
87108237 241 $fat =~ s/^([^\n]*\n?)//;
7bf1d4f2 242 $_ = $1;
243 return 1;
244 };
245 }
002ecfea 246 open my $fh, '<', \$fat
247 or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
48af1939 248 return $fh;
249 }
250 return
251 };
252
253 } # END OF FATPACK CODE
254 END_END
255 my @segments = map {
256 (my $stub = $_) =~ s/\.pm$//;
257 my $name = uc join '_', split '/', $stub;
cc5db92a 258 my $data = $files{$_}; $data =~ s/^/ /mg; $data =~ s/(?<!\n)\z/\n/;
48af1939 259 '$fatpacked{'.perlstring($_).qq!} = <<'${name}';\n!
260 .qq!${data}${name}\n!;
261 } sort keys %files;
9be5f3c0 262 return join "\n", $start, @segments, $end;
48af1939 263}
f5a54fa1 264
bb49414d 265=encoding UTF-8
266
f5a54fa1 267=head1 NAME
268
269App::FatPacker - pack your dependencies onto your script file
270
271=head1 SYNOPSIS
272
6da38a2d 273 $ fatpack pack myscript.pl >myscript.packed.pl
274
275Or, with more step-by-step control:
276
f5a54fa1 277 $ fatpack trace myscript.pl
62ceea28 278 $ fatpack packlists-for `cat fatpacker.trace` >packlists
279 $ fatpack tree `cat packlists`
24b4367e 280 $ (head -n1 myscript.pl |grep '^#!'; fatpack file; cat myscript.pl) >myscript.packed.pl
f5a54fa1 281
cb50b68f 282The C<head -n1 myscript.pl |grep '^#!'> code pulls out the Unix shebang
283line, if there is one, and injects it at the start of the packed script.
284
f5a54fa1 285See the documentation for the L<fatpack> script itself for more information.
286
3fdf85ca 287The programmatic API for this code is not yet fully decided, hence the 0.9
f5a54fa1 288release version. Expect that to be cleaned up for 1.0.
289
66a19d01 290=head1 SEE ALSO
291
292L<article for Perl Advent 2012|http://www.perladvent.org/2012/2012-12-14.html>
293
f5a54fa1 294=head1 SUPPORT
295
296Your current best avenue is to come annoy annoy mst on #toolchain on
297irc.perl.org. There should be a non-IRC means of support by 1.0.
298
299=head1 AUTHOR
300
301Matt S. Trout (mst) <mst@shadowcat.co.uk>
302
303=head2 CONTRIBUTORS
304
99b15200 305miyagawa - Tatsuhiko Miyagawa (cpan:MIYAGAWA) <miyagawa@bulknews.net>
306
307tokuhirom - MATSUNO★Tokuhiro (cpan:TOKUHIROM) <tokuhirom@gmail.com>
308
309dg - David Leadbeater (cpan:DGL) <dgl@dgl.cx>
310
311gugod - 劉康民 (cpan:GUGOD) <gugod@cpan.org>
312
313t0m - Tomas Doran (cpan:BOBTFISH) <bobtfish@bobtfish.net>
314
315sawyer - Sawyer X (cpan:XSAWYERX) <xsawyerx@cpan.org>
316
317ether - Karen Etheridge (cpan:ETHER) <ether@cpan.org>
318
cbd99f49 319Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
320
99b15200 321Many more people are probably owed thanks for ideas. Yet
f5a54fa1 322another doc nit to fix.
323
324=head1 COPYRIGHT
325
326Copyright (c) 2010 the App::FatPacker L</AUTHOR> and L</CONTRIBUTORS>
327as listed above.
328
329=head1 LICENSE
330
331This library is free software and may be distributed under the same terms
332as perl itself.
333
334=cut
335
48af1939 3361;
24d68aa2 337