#!/usr/bin/perl -w
# /Trilinos/commonTools/test/utilities/runbuild

################################################################################
# The Trilinos Project - runbuild
# 
# Mike Phenow, Jim Willenbring
#
# This is a utility for building Trilinos.  It uses the runconfigure and
# runmake utilities.  Given a complete invoke-configure file, it can produce
# a successful build, even (optionally) recovering from an arbitrary number of
# configure and build failures.  
#
# - Check contents of report.
# - Improve comments.
#
################################################################################

use strict;

# Variable Declarations ========================================================

# Command line arguments:

my $trilinosDir;        # Trilinos directory        (required argument) 
my $buildDir;           # build directory           (required argument) 
my $invokeConfigure;    # complete invoke-configure (required argument)
my $makeCmd;            # make command              (default: make)
my $recover;            # recover mode?
my $noBlameMake;        # assign blame during make?

my $outputDir;          # output directory          (default: .)
my $startTime;          # start time                (supplied by runharness)
my $verbosity;          # verbosity level           (default: 1)
my $logVerbosity;       # log file verbosity level  (default: 0)

my $resultsDir;         # absolute path to results directory
my %dependencies;       # package dependencies

my $runStartTime;
my $runStartTimeForFilename;

# Constants
my $v0 = "0";           # quiet
my $v1 = "1";           # normal verbosity
my $v2 = "2";           # level 2 verbosity
my $v3 = "4";           # level 3 verbosity
        
################################################################################
# Execution ####################################################################
################################################################################

getArgs();
init();

my $exitStatus = run();
cleanUp();
exit $exitStatus;

