File Coverage

blib/lib/lib/archive.pm
Criterion Covered Total %
statement 110 113 97.3
branch 28 38 73.6
condition 10 19 52.6
subroutine 18 18 100.0
pod n/a
total 166 188 88.3


line stmt bran cond sub pod time code
1             package lib::archive;
2              
3 6     6   411061 use strict;
  6         32  
  6         154  
4 6     6   28 use warnings;
  6         8  
  6         130  
5              
6 6     6   108 use 5.010001;
  6         26  
7              
8 6     6   26 use Carp qw(croak);
  6         9  
  6         300  
9 6     6   3680 use Archive::Tar;
  6         480152  
  6         398  
10 6     6   2690 use File::Spec::Functions qw(file_name_is_absolute rel2abs);
  6         4278  
  6         380  
11 6     6   36 use File::Basename qw(basename dirname fileparse);
  6         11  
  6         491  
12 6     6   37 use File::Path qw(make_path);
  6         23  
  6         267  
13 6     6   2305 use MIME::Base64 qw(decode_base64);
  6         3523  
  6         292  
14 6     6   57 use IO::Uncompress::Gunzip;
  6         11  
  6         181  
15 6     6   3019 use HTTP::Tiny;
  6         183451  
  6         7264  
16              
17             our $VERSION = "0.92";
18              
19             =pod
20              
21             =head1 NAME
22              
23             lib::archive - load pure-Perl modules directly from TAR archives
24              
25             =head1 SYNOPSIS
26              
27             use lib::archive ("../external/*.tgz", "lib/extra.tar");
28              
29             use MyModule; # the given tar archives will be searched first
30              
31             or
32              
33             use lib::archive qw(
34             https://www.cpan.org/modules/by-module/JSON/JSON-PP-2.97001.tar.gz
35             CPAN://YAML-PP-0.007.tar.gz
36             );
37              
38             use JSON::PP;
39             use YAML::PP;
40              
41             or
42              
43             use lib::archive '__DATA__';
44              
45             __DATA__
46            
47              
48              
49             =head1 DESCRIPTION
50              
51             Specify TAR archives to directly load modules from. The TAR files will be
52             searched like include dirs. Globs are expanded, so you can use wildcards
53             (not for URLs). If modules are present in more than one TAR archive, the
54             first one will be used.
55              
56             Relative paths will be interpreted as relative to the directory the
57             calling script or module resides in. So don't do a chdir() before using
58             lib::archive when you call your script with a relative path B use releative
59             paths for lib::archive.
60              
61             B
62             Everything is extracted on the fly>. When running under a debugger or the
63             environment variable PERL_LIB_ARCHIVE_EXTRACT is set to a directory name
64             the directories and files will be extracted to the filesystem. I case of
65             running under a debugger without PERL_LIB_ARCHIVE_EXTRACT being set the
66             extracted modules will be saved to the .lib_archive_extract directory
67             in the user's home directory (determined by C). The home
68             directory can be overwritten by setting the environment variable
69             PERL_LIB_ARCHIVE_HOME.
70              
71             An attempt will be made to create the directory should it not already exist.
72              
73             You can use every file format Archive::Tar supports.
74              
75             If the archive contains a toplevel directory 'lib' the module search path
76             will start there. Otherwise it will start from the root of the archive.
77              
78             If the archive is a gzipped TAR archive with the extension '.tar.gz' and the
79             archive contains a toplevel directory matching the archive name without the
80             extension the module search path starts with this directory. The above
81             rule for the subdirectory 'lib' applies from there. This means that e.g. for
82             'JSON-PP-2.97001.tar.gz' the modules will only be included from
83             'JSON-PP-2.97001/lib'.
84              
85             You can use URLs for loading modules directly from CPAN. Either specify the
86             complete URL like:
87              
88             use lib::archive 'https://www.cpan.org/modules/by-module/JSON/JSON-PP-2.97001.tar.gz';
89              
90             or use a shortcut like:
91              
92             use lib::archive 'CPAN://JSON-PP-2.97001.tar.gz';
93              
94             which will do exactly the same thing (at least in most cases: there seem to
95             be modules without an entry under 'modules/by-module/'; in that
96             case you have to use an URL pointing to the file under 'authors/id').
97              
98             If the environment variable CPAN_MIRROR is set, it will be used instead of
99             'https://www.cpan.org'.
100              
101             =head1 WHY
102              
103             There are two use cases that motivated the creation of this module:
104              
105             =over
106              
107             =item 1. bundling various self written modules as a versioned release
108              
109             =item 2. quickly switching between different versions of a module for debugging purposes
110              
111             =back
112              
113             =head1 AUTHOR
114              
115             Thomas Kratz Etomk@cpan.orgE
116              
117             =cut
118              
119             my $cpan = $ENV{CPAN_MIRROR} || 'https://www.cpan.org';
120             my $rx_url = qr!^(?:CPAN|https?)://!;
121             my $tar = Archive::Tar->new();
122             my $home = $ENV{PERL_LIB_ARCHIVE_HOME} // glob('~');
123              
124             sub import {
125 6     6   67 my ( $class, @entries ) = @_;
126 6         11 my %cache;
127              
128 6         16 my $caller_file = (caller)[1];
129 6         20 my $under_debugger = defined($DB::single);
130 6   66     35 my $extract_dir = $ENV{PERL_LIB_ARCHIVE_EXTRACT} // "$home/.lib_archive_extract";
131              
132 6         12 for my $entry (@entries) {
133 9         59 my $is_url = $entry =~ /$rx_url/;
134 9 100       43 my $arcs
    100          
135             = $is_url ? _get_url($entry)
136             : ( $entry eq '__DATA__' ) ? _get_data($caller_file)
137             : _get_files( $entry, $caller_file );
138 9         22 for my $arc (@$arcs) {
139 15 100       39 my $path = $is_url ? $entry : $arc->[0];
140 15         540 my $base = basename($path);
141 15         96 my @ver = $base =~ /(v?\d+\.\d+(?:\.\d+)?)/gi;
142 15         20 my %tmp;
143 15         22 my $mod = 0;
144 15         21 my $lib = 0;
145 15         54 for my $f ( $tar->read( $arc->[0] ) ) {
146 75 100       67862 next unless ( my $full = $f->full_path ) =~ /\.pm$/;
147 36         922 my @parts = split( '/', $full );
148 36 100 33     90 ++$mod && shift @parts if $parts[0] eq $arc->[1];
149 36 100 33     79 ++$lib && shift @parts if $parts[0] eq 'lib';
150 36         77 my $rel = join( '/', @parts );
151 36         77 $tmp{$rel}{$full} = $f->get_content_by_ref;
152             }
153 15         158 for my $rel ( keys %tmp ) {
154 23 100       72 my $full = join( '/', $mod ? $arc->[1] : (), $lib ? 'lib' : (), $rel );
    100          
155 23   100     211 $cache{$rel} //= { path => "$path/$full", content => $tmp{$rel}{$full}, arcver => $ver[-1] // '' };
      100        
156             }
157             }
158             }
159              
160             unshift @INC, sub {
161 47     47   425469 my ( $cref, $rel ) = @_;
162 47 100       17322 return unless my $rec = $cache{$rel};
163             $INC{$rel} = _expand( $rel, $rec->{content}, $rec->{arcver}, $extract_dir )
164 14 50 66     101 if $ENV{PERL_LIB_ARCHIVE_EXTRACT} or $under_debugger;
165 14 50 0     47 $INC{$rel} //= $rec->{path} unless $under_debugger;
166 6 50   6   38 open( my $pfh, '<', $rec->{content} ) or croak $!;
  6         11  
  6         40  
  14         259  
167 14         4991 return $pfh;
168 6         106 };
169              
170 6         3398 return;
171             }
172              
173              
174             sub _get_files {
175 6     6   13 my ( $glob, $cfile ) = @_;
176 6         12 ( my $glob_ux = $glob ) =~ s!\\!/!g;
177 6         22 ( my $cdir = dirname( rel2abs($cfile) ) ) =~ s!\\!/!g;
178 6 50       533 $glob_ux = "$cdir/$glob_ux" unless file_name_is_absolute($glob_ux);
179 6         45 my @files;
180 6         354 for my $f ( sort glob($glob_ux) ) {
181 11         277 my ( $module, $dirs, $suffix ) = fileparse( $f, qr/\.tar\.gz/ );
182 11         42 push @files, [ $f, $module ];
183             }
184 6         19 return \@files;
185             }
186              
187              
188             sub _get_url {
189 2     2   4 my ($url) = @_;
190              
191 2         14 my ($module) = $url =~ m!/([^/]+)\.tar\.gz$!;
192 2         8 my ($top) = split( /-/, $module );
193              
194 2         9 $url =~ s!^CPAN://!$cpan/modules/by-module/$top/!;
195              
196 2         20 my $rp = HTTP::Tiny->new->get($url);
197              
198 2         338936 my @zips;
199 2 50       737 if ( $rp->{success} ) {
200 2         33 my $z = IO::Uncompress::Gunzip->new( \$rp->{content} );
201 2         4185 push @zips, [ $z, $module ];
202             }
203             else {
204 0         0 croak "GET '$url' failed with status:", $rp->{status};
205             }
206 2         21 return \@zips;
207             }
208              
209              
210             sub _get_data {
211 1     1   2 my ($cfn) = @_;
212 1 50       55 open( my $fh, '<', $cfn ) or croak "couldn't open $cfn, $!";
213 1         7 local $/ = undef;
214 1         21 my $data = <$fh>;
215 1         10 close($fh);
216 1         27 $data =~ s/^.*\n__DATA__\r?\n/\n/s;
217 1         9 my @data = split( /\n\n+/, $data );
218 1         2 my @tars;
219              
220 1         2 for my $d (@data) {
221 2         17 my $content = decode_base64($d);
222 2         5 my $z = eval { IO::Uncompress::Gunzip->new( \$content ) };
  2         15  
223 2 50       2716 if ($z) {
224 2         5 push @tars, [ $z, '' ];
225 2         5 next;
226             }
227 0 0       0 open( my $cfh, '<', \$content ) or croak $!; ## no critic (RequireBriefOpen)
228 0         0 push @tars, [ $cfh, '' ];
229             }
230 1         9 return \@tars;
231             }
232              
233              
234             sub _expand {
235 14     14   48 my ( $rel, $cref, $ver, $exdir ) = @_;
236 14 100       57 my $fn = $ver ? "$exdir/$ver/$rel" : "$exdir/$rel";
237 14         2298 make_path( dirname($fn) );
238 14 50       1027 open( my $fh, '>', $fn ) or die "couldn't save $fn, $!\n";
239 14         248 print $fh $$cref;
240 14         703 close($fh);
241 14         102 return $fn;
242             }
243              
244              
245             1;