#!/usr/bin/perl -w
#
#  emchain -- toolchain builder for cross compiling
#  Copyright (C) 2006, 2007  Neil Williams <codehelp@debian.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  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 for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#

use Cwd;
use File::HomeDir;
use Config::Auto;
use Emdebian::Tools;
use strict;
use warnings;
use vars qw/ $verbose $dpkg_cross_dir $suite $arch $line $aptcmd $skip_gcc $skip_binutils $skip_libc $host $gcc_latest $gcc_vers $gcc_pkg_v $binutils_vers $libc_latest $libc_vers $libc_pkg_v $binutils_dir $gcc_dir $dev $binutils_deb @glibc_list @glibc_list2 @glibc_list3 $check $glibc_count $home $our_version $progname $logfile $date $log $cmdline $status $environ $result $workdir $msg %archtable/;

require "dpkg-cross.pl";
$our_version = "0.0.5";
$verbose = 1;
$suite = &get_suite();
# read in dpkg-cross default arch
&read_config();
$arch = &get_architecture();
$dpkg_cross_dir = &get_dpkg_cross_dir();
$workdir = &get_workdir;
$workdir = "." if ($workdir eq "");
$msg = &check_workdir($workdir);
die $msg if ($msg ne "");
chdir ("$workdir") if ($workdir ne ".");
chdir ("emchain") if ( -d "emchain");
print "Building toolchian in '" . cwd . "'\n" if ($verbose >= 2);
$date = `date +%F`;
$date =~ s/\n//;

## Pseudo-code:
# $ apt-get source gcc-3.4 binutils libc
# $ cd binutils-2.17
# $ TARGET=$target_gnu_type fakeroot debian/rules binary-cross (replace arch by arm, alpha,...)
# dpkg -i built-package
# apt-cross -i libdb1-compat libc6 libc6-dev linux-kernel-headers
# $ export GCC_TARGET=$arch 
# $ export DEB_CROSS_INDEPENDENT=yes (for gcc-4.1 and later).
# $ cd gcc-3.4-3.4.2
# $ debian/rules control
# $ dpkg-buildpackage -b -rfakeroot

sub usageversion {
    print(STDERR <<END)
emchain version $our_version

Usage:
 emchain [-a|--arch ARCH] [-w|--workdir] [-l|--log] [-v|--verbose] [-q|--quiet]
 emchain -h|--help|--version
 
Options:
 -a|--arch ARCH:      set architecture (default: defined by dpkg-cross)
 -w|--workdir DIR:    override the current working directory setting 
 -l|--log:            write a logfile in the working directory
 -v|--verbose:        be verbose (repeat for more verbosity)
 -q|--quiet:          be quiet [default]
 -h|--help:           print this usage message and exit
 --version:           print this usage message and exit

emchain implements the EmdebianSlind toolchain build process for
the latest versions of gcc, binutils and libc. To build toolchains
for older compilers, see the Emdebian website: www.emdebian.org

emchain uses dpkg-cross and apt-cross to determine the latest
versions of toolchain packages, downloads the source for each missing
package, builds the package using EmdebianSlind options and commands,
installs the cross-built packages and toolchain packages.

emchain needs to be run from a consistent working directory so that it
can check if new versions need to be downloaded. If an existing working 
directory has not been specified using debconf, the current directory
will be used.

END
        || die "$progname: failed to write usage: $!\n";
}

while( @ARGV ) {
    $_= shift( @ARGV );
    last if m/^--$/;
    if (!/^-/) {
        unshift(@ARGV,$_);
		last;
    }
	elsif (/^(-h|--help|--version)$/) {
        &usageversion();
		exit( 0 );
	}
	elsif (/^(-v|--verbose)$/) {
		$verbose++;
	}
	elsif (/^(-q|--quiet)$/) {
		$verbose--;
	}
	elsif (/^(-a|--arch)$/) {
		$arch = shift(@ARGV);
	}
	elsif (/^(-w|--workdir)$/) {
		$workdir = shift(@ARGV);
		chdir ("$workdir");
	}
	elsif (/^(-l|--log)$/) {
		$logfile = "emchain-" . $arch . "-" . $date . ".log";
	}
}

$msg = "$progname: no default architecture found.\n";
$msg .= "Please use '$progname --arch ARCH'.\n";
die $msg if ((!$arch)||($arch eq ""));

if ($logfile)
{
	open (LOGFILE, ">$logfile") or die "Cannot write to logfile $logfile: $!";
	print LOGFILE "emchain $our_version build log.\n$date\nArchitecture: $arch\n\n";
}

