#!/usr/bin/env perl
#!/bin/sh

#  -*-Perl-*-  (for Emacs)    vim:set filetype=perl:  (for vim)
#======================================================================#
#! /Good_Path/perl -w
# line 17

# Name:   pc_build
# Author: wd (wdobler [at] gmail [dot] com)
# Date:   23-Jun-2009
# Modified: 18-jun-2019/MR: added option --target=<targetname> (-t=<targetname>) for choosing a target
# SVN: $Id$
# Description:
#   Compile the Pencil Code, using settings from the 'Makefile' section in
#   the appropriate configuration files.
# Usage:
#   pc_build [-v|-h]
#   pc_build [-f <file1,file2,...>] [-H <host_ID>]
#   pc_build [<options>] [<MAKE_VAR1=val1> [<MAKE_VAR2>=val2 ...]]
# Options:
#   -h, --help      This help.
#   -d, --debug     Show debug output.
#       --clean     Run 'make clean'.
#       --cleanall  Run 'pc_setupsrc; make cleanall'.
#   -f <files>,
#       --config-files=<files>
#                   Use the given <files> (a comma-separated list) as
#                   configuration files, rather than trying to find a
#                   config file based on a host ID.
#                   If not provided and the environment variable PENCIL_CONFIG_FILES is set,
#                   the value of the latter is taken over for <files>.
#   -H <id>
#       --host-id=<id>
#                   Use the given <id> as host ID.
#   -i              Show system host ID and exit. This ignores local
#                   configuration of the host ID via command-line flags,
#                   environment variables, and 'host-ID' files.
#   -I              Show all matching host IDs, in the order in which they
#                   are tried, and exit.
#   -j, --jobs=<N>  Use make with a maximum of N parallelization jobs.
#       --parallel  Use make with unlimited parallelization.
#   -D, --debug-config
#                   Show the config files (existing or not) that would be
#                   read, then exit
#       --notify    Tell us (audibly and visually) when compilation is done.
#       --fast      Shortcut for the option FFLAGS+='-O0'.
#   -l, --last-config
#                   Build with the same config files as before.
#   -p, --previous-flags
#                   Build with the same flags as the previous time.
#   -q, --quiet     Be quiet.
#   -s, --serial    Use make w/o parallelization.
#   -t, --target=<target>
#                   Choose a target, default: "default-to-be" (=EXECUTABLES).
#                   For targets "*.x", extension can be omitted.
#                   Target "ALL": all executables in subdirectory "src".
#   -v, --version   Print version number.
# Examples:
#   pc_build
#       use host ID mechanism to identify the appropriate config file.
#   pc_build REAL_PRECISION=double
#       run 'make REAL_PRECISION=double', i.e. compile for double precision
#   pc_build -f compilers/GNU-GCC_MPI
#       will use the configuration files
#       ${PENCIL_HOME}/config/compilers/GNU-GCC.conf and
#       ${PENCIL_HOME}/config/mpi/default.conf (unless you have analogous
#       files under ~/.pencil/config to override the ones in ${PENCIL_HOME}).
#   pc_build -f /home/USER/myconfig
#       will use the configuration file /home/USER/myconfig.conf if it
#       exists.
#   pc_build -H workhorse
#       will look for, and use the configuration file for host ID
#       'workhorse'. If no config file is found, pc_build exits with an
#       error message.
#   pc_build --debug-config
#       will not build, but instead helps to choose an appropriate
#       location for a config file for the given computer

# To do / ideas:
#   - Allow make-style assignment arguments:
#       pc_build FC=myf90 FFLAGS+='-O0 -g'
#   - Allow handing down flags to 'make' with
#       -Wm,<flag1>,<flag2>,...
#       --make-flags=<flag1>,<flag2>,...
#   - Allow setting the search path

# Copyright (C) 2015  Wolfgang Dobler
#
# This program is free software; you can redistribute it and/or modify it
# under the same conditions as Perl or under the GNU General Public
# License, version 3 or later.

