The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use 5.012;
use warnings;

package MYIPC::Run3; #stolen from IPC::Run3

use constant is_win32  => 0 <= index $^O, "Win32";

BEGIN {
   if ( is_win32 ) {
      eval "use Win32 qw( GetOSName ); 1" or die $@;
   }
}

use Carp qw( croak );
use File::Temp qw( tempfile );
use POSIX qw( dup dup2 );

# We cache the handles of our temp files in order to
# keep from having to incur the (largish) overhead of File::Temp
my %fh_cache;
my $fh_cache_pid = $$;

sub _binmode {
    my ( $fh, $mode, $what ) = @_;
    # if $mode is not given, then default to ":raw", except on Windows,
    # where we default to ":crlf";
    # otherwise if a proper layer string was given, use that,
    # else use ":raw"
    my $layer = !$mode
       ? (is_win32 ? ":crlf" : ":raw")
       : ($mode =~ /^:/ ? $mode : ":raw");

    binmode $fh, ":raw" unless $layer eq ":raw";      # remove all layers first
    binmode $fh, $layer or croak "binmode $layer failed: $!";
}

sub _spool_data_to_child {
    my ( $type, $source, $binmode_it ) = @_;

    # If undef (not \undef) passed, they want the child to inherit
    # the parent's STDIN.
    return undef unless defined $source;

    my $fh;
    if ( ! $type ) {
        open $fh, "<", $source or croak "$!: $source";
       _binmode($fh, $binmode_it, "STDIN");
    } elsif ( $type eq "FH" ) {
        $fh = $source;
    } else {
        $fh = $fh_cache{in} ||= tempfile;
        truncate $fh, 0;
        seek $fh, 0, 0;
       _binmode($fh, $binmode_it, "STDIN");
        my $seekit;
        if ( $type eq "SCALAR" ) {

            # When the run3()'s caller asks to feed an empty file
            # to the child's stdin, we want to pass a live file
            # descriptor to an empty file (like /dev/null) so that
            # they don't get surprised by invalid fd errors and get
            # normal EOF behaviors.
            return $fh unless defined $$source;  # \undef passed

            $seekit = length $$source;
            print $fh $$source or die "$! writing to temp file";

        } elsif ( $type eq "ARRAY" ) {
            print $fh @$source or die "$! writing to temp file";
            $seekit = grep length, @$source;
        } elsif ( $type eq "CODE" ) {
            my $parms = [];  # TODO: get these from $options
            while (1) {
                my $data = $source->( @$parms );
                last unless defined $data;
                print $fh $data or die "$! writing to temp file";
                $seekit = length $data;
            }
        }

        seek $fh, 0, 0 or croak "$! seeking on temp file for child's stdin"
            if $seekit;
    }

    croak "run3() can't redirect $type to child stdin"
        unless defined $fh;

    return $fh;
}

sub _fh_for_child_output {
    my ( $what, $type, $dest, $options ) = @_;

    my $fh;
    if ( $type eq "SCALAR" && $dest == \undef ) {
        $fh = $fh_cache{nul} ||= do {
            open $fh, ">", File::Spec->devnull;
           $fh;
        };
    } elsif ( $type eq "FH" ) {
        $fh = $dest;
    } elsif ( !$type ) {
        open $fh, $options->{"append_$what"} ? ">>" : ">", $dest
           or croak "$!: $dest";
    } else {
        $fh = $fh_cache{$what} ||= tempfile;
        seek $fh, 0, 0;
        truncate $fh, 0;
    }

    my $binmode_it = $options->{"binmode_$what"};
    _binmode($fh, $binmode_it, uc $what);

    return $fh;
}

