#!/usr/bin/perl
#############################################################################
## Crytek Source File
## Copyright (C) 2006, Crytek Studios
##
## Creator: Sascha Demetrio
## Date: Jul 31, 2006
## Description: GNU-make based build system
#############################################################################

# Build wrapper script for compiler and linker invocations.
# Call with option -h for a description.
#
# Note that the script currently assumes that the compiler accepts the a file
# list through the -Wl,@$file syntax.  @$file is translated to -Wl,@$file in
# the compiler invocation (because some versions of GCC don't understand
# @$file).  This feature is controlled through the --ldlist option, which is
# enabled by default.

use warnings;
use strict;

use Getopt::Long qw(:config no_auto_abbrev bundling);
use String::ShellQuote;

our $outputFile = '';

# Parse the command line.
my $optMode = 'compile';
my $optConvertMSVC = 0;
my $optCygwinMode = 0;
my $optCygwinBaseDir;
my $optPrefix = '';
my $optIgnoreFile;
my $optErrorFile;
my $optSilentMode = 0;
my $optPS3Mode = 0;
my $optLdList = 1;
my $optCryCGMode = 0;
my $optCD = 0;
my $optHelp = 0;
GetOptions(
		'm|mode=s' => \$optMode,
		'x|msvc!' => \$optConvertMSVC,
		'c|cygwin!' => \$optCygwinMode,
		'b|basedir|base-dir=s' => \$optCygwinBaseDir,
		'p|prefix=s' => \$optPrefix,
		'i|ignore=s' => \$optIgnoreFile,
		'e|error=s' => \$optErrorFile,
		's|silent!' => \$optSilentMode,
		'cd!' => \$optCD,
		'ps3!' => \$optPS3Mode,
		'ldlist!' => \$optLdList,
		'cgmode!' => \$optCryCGMode,
		'h|help' => \$optHelp);

if ($optHelp)
{
  print <<EOF;
build.pl: Build wrapper script.
Synopsis:
  \$PERL build.pl (Options) -- (Command)
Options:
-m|--mode MODE
  Build mode.  One of 'compile', 'compile_pch', 'link'.  Default is 'compile'.
-x|--msvc
  Convert the build output messages to MSVC format.
-c|--cygwin
  Cygwin mode.  Convert file names of the form '/cygdrive/(DRIVE)/...' to
	'(DRIVE):/...'.  The pattern is recognized for stand alone options _and_ for
	paths follwing a -I option.
-b|--basedir BASEDIR
  Used in Cygwin mode only.  The base directory in Windows notation
	(drive:path).  If this option is specified, then the path prefix '/base' is
	converted to the specified base directory.
-p|--prefix PREFIX
  Strip the specified prefix from filenames in the output (works only for
  recognized output lines, unrecognized lines are echoed as-is).
-i|--ignore IGNOREFILE
  Specify a file containing ignore patterns.  All non-empty lines of the
  specified file are added to the list of ignore patterns.
-e|--error ERRORFILE
  Specify a file containing error patterns.  All non-empty lines of the
	specified file are added to the list of error patterns.  Whenever one of the
	error patterns is recognized in the compiler or linker output, then the
	build script will report an error to the caller (error code 7).
-s|--silent
  Silent operation.  No output except for the command output.  (Normal
  operation will echo the command executed.)
--ps3
	PS3 mode.  In PS3 mode and link mode, the link library order is patched and
	stub libraries are moved to the end of the list.
--ldlist
  LD file list prefix mode.  If this option is sepcified, then file list
	arguments are marked as linker arguments.  This means that argument of the
	form \@\$file are transformed into -Wl,\@\$file.  If the called executable
	is a linker (executable names 'ld' or 'PREFIX-ld'), then the LD file list
	mode is turned off unconditionally.  LD file list mode is enabled by default
	and can be turned off by passing the '--no-ldlist' option.
--cgmode
  Code generator mode.  This option enables some special argument
	transformations for the CryCG code generator.  This mode is enabled
	automatically if the executable name is 'crycg'.
--cd
  Change the current working directory to output directory.  The output
	directory is determined from the path specified with the -o command option.
	For the 'compile' and 'compile_pch' modes, this option is enabled
	automatically if the '-save-temps' command option is present.
-h|--help
  Display this help screen and exit.
Note:  Separating the command from the options with a double hyphen is not
strictly required, but strongly recommended.
EOF
  exit 0;
}