use strict;
use warnings;
BEGIN {
    # Make sure ${PENCIL_HOME}/lib/perl is in the Perl path
    if (-d "$ENV{PENCIL_HOME}/lib/perl") {
        unshift @INC, "$ENV{PENCIL_HOME}/lib/perl";
    } else {
        if ($0 =~ m!(.*[/\\])!) { unshift @INC, "$1../lib/perl"; }
    }
}

use Pencil::Util;
Pencil::Util::use_pencil_perl_modules(
    'Pencil::ConfigFinder',
    'Pencil::ConfigParser'
    ) or die;

use POSIX qw(strftime);
use Getopt::Long;
# Allow for '-Plp' as equivalent to '-P lp' etc:
Getopt::Long::config("bundling");

my $cmd_name = $0;
my @all_arguments = @ARGV;

my (%opts);                     # Options hash for GetOptions

## Process command line
GetOptions(\%opts,
           qw( -h   --help
               -d   --debug
                    --clean
                    --cleanall
               -f=s --config-files=s
               -H=s --host-id=s
               -i
               -j=n --jobs=n
                    --parallel
               -I
               -D   --debug-config
                    --notify
                    --fast
               -l   --last-config
               -p   --previous-flags
               -q   --quiet
               -s   --serial
               -t=s --target=s
               -v   --version )
          ) or die "Aborting.\n";

my $debug = ($opts{'d'} || $opts{'debug'} || '');
if ($debug) {
    printopts(\%opts);
    print "\@ARGV = '@ARGV'\n";
}

if ($opts{'h'} || $opts{'help'})    { die usage();   }
if ($opts{'v'} || $opts{'version'}) { die version(); }

my $clean        = (              $opts{'clean'}          || '');
my $cleanall     = (              $opts{'cleanall'}       || '');
my $config_files = ($opts{'f'} || $opts{'config-files'}   || undef);
my $host_id      = ($opts{'H'} || $opts{'host-id'}        || undef);
my $debug_config = ($opts{'D'} || $opts{'debug-config'}   || '');
my $show_system_host_id = ($opts{'i'}                     || '');
my $show_host_ids = ($opts{'I'}                           || '');
my $jobs         = ($opts{'j'} || $opts{'jobs'}           || undef);
my $parallel     = (              $opts{'parallel'}       || 0);
my $notify       = (              $opts{'notify'}         || '');
my $fast         = (              $opts{'fast'}           || '');
my $last_config  = ($opts{'l'} || $opts{'last-config'}    || '');
my $prev_flags   = ($opts{'p'} || $opts{'previous-flags'} || '');
my $quiet        = ($opts{'q'} || $opts{'quiet'}          || '');
my $serial       = ($opts{'s'} || $opts{'serial'}         || '');
my $target       = ($opts{'t'} || $opts{'target'}         || undef);

if (!defined $target){
    $target = 'default_to_be';
}
else{
    $target = (split("=",$target))[-1];
    $target =~ s/(^.*)\.x *$/$1/;
}

if ($show_system_host_id) {
    my $id = Pencil::ConfigFinder::get_host_id_system_info();
    print $id."\n";
    exit 0;
}

if ($show_host_ids) {
    foreach my $id (Pencil::ConfigFinder::get_host_ids()) {
        print $id."\n";
    }
    exit 0;
}

my @config_files;
mention($Pencil::ConfigFinder::debug);
$Pencil::ConfigFinder::debug = 1 if ($debug or $debug_config);

if (!defined $config_files) {
    if ($last_config && -e "src/.config-files" && !(-z "src/.config-files")) {
        open(CFH, '<', "src/.config-files");
        $config_files = <CFH>;
        close(CFH);
    } else {
        if (defined $ENV{'PENCIL_CONFIG_FILES'}) {
            $config_files = $ENV{'PENCIL_CONFIG_FILES'};
        }
    }
    if (defined $config_files) {
       $config_files =~ s/^\s+|\s+$//g;
       print STDERR "Use config file <$config_files>\n" unless ($quiet);
    }
}

