The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Module::Build::Bundle;

# $Id: Bundle.pm 8100 2013-03-15 08:23:53Z jonasbn $

use 5.006;    #$^V
use strict;
use warnings;
use Carp qw(croak);
use Cwd qw(getcwd);
use Tie::IxHash;
use English qw( -no_match_vars );
use File::Slurp;    #read_file
use base qw(Module::Build);
use utf8;

use constant EXTENDED_POD_LINK_VERSION => 5.12.0;
use constant PERMISSION_MASK           => '07777';
use constant WRITEPERMISSION           => '0644';
use constant FILEMODE                  => 2;

our $VERSION = '0.11';

#HACK: we need a writable copy for testing purposes
## no critic qw(Variables::ProhibitPackageVars Variables::ProhibitPunctuationVars)
our $myPERL_VERSION = $^V;

sub ACTION_build {
    my $self = shift;

    if ( !$self->{'_completed_actions'}{'contents'} ) {
        $self->ACTION_contents();
    }

    return Module::Build::Base::ACTION_build($self);
}

sub ACTION_contents {
    my $self = shift;

    #Fetching requirements from Build.PL
    my @list = %{ $self->requires() };

    my $section_header = $self->notes('section_header') || 'CONTENTS';

    my $sorted = 'Tie::IxHash'->new(@list);
    $sorted->SortByKey();

    my $pod = "=head1 $section_header\n\n=over\n\n";
    foreach ( $sorted->Keys ) {
        my ( $module, $version ) = $sorted->Shift();

        my $dist = $module;
        $dist =~ s/::/\-/g;

        my $module_path = $module;
        $module_path =~ s[::][/]g;
        $module_path .= '.pm';

        if ( $myPERL_VERSION ge EXTENDED_POD_LINK_VERSION ) {
            if ($version) {
                $pod .= "=item * L<$module|$module>, "
                    . "L<$version|http://search.cpan.org/dist/$dist-$version/lib/$module_path>\n\n";
            } else {
                $pod .= "=item * L<$module|$module>\n\n";
            }
        } else {
            if ($version) {
                $pod .= "=item * L<$module|$module>, $version\n\n";
            } else {
                $pod .= "=item * L<$module|$module>\n\n";
            }
        }
    }
    $pod .= "=back\n\n=head1";

    my $cwd = getcwd();

    my @path = split /::/, $self->{properties}->{module_name}
        || $self->{properties}->{module_name};

    #HACK: induced from test suite
    my $dir = $self->notes('temp_wd') ? $self->notes('temp_wd') : 'blib/lib';

    ## no critic qw(ValuesAndExpressions::ProhibitNoisyQuotes)
    my $file = ( join '/', ( $cwd, $dir, @path ) ) . '.pm';

    my $contents = read_file($file);

    my $rv = $contents =~ s/=head1\s*$section_header\s*.*=head1/$pod/s;

    if ( !$rv ) {
        croak "No $section_header section replaced";
    }

    ## no critic (Bangs::ProhibitBitwiseOperators)
    my $permissions = ( stat $file )[FILEMODE] & PERMISSION_MASK;
    chmod WRITEPERMISSION, $file
        or croak "Unable to make file: $file writable - $!";

    open my $fout, '>', $file
        or croak "Unable to open file: $file - $!";

    print $fout $contents;

    chmod $permissions, $file
        or croak "Unable to reinstate permissions for file: $file - $!";

    close $fout or croak "Unable to close file: $file - $!";

    return 1;
}