sub _read_child_output_fh {
    my ( $what, $type, $dest, $fh, $options ) = @_;

    return if $type eq "SCALAR" && $dest == \undef;

    seek $fh, 0, 0 or croak "$! seeking on temp file for child $what";

    if ( $type eq "SCALAR" ) {
        # two read()s are used instead of 1 so that the first will be
        # logged even it reads 0 bytes; the second won't.
        my $count = read $fh, $$dest, 10_000,
           $options->{"append_$what"} ? length $$dest : 0;
        while (1) {
            croak "$! reading child $what from temp file"
                unless defined $count;

            last unless $count;

            $count = read $fh, $$dest, 10_000, length $$dest;
        }
    } elsif ( $type eq "ARRAY" ) {
       if ($options->{"append_$what"}) {
           push @$dest, <$fh>;
       } else {
           @$dest = <$fh>;
       }
    } elsif ( $type eq "CODE" ) {
        local $_;
        while ( <$fh> ) {
            $dest->( $_ );
        }
    } else {
        croak "run3() can't redirect child $what to a $type";
    }

}

sub _type {
    my ( $redir ) = @_;

    return "FH" if eval {
        local $SIG{'__DIE__'};
        $redir->isa("IO::Handle")
    };

    my $type = ref $redir;
    return $type eq "GLOB" ? "FH" : $type;
}

sub _max_fd {
    my $fd = dup(0);
    POSIX::close $fd;
    return $fd;
}

my $run_call_time;
my $sys_call_time;
my $sys_exit_time;

sub run3 {
    my $options = @_ && ref $_[-1] eq "HASH" ? pop : {};

    my ( $cmd, $stdin, $stdout, $stderr ) = @_;

    if ( ref $cmd ) {
        croak "run3(): empty command"     unless @$cmd;
        croak "run3(): undefined command" unless defined $cmd->[0];
        croak "run3(): command name ('')" unless length  $cmd->[0];
    } else {
        croak "run3(): missing command" unless @_;
        croak "run3(): undefined command" unless defined $cmd;
        croak "run3(): command ('')" unless length  $cmd;
    }

    foreach (qw/binmode_stdin binmode_stdout binmode_stderr/) {
       if (my $mode = $options->{$_}) {
           croak qq[option $_ must be a number or a proper layer string: "$mode"]
              unless $mode =~ /^(:|\d+$)/;
       }
    }

    my $in_type  = _type $stdin;
    my $out_type = _type $stdout;
    my $err_type = _type $stderr;

    if ($fh_cache_pid != $$) {
       # fork detected, close all cached filehandles and clear the cache
       close $_ foreach values %fh_cache;
       %fh_cache = ();
       $fh_cache_pid = $$;
    }

    # This routine proceeds in stages so that a failure in an early
    # stage prevents later stages from running, and thus from needing
    # cleanup.

    my $in_fh  = _spool_data_to_child $in_type, $stdin,
        $options->{binmode_stdin} if defined $stdin;

    my $out_fh = _fh_for_child_output "stdout", $out_type, $stdout,
        $options if defined $stdout;

    my $tie_err_to_out =
        defined $stderr && defined $stdout && $stderr eq $stdout;

    my $err_fh = $tie_err_to_out
        ? $out_fh
        : _fh_for_child_output "stderr", $err_type, $stderr,
            $options if defined $stderr;

    # this should make perl close these on exceptions
#    local *STDIN_SAVE;
    local *STDOUT_SAVE;
    local *STDERR_SAVE;

    my $saved_fd0 = dup( 0 ) if defined $in_fh;

#    open STDIN_SAVE,  "<&STDIN"#  or croak "run3(): $! saving STDIN"
#        if defined $in_fh;
    open STDOUT_SAVE, ">&STDOUT" or croak "run3(): $! saving STDOUT"
        if defined $out_fh;
    open STDERR_SAVE, ">&STDERR" or croak "run3(): $! saving STDERR"
        if defined $err_fh;

    my $errno;
    my $ok = eval {
        # The open() call here seems to not force fd 0 in some cases;
        # I ran in to trouble when using this in VCP, not sure why.
        # the dup2() seems to work.
        dup2( fileno $in_fh, 0 )
#        open STDIN,  "<&=" . fileno $in_fh
            or croak "run3(): $! redirecting STDIN"
            if defined $in_fh;

#        close $in_fh or croak "$! closing STDIN temp file"
#            if ref $stdin;

        open STDOUT, ">&" . fileno $out_fh
            or croak "run3(): $! redirecting STDOUT"
            if defined $out_fh;

        open STDERR, ">&" . fileno $err_fh
            or croak "run3(): $! redirecting STDERR"
            if defined $err_fh;

        my $r = ref $cmd
              ? system { $cmd->[0] }
                       is_win32
                           ? map {
                                 # Probably need to offer a win32 escaping
                                 # option, every command may be different.
                                 ( my $s = $_ ) =~ s/"/"""/g;
                                 $s = qq{"$s"};
                                 $s;
                             } @$cmd
                           : @$cmd
              : system $cmd;

       $errno = $!;              # save $!, because later failures will overwrite it

        croak $! if defined $r && $r == -1 && !$options->{return_if_system_error};

        1;
    };
    my $x = $@;

    my @errs;

    if ( defined $saved_fd0 ) {
        dup2( $saved_fd0, 0 );
        POSIX::close( $saved_fd0 );
    }

#    open STDIN,  "<&STDIN_SAVE"#  or push @errs, "run3(): $! restoring STDIN"
#        if defined $in_fh;
    open STDOUT, ">&STDOUT_SAVE" or push @errs, "run3(): $! restoring STDOUT"
        if defined $out_fh;
    open STDERR, ">&STDERR_SAVE" or push @errs, "run3(): $! restoring STDERR"
        if defined $err_fh;

    croak join ", ", @errs if @errs;

    die $x unless $ok;

    _read_child_output_fh "stdout", $out_type, $stdout, $out_fh, $options
        if defined $out_fh && $out_type && $out_type ne "FH";
    _read_child_output_fh "stderr", $err_type, $stderr, $err_fh, $options
        if defined $err_fh && $err_type && $err_type ne "FH" && !$tie_err_to_out;

    $! = $errno;              # restore $! from system()

    return 1;
}