# Update the operation mode based on the command executable name.
if ($#ARGV > 0)
{
	my $cmd = $ARGV[0];

	if ($cmd =~ /\bld(\.exe)?$/
			or $cmd =~ /\/ld(\.exe)?$/
			or $cmd =~ /-ld(\.exe)?$/)
	{ $optLdList = 0; }

	if ($cmd =~ /\bcrycg(\.exe)?$/
			or $cmd =~ /\/crycg(\.exe)?$/)
	{ $optCryCGMode = 1; }
}

# List of message blocks.
our @blockList = ( );

# List of skip patterns.
our @skipPatterns = ( );

# List of error patterns.
our @errorPatterns = ( );

# Read the list of skip patterns.
sub readSkipPatterns
{
  local *IN;
  if ($optIgnoreFile)
  {
    open(IN, $optIgnoreFile)
			or die "Can't open skip pattern file '$optIgnoreFile': $!";
    while (<IN>)
    {
      if (/^\s*$/ or /^#/) { next; }
		  s/^\s+|\s+$//g;
      push @skipPatterns, $_;
    }
    close IN;
  }
}

# Read the list of error patterns.
sub readErrorPatterns
{
	local *IN;
	if ($optErrorFile)
	{
		open(IN, $optErrorFile)
			or die "Can't open error pattern file '$optErrorFile': $!";
		while (<IN>)
		{
			if (/^\s*$/ or /^#/) { next; }
		  s/^\s+|\s+$//g;
			push @errorPatterns, $_;
		}
		close IN;
	}
}

# This flag is set whenever an error pattern is found in the tool output.
my $errorPatternFound = 0;

# Filter input lines.  The function returns 1 if the line contains one of the
# filter patterns specified by the script caller.  If an error pattern is
# found, then the $errorPatternFound flag is set and an error message is
# written to the standard error.
sub filter
{
  my $line = shift;
  my $pattern;
  foreach $pattern (@skipPatterns)
  {
    if ($line =~ /$pattern/) { return 1; }
  }
	foreach $pattern (@errorPatterns)
	{
		if ($line =~ /$pattern/)
		{
			$line =~ s/warning:/error:/g;
			$line =~ s/\s+$//;
			print STDERR "$line [error pattern]\n";
			$errorPatternFound = 1;
		}
	}
  return 0;
}

# Convert a filename for the target environment.
sub convertFilename
{
  my $filename = shift;
  if ($optPrefix and $filename =~ /^${optPrefix}(.*$)/i)
	{
		$filename = $1;
	}
  # Remove /X/../ and /./ sequences from the filenames.
	# Note: This will not work correctly if the source tree is fucked up with
	# directory symlinks - don't do that!
	while ($filename =~ /\/\.\//)
	{
		$filename =~ s/\/\.\//\//;
	}
	while ($filename =~ /\/[^\/]+\/\.\.\//)
	{
		$filename =~ s/\/[^\/]+\/\.\.\//\//;
	}
  return $filename;
}

# Convert the messages for the target environment.
sub convert
{
  my $line = shift;
  if ($line =~ /^In file included from ([^:]+):([0-9]+)([,:])$/)
  {
    my $lineno = $2; my $sep = $3;
    my $filename = convertFilename $1;
    if ($optConvertMSVC) {
      $line = "$filename($lineno): Included from here...\n";
    } else {
      $line = "In file included from $filename:${lineno}$sep\n";
    }
  }
  elsif ($line =~ /^\s*from ([^:]+):([0-9]+)([,:])$/)
  {
    my $lineno = $2; my $sep = $3;
    my $filename = convertFilename $1;
    if ($optConvertMSVC) {
      $line = "$filename($lineno): from here...\n";
    } else {
      $line = "                 from $filename:${lineno}$sep\n";
    }
  }
  elsif ($line =~ /^(\S..[^:]*):([0-9]+):([0-9]+):(.*)$/)
  {
    my $lineno = $2; my $column = $3; my $message = $4;
    my $filename = convertFilename $1;
    if ($optConvertMSVC) {
      $line = "$filename($lineno):$message\n";
    } else {
      $line = "$filename:$lineno:$column:$message\n";
    }
  }
  elsif ($line =~ /^(\S..[^:]*):([0-9]+):(.*)$/)
  {
    my $lineno = $2; my $message = $3;
    my $filename = convertFilename $1;
    if ($optConvertMSVC) {
      $line = "$filename($lineno):$message\n";
    } else {
      $line = "$filename:$lineno:$message\n";
    }
  }
  elsif ($line =~ /^(\S..[^:]*):(.*)$/)
  {
    my $message = $2;
    my $filename = convertFilename $1;
    $line = "$filename:$message\n";
  }
  return $line;
}

# Process a block of command output.
sub processBlock
{
  my @block = ( );
  foreach (@_)
  {
    if (filter $_) { return; }
    push @block, convert $_;
  }
  push @blockList, join '', @block;
}

# Execute the specified command and process the output
sub processCmd
{
  my @block = ( );
	my $exitCode = 0;
	my $arg;
	my @args = ( );
	my @echoArgs = ( );
	my @cdArgs = ( );
	my @ps3StubArgs = ( );
	my $outputFile;
	my $outputDir;
	my $cd = $optCD;
	my $saveTemps = 0;
	my $oOption = 0;

	foreach $arg (@ARGV)
	{
		my $echoSuffix = '';
		if ($oOption)
		{
			$outputFile = $arg;
			$oOption = 0;
		}
		elsif ($arg =~ /^-o(.*$)/)
		{
			if ($1) { $outputFile = $1; } else { $oOption = 1; }
		}
		if (not $optSilentMode and $arg =~ /^@(.*$)/)
		{
			my $listFile = $1;
			$echoSuffix = '(';
			my $space = '';
			local *LIST;
			open(LIST, $listFile)
				or die "Can't read command input file '$listFile': $!";
			while (<LIST>)
			{
				s/^\s+|\s+$//g;
				$echoSuffix .= $_.$space;
				$space = ' ';
			}
			$echoSuffix .= ')';
			close LIST;
		}
		if ($optCygwinMode)
		{
			if ($arg =~ /^(-[IL]|)\/base\/(.*$)/)
			{
				$arg = "$1$optCygwinBaseDir/$2";
			}
			if ($arg =~ /^(-[IL]|)\/cygdrive\/([a-z])\/(.*$)/)
			{
				$arg = "$1$2:/$3";
			}
			if ($arg =~ /^@\/base\/(.*$)/)
			{
				$arg = "\@$optCygwinBaseDir/$1";
			}
			if ($arg =~ /^@\/cygdrive\/([a-z])\/(.*$)/)
			{
				$arg = "\@$1:/$2";
			}
		}
		if ($optLdList)
		{
			$arg =~ s/^@/-Wl,@/;
		}
		if ($optCryCGMode and $optCygwinMode)
		{
			if ($arg =~ /-([CD][^:=]+[:=])\/base\/(.*$)/)
			{
				$arg = "-$1$optCygwinBaseDir/$2";
			}
			if ($arg =~ /-([CD][^:=]+[:=])\/cygdrive\/([a-z])\/(.*$)/)
			{
				$arg = "-$1$2:/$3";
			}
		}
		if ($optMode eq 'compile' or $optMode eq 'compile_pch')
		{
			if ($arg =~ /^-save-temps$/) { $saveTemps = 1; }
		}
		$arg =~ s/\\/\//g;
		$arg =~ s/\/\/*/\//g;
		if ($optPS3Mode
				and $optMode eq 'link'
				and ($arg =~ /^-l.*_stub$/ or $arg =~ /^-Wl,--(no-)?whole-archive$/))
		{
			push @ps3StubArgs, $arg;
		}
		else
		{
			push @args, $arg;
			push @echoArgs, $arg . $echoSuffix;
		}
	}
	if ($outputFile)
	{
		$outputDir = $outputFile;
		$outputDir =~ s/[\/\\][^\/\\]+$//;
		if ($outputFile eq $outputDir) { $outputDir = '.'; }
	}
	foreach $arg (@ps3StubArgs)
	{
		push @args, $arg;
		push @echoArgs, $arg;
	}
	if ($saveTemps and $outputDir) { $cd = 1; }
	my $cmd = shell_quote @args;
	if ($cd) { $cmd = shell_quote('cd', $outputDir) . '&&' . $cmd; }
	if (not $optSilentMode)
	{
		my $echoCmd = shell_quote @echoArgs;
		print "$echoCmd\n";
	}
  local *CMDOUT;
  open CMDOUT, '-|', $cmd.' 2>&1 || echo @FAILED:$?@'
		or die "can not execute command: $!";
  $_ = <CMDOUT>;
  while ($_)
  {
		if (/\@FAILED:([0-9]+)\@/)
		{
			$exitCode = $1;
			if (not <CMDOUT>) { last; }
		}
    if (/^In /)
    {
      push @block, $_;
      while (<CMDOUT>)
      {
				if (/^\s*from /) { push @block, $_; } else { last; }
      }
			if (not $_) { last; }
    }
    if (/^[^:]*: In /)
    {
      push @block, $_;
      while (<CMDOUT>)
      {
				if (/^[^:]*: In /) { push @block, $_; } else { last; }
      }
			if (not $_) { last; }
    }
    if ($_) { push @block, $_; }
    while (<CMDOUT>)
    {
      if (/^[^:]*:   /) { push @block, $_; } else { last; }
    }
    processBlock(@block);
    @block = ( );
    if (not $_) { last; }
  }
  close CMDOUT;
	return $exitCode;
}

readSkipPatterns;
readErrorPatterns;
my $exitCode = processCmd;
foreach (@blockList) { print; }
if ($errorPatternFound and $exitCode == 0) { $exitCode = 7; }
exit $exitCode;

# Tools/build.pl
# vim:ts=2:sw=2

