#!/bin/sh
eval 'if [ -x /usr/local/cpanel/3rdparty/bin/perl ]; then exec /usr/local/cpanel/3rdparty/bin/perl -x -- $0 ${1+"$@"}; else exec /usr/bin/perl -x $0 ${1+"$@"}; fi;'
  if 0;

#!/usr/bin/perl
# cpanel - cp_util/ea-build.pl                    Copyright(c) 2015 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
#
# This script contains the guts of building and publishing EasyApache, as well
# as notifying others that a new EasyApache build has been released.
#
# It should not be run by hand, since it's called by the Build System.
#
# Much of the code is a direct copy of build_easyapache.pl, with the
# exception that a portion of what the old script USED to do, is
# now apart of ea-build-tool (in the build-manager repo).

package cp_util::ea_build;

BEGIN {
    unshift @INC, '/usr/local/cpanel/build-tools', '/usr/local/cpanel';

    # Added missing cpanel path for git binary (Case 75957)
    unless ( $ENV{PATH} =~ m#/usr/local/cpanel/3rdparty/bin# ) {
        $ENV{PATH} = "/usr/local/cpanel/3rdparty/bin:$ENV{PATH}";
    }
}

use strict;
use warnings;
use Cwd                      ();
use File::Find               ();
use Mail::Sender::Easy       ();
use Cpanel::FileUtils::Copy  ();
use Cpanel::SafeRun::Errors  ();
use Cpanel::SafeDir          ();
use Cpanel::Version::Compare ();
use cPBuild::JIRA            ();
use cPBuild::Func            ();
use cPBuild::Git             ();
use cPBuild::SCPS            ();

our $QUIET  = 0;
our $DEBUG  = 0;
our $DRYRUN = 0;

our $GitHost    = 'enterprise.cpanel.net';
our $Cpdist_dir = '/usr/local/cpdist';
our $QaBuildDir = '/home/qabuild';
our $Jira_url   = 'https://jira.cpanel.net';
our $FROM_ADDR  = q{buildbox@cpanel.net};

# These are the files and directories that are distributed
# to the public
our %collections = (
    'Cpanel'   => [], 'cp_util' => [], 'php_confs' => [],
    'profiles' => [], 'targz'   => [],
    'root' => [ 'cpdist/distlist-easy', 'cpdist/version_easy', 'cpdist/targz.yaml' ]
);

sub info {
    my $t = localtime;
    print "[$t] @_\n" unless $QUIET;
}

sub debug {
    return 1 unless $DEBUG;
    my $t = localtime;
    print "[$t] DEBUG: @_\n";
}

sub error {
    my $t = localtime;
    die "[$t] ERROR: @_\n";
}

# Wrapper so that I don't have duplicate this check everywhere
sub newgit {
    my $repo = shift;
    my $git;

    eval {
        # -d check to prevent uninitialized value warning in cPBuild
        error("Missing repo directory, $repo") unless -d $repo;
        $git = new cPBuild::Git( working_copy => $repo );
    };

    error("Unable to initialize cPBuild::Git, $@") if $@;

    return $git;
}

sub display_env {
    return 1 unless $DEBUG;
    debug("Environment Settings:");
    while ( my ( $key, $val ) = each(%ENV) ) {
        debug("\t$key=$val");
    }
}

sub run_command {
    my $aref = shift;    # command, in array form
    my $sref = shift;    # where to store stdout
    my $tmp;
    $sref = \$tmp unless defined $sref;    # doesn't care about output

    my $cwd = Cwd::getcwd();
    debug("(PWD:$cwd) Running local command: @$aref");
    $$sref = ${ Cpanel::SafeRun::Simple::_saferun_r( $aref, 1 ) };    # saferun returns scalar ref

    if ( $? == -1 || $? & 127 || ( $? >> 8 ) != 0 ) {
        error("Failed to execute, @$aref (exit $?):\n$$sref");
    }

    return 1;
}

# Generic routine to mock JIRA url
sub jira_url {
    my $tag = shift;
    return qq#$Jira_url/issues/?jql=labels%20%3D%20easyapache_${tag}#;
}

