# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::Install::CPAN;

use 5.14.0;
use strict;
use warnings;

use base qw(Exporter);
our @EXPORT = qw(
  BZ_LIB

  check_cpan_requirements
  set_cpan_config
  install_module
);

use Bugzilla::Constants;
use Bugzilla::Install::Requirements qw(have_vers);
use Bugzilla::Install::Util qw(bin_loc install_string);

use Config;
use CPAN;
use Cwd qw(abs_path);
use File::Path qw(rmtree);

# These are required for install-module.pl to be able to install
# all modules properly.
use constant REQUIREMENTS => (
  {module => 'CPAN', package => 'CPAN', version => '1.81',},
  {
    # When Module::Build isn't installed, the YAML module allows
    # CPAN to read META.yml to determine that Module::Build first
    # needs to be installed to compile a module.
    module  => 'YAML',
    package => 'YAML',
    version => 0,
  },
  {
    # Many modules on CPAN are now built with Dist::Zilla, which
    # unfortunately means they require this version of EU::MM to install.
    module  => 'ExtUtils::MakeMaker',
    package => 'ExtUtils-MakeMaker',
    version => '6.31',
  },
);

# 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,
  build_requires_install_policy => 'yes',
  cache_metadata                => 1,
  colorize_output               => 1,
  colorize_print                => 'bold',
  index_expire                  => 1,
  scan_cache                    => 'atstart',

  inhibit_startup_message => 1,

  bzip2 => bin_loc('bzip2'),
  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 => ['http://www.cpan.org/'],
};

sub check_cpan_requirements {
  my ($original_dir, $original_args) = @_;

  _require_compiler();

  my @install;
  foreach my $module (REQUIREMENTS) {
    my $installed = have_vers($module, 1);
    push(@install, $module) if !$installed;
  }

  return if !@install;

  my $restart_required;
  foreach my $module (@install) {
    $restart_required = 1 if $module->{module} eq 'CPAN';
    install_module($module->{module}, 1);
  }

  if ($restart_required) {
    chdir $original_dir;
    exec($^X, $0, @$original_args);
  }
}

sub _require_compiler {
  my @errors;

  my $cc_name   = $Config{cc};
  my $cc_exists = bin_loc($cc_name);

  if (!$cc_exists) {
    push(@errors, install_string('install_no_compiler'));
  }

  my $make_name   = $CPAN::Config->{make};
  my $make_exists = bin_loc($make_name);

  if (!$make_exists) {
    push(@errors, install_string('install_no_make'));
  }

  die @errors if @errors;
}

sub install_module {
  my ($name, $test) = @_;
  my $bzlib = BZ_LIB;

  # Make Module::AutoInstall install all dependencies and never prompt.
  local $ENV{PERL_AUTOINSTALL} = '--alldeps';

  # This makes Net::SSLeay not prompt the user, if it gets installed.
  # It also makes any other MakeMaker prompts accept their defaults.
  local $ENV{PERL_MM_USE_DEFAULT} = 1;

  # 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 'SOAP::Lite') {
    $CPAN::Config->{makepl_arg} .= " --noprompt";
  }

  my $module = CPAN::Shell->expand('Module', $name);
  if (!$module) {
    die install_string('no_such_module', {module => $name}) . "\n";
  }

  print install_string('install_module',
    {module => $name, version => $module->cpan_version})
    . "\n";

  if ($test) {
    CPAN::Shell->force('install', $name);
  }
  else {
    CPAN::Shell->notest('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 STDERR install_string('cpan_bugzilla_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) {
    require Config;

    $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\""
      . " --install_path lib=\"$bzlib\""
      . " --install_path arch=\"$bzlib/$Config::Config{archname}\"";
    $CPAN::Config->{mbuild_install_arg} = $CPAN::Config->{mbuild_arg};

    # 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');

=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<$test> - If true, we run tests on this module before installing,
but we still force the install if the tests fail. This is only used
when we internally install a newer CPAN module.

=back

Note that calling this function prints a B<lot> of information to
STDOUT and STDERR.

=back

=head1 B<Methods in need of POD>

=over

=item check_cpan_requirements

=back