if (!$archtable{$arch}){
	$msg = (qq;Error: dpkg-cross does not currently support "$arch".;);
	print LOGFILE $msg if ($logfile);
	die "\n$msg\n";
}
my $target_gnu_type = $archtable{$arch};
$msg = "Refreshing apt-cross $suite cache for $arch.\n";
print LOGFILE $msg if ($logfile);
print $msg if ($verbose >= 2);
`apt-cross -a $arch -l`;

# prepare variable names.
$skip_gcc = 0;
$skip_binutils = 0;
$skip_libc = 0;
$host = &host_arch("binutils");
$gcc_latest =  &find_latest_gcc("gcc", $arch, $suite);
if ($gcc_latest eq "0")
{
	print (qq/Warning: $progname cannot find a native gcc package for $arch.\n/);
	print (qq/$progname will try to build a toolchain from scratch using cache values for $host.\n/);
	$msg = "Refreshing apt-cross $suite cache for $host.\n";
	print LOGFILE $msg if ($logfile);
	print $msg if ($verbose >= 2);
	`apt-cross -a $host -l`;
	$gcc_latest = &find_latest_gcc("gcc", $host, $suite);
	$binutils_vers = &cache_version("binutils", $host);
	$libc_latest = &find_latest_libc("libc", $host, $suite);
	$gcc_vers = "gcc-" . $gcc_latest;
	$gcc_pkg_v = &cache_version($gcc_vers, $host);
	$libc_vers = "libc" . $libc_latest;
	$libc_pkg_v = &cache_version($libc_vers, $host);
	$binutils_dir = &split_debian($binutils_vers);
	$gcc_dir = &parse_gcc_dir($host);
}
else
{
	$binutils_vers = &cache_version("binutils", $arch);
	$libc_latest = &find_latest_libc("libc", $arch, $suite);
	$gcc_vers = "gcc-" . $gcc_latest;
	$gcc_pkg_v = &cache_version($gcc_vers, $arch);
	$libc_vers = "libc" . $libc_latest;
	$libc_pkg_v = &cache_version($libc_vers, $arch);
	$binutils_dir = &split_debian($binutils_vers);
	$gcc_dir = &parse_gcc_dir($arch);
}
$dev = $libc_vers . "-dev";
$binutils_deb = "binutils-${target_gnu_type}_${binutils_vers}_${host}.deb";
@glibc_list=qw//;
@glibc_list2=qw//;
@glibc_list3=qw//;
$glibc_count = 0;
&check_source_version("binutils");
&check_source_version("linux-kernel-headers");
&check_source_version($libc_vers);
&check_source_version($gcc_vers);

$log = "\nThis is emchain $our_version.\n Toolchain builder for cross compiling.\n\n";
print $log if ($verbose >= 3);
$log = "Using : ";
$log .= "$gcc_vers ($gcc_pkg_v) in $gcc_dir/, ";
$log .= "binutils $binutils_vers in binutils-$binutils_dir/ and ";
$log .= "$libc_vers $libc_pkg_v\n";
print LOGFILE $log if ($logfile);
print $log if ($verbose >= 2);
if ($logfile && $verbose >= 3)
{
	print "NOTE: A logfile is in use, some terminal output is being redirected.\n";
}

# apt-get source will skip existing downloads if up to date.
$log = "Running apt-get source . . . \n";
print LOGFILE $log if ($logfile);
print $log if ($verbose >= 2);
if ($verbose < 2) { $aptcmd = "apt-get -q"; }
else { $aptcmd = "apt-get"; }
my $gcc_src = `apt-cache -o Apt::Architecture=$arch -c $dpkg_cross_dir/apt.conf-$suite showsrc $gcc_vers 2>/dev/null`;
$gcc_src =~ /Version: (.*)\n/g;
$gcc_src = $1;
$log = "$aptcmd source $gcc_vers=$gcc_src binutils=$binutils_vers glibc=$libc_pkg_v";
print "$log\n" if ($verbose >= 2);
system "$log 2>/dev/null";

