#!/usr/bin/perl -w
# -*- perl -*-

#
# $Id: mkprereqinst,v 1.13 2003/11/28 23:48:32 eserte Exp $
# Author: Slaven Rezic
#
# Copyright (C) 2002, 2003 Slaven Rezic. All rights reserved.
# This package is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# Mail: slaven@rezic.de
# The latest version of mkprereqinst may be found at
#     http://www.perl.com/CPAN-local/authors/id/S/SR/SREZIC/
# or any other CPAN mirror.
#

use Getopt::Long;
use strict;
use vars qw($VERSION);
use ExtUtils::MakeMaker;
use File::Basename qw(basename);

$VERSION = sprintf("%d.%02d", q$Revision: 1.13 $ =~ /(\d+)\.(\d+)/);

my $v;
my $exec;
my $execopts;
my $do_cpan;
my $do_ppm;
my $do_apt;
my $do_aptitude;
my $inc_debian;
my $do_dump;
my $o = "prereqinst.pl";

my $min_cpan_version = "1.70";

if (!GetOptions("v!" => \$v,
		"exec!" => \$exec,
		"execopts=s" => \$execopts,
		"cpan!" => \$do_cpan,
		"ppm!" => \$do_ppm,
		"apt!" => \$do_apt,
		"aptitude!" => \$do_aptitude,
		"incdebian!" => \$inc_debian,
		"mincpanversion=s" => \$min_cpan_version,
		"dump!" => \$do_dump,
		"o=s" => \$o,
	       )) {
    require Pod::Usage;
    Pod::Usage::pod2usage(1);
}

my $prereq_pm;

if (@ARGV) {
    $prereq_pm = set_prereq_pm(@ARGV);
} elsif (-r "META.yml" && eval { require YAML }) {
    # Use META.yml of Build.PL
    my $meta = YAML::LoadFile("META.yml");
    $prereq_pm = $meta->{requires};
} else {

    $prereq_pm = get_prereq_from_Makefile_PL();
#    $prereq_pm = get_prereq_from_Makefile();

    if (ref $prereq_pm ne 'HASH') {
	warn "No prerequisites found in Makefile.PL\n";
	exit 0;
    }
}

my @debian_packages;
if ($inc_debian) {
    my %res = get_debian_packages(keys %$prereq_pm);
    if ($res{not_found}) {
	warn "WARN: $res{not_found} package(s) not available in Debian.\n";
    }
    @debian_packages = @{ $res{packages} };
}

if ($do_dump) {
    require YAML::Syck;
    if ($inc_debian) {
	print YAML::Syck::Dump(\@debian_packages);
    } else {
	print YAML::Syck::Dump(\%$prereq_pm);
    }
    exit;
}
my $code = "";

$code .= <<EOF;
#!/usr/bin/env perl
# -*- perl -*-
#
# DO NOT EDIT, created automatically by
# $0
# on @{[ scalar localtime ]}
#
# Run this script as
#    perl $o
#
# The latest version of @{[ basename($0) ]} may be found at
#     http://www.perl.com/CPAN-local/authors/id/S/SR/SREZIC/
# or any other CPAN mirror.

use Getopt::Long;
EOF

$code .= <<'EOF';
my $require_errors;
my $use = 'cpan';
my $q;

if (!GetOptions("ppm"      => sub { $use = 'ppm'      },
		"cpan"     => sub { $use = 'cpan'     },
		"apt"      => sub { $use = 'apt'      },
		"aptitude" => sub { $use = 'aptitude' },
                "q"        => \$q,
	       )) {
    die "usage: $0 [-q] [-ppm | -cpan | -apt | -aptitude]\n";
}

$ENV{FTP_PASSIVE} = 1;

EOF

my(@installs, @ppm_installs, @requires, @modlist);
while(my($mod, $ver) = each %$prereq_pm) {
    my $check_mod = "require $mod";
    if ($ver) {
	$check_mod .= "; $mod->VERSION($ver)";
    }
    push @installs, "install '$mod' if !eval '$check_mod';";
    (my $ppm = $mod) =~ s/::/-/g;
    push @ppm_installs, "do { print STDERR 'Install $ppm'.qq(\\n); PPM::InstallPackage(package => '$ppm') or warn ' (not successful)'.qq(\\n); } if !eval '$check_mod';";
    push @requires, "if (!eval 'require $mod;" . ($ver ? " $mod->VERSION($ver);" : "") . '\') { warn $@; $require_errors++ }';
    push @modlist, $mod . ($ver ? " $ver" : "");
}