# Determine which directories are to be included in tarballs
sub exclude_dir {
    my ($dirname) = @_;

    # Exclude version control metadata
    return 1 if $dirname =~ m/\.(?:git|svn)$/;

    # Exclude unpacked tarball source directories (Case 66501)
    return 1 if $dirname =~ m/\.pm.*\.tar\.gz.*\.d$/;

    return;
}

sub lock_update {
    my $host = shift;
    my $tag  = shift;
    my $tux  = sprintf( 'tux@%s', $host );
    debug("Locking qabuild for update");
    run_command( [ '/usr/bin/ssh', $tux, qq{$QaBuildDir/bin/locklatest}, '--dir', qq{$QaBuildDir/cpanelsync}, qq{easy-$tag} ] );
}

sub unlock_update {
    my $host = shift;
    my $tag  = shift;
    my $tux  = sprintf( 'tux@%s', $host );
    debug("Unlocking qabuild for update");
    run_command( [ '/usr/bin/ssh', $tux, qq{$QaBuildDir/bin/unlocklatest}, '--dir', qq{$QaBuildDir/cpanelsync}, qq{easy-$tag} ] );
}

# Update EasyApache files with correct version information
sub update_version {
    my $repodir = shift;
    my $tag     = shift;

    debug("Updating version information");

    if ( !-d $Cpdist_dir ) {
        Cpanel::SafeDir::safemkdir($Cpdist_dir);
    }

    # Update version file
    if ( open my $vers_fh, '>', qq{$Cpdist_dir/version_easy} ) {
        print {$vers_fh} $tag;
        close $vers_fh;
    }
    else {
        error("Failed to update version_easy file: $!");
    }

    # Update version in Cpanel::Easy::Apache
    if ( open my $vers_fh, '+<', "$repodir/Cpanel/Easy/Apache.pm" ) {
        my @lines;
        while ( my $line = readline $vers_fh ) {
            if ( $line =~ m/\[\%\s+version\s+\%\]/ ) {
                $line =~ s/\[\%\s+version\s+\%\]/$tag/;
            }
            push @lines, $line;
        }
        seek( $vers_fh, 0, 0 );
        print {$vers_fh} join( '', @lines );
        truncate( $vers_fh, tell($vers_fh) );
        close $vers_fh;
    }
    else {
        error("Unable to update version in Cpanel::Easy::Apache: $!");
    }
}

# Update targz.yaml using newly built one.  This is used by EasyApache to
# verify checksums of downloaded software.
sub update_targz {
    my $repodir = shift;

    debug("Updating targz.yaml");

    my $src = sprintf( '%s/targz.yaml', $repodir );
    my $dst = sprintf( '%s/targz.yaml', $Cpdist_dir );

    unlink $dst;
    Cpanel::FileUtils::Copy::safecopy( $src, $dst ) or error("Unable to copy to $dst: $!");
}

# Update distlist: this explains what version of software is used and the
# the software that should be downloaded to update a client.
sub update_distlist {
    my $tag = shift;

    debug("Updating distlist-easy");

    my $path = qq{$Cpdist_dir/distlist-easy};

    # Update distlist file
    if ( open my $fh, '>', $path ) {
        print {$fh} "version 1.0\n";
        print {$fh} "tree:easy:distversion:$tag:arch:NA:system:NA\n";
        foreach my $name ( sort keys %collections ) {
            next if $name eq 'root';
            print {$fh} $name . "\n";
        }

        # print {$distlist_fh} 'targz.yaml' . "\n";
        close $fh;
    }
    else {
        error("Failed to create distribution list, $path: $!");
    }
}

sub build {
    my $opts    = shift;
    my $repodir = $opts->{'repo'};
    my $tag     = $opts->{'totag'};

    debug("build: Begin");

    if ( !defined $repodir || !defined $tag ) {
        error("Missing parameters");
        exit 1;
    }

    info("Building EasyApache $tag");

    my $git = newgit($repodir);

    # Update version info before we build EasyApache
    update_version( $repodir, $tag );

    # Build EasyApache
    my $stdout;
    run_command( [ 'make', '-C', $repodir, 'unittest' ], \$stdout );
    debug($stdout);

    # Make sure we distribute the newly generated targz.yaml
    update_targz($repodir);

    # Update distribution config
    update_distlist($tag);

    info("Build complete");

    debug("build: End");

    # great success
    return 1;
}

