File Coverage

blib/lib/lib/archive.pm
Criterion Covered Total %
statement 116 119 97.4
branch 36 46 78.2
condition 18 28 64.2
subroutine 19 19 100.0
pod n/a
total 189 212 89.1


line stmt bran cond sub pod time code
1             package lib::archive;
2              
3 7     7   1958566 use strict;
  7         30  
  7         278  
4 7     7   46 use warnings;
  7         22  
  7         516  
5              
6 7     7   156 use 5.010001;
  7         34  
7              
8 7     7   51 use Carp qw(croak);
  7         21  
  7         585  
9 7     7   5701 use Archive::Tar;
  7         884813  
  7         655  
10 7     7   4277 use File::Spec::Functions qw(file_name_is_absolute rel2abs);
  7         6576  
  7         803  
11 7     7   102 use File::Basename qw(basename dirname fileparse);
  7         12  
  7         669  
12 7     7   59 use File::Path qw(make_path);
  7         18  
  7         529  
13 7     7   4465 use MIME::Base64 qw(decode_base64);
  7         6544  
  7         612  
14 7     7   56 use IO::Uncompress::Gunzip;
  7         21  
  7         303  
15 7     7   5260 use HTTP::Tiny;
  7         313323  
  7         14931  
16              
17             our $VERSION = "0.94";
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             For temporary excluding modules from being loaded via lib::archive the
102             environment variable PERL_LIB_ARCHIVE_IGNORE can be set to a regular expression.
103             It will be matched against the relative pathname of the modules (e.g. C).
104             A leading C will be split off and invert the match.
105              
106             =head1 WHY
107              
108             There are two use cases that motivated the creation of this module:
109              
110             =over
111              
112             =item 1. bundling various self written modules as a versioned release
113              
114             =item 2. quickly switching between different versions of a module for debugging purposes
115              
116             =back
117              
118             =head1 AUTHOR
119              
120             Thomas Kratz Etomk@cpan.orgE
121              
122             =cut
123              
124             my $cpan = $ENV{CPAN_MIRROR} || 'https://www.cpan.org';
125             my $rx_url = qr!^(?:CPAN|https?)://!;
126             my $tar = Archive::Tar->new();
127             my $home = $ENV{PERL_LIB_ARCHIVE_HOME} // glob('~');
128              
129              
130             sub import {
131 8     8   1039 my ( $class, @entries ) = @_;
132 8         18 my %cache;
133              
134 8         40 my $caller_file = (caller)[1];
135 8         71 my $under_debugger = defined($DB::single);
136 8   66     72 my $extract_dir = $ENV{PERL_LIB_ARCHIVE_EXTRACT} // "$home/.lib_archive_extract";
137 8         27 my $ignore = _get_ignore_sub();
138 8   66     45 my $under_cover = defined($Devel::Cover::VERSION) && !$ENV{PERL_LIB_ARCHIVE_TESTING};
139              
140 8         26 for my $entry (@entries) {
141 11         80 my $is_url = $entry =~ /$rx_url/;
142 11 100       68 my $arcs
    100          
143             = $is_url ? _get_url($entry)
144             : ( $entry eq '__DATA__' ) ? _get_data($caller_file)
145             : _get_files( $entry, $caller_file );
146 11         33 for my $arc (@$arcs) {
147 17 100       61 my $path = $is_url ? $entry : $arc->[0];
148 17         916 my $base = basename($path);
149 17         156 my @ver = $base =~ /(v?\d+\.\d+(?:\.\d+)?)/gi;
150 17         50 my %tmp;
151 17         33 my $mod = 0;
152 17         30 my $lib = 0;
153 17         99 for my $f ( $tar->read( $arc->[0] ) ) {
154 95 100       136481 next unless ( my $full = $f->full_path ) =~ /\.pm$/;
155 46         1555 my @parts = split( '/', $full );
156 46 100 33     185 ++$mod && shift @parts if $parts[0] eq $arc->[1];
157 46 100 33     135 ++$lib && shift @parts if $parts[0] eq 'lib';
158 46         108 my $rel = join( '/', @parts );
159 46         150 $tmp{$rel}{$full} = $f->get_content_by_ref;
160             }
161 17         607 for my $rel ( keys %tmp ) {
162 27 100       136 my $full = join( '/', $mod ? $arc->[1] : (), $lib ? 'lib' : (), $rel );
    100          
163 27   100     387 $cache{$rel} //= { path => "$path/$full", content => $tmp{$rel}{$full}, arcver => $ver[-1] // '' };
      100        
164             }
165             }
166             }
167              
168             unshift @INC, sub {
169 100     100   650162 my ( $cref, $rel ) = @_;
170 100 100 100     486 return if $ignore and $ignore->($rel);
171 98 100       79970 return unless my $rec = $cache{$rel};
172             $INC{$rel} = _expand( $rel, $rec->{content}, $rec->{arcver}, $extract_dir )
173             if $ENV{PERL_LIB_ARCHIVE_EXTRACT}
174 16 100 66     186 or ( $under_debugger and not $under_cover );
      100        
175 16 50 0     92 $INC{$rel} //= $rec->{path} unless $under_debugger;
176 16 50       278 open( my $pfh, '<', $rec->{content} ) or croak $!;
177 16         5777 return $pfh;
178 8         158 };
179              
180 8         308448 return;
181             }
182              
183              
184             sub _get_files {
185 8     8   21 my ( $glob, $cfile ) = @_;
186 8         22 ( my $glob_ux = $glob ) =~ s!\\!/!g;
187 8         38 ( my $cdir = dirname( rel2abs($cfile) ) ) =~ s!\\!/!g;
188 8 50       853 $glob_ux = "$cdir/$glob_ux" unless file_name_is_absolute($glob_ux);
189 8         68 my @files;
190 8         821 for my $f ( sort glob($glob_ux) ) {
191 13         495 my ( $module, $dirs, $suffix ) = fileparse( $f, qr/\.tar\.gz/ );
192 13         74 push @files, [ $f, $module ];
193             }
194 8         29 return \@files;
195             }
196              
197              
198             sub _get_url {
199 2     2   5 my ($url) = @_;
200              
201 2         36 my ($module) = $url =~ m!/([^/]+)\.tar\.gz$!;
202 2         9 my ($top) = split( /-/, $module );
203              
204 2         12 $url =~ s!^CPAN://!$cpan/modules/by-module/$top/!;
205              
206 2         24 my $rp = HTTP::Tiny->new->get($url);
207              
208 2         349985 my @zips;
209 2 50       4778 if ( $rp->{success} ) {
210 2         39 my $z = IO::Uncompress::Gunzip->new( \$rp->{content} );
211 2         13907 push @zips, [ $z, $module ];
212             }
213             else {
214 0         0 croak "GET '$url' failed with status:", $rp->{status};
215             }
216 2         33 return \@zips;
217             }
218              
219              
220             sub _get_data {
221 1     1   3 my ($cfn) = @_;
222 1 50       55 open( my $fh, '<', $cfn ) or croak "couldn't open $cfn, $!";
223 1         6 local $/ = undef;
224 1         34 my $data = <$fh>;
225 1         11 close($fh);
226 1         37 $data =~ s/^.*\n__DATA__\r?\n/\n/s;
227 1         10 my @data = split( /\n\n+/, $data );
228 1         2 my @tars;
229              
230 1         3 for my $d (@data) {
231 2         25 my $content = decode_base64($d);
232 2         5 my $z = eval { IO::Uncompress::Gunzip->new( \$content ) };
  2         22  
233 2 50       4022 if ($z) {
234 2         8 push @tars, [ $z, '' ];
235 2         14 next;
236             }
237 0 0       0 open( my $cfh, '<', \$content ) or croak $!; ## no critic (RequireBriefOpen)
238 0         0 push @tars, [ $cfh, '' ];
239             }
240 1         10 return \@tars;
241             }
242              
243              
244             sub _expand {
245 8     8   34 my ( $rel, $cref, $ver, $exdir ) = @_;
246 8 100       38 my $fn = $ver ? "$exdir/$ver/$rel" : "$exdir/$rel";
247 8         6512 make_path( dirname($fn) );
248 8 50       13050 open( my $fh, '>', $fn ) or die "couldn't save $fn, $!\n";
249 8         218 print $fh $$cref;
250 8         587 close($fh);
251 8         112 return $fn;
252             }
253              
254              
255             sub _get_ignore_sub {
256 8 100   8   33 return unless my $txt = $ENV{PERL_LIB_ARCHIVE_IGNORE};
257 2         11 my ( $rx, $neg ) = reverse( split( /^(!)/, $txt ) );
258 2 50       5 return unless $rx;
259 2         26 $rx = qr($rx);
260 2 100   5   10 return sub { $neg ? $_[0] !~ /$rx/ : $_[0] =~ /$rx/ };
  5         733  
261             }
262              
263              
264             1;