File Coverage

blib/lib/Alien/Libarchive/Installer.pm
Criterion Covered Total %
statement 15 322 4.6
branch 1 154 0.6
condition 0 61 0.0
subroutine 5 21 23.8
pod 13 13 100.0
total 34 571 5.9


line stmt bran cond sub pod time code
1             package Alien::Libarchive::Installer;
2              
3 7     7   243634 use strict;
  7         9  
  7         163  
4 7     7   21 use warnings;
  7         7  
  7         159  
5 7     7   2971 use File::ShareDir qw( dist_dir );
  7         31602  
  7         8081  
6              
7             # ABSTRACT: Installer for libarchive
8             our $VERSION = '0.15'; # VERSION
9              
10              
11             sub versions_available
12             {
13 0     0 1 0 require HTTP::Tiny;
14 0         0 my $url = "http://www.libarchive.org/downloads/";
15 0         0 my $response = HTTP::Tiny->new->get($url);
16            
17             die sprintf("%s %s %s", $response->{status}, $response->{reason}, $url)
18 0 0       0 unless $response->{success};
19              
20 0         0 my @versions;
21 0         0 push @versions, [$1,$2,$3] while $response->{content} =~ /libarchive-([1-9][0-9]*)\.([0-9]+)\.([0-9]+)\.tar.gz/g;
22 0 0 0     0 @versions = map { join '.', @$_ } sort { $a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] || $a->[2] <=> $b->[2] } @versions;
  0         0  
  0         0  