$code .= <<EOF;
if (\$use eq 'ppm') {
    require PPM;
@{[ join("\n", map("    $_", @ppm_installs)) ]}
} elsif (\$use eq 'cpan') {
    use CPAN;
    if (!eval q{ CPAN->VERSION($min_cpan_version) }) {
	install 'CPAN';
        CPAN::Shell->reload('cpan');
    }
@{[ join("\n", map("    $_", @installs)) ]}
} elsif (\$use eq 'apt' || \$use eq 'aptitude') {
    my \@cmd = (\$use, "install", @{[ join(", ", map { "'$_'" } @debian_packages) ]});
    system(\@cmd) == 0
        or warn "Failure while running \@cmd: \$?";
} else {
    die "Unhandled \\\$use <\$use>";
}
EOF

$code .= join("\n", @requires) . "\n\n";

$code .= 'if (!$require_errors) { warn "Autoinstallation of prerequisites completed\n" unless $q } else { warn "$require_errors error(s) encountered while installing prerequisites\n" } ' . "\n";

if ($exec) {
    package Prereqinst;
    local @ARGV;
    if    ($do_cpan)     { push @ARGV, "-cpan"      }
    elsif ($do_ppm)      { push @ARGV, "-ppm"       }
    elsif ($do_apt)      { push @ARGV, "-apt"       }
    elsif ($do_aptitude) { push @ARGV, "-aptitude"  }
    push @ARGV, split /\s+/, $execopts if $execopts;
    eval $code;
    die $@ if $@;
} else {
    open(F, "> $o") or die "Can't write $o: $!";
    print F $code;
    close F;
    chmod 0755, $o;

    system($^X, "-c", $o)
	and warn "\nThe generated file `$o' had errors.\n";
}

if ($v) {
    require Text::Wrap;
    print STDERR Text::Wrap::wrap("Dependencies: ", "              ",
				  join(", ", @modlist) . "\n");
}

sub set_prereq_pm {
    my(@args) = @_;
    my $prereq_pm = {};
    my $curr_mod;
    for(my $i=0; $i<=$#args; $i++) {
	if ($args[$i] =~ /^\d/) {
	    if (!defined $curr_mod) {
		die "Got version <$args[$i]>, but expected module name!";
	    }
	    $prereq_pm->{$curr_mod} = $args[$i];
	    undef $curr_mod;
	} else {
	    if (defined $curr_mod) {
		$prereq_pm->{$curr_mod} = undef;
	    }
	    $curr_mod = $args[$i];
	}
    }
    if (defined $curr_mod) {
	$prereq_pm->{$curr_mod} = undef;
    }
    $prereq_pm;
}

sub get_prereq_from_Makefile_PL {
    my $Makefile_PL;

    {
	local $^W = 0;

	*ExtUtils::MakeMaker::WriteMakefile = sub {
	    $Makefile_PL = { @_ };
	};

	do "Makefile.PL"; die $@ if $@;
    }

    return $Makefile_PL->{PREREQ_PM};
}

