File Coverage

blib/lib/FFI/CheckLib.pm
Criterion Covered Total %
statement 166 202 82.1
branch 75 108 69.4
condition 17 30 56.6
subroutine 22 28 78.5
pod 9 9 100.0
total 289 377 76.6


line stmt bran cond sub pod time code
1             package FFI::CheckLib;
2              
3 6     6   1263899 use strict;
  6         43  
  6         156  
4 6     6   45 use warnings;
  6         15  
  6         140  
5 6     6   26 use File::Spec;
  6         18  
  6         195  
6 6     6   33 use List::Util 1.33 qw( any );
  6         134  
  6         362  
7 6     6   41 use Carp qw( croak carp );
  6         10  
  6         284  
8 6     6   6152 use Env qw( @FFI_CHECKLIB_PATH );
  6         14025  
  6         39  
9 6     6   971 use base qw( Exporter );
  6         13  
  6         2925  
10              
11             our @EXPORT = qw(
12             find_lib
13             assert_lib
14             check_lib
15             check_lib_or_exit
16             find_lib_or_exit
17             find_lib_or_die
18             );
19              
20             our @EXPORT_OK = qw(
21             which
22             where
23             has_symbols
24             );
25              
26             # ABSTRACT: Check that a library is available for FFI
27             our $VERSION = '0.31'; # VERSION
28              
29              
30             our $system_path = [];
31             our $os ||= $^O;
32             my $try_ld_on_text = 0;
33              
34             sub _homebrew_lib_path {
35 1     1   551 require File::Which;
36 1 50       877 return undef unless File::Which::which('brew');
37 0         0 chomp(my $brew_path = (qx`brew --prefix`)[0]);
38 0         0 return "$brew_path/lib";
39             }
40              
41             sub _macports_lib_path {
42 1     1   6 require File::Which;
43 1         3 my $port_path = File::Which::which('port');
44 1 50       211 return undef unless $port_path;
45 0         0 $port_path =~ s|bin/port|lib|;
46 0         0 return $port_path;
47             }
48              
49             sub _darwin_extra_paths {
50 6   100 6   11357 my $pkg_managers = lc( $ENV{FFI_CHECKLIB_PACKAGE} || 'homebrew,macports' );
51 6 100       18 return () if $pkg_managers eq 'none';
52 5         23 my $supported_managers = {
53             homebrew => \&_homebrew_lib_path,
54             macports => \&_macports_lib_path
55             };
56 5         10 my @extra_paths = ();
57 5         18 foreach my $pkg_manager (split( /,/, $pkg_managers )) {
58 8 100       334 if (my $lib_path = $supported_managers->{$pkg_manager}()) {
59 6         26 push @extra_paths, $lib_path;
60             }
61             }
62 5         27 return @extra_paths;
63             }
64              
65             my @extra_paths = ();
66             if($os eq 'MSWin32' || $os eq 'msys')
67             {
68             $system_path = eval {
69             require Env;
70             Env->import('@PATH');
71             \our @PATH;
72             };
73             die $@ if $@;
74             }
75             else
76             {
77             $system_path = eval {
78             require DynaLoader;
79 6     6   48 no warnings 'once';
  6         11  
  6         13322  
80             \@DynaLoader::dl_library_path;
81             };
82             die $@ if $@;
83             @extra_paths = _darwin_extra_paths() if $os eq 'darwin';
84             }
85              
86             our $pattern = [ qr{^lib(.*?)\.so(?:\.([0-9]+(?:\.[0-9]+)*))?$} ];
87             our $version_split = qr/\./;
88              
89             if($os eq 'cygwin')
90             {
91             push @$pattern, qr{^cyg(.*?)(?:-([0-9])+)?\.dll$};
92             }
93             elsif($os eq 'msys')
94             {
95             # doesn't seem as though msys uses psudo libfoo.so files
96             # in the way that cygwin sometimes does. we can revisit
97             # this if we find otherwise.
98             $pattern = [ qr{^msys-(.*?)(?:-([0-9])+)?\.dll$} ];
99             }
100             elsif($os eq 'MSWin32')
101             {
102             # handle cases like libgeos-3-7-0___.dll, libproj_9_1.dll and libgtk-2.0-0.dll
103             $pattern = [ qr{^(?:lib)?(\w+?)(?:[_-]([0-9\-\._]+))?_*\.dll$}i ];
104             $version_split = qr/[_\-]/;
105             }
106             elsif($os eq 'darwin')
107             {
108             push @$pattern, qr{^lib(.*?)(?:\.([0-9]+(?:\.[0-9]+)*))?\.(?:dylib|bundle)$};
109             }
110             elsif($os eq 'linux')
111             {
112             if(-e '/etc/redhat-release' && -x '/usr/bin/ld')
113             {
114             $try_ld_on_text = 1;
115             }
116             }
117              
118             sub _matches
119             {
120 1577     1577   52511 my($filename, $path) = @_;
121              
122 1577         2084 foreach my $regex (@$pattern)
123             {
124             return [
125 2093 100       18152 $1, # 0 capture group 1 library name
    100          
126             File::Spec->catfile($path, $filename), # 1 full path to library
127             defined $2 ? (split $version_split, $2) : (), # 2... capture group 2 library version
128             ] if $filename =~ $regex;
129             }
130 547         1583 return ();
131             }
132              
133             sub _cmp
134             {
135 210     210   371 my($A,$B) = @_;
136              
137 210 100       472 return $A->[0] cmp $B->[0] if $A->[0] ne $B->[0];
138              
139 152         164 my $i=2;
140 152         161 while(1)
141             {
142 256 50 66     393 return 0 if !defined($A->[$i]) && !defined($B->[$i]);
143 256 100       352 return -1 if !defined $A->[$i];
144 246 100       451 return 1 if !defined $B->[$i];
145 118 100       249 return $B->[$i] <=> $A->[$i] if $A->[$i] != $B->[$i];
146 104         107 $i++;
147             }
148             }
149              
150              
151             my $diagnostic;
152              
153             sub _is_binary
154             {
155 0     0   0 -B $_[0]
156             }
157              
158             sub find_lib
159             {
160 82     82   145754 my(%args) = @_;
161              
162 82         133 undef $diagnostic;
163 82 50       192 croak "find_lib requires lib argument" unless defined $args{lib};
164              
165 82   100     403 my $recursive = $args{_r} || $args{recursive} || 0;
166              
167             # make arguments be lists.
168 82         141 foreach my $arg (qw( lib libpath symbol verify alien ))
169             {
170 410 100       660 next if ref $args{$arg} eq 'ARRAY';
171 390 100       530 if(defined $args{$arg})
172             {
173 97         216 $args{$arg} = [ $args{$arg} ];
174             }
175             else
176             {
177 293         458 $args{$arg} = [];
178             }
179             }
180              
181 82 50 33     200 if(defined $args{systempath} && !ref($args{systempath}))
182             {
183 0         0 $args{systempath} = [ $args{systempath} ];
184             }
185              
186 82         105 my @path = @{ $args{libpath} };
  82         142  
187 82 100       155 @path = map { _recurse($_) } @path if $recursive;
  2         5  
188              
189 82 50       143 if(defined $args{systempath})
190             {
191 0         0 push @path, grep { defined } @{ $args{systempath} }
  0         0  
  0         0  
192             }
193             else
194             {
195             # This is a little convaluted, but:
196             # 1. These are modifications of what we consider the "system" path
197             # if systempath isn't explicitly passed in as systempath
198             # 2. FFI_CHECKLIB_PATH is considered an authortative modification
199             # so it goes first and overrides FFI_CHECKLIB_PACKAGE
200             # 3. otherwise FFI_CHECKLIB_PACKAGE does its thing and goes on
201             # the end because homebrew does a good job of not replacing
202             # anything in the system by default.
203             # 4. We finally add what we consider the "system" path to the end of
204             # the search path so that libpath will be searched first.
205 82         190 my @system_path = @$system_path;
206 82 100       539 if($ENV{FFI_CHECKLIB_PATH})
207             {
208 2         11 @system_path = (@FFI_CHECKLIB_PATH, @system_path);
209             }
210             else
211             {
212 80         143 foreach my $extra_path (@extra_paths)
213             {
214 0 0   0   0 push @path, $extra_path unless any { $_ eq $extra_path } @path;
  0         0  
215             }
216             }
217 82         204 push @path, @system_path;
218             }
219              
220 82     85   233 my $any = any { $_ eq '*' } @{ $args{lib} };
  85         159  
  82         284  
221 82         206 my %missing = map { $_ => 1 } @{ $args{lib} };
  85         240  
  82         166  
222 82         131 my %symbols = map { $_ => 1 } @{ $args{symbol} };
  27         53  
  82         126  
223 82         98 my @found;
224              
225 82         119 delete $missing{'*'};
226              
227 82         100 alien: foreach my $alien (reverse @{ $args{alien} })
  82         147  
228             {
229 5 100       33 unless($alien =~ /^([A-Za-z_][A-Za-z_0-9]*)(::[A-Za-z_][A-Za-z_0-9]*)*$/)
230             {
231 1         220 croak "Doesn't appear to be a valid Alien name $alien";
232             }
233 4 100       5 unless(eval { $alien->can('dynamic_libs') })
  4         34  
234             {
235             {
236 3         4 my $pm = "$alien.pm";
  3         7  
237 3         10 $pm =~ s/::/\//g;
238 3         6 local $@ = '';
239 3         4 eval { require $pm };
  3         628  
240 3 100       14 next alien if $@;
241             }
242 2 100       3 unless(eval { $alien->can('dynamic_libs') })
  2         15  
243             {
244 1         96 croak "Alien $alien doesn't provide a dynamic_libs method";
245             }
246             }
247 2         7 unshift @path, [$alien->dynamic_libs];
248             }
249              
250 80         126 foreach my $path (@path)
251             {
252 169 50 66     2808 next if ref $path ne 'ARRAY' && ! -d $path;
253              
254             my @maybe =
255             # make determinist based on names and versions
256 147         211 sort { _cmp($a,$b) }
257             # Filter out the items that do not match the name that we are looking for
258             # Filter out any broken symbolic links
259 962 100 100     5178 grep { ($any || $missing{$_->[0]} ) && (-e $_->[1]) }
260             ref $path eq 'ARRAY'
261             ? do {
262             map {
263 2         3 my($v, $d, $f) = File::Spec->splitpath($_);
  2         25  
264 2         16 _matches($f, File::Spec->catpath($v,$d,''));
265             } @$path;
266             }
267 169 100       498 : do {
268 167         197 my $dh;
269 167         3872 opendir $dh, $path;
270             # get [ name, full_path ] mapping,
271             # each entry is a 2 element list ref
272 167         3327 map { _matches($_,$path) } readdir $dh;
  1507         2528  
273             };
274              
275 169 0 33     673 if($try_ld_on_text && $args{try_linker_script})
276             {
277             # This is tested in t/ci.t only
278             @maybe = map {
279 0 0       0 -B $_->[1] ? $_ : do {
  0         0  
280 0         0 my($name, $so) = @$_;
281 0         0 my $output = `/usr/bin/ld -t $so -o /dev/null -shared`;
282 0 0       0 $output =~ /\((.*?lib.*\.so.*?)\)/
283             ? [$name, $1]
284             : die "unable to parse ld output";
285             }
286             } @maybe;
287             }
288              
289             midloop:
290 169         309 foreach my $lib (@maybe)
291             {
292 180 100 100     1223 next unless $any || $missing{$lib->[0]};
293              
294 113         132 foreach my $verify (@{ $args{verify} })
  113         193  
295             {
296 48 100       107 next midloop unless $verify->(@$lib);
297             }
298              
299 68         458 delete $missing{$lib->[0]};
300              
301 68 100       152 if(%symbols)
302             {
303 12         110 require DynaLoader;
304 12         47 my $dll = DynaLoader::dl_load_file($lib->[1],0);
305 12         1306 foreach my $symbol (keys %symbols)
306             {
307 27 100       72 if(DynaLoader::dl_find_symbol($dll, $symbol) ? 1 : 0)
    100          
308             {
309 21         155 delete $symbols{$symbol}
310             }
311             }
312 12         53 DynaLoader::dl_unload_file($dll);
313             }
314              
315 68         156 my $found = $lib->[1];
316              
317 68 100       122 unless($any)
318             {
319 65         706 while(-l $found)
320             {
321 0         0 require File::Basename;
322 0         0 my $dir = File::Basename::dirname($found);
323 0         0 $found = File::Spec->rel2abs( readlink($found), $dir );
324             }
325             }
326              
327 68         278 push @found, $found;
328             }
329             }
330              
331 80 100       248 if(%missing)
    100          
332             {
333 17         60 my @missing = sort keys %missing;
334 17 50       45 if(@missing > 1)
335 0         0 { $diagnostic = "libraries not found: @missing" }
336             else
337 17         52 { $diagnostic = "library not found: @missing" }
338             }
339             elsif(%symbols)
340             {
341 6         29 my @missing = sort keys %symbols;
342 6 50       20 if(@missing > 1)
343 0         0 { $diagnostic = "symbols not found: @missing" }
344             else
345 6         20 { $diagnostic = "symbol not found: @missing" }
346             }
347              
348 80 100       162 return if %symbols;
349 74 100       812 return $found[0] unless wantarray;
350 47         257 return @found;
351             }
352              
353             sub _recurse
354             {
355 6     6   10 my($dir) = @_;
356 6 50       70 return unless -d $dir;
357 6         13 my $dh;
358 6         133 opendir $dh, $dir;
359 6         131 my @list = grep { -d $_ } map { File::Spec->catdir($dir, $_) } grep !/^\.\.?$/, readdir $dh;
  10         124  
  10         71  
360 6         53 closedir $dh;
361 6         30 ($dir, map { _recurse($_) } @list);
  4         13  
362             }
363              
364              
365             sub assert_lib
366             {
367 6 100 50 6 1 8334 croak $diagnostic || 'library not found' unless check_lib(@_);
368             }
369              
370              
371             sub check_lib_or_exit
372             {
373 0 0   0 1 0 unless(check_lib(@_))
374             {
375 0   0     0 carp $diagnostic || 'library not found';
376 0         0 exit;
377             }
378             }
379              
380              
381             sub find_lib_or_exit
382             {
383 0     0 1 0 my(@libs) = find_lib(@_);
384 0 0       0 unless(@libs)
385             {
386 0   0     0 carp $diagnostic || 'library not found';
387 0         0 exit;
388             }
389 0 0       0 return unless @libs;
390 0 0       0 wantarray ? @libs : $libs[0];
391             }
392              
393              
394             sub find_lib_or_die
395             {
396 0     0 1 0 my(@libs) = find_lib(@_);
397 0 0       0 unless(@libs)
398             {
399 0   0     0 croak $diagnostic || 'library not found';
400             }
401 0 0       0 return unless @libs;
402 0 0       0 wantarray ? @libs : $libs[0];
403             }
404              
405              
406             sub check_lib
407             {
408 12 100   12 1 8161 find_lib(@_) ? 1 : 0;
409             }
410              
411              
412             sub which
413             {
414 1     1 1 2498 my($name) = @_;
415 1 50       3 croak("cannot which *") if $name eq '*';
416 1         3 scalar find_lib( lib => $name );
417             }
418              
419              
420             sub where
421             {
422 2     2 1 5171 my($name) = @_;
423             $name eq '*'
424             ? find_lib(lib => '*')
425 2 100   0   10 : find_lib(lib => '*', verify => sub { $_[0] eq $name });
  0         0  
426             }
427              
428              
429             sub has_symbols
430             {
431 6     6 1 5842 my($path, @symbols) = @_;
432 6         27 require DynaLoader;
433 6         12 my $dll = DynaLoader::dl_load_file($path, 0);
434              
435 6         666 my $ok = 1;
436              
437 6         13 foreach my $symbol (@symbols)
438             {
439 11 100       59 unless(DynaLoader::dl_find_symbol($dll, $symbol))
440             {
441 2         14 $ok = 0;
442 2         3 last;
443             }
444             }
445              
446 6         29 DynaLoader::dl_unload_file($dll);
447              
448 6         46 $ok;
449             }
450              
451              
452             sub system_path
453             {
454 1     1 1 2216 $system_path;
455             }
456              
457             1;
458              
459             __END__