23             }
24              
25              
26             sub fetch
27             {
28 0     0 1 0 my($class, %options) = @_;
29            
30 0   0     0 my $dir = $options{dir} || eval { require File::Temp; File::Temp::tempdir( CLEANUP => 1 ) };
31              
32 0         0 require HTTP::Tiny;
33 0   0     0 my $version = $options{version} || do {
34             my @versions = $class->versions_available;
35             die "unable to determine latest version from listing"
36             unless @versions > 0;
37             $versions[-1];
38             };
39              
40 0 0       0 if(defined $ENV{ALIEN_LIBARCHIVE_INSTALL_MIRROR})
41             {
42 0         0 my $fn = File::Spec->catfile($ENV{ALIEN_LIBARCHIVE_INSTALL_MIRROR}, "libarchive-$version.tar.gz");
43 0 0       0 return wantarray ? ($fn, $version) : $fn;
44             }
45              
46 0         0 my $url = "http://www.libarchive.org/downloads/libarchive-$version.tar.gz";
47            
48 0         0 my $response = HTTP::Tiny->new->get($url);
49            
50             die sprintf("%s %s %s", $response->{status}, $response->{reason}, $url)
51 0 0       0 unless $response->{success};
52            
53 0         0 require File::Spec;
54            
55 0         0 my $fn = File::Spec->catfile($dir, "libarchive-$version.tar.gz");
56            
57 0         0 open my $fh, '>', $fn;
58 0         0 binmode $fh;
59 0         0 print $fh $response->{content};
60 0         0 close $fh;
61            
62 0 0       0 wantarray ? ($fn, $version) : $fn;
63             }
64              
65              
66             sub build_requires
67             {
68 1     1 1 12 my %prereqs = (
69             'HTTP::Tiny' => 0,
70             'Archive::Tar' => 0,
71             'Alien::patch' => '0.08',
72             );
73            
74 1 50       6 if($^O eq 'MSWin32')
75             {
76 0         0 require Config;
77 0 0       0 if($Config::Config{cc} =~ /cl(\.exe)?$/i)
78             {
79 0         0 $prereqs{'Alien::CMake'} = '0.05';
80             }
81             else
82             {
83 0         0 $prereqs{'Alien::MSYS'} = '0.07';
84 0         0 $prereqs{'PkgConfig'} = '0.07620';
85             }
86             }
87            
88 1         7 \%prereqs;
89             }
90              
91              
92             sub system_requires
93             {
94 0     0 1   my %prereqs = ();
95 0           \%prereqs;
96             }
97              
98              
99             sub system_install
100             {
101 0     0 1   my($class, %options) = @_;
102              
103 0 0         $options{alien} = 1 unless defined $options{alien};
104 0   0       $options{test} ||= 'compile';
105             die "test must be one of compile, ffi or both"
106 0 0         unless $options{test} =~ /^(compile|ffi|both)$/;
107              
108 0 0 0       if($options{alien} && eval q{ use Alien::Libarchive 0.21; 1 })
109             {
110 0           my $alien = Alien::Libarchive->new;
111            
112 0           require File::Spec;
113 0           my $dir;
114             my(@dlls) = map {
115 0           my($v,$d,$f) = File::Spec->splitpath($_);
  0            
116 0           $dir = [$v,File::Spec->splitdir($d)];
117 0           $f;
118             } $alien->dlls;
119            
120 0           my $build = bless {
121             cflags => [$alien->cflags],
122             libs => [$alien->libs],
123             dll_dir => $dir,
124             dlls => \@dlls,
125             prefix => File::Spec->rootdir,
126             }, $class;
127 0           eval {
128 0 0 0       $build->test_compile_run || die $build->error if $options{test} =~ /^(compile|both)$/;
129 0 0 0       $build->test_ffi || die $build->error if $options{test} =~ /^(ffi|both)$/;
130             };
131 0 0         return $build unless $@;
132             }
133              
134 0           my $build = bless {
135             cflags => _try_pkg_config(undef, 'cflags', '', ''),
136             libs => _try_pkg_config(undef, 'libs', '-larchive', ''),
137             }, $class;
138            
139 0 0         if($options{test} =~ /^(ffi|both)$/)
140             {
141 0           my @dir_search_list;
142            
143 0 0         if($^O eq 'MSWin32')
144             {
145             # On MSWin32 the entire path is not included in dl_library_path
146             # but that is the most likely place that we will find dlls.
147 0           @dir_search_list = grep { -d $_ } split /;/, $ENV{PATH};
  0            
148             }
149             else
150             {
151 0           require DynaLoader;
152 0           @dir_search_list = grep { -d $_ } @DynaLoader::dl_library_path
  0            
153             }
154            
155 0           found_dll: foreach my $dir (@dir_search_list)
156             {
157 0           my $dh;
158 0 0         opendir($dh, $dir) || next;
159             # sort by filename length so that libarchive.so.12.0.4
160             # is preferred over libarchive.so.12 or libarchive.so
161             # if only to make diagnostics point to the more specific
162             # version.
163 0           foreach my $file (sort { length $b <=> length $a } readdir $dh)
  0            
164             {
165 0 0         if($^O eq 'MSWin32')
    0          
166             {
167 0 0         next unless $file =~ /^libarchive-[0-9]+\.dll$/i;
168             }
169             elsif($^O eq 'cygwin')
170             {
171 0 0         next unless $file =~ /^cygarchive-[0-9]+\.dll$/i;
172             }
173             else
174             {
175 0 0         next unless $file =~ /^libarchive\.(dylib|so(\.[0-9]+)*)$/;
176             }
177 0           require File::Spec;
178 0           my($v,$d) = File::Spec->splitpath($dir, 1);
179 0           $build->{dll_dir} = [File::Spec->splitdir($d)];
180 0           $build->{prefix} = $v;
181 0           $build->{dlls} = [$file];
182 0           closedir $dh;
183 0           last found_dll;
184             }
185 0           closedir $dh;
186             }
187             }
188              
189 0 0 0       $build->test_compile_run || die $build->error if $options{test} =~ /^(compile|both)$/;
190 0 0 0       $build->test_ffi || die $build->error if $options{test} =~ /^(ffi|both)$/;
191 0           $build;
192             }
193              
194              
195             sub _try_pkg_config
196             {
197 0     0     my($dir, $field, $guess, $extra) = @_;
198            
199 0 0         unless(defined $dir)
200             {
201 0           require File::Temp;
202 0           $dir = File::Temp::tempdir(CLEANUP => 1);
203             }
204            
205 0           require Config;
206 0   0       local $ENV{PKG_CONFIG_PATH} = join $Config::Config{path_sep}, $dir, split /$Config::Config{path_sep}/, ($ENV{PKG_CONFIG_PATH}||'');
207              
208 0           my $value = eval {
209             # you probably think I am crazy...
210 0           eval q{ use PkgConfig 0.07620 };
211 0 0         die $@ if $@;
212 0           my $value = `$^X $INC{'PkgConfig.pm'} --silence-errors libarchive $extra --$field`;
213 0 0         die if $?;
214 0           $value;
215             };
216              
217 0 0         unless(defined $value) {
218 7     7   40 no warnings;
  7         10  
  7         14802  
219 0           $value = `pkg-config --silence-errors libarchive $extra --$field`;
220 0 0         return $guess if $?;
221             }
222            
223 0           chomp $value;
224 0           require Text::ParseWords;
225 0           [Text::ParseWords::shellwords($value)];
226             }
227              
228             sub _msys
229             {
230 0     0     my($sub) = @_;
231 0           require Config;
232 0 0         if($^O eq 'MSWin32')
233             {
234 0 0         if($Config::Config{cc} !~ /cl(\.exe)?$/i)
235             {
236 0           require Alien::MSYS;
237 0     0     return Alien::MSYS::msys(sub{ $sub->('make') });
  0            
238             }
239             }
240 0           $sub->($Config::Config{make});
241             }
242              
243             sub build_install
244             {
245 0     0 1   my($class, $prefix, %options) = @_;
246            
247 0   0       $options{test} ||= 'compile';
248             die "test must be one of compile, ffi or both"
249 0 0         unless $options{test} =~ /^(compile|ffi|both)$/;
250 0 0         die "need an install prefix" unless $prefix;
251            
252 0           $prefix =~ s{\\}{/}g;
253            
254 0   0       my $dir = $options{dir} || do { require File::Temp; File::Temp::tempdir( CLEANUP => 1 ) };
255            
256 0           require Archive::Tar;
257 0           my $tar = Archive::Tar->new;
258 0   0       $tar->read($options{tar} || $class->fetch);
259            
260 0           require Cwd;
261 0           my $save = Cwd::getcwd();
262            
263 0           chdir $dir;
264 0           my $build = eval {
265            
266 0           $tar->extract;
267              
268 0           chdir do {
269 0           opendir my $dh, '.';
270 0           my(@list) = grep !/^\./,readdir $dh;
271 0           close $dh;
272 0 0         die "unable to find source in build root" if @list == 0;
273 0 0         die "confused by multiple entries in the build root" if @list > 1;
274 0           $list[0];
275             };
276            
277 0           do {
278 0           $DB::single = 1;
279 0           my $share_dir = dist_dir('Alien-Libarchive-Installer');
280 0           my($version) = [File::Spec->splitpath(Cwd::getcwd())]->[2] =~ /libarchive-([0-9\.]+)/;
281 0 0         if($version)
282             {
283 0           my $patch = File::Spec->catdir($share_dir, 'patches', "$version.patch");
284 0 0         if(-r $patch)
285             {
286 0           require Alien::patch;
287 0           Alien::patch->import(); # add patch to the path if not already there.
288 0           my $patch_exe = Alien::patch->exe;
289 0           system "$patch_exe -p1 < $patch";
290 0 0         die "patch failed" if $?;
291             }
292             }
293             else
294             {
295 0           warn "unable to determine version number.";
296             }
297             };
298            
299             _msys(sub {
300 0     0     my($make) = @_;
301 0           require Config;
302 0 0         if($Config::Config{cc} !~ /cl(\.exe)?$/i)
303             {
304 0           system 'sh', 'configure', "--prefix=$prefix", '--with-pic';
305 0 0         die "configure failed" if $?;
306             }
307             else
308             {
309 0           require Alien::CMake;
310 0           my $cmake = Alien::CMake->config('prefix') . '/bin/cmake.exe';
311 0 0         my $system = $make =~ /nmake(\.exe)?$/ ? 'NMake Makefiles' : 'MinGW Makefiles';
312 0           system $cmake,
313             -G => $system,
314             "-DCMAKE_MAKE_PROGRAM:PATH=$make",
315             "-DCMAKE_INSTALL_PREFIX:PATH=$prefix",
316             "-DENABLE_TEST=OFF",
317             ".";
318 0 0         die "cmake failed" if $?;
319             }
320 0           system $make, 'all';
321 0 0         die "make all failed" if $?;
322 0           system $make, 'install';
323 0 0         die "make install failed" if $?;
324 0           });
325              
326 0           require File::Spec;
327              
328 0 0         foreach my $name ($^O =~ /^(MSWin32|cygwin)$/ ? ('bin','lib') : ('lib'))
329             {
330 0           do {
331 0           my $static_dir = File::Spec->catdir($prefix, $name);
332 0           my $dll_dir = File::Spec->catdir($prefix, 'dll');
333 0           require File::Path;
334 0           File::Path::mkpath($dll_dir, 0, 0755);
335 0           my $dh;
336 0           opendir $dh, $static_dir;
337 0           my @list = readdir $dh;
338 0 0         @list = grep { /\.so/ || /\.(dylib|la|dll|dll\.a)$/ } grep !/^\./, @list;
  0            
339 0           closedir $dh;
340 0           foreach my $basename (@list)
341             {
342 0           my $from = File::Spec->catfile($static_dir, $basename);
343 0           my $to = File::Spec->catfile($dll_dir, $basename);
344 0 0         if(-l $from)
345             {
346 0           symlink(readlink $from, $to);
347 0           unlink($from);
348             }
349             else
350             {
351 0           require File::Copy;
352 0           File::Copy::move($from, $to);
353             }
354             }
355             };
356             }
357              
358 0           my $pkg_config_dir = File::Spec->catdir($prefix, 'lib', 'pkgconfig');
359            
360 0           my $pcfile = File::Spec->catfile($pkg_config_dir, 'libarchive.pc');
361            
362 0           do {
363 0           my @content;
364 0 0         if($Config::Config{cc} !~ /cl(\.exe)?$/i)
365             {
366 0           open my $fh, '<', $pcfile;
367 0           @content = map { s{$prefix}{'${pcfiledir}/../..'}eg; $_ } do { <$fh> };
  0            
  0            
  0            
  0            
368 0           close $fh;
369             }
370             else
371             {
372             # TODO: later when we know the version with more
373             # certainty, we can update this file with the
374             # Version
375 0           @content = join "\n", "prefix=\${pcfiledir}/../..",
376             "exec_prefix=\${prefix}",
377             "libdir=\${exec_prefix}/lib",
378             "includedir=\${prefix}/include",
379             "Name: libarchive",
380             "Description: library that can create and read several streaming archive formats",
381             "Cflags: -I\${includedir}",
382             "Libs: advapi32.lib \${libdir}/archive_static.lib",
383             "Libs.private: ",
384             "";
385 0           require File::Path;
386 0           File::Path::mkpath($pkg_config_dir, 0, 0755);
387             }
388            
389 0           my($version) = map { /^Version:\s*(.*)$/; $1 } grep /^Version: /, @content;
  0            
  0            
390             # older versions apparently didn't include the necessary -I and -L flags
391 0 0 0       if(defined $version && $version =~ /^[12]\./)
392             {
393 0           for(@content)
394             {
395 0           s/^Libs: /Libs: -L\${libdir} /;
396             }
397 0           push @content, "Cflags: -I\${includedir}\n";
398             }
399            
400 0           open my $fh, '>', $pcfile;
401 0           print $fh @content;
402 0           close $fh;
403             };
404            
405             my $build = bless {
406             cflags => _try_pkg_config($pkg_config_dir, 'cflags', '-I' . File::Spec->catdir($prefix, 'include'), '--static'),
407             libs => _try_pkg_config($pkg_config_dir, 'libs', '-L' . File::Spec->catdir($prefix, 'lib'), '--static'),
408             prefix => $prefix,
409             dll_dir => [ 'dll' ],
410 0           dlls => do {
411 0           opendir(my $dh, File::Spec->catdir($prefix, 'dll'));
412 0 0         [grep { ! -l File::Spec->catfile($prefix, 'dll', $_) } grep { /\.so/ || /\.(dll|dylib)$/ } grep !/^\./, readdir $dh];
  0            
  0            
413             },
414             }, $class;
415            
416 0 0 0       if($^O eq 'cygwin' || $^O eq 'MSWin32')
417             {
418             # TODO: should this go in the munged pc file?
419 0           unshift @{ $build->{cflags} }, '-DLIBARCHIVE_STATIC';
  0            
420             }
421              
422 0 0 0       $build->test_compile_run || die $build->error if $options{test} =~ /^(compile|both)$/;
423 0 0 0       $build->test_ffi || die $build->error if $options{test} =~ /^(ffi|both)$/;
424 0           $build;
425             };
426            
427 0           my $error = $@;
428 0           chdir $save;
429 0 0         die $error if $error;
430 0           $build;
431             }
432              
433              
434 0     0 1   sub cflags { shift->{cflags} }
435 0     0 1   sub libs { shift->{libs} }
436 0     0 1   sub version { shift->{version} }
437              
438             sub dlls
439             {
440 0     0 1   my($self, $prefix) = @_;
441            
442 0 0         $prefix = $self->{prefix} unless defined $prefix;
443 0 0 0       $prefix = '' if $^O eq 'MSWin32' && $prefix eq '\\';
444            
445 0 0 0       unless(defined $self->{dlls} && defined $self->{dll_dir})
446             {
447             # Question: is this necessary in light of the better
448             # dll detection now done in system_install ?
449 0 0         if($^O eq 'cygwin')
450             {
451             # /usr/bin/cygarchive-13.dll
452 0           opendir my $dh, '/usr/bin';
453 0           $self->{dlls} = [grep /^cygarchive-[0-9]+.dll$/i, readdir $dh];
454 0           $self->{dll_dir} = [];
455 0           $prefix = '/usr/bin';
456 0           closedir $dh;
457             }
458             else
459             {
460 0           require DynaLoader;
461 0 0         $self->{libs} = [] unless defined $self->{libs};
462 0 0         $self->{libs} = [ $self->{libs} ] unless ref $self->{libs};
463 0           my $path = DynaLoader::dl_findfile(grep /^-l/, @{ $self->libs });
  0            
464 0 0         die "unable to find dynamic library" unless defined $path;
465 0           require File::Spec;
466 0           my($vol, $dirs, $file) = File::Spec->splitpath($path);
467 0 0         if($^O eq 'openbsd')
468             {
469             # on openbsd we get the .a file back, so have to scan
470             # for .so.#.# as there is no .so symlink
471 0           opendir(my $dh, $dirs);
472 0           $self->{dlls} = [grep /^libarchive.so/, readdir $dh];
473 0           closedir $dh;
474             }
475             else
476             {
477 0           $self->{dlls} = [ $file ];
478             }
479 0           $self->{dll_dir} = [];
480 0           $self->{prefix} = $prefix = File::Spec->catpath($vol, $dirs);
481             }
482             }
483            
484 0 0 0       if($prefix eq '' && $self->{dll_dir}->[0] eq '')
485             {
486 0           shift @{ $self->{dll_dir} };
  0            
487             }
488            
489 0           require File::Spec;
490             $^O eq 'MSWin32'
491 0           ? map { File::Spec->catfile( @{ $self->{dll_dir} }, $_ ) } @{ $self->{dlls} }
  0            
  0            
492 0 0         : map { File::Spec->catfile($prefix, @{ $self->{dll_dir} }, $_ ) } @{ $self->{dlls} };
  0            
  0            
  0            
493             }
494              
495              
496             sub test_compile_run
497             {
498 0     0 1   my($self, %opt) = @_;
499 0           delete $self->{error};
500 0 0         $self->{quiet} = 1 unless defined $self->{quiet};
501 0   0       my $cbuilder = $opt{cbuilder} || do { require ExtUtils::CBuilder; ExtUtils::CBuilder->new(quiet => $self->{quiet}) };
502            
503 0 0         unless($cbuilder->have_compiler)
504             {
505 0           $self->{error} = 'no compiler';
506 0           return;
507             }
508            
509 0           require File::Spec;
510 0   0       my $dir = $opt{dir} || do { require File::Temp; File::Temp::tempdir( CLEANUP => 1 ) };
511 0           my $fn = File::Spec->catfile($dir, 'test.c');
512 0           do {
513 0           open my $fh, '>', $fn;
514 0           print $fh "#include \n",
515             "#include \n",
516             "#include \n",
517             "int\n",
518             "main(int argc, char *argv[])\n",
519             "{\n",
520             " printf(\"version = '%d'\\n\", archive_version_number());\n",
521             " return 0;\n",
522             "}\n";
523 0           close $fh;
524             };
525            
526 0           my $test_object = eval {
527             $cbuilder->compile(
528             source => $fn,
529 0   0       extra_compiler_flags => $self->{cflags} || [],
530             );
531             };
532            
533 0 0         if(my $error = $@)
534             {
535 0           $self->{error} = $error;
536 0           return;
537             }
538            
539 0           my $test_exe = eval {
540             $cbuilder->link_executable(
541             objects => $test_object,
542 0   0       extra_linker_flags => $self->{libs} || [],
543             );
544             };
545            
546 0 0         if(my $error = $@)
547             {
548 0           $self->{error} = $error;
549 0           return;
550             }
551            
552 0 0         if($test_exe =~ /\s/)
553             {
554 0 0         $test_exe = Win32::GetShortPathName($test_exe) if $^O eq 'MSWin32';
555 0 0         $test_exe = Cygwin::win_to_posix_path(Win32::GetShortPathName(Cygwin::posix_to_win_path($test_exe))) if $^O eq 'cygwin';
556             }
557            
558 0           my $output = `$test_exe`;
559            
560 0 0         if($? == -1)
    0          
    0          
561             {
562 0           $self->{error} = "failed to execute $!";
563 0           return;
564             }
565             elsif($? & 127)
566             {
567 0           $self->{error} = "child died with siganl " . ($? & 127);
568 0           return;
569             }
570             elsif($?)
571             {
572 0           $self->{error} = "child exited with value " . ($? >> 8);
573 0           return;
574             }
575            
576 0 0         if($output =~ /version = '([0-9]+)([0-9]{3})([0-9]{3})'/)
577             {
578 0           return $self->{version} = join '.', map { int } $1, $2, $3;
  0            
579             }
580             else
581             {
582 0           $self->{error} = "unable to retrieve version from output";
583 0           return;
584             }
585             }
586              
587              
588             sub test_ffi
589             {
590 0     0 1   my($self) = @_;
591 0           require FFI::Raw;
592 0           delete $self->{error};
593              
594 0           foreach my $dll ($self->dlls)
595             {
596 0           my $archive_version_number = eval {
597 0           FFI::Raw->new(
598             $dll, 'archive_version_number',
599             FFI::Raw::int(),
600             );
601             };
602 0 0         next if $@;
603 0 0         if($archive_version_number->() =~ /^([0-9]+)([0-9]{3})([0-9]{3})/)
604             {
605 0           return $self->{version} = join '.', map { int } $1, $2, $3;
  0            
606             }
607             }
608 0           $self->{error} = 'could not find archive_version_number';
609 0           return;
610             }
611              
612              
613 0     0 1   sub error { shift->{error} }
614              
615             1;
616              
617             __END__