#!/usr/bin/perl
#-------------------------------------------------------------------------------
# unicc_gnu.pl
#
# Copyright (C) 2006-2008 Oliver Hamann.
#
# Homepage: http://eaglemode.sourceforge.net/
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License version 3 as published by the
# Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------

use strict;
use warnings;
use Config;
use File::Basename;
use File::Spec::Functions;
use File::stat;


#=============================== Parse arguments ===============================

my $Type="cexe";
my $Name="unnamed";
my $ObjDir=".";
my $BinDir=".";
my $LibDir=".";
my @LibSearchDirs=();
my @IncSearchDirs=();
my @Links=();
my $Math=0;
my $Rtti=0;
my $Exceptions=0;
my $Debug=0;
my @Defs=();
my @Srcs=();

for (my $i=0; $i<@ARGV; $i++) {
	if ($ARGV[$i] eq "--type" and $i+1<@ARGV) {
		$Type=$ARGV[++$i];
	}
	elsif ($ARGV[$i] eq "--name" and $i+1<@ARGV) {
		$Name=$ARGV[++$i];
	}
	elsif ($ARGV[$i] eq "--obj-dir" and $i+1<@ARGV) {
		$ObjDir=$ARGV[++$i];
	}
	elsif ($ARGV[$i] eq "--bin-dir" and $i+1<@ARGV) {
		$BinDir=$ARGV[++$i];
	}
	elsif ($ARGV[$i] eq "--lib-dir" and $i+1<@ARGV) {
		$LibDir=$ARGV[++$i];
		push(@LibSearchDirs,$LibDir);
	}
	elsif ($ARGV[$i] eq "--lib-search-dir" and $i+1<@ARGV) {
		push(@LibSearchDirs,$ARGV[++$i]);
	}
	elsif ($ARGV[$i] eq "--inc-search-dir" and $i+1<@ARGV) {
		push(@IncSearchDirs,$ARGV[++$i]);
	}
	elsif ($ARGV[$i] eq "--link" and $i+1<@ARGV) {
		push(@Links,$ARGV[++$i]);
	}
	elsif ($ARGV[$i] eq "--math") {
		$Math=1;
	}
	elsif ($ARGV[$i] eq "--rtti") {
		$Rtti=1;
	}
	elsif ($ARGV[$i] eq "--exceptions") {
		$Exceptions=1;
	}
	elsif ($ARGV[$i] eq "--debug") {
		$Debug=1;
	}
	elsif ($ARGV[$i] eq "--def" and $i+1<@ARGV) {
		push(@Defs,$ARGV[++$i]);
	}
	elsif ($ARGV[$i]=~/^--/) {
		die("Unknown option '$ARGV[$i]', stopped");
	}
	else {
		push(@Srcs,$ARGV[$i]);
	}
}

if (@Srcs <= 0) {
	die("No source files, stopped");
}


#========================= Determine compiler version ==========================

my $GccVersion; # real or integer number
my $IsWin;
my $IsCygwin;
my $IsWinOrCygwin;

{
	my $s=`gcc --version`;
	if ($? != 0) { exit(1); }
	for (;; $s=substr($s,1)) {
		if (length($s)==0) { die("Cannot determine GCC version, stopped"); }
		if (ord($s)>=48 and ord($s)<=57) { last; }
	}
	my $l;
	for ($l=1; $l<length($s); $l++) {
		if (ord(substr($s,$l))<48 or ord(substr($s,$l))>57) { last; }
	}
	if (substr($s,$l,1) eq "." and ord(substr($s,$l+1))>=48 and
	    ord(substr($s,$l+1))<=57) {
		for ($l=$l+2; $l<length($s); $l++) {
			if (ord(substr($s,$l))<48 or ord(substr($s,$l))>57) { last; }
		}
	}
	$GccVersion=substr($s,0,$l);
	if ($Config{'osname'} eq 'MSWin32') { $IsWin=1; }
	else { $IsWin=0; }
	if ($Config{'osname'} eq 'cygwin') { $IsCygwin=1; }
	else { $IsCygwin=0; }
	$IsWinOrCygwin=$IsWin||$IsCygwin;
}


#================= Have paths of output files and other infos ==================

my @CSuffixes;
my @CppSuffixes;
my @Objs=();
my @IsCpp=();
my $AnyCpp;
my $Output;
my $ImportLib;

