#!/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 - build-tools/build_easyapache.pl        Copyright(c) 2013 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package Cpanel::Easy::Build;

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

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

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

our $GitHost    = 'enterprise.cpanel.net';
our $Clone_url  = qq{https://$GitHost/scm/cpanel/easyapache.git};
our $Base_dir   = '/usr/local/ea';
our $Repo_dir   = 'easyapache-git';
our $Cpdist_dir = '/usr/local/cpdist';

our $JIRA_URL      = 'http://jira.cpanel.net';
our $Update_host   = 'httpupdate0.cpanel.net';    # server where easyapache is published
our $Update_user   = 'tux';
our $Published_dir = '/cpanelsync/easy';          # sub directory of /home/qabuild where we publish easyapache
our $Update_server = 'qa-build.cpanel.net';       # server we internally access easyapache

our $FROM_ADDR = q{buildbox@cpanel.net};
our @TO_ADDR   = (qw{ qa@cpanel.net dev@cpanel.net easyapache@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";
}

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

# Generic routine so that we can mock with unit tests
sub find_bin {
    my $bin = shift;
    my $path;

    for my $dir (qw( /bin /usr/bin /usr/local/bin )) {
        my $p = "$dir/$bin";
        if ( -x $p ) {
            $path = $p;
            last;
        }
    }

    return $path;
}

# Generic routine so we can mock with unit tests
sub repo_path {
    return sprintf( '%s/%s', $Base_dir, $Repo_dir );
}

# Generic routine to mock Jira url
sub jira_url {
    my $tag = shift;

    # This url will use search query 'project = EAL AND labels = <tag>'
    return qq#$JIRA_URL/issues/?jql=project%20%3D%20EAL%20AND%20labels%20%3D%20${tag}#;
}

# Generic routine to publish url
sub publish_url {
    return sprintf( '%s%s', $Update_server, $Published_dir );
}

# Wrapper routine to run system commands and auto handle errors
sub run_cmd_nofail {
    my @cmd = @_;

    debug( "Running command: " . join( ' ', @cmd ) . "\n" );

    my $exit_code = system(@cmd);
    $exit_code = $exit_code >> 8;
    if ( $exit_code != 0 ) {
        die "Failed with exit code $exit_code while running: " . join( ' ', @cmd ) . "\n";
    }
}

# Retrieve current released version of EasyApache (on qa-build)
sub current_build {
    my $curl = find_bin('curl');
    die "Unable to find curl binary" unless $curl;

    my @curl = ( $curl, qw( fetch -o - ) );
    my $easy = publish_url . '/version_easy';
    debug("@curl $easy");
    my $version = Cpanel::SafeRun::Errors::saferunnoerror( @curl, $easy );
    chomp $version if $version;

    return $version;
}

# Creates and updates git repo with the tagged code before we
# build.
sub prep_repo {
    my $tag       = shift;
    my $repo_path = repo_path();

    # Make sure repo is available
    if ( !-e $Base_dir ) {
        die "Unable to create base directory, $Base_dir: $!";
    }

    if ( !-e $repo_path ) {
        chdir $Base_dir or die "Unable to chdir into base directory, $Base_dir: $!";
        info("Generating initial git clone of easyapache.  This is a very time consuming process");
        run_cmd_nofail( 'git', 'clone', $Clone_url, $Repo_dir );
    }

    if ( !chdir $repo_path ) {
        die "Unable to chdir to $repo_path: $!";
    }

    debug("Prepping repo: $repo_path");

    # Update repo and checkout tag
    my $git = cPBuild::Git->new( working_copy => $repo_path ) or die("Failed to initialize cPBuild::Git");

    $git->checkout_branch('master');
    $git->clean_working();
    $git->assert_status_clean();
    $git->fetch('origin');
    $git->checkout_tag($tag);
    $git->assert_status_clean();

    return $git;
}

sub clean_repo {
    my $git = shift;
    debug("Cleaning repo");
    $git->clean_working();
}

# 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;
}

# Highest version tag < the new tag
sub previous_build_tag {
    my ( $git, $new_tag ) = @_;
    my $result;
    my $new_version = $new_tag;
    $new_version =~ s/^v//;
    my $latest_version = '0';
    my ( $tag_out, $tag_err ) = $git->run2('tag');
    my @tags = split /\n/, $tag_out;
    for my $tag (@tags) {
        next unless $tag =~ /^v?(\d+(?:\.\d+)+)$/;
        my $version = $1;
        next if Cpanel::Version::Compare::compare( $version, '>=', $new_version );
        next if Cpanel::Version::Compare::compare( $version, '<=', $latest_version );
        $latest_version = $version;
        $result         = $tag;
    }
    return $result;
}

sub lock_update {
    my $ssh_user = sprintf( '%s@%s', $Update_user, $Update_host );
    debug("Locking qabuild for update");
    run_cmd_nofail( 'ssh', $ssh_user, '/usr/local/scripts/locklatest', 'easy_DEVEL' );
}

sub unlock_update {
    my $ssh_user = sprintf( '%s@%s', $Update_user, $Update_host );
    debug("Unlocking qabuild for update");
    run_cmd_nofail( 'ssh', $ssh_user, '/usr/local/scripts/unlocklatest', 'easy_DEVEL' );
}

# Update EasyApache files with correct version information
sub update_version {
    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 {
        die "Failed to update version file: $!";
    }

    # Update version in Cpanel::Easy::Apache
    my $repo_dir = repo_path();
    if ( open my $vers_fh, '+<', "$repo_dir/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 {
        die "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 {
    debug("Updating targz.yaml");

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

    unlink $dst;
    Cpanel::FileUtils::Copy::safecopy( $src, $dst ) or die "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 {
        die "Failed to create distribution list, $path: $!";
    }
}

sub build {
    my $git = shift;    # passing this around just in case
    my $tag = shift;

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

    # Build the tarballs and targz.yaml
    debug("Executing EasyApache makefile build");
    run_cmd_nofail( 'make', 'unittest' );

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

    # Update distribution config
    update_distlist($tag);
}

sub publish {
    my $git = shift;
    my $tag = shift;

    if ($DRYRUN) {
        warn "Dryrun enabled.  Not publishing";
        return 1;
    }

    debug("Publishing EasyApache $tag");

    my $repo_path = repo_path();
    my $update_location = sprintf( '%s@%s', $Update_user, $Update_host );

    lock_update();

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

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

            # 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 }, "$repo_path/$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$repo_path/\E},
        );
    }

    $scps->quit() || die "Transmit failure";

    unlock_update();
    sign();
}

