#!/usr/bin/env perl
# apbuild - a GCC wrapper for creating portable binaries
# Copyright (c) 2003 Hongli Lai
# Distributed under the GNU General Public License

use warnings;
use strict;
use FindBin;
use IPC::Open3;
use Cwd qw(abs_path);

our $appath = undef;
our $gcc = 'gcc';
$appath = $ENV{'APBUILD_PATH'} if (defined ($ENV{'APBUILD_PATH'}) && $ENV{'APBUILD_PATH'} ne '');
$gcc = $ENV{'APBUILD_CC'} if (defined ($ENV{'APBUILD_CC'}) && $ENV{'APBUILD_CC'} ne '');

if (!defined ($appath)) {
	$appath = $FindBin::Bin;
	$appath =~ s/^(.*)\/.*?$/$1/;
	$appath .= '/include/apbuild';
	if (! -f "$appath/apsymbols.h" && -f "$FindBin::Bin/apsymbols.h") {
		$appath = "$FindBin::Bin";
	}
}


# Special constants
our @linking = ('-Wl,--rpath,${ORIGIN}/../lib');
our @include = ('-I', $appath, '-include', 'apsymbols.h');
our $objectTypes = 'o|a|so|la|lo|al';

# These are options that require an extra parameter
our $extraTypes = 'o|u|Xlinker|b|V|MF|MT|MQ|I|L|R';


# Check whether gcc supports -shared-libgcc.
# This prevents gcc from statically linking libgcc_s.a,
# which creates a GCC_3.0 dependancy on /lib/libc.so.6
CHECKGCC: {
	# Check if cache file already exists and is up-to-date

	my ($gcc_filename, @stat, $gcc_mtime);
	if (-x $gcc) {
		$gcc_filename = abs_path ($gcc);
	}
	foreach my $dir (split (/:+/, $ENV{'PATH'})) {
		if (-x "$dir/$gcc") {
			$gcc_filename = "$dir/$gcc";
			last;
		}
	}

	if (!defined $gcc_filename) {
		print STDERR "$gcc: command not found\n";
		exit (127);
	}

	@stat = stat ($gcc_filename);
	$gcc_mtime = $stat[9];
	if (open (F, "< $ENV{'HOME'}/.apbuild")) {
		foreach my $line (<F>) {
			$line =~ s/\n//;
			my ($filename, $supported, $mtime) = split (/\t/, $line);
			if ($filename eq $gcc_filename) {
				if ($mtime eq $gcc_mtime) {
					push (@linking, '-shared-libgcc') if ($supported);
					last CHECKGCC;
				} else {
					# Cache outdated
					unlink ("$ENV{'HOME'}/.apbuild");
					last;
				}
			}
		}
		close (F);
	}

	# Detect gcc version
	my ($r, $w, $e, $lc_all);

	$lc_all = $ENV{'LC_ALL'};
	$ENV{'LC_ALL'} = 'C';

	my $pid = open3 ($w, $r, $r, $gcc, '-v');
	my @output = <$r>;
	close ($r);
	close ($w);
	waitpid ($pid, 0);

	if (defined ($lc_all)) {
		$ENV{'LC_ALL'} = $lc_all;
	} else {
		delete $ENV{'LC_ALL'};
	}

	# Check whether gcc >= 3.0
	my ($major) = $output[@output - 1] =~ /version ([0-9]+)/;
	my $supported = 0;
	if ($major >= 3)
	{
		push (@linking, '-shared-libgcc');
		$supported = 1;
	}

	# Save this information in a file
	open (F, ">> $ENV{'HOME'}/.apbuild");
	print F "$gcc_filename\t$supported\t$gcc_mtime\n";
	close (F);
}


# Basically, there are 5 situations in which the compiler is used:
# 1) Compilation (to an object file).
# 2) Linking.
# 3) Compilation and linking.
# 4) Dependancy checking with -M* or -E.
# 5) None of the above. Compiler is invoked with --help or something.
# Note that source files may also contain non-C/C++ files.

our $situation = undef;

for (@ARGV) {
	if (/^-c$/) {
		$situation = 1;
		last;
	} elsif (/^-M(|M|G)$/ || /^-E$/) {
		$situation = 4;
		last;
	}
}

if (!defined ($situation)) {
	our $i = 0;
	for (@ARGV) {
		if (!(/^-/) && !(/\.($objectTypes)$/)) {
			if ($i > 0 && $ARGV[$i - 1] =~ /^-($extraTypes)$/) {
				$i++;
				next;
			}
			$situation = 3;
			last;
		}
		$i++;
	}
}

if (!defined ($situation)) {
	our $files = 0;
	for (@ARGV) {
		$files++ if (!(/^-/));
	}

	if ($files == 0) {
		$situation = 5;
	} else {
		$situation = 2;
	}
}


# Handle each situation
debug ("apgcc @ARGV\n");
debug ("Situation: $situation\n");