# Updates the symlink on the remote server to point the 'easy' link to
# the version requested.  We use 'easy' to maintain backwards compatibility.
# This ensures that easyapache doesn't need to change very much, because
# we can allow it to continue looking at this directory for updates, just
# as it always has.
sub set_stable {
    my $totag = shift;
    my $host  = shift;
    my $tux   = sprintf( 'tux@%s', $host );

    # For some reason, -f isn't overwriting the existing symlink on
    # branch-build, so the 'rm' command comes first, then the 'ln'.
    # NOTE: Could introduce a race condition where someone is
    #  trying to update while this is publishing to the test server.
    #  But, it's an internal only problem.  Hopefully it's not a
    #  big deal.
    run_command( [ q{/usr/bin/ssh}, $tux, q{/bin/rm}, q{-f}, qq{$QaBuildDir/cpanelsync/easy} ] );
    run_command( [ q{/usr/bin/ssh}, $tux, q{/bin/ln}, q{-sf}, qq{easy-$totag}, qq{$QaBuildDir/cpanelsync/easy} ] );

    return ( $@ ? 0 : 1 );
}

# NOTE: This depends on build() already being called.
sub publish {
    my $opts      = shift;
    my $repodir   = $opts->{'repo'};
    my $publishto = $opts->{'publish-to'};    # which server the code is installed onto
    my $totag     = $opts->{'totag'};
    my $setstable = $opts->{'set-stable'};    # tells publish to update 'easy' symlink

    debug("publish: Begin");

    if ( !defined $repodir || !defined $publishto || !defined $totag ) {
        error("Missing parameters");
        exit 1;
    }

    my $git = newgit($repodir);

    # verify correct usage before bailing out for dryrun
    if ($DRYRUN) {
        info("Dryrun enabled.  Not publishing to $publishto ($totag)");
        return 1;
    }

    info("Publishing EasyApache $totag");

    my $update_location = sprintf( 'tux@%s', $publishto );

    lock_update( $publishto, $totag );

    my $scps = cPBuild::SCPS->new(
        'tree'        => "easy-$totag",
        'distversion' => $totag,
        'arch'        => 'NA',
        'system'      => 'NA',
        'verbose'     => 1,
        'location'    => $update_location,
    );

    my $olddir = Cwd::getcwd();    # save because publish changes cwd

    # Publish file collections
    foreach my $name ( sort keys %collections ) {
        if ( $name eq 'root' ) {
            chdir '/usr/local' or error("Unable to chdir in to /usr/local");
        }
        else {
            chdir $repodir or error("Unable to chdir into $repodir: $!");

            # Generate file lists
            my $wanted = sub {
                if ( -d && exclude_dir($_) ) {
                    $File::Find::prune = 1;
                    return;
                }
                return if (/\.ptar$/);        # Exclude Pristine tar files
                return if (/^\._/);           # Exclude TextMate backup files
                return if (/~$/);             # Exclude vim backup files
                return if (/^\.DS_Store/);    # Exclude Mac desktop files

                push @{ $collections{$name} }, $File::Find::name;
            };

            File::Find::find( { 'wanted' => $wanted }, "$repodir/$name" );
            debug( "NAME: $name\n" . join( "\n", sort @{ $collections{$name} } ) . "\n" );
        }

        # Publish files
        cPBuild::Func::createtarball(
            'name'  => $name,
            'files' => $collections{$name},
            'scps'  => $scps,
            'munge' => qr{^\Q$repodir/\E},
        );
    }

    chdir $olddir;    # restore so calling application isn't confused

    if ( $scps->quit() ) {

        # Update symlink on server, but don't let it prevent us
        # from unlocking if this happens to fail.  Also, don't
        # use error(), it will prevent an unlock
        if ($setstable) {
            eval { set_stable( $totag, $publishto ) };
            info($@) if $@;
        }
    }
    else {
        # don't use error() here, it will prevent an unlock
        info("ERROR: Publish transmit failure");
    }

    unlock_update( $publishto, $totag );

    info("Signing easyapache build...");
    my $signing_cmd = "/usr/local/scripts/sign-build /home/tux/.gnupg/ release-team\@cpanel.net --files /home/cpanel/cpanelsync/easy-$totag/{.cpanelsync,distlist-easy,targz.yaml,version_easy,*/.cpanelsync}";
    run_command( [ '/usr/bin/ssh', $update_location, $signing_cmd ] );

    debug("publish: End");
}

