Commit 212433f2 authored by mkanat%bugzilla.org's avatar mkanat%bugzilla.org

Bug 262269: A tool to auto-install missing perl packages on non-Windows systems

Patch By Max Kanat-Alexander <mkanat@bugzilla.org> (module owner)
parent 9f8e0ce6
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by Everything Solved are Copyright (C) 2007
# Everything Solved, Inc. All Rights Reserved.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Install::CPAN;
use strict;
use base qw(Exporter);
our @EXPORT = qw(set_cpan_config install_module BZ_LIB);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(bin_loc install_string);
use CPAN;
use Cwd qw(abs_path);
use File::Path qw(rmtree);
# We need the absolute path of ext_libpath, because CPAN chdirs around
# and so we can't use a relative directory.
#
# We need it often enough (and at compile time, in install-module.pl) so
# we make it a constant.
use constant BZ_LIB => abs_path(bz_locations()->{ext_libpath});
# CPAN requires nearly all of its parameters to be set, or it will start
# asking questions to the user. We want to avoid that, so we have
# defaults here for most of the required parameters we know about, in case
# any of them aren't set. The rest are handled by set_cpan_defaults().
use constant CPAN_DEFAULTS => {
auto_commit => 0,
# We always force builds, so there's no reason to cache them.
build_cache => 0,
cache_metadata => 1,
index_expire => 1,
scan_cache => 'atstart',
inhibit_startup_message => 1,
mbuild_install_build_command => './Build',
curl => bin_loc('curl'),
gzip => bin_loc('gzip'),
links => bin_loc('links'),
lynx => bin_loc('lynx'),
make => bin_loc('make'),
pager => bin_loc('less'),
tar => bin_loc('tar'),
unzip => bin_loc('unzip'),
wget => bin_loc('wget'),
urllist => [qw(
http://cpan.pair.com/
http://mirror.hiwaay.net/CPAN/
ftp://ftp.dc.aleron.net/pub/CPAN/
http://perl.secsup.org/
http://mirrors.kernel.org/cpan/)],
};
sub install_module {
my ($name, $notest) = @_;
my $bzlib = BZ_LIB;
# Certain modules require special stuff in order to not prompt us.
my $original_makepl = $CPAN::Config->{makepl_arg};
# This one's a regex in case we're doing Template::Plugin::GD and it
# pulls in Template-Toolkit as a dependency.
if ($name =~ /^Template/) {
$CPAN::Config->{makepl_arg} .= " TT_ACCEPT=y TT_EXTRAS=n";
}
elsif ($name eq 'XML::Twig') {
$CPAN::Config->{makepl_arg} = "-n $original_makepl";
}
elsif ($name eq 'Net::LDAP') {
$CPAN::Config->{makepl_arg} .= " --skipdeps";
}
elsif ($name eq 'SOAP::Lite') {
$CPAN::Config->{makepl_arg} .= " --noprompt";
}
my $module = CPAN::Shell->expand('Module', $name);
print install_string('install_module',
{ module => $name, version => $module->cpan_version }) . "\n";
if ($notest) {
CPAN::Shell->notest('install', $name);
}
else {
CPAN::Shell->force('install', $name);
}
# If it installed any binaries in the Bugzilla directory, delete them.
if (-d "$bzlib/bin") {
File::Path::rmtree("$bzlib/bin");
}
$CPAN::Config->{makepl_arg} = $original_makepl;
}
sub set_cpan_config {
my $do_global = shift;
my $bzlib = BZ_LIB;
# We set defaults before we do anything, otherwise CPAN will
# start asking us questions as soon as we load its configuration.
eval { require CPAN::Config; };
_set_cpan_defaults();
# Calling a senseless autoload that does nothing makes us
# automatically load any existing configuration.
# We want to avoid the "invalid command" message.
open(my $saveout, ">&STDOUT");
open(STDOUT, '>/dev/null');
eval { CPAN->ignore_this_error_message_from_bugzilla; };
undef $@;
close(STDOUT);
open(STDOUT, '>&', $saveout);
my $dir = $CPAN::Config->{cpan_home};
if (!defined $dir || !-w $dir) {
# If we can't use the standard CPAN build dir, we try to make one.
$dir = "$ENV{HOME}/.cpan";
mkdir $dir;
# If we can't make one, we finally try to use the Bugzilla directory.
if (!-w $dir) {
print "WARNING: Using the Bugzilla directory as the CPAN home.\n";
$dir = "$bzlib/.cpan";
}
}
$CPAN::Config->{cpan_home} = $dir;
$CPAN::Config->{build_dir} = "$dir/build";
# We always force builds, so there's no reason to cache them.
$CPAN::Config->{keep_source_where} = "$dir/source";
# This is set both here and in defaults so that it's always true.
$CPAN::Config->{inhibit_startup_message} = 1;
# Automatically install dependencies.
$CPAN::Config->{prerequisites_policy} = 'follow';
# Unless specified, we install the modules into the Bugzilla directory.
if (!$do_global) {
$CPAN::Config->{makepl_arg} .= " LIB=\"$bzlib\""
. " INSTALLMAN1DIR=\"$bzlib/man/man1\""
. " INSTALLMAN3DIR=\"$bzlib/man/man3\""
# The bindirs are here because otherwise we'll try to write to
# the system binary dirs, and that will cause CPAN to die.
. " INSTALLBIN=\"$bzlib/bin\""
. " INSTALLSCRIPT=\"$bzlib/bin\""
# INSTALLDIRS=perl is set because that makes sure that MakeMaker
# always uses the directories we've specified here.
. " INSTALLDIRS=perl";
$CPAN::Config->{mbuild_arg} = "--install_base \"$bzlib\"";
# When we're not root, sometimes newer versions of CPAN will
# try to read/modify things that belong to root, unless we set
# certain config variables.
$CPAN::Config->{histfile} = "$dir/histfile";
$CPAN::Config->{use_sqlite} = 0;
$CPAN::Config->{prefs_dir} = "$dir/prefs";
# Unless we actually set PERL5LIB, some modules can't install
# themselves, like DBD::mysql, DBD::Pg, and XML::Twig.
my $current_lib = $ENV{PERL5LIB} ? $ENV{PERL5LIB} . ':' : '';
$ENV{PERL5LIB} = $current_lib . $bzlib;
}
}
sub _set_cpan_defaults {
# If CPAN hasn't been configured, we try to use some reasonable defaults.
foreach my $key (keys %{CPAN_DEFAULTS()}) {
$CPAN::Config->{$key} = CPAN_DEFAULTS->{$key}
if !defined $CPAN::Config->{$key};
}
my @missing;
# In newer CPANs, this is in HandleConfig. In older CPANs, it's in
# Config.
if (eval { require CPAN::HandleConfig }) {
@missing = CPAN::HandleConfig->missing_config_data;
}
else {
@missing = CPAN::Config->missing_config_data;
}
foreach my $key (@missing) {
$CPAN::Config->{$key} = '';
}
}
1;
__END__
=head1 NAME
Bugzilla::Install::CPAN - Routines to install Perl modules from CPAN.
=head1 SYNOPSIS
use Bugzilla::Install::CPAN;
set_cpan_config();
install_module('Module::Name', 1);
=head1 DESCRIPTION
This is primarily used by L<install-module> to do the "hard work" of
installing CPAN modules.
=head1 SUBROUTINES
=over
=item C<set_cpan_config>
Sets up the configuration of CPAN for this session. Must be called
before L</install_module>. Takes one boolean parameter. If true,
L</install_module> will install modules globally instead of to the
local F<lib/> directory. On most systems, you have to be root to do that.
=item C<install_module>
Installs a module from CPAN. Takes two arguments:
=over
=item C<$name> - The name of the module, just like you'd pass to the
C<install> command in the CPAN shell.
=item C<$notest> - If true, we skip running tests on this module. This
can greatly speed up the installation time.
=back
Note that calling this function prints a B<lot> of information to
STDOUT and STDERR.
=back
......@@ -114,6 +114,7 @@ sub FILESYSTEM {
'customfield.pl' => { perms => $owner_executable },
'email_in.pl' => { perms => $ws_executable },
'sanitycheck.pl' => { perms => $ws_executable },
'install-module.pl' => { perms => $owner_executable },
'docs/makedocs.pl' => { perms => $owner_executable },
'docs/rel_notes.txt' => { perms => $ws_readable },
......
......@@ -31,8 +31,10 @@ package Bugzilla::Install::Localconfig;
use strict;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(bin_loc);
use Data::Dumper;
use File::Basename qw(dirname);
use IO::File;
use Safe;
......@@ -349,44 +351,11 @@ EOT
return { old_vars => \@old_vars, new_vars => \@new_vars };
}
sub _get_default_cvsbin {
return '' if ON_WINDOWS;
my $cvs_executable = `which cvs`;
if ($cvs_executable =~ /no cvs/ || $cvs_executable eq '') {
# If which didn't find it, just set to blank
$cvs_executable = "";
} else {
chomp $cvs_executable;
}
return $cvs_executable;
}
sub _get_default_interdiffbin {
return '' if ON_WINDOWS;
my $interdiff = `which interdiff`;
if ($interdiff =~ /no interdiff/ || $interdiff eq '') {
# If which didn't find it, just set to blank
$interdiff = '';
} else {
chomp $interdiff;
}
return $interdiff;
}
sub _get_default_cvsbin { return bin_loc('cvs') }
sub _get_default_interdiffbin { return bin_loc('interdiff') }
sub _get_default_diffpath {
return '' if ON_WINDOWS;
my $diff_binaries;
$diff_binaries = `which diff`;
if ($diff_binaries =~ /no diff/ || $diff_binaries eq '') {
# If which didn't find it, set to blank
$diff_binaries = "";
} else {
$diff_binaries =~ s:/diff\n$::;
}
return $diff_binaries;
my $diff_bin = bin_loc('diff');
return dirname($diff_bin);
}
1;
......
......@@ -434,6 +434,10 @@ EOT
printf "%15s: $command\n", $module->{package};
}
}
if ($output && $check_results->{any_missing}) {
print install_string('install_all', { perl => $^X });
}
}
sub check_graphviz {
......@@ -530,7 +534,7 @@ sub install_command {
$package = $module->{package};
}
else {
$command = "$^X -MCPAN -e 'install \"\%s\"'";
$command = "$^X install-module.pl \%s";
# Non-Windows installations need to use module names, because
# CPAN doesn't understand package names.
$package = $module->{module};
......
......@@ -34,6 +34,7 @@ use Safe;
use base qw(Exporter);
our @EXPORT_OK = qw(
bin_loc
get_version_and_os
indicate_progress
install_string
......@@ -41,6 +42,21 @@ our @EXPORT_OK = qw(
vers_cmp
);
sub bin_loc {
my ($bin) = @_;
return '' if ON_WINDOWS;
# Don't print any errors from "which"
open(my $saveerr, ">&STDERR");
open(STDERR, '>/dev/null');
my $loc = `which $bin`;
close(STDERR);
open(STDERR, ">&", $saveerr);
my $exit_code = $? >> 8; # See the perlvar manpage.
return '' if $exit_code > 0;
chomp($loc);
return $loc;
}
sub get_version_and_os {
# Display version information
my @os_details = POSIX::uname;
......@@ -340,6 +356,11 @@ export them.
=over
=item C<bin_loc>
On *nix systems, given the name of a binary, returns the path to that
binary, if the binary is in the C<PATH>.
=item C<get_version_and_os>
Returns a hash containing information about what version of Bugzilla we're
......
#!/usr/bin/perl -w
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by Everything Solved are Copyright (C) 2007
# Everything Solved, Inc. All Rights Reserved.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
use warnings;
# Have to abs_path('.') or calls to Bugzilla modules won't work once
# CPAN has chdir'ed around. We do all of this in this funny order to
# make sure that we use the lib/ modules instead of the base Perl modules,
# in case the lib/ modules are newer.
use Cwd qw(abs_path);
use lib abs_path('.');
use Bugzilla::Constants;
use lib abs_path(bz_locations()->{ext_libpath});
use Bugzilla::Install::CPAN;
use Bugzilla::Constants;
use Bugzilla::Install::Requirements;
use Data::Dumper;
use Getopt::Long;
use Pod::Usage;
our %switch;
GetOptions(\%switch, 'all|a', 'upgrade-all|u', 'show-config|s', 'global|g',
'help|h');
pod2usage({ -verbose => 1 }) if $switch{'help'};
pod2usage({ -verbose => 0 }) if (!%switch && !@ARGV);
set_cpan_config($switch{'global'});
if ($switch{'show-config'}) {
print Dumper($CPAN::Config);
exit;
}
my $can_notest = 1;
if (substr(CPAN->VERSION, 0, 3) < 1.8) {
$can_notest = 0;
print "* Note: If you upgrade your CPAN module, installs will be faster.\n";
print "* You can upgrade CPAN by doing: $^X install-module.pl CPAN\n";
}
if ($switch{'all'} || $switch{'upgrade-all'}) {
my @modules;
if ($switch{'upgrade-all'}) {
@modules = (@{REQUIRED_MODULES()}, @{OPTIONAL_MODULES()});
push(@modules, DB_MODULE->{$_}->{dbd}) foreach (keys %{DB_MODULE()});
}
else {
# This is the only time we need a Bugzilla-related module, so
# we require them down here. Otherwise this script can be run from
# any directory, even outside of Bugzilla itself.
my $reqs = check_requirements(0);
@modules = (@{$reqs->{missing}}, @{$reqs->{optional}});
my $dbs = DB_MODULE;
foreach my $db (keys %$dbs) {
push(@modules, $dbs->{$db}->{dbd})
if !have_vers($dbs->{$db}->{dbd}, 0);
}
}
foreach my $module (@modules) {
my $cpan_name = $module->{module};
# --all shouldn't include mod_perl2, because it can have some complex
# configuration, and really should be installed on its own.
next if $cpan_name eq 'mod_perl2';
install_module($cpan_name, $can_notest);
}
my $dbs = DB_MODULE;
foreach my $db (keys %$dbs) {
install_module($dbs->{$db}->{dbd}->{module}, $can_notest)
unless have_vers($dbs->{$db}->{dbd}, 0);
}
}
foreach my $module (@ARGV) {
install_module($module, $can_notest);
}
__END__
=head1 NAME
install-module.pl - Installs or upgrades modules from CPAN
=head1 SYNOPSIS
./install-module.pl Module::Name [--global]
./install-module.pl --all [--global]
./install-module.pl --all-upgrade [--global]
./install-module.pl --show-config
Do "./install-module.pl --help" for more information.
=head1 OPTIONS
=over
=item B<Module::Name>
The name of a module that you want to install from CPAN. This is the
same thing that you'd give to the C<install> command in the CPAN shell.
You can specify multiple module names separated by a space to install
multiple modules.
=item B<--global>
This makes install-module install modules globally for all applications,
instead of just for Bugzilla.
On most systems, you have to be root for C<--global> to work.
=item B<--all>
This will make install-module do its best to install every required
and optional module that is not installed that Bugzilla can use.
Some modules may fail to install. You can run checksetup.pl to see
which installed properly.
=item B<--upgrade-all>
This is like C<--all>, except it forcibly installs the very latest
version of every Bugzilla prerequisite, whether or not you already
have them installed.
=item B<--show-config>
Prints out the CPAN configuration in raw Perl format. Useful for debugging.
=item B<--help>
Shows this help.
=back
......@@ -35,6 +35,15 @@
checking_modules => 'Checking perl modules...',
header => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n"
. "* Running on ##os_name## ##os_ver##",
install_all => <<EOT,
To attempt an automatic install of every required and optional module
with one command, do:
##perl## install-module.pl --all
EOT
install_module => 'Installing ##module## version ##version##...',
module_found => "found v##ver##",
module_not_found => "not found",
module_ok => 'ok',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment