package Dist::Zilla::Plugin::Conflicts; use strict; use warnings; use namespace::autoclean; our $VERSION = '0.20'; use Dist::CheckConflicts 0.02 (); use Dist::Zilla 4.0 (); use Dist::Zilla::File::InMemory; use Dist::Zilla::File::FromCode; use Moose; with qw( Dist::Zilla::Role::FileGatherer Dist::Zilla::Role::InstallTool Dist::Zilla::Role::MetaProvider Dist::Zilla::Role::PrereqSource Dist::Zilla::Role::TextTemplate ); has _conflicts => ( is => 'ro', isa => 'HashRef', default => sub { {} }, ); has _script => ( is => 'ro', isa => 'Str', predicate => '_has_script', ); has _conflicts_module_name => ( is => 'ro', isa => 'Str', init_arg => undef, lazy => 1, builder => '_build_conflicts_module_name', ); has _conflicts_module_path => ( is => 'ro', isa => 'Str', init_arg => undef, lazy => 1, builder => '_build_conflicts_module_path', ); around BUILDARGS => sub { my $orig = shift; my $class = shift; my $args = $class->$orig(@_); my $zilla = delete $args->{zilla}; my $name = delete $args->{plugin_name}; my $bin = delete $args->{'-script'}; return { zilla => $zilla, plugin_name => $name, ( defined $bin ? ( _script => $bin ) : () ), _conflicts => $args, }; }; sub _build_conflicts_module_name { my $self = shift; ( my $base = $self->zilla()->name() ) =~ s/-/::/g; return $base . '::Conflicts'; } sub _build_conflicts_module_path { my $self = shift; my $path = join '/', split /-/, $self->zilla()->name(); return "lib/$path/Conflicts.pm"; } sub register_prereqs { my ($self) = @_; $self->zilla->register_prereqs( { phase => 'configure' }, 'Dist::CheckConflicts' => '0.02', ); $self->zilla->register_prereqs( { phase => 'runtime' }, 'Dist::CheckConflicts' => '0.02', ); } sub gather_files { my $self = shift; $self->add_file( Dist::Zilla::File::FromCode->new( name => $self->_conflicts_module_path(), code => sub { $self->_generate_conflicts_module }, ) ); if ( $self->_has_script() ) { $self->add_file( Dist::Zilla::File::InMemory->new( name => $self->_script(), content => $self->_generate_conflicts_script(), ) ); } return; } { # The weird {{ '=' . 'pod' }} stuff is to hide this POD from # podtidy. Otherwise it starts formatting it, which we don't want. my $conflicts_module_template = <<'EOF'; package # hide from PAUSE {{ $module_name }}; use strict; use warnings; # this module was generated with {{ ref($plugin) . ' ' . ($plugin->VERSION || '') }} use Dist::CheckConflicts -dist => '{{ $dist_name }}', -conflicts => { {{ $conflicts_dump }}, }, {{ $also_dump }} ; 1; !!closing comments!! __END__ {{ '=' . 'pod' }} {{ '=' . 'for' }} Pod::Coverage *EVERYTHING* {{ '=' . 'head1' }} NAME {{ $module_name }} - Check for conflicts between {{ $dist_name }} and installed packages {{ '=' . 'head1' }} DESCRIPTION This module contains information about conflicts between this distribution and other CPAN distributions. It does not have any user-facing parts. This module was generated by {{ ( ref $plugin ) . q{ } . ( $plugin->VERSION // '' ) }}. {{ '=' . 'cut' }} EOF # Avoid false positives in toolchain things -- e.g. [SurgicalPodWeaver], # and MetaCPAN seems to look for this when picking a summary for the # recent uploads page. $conflicts_module_template =~ s/\Q!!closing comments!!/ '# ABST' . 'RACT: Check for conflicts between {{ $dist_name }} and installed packages' . "\n" . '# Dist::Zilla: -' . 'PodWeaver' /e; sub _generate_conflicts_module { my $self = shift; my $conflicts = $self->_conflicts(); my $conflicts_dump = join ",\n ", map {qq['$_' => '$conflicts->{$_}']} sort keys %{$conflicts}; my $also_dump = join "\n ", sort grep { $_ ne 'perl' } map { $_->required_modules() } $self->zilla()->prereqs()->requirements_for(qw(runtime requires)); $also_dump = ' -also => [ qw(' . "\n" . q{ } . $also_dump . "\n" . ' ) ],' . "\n" if length $also_dump; ( my $dist_name = $self->zilla()->name() ) =~ s/-/::/g; return $self->fill_in_string( $conflicts_module_template, { plugin => \$self, dist_name => \$dist_name, module_name => \( $self->_conflicts_module_name() ), conflicts_dump => \$conflicts_dump, also_dump => \$also_dump, }, ); } } { # If dzil sees this string PODXXXX anywhere in this code it uses that as the # name for the module. my $podname_hack = 'POD' . 'NAME'; my $script_template = <<'EOF'; #!/usr/bin/perl use strict; use warnings; # %s: {{ $filename }} # this script was generated with {{ ref($plugin) . ' ' . ($plugin->VERSION || '') }} use Getopt::Long; use {{ $module_name }}; my $verbose; GetOptions( 'verbose|v' => \$verbose ); if ($verbose) { {{ $module_name }}->check_conflicts; } else { my @conflicts = {{ $module_name }}->calculate_conflicts; print "$_\n" for map { $_->{package} } @conflicts; exit @conflicts; } EOF $script_template = sprintf( $script_template, $podname_hack ); sub _generate_conflicts_script { my $self = shift; ( my $filename = $self->_script() ) =~ s{^.*/}{}; return $self->fill_in_string( $script_template, { plugin => \$self, filename => \$filename, module_name => \( $self->_conflicts_module_name() ), }, ); } } # XXX - this should really be a separate phase that runs after InstallTool - # until then, all we can do is die if we are run too soon sub setup_installer { my $self = shift; my $found_installer; for my $file ( @{ $self->zilla()->files() } ) { if ( $file->name() =~ /Makefile\.PL$/ ) { $self->_munge_makefile_pl($file); $found_installer++; } elsif ( $file->name() =~ /Build\.PL$/ ) { $self->_munge_build_pl($file); $found_installer++; } } return if $found_installer; $self->log_fatal( 'No Makefile.PL or Build.PL was found.' . ' [Conflicts] should appear in your dist.ini' . ' after [MakeMaker] or [ModuleBuild]!' ); } sub _munge_makefile_pl { my $self = shift; my $makefile = shift; my $content = $makefile->content(); $content =~ s/(use ExtUtils::MakeMaker.*)/$1\ncheck_conflicts();/; $content .= "\n" . $self->_check_conflicts_sub(); $makefile->content($content); return; } sub _munge_build_pl { my $self = shift; my $build = shift; my $content = $build->content(); $content =~ s/(use Module::Build.*)/$1\ncheck_conflicts();/; $content .= "\n" . $self->_check_conflicts_sub(); $build->content($content); return; } { my $check_conflicts_template = <<'CC_SUB'; sub check_conflicts { if ( eval { require './{{ $conflicts_module_path }}'; 1; } ) { if ( eval { {{ $conflicts_module_name }}->check_conflicts; 1 } ) { return; } else { my $err = $@; $err =~ s/^/ /mg; warn "***\n$err***\n"; } } else { print <<'EOF'; *** {{ $warning }} *** EOF } return if $ENV{AUTOMATED_TESTING} || $ENV{NONINTERACTIVE_TESTING}; # More or less copied from Module::Build return if $ENV{PERL_MM_USE_DEFAULT}; return unless -t STDIN && ( -t STDOUT || !( -f STDOUT || -c STDOUT ) ); sleep 4; } CC_SUB sub _check_conflicts_sub { my $self = shift; my $warning; if ( $self->_has_script() ) { ( my $filename = $self->_script() ) =~ s{^.*/}{}; $warning = <<"EOF"; Your toolchain doesn't support configure_requires, so Dist::CheckConflicts hasn't been installed yet. You should check for conflicting modules manually using the '$filename' script that is installed with this distribution once the installation finishes. EOF } else { my $mod = $self->_conflicts_module_name(); $warning = <<"EOF"; Your toolchain doesn't support configure_requires, so Dist::CheckConflicts hasn't been installed yet. You should check for conflicting modules manually by examining the list of conflicts in $mod once the installation finishes. EOF } chomp $warning; return $self->fill_in_string( $check_conflicts_template, { conflicts_module_path => \( $self->_conflicts_module_path() ), conflicts_module_name => \( $self->_conflicts_module_name() ), warning => \$warning, }, ); } } sub metadata { my $self = shift; my $conflicts = $self->_conflicts; return { x_breaks => { map { $_ => '<= ' . $conflicts->{$_} } keys %$conflicts } }; } __PACKAGE__->meta->make_immutable; 1; # ABSTRACT: Declare conflicts for your distro __END__ =pod =encoding UTF-8 =head1 NAME Dist::Zilla::Plugin::Conflicts - Declare conflicts for your distro =head1 VERSION version 0.20 =head1 SYNOPSIS In your F: [Conflicts] Foo::Bar = 0.05 Thing = 2 =head1 DESCRIPTION This module lets you declare conflicts on other modules (usually dependencies of your module) in your F. Declaring conflicts does several thing to your distro. First, it generates a module named something like C. This module will use L to declare and check conflicts. The package name will be obscured from PAUSE by putting a newline after the C keyword. All of your runtime prereqs will be passed in the C<-also> parameter to L. Second, it adds code to your F or F to load the generated module and print warnings if conflicts are detected. Finally, it adds the conflicts to the F and/or F files under the "x_breaks" key. =for Pod::Coverage gather_files metadata register_prereqs setup_installer =head1 USAGE Using this module is simple, add a "[Conflicts]" section and list each module you conflict with: [Conflicts] Module::X = 0.02 The version listed is the last version that I work. In other words, any version of C greater than 0.02 should work with this release. The special key C<-script> can also be set, and given the name of a script to generate, as in: [Conflicts] -script = bin/foo-conflicts Module::X = 0.02 This script will be installed with your module, and can be run to check for currently installed modules which conflict with your module. This allows users an easy way to fix their conflicts - simply run a command such as C to bring all of your conflicting modules up to date. B Currently, this plugin only works properly if it is listed in your F I the plugin which generates your F or F. This is a limitation of L that will hopefully be addressed in a future release. =head1 SEE ALSO =over 4 =item * L =item * L =item * L =back =head1 SUPPORT Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. Bugs may be submitted at L. =head1 SOURCE The source code repository for Dist-Zilla-Plugin-Conflicts can be found at L. =head1 DONATIONS If you'd like to thank me for the work I've done on this module, please consider making a "donation" to me via PayPal. I spend a lot of free time creating free software, and would appreciate any support you'd care to offer. Please note that B in order for me to continue working on this particular software. I will continue to do so, inasmuch as I have in the past, for as long as it interests me. Similarly, a donation made in this way will probably not make me work on this software much more, unless I get so many donations that I can consider working on free software full time (let's all have a chuckle at that together). To donate, log into PayPal and send money to autarch@urth.org, or use the button at L. =head1 AUTHOR Dave Rolsky =head1 CONTRIBUTORS =for stopwords Karen Etheridge Philippe Bruhat (BooK) =over 4 =item * Karen Etheridge =item * Philippe Bruhat (BooK) =back =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2023 by Dave Rolsky. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible) The full text of the license can be found in the F file included with this distribution. =cut