@CSuffixes=(".c", ".C");
@CppSuffixes=(".cpp", ".cxx", ".cc");
$AnyCpp=0;
for (my $i=0; $i<@Srcs; $i++) {
	my ($f,$d,$s)=fileparse($Srcs[$i],@CSuffixes);
	if ($s eq "") {
		($f,$d,$s)=fileparse($Srcs[$i],@CppSuffixes);
		if ($s eq "") {
			die("Source file '$Srcs[$i]' has illegal suffix, stopped");
		}
		push(@IsCpp,1);
		$AnyCpp=1;
	}
	else {
		push(@IsCpp,0);
	}
	push(@Objs,catfile($ObjDir,"$f.o"));
}

if ($Type eq "cexe" or $Type eq "wexe") {
	if ($IsWinOrCygwin) {
		$Output=catfile($BinDir,"$Name.exe");
	}
	else {
		$Output=catfile($BinDir,$Name);
	}
}
elsif ($Type eq "lib") {
	$Output=catfile($LibDir,"lib$Name.a");
}
elsif ($Type eq "dynlib") {
	if ($IsWinOrCygwin) {
		$Output=catfile($LibDir,"$Name.dll");
		$ImportLib=catfile($LibDir,"lib$Name.dll.a");
	}
	else {
		$Output=catfile($LibDir,"lib$Name.so");
	}
}
else {
	die("Illegal type of output: '$Type', stopped");
}


#==================== GetDirectIncludes & GetSrcUpdateTime =====================

my %DirectIncludesCache;
my %SrcUpdateTimeCache;

sub GetDirectIncludes
{
	my $srcFile=shift;
	my @result;
	my $fh;

	if (exists $DirectIncludesCache{$srcFile}) {
		return @{$DirectIncludesCache{$srcFile}};
	}
	@result=();
	open($fh,$srcFile);
	while (defined(my $s=readline($fh))) {
		if (index($s,"#")<0) { next; }
		while (ord($s)<=32) { $s=substr($s,1); }
		if (substr($s,0,1) ne "#") { next; }
		$s=substr($s,1);
		while (ord($s)<=32) { $s=substr($s,1); }
		if (substr($s,0,7) ne "include") { next; }
		$s=substr($s,7);
		while (ord($s)<=32) { $s=substr($s,1); }
		my $e;
		if (substr($s,0,1) eq "<") { $e=">"; }
		elsif (substr($s,0,1) eq "\"") { $e="\""; }
		else { next; }
		my $i=index($s,$e,2);
		if ($i<2) { next; }
		$s=substr($s,1,$i-1);
		my @dirs=@IncSearchDirs;
		if ($e eq "\"") {
			my ($f,$d)=fileparse($s);
			@dirs=($d,@dirs);
		}
		for (my $i=0; $i<@dirs; $i++) {
			my $p=catfile($dirs[$i],$s);
			if (! -f $p) { next; }
			push(@result,$p);
			last;
		}
	}
	close($fh);
	$DirectIncludesCache{$srcFile}=[@result];
	return @result;
}

sub GetSrcUpdateTime
{
	my $srcFile=shift;
	my $r;

	$r=$SrcUpdateTimeCache{$srcFile};
	if (defined($r)) {
		if ($r!=-1) { return $r; }
		my @a=($srcFile);
		for (my $i=0; $i<@a; $i++) {
			my $s=stat($a[$i]);
			if ($i==0 or $r<$s->mtime) { $r=$s->mtime; }
			my @d=GetDirectIncludes($a[$i]);
			for (my $j=0; $j<@d; $j++) {
				my $k;
				for ($k=0; $k<@a; $k++) {
					if ($d[$j] eq $a[$k]) { last; }
				}
				if ($k>=@a) { push(@a,$d[$j]); }
			}
		}
	}
	else {
		$SrcUpdateTimeCache{$srcFile}=-1;
		my $s=stat($srcFile);
		$r=$s->mtime;
		my @d=GetDirectIncludes($srcFile);
		for (my $i=0; $i<@d; $i++) {
			my $t=GetSrcUpdateTime($d[$i]);
			if ($r<$t) { $r=$t }
		}
	}
	$SrcUpdateTimeCache{$srcFile}=$r;
	return $r;
}


#==================== Compile source files to object files =====================