#lifted from Module::Build::Base
sub create_mymeta {
    my ($self)     = @_;
    my $mymetafile = $self->mymetafile;
    my $metafile   = $self->metafile;

    # cleanup
    if ( $self->delete_filetree($mymetafile) ) {
        $self->log_verbose("Removed previous '$mymetafile'\n");
    }
    $self->log_info(
        "Creating new '$mymetafile' with configuration results\n");

    # use old meta and update prereqs, if possible
    my $mymeta;
    if ( -e $metafile ) {
        $mymeta = eval { $self->read_metafile( $self->metafile ) };
    }

    # if we read META OK, just update it
    if ( defined $mymeta ) {
        my $prereqs = $self->_normalize_prereqs;
        for my $t ( keys %{$prereqs} ) {
            $mymeta->{$t} = $prereqs->{$t};
        }
    }

    # but generate from scratch, ignoring errors if META doesn't exist
    else {
        $mymeta = $self->get_metadata( fatal => 0 );
    }

    my $package = ref $self;

    # MYMETA is always static
    $mymeta->{dynamic_config} = 0;

    # Note which M::B created it
    #JONASBN: changed from originally lifted code
    $mymeta->{generated_by} = "$package version $VERSION";

    $self->write_metafile( $mymetafile, $mymeta );
    return 1;
}

#lifted from Module::Build::Base
sub get_metadata {
    my ( $self, %args ) = @_;

    my $metadata = {};
    $self->prepare_metadata( $metadata, undef, \%args );

    my $package = ref $self;

    #JONASBN: changed from originally lifted code
    $metadata->{generated_by} = "$package version $VERSION";

    #JONASBN: changed from originally lifted code
    $metadata->{configure_requires} = { "$package" => $VERSION };

    return $metadata;
}

#Lifed from Module::Build::Base
sub do_create_metafile {
    my $self = shift;
    return if $self->{wrote_metadata};

    my $p = $self->{properties};

    #JONASBN: changed from originally lifted code
    my $metafile = $self->metafile;

    unless ( $p->{license} ) {
        $self->log_warn(
            "No license specified, setting license = 'unknown'\n");
        $p->{license} = 'unknown';
    }
    unless ( exists $self->valid_licenses->{ $p->{license} } ) {
        croak "Unknown license type '$p->{license}'";
    }

    # If we're in the distdir, the metafile may exist and be non-writable.
    $self->delete_filetree($metafile);
    $self->log_info("Creating $metafile\n");

    # Since we're building ourself, we have to do some special stuff
    # here: the ConfigData module is found in blib/lib.
    local @INC = @INC;
    if ( ( $self->module_name || '' ) eq 'Module::Build' ) {
        $self->depends_on('config_data');
        push @INC, File::Spec->catdir( $self->blib, 'lib' );
    }

    if ($self->write_metafile(
            $self->metafile, $self->get_metadata( fatal => 1 ),
        )
        )
    {
        $self->{wrote_metadata} = 1;
        $self->_add_to_manifest( 'MANIFEST', $metafile );
    }

    return 1;
}

1;

__END__

=head1 NAME

Module::Build::Bundle - subclass for supporting Tasks and Bundles

=head1 VERSION

This documentation describes version 0.11

=head1 SYNOPSIS

    #In your Build.PL
    use Module::Build::Bundle;
    
    #Example lifted from: Perl::Critic::logicLAB 
    my $build = Module::Build::Bundle->new(
        dist_author   => 'Jonas B. Nielsen (jonasbn), <jonasbn@cpan.org>',
        module_name   => 'Perl::Critic::logicLAB',
        license       => 'artistic',
        create_readme => 1,
        requires      => {
            'Perl::Critic::Policy::logicLAB::ProhibitUseLib' => '0',
            'Perl::Critic::Policy::logicLAB::RequireVersionFormat' => '0',
        },
    );
    
    $build->create_build_script();


    #In your shell
    % ./Build contents
    
    #Or implicitly executing contents action
    % ./Build
    
=head1 DESCRIPTION

=head2 FEATURES

=over

=item * Autogeneration of POD for Bundle and Task distributions via a build action

=item * Links to required/listed distributions, with or without versions

=item * Links to specific versions of distributions for perl 5.12.0 or newer if a 
version is specified

=item * Inserts a POD section named CONTENTS or something specified by the
caller

=back

This module adds a very basic action for propagating a requirements list from
a F<Build.PL> file's requires section to the a POD section in a designated
distribution.

=head1 SUBROUTINES/METHODS

=head2 ACTION_contents

This is the build action parsing the requirements specified in the F<Build.PL>
file. It creates a POD section (see also L</FEATURES> above).