# Only situations that involve compiling or linking need to be treated specially
if ($situation == 1) {
	# Extract the parameters and files
	our @params = ();
	our @files = ();
	our @command = ();
	our $status;
	our $i = 0;

	for (@ARGV) {
		if (/^-/ || /\.($objectTypes|h)$/ || ($i > 0 && $ARGV[$i - 1] =~ /^-($extraTypes)$/)) {
			push (@params, $_);
		} else {
			push (@files, $_);
		}
		$i++;
	}

	for (@files) {
		if (/\.(c|C|cpp|cc|c\+\+)$/) {
			@command = ($gcc, @include, $_, @params);
		} else {
			@command = ($gcc, $_, @params);
		}

		debug ("@command\n");
		$status = system (@command);
		exit (127) if ($status == -1);
		exit ($status / 256) if ($status != 0);
	}

} elsif ($situation == 2) {
	our @command = ($gcc, @linking, manipulateLinkerOptions (@ARGV));
	debug ("@command\n");
	exec (@command);

} elsif ($situation == 3) {
	# Extract the parameters and files
	our @params = ();
	our @files = ();
	our @command = ();
	our $i = 0;
	our $output = 'a.out';
	our $status;

	for (@ARGV) {
		if (/^-/) {
			# Filter out linker options
			push (@params, $_) unless (/^-(L|Wl|o$|l|s|n|R)/);
		} elsif ($i > 0 && $ARGV[$i - 1] =~ /^-($extraTypes)$/) {
			$output = $_;
		} else {
			push (@files, $_);
		}
		$i++;
	}

	# Compile & link only one file
	if (@files == 1) {
		@command = ($gcc, @include, @linking, manipulateLinkerOptions (@ARGV));
		debug ("@command\n");
		exec (@command);
	}

	# Compile individual files into objects
	for (@files) {
		if (/\.(c|C|cpp|cc|c\+\+)$/) {
			@command = ($gcc, @include, '-c', @params, $_);
		} elsif (/\.($objectTypes)$/) {
			next;
		} else {
			@command = ($gcc, '-c', @params, $_);
		}

		debug ("@command\n");
		$status = system (@command);
		exit (127) if ($status == -1);
		exit ($status / 256) if ($status != 0);

		s/^(.*)\..*?$/$1.o/;
	}

	# Finally, link all objects together
	# Filter out arguments
	@params = ();
	for (@ARGV) {
		push (@params, $_) if (/^-/);
		push (@params, $output) if (/^-o$/);
	}

	@command = ($gcc, @linking, @files, manipulateLinkerOptions (@params));
	debug ("@command\n");
	exec (@command);

} else {
	my $ret = system ($gcc, @ARGV);
	$ret /= 256;
	if (defined $ARGV[0] && $ARGV[0] eq '--help') {
		print	"\napbuild environment variables:\n";
		print	"  APBUILD_PATH=path         Specifies the include path for apsymbols.h\n" .
			"                            (like: /usr/local/include/apbuild)\n" .
			"  APBUILD_DEBUG=1           Enable debugging messages\n" .
			"  APBUILD_NO_STATIC_X       Do not force static linking to certain non-standard\n" .
			"                            X libraries (like libXrender)\n" .
			"  APBUILD_BOGUS_DEPS=deps   Specify a list of whitespace-seperated bogus\n" .
			"                            library dependancies (like: X11 ICE png). These\n" .
			"                            libraries will not be linked.\n";
	}
	exit ($ret);
}



sub manipulateLinkerOptions {
	my @argv = @_;
	@argv = removeBogusDeps (@argv);
	@argv = forceStatic (@argv);
	return @argv;
}


# Remove bogus library dependancies
sub removeBogusDeps {
	return @_ if (!defined ($ENV{'APBUILD_BOGUS_DEPS'}) || $ENV{'APBUILD_BOGUS_DEPS'} eq "");
	my @bogus = split (/ +/, $ENV{'APBUILD_BOGUS_DEPS'});
	return @_ if (@bogus == 0);

	my @argv = ();
	my $i = 0;
	foreach my $arg (@_) {
		# Remove bogus -l linker options
		my $remove = 0;
		foreach my $dep (@bogus) {
			if ($arg eq "-l$dep" || ($dep eq "pthread" && $arg eq "-pthread")) {
				$remove = 1;
				last;
			}
		}
		if ($remove) {
			$i++;
			next;
		}

		# Remove bogus /usr/wherever/foo.so linker options
		if (!($i > 0 && $_[$i - 1] =~ /^-($extraTypes)$/) && $arg =~ /\/lib.*?\.so$/) {
			my ($soname) = $arg =~ /^.*\/lib(.*?)\.so$/;
			foreach my $dep (@bogus) {
				if ($soname eq $dep) {
					$remove = 1;
					last;
				}
			}
		}
		push (@argv, $arg) if (!$remove);
		$i++;
	}

	return @argv;
}


# Force static linking against certain X libraries.  They're not big anyway.
sub forceStatic {
	return @_ if (defined ($ENV{'APBUILD_NO_STATIC_X'}) && $ENV{'APBUILD_NO_STATIC_X'} eq "1");

	my @xlibs = qw (Xrender Xcursor Xi Xinerama Xrandr Xv Xxf86dga Xxf86misc Xxf86vm);
	my @libs = ();

	for (@_) {
		# Add all non-linking options
		unless (/^-l/) {
			push (@libs, $_);
			next;
		}

		# Is this an X library? If yes, static link it
		for my $xlib (@xlibs) {
			next unless ($_ eq "-l${xlib}");

			push (@libs, '-Wl,-Bstatic');
			push (@libs, $_);
			push (@libs, '-Wl,-Bdynamic');
			last;
		}
		# No; add it
		push (@libs, $_);
	}
	return @libs;
}


our $debugOpened = 0;

sub debug {
	return if (!$ENV{'APBUILD_DEBUG'});

	if (!$debugOpened) {
		if (open (DEBUG, '>/dev/tty')) {
			$debugOpened = 1;
		} else {
			return;
		}
	}

	my @args = split (/\n/, "@_");
	foreach (@args) {
		$_ = '# ' . $_;
		$_ .= "\n";
	}

	print DEBUG "\033[1;33m";
	print DEBUG join ('', @args);
	print DEBUG "\033[0m";
}
