File Coverage

blib/lib/ORDB/CPANMeta/Generator.pm
Criterion Covered Total %
statement 132 159 83.0
branch 19 40 47.5
condition 1 3 33.3
subroutine 27 28 96.4
pod 4 7 57.1
total 183 237 77.2


line stmt bran cond sub pod time code
1             package ORDB::CPANMeta::Generator;
2              
3             =pod
4              
5             =head1 NAME
6              
7             ORDB::CPANMeta::Generator - Generator for the CPAN Meta database
8              
9             =head1 DESCRIPTION
10              
11             This is the module that is used to generate the "CPAN Meta" database.
12              
13             For more information, and to access this database as a consumer, see
14             the L module.
15              
16             The bulk of the work done in this module is actually achieved with:
17              
18             L - Fetching the index and dist tarballs
19              
20             L - Expanding and processing the tarballs
21              
22             L - Preparing the SQLite database for distribution
23              
24             =head1 METHODS
25              
26             =cut
27              
28 2     2   33680 use 5.008005;
  2         8  
  2         81  
29 2     2   13 use strict;
  2         3  
  2         192  
30 2     2   11 use Carp ();
  2         13  
  2         36  
31 2     2   11 use File::Spec 3.29 ();
  2         37  
  2         39  
32 2     2   11 use File::Path 2.07 ();
  2         44  
  2         46  
33 2     2   656913 use File::Remove 1.42 ();
  2         2237  
  2         45  
34 2     2   503434 use File::HomeDir 0.86 ();
  2         18445  
  2         68  
35 2     2   23 use File::Basename 0 ();
  2         57  
  2         49  
36 2     2   7285 use Module::CoreList 2.46 ();
  2         105514  
  2         1284  
37 2     2   2383 use Parse::CPAN::Meta 1.4200 ();
  2         2666  
  2         50  
38 2     2   1836 use Params::Util 1.00 ();
  2         9149  
  2         58  
39 2     2   2664 use Getopt::Long 2.34 ();
  2         27255  
  2         131  
40 2     2   5489 use DBI 1.609 ();
  2         38072  
  2         110  
41 2     2   2297 use CPAN::Meta 2.112621 ();
  2         1185947  
  2         63  
42 2     2   2237 use CPAN::Mini 0.576 ();
  2         353180  
  2         131  
43 2     2   2411 use CPAN::Mini::Visit 1.14 ();
  2         429824  
  2         74  
44 2     2   2180 use Xtract::Publish 0.12 ();
  2         157156  
  2         146  
45              
46             our $VERSION = '0.12';
47              
48 2         18 use Object::Tiny 1.06 qw{
49             minicpan
50             sqlite
51             publish
52             visit
53             trace
54             delta
55             prefer_bin
56             warnings
57             dbh
58 2     2   27 };
  2         36  
