#!/usr/bin/perl -w
#
# debian-builder  -  Rebuild some or all of Debian from source.
# 
#                    This code is intentionally simple.
#
# Homepage:
#   http://www.steve.org.uk/Software/debian-builder/
#
# Author:
#  Steve Kemp <steve@steve.org.uk>
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
#  Steve Kemp
#  ---
#  http://www.steve.org.uk/
#
#

use strict;
use File::Copy;
use Getopt::Long;

#
#  Version number
#
my $RELEASE_VERSION           = "1.3";


#
# Command line flags.
#

my $VERBOSE                   = 0;
my $VERSION                   = 0;
my $HELP                      = 0;
my $DEBUG_DUMP_PACKAGES       = 0;
my $DEBUG_DUMP_BUILDDEPS      = 0;
my $DEBUG_DUMP_SOURCE_PACKAGE = 0;
my $DEBUG_DEP_HANDLING        = 0;

#
# Configuration files from the config file, or the command line.
#
our %CONFIG;





#
#  Start of the code
#


#
#  Parse global configuration file.
#
parseOptions();


#
#  Allow command line to override.
#
parseArguments();


#
#  Handle some of the simple cases first.
#
if ( $HELP )
{
    &showHelp();
    exit;
}
if ( $VERSION )
{
    &showVersion();
    exit;
}



#
#  now we handle some of the debug options.
#
if ( $DEBUG_DUMP_PACKAGES )
{
    my %INSTALLED = getInstalledPackages();

    foreach my $package (sort keys %INSTALLED)
    {
	print $package . "\n";
    }
    exit;
}


#
#  We assume that we are given the name of a package to build
#
my $package = undef;

while( $package = shift )
{

    buildPackage( $package );
}

exit;







#
#  Do all the work of building the given package.
#
#  Return '1' on success, '0' on failure of any kind.
#  
#
sub buildPackage 
{

    my $package = shift;

    
    $VERBOSE &&	print "Starting build of $package\n";


    #
    #  See if we need a different source package than the name
    # we were given.
    #
    my $source = getSourcePackage( $package );

    #
    #  An early debugging hook
    #
    if ( $DEBUG_DUMP_SOURCE_PACKAGE )
    {
		print "Package: $package\n";
		print "Source : $source\n";
		exit;
    }

    $package = $source;



    #
    #  Get the package source, and clean it a little.
    #
    my $dir = $CONFIG{'build_dir'};
    system( "cd $dir ; \
             apt-get source $package > /dev/null; \
             rm $package*.dsc; \
             rm $package*.diff.gz" );

    #
    #  Change to the directory which contains the unpacked code.
    #
    opendir( DIR, $dir );
    my $work = "";
    foreach my $ent ( readdir( DIR ) )
    {
	next if ( $ent =~ /^\./ );
	if ( ( -d $dir . "/" . $ent ) &&  ( $ent =~ /$package/ ) )
	{
	    $work = $dir . "/" . $ent;
	}
    }
    closedir( DIR );


    #
    #  Dump the build directory.
    #
    $VERBOSE && print "Source directory is $work\n";


    #
    #  Attempt to find the required packages to compile this beast.
    #
    my @deps = getBuildDeps( $work . "/debian/control" );

    #
    #  See which packages are already installed.
    # 
    my %INSTALLED = getInstalledPackages();

    #
    #  A hash of the packages we will need to add.
    my @ADDED = ();


    # 
    # Now that we know which packages we need to install
    # install them.
    #
    foreach my $dep ( @deps )
    {
	$VERBOSE && print "Build-Dep: $dep";

	if ( ! $INSTALLED{ $dep } )
	{
	    push @ADDED, $dep;
	    $VERBOSE && print " needs to be installed.\n";
	}
	else
	{
	    $VERBOSE && print " already present.\n";
	}
    }
    

    #
    #  If we're just running to see the build dependencies
    # then show them and exit.
    #
    if ( $DEBUG_DUMP_BUILDDEPS )
    {
	print "Build dependences for $package:\n";
	foreach my $dep ( @deps )
	{
	    print "\t$dep";
	    if ( $INSTALLED{$dep} )
	    {
		print " already installed.";
	    }
	    print "\n";
	}
	exit;
    }


    #
    #  Actually install the missing build-dependency.
    #
    foreach my $missing ( @ADDED )
    {
	$VERBOSE && print "Adding the missing build dependency: $missing\n";
	installPackage( $missing );
    }


    #
    # Patch up the changelog file to include the required suffix.
    #
    my $suffix = $CONFIG{'package_suffix'} || "ssp";

    open( CHANGE, "<$work/debian/changelog" );
    my @LINES = <CHANGE>;
    close( CHANGE );

    if ($LINES[0] =~ /$package \(([^\)]+)\) (.*)/ )
    {
	my $ver = $1;
	if ( $ver =~ /$suffix/ )
	{
	    # Contains the 'ssp' marker already...
	    $VERBOSE && print "Package already has the suffix '$suffix': " . $LINES[0] . "\n";
	}
	else
	{
	    $LINES[ 0 ] = $package . " (" . $1 . $suffix . ") " . $2;
	    $VERBOSE && print "Package has new version: " . $LINES[0] . "\n";
	}
    }

    #
    # Store the update changelog.
    #
    open( CHANGE, ">$work/debian/changelog" );
    foreach my $line ( @LINES )
    {
        print CHANGE $line;
    }
    close(CHANGE);

    my $log_dir = $CONFIG{'log_dir'};

    #
    # Build the .deb
    #
    system( "cd $work; \
             debuild  | tee $log_dir/$package.log" );


    #
    # Move the deb, then nuke the build directory.
    #
    # here is where we tell that something produced a .deb or
    # not.
    #
    my $result = 0;


    #
    # Directory where we move built packages to
    #
    my $bin_dir = $CONFIG{'binary_dir'};

    system( "rm -rf $work" );
    opendir( DIR, $dir );
    foreach my $ent ( readdir( DIR ) )
    {
	next if ( $ent =~ /^\./ );
	next if ( $ent =~ /asc$/ );  # Skip .asc files.
	if ( $ent =~ /\.deb$/ ) 
	{
	    print "Moving deb : $ent - $bin_dir\n";
	    File::Copy::move( $dir . "/" . $ent, $bin_dir );
	    $result ++;
	}
	elsif ( ( $ent =~ /\.changes$/ )  ||
		( $ent =~ /\.dsc$/ ) ||
		( $ent =~ /\.diff.gz$/ ) ||
		( $ent =~ /\.build$/ ) ||
		( $ent =~ /\.tar.gz$/ ) )
	{
	    print "Moving other file : $ent - $bin_dir\n";
	    File::Copy::move( $dir . "/" . $ent, $bin_dir );
	    $result ++;
	}
	else
	{
	    print "Removing file .. $ent\n";
	    unlink( $dir . "/" . $ent );
	}
    }
    closedir( DIR );

    
    #
    #  Uninstall all the build dependency packages we installed.
    #
    foreach my $dep ( @ADDED )
    {
	print "Removing the added package $dep\n";
	removePackage( $dep );
    }

    return( $result );
}



