File Coverage

blib/lib/Debian/AptContents.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Debian::AptContents;
2              
3 13     13   2048018 use strict;
  13         49  
  13         420  
4 13     13   76 use warnings;
  13         43  
  13         1028  
5              
6             our $VERSION = '0.77';
7              
8             =head1 NAME
9              
10             Debian::AptContents - parse/search through apt-file's Contents files
11              
12             =head1 SYNOPSIS
13              
14             my $c = Debian::AptContents->new( { homedir => '~/.dh-make-perl' } );
15             my @pkgs = $c->find_file_packages('/usr/bin/foo');
16             my $dep = $c->find_perl_module_package('Foo::Bar');
17              
18             =head1 TODO
19              
20             This needs to really work not only for Perl modules.
21              
22             A module specific to Perl modules is needed by dh-make-perl, but it can
23             subclass Debian::AptContents, which needs to become more generic.
24              
25             =cut
26              
27 13     13   131 use base qw(Class::Accessor);
  13         19  
  13         3818  
28             __PACKAGE__->mk_accessors(
29             qw(
30             cache homedir cache_file contents_dir contents_files verbose
31             source sources dist
32             )
33             );
34              
35 13     13   6434 use Config;
  13         28  
  13         1314  
36 13     13   9265 use Debian::Dependency;
  0            
  0            
37             use DhMakePerl::Utils qw(find_core_perl_dependency);
38             use File::Spec::Functions qw( catfile catdir splitpath );
39             use IO::Uncompress::Gunzip;
40             use List::MoreUtils qw(uniq);
41             use Module::CoreList ();
42             use Storable;
43             use AptPkg::Config;
44              
45             $AptPkg::Config::_config->init();
46              
47             our $oldstable_perl = '5.10.1';
48              
49             =head1 CONSTRUCTOR
50              
51             =over
52              
53             =item new
54              
55             Constructs new instance of the class. Expects at least C option.
56              
57             =back
58              
59             =head1 FIELDS
60              
61             =over
62              
63             =item homedir
64              
65             (B) Directory where the object stores its cache.
66              
67             =item contents_dir
68              
69             Directory where L stores Contents files are stored. Default is
70             F
71              
72             =item sources
73              
74             A path to a F file or an array ref of paths to sources.list
75             files. If not given uses AptPkg's Config to get the list.
76              
77             =item dist
78              
79             Used for filtering on the C part of the repository paths listed in
80             L. Default is empty, meaning no filtering.
81              
82             =item contents_files
83              
84             Arrayref of F file names. Default is to parse the files in C
85             and to look in C for matching files.
86              
87             =item cache_file
88              
89             Path to the file with cached parsed information from all F files.
90             Default is F under C.
91              
92             =item cache
93              
94             Filled by C. Used by C and (obviously)
95             C
96              
97             =item verbose
98              
99             Verbosity level. 0 means silent, the bigger the more the jabber. Default is 1.
100              
101             =back
102              
103             =cut
104              
105             sub new {
106             my $class = shift;
107             $class = ref($class) if ref($class);
108             my $self = $class->SUPER::new(@_);
109              
110             # required options
111             $self->homedir
112             or die "No homedir given";
113              
114             # some defaults
115             $self->contents_dir('/var/cache/apt/apt-file')
116             unless $self->contents_dir;
117             $self->sources( [ $self->sources ] )
118             if $self->sources and not ref( $self->sources );
119             $self->sources(
120             [ $AptPkg::Config::_config->get_file('Dir::Etc::sourcelist'),
121             glob(
122             $AptPkg::Config::_config->get_dir('Dir::Etc::sourceparts')
123             . '/*.list'
124             )
125             ]
126             ) unless defined( $self->sources );
127             $self->contents_files( $self->get_contents_files )
128             unless $self->contents_files;
129             $self->cache_file( catfile( $self->homedir, 'Contents.cache' ) )
130             unless $self->cache_file;
131             $self->verbose(1) unless defined( $self->verbose );
132              
133             $self->read_cache();
134              
135             return $self;
136             }
137              
138             =head1 OBJECT METHODS
139              
140             =over
141              
142             =item warning
143              
144             Used internally. Given a verbosity level and a message, prints the message to
145             STDERR if the verbosity level is greater than or equal of the value of
146             C.
147              
148             =cut
149              
150             sub warning {
151             my ( $self, $level, $msg ) = @_;
152              
153             warn "$msg\n" if $self->verbose >= $level;
154             }
155              
156             =item repo_source_to_contents_paths
157              
158             Given a line with Debian package repository path (typically taken from
159             F), converts it to the corresponding F file names.
160              
161             =cut
162              
163             sub repo_source_to_contents_paths {
164             my ( $self, $source ) = @_;
165              
166             # Weed out options in brackets first
167             $source =~ s/\[[^][]+\]//;
168              
169             my ( $schema, $uri, $dist, @components ) = split /\s+/, $source;
170             my ( $proto, $host, $port, $dir ) = $uri =~ m{
171             ^
172             (?:([^:/?\#]+):)? # proto
173             (?://
174             (?:[^:]+:[^@]+@)? # username:password@
175             ([^:/?\#]*) # host
176             (?::(\d+))? # port
177             )?
178             ([^?\#]*) # path
179             }x;
180              
181             unless ( defined $schema ) {
182             $self->warning( 1, "'$_' has unknown format" );
183             next;
184             }
185              
186             return unless $schema eq 'deb';
187              
188             if ( $self->dist ) {
189             if ( $self->dist =~ /^\s*{\s*(.+)\s*}\s*$/ ) {
190             return unless grep {/^$dist$/} split( /\s*,\s*/, $1 );
191             }
192             else {
193             return if $dist ne $self->dist;
194             }
195             }
196              
197             $host ||= ''; # set empty string if $host is undef
198             $dir ||= ''; # deb http://there sid main
199              
200             s{/$}{} for ( $host, $dir, $dist ); # remove trailing /
201             s{^/}{} for ( $host, $dir, $dist ); # remove initial /
202             s{/}{_}g for ( $host, $dir, $dist ); # replace remaining /
203              
204             # Make sure to generate paths both with and without components to
205             # be compatible with both old and new apt-file versions. See:
206             # https://bugs.launchpad.net/ubuntu/+source/dh-make-perl/+bug/1034881
207             push(@components, '');
208              
209             return map
210             { $host . "_" . join( "_", grep( { defined and length } $dir, "dists", $dist, $_ ) ) }
211             @components;
212             }
213              
214             =item get_contents_files
215              
216             Reads F, gives the repository paths to
217             C and returns an arrayref of file names of
218             Contents files.
219              
220             =cut
221              
222             sub get_contents_files {
223             my $self = shift;
224              
225             my $archspec = `dpkg --print-architecture`;
226             chomp($archspec);
227              
228             my @res;
229              
230             for my $s ( @{ $self->sources } ) {
231             # by default ->sources contains a list of files that APT would look
232             # at. Some of them may not exist, so do not fail if this is the case
233             next unless -e $s;
234              
235             my $src = IO::File->new( $s, 'r' )
236             or die "Unable to open '$s': $!\n";
237              
238             while (<$src>) {
239             chomp;
240             s/#.*//;
241             s/^\s+//;
242             s/\s+$//;
243             next unless $_;
244              
245             for my $path ( $self->repo_source_to_contents_paths($_) ) {
246             # try all of with/out architecture and
247             # un/compressed
248             for my $a ( '', "-$archspec" ) {
249             for my $c ( '', '.gz' ) {
250             my $f = catfile( $self->contents_dir,
251             "${path}_Contents$a$c", );
252             push @res, $f if -e $f;
253             }
254             }
255             }
256             }
257             }
258              
259             return [ uniq sort @res ];
260             }
261              
262             =item read_cache
263              
264             Reads the cached parsed F files. If there are F files with
265             more recent mtime than that of the cache (or if there is no cache at all),
266             parses all F and stores the cache via C for later
267             invocation.
268              
269             =cut
270              
271             sub read_cache {
272             my $self = shift;
273              
274             my $cache;
275              
276             if ( -r $self->cache_file ) {
277             $cache = eval { Storable::retrieve( $self->cache_file ) };
278             undef($cache) unless ref($cache) and ref($cache) eq 'HASH';
279             }
280              
281             # see if the cache is stale
282             if ( $cache and $cache->{stamp} and $cache->{contents_files} ) {
283             undef($cache)
284             unless join( '><', @{ $self->contents_files } ) eq
285             join( '><', @{ $cache->{contents_files} } );
286              
287             # file lists are the same?
288             # see if any of the files has changed since we
289             # last read it
290             if ($cache) {
291             for ( @{ $self->contents_files } ) {
292             if ( ( stat($_) )[9] > $cache->{stamp} ) {
293             undef($cache);
294             last;
295             }
296             }
297             }
298             }
299             else {
300             undef($cache);
301             }
302              
303             unless ($cache) {
304             $self->source('parsed files');
305             $cache->{stamp} = time;
306             $cache->{contents_files} = [];
307             $cache->{apt_contents} = {};
308             for ( @{ $self->contents_files } ) {
309             push @{ $cache->{contents_files} }, $_;
310             my $f
311             = /\.gz$/
312             ? IO::Uncompress::Gunzip->new($_)
313             : IO::File->new( $_, 'r' );
314              
315             unless ($f) {
316             warn "Error reading '$_': $!\n";
317             next;
318             }
319              
320             $self->warning( 1, "Parsing $_ ..." );
321             my $capturing = 0;
322             my $line;
323             while ( defined( $line = $f->getline ) ) {
324             if ($capturing) {
325             my ( $file, $packages ) = split( /\s+/, $line );
326             next unless $file =~ s{
327             ^usr/
328             (?:share|lib)/
329             (?:perl\d+/ # perl5/
330             | perl/(?:\d[\d.]+)/ # or perl/5.10/
331             )
332             }{}x;
333             $cache->{apt_contents}{$file} = exists $cache->{apt_contents}{$file}
334             ? $cache->{apt_contents}{$file}.','.$packages
335             : $packages;
336              
337             # $packages is a comma-separated list of
338             # section/package items. We'll parse it when a file
339             # matches. Otherwise we'd parse thousands of entries,
340             # while checking only a couple
341             }
342             else {
343             $capturing = 1 if $line =~ /^FILE\s+LOCATION/;
344             }
345             }
346             }
347              
348             if ( %{ $cache->{apt_contents} } ) {
349             $self->cache($cache);
350             $self->store_cache;
351             }
352             }
353             else {
354             $self->source('cache');
355             $self->warning( 1,
356             "Using cached Contents from " . localtime( $cache->{stamp} ) );
357              
358             $self->cache($cache);
359             }
360             }
361              
362             =item store_cache
363              
364             Writes the contents of the parsed C to the C.
365              
366             Storable is used to stream the data. Along with the information from
367             F files, a time stamp is stored.
368              
369             =cut
370              
371             sub store_cache {
372             my $self = shift;
373              
374             my ( $vol, $dir, $file ) = splitpath( $self->cache_file );
375              
376             $dir = catdir( $vol, $dir );
377             unless ( -d $dir ) {
378             mkdir $dir
379             or die "Error creating directory '$dir': $!\n";
380             }
381              
382             Storable::nstore( $self->cache, $self->cache_file . '-new' );
383             rename( $self->cache_file . '-new', $self->cache_file );
384             }
385              
386             =item find_file_packages
387              
388             Returns a list of packages where the given file was found.
389              
390             F files store the package section together with package name. That is
391             stripped.
392              
393             Returns an empty list of the file is not found in any package.
394              
395             =cut
396              
397             sub find_file_packages {
398             my ( $self, $file ) = @_;
399              
400             my $packages = $self->cache->{apt_contents}{$file};
401              
402             return () unless $packages;
403              
404             my @packages = split( /,/, $packages ); # Contents contains a
405             # comma-delimited list
406             # of packages
407              
408             s{.+/}{} for @packages; # remove section. Greedy on purpose
409             # otherwise it won't strip enough off Ubuntu's
410             # usr/share/perl5/Config/Any.pm universe/perl/libconfig-any-perl
411              
412             # in-core dependencies are given by find_core_perl_dependency
413             @packages = grep {
414             ( $_ ne 'perl-base' )
415             and ( $_ ne 'perl' )
416             and ( $_ ne 'perl-modules' )
417             } @packages;
418              
419             return uniq @packages;
420             }
421              
422             =item find_perl_module_package( $module, $version )
423              
424             Given Perl module name (e.g. Foo::Bar), returns a L object
425             representing the required Debian package and version. If the module is a core
426             one, suitable dependency on perl is returned.
427              
428             If the package is also available in a separate package, an alternative
429             dependency is returned.
430              
431             In case the version of the currently running Perl interpreter is lower than the
432             version in which the wanted module is available in core, the separate package
433             is preferred. Otherwise the perl dependency is the first alternative.
434              
435             =cut
436              
437             sub find_perl_module_package {
438             my ( $self, $module, $version ) = @_;
439              
440             # see if the module is included in perl core
441             my $core_dep = find_core_perl_dependency( $module, $version );
442              
443             # try module packages
444             my $module_file = $module;
445             $module_file =~ s|::|/|g;
446              
447             my @matches = $self->find_file_packages("$module_file.pm");
448              
449             # rank non -perl packages lower
450             @matches = sort {
451             if ( $a !~ /-perl$/ ) { return 1; }
452             elsif ( $b !~ /-perl$/ ) { return -1; }
453             else { return $a cmp $b; } # or 0?
454             } @matches;
455              
456             # we don't want perl, perl-base and perl-modules here
457             @matches = grep { !/^perl(?:-(?:base|modules))?$/ } @matches;
458              
459             my $direct_dep;
460             $direct_dep = Debian::Dependency->new(
461             ( @matches > 1 )
462             ? [ map ( { pkg => $_, rel => '>=', ver => $version }, @matches ) ]
463             : ( $matches[0], $version )
464             ) if @matches;
465              
466             my $running_perl = $Config::Config{version};
467              
468             if ($core_dep) {
469              
470             # the core dependency is satosfied by oldstable?
471             if ( $core_dep->ver <= $oldstable_perl ) {
472             # drop the direct dependency and remove the version
473             undef($direct_dep);
474              
475             $core_dep->ver(undef);
476             $core_dep->rel(undef);
477             }
478              
479             if ($direct_dep) {
480             # both in core and in a package.
481             if( $running_perl >= $core_dep->ver ) {
482             return Debian::Dependency->new("$core_dep | $direct_dep");
483             }
484             else {
485             return Debian::Dependency->new("$direct_dep | $core_dep");
486             }
487             }
488             else {
489             # only in core
490             return $core_dep;
491             }
492             }
493             else {
494             # maybe in a package
495             return $direct_dep;
496             }
497             }
498              
499             1;
500              
501             =back
502              
503             =head1 AUTHOR
504              
505             =over 4
506              
507             =item Damyan Ivanov
508              
509             =back
510              
511             =head1 COPYRIGHT & LICENSE
512              
513             =over 4
514              
515             =item Copyright (C) 2008, 2009, 2010 Damyan Ivanov
516              
517             =back
518              
519             This program is free software; you can redistribute it and/or modify it under
520             the terms of the GNU General Public License version 2 as published by the Free
521             Software Foundation.
522              
523             This program is distributed in the hope that it will be useful, but WITHOUT ANY
524             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
525             PARTICULAR PURPOSE. See the GNU General Public License for more details.
526              
527             You should have received a copy of the GNU General Public License along with
528             this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
529             Street, Fifth Floor, Boston, MA 02110-1301 USA.
530              
531             =cut