package main;

use Storable              qw(nstore);
use Data::Dumper          qw(Dumper);
use Getopt::Long          qw(GetOptions);
use File::Spec::Functions qw(catfile);
use Config;
use FindBin;
warn ">> started '$0'\n";

# parse commandline options
my @spec = (
    'module=s@',
    'install_to=s',
    'url=s',
    'verbose=i',
    'skiptest=i',
    'uninstall=i',
    'ignore_testfailure=i',
    'ignore_uptodate=i',
    'prereqs=i',
    'interactivity=i',
    'makefilepl_param=s',
    'buildpl_param=s',
    'signature=i',
    'out_dumper=s',
    'out_nstore=s',
);
my %a = ();
GetOptions(\%a, @spec) or die ">> invalid option(s)";

# defaults
$a{module}      //= [];
$a{install_to}  //= '';
$a{url}         //= ''; #'http://cpan.strawberryperl.com';
$a{verbose}     //= 1;
$a{uninstall}   //= 0;
$a{skiptest}           //= 0; # 1 = do not run 'test' at all
$a{ignore_testfailure} //= 0; # 1 = if 'test' fails continue with 'install'
$a{ignore_uptodate}    //= 0; # 1 = install even if the module is already uptodate
$a{prereqs}            //= 1; # 0 = Do not install, 1 = Install, 2 = Ask, 3 = Ignore
$a{interactivity}      //= 0; # 1 = allow_build_interactivity
$a{makefilepl_param}   //= '';
$a{buildpl_param}      //= '';
$a{signature}          //= 0; # 0 = ignore signature, 1 = check signature if available
$a{out_dumper}         //= "install-log.$$.dumper.txt";
$a{out_nstore}         //= "install-log.$$.nstore.txt";

$a{url} =~ s|/$||;
for (@{$a{module}}) {
  $_ =~ s/-/::/g unless $_ =~ /[\/\.]/;
}

### SUBROUTINES