# Taken from CPAN.pm
sub get_prereq_from_Makefile {
    my %p;
    open(M, "Makefile") or die "Can't open Makefile";
    while(<M>) {
	last if /MakeMaker post_initialize section/;
	my($p) = m{^[\#]
                 \s+PREREQ_PM\s+=>\s+(.+)
                 }x;
        next unless $p;
        while ( $p =~ m/(?:\s)([\w\:]+)=>q\[(.*?)\],?/g ){
	    if ( defined $p{$1} ) {
		warn "Warning: PREREQ_PM mentions $1 more than once, last mention wins";
	    }
	    $p{$1} = $2;
	}
	last;
    }
    close M;

    \%p;
}

sub get_debian_packages {
    my(@modules) = @_;

    warn "Searching for Debian packages. This may take some time...\n";
    my $tp;
    if (eval { require Time::Progress; 1 }) {
	$tp = Time::Progress->new;
	$tp->attr(min => 0, max => $#modules);
	$tp->restart;
    }

    my $locator = 'apt-file';
    my @cmd = ('apt-file', 'search', '--regexp');

    my $not_found_count = 0;
    my %package;
    my $module_i = 0;
    for my $module (@modules) {
	(my $module_file = $module) =~ s{::}{/};
	$module_file .= ".pm";

	# ?: is only needed for apt-file < 2.1.0
	my $regexp;
	if ($locator eq 'apt-file') {
	    $regexp = "(?:" . join("|", map { substr($_, 1) } grep { $_ ne "." } @INC) . ")/$module_file";
	} elsif ($locator eq 'dlocate') {
	    $regexp = "("   . join("|", map { substr($_, 1) } grep { $_ ne "." } @INC) . ")/$module_file";
	} else {
	    die "Locator <$locator>?";
	}

	my $found = 0;
	open my $fh, "-|", @cmd, $regexp
	    or die $!;
	while(<$fh>) {
	    chomp;
	    my($pack, $file) = split /\s*:\s*/;
	    $package{$pack}++;
	    $found = 1;
	}
	if (!$found) {
	    warn "Cannot find package for $module\n";
	    $not_found_count++;
	}

	print STDERR $tp->report("\r%p, ETA %f %40b", $module_i++);
    }
    print STDERR "\n" if $tp;

    (packages  => [sort keys %package],
     not_found => $not_found_count,
    );
}

__END__

=head1 NAME

mkprereqinst - create a prereqinst file for perl module distributions

=head1 DESCRIPTION

C<mkprereqinst> creates a C<prereqinst.pl> file. The created file can
be included to perl module and script distributions to help people to
automatically get and install module prerequisites through C<CPAN.pm>
or C<PPM.pm>.

The standard installation process of perl distributions with a
prereqinst file will look as following:

    perl Makefile.PL
    (if there are some modules missing then execute the next line)
    perl prereqinst.pl
    make all test install

For a Build.PL-based distribution, use the following

    perl Build
    (if there are some modules missing then execute the next line)
    perl prereqinst.pl
    perl Build test
    perl Build install

If the user needs superuser privileges to install something on his
system, then C<perl prereqinst.pl> and C<make install> should be run
as superuser, e.g. with help of the C<su> or C<sudo> command.

ActivePerl users may use

    perl prereqinst.pl -ppm

to fetch the modules through C<PPM> instead of C<CPAN>.

For an alternative approach see the CPAN module
L<ExtUtils::AutoInstall>. Some differences are:

                            | mkpreqinst | ExtUtils::AutoInstall
 ---------------------------+------------+----------------------
 Support for CPAN           |    yes     |    yes
 Support for CPANPLUS       |	 no    	 |    yes
 Support for PPM            |	 yes	 |    no
 Needs Makefile.PL changes  |	 no 	 |    yes
 Support for Build.PL       |    yes     |    ???
 Different build process    |    yes     |    no
 Has a lot of fancy options |	 no    	 |    yes

=head2 OPTIONS

C<mkprereqinst> accepts the following options:

=over

=item C<-v>

Be verbose.

=item C<-exec>

Instead of creating the C<prereqinst.pl>, execute the generated code.

=item C<-cpan>, C<-ppm>, C<-apt>, C<-aptitude>

These options are only useful in conjunction with the C<-exec> option
and force the type of auto-installation.

=item C<-incdebian>

Search for matching Debian packages using L<apt-file(1)> and include
into the generated code. Note that the generated file really only
works if all dependencies are actually available as Debian packages.

Using this option is a prerequisite to use the C<-apt> and
C<-aptitude> options.

=item C<-dump>

Just dump the dependent modules or packages (if C<-incdebian> was
specified) as YAML.

=item C<-o> outfile

Use another output file than the default C<prereqinst.pl>.

=item C<-mincpanversion> version

Specify minimal needed CPAN.pm version. If the user has an older
version, then CPAN.pm tries to install and reload itself. The default
is 1.70.

Former CPAN.pm versions used sometimes to install a new perl version.
This is fixed in 1.70, so it is recommended to use at least this
version.

=back

It is also possible to supply a list of modules and version numbers on
the command line. In this case the Makefile.PL and Build.PL are
ignored. Example:

    mkprereqinst XML::Parser 2.30 Tk GD

=head1 BUGS and TODO

The script does nasty things with C<ExtUtils::MakeMaker> and the
C<WriteMakefile> subroutine.

It is annoying to create the prereqinst.pl file if the Makefile.PL is
interactive.

OS-related or other conditions in C<PREREQ_PM> are not handled.

There are problems with the mapping of perl module names to PPM
package names.

prereqinst.pl should autodetected whether the system is a CPAN or a
PPM system. With -cpan or -ppm it would be possible to change the
default.


=head1 README

mkprereqinst creates a prereqinst file. The created file can be
included to perl module and script distributions to help people to get
and install module dependecies through CPAN.pm or PPM.pm

=head1 PREREQUISITES

only standard perl modules

=head1 COREQUISITES

YAML

=head1 OSNAMES

any

=head1 SCRIPT CATEGORIES

CPAN

=head1 AUTHOR

Slaven Rezic <slaven@rezic.de>

=head1 SEE ALSO

L<CPAN>, L<PPM>, L<ExtUtils::MakeMaker>, L<Module::Build>, L<Net::FTP>.

=cut