sub sign {
    my $ssh_user = sprintf( '%s@%s', $Update_user, $Update_host );
    debug("Signing build");
    run_cmd_nofail( 'ssh', $ssh_user, '/usr/local/scripts/sign-build /home/tux/.gnupg/ release-team@cpanel.net --files /home/qabuild/cpanelsync/easy_DEVEL/{.cpanelsync,distlist-easy,targz.yaml,version_easy,*/.cpanelsync}' );
}

# 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.
sub generate_changelog {
    my $git    = shift;
    my $curtag = shift;
    my $changelog;
    my @cases;

    my $prevtag = Cpanel::Easy::Build::previous_build_tag( $git, $curtag );

    # if we can't find a previous build, we don't know what cases to put
    # into the changelog.
    if ($prevtag) {
        my $merge_base = ( $git->run2( 'merge-base', $curtag, $prevtag ) )[0];
        chomp $merge_base if defined $merge_base;

        if ($merge_base) {
            my $tmp = $git->log( '--no-merges', "$prevtag..$curtag" );

            # replace commit sha with link of the commit
            $tmp =~ s{^(commit\s+)([0-9a-f]+)}{https://$GitHost/projects/CPANEL/repos/easyapache/commits/$2}xmsg;

            my $jr_url = Cpanel::Easy::Build::jira_url($curtag);
            my $jr_msg = "The relevant cases can be seen in Jira via this link: $jr_url";

            $changelog = $jr_msg . "\n\n" . $tmp;

            # and return a list of parsed changelog cases
            @cases = parse_cases($changelog);
        }
        else {
            $changelog = qq{Log unavailable.  '$curtag' does not appear related to previous build, '$prevtag'};
        }
    }
    else {
        $changelog = qq{Unable to generate changelog; no previous build found\n};
    }

    debug("Found the following cases: @cases");

    return ( $changelog, @cases );
}

sub update_jira {
    my %args  = @_;
    my $tag   = $args{'tag'};
    my $cases = $args{'cases'};
    my $jrtag = "easyapache_$tag";
    my @errors;

    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 $jr = cPBuild::JIRA->new();

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

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

# Send out an e-mail
sub notify {
    my %args      = @_;
    my $tag       = $args{'tag'};
    my $changelog = $args{'changelog'};
    my $addmsg    = $args{'additional_msg'};    # any additional text that's appended to body of email

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

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

    debug("Sending notification e-mail: @TO_ADDR");

    eval { Mail::Sender::Easy::email( { 'smtp' => 'mail.cpanel.net', 'from' => $FROM_ADDR, 'to' => [@TO_ADDR], 'subject' => "[EasyApache Test] $tag has been published to qa-build", '_text' => $body, 'priority' => 1, 'auth' => 'LOGIN', 'authid' => $cPBuild::Func::mail_user, 'authpwd' => $cPBuild::Func::mail_passwd, 'on_errors' => 'undef', } ) };

    if ($@) {
        warn "WARNING: Failed to send e-mail to [@TO_ADDR]: $@";
    }
}

1;

package main;

use strict;
use warnings;
use Cpanel::SafeDir         ();
use Cpanel::SafeRun::Errors ();
use Cpanel::Usage           ();
use cPBuild::Func           ();
use File::Find              ();
use cPBuild::Git            ();
use Cpanel::FileUtils::Copy ();
use cPBuild::JIRA           ();

sub usage {
    print <<EO_USAGE;
build_easyapache.pl [options]

    Options:
      --help       Brief help message
      --tag        Git tag to build from
      --force      Force republication if the git tag has not changed
      --verbose    Print additional debugging information
      --dryrun     Don't publish, update Jira, or send e-mail

EO_USAGE
    exit 1;
}

sub run {
    my ( $force, $verbose, $tag, $dryrun );

    Cpanel::Usage::wrap_options( \@ARGV, \&usage, { 'force' => \$force, 'verbose' => \$verbose, 'tag' => \$tag, 'dryrun' => \$dryrun } );
    $Cpanel::Easy::Build::DEBUG  = $verbose;
    $Cpanel::Easy::Build::DRYRUN = $dryrun;

    unless ($tag) {
        print "You must specify a tag to build EasyApache!\n\n";
        usage();
    }

    # Determine if we need to even kick off a build
    Cpanel::Easy::Build::info("Determining current EasyApache build");
    my $curtag = Cpanel::Easy::Build::current_build();

    unless ($curtag) {
        die "Could not determine the previously published tag for this tree\n";
    }

    Cpanel::Easy::Build::info("Previously published tag for this tree: $curtag");

    # Same build already exists on server, bail out
    if ( !$force && $curtag eq $tag ) {
        Cpanel::Easy::Build::info("Build is up to date ($curtag)");
        exit 0;
    }

    Cpanel::Easy::Build::info("Building EasyApache $tag");

    # Server build is different (or forced)... LET'S DO THIS
    my $git = Cpanel::Easy::Build::prep_repo($tag);

    # Build EasyApache
    Cpanel::Easy::Build::build( $git, $tag );

    # Publish EasyApache for testing
    Cpanel::Easy::Build::publish( $git, $tag );

    # Gather list of cases that were added and generate changelog
    my ( $changelog, @cases ) = Cpanel::Easy::Build::generate_changelog( $git, $tag );

    # Tag each case referenced by the changelog
    my $jrerr = Cpanel::Easy::Build::update_jira( 'tag' => $tag, 'cases' => [@cases] );

    # Send e-mail to mailing lists to notify folks about new build
    Cpanel::Easy::Build::notify( 'tag' => $tag, 'changelog' => $changelog, 'additional_msg' => $jrerr );

    # Clean up build files in repo
    Cpanel::Easy::Build::clean_repo($git);

    Cpanel::Easy::Build::info("Build Complete");
}

# If invoked as a shell script, run the main program.
__PACKAGE__->run(@ARGV) unless caller();