################################################################################
# Subroutines ##################################################################
################################################################################

    ############################################################################
    # getArgs()
    #
    # Parse command line arguments.
    #
    #   - args:     NONE
    #
    #   - returns:  NONE
    #

    sub getArgs {
        
        # Argument variables that don't need to be global.
        my $quiet;
        my $help;

        # Gather command line arguments.
        use Getopt::Long;
        GetOptions( "trilinos-dir=s" => \$trilinosDir,
                    "build-dir=s" => \$buildDir,
                    "invoke-configure=s" => \$invokeConfigure,
                    "make-cmd=s" => \$makeCmd,
                    "recover" => \$recover,              
                    "no-blame-make" => \$noBlameMake,              
                    "output-dir=s" => \$outputDir,
                    "start-time=s" => \$startTime,        
                    "verbosity=i" => \$verbosity,
                    "log-verbosity=i" => \$logVerbosity,
                    "quiet" => \$quiet,
                    "help" => \$help );
        
        # Print help and exit.
        if ($help) { 
            printHelp();
            exit;
        }
        
        # Enforce and/or prepare arguments.
        
        # check for existance of trilinos-dir argument and actual directory
        if (!$trilinosDir) {
            die "trilinos-dir value required, see --help for more information\n"; 
        } else {
            if (!stat($trilinosDir)) {
                die "cannot stat trilinos-dir: $trilinosDir\n";
            }
        }
        
        # Check for existance of build-dir argument and actual directory.
        if (!$buildDir) {
            die "build-dir value required, see --help for more information\n"; 
        } 
        
        # Check for existance of invoke-configure argument and actual file
        if (!$invokeConfigure) {
            die "invoke-configure value required, see --help for more information\n"; 
        } else {
            if ($invokeConfigure !~ m/^\//) {
                $invokeConfigure = "$trilinosDir/commonTools/test/utilities/$invokeConfigure";
            }
            if (!stat($invokeConfigure)) {
                die "cannot stat invoke-configure: $invokeConfigure\n";
            } 
        }
        
        # Prevent problems with uninitialized value
        if (!$makeCmd) {
            $makeCmd = "";
        }
        
        # Prevent problems with uninitialized value
        if (!$recover) {
            $recover = "";
        }
        
        # Prevent problems with uninitialized value
        if (!$noBlameMake) {
            $noBlameMake = "";
        }
        
        # Check for existance of output directory, use it to create the
        # complete path for the results directory, and create the results
        # directory.
        if (!$outputDir) {
            $outputDir = "";
            $resultsDir = "$trilinosDir/commonTools/test/utilities/results"; 
        } else {
            if ($outputDir =~ m/^\//) {
                $resultsDir = $outputDir;
            } else {
                $resultsDir = "$trilinosDir/commonTools/test/utilities/$outputDir";
            }
        }
        if (!stat($resultsDir)) {
            system("mkdir $resultsDir") == 0 or die "cannot create $resultsDir, died";
        }
        
        # Set verbosity level to corresponding constant.  0, 1, 2, and 3 are
        # used for the levels the user may specify, but they are stored as 0,
        # 1, 2, and 4 so they can be combined and processed with bitwise
        # operations.
        if ($verbosity) {
            if      ($verbosity == 0) { $verbosity = $v0; }
            elsif   ($verbosity == 1) { $verbosity = $v1; }
            elsif   ($verbosity == 2) { $verbosity = $v2; }
            elsif   ($verbosity == 3) { $verbosity = $v3; }
        } else {
            $verbosity = $v1; 
        }
        
        # Set log verbosity level to corresponding constant.  Numbering scheme
        # is the same for the log verbosity as it is for the standard verbosity.
        # There is not distinct output and log output, the if a log verbosity
        # is given, then that level of output is written to a file instead of
        # to standard out.  
        if ($logVerbosity) {
            if      ($logVerbosity == 0) { $logVerbosity = $v0; }
            elsif   ($logVerbosity == 1) { $logVerbosity = $v1; }
            elsif   ($logVerbosity == 2) { $logVerbosity = $v2; }
            elsif   ($logVerbosity == 3) { $logVerbosity = $v3; }        
        } else {
            $logVerbosity = $v1; 
        }
        
        # Set quiet mode--same as passing --verbosity=0.
        if ($quiet) {
            $verbosity = $v0; 
        }
        
    } # getArgs()

    ############################################################################
    # init()
    #
    # Prepares varibles.
    #
    #   - args:     NONE
    #
    #   - returns:  NONE
    #

    sub init {
        
        # We've been passed the --start-time= command line argument by
        # runharness, so use that instead of capturing our own.
        if ($startTime) {
            $runStartTimeForFilename = $startTime;
        } else {    
            # Capture and format build start time.
            (my $se, my $mn, my $hr, my $da, my $mo, my $yr) = (localtime)[0..5];
            $yr = sprintf("%02d", $yr % 100);
            $mo = sprintf("%02d", $mo+1);
            $da = sprintf("%02d", $da);
            $hr = sprintf("%02d", $hr);
            $mn = sprintf("%02d", $mn);
            $se = sprintf("%02d", $se);
            $runStartTime = $yr."-".$mo."-".$da." ".$hr.":".$mn.":".$se;
            $runStartTimeForFilename = $yr."-".$mo."-".$da."_".$hr.".".$mn.".".$se;
        }
        
        # Create results directory.
        $resultsDir .= "/$runStartTimeForFilename-build";
        system("mkdir $resultsDir") == 0 or die "cannot create $resultsDir, died";
        
        # --start-time= command line argument was passed from runharness so it
        # would know what the results directory name was.  So...since we know
        # we're being run from runharness, we know we also need to create the
        # build info file, so runconfigure can append its information to it.
        if ($startTime) {
            
            # Create build info file for database.
            my $buildInfoFile = "$resultsDir/build.txt.tmp1";
            open (BUILD_FILE, ">$buildInfoFile")
                or die "can't open build info file $buildInfoFile for writing, died";            
            my $string = "";
            $string .= "TRILINOS_DIR         = $trilinosDir\n";
            print BUILD_FILE $string;        
            close BUILD_FILE;
        
        }
        
        # Print list of variables for debugging.
        my $message = "";
        $message .= "init():\n";
        $message .= "  \$trilinosDir = $trilinosDir\n";
        $message .= "  \$buildDir = $buildDir\n";     
        $message .= "  \$invokeConfigure = $invokeConfigure\n";      
        $message .= "  \$recover = $recover\n";
        $message .= "  \$noBlameMake = $noBlameMake\n";
        $message .= "  \$outputDir = $outputDir\n";
        $message .= "  \$verbosity = $verbosity\n";
        $message .= "  \$logVerbosity = $logVerbosity\n";
        $message .= "  \$resultsDir = $resultsDir\n";
        $message .= "  \n";
        printMessage($message, $v3);
        
    } # init()
    
    ############################################################################
    # run()
    #
    # Runs runconfigure and runmake
    #
    #   - args:     NONE
    #
    #   - returns:  NONE
    #

    sub run {
        
        my $argVerbosity;
        if      ($verbosity == $v0) { $argVerbosity = 0; }
        elsif   ($verbosity == $v1) { $argVerbosity = 1; }
        elsif   ($verbosity == $v2) { $argVerbosity = 2; }
        elsif   ($verbosity == $v3) { $argVerbosity = 3; }
        
        my $argLogVerbosity;
        if      ($logVerbosity == $v0) { $argLogVerbosity = 0; }
        elsif   ($logVerbosity == $v1) { $argLogVerbosity = 1; }
        elsif   ($logVerbosity == $v2) { $argLogVerbosity = 2; }
        elsif   ($logVerbosity == $v3) { $argLogVerbosity = 3; }
        
        my $cmd = "";
        my $status = 0;
        my $makeAttempted = 0;
        my $makePassed = 0;
        my $makeBrokenPackage = "";
        my $makeBrokenSubdir = "";
        
        while (!$makePassed) {
            
            # Run runconfigure
            $cmd  = "perl runconfigure ";
            $cmd .= "--trilinos-dir=$trilinosDir ";
            $cmd .= "--build-dir=$buildDir ";
            $cmd .= "--invoke-configure=$invokeConfigure ";
            if ($recover) {
                $cmd .= "--recover ";
                if ($makeAttempted) {
                    $cmd .= "--make-broken-package=$makeBrokenPackage ";
                    $cmd .= "--make-broken-subdir=$makeBrokenSubdir ";                 
                }
            }
            $cmd .= "--output-dir=$resultsDir ";
            $cmd .= "--verbosity=$argVerbosity ";
            $cmd .= "--log-verbosity=$argLogVerbosity ";
            $status = system($cmd);
            $status = $status >> 8;
            if ($status == 86) {
                return 86;
            } elsif ($status != 0 && !$recover) { 
                return 1;
            }            
            
            # Capture and format make start time.
            # This needs to be done so that, in the event of a make failure,
            # we can use the make start time to access the correct make output
            # file to obtain the broken package and broken subdir.  This needs
            # to be done because there aren't enough bits in a standard unix
            # exit code to encode both the failed package and the failed 
            # subdir.
            (my $se, my $mn, my $hr, my $da, my $mo, my $yr) = (localtime)[0..5];
            $yr = sprintf("%02d", $yr % 100);
            $mo = sprintf("%02d", $mo+1);
            $da = sprintf("%02d", $da);
            $hr = sprintf("%02d", $hr);
            $mn = sprintf("%02d", $mn);
            $se = sprintf("%02d", $se);
            my $makeStartTimeForFilename = $yr."-".$mo."-".$da."_".$hr.".".$mn.".".$se;
                            
            # Run runmake
            $cmd  = "perl runmake ";
            $cmd .= "--trilinos-dir=$trilinosDir ";
            $cmd .= "--build-dir=$buildDir ";
            $cmd .= ($makeCmd?"--make-cmd=\"$makeCmd\" ":"");
            $cmd .= ($noBlameMake?"--no-blame ":"");
            $cmd .= "--output-dir=$resultsDir ";
            $cmd .= "--verbosity=$argVerbosity ";
            $cmd .= "--log-verbosity=$argLogVerbosity ";
            $cmd .= "--start-time=$makeStartTimeForFilename ";
            $status = system($cmd);
            $status = $status >> 8;
            $makeAttempted = 1;
            if ($status == 0) { 
                $makePassed = 1;
            } else {
                if ($recover) {
                
                    # Parse make info file.
                    open (MAKE_INFO_FILE, "<$resultsDir/$makeStartTimeForFilename-make.txt") or die "! Can't open make info file, died";
                    undef $/;                           # undefine input record separator
                    my $makeInfoFile=<MAKE_INFO_FILE>;  # copy entire file
                    $/ = "\n";                          # restore it to default newline
                    close MAKE_INFO_FILE;
                    
                    if ($makeInfoFile =~ m/BROKEN_PACKAGE\s*=\s*(\w+)/) {
                        $makeBrokenPackage = $1;
                    }
                    
                    if ($makeInfoFile =~ m/BROKEN_SUBDIR\s*=\s*(\w+)/) {
                        $makeBrokenSubdir = $1;
                    }
                    
                } else {
                    return 1; 
                }
            }
            
        } # while (!$makePassed)

        # If --no-blame-make was passed, assume we were invoked by testtarball
        # in the tarball-creation stage, so skip compiling the tests and
        # examples since their failure would cause testtarball to assume that
        # the tarball creation had failed and thus, would not proceed to 
        # expand and test the tarball.

        if (!$noBlameMake) {
        
            # Now we have to run `make examples` and `make tests` since `make`
            # now only compiles the libraries.  For now, we are not going to
            # attempt any recovery for tests and examples.

            $cmd = "";
            $status = 0;
            $makeAttempted = 0;
            $makePassed = 0;
            $makeBrokenPackage = "";
            $makeBrokenSubdir = "";
        
            while (!$makePassed) {

                # Make Examples
                $cmd  = "perl runconfigure ";
                $cmd .= "--trilinos-dir=$trilinosDir ";
                $cmd .= "--build-dir=$buildDir ";
                if (!$makeAttempted) {
                    $cmd .= "--new-make-target ";
                }
                if ($makeAttempted) {
                    $cmd .= "--make-broken-package=$makeBrokenPackage ";
                    $cmd .= "--make-broken-subdir=$makeBrokenSubdir ";
                }
                $cmd .= "--output-dir=$resultsDir ";
                $cmd .= "--verbosity=$argVerbosity ";
                $cmd .= "--log-verbosity=$argLogVerbosity ";
                $status = system($cmd);
                $status = $status >> 8;
                if ($status == 86) {
                    return 86;
                } elsif ($status != 0 && !$recover) { 
                    return 1;
                }            
                (my $se, my $mn, my $hr, my $da, my $mo, my $yr) = 
                    (localtime)[0..5];
                $yr = sprintf("%02d", $yr % 100); $mo = sprintf("%02d", $mo+1);
                $da = sprintf("%02d", $da); $hr = sprintf("%02d", $hr);
                $mn = sprintf("%02d", $mn); $se = sprintf("%02d", $se);
                my $makeStartTimeForFilename = $yr."-".$mo."-".$da;
                $makeStartTimeForFilename .= "_".$hr.".".$mn.".".$se;
                $cmd  = "perl runmake ";
                $cmd .= "--trilinos-dir=$trilinosDir ";
                $cmd .= "--build-dir=$buildDir ";
                $cmd .= "--make-cmd=".($makeCmd?"\"$makeCmd examples\" "
                    : "\"make examples\" ");
                $cmd .= "--output-dir=$resultsDir ";
                $cmd .= "--verbosity=$argVerbosity ";
                $cmd .= "--log-verbosity=$argLogVerbosity ";
                $cmd .= "--start-time=$makeStartTimeForFilename ";
                $status = system($cmd);
                $status = $status >> 8;
                $makeAttempted = 1;
                if ($status == 0) { 
                    $makePassed = 1;
                } else {
                    if ($recover) {
                        # Parse make info file.
                        open (MAKE_INFO_FILE, 
                          "<$resultsDir/$makeStartTimeForFilename-make.txt") 
                          or die "! Can't open make info file, died";
                        undef $/;       # undefine input record separator
                        my $makeInfoFile=<MAKE_INFO_FILE>;  # copy entire file
                        $/ = "\n";      # restore it to default newline
                        close MAKE_INFO_FILE;
                        if ($makeInfoFile =~ m/BROKEN_PACKAGE\s*=\s*(\w+)/) {
                            $makeBrokenPackage = $1;
                        }
                        if ($makeInfoFile =~ m/BROKEN_SUBDIR\s*=\s*(\w+)/) {
                            $makeBrokenSubdir = $1;
                        }
                    } else {
                        return 1; 
                    }
                }

            }
            
            $cmd = "";
            $status = 0;
            $makeAttempted = 0;
            $makePassed = 0;
            $makeBrokenPackage = "";
            $makeBrokenSubdir = "";
        
            while (!$makePassed) {

                # Make Tests
                $cmd  = "perl runconfigure ";
                $cmd .= "--trilinos-dir=$trilinosDir ";
                $cmd .= "--build-dir=$buildDir ";
                if (!$makeAttempted) {
                    $cmd .= "--new-make-target ";
                }
                if ($makeAttempted) {
                    $cmd .= "--make-broken-package=$makeBrokenPackage ";
                    $cmd .= "--make-broken-subdir=$makeBrokenSubdir ";
                }
                $cmd .= "--output-dir=$resultsDir ";
                $cmd .= "--verbosity=$argVerbosity ";
                $cmd .= "--log-verbosity=$argLogVerbosity ";
                $status = system($cmd);
                $status = $status >> 8;
                if ($status == 86) {
                    return 86;
                } elsif ($status != 0 && !$recover) { 
                    return 1;
                }            
                (my $se, my $mn, my $hr, my $da, my $mo, my $yr) = 
                    (localtime)[0..5];
                $yr = sprintf("%02d", $yr % 100); $mo = sprintf("%02d", $mo+1);
                $da = sprintf("%02d", $da); $hr = sprintf("%02d", $hr);
                $mn = sprintf("%02d", $mn); $se = sprintf("%02d", $se);
                my $makeStartTimeForFilename = $yr."-".$mo."-".$da;
                $makeStartTimeForFilename .= "_".$hr.".".$mn.".".$se;
                $cmd  = "perl runmake ";
                $cmd .= "--trilinos-dir=$trilinosDir ";
                $cmd .= "--build-dir=$buildDir ";
                $cmd .= "--make-cmd=".($makeCmd?"\"$makeCmd tests\" "
                    : "\"make tests\" ");
                $cmd .= "--output-dir=$resultsDir ";
                $cmd .= "--verbosity=$argVerbosity ";
                $cmd .= "--log-verbosity=$argLogVerbosity ";
                $cmd .= "--start-time=$makeStartTimeForFilename ";
                $status = system($cmd);
                $status = $status >> 8;
                $makeAttempted = 1;
                if ($status == 0) { 
                    $makePassed = 1;
                } else {
                    if ($recover) {
                        # Parse make info file.
                        open (MAKE_INFO_FILE, 
                          "<$resultsDir/$makeStartTimeForFilename-make.txt") 
                          or die "! Can't open make info file, died";
                        undef $/;       # undefine input record separator
                        my $makeInfoFile=<MAKE_INFO_FILE>;  # copy entire file
                        $/ = "\n";      # restore it to default newline
                        close MAKE_INFO_FILE;
                        if ($makeInfoFile =~ m/BROKEN_PACKAGE\s*=\s*(\w+)/) {
                            $makeBrokenPackage = $1;
                        }
                        if ($makeInfoFile =~ m/BROKEN_SUBDIR\s*=\s*(\w+)/) {
                            $makeBrokenSubdir = $1;
                        }
                    } else {
                        return 1; 
                    }
                }

            }
        
        }

        return 0;
        
    } # run()

    ############################################################################
    # cleanUp()
    #
    # Clean up environment variables, temp files, etc.
    #
    #   - args:     NONE
    #
    #   - returns:  NONE
    #

    sub cleanUp {
    
        # Currently, there is nothing to clean up, but I will leave this
        # subroutine here for potential future use.
           
    } # cleanUp()
    
    ############################################################################
    # printMessage()
    #
    # Prints an event if the verbosity is set.
    #
    #   - args:     $message        (message to be printed)
    #               $level          (verbosity level of message)
    #
    #   - returns:  NONE
    #

    sub printMessage {
        my $message = $_[0];
        my $level = $_[1];
        
        if ($verbosity & $level) {
            print $message;
        }
        
        if ($logVerbosity & $level) {
            my $log = $resultsDir."/log.txt";
            open (LOG, ">>$log")
                or die "can't open $log";
            print LOG $message;
            close LOG;
        }
    } # printMessage()

    ############################################################################
    # printHelp()
    #
    # Prints help output.
    #
    #   - args:     NONE
    #
    #   - returns:  NONE
    #

    sub printHelp {
        print "runbuild - The Trilinos Build Utility\n";
        print "\n";
        print "Usage:  perl runbuild --trilinos-dir=/home/user/Trilinos --build-dir=MPI\n";
        print "\n";
        print "Options:\n";
        print "\n";
        print "  --trilinos-dir=DIR         Specify the absolute path to the top-level\n";
        print "                             Trilinos directory that contains this program.\n";
        print "                             Example: /home/user/Trilinos\n";
        print "                             REQUIRED.\n";
        print "\n";
        print "  --build-dir=DIR            Specify the name of the build directory where you\n";
        print "                             would like to build Trilnos.  If a relative\n";
        print "                             path is given, it is assumed to be in the given\n";
        print "                             Trilinos directory.\n";
        print "                             REQUIRED.\n";
        print "\n";
        print "  --invoke-configure=FILE    Relative paths to a complete invoke-configure file.\n";
        print "                             REQUIRED.\n";
        print "\n";
        print "  --make-cmd=COMMAND         Specify the make command for this system.  This can\n";
        print "                             be used to specify make flags, \"make -j4\", for\n";
        print "                             example\n";
        print "                             Default: \"make\"\n";
        print "\n";
        print "  --recover                  If this flag is present, runbuild will attempt\n";
        print "                             to remove a broken package and its dependents and\n";
        print "                             continue until some subset builds successfully.\n";
        print "\n";
        print "  --output-dir=DIR           Specify the directory in which to create the\n";
        print "                             directory containing the results.\n";
        print "                             Default: \".\"\n";
        print "\n";
        print "  --verbosity=LEVEL          0 = no non-fatal ouput (same as --quiet)\n";
        print "                             1 = normal output (default)\n";
        print "                             2 = level 2 verbosity\n";
        print "                             3 = level 3 verbosity\n";
        print "\n";
        print "  --log-verbosity=LEVEL      0 = no log\n";
        print "                             1 = normal output (default)\n";
        print "                             2 = level 2 verbosity\n";
        print "                             3 = level 3 verbosity\n";
        print "\n";
        print "  --quiet                    Produce no non-fatal output.\n";
        print "\n";
        print "  --help                     Print this help output and exit.\n";
        print "\n";
        print "Notes:\n";
        print "  - For more information, see README-runbuild in\n";
        print "    Trilinos/commonTools/test/utilities/\n";
        print "    or visit http://software.sandia.gov/trilinos/developer/\n";
        print "\n";
    } # printHelp()
