The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
##
# name:      Module::Package::Plugin
# abstract:  Base class for Module::Package author-side plugins
# author:    Ingy döt Net <ingy@cpan.org>
# license:   perl
# copyright: 2011
# see:
# - Module::Package
# - Module::Package::Tutorial

use 5.008003;
use utf8;

package Module::Package::Plugin;
use Moo 0.009008;

our $VERSION = '0.30';

use Cwd 0 ();
use File::Find 0 ();
use Module::Install 1.01 ();
use Module::Install::AuthorRequires 0.02 ();
use Module::Install::ManifestSkip 0.19 ();
use IO::All 0.41;

has mi => (is => 'rw');
has options => (
    is => 'rw',
    lazy => 1,
    default => sub {
        my ($self) = @_;
        $self->mi->package_options;
    },
);

#-----------------------------------------------------------------------------#
# These 3 functions (initial, main and final) make up the Module::Package
# plugin API. Subclasses MUST override 'main', and should rarely override
# 'initial' and 'final'.
#-----------------------------------------------------------------------------#
sub initial {
    my ($self) = @_;
    # Load pkg/conf.yaml if it exists
    $self->eval_deps_list;
}

sub main {
    my ($self) = @_;
    my $class = ref($self);
    die "$class cannot be used as a Module::Package plugin. Use a subclass"
        if $class eq __PACKAGE__;
    die "$class needs to provide a method called 'main()'";
}

sub final {
    my ($self) = @_;

    $self->manifest_skip;

    # NOTE These must match Module::Install::Package::_final.
    $self->all_from;
    $self->requires_from;
    $self->install_bin;
    $self->install_share;
    $self->WriteAll;

    $self->write_deps_list;
}

#-----------------------------------------------------------------------------#
# This is where the useful methods (that author plugins can invoke) live.
#-----------------------------------------------------------------------------#
sub pm_file { return "$main::PM" }
sub pod_file { return "$main::POD" }
sub pod_or_pm_file { return "$main::POD" || "$main::PM" }

my $deps_list_file = 'pkg/deps_list.pl';
sub eval_deps_list {
    my ($self) = @_;
    return if not $self->options->{deps_list};
    my $data = '';
    if (-e 'Makefile.PL') {
        my $text = io('Makefile.PL')->all;
        if ($text =~ /.*\n__(?:DATA|END)__\r?\n(.*)/s) {
            $data = $1;
        }
    }
    if (-e $deps_list_file and -s $deps_list_file) {
        package main;
        require $deps_list_file;
    }
    elsif ($data) {
        package main;
        eval $data;
        die $@ if $@;
    }
}

sub write_deps_list {
    my ($self) = @_;
    return if not $self->options->{deps_list};
    my $text = $self->generate_deps_list;
    if (-e $deps_list_file) {
        my $old_text = io($deps_list_file)->all;
        $text .= "\n1;\n" if $text;
        if ($text ne $old_text) {
            warn "Updating $deps_list_file\n";
            io($deps_list_file)->print($text);
        }
        $text = '';
    }
    if (
        -e 'Makefile.PL' and
        io('Makefile.PL')->all =~ /^__(?:DATA|END)__$/m
    ) {
        my $perl = io('Makefile.PL')->all;
        my $old_perl = $perl;
        $perl =~ s/(.*\n__(?:DATA|END)__\r?\n).*/$1/s or die $perl;
        if (-e $deps_list_file) {
            io('Makefile.PL')->print($perl);
            return;
        }
        $perl .= "\n" . $text if $text;
        if ($perl ne $old_perl) {
            warn "Updating deps_list in Makefile.PL\n";
            io('Makefile.PL')->print($perl);
            if (-e 'Makefile') {
                sleep 1;
                io('Makefile')->touch;
            }
        }
    }
    elsif ($text) {
        warn <<"...";
Note: Can't find a place to write deps list, and deps_list option is true.
      touch $deps_list_file or add __END__ to Makefile.PL.
      See 'deps_list' in Module::Package::Plugin documentation.
Deps List:
${$_ = $text; chomp; s/^/    /mg; \$_}
...
    }
}

