#!/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