1# -*- Mode: cperl; coding: utf-8; cperl-indent-level: 4 -*- 2# vim: ts=4 sts=4 sw=4: 3 4use 5.006; 5use strict; 6package CPAN::Distroprefs; 7 8use vars qw($VERSION); 9$VERSION = '6.0001'; 10 11package CPAN::Distroprefs::Result; 12 13use File::Spec; 14 15sub new { bless $_[1] || {} => $_[0] } 16 17sub abs { File::Spec->catfile($_[0]->dir, $_[0]->file) } 18 19sub __cloner { 20 my ($class, $name, $newclass) = @_; 21 $newclass = 'CPAN::Distroprefs::Result::' . $newclass; 22 no strict 'refs'; 23 *{$class . '::' . $name} = sub { 24 $newclass->new({ 25 %{ $_[0] }, 26 %{ $_[1] }, 27 }); 28 }; 29} 30BEGIN { __PACKAGE__->__cloner(as_warning => 'Warning') } 31BEGIN { __PACKAGE__->__cloner(as_fatal => 'Fatal') } 32BEGIN { __PACKAGE__->__cloner(as_success => 'Success') } 33 34sub __accessor { 35 my ($class, $key) = @_; 36 no strict 'refs'; 37 *{$class . '::' . $key} = sub { $_[0]->{$key} }; 38} 39BEGIN { __PACKAGE__->__accessor($_) for qw(type file ext dir) } 40 41sub is_warning { 0 } 42sub is_fatal { 0 } 43sub is_success { 0 } 44 45package CPAN::Distroprefs::Result::Error; 46use vars qw(@ISA); 47BEGIN { @ISA = 'CPAN::Distroprefs::Result' } ## no critic 48BEGIN { __PACKAGE__->__accessor($_) for qw(msg) } 49 50sub as_string { 51 my ($self) = @_; 52 if ($self->msg) { 53 return sprintf $self->fmt_reason, $self->file, $self->msg; 54 } else { 55 return sprintf $self->fmt_unknown, $self->file; 56 } 57} 58 59package CPAN::Distroprefs::Result::Warning; 60use vars qw(@ISA); 61BEGIN { @ISA = 'CPAN::Distroprefs::Result::Error' } ## no critic 62sub is_warning { 1 } 63sub fmt_reason { "Error reading distroprefs file %s, skipping: %s" } 64sub fmt_unknown { "Unknown error reading distroprefs file %s, skipping." } 65 66package CPAN::Distroprefs::Result::Fatal; 67use vars qw(@ISA); 68BEGIN { @ISA = 'CPAN::Distroprefs::Result::Error' } ## no critic 69sub is_fatal { 1 } 70sub fmt_reason { "Error reading distroprefs file %s: %s" } 71sub fmt_unknown { "Unknown error reading distroprefs file %s." } 72 73package CPAN::Distroprefs::Result::Success; 74use vars qw(@ISA); 75BEGIN { @ISA = 'CPAN::Distroprefs::Result' } ## no critic 76BEGIN { __PACKAGE__->__accessor($_) for qw(prefs extension) } 77sub is_success { 1 } 78 79package CPAN::Distroprefs::Iterator; 80 81sub new { bless $_[1] => $_[0] } 82 83sub next { $_[0]->() } 84 85package CPAN::Distroprefs; 86 87use Carp (); 88use DirHandle; 89 90sub _load_method { 91 my ($self, $loader, $result) = @_; 92 return '_load_yaml' if $loader eq 'CPAN' or $loader =~ /^YAML(::|$)/; 93 return '_load_' . $result->ext; 94} 95 96sub _load_yaml { 97 my ($self, $loader, $result) = @_; 98 my $data = eval { 99 $loader eq 'CPAN' 100 ? $loader->_yaml_loadfile($result->abs) 101 : [ $loader->can('LoadFile')->($result->abs) ] 102 }; 103 if (my $err = $@) { 104 die $result->as_warning({ 105 msg => $err, 106 }); 107 } elsif (!$data) { 108 die $result->as_warning; 109 } else { 110 return @$data; 111 } 112} 113 114sub _load_dd { 115 my ($self, $loader, $result) = @_; 116 my @data; 117 { 118 package CPAN::Eval; 119 # this caused a die in CPAN.pm, and I am leaving it 'fatal', though I'm 120 # not sure why we wouldn't just skip the file as we do for all other 121 # errors. -- hdp 122 my $abs = $result->abs; 123 open FH, "<$abs" or die $result->as_fatal(msg => "$!"); 124 local $/; 125 my $eval = <FH>; 126 close FH; 127 no strict; 128 eval $eval; 129 if (my $err = $@) { 130 die $result->as_warning({ msg => $err }); 131 } 132 my $i = 1; 133 while (${"VAR$i"}) { 134 push @data, ${"VAR$i"}; 135 $i++; 136 } 137 } 138 return @data; 139} 140 141sub _load_st { 142 my ($self, $loader, $result) = @_; 143 # eval because Storable is never forward compatible 144 my @data = eval { @{scalar $loader->can('retrieve')->($result->abs) } }; 145 if (my $err = $@) { 146 die $result->as_warning({ msg => $err }); 147 } 148 return @data; 149} 150 151sub _build_file_list { 152 if (@_ > 3) { 153 die "_build_file_list should be called with 3 arguments, was called with more. First argument is '$_[0]'."; 154 } 155 my ($dir, $dir1, $ext_re) = @_; 156 my @list; 157 my $dh; 158 unless (opendir($dh, $dir)) { 159 $CPAN::Frontend->mywarn("ignoring prefs directory '$dir': $!"); 160 return @list; 161 } 162 while (my $fn = readdir $dh) { 163 next if $fn eq '.' || $fn eq '..'; 164 if (-d "$dir/$fn") { 165 next if $fn =~ /^[._]/; # prune .svn, .git, .hg, _darcs and what the user wants to hide 166 push @list, _build_file_list("$dir/$fn", "$dir1$fn/", $ext_re); 167 } else { 168 if ($fn =~ $ext_re) { 169 push @list, "$dir1$fn"; 170 } 171 } 172 } 173 return @list; 174} 175 176sub find { 177 my ($self, $dir, $ext_map) = @_; 178 179 return CPAN::Distroprefs::Iterator->new(sub { return }) unless %$ext_map; 180 181 my $possible_ext = join "|", map { quotemeta } keys %$ext_map; 182 my $ext_re = qr/\.($possible_ext)$/; 183 184 my @files = _build_file_list($dir, '', $ext_re); 185 @files = sort @files if @files; 186 187 # label the block so that we can use redo in the middle 188 return CPAN::Distroprefs::Iterator->new(sub { LOOP: { 189 190 my $fn = shift @files; 191 return unless defined $fn; 192 my ($ext) = $fn =~ $ext_re; 193 194 my $loader = $ext_map->{$ext}; 195 196 my $result = CPAN::Distroprefs::Result->new({ 197 file => $fn, ext => $ext, dir => $dir 198 }); 199 # copied from CPAN.pm; is this ever actually possible? 200 redo unless -f $result->abs; 201 202 my $load_method = $self->_load_method($loader, $result); 203 my @prefs = eval { $self->$load_method($loader, $result) }; 204 if (my $err = $@) { 205 if (ref($err) && eval { $err->isa('CPAN::Distroprefs::Result') }) { 206 return $err; 207 } 208 # rethrow any exceptions that we did not generate 209 die $err; 210 } elsif (!@prefs) { 211 # the loader should have handled this, but just in case: 212 return $result->as_warning; 213 } 214 return $result->as_success({ 215 prefs => [ 216 map { CPAN::Distroprefs::Pref->new({ data => $_ }) } @prefs 217 ], 218 }); 219 } }); 220} 221 222package CPAN::Distroprefs::Pref; 223 224use Carp (); 225 226sub new { bless $_[1] => $_[0] } 227 228sub data { shift->{data} } 229 230sub has_any_match { $_[0]->data->{match} ? 1 : 0 } 231 232sub has_match { 233 my $match = $_[0]->data->{match} || return 0; 234 exists $match->{$_[1]} || exists $match->{"not_$_[1]"} 235} 236 237sub has_valid_subkeys { 238 grep { exists $_[0]->data->{match}{$_} } 239 map { $_, "not_$_" } 240 $_[0]->match_attributes 241} 242 243sub _pattern { 244 my $re = shift; 245 my $p = eval sprintf 'qr{%s}', $re; 246 if ($@) { 247 $@ =~ s/\n$//; 248 die "Error in Distroprefs pattern qr{$re}\n$@"; 249 } 250 return $p; 251} 252 253sub _match_scalar { 254 my ($match, $data) = @_; 255 my $qr = _pattern($match); 256 return $data =~ /$qr/; 257} 258 259sub _match_hash { 260 my ($match, $data) = @_; 261 for my $mkey (keys %$match) { 262 (my $dkey = $mkey) =~ s/^not_//; 263 my $val = defined $data->{$dkey} ? $data->{$dkey} : ''; 264 if (_match_scalar($match->{$mkey}, $val)) { 265 return 0 if $mkey =~ /^not_/; 266 } 267 else { 268 return 0 if $mkey !~ /^not_/; 269 } 270 } 271 return 1; 272} 273 274sub _match { 275 my ($self, $key, $data, $matcher) = @_; 276 my $m = $self->data->{match}; 277 if (exists $m->{$key}) { 278 return 0 unless $matcher->($m->{$key}, $data); 279 } 280 if (exists $m->{"not_$key"}) { 281 return 0 if $matcher->($m->{"not_$key"}, $data); 282 } 283 return 1; 284} 285 286sub _scalar_match { 287 my ($self, $key, $data) = @_; 288 return $self->_match($key, $data, \&_match_scalar); 289} 290 291sub _hash_match { 292 my ($self, $key, $data) = @_; 293 return $self->_match($key, $data, \&_match_hash); 294} 295 296# do not take the order of C<keys %$match> because "module" is by far the 297# slowest 298sub match_attributes { qw(env distribution perl perlconfig module) } 299 300sub match_module { 301 my ($self, $modules) = @_; 302 return $self->_match("module", $modules, sub { 303 my($match, $data) = @_; 304 my $qr = _pattern($match); 305 for my $module (@$data) { 306 return 1 if $module =~ /$qr/; 307 } 308 return 0; 309 }); 310} 311 312sub match_distribution { shift->_scalar_match(distribution => @_) } 313sub match_perl { shift->_scalar_match(perl => @_) } 314 315sub match_perlconfig { shift->_hash_match(perlconfig => @_) } 316sub match_env { shift->_hash_match(env => @_) } 317 318sub matches { 319 my ($self, $arg) = @_; 320 321 my $default_match = 0; 322 for my $key (grep { $self->has_match($_) } $self->match_attributes) { 323 unless (exists $arg->{$key}) { 324 Carp::croak "Can't match pref: missing argument key $key"; 325 } 326 $default_match = 1; 327 my $val = $arg->{$key}; 328 # make it possible to avoid computing things until we have to 329 if (ref($val) eq 'CODE') { $val = $val->() } 330 my $meth = "match_$key"; 331 return 0 unless $self->$meth($val); 332 } 333 334 return $default_match; 335} 336 3371; 338 339__END__ 340 341=head1 NAME 342 343CPAN::Distroprefs -- read and match distroprefs 344 345=head1 SYNOPSIS 346 347 use CPAN::Distroprefs; 348 349 my %info = (... distribution/environment info ...); 350 351 my $finder = CPAN::Distroprefs->find($prefs_dir, \%ext_map); 352 353 while (my $result = $finder->next) { 354 355 die $result->as_string if $result->is_fatal; 356 357 warn($result->as_string), next if $result->is_warning; 358 359 for my $pref (@{ $result->prefs }) { 360 if ($pref->matches(\%info)) { 361 return $pref; 362 } 363 } 364 } 365 366 367=head1 DESCRIPTION 368 369This module encapsulates reading L<Distroprefs|CPAN> and matching them against CPAN distributions. 370 371=head1 INTERFACE 372 373 my $finder = CPAN::Distroprefs->find($dir, \%ext_map); 374 375 while (my $result = $finder->next) { ... } 376 377Build an iterator which finds distroprefs files in the tree below the 378given directory. Within the tree directories matching C<m/^[._]/> are 379pruned. 380 381C<%ext_map> is a hashref whose keys are file extensions and whose values are 382modules used to load matching files: 383 384 { 385 'yml' => 'YAML::Syck', 386 'dd' => 'Data::Dumper', 387 ... 388 } 389 390Each time C<< $finder->next >> is called, the iterator returns one of two 391possible values: 392 393=over 394 395=item * a CPAN::Distroprefs::Result object 396 397=item * C<undef>, indicating that no prefs files remain to be found 398 399=back 400 401=head1 RESULTS 402 403L<C<find()>|/INTERFACE> returns CPAN::Distroprefs::Result objects to 404indicate success or failure when reading a prefs file. 405 406=head2 Common 407 408All results share some common attributes: 409 410=head3 type 411 412C<success>, C<warning>, or C<fatal> 413 414=head3 file 415 416the file from which these prefs were read, or to which this error refers (relative filename) 417 418=head3 ext 419 420the file's extension, which determines how to load it 421 422=head3 dir 423 424the directory the file was read from 425 426=head3 abs 427 428the absolute path to the file 429 430=head2 Errors 431 432Error results (warning and fatal) contain: 433 434=head3 msg 435 436the error message (usually either C<$!> or a YAML error) 437 438=head2 Successes 439 440Success results contain: 441 442=head3 prefs 443 444an arrayref of CPAN::Distroprefs::Pref objects 445 446=head1 PREFS 447 448CPAN::Distroprefs::Pref objects represent individual distroprefs documents. 449They are constructed automatically as part of C<success> results from C<find()>. 450 451=head3 data 452 453the pref information as a hashref, suitable for e.g. passing to Kwalify 454 455=head3 match_attributes 456 457returns a list of the valid match attributes (see the Distroprefs section in L<CPAN>) 458 459currently: C<env perl perlconfig distribution module> 460 461=head3 has_any_match 462 463true if this pref has a 'match' attribute at all 464 465=head3 has_valid_subkeys 466 467true if this pref has a 'match' attribute and at least one valid match attribute 468 469=head3 matches 470 471 if ($pref->matches(\%arg)) { ... } 472 473true if this pref matches the passed-in hashref, which must have a value for 474each of the C<match_attributes> (above) 475 476=head1 LICENSE 477 478This program is free software; you can redistribute it and/or modify it under 479the same terms as Perl itself. 480 481=cut 482