if (defined $config_files) {
    my @files = split(/[,\s\+]+/s, $config_files);
    @config_files = Pencil::ConfigFinder::locate_config_files(@files);
} else {
    my $config_file;
    if (defined $host_id) {
        $config_file = Pencil::ConfigFinder::find_config_file_for_host($host_id);
    } else {
        $config_file = Pencil::ConfigFinder::find_config_file();
    }
    die "Fatal: Couldn't find config file.\n" unless (defined $config_file);
    $config_files=$config_file;
    push @config_files, $config_file;
    print STDERR "Found config file <$config_files[0]>\n" unless ($quiet);
}
die "No configuration file found\n" unless @config_files;

open(CFH, '>', "src/.config-files") or die $!;
print CFH $config_files;
close(CFH);

if ($debug_config) {
    if (($opts{'f'} || $opts{'config-files'}) && @config_files) {
        print "Using <";
        print @config_files;
        print ">\n";
    }
    # The desired output is now printed
    exit 0;
}

my $log_file = 'pc_commands.log';
if ($prev_flags) {
    my $cmd = `grep -w pc_build pc_commands.log | tail -1`;
    exec $cmd;
    exit 0;
}

log_command_line($cmd_name, \@all_arguments);

if ($fast) {
    push @ARGV, 'FFLAGS+=-O0';
}

my @extra_make_args = @ARGV;

if ($cleanall) {
    system('pc_setupsrc');
    make('cleanall');
    exit 0;
}

if ($clean) {
    make('clean');
    exit 0;
}

my $parser = new Pencil::ConfigParser(@config_files);
$parser->debug(1) if ($debug);

unless (-e './src/cparam.f90') {
    print STDERR "Running pc_setupsrc\n";
    system('pc_setupsrc');
}

my @make_args = @{$parser->get_makefile_args()};
push @make_args, @extra_make_args;