sub generate_deps_list {
    my ($self) = @_;
    my $base = Cwd::cwd();
    my %skip = map {($_, 1)}
        qw(Module::Package Module::Install),
        $self->skip_deps(ref($self)),
        (map "Module::Install::$_", qw(
            Admin AuthorRequires AutoInstall Base Bundle Can Compiler
            Deprecated DSL External Fetch Include Inline Makefile MakeMaker
            ManifestSkip Metadata Package PAR Run Scripts Share Win32 With
            WriteAll 
        ));
    my @skip;
    for my $module (keys %skip) {
        if ($skip{"Module::Install::$module"}) {
            push @skip, "${module}::";
        }
    }
    my @inc = ();
    File::Find::find(sub {
        return unless -f $_ and $_ =~ /\.pm$/;
        my $module = $File::Find::name;
        $module =~ s!inc[\/\\](.*)\.pm$!$1!;
        return if -e "$base/lib/$module.pm";
        $module =~ s!/+!::!g;
        return if $skip{$module};
        for my $prefix (@skip) {
            return if $module =~ /^\Q$prefix\E/;
        }
        push @inc, $module;
    }, 'inc');
    if (grep /^Module::Install::TestBase$/, @inc) {
        @inc = grep not(/^(Test::|Spiffy)/), @inc; 
    }
    if (not $Module::Package::plugin_version) {
        my $module = ref($self);
        $module =~ s/::[a-z].*//;
        unshift @inc, $module;
    };
    my $text = '';
    no strict 'refs';
    $text .= join '', map {
        my $version = ${"${_}::VERSION"} || '';
        if ($version) {
            "author_requires '$_' => '$version';\n";
        }
        else {
            "author_requires '$_';\n";
        }
    } @inc;
    $text = <<"..." . $text if $text;
# Deps list generated by:
author_requires 'Module::Package' => '$Module::Package::VERSION';

...
    return $text;
}

sub skip_deps {
    my ($self, $file) = @_;
    $file =~ s/^(.*)::[^A-Z].*$/$1/
        or die "Can't grok paackage '$file'";
    $file =~ s!::!/!g;
    $file .= '.pm';
    $file = $INC{$file} or return ();
    my $content = Module::Install::_readperl($file);
    return ($content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+(?:[\d\.]+)/mg);
}

# We generate a MANIFEST.SKIP and add things to it.
# We add pkg/, because that should only contain author stuff.
# We add author only M::I plugins, so they don't get distributed.
sub manifest_skip {
    my ($self) = @_;
    return unless $self->options->{manifest_skip};
    $self->mi->manifest_skip;

    $self->set_author_only_defaults;
    my @skips = (
        "^pkg/\n",
        "^inc/.*\\.pod\n",
    );
    for (sort keys %INC) {
        my $path = $_;
        s!/!::!g;
        s!\.pm$!!;
        next unless /^Module::Install::/;
        no strict 'refs';
        push @skips, "^inc/$path\$\n"
            if ${"${_}::AUTHOR_ONLY"}
    }

    io('MANIFEST.SKIP')->append(join '', @skips);
    if (-e 'pkg/manifest.skip') {
        io('MANIFEST.SKIP')->append(io('pkg/manifest.skip')->all);
    }

    $self->mi->clean_files('MANIFEST MANIFEST.SKIP');
}

sub check_use_test_base {
    my ($self) = @_;
    my @files;
    File::Find::find(sub {
        return unless -f $_ and $_ =~ /\.(pm|t)$/;
        push @files, $File::Find::name;
    }, 't');
    for my $file (@files) {
        if (io($file)->all =~ /\bTest::Base\b/) {
            $self->mi->use_test_base;
            return;
        }
    }
}

sub check_use_testml {
    my ($self) = @_;
    my $found = 0;
    File::Find::find(sub {
        return unless -f $_ and $_ =~ /\.t$/;
        return unless io($_)->all =~ /\buse TestML\b/;
        $found = 1;
    }, 't');
    if ($found or -e 't/testml') {
        $self->mi->use_testml;
    }
}

sub check_test_common {
    my ($self) = @_;
    if (-e 't/common.yaml') {
        $self->mi->test_common_update;
    }
}

sub check_use_gloom {
    my ($self) = @_;
    my @files;
    File::Find::find(sub {
        return unless -f $_ and $_ =~ /\.pm$/;
        return if $File::Find::name eq 'lib/Gloom.pm';
        return if $File::Find::name eq 'lib/Module/Install/Gloom.pm';
        return unless io($_)->getline =~ /\bGloom\b/;
        push @files, $File::Find::name;
    }, 'lib');
    for my $file (@files) {
        $file =~ s/^lib\/(.*)\.pm$/$1/ or die;
        $file =~ s/\//::/g;
        $self->mi->use_gloom($file);
    }
}

sub strip_extra_comments {
    # TODO later
}

#-----------------------------------------------------------------------------#
# These functions are wrappers around Module::Install functions of the same
# names. They are generally safer (and simpler) to call than the real ones.
# They should almost always be chosen by Module::Package::Plugin subclasses.
#-----------------------------------------------------------------------------#
sub post_all_from {
    my $self = shift;
    push @{$self->{post_all_from} ||= []}, @_;
}
sub all_from {
    my $self = shift;
    $self->mi->_all_from(@_);
    $_->() for @{$self->{post_all_from} || []};
}
sub post_WriteAll {
    my $self = shift;
    push @{$self->{post_WriteAll} ||= []}, @_;
}
sub WriteAll {
    my $self = shift;
    $self->mi->_WriteAll(@_);
    $_->() for @{$self->{post_WriteAll} || []};
}
sub requires_from { my $self = shift; $self->mi->_requires_from(@_) }
sub install_bin { my $self = shift; $self->mi->_install_bin(@_) }
sub install_share { my $self = shift; $self->mi->_install_share(@_) }