# Look through autogenerated changelog for Jira cases
sub parse_cases {
    my $changelog = shift;
    my %tmp = map { $_ => 1 } $changelog =~ m/case:?\s+([a-z]{2,6}-\d+)\:?/imsg;
    return ( sort keys %tmp );
}

# Looks at the difference between two tags and generates
# a changelog of commits.  Assumes ea-build-tool did
# its due-diligence to ensure that these two tags are
# infact, related.
sub generate_changelog {
    my $opts    = shift;
    my $repodir = $opts->{'repo'};
    my $totag   = $opts->{'totag'};
    my $fromtag = $opts->{'fromtag'};

    debug("generate_changelog: Begin");

    if ( !defined $repodir || !defined $totag || !defined $fromtag ) {
        error("Missing parameters");
        exit 1;
    }

    info("Generating ChangeLog for $totag");

    my $git = newgit($repodir);
    my @cases;

    # Generate a Jira message
    my $jr_url = jira_url($totag);
    my $jr_msg = "The relevant cases can be seen in Jira via this link: $jr_url";

    # "git" the log, and replace commit sha with link of the commit so that
    # people can actually look at the changes
    my $gitlog;
    debug("git log --no-merges $fromtag..$totag");
    eval { $gitlog = $git->log( '--no-merges', "$fromtag..$totag" ) };
    error("Unable to retrieve git log: @") if $@;

    $gitlog =~ s{^(commit\s+)([0-9a-f]+)}{https://$GitHost/projects/CPANEL/repos/easyapache/commits/$2}xmsg;

    my $changelog = $jr_msg . "\n\n" . $gitlog;

    # and return a list of parsed changelog cases
    @cases = parse_cases($changelog);

    debug("generate_changelog: End - Found the following cases: @cases");

    return ( $changelog, @cases );
}

sub update_jira {
    my $opts = shift;
    my %args = @_;

    my $totag = $opts->{'totag'};
    my $cases = $args{'cases'};
    my $jrtag = "easyapache_$totag";
    my @errors;

    debug("update_jira: Begin");

    if ( !defined $totag ) {
        error("Missing parameters");
        exit 1;
    }

    if ($DRYRUN) {
        warn "Dryrun enabled.  Not updating Jira";
        info("Jira Tag: $jrtag");
        info("Jira Cases: @$cases");
        return undef;    # returning a non-empty string designates error message
    }

    debug("Updating Jira with '$jrtag'");

    my $jira = cPBuild::JIRA->new( credentials_file => cPBuild::JIRA::default_buildbox_credential_file() );

    foreach my $case ( sort @$cases ) {
        my $tagged = eval { $jira->set_tag( $case, $jrtag ); 1; };
        if ( !$tagged ) {
            my $msg = "WARNING: Failed to tag Jira Case $case with '$jrtag'";
            warn("$msg ($@)");
            push @errors, $msg;
        }
        else {
            debug("Tagging Jira case $case");
        }
    }

    debug("update_jira: End");

    return join( "\n", @errors );
}

# Send out an e-mail
sub email {
    my $opts = shift;
    my %args = @_;

    my $totag     = $opts->{'totag'};
    my $emails    = $opts->{'emails'};
    my $pubserver = $opts->{'publish-to'};      # the server we published to
    my $changelog = $args{'changelog'};
    my $addmsg    = $args{'additional_msg'};    # any additional text that's appended to body of email

    debug("email: Begin");

    if ( !defined $totag || !defined $emails || !$pubserver ) {
        error("Missing parameters");
        exit 1;
    }

    $emails =~ s/\s*//g;
    my @to_addr = split( /\,/, $emails );

    # body of the e-mail
    my $body = "EasyApache $totag has been published to $pubserver.";
    $body .= "\n\n$addmsg" if $addmsg;
    $body .= "\n\n$changelog\n";

    if ($DRYRUN) {
        warn "Dryrun enabled.  Not sending e-mail";
        info("Tag: $totag");
        info("From Address: $FROM_ADDR");
        info("To Address(s): @to_addr");
        debug("Email Content:\n$body");
        return 1;
    }

    debug("Sending notification to: @to_addr");

    my $email_hr = { 'smtp' => 'mail.cpanel.net', 'from' => $FROM_ADDR, 'to' => [@to_addr], 'subject' => "[EasyApache Test] $totag has been published to $pubserver", '_text' => $body, 'priority' => 1, 'auth' => 'LOGIN', 'authid' => $cPBuild::Func::mail_user, 'authpwd' => $cPBuild::Func::mail_passwd, 'on_errors' => 'undef', };

    # STDERR is stiffled so just use STDOUT for now:
    $email_hr->{debug} = \*STDOUT if $DEBUG;
    Mail::Sender::Easy::email($email_hr) || print "WARNING: Failed to send email to [@to_addr]: $@\n";

    debug("email: End");
}

1;

package main;

use strict;
use warnings;
use Cpanel::Usage ();

sub usage {
    my $code = shift || 0;
    my $errmsg = shift;

    print "ERROR: $errmsg\n\n" if defined $errmsg;

    print <<'EO_USAGE';
ea_build.pl [OPTIONS]

This is used by the cPanel/WHM build system to create an EasyApache release.
If you're wanting to use the old EasyApache build script, look at
build_easyapache.pl.

Usage:

    Options:
        --help              Brief help message
        --debug|verbose     Turn debugging on
        --version           Display the version of this software
        --dryrun            Don't actually do anything, just pretend to
        --action <command>  Perform an action (shown below)

    Commands:
        --action build --repo <dir> --totag <tag>
            Build EasyApache into a publishable set of archives.

        --action publish --repo <dir> --publish-to <server> --totag <tag>
            Publish an EasyApache build to <server>.  Additionally,
            if you want to update the easyapache symlink 'easy' on
            the server which points to what cutomers use, then you
            must also pass '--set-stable'.

        --action notify --repo <dir> --emails <1@foo.com,2@foo.com> --publish-to <server> --fromtag <tag> --totag <tag>
            Update Jira and send an e-mail notifying people of the
            new build.

    Examples:
        --action build --dryrun --debug --repo /usr/local/easyapache --totag 3.20.6
        --action publish --repo /usr/local/easyapache --publish-to buildmonkey.dev.cpanel.net --totag 3.20.6
        --action notify --repo /usr/local/easyapache --emails easyapache@cpanel.net --publish-to buildmonkey.dev.cpanel.net --fromtag 3.20.5 --totag 3.20.6

EO_USAGE
    exit $code;
}

sub run {
    my @args = @_;
    my %opts;

    Cpanel::Usage::wrap_options( { remove => 1 }, \@args, \&usage, \%opts );

    $cp_util::ea_build::DEBUG = $opts{'debug'} || $opts{'verbose'};
    $cp_util::ea_build::DRYRUN = $opts{'dryrun'};

    cp_util::ea_build::debug("run: Begin");

    my $cmd = $opts{'action'};
    my $ret;

    unless ( defined $cmd ) {
        usage( 1, q{Missing --action} );
    }

    cp_util::ea_build::display_env();

    if ( $cmd eq 'build' ) {
        $ret = cp_util::ea_build::build( \%opts );
    }
    elsif ( $cmd eq 'publish' ) {
        $ret = cp_util::ea_build::publish( \%opts );
    }
    elsif ( $cmd eq 'notify' ) {

        # Gather list of cases that were added and generate changelog
        my ( $changelog, @cases ) = cp_util::ea_build::generate_changelog( \%opts );

        # Tag each case referenced by the changelog
        my $jrerr = cp_util::ea_build::update_jira( \%opts, 'cases' => [@cases] );

        # Send e-mail to mailing lists to notify folks about new build
        cp_util::ea_build::email( \%opts, 'changelog' => $changelog, 'additional_msg' => $jrerr );

        $ret = 1;
    }
    else {
        usage( 1, qq{Unknown command, $cmd} );
    }

    cp_util::ea_build::debug("run: End ($ret)");

    return $ret;
}

# If invoked as a shell script, run the main program.
unless ( caller() ) {
    exit( __PACKAGE__->run(@ARGV) ? 0 : 1 );    # UNIX exit code
}