59              
60              
61              
62              
63              
64             ######################################################################
65             # Constructor and Accessors
66              
67             =pod
68              
69             =head2 new
70              
71             The C constructor creates a new processor/generator.
72              
73             =cut
74              
75             sub new {
76 1     1 1 2604 my $self = shift->SUPER::new(@_);
77              
78             # Set the default path to the database
79 1 50       43 unless ( defined $self->sqlite ) {
80 0 0       0 $self->{sqlite} = File::Spec->catdir(
81             File::HomeDir->my_data,
82             ($^O eq 'MSWin32' ? 'Perl' : '.perl'),
83             'ORDB-CPANMeta-Generator',
84             'metadb.sqlite',
85             );
86             }
87              
88             # Set the default path to the publishing location
89 1 50       17 unless ( exists $self->{publish} ) {
90 1         3 $self->{publish} = 'cpanmeta';
91             }
92              
93 1         4 return $self;
94             }
95              
96             =pod
97              
98             =head2 dir
99              
100             The C method returns the directory that the SQLite
101             database will be written into.
102              
103             =cut
104              
105             sub dir {
106 2     2 1 44 File::Basename::dirname($_[0]->sqlite);
107             }
108              
109             =pod
110              
111             =head2 dsn
112              
113             The C method returns the L DSN that is used to connect
114             to the generated database.
115              
116             =cut
117              
118             sub dsn {
119 2     2 1 864 "DBI:SQLite:" . $_[0]->sqlite
120             }
121              
122              
123              
124              
125              
126             ######################################################################
127             # Main Methods
128              
129             =pod
130              
131             =head2 run
132              
133             The C method executes the process that will produce and fill the
134             final database.
135              
136             =cut
137              
138             sub run {
139 1     1 1 482 my $self = shift;
140              
141             # Normalise
142 1 50       21 $self->{prefer_bin} = $self->prefer_bin ? 1 : 0;
143              
144             # Create the output directory
145 1         10 File::Path::make_path($self->dir);
146 1 50       112 unless ( -d $self->dir ) {
147 0         0 Carp::croak("Failed to create '" . $self->dir . "'");
148             }
149              
150             # Clear the database if it already exists
151 1 50       65 unless ( $self->delta ) {
152 1 50       27 if ( -f $self->sqlite ) {
153 0         0 File::Remove::remove($self->sqlite);
154             }
155 1 50       30 if ( -f $self->sqlite ) {
156 0         0 Carp::croak("Failed to clear " . $self->sqlite);
157             }
158             }
159              
160             # Update the minicpan if needed
161 1 50       28 if ( Params::Util::_HASH($self->minicpan) ) {
162 0         0 CPAN::Mini->update_mirror(
163             trace => $self->trace,
164             no_conn_cache => 1,
165 0         0 %{$self->minicpan},
166             );
167 0         0 $self->{minicpan} = $self->minicpan->{local};
168             }
169              
170             # Connect to the database
171 1         13 my $dbh = DBI->connect($self->dsn);
172 1 50       15474 unless ( $dbh ) {
173 0         0 Carp::croak("connect: \$DBI::errstr");
174             }
175              
176             # Create the tables
177 1         10 $dbh->do(<<'END_SQL');
178             CREATE TABLE IF NOT EXISTS meta_distribution (
179             release TEXT NOT NULL,
180             meta INTEGER,
181             meta_name TEXT,
182             meta_version TEXT,
183             meta_abstract TEXT,
184             meta_generated TEXT,
185             meta_from TEXT,
186             meta_license TEXT
187             );
188             END_SQL
189              
190 1         3113637 $dbh->do(<<'END_SQL');
191             CREATE TABLE IF NOT EXISTS meta_dependency (
192             release TEXT NOT NULL,
193             module TEXT NOT NULL,
194             version TEXT NULL,
195             phase TEXT NOT NULL,
196             core REAL NULL
197             )
198             END_SQL
199              
200             ### NOTE: This does nothing right now but will later.
201             # Build the index of seen archives.
202             # While building the index, remove entries
203             # that are no longer in the minicpan.
204 1         43618 my $ignore = undef;
205 1 50       274 if ( $self->delta ) {
206 0         0 $dbh->begin_work;
207 0         0 my %seen = ();
208 0         0 my $dists = $dbh->selectcol_arrayref(
209             'SELECT DISTINCT release FROM meta_distribution'
210             );
211 0         0 foreach my $dist ( @$dists ) {
212 0         0 my $one = substr($dist, 0, 1);
213 0         0 my $two = substr($dist, 0, 2);
214 0         0 my $path = File::Spec->catfile(
215             $self->minicpan,
216             'authors', 'id',
217             $one, $two,
218             split /\//, $dist,
219             );
220 0 0       0 if ( -f $path ) {
221             # Add to the ignore list
222 0         0 $seen{$dist} = 1;
223 0         0 next;
224             }
225              
226             # Clear the release from the database
227             $dbh->do(
228 0         0 'DELETE FROM meta_distribution WHERE release = ?',
229             {}, $dist,
230             );
231             }
232             $dbh->do(
233 0         0 'DELETE FROM meta_dependency WHERE release NOT IN '
234             . '( SELECT release FROM meta_distribution )',
235             );
236 0         0 $dbh->commit;
237              
238             # NOW we need to start ignoring something
239             $ignore = [
240             sub {
241 0     0   0 $seen{ $_[0]->{dist} }
242             }
243 0         0 ];
244             }
245              
246             # Clear indexes for speed
247 1         14 $self->drop_indexes( $dbh );
248              
249             # Run the visitor to generate the database
250 1         15 $dbh->begin_work;
251 1         24 my @meta_dist = ();
252 1         3 my @meta_deps = ();
253             my $visitor = CPAN::Mini::Visit->new(
254             acme => 1,
255             warnings => $self->warnings,
256             minicpan => $self->minicpan,
257             # This does nothing now but will later
258             ignore => $ignore,
259             prefer_bin => $self->prefer_bin,
260             callback => sub {
261 4 50   4   1473356 print STDERR "$_[0]->{dist}\n" if $self->trace;
262 4         57 my $the = shift;
263 4         21 my $meta = undef;
264 4         17 my @deps = ();
265 4         61 my $dist = {
266             release => $the->{dist},
267             meta => 0,
268             };
269 4         60 my $yaml_file = File::Spec->catfile(
270             $the->{tempdir}, 'META.yml',
271             );
272 4         52 my $json_file = File::Spec->catfile(
273             $the->{tempdir}, 'META.json',
274             );
275 4 50       267 if ( -f $json_file ) {
    50          
276 0         0 $meta = eval {
277 0         0 CPAN::Meta->load_file($json_file)
278             };
279             } elsif ( -f $yaml_file ) {
280 4         7 $meta = eval {
281 4         132 CPAN::Meta->load_file($yaml_file)
282             };
283             }
284 4 50 33     79476 unless ( $@ or not defined $meta ) {
285 4         15 $dist->{meta} = 1;
286 4         33 $dist->{meta_name} = $meta->name;
287 4         49 $dist->{meta_version} = $meta->version;
288 4         57 $dist->{meta_abstract} = $meta->abstract;
289 4         40 $dist->{meta_generated} = $meta->generated_by;
290 4         64 $dist->{meta_generated} =~ s/,.+//;
291 4         31 $dist->{meta_license} = join ', ', $meta->licenses;
292 4         2878 $dist->{meta_from} = undef;
293              
294             # Fetch the dependency blocks
295 4         44 my $core = $meta->effective_prereqs;
296 4         6212 foreach my $when ( qw{ configure build test runtime } ) {
297 16         66 my $requires = $core->requirements_for($when, 'requires');
298 16         775 my $hash = $requires->as_string_hash;
299 16         248 push @deps, map { +{
  7         156  
300             release => $the->{dist},
301             phase => $when,
302             module => $_,
303             version => $hash->{$_},
304             } } sort keys %$hash;
305             }
306             }
307             $dbh->do(
308 4         197 'INSERT INTO meta_distribution VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )', {},
309             $dist->{release},
310             $dist->{meta},
311             $dist->{meta_name},
312             $dist->{meta_version},
313             $dist->{meta_abstract},
314             $dist->{meta_generated},
315             $dist->{meta_from},
316             $dist->{meta_license},
317             );
318             $dbh->do(
319             'INSERT INTO meta_dependency VALUES ( ?, ?, ?, ?, ? )', {},
320             $_->{release},
321             $_->{module},
322             $_->{version},
323             $_->{phase},
324             $_->{module} eq 'perl'
325             ? $_->{version}
326             : scalar Module::CoreList->first_release(
327             $_->{module}, $_->{version},
328             ),
329 4 100       1899 ) foreach @deps;
330 4 50       61291 unless ( $the->{counter} % 100 ) {
331 0         0 $dbh->commit;
332 0         0 $dbh->begin_work;
333             }
334             },
335 1         40 );
336 1         745 $visitor->run;
337 1         1614068 $dbh->commit;
338              
339             # Generate the indexes
340 1         29 $self->create_indexes( $dbh );
341              
342             # Clean and optimise the database
343 1         13 $dbh->do('PRAGMA user_version = 10');
344 1         25358 $dbh->do('VACUUM');
345 1         22440 $dbh->do('ANALYZE main');
346              
347             # Publish the database to the current directory
348 1 50       88969 if ( defined $self->publish ) {
349 1 50       45 print STDERR "Publishing the generated database...\n" if $self->trace;
350 1         165 Xtract::Publish->new(
351             from => $self->sqlite,
352             sqlite => $self->publish,
353             trace => $self->trace,
354             raw => 0,
355             gz => 1,
356             bz2 => 1,
357             lz => 1,
358             )->run;
359             }
360              
361 1         199782 return 1;
362             }
363              
364              
365              
366              
367              
368             ######################################################################
369             # Index Management
370              
371 2         588 use constant INDEX => (
372             [ 'meta_distribution', 'release' ],
373             [ 'meta_dependency', 'release' ],
374             [ 'meta_dependency', 'phase' ],
375             [ 'meta_dependency', 'module' ],
376 2     2   3742 );
  2         4  