By default it overwrites the CONTENTS section with a POD link listing. You can
specify a note indicating if what section you want to overwrite using the
section_header note.

    #Example lifted from: Perl::Critic::logicLAB 
    my $build = Module::Build::Bundle->new(
        dist_author   => 'Jonas B. Nielsen (jonasbn), <jonasbn@cpan.org>',
        module_name   => 'Perl::Critic::logicLAB',
        license       => 'artistic',
        create_readme => 1,
        requires      => {
            'Perl::Critic::Policy::logicLAB::ProhibitUseLib' => '0',
            'Perl::Critic::Policy::logicLAB::RequireVersionFormat' => '0',
        },
    );
    
    $build->notes('section_header' => 'POLICIES');

    $build->create_build_script();

The section of course has to be present.

Based on your version of perl and your F<Build.PL> requirements, the links will
be rendered in the following formats:

Basic:

    #Build.PL
    requires => {
        'Some::Package' => '0',
    }

    #POD, perl all
    =item * L<Some::Package|Some::Package>

With version:

    #Build.PL
    requires => {
        'Some::Package' => '1.99',
    }

    #POD, perl < 5.12.0
    =item * L<Some::Package|Some::Package>, 1.99

    #POD, perl >= 5.12.0
    =item * L<Some::Package|Some::Package>, L<1\.99\|http://search.cpan.org/dist/Some-Package-1.99/lib/Some/Package.pm>

=head2 ACTION_build

This is a simple wrapper around the standard action: L<Module::Build|Module::Build>
build action. It checks whether L</ACTION_contents> have been executed, if not
it executes it.

=head2 create_mymeta

This method has been lifted from L<Module::Build::Base|Module::Build::Base> and altered.

It sets the:

=over

=item * 'generated by <package> version <package version>' string in F<MYMETA.yml>

=back

For Module::Build::Bundle:

    #Example MYMETA.yml
    configure_requires:
        Module::Build::Bundle: 0.01
    generated_by: 'Module::Build::Bundle version 0.01'
    
=head2 get_metadata

This method has been lifted from L<Module::Build::Base|Module::Build::Base> and
altered.

It sets:

=over

=item * 'generated by <package> version <package version>' string in F<META.yml>

=item * configure_requires: <package>: <version> 

=back

For Module::Build::Bundle:

    #Example META.yml
    configure_requires:
        Module::Build::Bundle: 0.01
    generated_by: 'Module::Build::Bundle version 0.01'

=head2 do_create_metafile

This method has been lifted from L<Module::Build::Base|Module::Build::Base> and
altered.

The method was overwritten to be more testable. The method created the relevant
META file.

=head1 DIAGNOSTICS

=over

=item * No <section> section to be replaced

If the POD to be updated does not contain a placeholder section the action
will die with the above message.

The default minimal section should look something like:

    =head1 CONTENTS
    
    =head1
    
Or if you provide your own section_header

    =head1 <section header>
    
    =head1

=back

=head1 CONFIGURATION AND ENVIRONMENT

=head2 CONTENTS SECTION

The module does per default look for the section named: CONTENTS.

This is the section used in Bundles, this can be overwritten using the section
parameter.

For example L<Perl::Critic::logicLAB|Perl::Critic::logicLAB> uses a section
named POLICIES and L<Task::BeLike::JONASBN> uses DEPENDENCIES.

The problem is that the section has to be present or else the contents action
will throw an error.

Module::Build::Bundle is primarily aimed at Bundle distributions. Their use is
however no longer recommended and L<Task> provides a better way.

=head1 DEPENDENCIES

=over

=item * perl 5.6.0

=item * L<Module::Build::Base|Module::Build::Base>, part of the L<Module::Build>
distribution

=back

=head1 INCOMPATIBILITIES

The distribution requires perl version from 5.6.0 and up.

=head1 BUGS AND LIMITATIONS

Currently Module::Build::Bundle is not able to handle root based distributions
meaning distributions with a single Perl module located in the root directory
instead of the lib structure.