#-----------------------------------------------------------------------------#
# Other housekeeping stuffs
#-----------------------------------------------------------------------------#
# This gets called very last on the author side.
sub replicate_module_package {
    my $target_file = 'inc/Module/Package.pm';
    if (-e 'inc/.author' and not -e $target_file) {
        my $source_file = $INC{'Module/Package.pm'}
            or die "Can't bootstrap inc::Module::Package";
        Module::Install::Admin->copy($source_file, $target_file);
    }
}

# TODO Check all versions possible here.
sub version_check {
    my ($self, $version) = @_;
    die <<"..."

Error! Something has gone awry:
    inc::Module::Package version=$inc::Module::Package::VERSION
    Module::Package version=$::Module::Package::VERSION
    Module::Install::Package version=$version
    Module::Package::Plugin version=$VERSION
Try upgrading Module::Package.

...
    unless $version == $VERSION and
        $version == $Module::Package::VERSION and
        $version == $inc::Module::Package::VERSION;
}

# This is a set of known AUTHOR_ONLY plugins. Until authors set this variable
# themselves, do it here to make sure these get added to the MANIFEST.SKIP and
# thus do not end up in the distributions, causing bloat.
sub set_author_only_defaults {
    my @known_author_only = qw(
        AckXXX
        AuthorRequires
        AutoLicense
        GitHubMeta
        ManifestSkip
        ReadmeFromPod
        ReadmeMarkdownFromPod
        Repository
        Stardoc
        TestBase
        TestML
        VersionCheck
    );
    for (@known_author_only) {
        no strict 'refs';
        ${"Module::Install::${_}::AUTHOR_ONLY"} = 1
            unless defined ${"Module::Install::${_}::AUTHOR_ONLY"};
    }
}

#-----------------------------------------------------------------------------#
# These are the usable subclasses that this module provides.  Currently there
# is only one, ':basic'. It does the minimum amount possible.  Even though it
# seems to do nothing, there is plenty of functionality that happens in the
# final() method.
#-----------------------------------------------------------------------------#
package Module::Package::Plugin::basic;
use Moo;
extends 'Module::Package::Plugin';

sub main {
    my ($self) = @_;
}

1;

=head1 SYNOPSIS

    package Module::Package::Name;

    package Module::Package::Name::flavor;
    use Moo;
    extends 'Module::Package::Plugin';

    sub main {
        my ($self) = @_;
        $self->mi->some_module_install_author_plugin;
        $self->mi->other_author_plugin;
    }

    1;

=head1 DESCRIPTION

This module is the base class for Module::Package plugins. 

=head1 EXAMPLE

Take a look at the L<Module::Package::Ingy> module, for a decent starting
point example. That plugin module is actually used to package Module::Package
itself.

=head1 API

To create a Module::Package plugin you need to subclass
Module::Package::Plugin and override the C<main> method, and possibly other
things. This section describes how that works.

Makefile.PL processing happens in the following order:

    - 'use inc::Module::Package...' is invoked
    - $plugin->initial is called
    - BEGIN blocks in Makefile.PL are run
    - $plugin->main is called
    - The body of Makefile.PL is run
    - $plugin->final is called

=head2 initial

This method is call during the processing of 'use inc::Module::Package'. You
probably don't need to subclass it. If you do you probably want to call the
SUPER method.

It runs the deps_list, if any and guesses the primary modules file path.

=head2 main

This is the method you must override. Do all the things you want. You can call
C<all_from>, if you need to get sequencing right, otherwise it gets called by
final(). Don't call C<WriteAll>, it get's called automatically in final().

=head2 final

This does all the things after the entire Makefile.PL body has run. You
probably don't need to override it.

=head1 OPTIONS

The following options are available for use from the Makefile.PL:

    use Module::Package 'Foo:bar',
        deps_list => 0|1,
        install_bin => 0|1,
        install_share => 0|1,
        manifest_skip => 0|1,
        requires_from => 0|1;

These options can be used by any subclass of this module.

=head2 deps_list

Default is 1.

This option tells Module::Package to generate a C<author_requires> deps list,
when you run the Makefile.PL. This list will go in the file
C<pkg/deps_list.pl> if that exists, or after a '__END__' statement in your
Makefile.PL. If neither is available, a reminder will be warned (only when the
author runs it).

This list is important if you want people to be able to collaborate on your
modules easily.

=head2 install_bin

Default is 1.

All files in a C<bin/> directory will be installed. It will call the
C<install_script> plugin for you. Set this option to 0 to disable it.

=head2 install_share

Default is 1.

All files in a C<share/> directory will be installed. It will call the
C<install_share> plugin for you. Set this option to 0 to disable it.

=head2 manifest_skip

Default is 1.

This option will generate a sane MANIFEST.SKIP for you and delete it again
when you run C<make clean>. You can add your own skips in the file called
C<pkg/manifest.skip>. You almost certainly want this option on. Set to 0 if
you are weird.

=head2 requires_from

Default is 1.

This option will attempt to find all the requirements from the primary module.
If you make any of your own requires or requires_from calls, this option will
do nothing.