# Handle MAKE_VAR1=-j4, etc.
# NOTE: This should be implemented with -Wm,<options>, akin to
# pencil-test's -Wa or gcc's -Wl options
map { s/^\s*MAKE_VAR\d*\s*=\s*// } @make_args;
if ($serial) {
    my $ind;
    for( $ind = 0; $ind < scalar @make_args; $ind +=1 ) {
        if ($make_args[$ind] =~ ' *(?:-j|--jobs=\d*) *'){
             splice(@make_args, $ind, 1); last;
        }
    }
}
if ($parallel) { push @make_args, "-j"; }
if (defined ($jobs)) { push @make_args, "--jobs=".$jobs; }

if ($target eq "ALL") { $target = 'fast'; }

if ($debug) {
    push @make_args, "--debug";
}

my $make_cmd_line = "'".join("' '", @make_args, $target)."'";
print STDERR "Running make ", $make_cmd_line, "\n" unless ($quiet);
write_log_line('# make '.$make_cmd_line);
make(@make_args, $target);

# ---------------------------------------------------------------------- #
sub make {
# Run 'make' with the given arguments
    my @make_args = @_;

    system('make', @make_args) == 0
      or die "'make @make_args' failed: <$!>\n";
}
# ---------------------------------------------------------------------- #
sub mention {
# Reference a variable without doing anything.
# Use this to suppress ''Name "Pencil::ConfigFinder::debug" used only
# once'' warning
    my @args = @_;
}
# ---------------------------------------------------------------------- #
sub write_log_line {
# Write one line to the log file.
    my ($line) = @_;
    my $out;

    if (!-e $log_file) {
        open($out, '> '.$log_file);
        print $out "# Log file of Pencil Code commands.\n";
        print $out "# This compiles with the same parameters as last time in this directory:\n";
        print $out "# pc_build -p\n";
        close $out;
    }

    open($out, '>> '.$log_file)
      or return warn_politely("Cannot write to $log_file");
    print $out "$line", "\n";
    close $out;
}
# ---------------------------------------------------------------------- #
sub log_command_line {
# Write the current command line to log file that can be run in order to
# compile with exactly the same parameters.
    my ($cmd, $args_ref) = @_;
    my @args = quote_spaces(@$args_ref);
    my $out;

    my $timestamp = POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime);
    write_log_line("\n# ".$timestamp);
    write_log_line("$cmd @args");
}
# ---------------------------------------------------------------------- #
sub comment_out_all_lines {
# Create a new file.
# If $log_file exists, copy each line from $log_file to the new file,
# commenting out all non-empty, non-commented lines.
# Otherwise write a descriptive header to the new file.
# Access permissions of the new files are the same as for the old file,
# or determined from the user's umask.
# Return a file handle and the file name for the new file.
    my ($log_file) = @_;

    use File::Temp qw/ :mktemp /;

    my ($out, $tmp_file) = mkstemp('pc_build_tmp_XXXXX');

    my $permissions;
    if (-e $log_file) {
        $permissions =  (stat "$log_file")[2] & 07777;
        open(my $in, '<', $log_file)
          or return warn_politely("Cannot read from $log_file");
        while (defined(my $line = <$in>)) {
            if ($line !~ /^\s*#/) {
                $line = "# ".$line;
            }
            print $out $line;
        }
        close $in;
    } else {
        # rw-rw-rw- is 0666, but umask may disallow some of those bits
        $permissions = 0666 & ~umask();
        (my $header = <<'HERE') =~ s/^\s{8}//gm;
        # Log file of Pencil Code commands.
        # This compiles with the same parameters as last time in this directory:
        # pc_build -p
HERE
        print $out $header;
    }

    chmod($permissions, $out);

    return ($out, $tmp_file);
}
# ---------------------------------------------------------------------- #
sub warn_politely {
# Print a warning and return ''
    my ($warning) = @_;

    chomp $warning;
    warn "$warning\n";
    return '';
}
# ---------------------------------------------------------------------- #
sub quote_spaces {
# Put any string containing spaces in ''.
# This is primitive and won't help for command line arguments containing
# the single quote character.
    my @strings = @_;

    my @quoted = ();
    foreach my $string (@strings) {
        if ($string =~ /\s/) {
            $string = "'$string'";
        }
        push @quoted, $string;
    }

    return @quoted;
}
# ---------------------------------------------------------------------- #
sub printopts {
# Print command line options
    my ($optsref) = @_;
    my %opts = %$optsref;
    foreach my $opt (keys(%opts)) {
        print STDERR "\$opts{$opt} = '$opts{$opt}'\n";
    }
}
# ---------------------------------------------------------------------- #
sub usage {
# Extract description and usage information from this file's header.
    my $thisfile = __FILE__;
    local $/ = '';              # Read paragraphs
    open(FILE, "< $thisfile") or die "Cannot open $thisfile\n";
    while (<FILE>) {
        # Paragraph _must_ contain 'Description:' or 'Usage:'
        next unless /^\s*\#\s*(Description|Usage):/m;
        # Drop 'Author:', etc. (anything before 'Description:' or 'Usage:')
        s/.*?\n(\s*\#\s*(Description|Usage):\s*\n.*)/$1/s;
        # Don't print comment sign:
        s/^\s*# ?//mg;
        last;                        # ignore body
    }
    return $_ || "<No usage information found>\n";
}
# ---------------------------------------------------------------------- #
sub version {
# Return CVS data and version info.
    my $doll='\$';              # Need this to trick CVS
    my $cmdname = (split('/', $0))[-1];
    my $rev = '$Revision: 1.12 $';
    my $date = '$Date: 2008/07/07 21:37:16 $';
    $rev =~ s/${doll}Revision:\s*(\S+).*/$1/;
    $date =~ s/${doll}Date:\s*(\S+).*/$1/;

    return "$cmdname version $rev ($date)\n";
}
# ---------------------------------------------------------------------- #
END {
    Pencil::Util::notify('building') if $notify;
}
# ---------------------------------------------------------------------- #

# End of file build