File Coverage

lib/App/PP/Autolink.pm
Criterion Covered Total %
statement 47 310 15.1
branch 0 106 0.0
condition 0 20 0.0
subroutine 16 29 55.1
pod 0 9 0.0
total 63 474 13.2


line stmt bran cond sub pod time code
1             # logic initially based on pp_simple.pl
2             # Should cache the Module::Scandeps result
3             # and then clean it up after using it.
4            
5             package App::PP::Autolink;
6            
7 1     1   1198 use strict;
  1         2  
  1         30  
8 1     1   5 use warnings;
  1         2  
  1         22  
9 1     1   17 use 5.014;
  1         3  
10            
11 1     1   6 use Carp;
  1         2  
  1         71  
12 1     1   571 use English qw / -no_match_vars /;
  1         1590  
  1         5  
13            
14 1     1   333 use File::Which qw( which );
  1         2  
  1         52  
15 1     1   562 use Capture::Tiny qw/ capture /;
  1         23880  
  1         66  
16 1     1   7 use List::Util 1.45 qw( uniq any );
  1         19  
  1         101  
17 1     1   531 use File::Find::Rule qw/ rule find /;
  1         8082  
  1         6  
18 1     1   920 use Path::Tiny qw/ path /;
  1         12714  
  1         67  
19             #use File::Temp qw/ tempfile /;
20 1     1   718 use Module::ScanDeps;
  1         24900  
  1         71  
21 1     1   983 use Env qw /@PATH/;
  1         2629  
  1         7  
22            
23 1     1   167 use Config;
  1         3  
  1         32  
24 1     1   666 use Getopt::ArgvFile default=>1;
  1         3825  
  1         6  
25 1     1   4866 use Getopt::Long qw / GetOptionsFromArray :config pass_through /;
  1         11176  
  1         15  
26            
27             our $VERSION = '2.10';
28            
29 1     1   388 use constant CASE_INSENSITIVE_OS => ($^O eq 'MSWin32');
  1         68  
  1         3813  
