File Coverage

blib/lib/lib/archive.pm
Criterion Covered Total %
statement 70 71 98.5
branch 18 20 90.0
condition 4 8 50.0
subroutine 12 12 100.0
pod n/a
total 104 111 93.6


line stmt bran cond sub pod time code
1             package lib::archive;
2              
3 3     3   342567 use strict;
  3         12  
  3         76  
4 3     3   14 use warnings;
  3         4  
  3         69  
5              
6 3     3   46 use 5.010001;
  3         12  
7              
8 3     3   13 use Carp qw(croak);
  3         6  
  3         175  
9 3     3   1834 use Archive::Tar;
  3         262024  
  3         233  
10 3     3   1690 use File::Spec::Functions qw(file_name_is_absolute rel2abs);
  3         2234  
  3         185  
11 3     3   19 use File::Basename qw(dirname fileparse);
  3         4  
  3         2925  
12              
13             our $VERSION = "0.7";
14              
15             =pod
16              
17             =head1 NAME
18              
19             lib::archive - load pure-Perl modules directly from TAR archives
20              
21             =head1 SYNOPSIS
22              
23             use lib::archive ("../external/*.tgz", "lib/extra.tar");
24              
25             use MyModule; # the given tar archives will be searched first
26              
27             or
28              
29             use lib::archive qw(
30             https://www.cpan.org/modules/by-module/JSON/JSON-PP-2.97001.tar.gz
31             CPAN://YAML-PP-0.007.tar.gz
32             );
33              
34             use JSON::PP;
35             use YAML::PP;
36              
37             =head1 DESCRIPTION
38              
39             Specify TAR archives to directly load modules from. The TAR files will be
40             searched like include dirs. Globs are expanded, so you can use wildcards
41             (not for URLs). If modules are present in more than one TAR archive, the
42             first one will be used.
43              
44             Relative paths will be interpreted as relative to the directory the
45             calling script or module resides in. So don't do a chdir() before using
46             lib::archive when you call your script with a relative path B use releative
47             paths for lib::archive.
48              
49             B
50             extracted on the fly>.
51              
52             You can use every file format Archive::Tar supports.
53              
54             If the archive contains a toplevel directory 'lib' the module search path
55             will start there. Otherwise it will start from the root of the archive.
56              
57             If the archive is a gzipped TAR archive with the extension '.tar.gz' and the
58             archive contains a toplevel directory matching the archive name without the
59             extension the module search path starts with this directory. The above
60             rule for the subdirectory 'lib' applies from there. This means that e.g. for
61             'JSON-PP-2.97001.tar.gz' the modules will only be included from
62             'JSON-PP-2.97001/lib'.
63              
64             You can use URLs for loading modules directly from CPAN. Either specify the
65             complete URL like:
66              
67             use lib::archive 'https://www.cpan.org/modules/by-module/JSON/JSON-PP-2.97001.tar.gz';
68              
69             or use a shortcut like:
70              
71             use lib::archive 'CPAN://JSON-PP-2.97001.tar.gz';
72              
73             which will do exactly the same thing (at least in most cases: there seem to
74             be modules without an entry under 'modules/by-module/'; in that
75             case you have to use an URL pointing to the file under 'authors/id').
76              
77             If the environment variable CPAN_MIRROR is set, it will be used instead of
78             'https://www.cpan.org'.
79              
80             =head1 WHY
81              
82             There are two use cases that motivated the creation of this module:
83              
84             =over
85              
86             =item 1. bundling various self written modules as a versioned release
87              
88             =item 2. quickly switching between different versions of a module for debugging purposes
89              
90             =back
91              
92             =head1 AUTHOR
93              
94             Thomas Kratz Etomk@cpan.orgE
95              
96             =cut
97              
98             my $cpan = $ENV{CPAN_MIRROR} || 'https://www.cpan.org';
99             my $is_url = qr!^(?:CPAN|https?)://!;
100             my $tar = Archive::Tar->new();
101              
102             sub import {
103 3     3   26 my $class = shift;
104 3         24 my %cache;
105              
106 3         13 ( my $acdir = dirname( rel2abs( (caller)[1] ) ) ) =~ s!\\!/!g;
107              
108 3         316 for my $entry (@_) {
109 5         27 my $is_url = $entry =~ /$is_url/;
110 5 100       20 my $arcs = $is_url ? _get_url($entry) : _get_files( $entry, $acdir );
111 5         14 for my $arc (@$arcs) {
112 7 100       247 my $path = $is_url ? $entry : $arc->[0];
113 7         11 my %tmp;
114 7         13 my $mod = 0;
115 7         9 my $lib = 0;
116 7         31 for my $f ( $tar->read( $arc->[0] ) ) {
117 43 100       39286 next unless ( my $full = $f->full_path ) =~ /\.pm$/;
118 15         368 my @parts = split( '/', $full );
119 15 100 33     40 ++$mod && shift @parts if $parts[0] eq $arc->[1];
120 15 100 33     36 ++$lib && shift @parts if $parts[0] eq 'lib';
121 15         30 my $rel = join( '/', @parts );
122 15         27 $tmp{$rel}{$full} = $f->get_content_by_ref;
123             }
124 7         81 for my $rel ( keys %tmp ) {
125 10 100       36 my $full = join( '/', $mod ? $arc->[1] : (), $lib ? 'lib' : (), $rel );
    100          
126 10   100     80 $cache{$rel} //= { path => "$path/$full", content => $tmp{$rel}{$full} };
127             }
128             }
129             }
130             unshift @INC, sub {
131 28     28   196559 my ( $cref, $rel ) = @_;
132 28 100       10977 return unless my $rec = $cache{$rel};
133 8         21 $INC{$rel} = $rec->{path};
134 3     3   18 open(my $fh, '<', $rec->{content});
  3         5  
  3         17  
  8         150  
135 8         2612 return $fh;
136 3         1705 };
137             }
138              
139              
140             sub _get_files {
141 3     3   6 my ( $glob, $cdir ) = @_;
142 3         6 ( my $glob_ux = $glob ) =~ s!\\!/!g;
143 3 50       9 $glob_ux = "$cdir/$glob_ux" unless file_name_is_absolute($glob_ux);
144 3         23 my @files;
145 3         174 for my $f ( sort glob($glob_ux) ) {
146 5         165 my ( $module, $dirs, $suffix ) = fileparse( $f, qr/\.tar\.gz/ );
147 5         25 push @files, [ $f, $module ];
148             }
149 3         7 return \@files;
150             }
151              
152              
153             sub _get_url {
154 2     2   4 my ($url) = @_;
155              
156 2         11 my ($module) = $url =~ m!/([^/]+)\.tar\.gz$!;
157 2         7 my ($top) = split( /-/, $module );
158              
159 2         8 $url =~ s!^CPAN://!$cpan/modules/by-module/$top/!;
160              
161 2         12 require HTTP::Tiny;
162 2         13 require IO::Uncompress::Gunzip;
163              
164 2         13 my $rp = HTTP::Tiny->new->get($url);
165              
166 2         156948 my @zips;
167 2 50       895 if ( $rp->{success} ) {
168 2         26 my $z = IO::Uncompress::Gunzip->new( \$rp->{content} );
169 2         5966 push @zips, [ $z, $module ];
170             }
171             else {
172 0         0 croak "GET '$url' failed with status:", $rp->{status};
173             }
174 2         16 return \@zips;
175             }
176              
177              
178             1;