summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/Dist/CheckConflicts.pm361
1 files changed, 361 insertions, 0 deletions
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 <<EOF;
+Conflict detected for $DISTS{$for}:
+ $mod is version $version, but must be greater than version $conflict_ver
+EOF
+ return;
+ }
+ }
+}
+
+
+sub conflicts {
+ my $package = shift;
+ return %{ $CONFLICTS{ $package } };
+}
+
+
+sub dist {
+ my $package = shift;
+ return $DISTS{ $package };
+}
+
+
+sub check_conflicts {
+ my $package = shift;
+ my $dist = $package->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<Foo>, and some other module C<Bar> uses
+C<Foo>. If C<Foo> were to change its API in a non-backwards-compatible way,
+this would cause C<Bar> to break until it is updated to use the new API. C<Foo>
+can't just depend on the fixed version of C<Bar>, because this will cause a
+circular dependency (because C<Bar> is already depending on C<Foo>), and this
+doesn't express intent properly anyway - C<Foo> doesn't use C<Bar> 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<use> 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<Foo> dist (assuming that C<Foo::Conflicts> is the module in
+the C<Foo> 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<import()>), as a hash.
+
+=head2 dist
+
+Returns the dist name (either as specified by the C<-dist> parameter to
+C<import()>, or the package name which C<use>d 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<package>, C<installed>, and C<required> keys.
+
+=head1 BUGS
+
+No known bugs.
+
+Please report any bugs to GitHub Issues at
+L<https://github.com/doy/dist-checkconflicts/issues>.
+
+=head1 SEE ALSO
+
+L<Module::Install::CheckConflicts>
+
+L<Dist::Zilla::Plugin::Conflicts>
+
+=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<https://metacpan.org/release/Dist-CheckConflicts>
+
+=item * Github
+
+L<https://github.com/doy/dist-checkconflicts>
+
+=item * RT: CPAN's request tracker
+
+L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Dist-CheckConflicts>
+
+=item * CPAN Ratings
+
+L<http://cpanratings.perl.org/d/Dist-CheckConflicts>
+
+=back
+
+=head1 AUTHOR
+
+Jesse Luehrs <doy@tozt.net>
+
+=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