#
#  Read the global configuration file, then override with the users
# file if present
#
sub parseOptions
{
    my $global = "/etc/debian-builder/debian-builder.conf";

    if ( -e $global )
    {
	$VERBOSE && print "Reading $global\n";
	&parseConfigurationFile( $global );
    }

    if ( -e $ENV{"HOME"} . "/.debian-builderrc" )
    {
	$VERBOSE && print "Reading ~/.debian-builderrc\n";
	&parseConfigurationFile( $ENV{"HOME"} . "/.debian-builderrc" ) ;
    }
}


#
#  Parse the named configuration file.
#
#  Set the global '%CONFIG' hash with the values we read.
#
sub parseConfigurationFile
{
  my $FILE = shift;


  if ( ( ! -e $FILE )  && ( $VERBOSE ) )
  {
    print "Configuration file '$FILE' missing.\n";
    return;
  }

  open( FILY, "<$FILE" ) or die "Cannot open file: $FILE - $!";

  my $line       = ""; 
  my $fieldCount = -1;
  my $lineCount  = 0;

  while (defined($line = <FILY>) ) 
  {
    chomp $line;
    if ($line =~ s/\\$//) 
    {
      $line .= <FILY>;
      redo unless eof(FILY);
    }
      
    # Skip lines beginning with comments
    next if ( $line =~ /^([ \t]*)\#/ );

    # Skip blank lines
    next if ( length( $line ) < 1 );

    # Strip trailing comments.
    if ( $line =~ /(.*)\#(.*)/ )
    {
      $line = $1;
    }

    # Find variable settings
    if ( $line =~ /([^=]+)=(.+)/ )
    {
      my $key = $1;
      my $val = $2;

      # Strip leading and trailing whitespace.
      $key =~ s/^\s+//;
      $key =~ s/\s+$//;
      $val =~ s/^\s+//;
      $val =~ s/\s+$//;
	    
      # Store value.
      $CONFIG{ $key } = $val;

      if ( $VERBOSE > 2 )
      {
	  print "Set: '$key' -> '$val'\n";
      }
    }
  }

  close( FILY );
}


#
#  Parse command line flags.
#
sub parseArguments
{
    GetOptions( "verbose", \$VERBOSE,
		"debug-dump-packages", \$DEBUG_DUMP_PACKAGES,
		"debug-dump-build-deps", \$DEBUG_DUMP_BUILDDEPS,
		"debug-dump-source-package", \$DEBUG_DUMP_SOURCE_PACKAGE,
		"debug-dependency-handling", \$DEBUG_DEP_HANDLING,
		"help", \$HELP,
		"version", \$VERSION,
		);
}


#
#  Return the source package required to build package 'foo'
#
sub getSourcePackage 
{
    my $package = shift;
    my $available = "/var/lib/dpkg/available";

    my $found  = 0;
    my $source = $package;

    open( AVAIL, "<" . $available );
    while( my $line = <AVAIL> )
    {
	if ( $line =~ /^Package: (.*)/ )
	{
	    my $pkg = $1;
	    if ( $pkg eq $package )
	    {
		$found =1;
	    }
	}
	
	if ( $line =~ /Source: (.*)/ )
	{
	    if ( $found  )
	    {
		$source = $1;
	    }
	}
	if ( length( $line  ) < 2)
	{
	    $found = 0;
	}
    
    } 
    close( AVAIL );

    if ( $source =~ /(.*) (.*)/ )
    {
	$source = $1;
    }
    return( $source );
}



#
#  Return an array of packages which must be installed to 
# build the given package
#
sub getBuildDeps 
{
    my $file = shift;
    my @NEEDED  = ();

    if ( $VERBOSE > 2 )
    {
	print "Calculating build-deps from $file\n";
    }

    open( CONTROL, "<$file" );
    my @lines = <CONTROL>;
    close( CONTROL );

    foreach my $line ( @lines )
    {
	if ( $line =~ /^Build-Depends:\s*(.*)\s*$/i )
	{
	    my $deps = $1;
	    
	    my @foo = split( /,/, $deps );
	    foreach my $f ( @foo )
	    {
		if ( $f =~ /(.*)\|(.*)/ )
		{
		    my $left  = $1;
		    my $right = $2;
		    if ( ( $left =~ /[0-9]/ ) and ( ! $right =~ /[0-9]/ )  )
		    {
			$f = $right;
		    }
		    else
		    {
			$f = $left;
		    }
		}

		if ( $f =~ /(.*) \(.*\)/ )
		{
		    $f = $1;
		}

		$f =~ s/^\s+//;
		$f =~ s/\s+$//;

		#
		# Handle a case like Perl "-Depends: netbase [!hurd-i386]";
		#
		if ( $f =~ /(.*) (.*)/ )
		{
		    $f = $1;
		}
		push @NEEDED, $f;

		if ( $f =~ /(.*)-dev$/ )
		{
		    push @NEEDED, $1;
		}
	    }
	}
    } 
    return( @NEEDED );
   
}


#
# Return a hash of all the currently installed packages.
#
sub getInstalledPackages
{
    my $file = "/var/lib/dpkg/status";

    open( STATUS, "<$file" ) or die "Cannot open $file - $!";

    my %INSTALLED;

    my $package = "";
    my $status  = "";

    foreach my $line ( <STATUS> ) 
    {
	if ( $line =~ /^Package: (.*)/ )
	{
	    $package = $1;
	}
	if ( $line =~ /^Status: (.*)/ )
	{
	    $status = $1;
	}

	if ( length( $package ) && length( $status ) )
	{
	    if ( $status =~ / installed/ ) 
	    {
		$INSTALLED{$package} = 1;
	    }

	    $package = "";
	    $status  = "";
	}
    }
    close( STATUS );

    return( %INSTALLED );
}



#
#  Install a package
#
sub installPackage 
{
    my $package = shift;

    $VERBOSE && print "Installing $package\n";

    system( "apt-get -y install $package" );
}


#
#  Remove a package
#
sub removePackage
{
    my $package = shift;

    $VERBOSE && print "Purging $package\n";

    system( "dpkg --purge $package" );
}


#
#  Give some help output
#
sub showHelp()
{
    &showVersion();

    print <<E_O_HELP;

  Rebuilds a Debian package from it's source code.

Usage:
    debian-builder [options...] packageName


  General options:

   --help    Show this help.
   --version  Show the version number.
   --verbose  Show more output

  Debug options:

   --debug-dump-packages
       Dump a list of all packages installed upon this host
   --debug-dump-build-deps
       Show the required build dependencies for the given package
   --debug-dependency-handling
       Show which packages must be installed to build the package.
E_O_HELP
}


#
#  Show the version number.
#
sub showVersion()
{
    print <<E_O_VERSION;
debian-builder -  v$RELEASE_VERSION by Steve Kemp
                  http://www.steve.org.uk/Software/debian-builder/
E_O_VERSION
}