if (! -f $binutils_deb) {
	chdir ("binutils-$binutils_dir");
	print "Building $binutils_deb in binutils-$binutils_dir\n" if ($verbose >= 2);
	$log = `TARGET=$target_gnu_type fakeroot debian/rules binary-cross`;
	print LOGFILE $log if ($logfile);
	chdir ("../");
	$log = `sudo dpkg -i $binutils_deb`;
	print LOGFILE $log if ($logfile);
	print $log if ($verbose >= 3);
}
# apt-cross also skips download/conversion of existing debs
# but does not skip installation. :-(
if ($verbose < 2) { $aptcmd = "apt-cross -i"; }
else { $aptcmd = "apt-cross -v -i"; }
# check with dpkg-cross --status for Status: install ok installed\n
$cmdline = &check_cross("libdb1-compat");
$cmdline .= &check_cross("$libc_vers");
$cmdline .= &check_cross("$dev");
$cmdline .= &check_cross("linux-kernel-headers");
# only run cmdline if there is something for apt-cross to do
if ($cmdline ne "")
{
	$log = `sudo $aptcmd -a $arch $cmdline`;
	print LOGFILE $log if ($logfile);
	print $log if ($verbose >= 3);
}
$log = "";

push @glibc_list, "gcc-${gcc_latest}-${target_gnu_type}-base_${gcc_pkg_v}_${host}.deb";
push @glibc_list, "libstdc++${libc_latest}-${arch}-cross_${gcc_pkg_v}_all.deb";
push @glibc_list, "gcc-${gcc_latest}-${target_gnu_type}_${gcc_pkg_v}_${host}.deb";
push @glibc_list, "cpp-${gcc_latest}-${target_gnu_type}_${gcc_pkg_v}_${host}.deb";
push @glibc_list, "libgcc1-${arch}-cross_${gcc_pkg_v}_all.deb";
$glibc_count += scalar @glibc_list;

foreach $check (@glibc_list) {
	$log = "checking for $check\n";
	print $log if ($verbose >= 3);
	print LOGFILE $log if ($logfile);
	if (-f $check) {
		$skip_gcc++;	
	}
	else {
		$log = "missing $check\n"; 
		print $log if ($verbose >= 2);
		print LOGFILE $log if ($logfile);
	}
}

push @glibc_list2, "g++-${gcc_latest}-${target_gnu_type}_${gcc_pkg_v}_${host}.deb";
push @glibc_list2, 
	"libstdc++${libc_latest}-${gcc_latest}-dev-${arch}-cross_${gcc_pkg_v}_all.deb";

$glibc_count += scalar @glibc_list2;
foreach $check (@glibc_list2) {
	$log = "checking for $check\n";
	print $log if ($verbose >= 3);
	print LOGFILE $log if ($logfile);
	if (-f $check) {
		$skip_gcc++;	
	}
	else {
		$log = "missing $check\n";
		print $log if ($verbose >= 2);
		print LOGFILE $log if ($logfile);
	}
}

push @glibc_list3, 
	"libstdc++${libc_latest}-${gcc_latest}-pic-${arch}-cross_${gcc_pkg_v}_all.deb";
push @glibc_list3, 
	"libstdc++${libc_latest}-${gcc_latest}-dbg-${arch}-cross_${gcc_pkg_v}_all.deb";

$glibc_count += scalar @glibc_list3;
foreach $check (@glibc_list3) {
	$log = "checking for $check\n";
	print $log if ($verbose >= 3);
	print LOGFILE $log if ($logfile);
	if (-f $check) {
		$skip_gcc++;
	}
	else {
		$log = "missing $check\n";
		print $log if ($verbose >= 2);
		print LOGFILE $log if ($logfile);
	}
}

if ($skip_gcc < $glibc_count) {
	chdir ($gcc_dir);
	$environ = "GCC_TARGET=$arch DEB_CROSS_INDEPENDENT=yes";
	$log = `$environ debian/rules control`;
	# write log incrementally so that any fatal errors don't lose content
	print LOGFILE $log if ($logfile);
	$log = "Building gcc in $gcc_dir . . . (this will take a while)\n";
	print LOGFILE $log if ($logfile);
	print $log if ($verbose >= 2);
	# ToDo: Log to a separate file using tee (ala debuild) to save memory?
	# open BUILD, "| tee ../$build" open STDOUT, ">&BUILD" open STDERR, ">&BUILD"
	if ($verbose >= 3) {
		$result = system "$environ dpkg-buildpackage -b -uc -us -rfakeroot";
	}
	else {
		$result = system "$environ dpkg-buildpackage -b -uc -us -rfakeroot > /dev/null";
	}
	if ($result != 0) {
		die "$progname: Build failed. See the dpkg build log for more info.\n";
	}
	chdir ("../");
	foreach $result (@glibc_list)
	{
		$log = "$progname: Failed to create $result package\n";
		print LOGFILE $log if (! -r $result);
		die ($log) if (! -r $result);
	}
	foreach $result (@glibc_list2)
	{
		$log = "$progname: Failed to create $result package\n";
		print LOGFILE $log if (! -r $result);
		die ($log) if (! -r $result);
	}
	foreach $result (@glibc_list3)
	{
		$log = "$progname: Failed to create $result package\n";
		print LOGFILE $log if (! -r $result);
		die ($log) if (! -r $result);
	}
	$log = "$gcc_vers ($gcc_pkg_v) built successfully.\n";
	print $log if ($verbose >= 2);
	print LOGFILE $log if ($logfile);
	my $install = (join (' ', @glibc_list));
	$log = `sudo dpkg -i $install`;
	print $log if ($verbose >= 3);
	print LOGFILE $log if ($logfile);
	$install = (join (' ', @glibc_list2));
	$log = `sudo dpkg -i $install`;
	print $log if ($verbose >= 3);
	print LOGFILE $log if ($logfile);
	$install = (join (' ', @glibc_list3));
	$log = `sudo dpkg -i $install`;
	print $log if ($verbose >= 3);
	print LOGFILE $log if ($logfile);
}

