File Coverage

blib/lib/App/FatPacker.pm
Criterion Covered Total %
statement 83 175 47.4
branch 9 44 20.4
condition 4 30 13.3
subroutine 24 36 66.6
pod 0 22 0.0
total 120 307 39.0


line stmt bran cond sub pod time code
1             package App::FatPacker;
2              
3 3     3   65823 use strict;
  3         7  
  3         103  
4 3     3   14 use warnings FATAL => 'all';
  3         5  
  3         133  
5 3     3   91 use 5.008001;
  3         8  
6 3     3   2446 use Getopt::Long;
  3         28658  
  3         12  
7 3     3   403 use Cwd qw(cwd);
  3         3  
  3         121  
8 3     3   12 use File::Find qw(find);
  3         3  
  3         143  
9 3         172 use File::Spec::Functions qw(
10             catdir splitpath splitdir catpath rel2abs abs2rel
11 3     3   1399 );
  3         1674  
12 3     3   14 use File::Spec::Unix;
  3         3  
  3         52  
13 3     3   844 use File::Copy qw(copy);
  3         3564  
  3         190  
14 3     3   27 use File::Path qw(mkpath rmtree);
  3         4  
  3         155  
15 3     3   12 use B qw(perlstring);
  3         3  
  3         5455  
16              
17             our $VERSION = '0.010007'; # 0.10.7
18              
19             $VERSION = eval $VERSION;
20              
21             sub call_parser {
22 0     0 0 0 my $self = shift;
23 0         0 my ($args, $options) = @_;
24              
25 0         0 local *ARGV = [ @{$args} ];
  0         0  
26 0         0 $self->{option_parser}->getoptions(@$options);
27              
28 0         0 return [ @ARGV ];
29             }
30              
31             sub lines_of {
32 0     0 0 0 map +(chomp,$_)[1], do { local @ARGV = ($_[0]); <> };
  0         0  
  0         0  
33             }
34              
35             sub stripspace {
36 4     4 0 14 my ($text) = @_;
37 4 50       103 $text =~ /^(\s+)/ && $text =~ s/^$1//mg;
38 4         52 $text;
39             }
40              
41             sub import {
42 2 50 33 2   1289 $_[1] && $_[1] eq '-run_script'
43             and return shift->new->run_script;
44             }
45              
46             sub new {
47 2     2 0 1910 bless {
48             option_parser => Getopt::Long::Parser->new(
49             config => [ qw(require_order pass_through bundling no_auto_abbrev) ]
50             ),
51             }, $_[0];
52             }
53              
54             sub run_script {
55 0     0 0 0 my ($self, $args) = @_;
56 0 0       0 my @args = $args ? @$args : @ARGV;
57 0   0     0 (my $cmd = shift @args || 'help') =~ s/-/_/g;
58              
59 0 0       0 if (my $meth = $self->can("script_command_${cmd}")) {
60 0         0 $self->$meth(\@args);
61             } else {
62 0         0 die "No such command ${cmd}";
63             }
64             }
65              
66             sub script_command_help {
67 0     0 0 0 print "Try `perldoc fatpack` for how to use me\n";
68             }
69              
70             sub script_command_pack {
71 0     0 0 0 my ($self, $args) = @_;
72              
73 0         0 my @modules = split /\r?\n/, $self->trace(args => $args);
74 0         0 my @packlists = $self->packlists_containing(\@modules);
75              
76 0         0 my $base = catdir(cwd, 'fatlib');
77 0         0 $self->packlists_to_tree($base, \@packlists);
78              
79 0         0 my $file = shift @$args;
80 0         0 print $self->fatpack_file($file);
81             }
82              
83             sub script_command_trace {
84 0     0 0 0 my ($self, $args) = @_;
85              
86 0         0 $args = $self->call_parser($args => [
87             'to=s' => \my $file,
88             'to-stderr' => \my $to_stderr,
89             'use=s' => \my @additional_use
90             ]);
91              
92 0 0 0     0 die "Can't use to and to-stderr on same call" if $file && $to_stderr;
93              
94 0   0     0 $file ||= 'fatpacker.trace';
95              
96 0 0 0     0 if (!$to_stderr and -e $file) {
97 0 0       0 unlink $file or die "Couldn't remove old trace file: $!";
98             }
99 0         0 my $arg = do {
100 0 0       0 if ($to_stderr) {
    0          
101 0         0 ">&STDERR"
102             } elsif ($file) {
103 0         0 ">>${file}"
104             }
105             };
106              
107 0         0 $self->trace(
108             use => \@additional_use,
109             args => $args,
110             output => $arg,
111             );
112             }
113              
114             sub trace {
115 0     0 0 0 my ($self, %opts) = @_;
116              
117 0         0 my $output = $opts{output};
118 0 0 0     0 my $trace_opts = join ',', $output||'>&STDOUT', @{$opts{use}||[]};
  0         0  
119              
120             local $ENV{PERL5OPT} = join ' ',
121 0   0     0 ($ENV{PERL5OPT}||()), '-MApp::FatPacker::Trace='.$trace_opts;
122              
123 0 0       0 my @args = @{$opts{args}||[]};
  0         0  
124              
125 0 0       0 if ($output) {
126             # user specified output target, JFDI
127 0         0 system $^X, @args;
128 0         0 return;
129             } else {
130             # no output target specified, slurp
131 0         0 open my $out_fh, "$^X @args |";
132 0         0 return do { local $/; <$out_fh> };
  0         0  
  0         0  
133             }
134             }
135              
136             sub script_command_packlists_for {
137 0     0 0 0 my ($self, $args) = @_;
138 0         0 foreach my $pl ($self->packlists_containing($args)) {
139 0         0 print "${pl}\n";
140             }
141             }
142              
143             sub packlists_containing {
144 0     0 0 0 my ($self, $targets) = @_;
145 0         0 my @targets;
146             {
147 0         0 local @INC = ('lib', @INC);
  0         0  
148 0         0 foreach my $t (@$targets) {
149 0 0       0 unless (eval { require $t; 1}) {
  0         0  
  0         0  
150 0         0 warn "Failed to load ${t}: $@\n"
151             ."Make sure you're not missing a packlist as a result\n";
152 0         0 next;
153             }
154 0         0 push @targets, $t;
155             }
156             }
157 0         0 my @search = grep -d $_, map catdir($_, 'auto'), @INC;
158 0         0 my %pack_rev;
159             find({
160             no_chdir => 1,
161             wanted => sub {
162 0 0 0 0   0 return unless /[\\\/]\.packlist$/ && -f $_;
163 0         0 $pack_rev{$_} = $File::Find::name for lines_of $File::Find::name;
164             },
165 0         0 }, @search);
166 0   0     0 my %found; @found{map +($pack_rev{Cwd::abs_path($INC{$_})}||()), @targets} = ();
  0         0  
167 0         0 sort keys %found;
168             }
169              
170             sub script_command_tree {
171 0     0 0 0 my ($self, $args) = @_;
172 0         0 my $base = catdir(cwd,'fatlib');
173 0         0 $self->packlists_to_tree($base, $args);
174             }
175              
176             sub packlists_to_tree {
177 0     0 0 0 my ($self, $where, $packlists) = @_;
178 0         0 rmtree $where;
179 0         0 mkpath $where;
180 0         0 foreach my $pl (@$packlists) {
181 0         0 my ($vol, $dirs, $file) = splitpath $pl;
182 0         0 my @dir_parts = splitdir $dirs;
183 0         0 my $pack_base;
184 0         0 PART: foreach my $p (0 .. $#dir_parts) {
185 0 0       0 if ($dir_parts[$p] eq 'auto') {
186             # $p-2 normally since it's /$Config{archname}/auto but
187             # if the last bit is a number it's $Config{archname}/$version/auto
188             # so use $p-3 in that case
189 0         0 my $version_lib = 0+!!($dir_parts[$p-1] =~ /^[0-9.]+$/);
190 0         0 $pack_base = catpath $vol, catdir @dir_parts[0..$p-(2+$version_lib)];
191 0         0 last PART;
192             }
193             }
194 0 0       0 die "Couldn't figure out base path of packlist ${pl}" unless $pack_base;
195 0         0 foreach my $source (lines_of $pl) {
196             # there is presumably a better way to do "is this under this base?"
197             # but if so, it's not obvious to me in File::Spec
198 0 0       0 next unless substr($source,0,length $pack_base) eq $pack_base;
199 0         0 my $target = rel2abs( abs2rel($source, $pack_base), $where );
200 0         0 my $target_dir = catpath((splitpath $target)[0,1]);
201 0         0 mkpath $target_dir;
202 0         0 copy $source => $target;
203             }
204             }
205             }
206              
207             sub script_command_file {
208 2     2 0 998 my ($self, $args) = @_;
209 2         4 my $file = shift @$args;
210 2         8 print $self->fatpack_file($file);
211             }
212              
213             sub fatpack_file {
214 2     2 0 3 my ($self, $file) = @_;
215              
216 2         3 my $shebang = "";
217 2         3 my $script = "";
218 2 100 66     24 if ( defined $file and -r $file ) {
219 1         3 ($shebang, $script) = $self->load_main_script($file);
220             }
221              
222 2         7 my @dirs = $self->collect_dirs();
223 2         189 my %files;
224 2         13 $self->collect_files($_, \%files) for @dirs;
225              
226 2         11 return join "\n", $shebang, $self->fatpack_code(\%files), $script;
227             }
228              
229             # This method can be overload in sub classes
230             # For example to skip POD
231             sub load_file {
232 6     6 0 9 my ($self, $file) = @_;
233 6         28 my $content = do {
234 6         25 local (@ARGV, $/) = ($file);
235             <>
236 6         229 };
237 6         28 close ARGV;
238 6         446 return $content;
239             }
240              
241             sub collect_dirs {
242 2     2 0 4 my ($self) = @_;
243 2         4271 my $cwd = cwd;
244 2         45 return grep -d, map rel2abs($_, $cwd), ('lib','fatlib');
245             }
246              
247             sub collect_files {
248 3     3 0 7 my ($self, $dir, $files) = @_;
249             find(sub {
250 10 100   10   346 return unless -f $_;
251 6 50 33     32 !/\.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;
252 6         16 $files->{File::Spec::Unix->abs2rel($File::Find::name,$dir)} =
253             $self->load_file($File::Find::name);
254 3         255 }, $dir);
255             }
256              
257             sub load_main_script {
258 1     1 0 2 my ($self, $file) = @_;
259 1 50       21 open my $fh, "<", $file or die("Can't read $file: $!");
260 1         18 my $shebang = <$fh>;
261 1         8 my $script = join "", <$fh>;
262 1         6 close $fh;
263 1 50       4 unless ( index($shebang, '#!') == 0 ) {
264 1         3 $script = $shebang . $script;
265 1         1 $shebang = "";
266             }
267 1         5 return ($shebang, $script);
268             }
269              
270             sub fatpack_start {
271 2     2 0 12 return stripspace <<' END_START';
272             # This chunk of stuff was generated by App::FatPacker. To find the original
273             # file's code, look for the end of this BEGIN block or the string 'FATPACK'
274             BEGIN {
275             my %fatpacked;
276             END_START
277             }
278              
279             sub fatpack_end {
280 2     2 0 6 return stripspace <<' END_END';
281             s/^ //mg for values %fatpacked;
282              
283             my $class = 'FatPacked::'.(0+\%fatpacked);
284             no strict 'refs';
285             *{"${class}::files"} = sub { keys %{$_[0]} };
286              
287             if ($] < 5.008) {
288             *{"${class}::INC"} = sub {
289             if (my $fat = $_[0]{$_[1]}) {
290             my $pos = 0;
291             my $last = length $fat;
292             return (sub {
293             return 0 if $pos == $last;
294             my $next = (1 + index $fat, "\n", $pos) || $last;
295             $_ .= substr $fat, $pos, $next - $pos;
296             $pos = $next;
297             return 1;
298             });
299             }
300             };
301             }
302              
303             else {
304             *{"${class}::INC"} = sub {
305             if (my $fat = $_[0]{$_[1]}) {
306             open my $fh, '<', \$fat
307             or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
308             return $fh;
309             }
310             return;
311             };
312             }
313              
314             unshift @INC, bless \%fatpacked, $class;
315             } # END OF FATPACK CODE
316             END_END
317             }
318              
319             sub fatpack_code {
320 2     2 0 5 my ($self, $files) = @_;
321             my @segments = map {
322 2         12 (my $stub = $_) =~ s/\.pm$//;
  6         19  
323 6         22 my $name = uc join '_', split '/', $stub;
324 6         8 my $data = $files->{$_}; $data =~ s/^/ /mg; $data =~ s/(?
  6         28  
  6         12  
325 6         34 '$fatpacked{'.perlstring($_).qq!} = '#line '.(1+__LINE__).' "'.__FILE__."\\"\\n".<<'${name}';\n!
326             .qq!${data}${name}\n!;
327             } sort keys %$files;
328              
329 2         7 return join "\n", $self->fatpack_start, @segments, $self->fatpack_end;
330             }
331              
332             =encoding UTF-8
333              
334             =head1 NAME
335              
336             App::FatPacker - pack your dependencies onto your script file
337              
338             =head1 SYNOPSIS
339              
340             $ fatpack pack myscript.pl >myscript.packed.pl
341              
342             Or, with more step-by-step control:
343              
344             $ fatpack trace myscript.pl
345             $ fatpack packlists-for `cat fatpacker.trace` >packlists
346             $ fatpack tree `cat packlists`
347             $ fatpack file myscript.pl >myscript.packed.pl
348              
349             See the documentation for the L script itself for more information.
350              
351             The programmatic API for this code is not yet fully decided, hence the 0.x
352             release version. Expect that to be cleaned up for 1.0.
353              
354             =head1 SEE ALSO
355              
356             L
357              
358             =head1 SUPPORT
359              
360             Bugs may be submitted through L
361             (or L).
362              
363             You can normally also obtain assistance on irc, in #toolchain on irc.perl.org.
364              
365             =head1 AUTHOR
366              
367             Matt S. Trout (mst)
368              
369             =head2 CONTRIBUTORS
370              
371             miyagawa - Tatsuhiko Miyagawa (cpan:MIYAGAWA)
372              
373             tokuhirom - MATSUNO★Tokuhiro (cpan:TOKUHIROM)
374              
375             dg - David Leadbeater (cpan:DGL)
376              
377             gugod - 劉康民 (cpan:GUGOD)
378              
379             t0m - Tomas Doran (cpan:BOBTFISH)
380              
381             sawyer - Sawyer X (cpan:XSAWYERX)
382              
383             ether - Karen Etheridge (cpan:ETHER)
384              
385             Mithaldu - Christian Walde (cpan:MITHALDU)
386              
387             dolmen - Olivier Mengué (cpan:DOLMEN)
388              
389             djerius - Diab Jerius (cpan:DJERIUS)
390              
391             haarg - Graham Knop (cpan:HAARG>
392              
393             Many more people are probably owed thanks for ideas. Yet
394             another doc nit to fix.
395              
396             =head1 COPYRIGHT
397              
398             Copyright (c) 2010 the App::FatPacker L and L
399             as listed above.
400              
401             =head1 LICENSE
402              
403             This library is free software and may be distributed under the same terms
404             as perl itself.
405              
406             =cut
407              
408             1;
409