30            
31             my $RE_DLL_EXT = qr/\.$Config::Config{so}$/i;
32             if ($^O eq 'darwin') {
33             $RE_DLL_EXT = qr/\.($Config::Config{so}|bundle)$/i;
34             }
35            
36             my $ldd_exe = which('ldd');
37            
38            
39             sub new {
40 0     0 0   my ($class, @args) = @_;
41            
42 0           my $self = bless {}, $class;
43            
44             $self->{autolink_list_method}
45 0 0         = $^O eq 'MSWin32' ? 'get_autolink_list'
    0          
    0          
46             : $^O eq 'darwin' ? 'get_autolink_list_macos'
47             : $ldd_exe ? 'get_autolink_list_ldd'
48             # objdump behaves differently on linux (centos at least)
49             : die 'Unable to generate autolink list';
50            
51             # slightly messy, but issues with pass_through and --no-x
52 0           $self->{no_execute_flag} = not grep {$_ eq '-x'} @args;
  0            
53            
54             # Should trap any scandeps args (if diff from pp).
55 0           my @args_array = @args;
56 0           my @argv_linkers;
57            
58 0           GetOptionsFromArray (
59             \@args_array,
60             "link|l=s" => \@argv_linkers,
61             );
62 0           $self->{argv_linkers} = \@argv_linkers;
63 0           $self->{args_to_pass_to_pp} = \@args_array;
64            
65             # pp allows multiple .pl files.
66 0 0         my $script_fullname = $args[-1] or die 'no input file specified';
67 0           $self->{script_fullname} = $script_fullname;
68            
69 0           $self->{alien_sys_installs} = [];
70 0           $self->{alien_deps} = [];
71            
72 0           return $self;
73             }
74            
75             sub build {
76 0     0 0   my ($self) = @_;
77            
78             # reassemble the arg list
79 0           my $argv_linkers = $self->{argv_linkers};
80 0           my $args_array = $self->{args_to_pass_to_pp};
81             my @args_for_pp = (
82 0           (map {("--link" => $_)} @$argv_linkers),
  0            
83             @$args_array,
84             );
85            
86 0           my $method = $self->{autolink_list_method};
87 0           my @dll_list = $self->$method;
88 0           my $alien_sys_installs = $self->{alien_sys_installs};
89            
90             # two-step process to get unique paths
91 0           my %tmp = map {($_ => '--link')} (@dll_list, @$alien_sys_installs);
  0            
92 0           my @links = reverse %tmp;
93            
94 0 0         if (@$alien_sys_installs) {
95 0           say 'Alien sys dlls added: ' . join ' ', @$alien_sys_installs;
96 0           say '';
97             }
98             else {
99 0           say "No alien system dlls detected\n";
100             }
101            
102 0           say 'Detected link list: ' . join ' ', grep {$_ ne '--link'} @links;
  0            
103 0           say '';
104            
105 0           my @aliens = uniq @{$self->{alien_deps}};
  0            
106 0           my @alien_deps = map {; '-M' => $_} @aliens;
  0            
107 0           say 'Detected aliens: ' . join ' ', sort @aliens;
108 0           say '';
109            
110 0           my @command = (
111             'pp',
112             @links,
113             #"--cachedeps=$cache_file",
114             @alien_deps,
115             @args_for_pp,
116             );
117            
118 0           say 'CMD: ' . join ' ', @command;
119 0 0         system (@command) == 0
120             or die "system @command failed: $?";
121            
122 0           return;
123             }
124            
125            
126            
127             sub get_autolink_list {
128 0     0 0   my ($self) = @_;
129            
130 0           my $argv_linkers = $self->{argv_linkers};
131            
132 0 0         my $OBJDUMP = which('objdump') or die "objdump not found";
133            
134 0           my @exe_path = @PATH;
135            
136 0           my @system_paths;
137            
138 0 0         if ($OSNAME =~ /MSWin32/i) {
139             # skip anything under the C:\Windows folder,
140             # blank entries
141             # and no longer extant folders
142 0   0       my $system_root = $ENV{SystemRoot} || $ENV{WINDIR};
143 0 0         @system_paths = grep {$_ and $_ =~ m|^\Q$system_root\E|i} @exe_path;
  0            
144 0 0 0       @exe_path = grep {$_ and (-e $_) and $_ !~ m|^\Q$system_root\E|i} @exe_path;
  0            
145             #say "PATHS: " . join ' ', @exe_path;
146             }
147             # what to skip for linux or mac?
148            
149             # get all the DLLs in the path - saves repeated searching lower down
150 0           my @dll_files = File::Find::Rule->file()
151             ->name( "*.$Config::Config{so}" )
152             ->maxdepth(1)
153             ->in( @exe_path );
154            
155 0           if (CASE_INSENSITIVE_OS) {
156             @dll_files = map {lc $_} @dll_files;
157             }
158            
159 0           my %dll_file_hash;
160 0           foreach my $file (@dll_files) {
161 0           my $basename = path($file)->basename;
162 0   0       $dll_file_hash{$basename} //= $file; # we only want the first in the path
163             }
164            
165            
166             # lc is dirty and underhanded
167             # - need to find a different approach to get
168             # canonical file name while handling case,
169             # poss Win32::GetLongPathName
170 0           my @dlls = @$argv_linkers;
171 0           push @dlls,
172             $self->get_dep_dlls;
173            
174 0           if (CASE_INSENSITIVE_OS) {
175             @dlls = map {lc $_} @dlls;
176             }
177             #say join "\n", @dlls;
178            
179 0           my $re_skippers = $self->get_dll_skipper_regexp();
180 0           my %full_list;
181             my %searched_for;
182 0           my $iter = 0;
183            
184 0           my @missing;
185            
186             DLL_CHECK:
187 0           while (1) {
188 0           $iter++;
189 0           say "DLL check iter: $iter";
190             #say join ' ', @dlls;
191             my ( $stdout, $stderr, $exit ) = capture {
192 0     0     system( $OBJDUMP, '-p', @dlls );
193 0           };
194 0 0         if( $exit ) {
195 0           $stderr =~ s{\s+$}{};
196 0           warn "(@dlls):$exit: $stderr ";
197 0           exit;
198             }
199 0           @dlls = $stdout =~ /DLL.Name:\s*(\S+)/gmi;
200            
201 0           if (CASE_INSENSITIVE_OS) {
202             @dlls = map {lc $_} @dlls;
203             }
204            
205             # extra grep appears wasteful but useful for debug
206             # since we can easily disable it
207             @dlls
208             = sort
209 0           grep {!exists $full_list{$_}}
210 0           grep {$_ !~ /$re_skippers/}
  0            
211             uniq
212             @dlls;
213            
214 0 0         if (!@dlls) {
215 0           say 'no more DLLs';
216 0           last DLL_CHECK;
217             }
218            
219 0           my @dll2;
220 0           foreach my $file (@dlls) {
221 0 0         next if $searched_for{$file};
222            
223 0 0         if (exists $dll_file_hash{$file}) {
224 0           push @dll2, $dll_file_hash{$file};
225             }
226             else {
227 0           push @missing, $file;
228             }
229            
230 0           $searched_for{$file}++;
231             }
232 0           @dlls = uniq @dll2;
233 0           my $key_count = keys %full_list;
234 0           @full_list{@dlls} = (1) x @dlls;
235            
236             # did we add anything new?
237 0 0         last DLL_CHECK if $key_count == scalar keys %full_list;
238             }
239            
240 0           my @l2 = sort keys %full_list;
241            
242 0 0         if (@missing) {
243 0           my @missing2;
244             MISSING:
245 0           foreach my $file (uniq @missing) {
246             next MISSING
247 0 0   0     if any {-e "$_/$file"} @system_paths;
  0            
248 0           push @missing2, $file;
249             }
250            
251 0 0         if (@missing2) {
252             say STDERR "\nUnable to locate these DLLS, packed script might not work: "
253 0           . join ' ', sort {$a cmp $b} @missing2;
  0            
254 0           say '';
255             }
256             }
257            
258 0 0         return wantarray ? @l2 : \@l2;
259             }
260            
261             sub _resolve_rpath_mac {
262 0     0     my ($source, $target) = @_;
263            
264 0           say "Resolving rpath for $source wrt $target";
265            
266             # clean up the target
267 0           $target =~ s|\@rpath/||;
268            
269 0           my @results = qx /otool -l $source/;
270 0           while (my $line = shift @results) {
271 0 0         last if $line =~ /LC_RPATH/;
272             }
273 0           my @lc_rpath_chunk;
274 0           while (my $line = shift @results) {
275 0 0         last if $line =~ /LC_/; # any other command
276 0           push @lc_rpath_chunk, $line;
277             }
278             my @paths
279 0           = map {s/\s\(offset.+$//r}
280 0           map {s/^\s+path //r}
281 0           grep {/^\s+path/}
  0            
282             @lc_rpath_chunk;
283 0           my $loader_path = path ($source)->parent->stringify;
284 0           my @checked_paths;
285 0           foreach my $path (@paths) {
286 0           chomp $path; # should be done above
287 0           $path =~ s/\@loader_path/$loader_path/;
288 0           $path = path($path, $target);
289 0 0         if ($path->exists) {
290 0           $path = $path->realpath->stringify;
291 0           push @checked_paths, $path;
292             }
293             }
294            
295             # should handle multiple paths
296 0           return $checked_paths[0];
297             }
298            
299             sub _resolve_loader_path_mac {
300 0     0     my ($source, $target) = @_;
301 0           say "Resolving loader_path for $source wrt $target";
302 0           my $source_path = path($source)->parent->stringify;
303 0           $target =~ s/\@loader_path/$source_path/;
304 0           return $target;
305             }
306            
307            
308             sub get_autolink_list_macos {
309 0     0 0   my ($self) = @_;
310            
311 0           my $argv_linkers = $self->{argv_linkers};
312            
313 0 0         my $OTOOL = which('otool') or die "otool not found";
314            
315 0           my @bundle_list = $self->get_dep_dlls;
316 0           my @libs_to_pack;
317             my %seen;
318            
319 0           my @target_libs = (
320             @$argv_linkers,
321             @bundle_list,
322             #'/usr/local/opt/libffi/lib/libffi.6.dylib',
323             #($pixbuf_query_loader,
324             #find_so_files ($gdk_pixbuf_dir) ) if $pack_gdkpixbuf,
325             );
326 0           while (my $lib = shift @target_libs) {
327 0           say "otool -L $lib";
328 0           my @lib_arr = qx /otool -L $lib/;
329 0 0         warn qq["otool -L $lib" failed\n]
330             if not $? == 0;
331 0           shift @lib_arr; # first result is dylib we called otool on
332             DEP_LIB:
333 0           foreach my $line (@lib_arr) {
334 0           $line =~ /^\s+(.+?)\s/;
335 0           my $dylib = $1;
336 0 0         if ($dylib =~ /\@rpath/i) {
    0          
337 0           my $orig_name = $dylib;
338 0           $dylib = _resolve_rpath_mac($lib, $dylib);
339 0 0         if (!defined $dylib) {
340 0           say STDERR "Cannot resolve rpath for $orig_name, dependency of $lib";
341 0           next DEP_LIB;
342             }
343             }
344             elsif ($dylib =~ /\@loader_path/) {
345 0           my $orig_name = $dylib;
346 0           $dylib = _resolve_loader_path_mac($lib, $dylib);
347             }
348 0 0         next if $seen{$dylib};
349 0 0         next if $dylib =~ m{^/System}; # skip system libs
350             #next if $dylib =~ m{^/usr/lib/system};
351 0 0         next if $dylib =~ m{^/usr/lib/libSystem};
352 0 0         next if $dylib =~ m{^/usr/lib/};
353 0 0         next if $dylib =~ m{\Qdarwin-thread-multi-2level/auto/share/dist/Alien\E}; # another alien
354 0           say "adding $dylib for $lib";
355 0           push @libs_to_pack, $dylib;
356 0           $seen{$dylib}++;
357             # add this dylib to the search set
358 0           push @target_libs, $dylib;
359             }
360             }
361            
362 0           @libs_to_pack = sort @libs_to_pack;
363            
364 0 0         return wantarray ? @libs_to_pack : \@libs_to_pack;
365             }
366            
367             sub get_autolink_list_ldd {
368 0     0 0   my ($self) = @_;
369            
370 0           my $argv_linkers = $self->{argv_linkers};
371            
372 0           my @bundle_list = $self->get_dep_dlls;
373 0           my @libs_to_pack;
374             my %seen;
375            
376 0           my $RE_skip = $self->get_ldd_skipper_regexp;
377            
378 0           my @target_libs = (
379             @$argv_linkers,
380             @bundle_list,
381             );
382 0           while (my $lib = shift @target_libs) {
383 0 0         if ($lib =~ $RE_skip) {
384 0           say "skipping $lib";
385 0           next;
386             }
387            
388 0           say "ldd $lib";
389 0           my $out = qx /ldd $lib/;
390 0 0         warn qq["ldd $lib" failed\n]
391             if not $? == 0;
392            
393             # much of this logic is from PAR::Packer
394             # https://github.com/rschupp/PAR-Packer/blob/04a133b034448adeb5444af1941a5d7947d8cafb/myldr/find_files_to_embed/ldd.pl#L47
395 0           my %dlls = $out =~ /^ \s* (\S+) \s* => \s* ( \/ \S+ ) /gmx;
396            
397             DLL:
398 0           foreach my $name (keys %dlls) {
399             #say "$name, $dlls{$name}";
400 0 0 0       if ($seen{$name} or $name =~ $RE_skip) {
401 0           delete $dlls{$name};
402 0           next DLL;
403             }
404            
405 0           $seen{$name}++;
406            
407 0           my $path = path($dlls{$name})->realpath;
408            
409             #say "Checking $name => $path";
410            
411 0 0 0       if (not -r $path) {
    0 0        
412 0           warn qq[# ldd reported strange path: $path\n];
413 0           delete $dlls{$name};
414             }
415             elsif (
416             #$path =~ m{^(?:/usr)?/lib(?:32|64)?/} # system lib
417             $path =~ $RE_skip
418             or $path =~ m{\Qdarwin-thread-multi-2level/auto/share/dist/Alien\E} # alien in share
419             or $name =~ m{^lib(?:c|gcc_s|stdc\+\+)\.} # should already be packed?
420             ) {
421             #say "skipping $name => $path";
422             #warn "re1" if $path =~ m{^(?:/usr)?/lib(?:32|64)/};
423             #warn "re2" if $path =~ m{\Qdarwin-thread-multi-2level/auto/share/dist/Alien\E};
424             #warn "re3" if $name =~ m{^lib(?:gcc_s|stdc\+\+)\.};
425 0           delete $dlls{$name};
426             }
427             }
428 0           push @target_libs, sort values %dlls;
429 0           push @libs_to_pack, sort values %dlls;
430             }
431            
432 0           @libs_to_pack = sort @libs_to_pack;
433            
434 0 0         return wantarray ? @libs_to_pack : \@libs_to_pack;
435             }
436            
437            
438             # needed for gdkpixbuf, when we support it
439             sub find_so_files {
440 0     0 0   my ($self, $target_dir) = @_;
441 0 0         return if !defined $target_dir;
442            
443 0           my @files = File::Find::Rule->extras({ follow => 1, follow_skip=>2 })
444             ->file()
445             ->name( qr/\.so$/ )
446             ->in( $target_dir );
447 0 0         return wantarray ? @files : \@files;
448             }
449            
450             sub get_ldd_skipper_regexp {
451 0     0 0   my ($self) = @_;
452 0           my @skip = qw /libm libc libpthread libdl/;
453 0           my $sk = join '|', @skip;
454 0           my $qr_skip = qr {\b(?:$sk)\.$Config::Config{so}};
455            
456 0           return $qr_skip;
457             }
458            
459             sub get_dll_skipper_regexp {
460 0     0 0   my ($self) = @_;
461            
462             # PAR packs these automatically these days.
463 0           my @skip = qw /
464             perl5\d\d
465             libstdc\+\+\-6
466             libgcc_s_seh\-1
467             libwinpthread\-1
468             libgcc_s_sjlj\-1
469             /;
470 0           my $sk = join '|', @skip;
471 0           my $qr_skip = qr /^(?:$sk)$RE_DLL_EXT$/;
472 0           return $qr_skip;
473             }
474            
475             # find dependent dlls
476             # could also adapt some of Module::ScanDeps::_compile_or_execute
477             # as it handles more edge cases
478             sub get_dep_dlls {
479 0     0 0   my ($self) = @_;
480            
481 0           my $script = $self->{script_fullname};
482 0           my $no_execute_flag = $self->{no_execute_flag};
483 0           my $alien_sys_installs = $self->{alien_sys_installs};
484 0           my $cache_file = $self->{cache_file};
485            
486             # This is clunky:
487             # make sure $script/../lib is in @INC
488             # assume script is in a bin folder
489 0           my $rlib_path = (path ($script)->parent->parent->stringify) . '/lib';
490             #say "======= $rlib_path/lib ======";
491 0 0         local @INC = (@INC, $rlib_path)
492             if -d $rlib_path;
493            
494 0           my $deps_hash = scan_deps(
495             files => [ $script ],
496             recurse => 1,
497             execute => !$no_execute_flag,
498             cache_file => $cache_file,
499             );
500            
501             #my @lib_paths
502             # = map {path($_)->absolute}
503             # grep {defined} # needed?
504             # @Config{qw /installsitearch installvendorarch installarchlib/};
505             #say join ' ', @lib_paths;
506             my @lib_paths
507 0           = reverse sort {length $a <=> length $b}
508 0           map {path($_)->absolute}
  0            
509             @INC;
510            
511 0           my $paths = join '|', map {quotemeta} @lib_paths;
  0            
512 0           my $inc_path_re = qr /^($paths)/i;
513             #say $inc_path_re;
514            
515             #say "DEPS HASH:" . join "\n", keys %$deps_hash;
516 0           my %dll_hash;
517             my @aliens;
518 0           foreach my $package (keys %$deps_hash) {
519 0           my $details = $deps_hash->{$package};
520 0   0       my @uses = @{$details->{uses} // []};
  0            
521 0 0         if ($details->{key} =~ m{^Alien/.+\.pm$}) {
522 0           push @aliens, $package;
523             }
524            
525             push @uses, $package
526 0 0         if $details->{file} =~ $RE_DLL_EXT;
527            
528 0 0         next if !@uses;
529            
530 0           foreach my $dll (grep {$_ =~ $RE_DLL_EXT} @uses) {
  0            
531 0           my $dll_path = $deps_hash->{$package}{file};
532             # Remove trailing component of path after /lib/
533 0 0         if ($dll_path =~ m/$inc_path_re/) {
534 0           $dll_path = $1 . '/' . $dll;
535             }
536             else {
537             # fallback, get everything after /lib/
538 0           $dll_path =~ s|(?<=/lib/).+?$||;
539 0           $dll_path .= $dll;
540             }
541             #say $dll_path;
542 0 0         croak "either cannot find or cannot read $dll_path "
543             . "for package $package"
544             if not -r $dll_path;
545 0           $dll_hash{$dll_path}++;
546             }
547             }
548             # handle aliens
549             ALIEN:
550 0           foreach my $package (@aliens) {
551 0 0         next if $package =~ m{^Alien/(Base|Build)};
552 0           my $package_inc_name = $package;
553 0           $package =~ s{/}{::}g;
554 0           $package =~ s/\.pm$//;
555 0 0         if (!$INC{$package_inc_name}) {
556             # if the execute flag was off then try to load the package
557 0           eval "require $package";
558 0 0         if ($@) {
559 0           say "Unable to require $package, skipping (error is $@)";
560 0           next ALIEN;
561             }
562             }
563             # some older aliens might do different things
564 0 0         next ALIEN if !$package->isa ('Alien::Base');
565 0           say "Finding dynamic libs for $package";
566 0           foreach my $path ($package->dynamic_libs) {
567 0           $dll_hash{$path}++;
568             }
569 0 0         if ($package->install_type eq 'system') {
570 0           push @$alien_sys_installs, $package->dynamic_libs;
571             }
572 0           push @{$self->{alien_deps}}, $package;
  0            
573             }
574            
575 0           my @dll_list = sort keys %dll_hash;
576 0 0         return wantarray ? @dll_list : \@dll_list;
577             }
578            
579            
580             1;
581            
582             __END__