for (my $i=0; $i<@Srcs; $i++) {
	if (-f $Objs[$i]) {
		my $s=stat($Objs[$i]);
		if ($s->mtime>=GetSrcUpdateTime($Srcs[$i])) { next; }
	}
	my @args=();
	push(@args,"gcc");
	if ($Debug) { push(@args,"-g"); }
	push(@args,"-O2");
	if ($Type eq "dynlib" and !$IsWinOrCygwin) { push(@args,"-fPIC"); }
	if ($Rtti==0) { push(@args,"-fno-rtti"); }
	if ($Exceptions) { push(@args,"-fexceptions"); }
	else { push(@args,"-fno-exceptions"); }
	push(@args,"-funsigned-char");
	if ($GccVersion>=3.0) {
		push(@args,"-fmessage-length=0");
	}
	push(@args,"-Wall");
	if ($GccVersion<4.3) { #??? GCC 4.3.0 does not inline several destructors.
		push(@args,"-Winline");
	}
	push(@args,"-Wpointer-arith");
	push(@args,"-Wredundant-decls");
	push(@args,"-Wsign-compare");
	push(@args,"-Wwrite-strings");
	if ($IsCpp[$i]) {
		push(@args,"-Wsign-promo");
		push(@args,"-Wsynth");
	}
	if ($GccVersion>=3.0) {
		push(@args,"-Wshadow");
		if ($GccVersion>=4.0) {
			push(@args,"-Wstrict-aliasing=2");
			if ($IsCpp[$i]) {
				push(@args,"-Wno-invalid-offsetof");
			}
		}
	}
	for (my $j=0; $j<@IncSearchDirs; $j++) {
		push(@args,"-I$IncSearchDirs[$j]");
	}
	for (my $j=0; $j<@Defs; $j++) {
		push(@args,"-D$Defs[$j]");
	}
	push(@args,"-c");
	push(@args,$Srcs[$i]);
	push(@args,"-o");
	push(@args,$Objs[$i]);

	print("@args\n");
	if (system(@args)!=0) { exit(1); }
}


#=========================== Output file up-to-date? ===========================

if (-e $Output) {
	my $t=0;
	for (my $i=0; $i<@Objs; $i++) {
		my $s=stat($Objs[$i]);
		if ($t<$s->mtime) { $t=$s->mtime; }
	}
	for (my $i=0; $i<@Links; $i++) {
		for (my $j=0; $j<@LibSearchDirs; $j++) {
			my $p;
			$p=catfile($LibSearchDirs[$j],"lib$Links[$i].a");
			if (-e $p) {
				my $s=stat($p);
				if ($t<$s->mtime) { $t=$s->mtime; }
			}
			if ($IsWinOrCygwin) {
				$p=catfile($LibSearchDirs[$j],"lib$Links[$i].dll.a");
			}
			else {
				$p=catfile($LibSearchDirs[$j],"lib$Links[$i].so");
			}
			if (-e $p) {
				my $s=stat($p);
				if ($t<$s->mtime) { $t=$s->mtime; }
			}
		}
	}
	my $s=stat($Output);
	if ($s->mtime>=$t) { exit(0); }
}


#============================= Produce output file =============================

# Delete old output file.
if (-e $Output) { unlink($Output); }

# Create new one.
if ($Type eq "cexe" or $Type eq "wexe" or $Type eq "dynlib") {
	my @args=();
	push(@args,"gcc");
	if ($Debug) { push(@args,"-g"); }
	if ($Type eq "dynlib") { push(@args,"-shared"); }
	if ($IsWin and $Type eq "wexe") { push(@args,"-mwindows"); }
	for (my $i=0; $i<@LibSearchDirs; $i++) {
		push(@args,"-L$LibSearchDirs[$i]");
	}
	push(@args,@Objs);
	for (my $i=0; $i<@Links; $i++) {
		push(@args,"-l$Links[$i]");
	}
	if ($IsCygwin && -e "/lib/libcygipc.a") { push(@args,"-lcygipc"); }
	if ($Math) { push(@args,"-lm"); }
	if ($AnyCpp) { push(@args,"-lstdc++"); }
	push(@args,"-o");
	push(@args,$Output);
	if ($IsWinOrCygwin and $Type eq "dynlib") {
		push(@args,"-Xlinker");
		push(@args,"--out-implib");
		push(@args,"-Xlinker");
		push(@args,$ImportLib);
	}
	print("@args\n");
	if (system(@args)!=0) { exit(1); }
}
elsif ($Type eq "lib") {
	my @args=();
	push(@args,"ar");
	push(@args,"rcs");
	push(@args,$Output);
	push(@args,@Objs);
	print("@args\n");
	if (system(@args)!=0) { exit(1); }
}
else {
	die("Illegal type of output: '$Type', stopped");
}