sub save_output {
  my ($data, $out_nstore, $out_dumper) = @_;

  if ($out_nstore) {
    warn ">> storing results via Storable to '$out_nstore'\n";
    nstore($data, $out_nstore) or die ">> store failed";;
  }

  if ($out_dumper) {
    warn ">> storing results via Data::Dumper to '$out_nstore'\n";
    open my $fh, ">", $out_dumper or die ">> open: $!";
    print $fh Dumper($data) or die ">> print: $!";
    close $fh or die ">> close: $!";
  }
}

### MAIN

die ">> invalid install_to option" if $a{install_to} && $a{install_to} !~ /(perl|site|vendor)/;
die ">> invalid prereqs option (only 0, 1 or 3 allowed)" if defined $a{prereqs} && $a{prereqs} !~ /^(0|1|3)$/;
die ">> no modules specified" unless scalar(@{$a{module}});

my $success = 1;
my $env = {};
my @args = ($^X, "$FindBin::Bin/cpanm");

push @args, @{$a{module}};
push @args, '--verbose'         if $a{verbose};
push @args, '--notest'          if $a{skiptest};
push @args, '--force'           if $a{ignore_testfailure};
push @args, '--reinstall'       if $a{ignore_uptodate};
push @args, '--interactive'     if $a{interactivity};
push @args, '--uninstall'       if $a{uninstall};
push @args, '--mirror', $a{url}, '--mirror-only' if $a{url};
push @args, '--configure-args', ($a{buildpl_param} || $a{makefilepl_param}) if $a{makefilepl_param} || $a{buildpl_param};

if ($a{install_to} eq 'site') {
  $env->{PERL_MM_OPT}="INSTALLDIRS=site UNINST=1";      # INSTALL_BASE=$Config{sitelibexp}
  $env->{PERL_MB_OPT}="--installdirs=site --uninst=1";  # --install_base=$Config{vendorlibexp}
}
elsif ($a{install_to} eq 'vendor') {
  $env->{PERL_MM_OPT}="INSTALLDIRS=vendor UNINST=1";    # INSTALL_BASE=$Config{vendorlibexp}
  $env->{PERL_MB_OPT}="--installdirs=vendor uninst=1";  # --install_base=$Config{vendorlibexp}
}
elsif ($a{install_to} eq 'perl' || $a{install_to} eq 'core') {
  $env->{PERL_MM_OPT}="INSTALLDIRS=perl UNINST=1";      # INSTALL_BASE=$Config{vendorlibexp}
  $env->{PERL_MB_OPT}="--installdirs=core --uninst=1";  # --install_base=$Config{vendorlibexp}
}
else {
  $env->{PERL_MM_OPT}='UNINST=1';
  $env->{PERL_MB_OPT}='--uninst=1';
}

### --configure-args, --build-args, --test-args, --install-args
# $a{prereqs}            //= 1; # 0 = Do not install, 1 = Install, 2 = Ask, 3 = Ignore
# $a{signature}          //= 0; # 0 = ignore signature, 1 = check signature if available

my ($exit_code, $out);
{
  my $rv;
  my %original_env = %ENV;
  local %ENV;
  %ENV = (%original_env, %$env);
  warn ">> ", join ' ', @{$a{module}}, "\n";
  if (eval 'use IPC::Run3; 1') {
    $rv = IPC::Run3::run3(\@args, \undef, \$out, \$out);
  }
  else {
    warn "Fallback to `MYIPC::Run3`\n";
    $rv = MYIPC::Run3::run3(\@args, \undef, \$out, \$out);
  }
  $exit_code = $? // -666;
  $success = $rv && $exit_code == 0 ? 1 : 0;
}

say "###\n", $out, "###";
say "###\n", Dumper(\@args), "###";

my @list = split /[\n\r]+/, $out;
@list = map { s/[\r\n]*$//; $_ } @list;
@list = grep { /^Successfully (re)?installed (\S+)/ } @list;
@list = map { s/^Successfully (re)?installed (\S+).*$/$2/; $_ } @list;

save_output({installed => \@list, success=>$success}, $a{out_nstore}, $a{out_dumper});
die ">> FAILUE [exit_code=$exit_code]\n" unless $success;
warn ">> done!\n";
exit 0;