Apart from that there are no known special limitations or bugs at this time,
but I am certain there are plenty of scenarios is distribution packaging the
module is not currently handling.

The module only supports Bundle/Task distributions based on L<Module::Build>.
The implementation is based on a subclass of L<Module::Build>, which can replace
L<Module::Build> in your F<Build.PL> (See: L</SYNOPSIS>).

As described previously in the documentation a section of documentation can only
replaced. A section with the generated contents cannot be added with out a
placeholder in the form of designated section title. This might be changed in the
future.

Before version 0.11 the designated module was worked on in F<lib/>, I am still
unsure as to what the right place to do this is. Perhaps I<hooking> into the
build phase is not a good idea at all.

=head1 BUG REPORTING

Please report any bugs or feature requests via:

=over

=item * email: bug−module-build-bundle at rt.cpan.org

=item * HTTP: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Module-Build-Bundle>

=back

=head1 TEST AND QUALITY

=head2 TEST COVERAGE

    ---------------------------- ------ ------ ------ ------ ------ ------ ------
    File                           stmt   bran   cond    sub    pod   time  total
    ---------------------------- ------ ------ ------ ------ ------ ------ ------
    lib/Module/Build/Bundle.pm     48.5   13.9   14.3   84.2  100.0  100.0   45.7
    Total                          48.5   13.9   14.3   84.2  100.0  100.0   45.7
    ---------------------------- ------ ------ ------ ------ ------ ------ ------

The above coverage report is based on release 0.11

=head1 QUALITY AND CODING STANDARD

The code passes L<Perl::Critic> tests a severity: 1 (brutal)

The following policies have been disabled:

       Perl::Critic::Policy::InputOutput::RequireBracedFileHandleWithPrint

L<Perl::Critic> resource file, can be located in the F<t/> directory of the
distribution see F <t/perlcriticrc>

L<Perl::Tidy> resource file, can be obtained from the following URL:

=over

=item * L<https://logiclab.jira.com/wiki/display/OPEN/Perl-Tidy>

=back

=head1 DEVELOPMENT

=head1 TODO

Please see: L<https://logiclab.jira.com/browse/MBB#selectedTab=com.atlassian.jira.plugin.system.project%3Aroadmap-panel>

=head1 SEE ALSO

=over

=item * L<Task|Task>

=item * L<TaskBeLike::JONASBN|TaskBeLike::JONASBN>

=item * L<Perl::Critic::logicLAB|Perl::Critic::logicLAB>

=item * L<CPAN|CPAN>

=item * L<CPAN::Bundle|http://cpansearch.perl.org/src/ANDK/CPAN-1.9402/lib/CPAN/Bundle.pm>

=item * L<https://logiclab.jira.com/wiki/display/OPEN/Module-Build-Bundle>

=back

=head1 MOTIVATION

The motivation was driven by two things.

=over

=item * The joy of fooling around with L<Module::Build|Module::Build>

=item * The need for automating the documentation generation

=back

I have a few perks and one of them is that I never get to automate stuff until
very late and I always regret that. So when I released
L<Bundle::JONASBN|Bundle::JONASBN>, now L<Task::BeLike::JONASBN::Task::BeLike::JONASBN>
I thought I might aswell get it automated right away.

This module lived for a long time as a part of L<Bundle::JONASBN|Bundle::JONASBN>
but then I needed it for some other distributions, so I decided to separate it out.

=head1 ACKNOWLEDGEMENTS

=over

=item * Adam Kennedy (ADAMK) author of L<Task>, a very basic and simple solution

=item * The L<Module::Build> developers

=item * Lars Dɪᴇᴄᴋᴏᴡ (DAXIM) for reporting RT:83754, resulting in release 0.11

=item * Andreas J. König (ANDK) for reporting RT:82128, included in release 0.10

=back

=head1 AUTHOR

=over

=item * Jonas B. Nielsen (jonasbn) C<< <jonasbn@cpan.org> >>

=back

=head1 LICENSE AND COPYRIGHT

Copyright 2010-2013 jonasbn, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut