File Coverage

blib/lib/lib/archive.pm
Criterion Covered Total %
statement 66 67 98.5
branch 18 20 90.0
condition 4 8 50.0
subroutine 11 11 100.0
pod n/a
total 99 106 93.4


line stmt bran cond sub pod time code
1             package lib::archive;
2              
3 3     3   411382 use strict;
  3         14  
  3         80  
4 3     3   15 use warnings;
  3         6  
  3         86  
5              
6 3     3   57 use 5.010001;
  3         11  
7              
8 3     3   15 use Carp qw(croak);
  3         5  
  3         159  
9 3     3   1855 use Archive::Tar;
  3         251237  
  3         219  
10 3     3   1588 use File::Spec::Functions qw(file_name_is_absolute rel2abs);
  3         2226  
  3         184  
11 3     3   21 use File::Basename qw(dirname fileparse);
  3         6  
  3         2710  
12              
13             our $VERSION = "0.5";
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         33 my %cache;
105              
106 3         16 ( my $acdir = dirname( rel2abs( (caller)[1] ) ) ) =~ s!\\!/!g;
107              
108 3         478 for my $entry (@_) {
109 5         36 my $is_url = $entry =~ /$is_url/;
110 5 100       22 my $arcs = $is_url ? _get_url($entry) : _get_files( $entry, $acdir );
111 5         12 for my $arc (@$arcs) {
112 7 100       23 my $path = $is_url ? $entry : $arc->[0];
113 7         11 my %tmp;
114 7         8 my $mod = 0;
115 7         11 my $lib = 0;
116 7         34 for my $f ( $tar->read( $arc->[0] ) ) {
117 43 100       39502 next unless ( my $full = $f->full_path ) =~ /\.pm$/;
118 15         408 my @parts = split( '/', $full );
119 15 100 33     45 ++$mod && shift @parts if $parts[0] eq $arc->[1];
120 15 100 33     40 ++$lib && shift @parts if $parts[0] eq 'lib';
121 15         34 my $rel = join( '/', @parts );
122 15         31 $tmp{$rel}{$full} = $f->get_content_by_ref;
123             }
124 7         82 for my $rel ( keys %tmp ) {
125 10 100       38 my $full = join( '/', $mod ? $arc->[1] : (), $lib ? 'lib' : (), $rel );
    100          
126 10   100     85 $cache{$rel} //= { path => "$path/$full", content => $tmp{$rel}{$full} };
127             }
128             }
129             }
130             unshift @INC, sub {
131 25     25   190274 my ( $cref, $rel ) = @_;
132 25 100       9091 return unless my $rec = $cache{$rel};
133 8         17 $INC{$rel} = $rec->{path};
134 8         1571 return $rec->{content};
135 3         2255 };
136             }
137              
138              
139             sub _get_files {
140 3     3   8 my ( $glob, $cdir ) = @_;
141 3         7 ( my $glob_ux = $glob ) =~ s!\\!/!g;
142 3 50       9 $glob_ux = "$cdir/$glob_ux" unless file_name_is_absolute($glob_ux);
143 3         26 my @files;
144 3         198 for my $f ( sort glob($glob_ux) ) {
145 5         144 my ( $module, $dirs, $suffix ) = fileparse( $f, qr/\.tar\.gz/ );
146 5         25 push @files, [ $f, $module ];
147             }
148 3         9 return \@files;
149             }
150              
151              
152             sub _get_url {
153 2     2   5 my ($url) = @_;
154              
155 2         13 my ($module) = $url =~ m!/([^/]+)\.tar\.gz$!;
156 2         8 my ($top) = split( /-/, $module );
157              
158 2         10 $url =~ s!^CPAN://!$cpan/modules/by-module/$top/!;
159              
160 2         18 require HTTP::Tiny;
161 2         17 require IO::Uncompress::Gunzip;
162              
163 2         17 my $rp = HTTP::Tiny->new->get($url);
164              
165 2         232405 my @zips;
166 2 50       1242 if ( $rp->{success} ) {
167 2         29 my $z = IO::Uncompress::Gunzip->new( \$rp->{content} );
168 2         4392 push @zips, [ $z, $module ];
169             }
170             else {
171 0         0 croak "GET '$url' failed with status:", $rp->{status};
172             }
173 2         17 return \@zips;
174             }
175              
176              
177             1;