if ($logfile) {
	close (LOGFILE);
}

# end : show confirmation.
print "\nSuccess! \nInstalled toolchains:\n\n";
my $all_vers = $gcc_vers;
$all_vers =~ s/gcc/'\?\?\?'/;
$all_vers .= "* 'libstdc*' 'libc6*' 'libgcc1*' 'linux-kernel-headers*'";
my $success = `dpkg -l 'binutils*' $all_vers`;
my @s = split (/\n/, $success);
foreach my $s (@s)
{
	if ($s =~ /^ii +(.*)/)
	{
		print "$1\n";
	}
}
print "\n";
exit 0;

# subroutines.
sub check_cross()
{
	# needs a versioned check!
	$log = "checking dpkg-cross status for $_[0] on $arch . . . ";
	$status = `apt-cross -a $arch -s $_[0]`;
	if ((!$status =~ /Status: install ok installed\n/g) || (!$status))
	{
		$log .= "\n$_[0] cross-build not installed yet. Installing ...\n";
		print $log if ($verbose >= 2);
		print LOGFILE $log if ($logfile);
		return "$_[0] ";
	}
	else {
		$log .= "installed ok.\n$status";
	}
	print $log if ($verbose >= 2);
	print LOGFILE $log if ($logfile);
	# still return the filename for now. Need to use dpkg-cross -u in apt-cross.
	return "";
#	return "$_[0]";
}

sub split_debian()
{
	return "" if (!$_[0]);
	$_[0] =~ /([0-9\.a-z]*)-[0-9].*/;
	return $1;
}

sub cache_version()
{
	my $result = `apt-cache -o Apt::Architecture=$_[1] -c $dpkg_cross_dir/apt.conf-$suite show $_[0] 2>/dev/null`;
	$result =~ /Version: (.*)\n/g;
	return $1;
}

sub check_source_version()
{
	my $main_src = `apt-cache -o Apt::Architecture=$arch showsrc $_[0] 2>/dev/null`;
	return if (!$main_src);
	$main_src =~ /Version: (.*)\n/g;
	$main_src = $1;

	my $cross_src = `apt-cache -o Apt::Architecture=$arch -c $dpkg_cross_dir/apt.conf-$suite showsrc $_[0] 2>/dev/null`;
	$cross_src =~ /Version: (.*)\n/g;
	$cross_src = $1;
	
	if ($main_src ne $cross_src) {
		my $msg = "\n$progname: Error. Mismatch in source versions\n";
		$msg .= "$arch does not appear to have built version $main_src of $_[0] successfully yet. Therefore it is unlikely that $progname will be able to build a usable cross-compiler using the current upstream source of $_[0]. Only version $cross_src is available on $arch and $progname is unable to proceed.\n";
		$msg .= "Please run $progname again when the $arch port has updated.\n";
		$msg = `echo "$msg" | fold -s`;
		print LOGFILE $msg if ($logfile);
		close (LOGFILE) if ($logfile);
		die ($msg);
	}
}

sub parse_gcc_dir()
{
	my $result = `apt-cache -o Apt::Architecture=$_[0] -c $dpkg_cross_dir/apt.conf-$suite show $gcc_vers 2>/dev/null`;
	return "" if (!$result);
	my $r = $result;
	$r =~ /Version: (.*)\n/g;
	if (! -d $1) {
		$r = $result;
		$r =~ /Source: $gcc_vers \((.*)\)\n/;
		my $t = &split_debian($1);
		if (-d "$gcc_vers-$t") {
			return "$gcc_vers-$t"
		}
	}
	return "$gcc_vers-$1";
}
