From dfaae12e4d85f7c6821ae4a9fbf7e463d570e7ba Mon Sep 17 00:00:00 2001 From: Lorry Tar Creator Date: Thu, 3 Apr 2014 14:57:29 +0000 Subject: Dist-CheckConflicts-0.11 --- lib/Dist/CheckConflicts.pm | 361 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 lib/Dist/CheckConflicts.pm (limited to 'lib/Dist') diff --git a/lib/Dist/CheckConflicts.pm b/lib/Dist/CheckConflicts.pm new file mode 100644 index 0000000..8370a01 --- /dev/null +++ b/lib/Dist/CheckConflicts.pm @@ -0,0 +1,361 @@ +package Dist::CheckConflicts; +BEGIN { + $Dist::CheckConflicts::AUTHORITY = 'cpan:DOY'; +} +$Dist::CheckConflicts::VERSION = '0.11'; +use strict; +use warnings; +use 5.006; +# ABSTRACT: declare version conflicts for your dist + +use base 'Exporter'; +our @EXPORT = our @EXPORT_OK = ( + qw(conflicts check_conflicts calculate_conflicts dist) +); + +use Carp; +use Module::Runtime 0.009 'module_notional_filename', 'require_module'; + + +my %CONFLICTS; +my %HAS_CONFLICTS; +my %DISTS; + +sub import { + my $pkg = shift; + my $for = caller; + + my ($conflicts, $alsos, $dist); + ($conflicts, @_) = _strip_opt('-conflicts' => @_); + ($alsos, @_) = _strip_opt('-also' => @_); + ($dist, @_) = _strip_opt('-dist' => @_); + + my %conflicts = %{ $conflicts || {} }; + for my $also (@{ $alsos || [] }) { + eval { require_module($also) } or next; + if (!exists $CONFLICTS{$also}) { + $also .= '::Conflicts'; + eval { require_module($also) } or next; + } + if (!exists $CONFLICTS{$also}) { + next; + } + my %also_confs = $also->conflicts; + for my $also_conf (keys %also_confs) { + $conflicts{$also_conf} = $also_confs{$also_conf} + if !exists $conflicts{$also_conf} + || $conflicts{$also_conf} lt $also_confs{$also_conf}; + } + } + + $CONFLICTS{$for} = \%conflicts; + $DISTS{$for} = $dist || $for; + + if (grep { $_ eq ':runtime' } @_) { + for my $conflict (keys %conflicts) { + $HAS_CONFLICTS{$conflict} ||= []; + push @{ $HAS_CONFLICTS{$conflict} }, $for; + } + + # warn for already loaded things... + for my $conflict (keys %conflicts) { + if (exists $INC{module_notional_filename($conflict)}) { + _check_version([$for], $conflict); + } + } + + # and warn for subsequently loaded things... + @INC = grep { + !(ref($_) eq 'ARRAY' && @$_ > 1 && $_->[1] == \%CONFLICTS) + } @INC; + unshift @INC, [ + sub { + my ($sub, $file) = @_; + + (my $mod = $file) =~ s{\.pm$}{}; + $mod =~ s{/}{::}g; + return unless $mod =~ /[\w:]+/; + + return unless defined $HAS_CONFLICTS{$mod}; + + { + local $HAS_CONFLICTS{$mod}; + require $file; + } + + _check_version($HAS_CONFLICTS{$mod}, $mod); + + # the previous require already handled it + my $called; + return sub { + return 0 if $called; + $_ = "1;"; + $called = 1; + return 1; + }; + }, + \%CONFLICTS, # arbitrary but unique, see above + ]; + } + + $pkg->export_to_level(1, @_); +} + +sub _strip_opt { + my ($opt, @args) = @_; + + my $val; + for my $idx ( 0 .. $#args - 1 ) { + if (defined $args[$idx] && $args[$idx] eq $opt) { + $val = (splice @args, $idx, 2)[1]; + last; + } + } + + return ( $val, @args ); +} + +sub _check_version { + my ($fors, $mod) = @_; + + for my $for (@$fors) { + my $conflict_ver = $CONFLICTS{$for}{$mod}; + my $version = do { + no strict 'refs'; + ${ ${ $mod . '::' }{VERSION} }; + }; + + if ($version le $conflict_ver) { + warn <dist; + my @conflicts = $package->calculate_conflicts; + return unless @conflicts; + + my $err = "Conflicts detected for $dist:\n"; + for my $conflict (@conflicts) { + $err .= " $conflict->{package} is version " + . "$conflict->{installed}, but must be greater than version " + . "$conflict->{required}\n"; + } + die $err; +} + + +sub calculate_conflicts { + my $package = shift; + my %conflicts = $package->conflicts; + + my @ret; + + + CONFLICT: + for my $conflict (keys %conflicts) { + my $success = do { + local $SIG{__WARN__} = sub {}; + eval { require_module($conflict) }; + }; + my $error = $@; + my $file = module_notional_filename($conflict); + next if not $success and $error =~ /Can't locate \Q$file\E in \@INC/; + + warn "Warning: $conflict did not compile" if not $success; + my $installed = $success ? $conflict->VERSION : 'unknown'; + push @ret, { + package => $conflict, + installed => $installed, + required => $conflicts{$conflict}, + } if not $success or $installed le $conflicts{$conflict}; + } + + return sort { $a->{package} cmp $b->{package} } @ret; +} + + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Dist::CheckConflicts - declare version conflicts for your dist + +=head1 VERSION + +version 0.11 + +=head1 SYNOPSIS + + use Dist::CheckConflicts + -dist => 'Class-MOP', + -conflicts => { + 'Moose' => '1.14', + 'namespace::autoclean' => '0.08', + }, + -also => [ + 'Package::Stash::Conflicts', + ]; + + __PACKAGE__->check_conflicts; + +=head1 DESCRIPTION + +One shortcoming of the CPAN clients that currently exist is that they have no +way of specifying conflicting downstream dependencies of modules. This module +attempts to work around this issue by allowing you to specify conflicting +versions of modules separately, and deal with them after the module is done +installing. + +For instance, say you have a module C, and some other module C uses +C. If C were to change its API in a non-backwards-compatible way, +this would cause C to break until it is updated to use the new API. C +can't just depend on the fixed version of C, because this will cause a +circular dependency (because C is already depending on C), and this +doesn't express intent properly anyway - C doesn't use C at all. The +ideal solution would be for there to be a way to specify conflicting versions +of modules in a way that would let CPAN clients update conflicting modules +automatically after an existing module is upgraded, but until that happens, +this module will allow users to do this manually. + +This module accepts a hash of options passed to its C statement, with +these keys being valid: + +=over 4 + +=item -conflicts + +A hashref of conflict specifications, where keys are module names, and values +are the last broken version - any version greater than the specified version +should work. + +=item -also + +Additional modules to get conflicts from (potentially recursively). This should +generally be a list of modules which use Dist::CheckConflicts, which correspond +to the dists that your dist depends on. (In an ideal world, this would be +intuited directly from your dependency list, but the dependency list isn't +available outside of build time). + +=item -dist + +The name of the distribution, to make the error message from check_conflicts +more user-friendly. + +=back + +The methods listed below are exported by this module into the module that uses +it, so you should call these methods on your module, not Dist::CheckConflicts. + +As an example, this command line can be used to update your modules, after +installing the C dist (assuming that C is the module in +the C dist which uses Dist::CheckConflicts): + + perl -MFoo::Conflicts -e'print "$_\n" + for map { $_->{package} } Foo::Conflicts->calculate_conflicts' | cpanm + +As an added bonus, loading your conflicts module will provide warnings at +runtime if conflicting modules are detected (regardless of whether they are +loaded before or afterwards). + +=head1 METHODS + +=head2 conflicts + +Returns the conflict specification (the C<-conflicts> parameter to +C), as a hash. + +=head2 dist + +Returns the dist name (either as specified by the C<-dist> parameter to +C, or the package name which Cd this module). + +=head2 check_conflicts + +Examine the modules that are currently installed, and throw an exception with +useful information if any modules are at versions which conflict with the dist. + +=head2 calculate_conflicts + +Examine the modules that are currently installed, and return a list of modules +which conflict with the dist. The modules will be returned as a list of +hashrefs, each containing C, C, and C keys. + +=head1 BUGS + +No known bugs. + +Please report any bugs to GitHub Issues at +L. + +=head1 SEE ALSO + +L + +L + +=head1 SUPPORT + +You can find this documentation for this module with the perldoc command. + + perldoc Dist::CheckConflicts + +You can also look for information at: + +=over 4 + +=item * MetaCPAN + +L + +=item * Github + +L + +=item * RT: CPAN's request tracker + +L + +=item * CPAN Ratings + +L + +=back + +=head1 AUTHOR + +Jesse Luehrs + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2014 by Jesse Luehrs. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +=cut -- cgit v1.2.1