377              
378             sub drop_indexes {
379 1     1 0 5 my $self = shift;
380 1         3 my $dbh = shift;
381 1         4 foreach my $i ( INDEX ) {
382 4         1376 $dbh->do("DROP INDEX IF EXISTS $i->[0]__$i->[1]");
383             }
384 1         534 return 1;
385             }
386              
387             sub create_indexes {
388 1     1 0 11 my $self = shift;
389 1         3 my $dbh = shift;
390 1         13 foreach my $i ( INDEX ) {
391 4         100579 $self->create_index( $dbh, @$i );
392             }
393 1         87463 return 1;
394             }
395              
396             sub create_index {
397 4     4 0 92 $_[1]->do("CREATE INDEX IF NOT EXISTS $_[2]__$_[3] on $_[2] ( $_[3] )");
398             }
399              
400             1;
401              
402             =pod
403              
404             =head1 SUPPORT
405              
406             Bugs should be reported via the CPAN bug tracker at
407              
408             L
409              
410             For other issues, contact the author.
411              
412             =head1 AUTHOR
413              
414             Adam Kennedy Eadamk@cpan.orgE
415              
416             =head1 COPYRIGHT
417              
418             Copyright 2009 - 2012 Adam Kennedy.
419              
420             This program is free software; you can redistribute
421             it and/or modify it under the same terms as Perl itself.
422              
423             The full text of the license can be found in the
424             LICENSE file included with this module.
425              
426             =cut