#!/usr/bin/perl
#
# sbuild: build packages, obeying source dependencies
# Copyright © 1998-2000 Roman Hodek <Roman.Hodek@informatik.uni-erlangen.de>
# Copyright © 2005      Ryan Murray <rmurray@debian.org>
# Copyright © 2005-2009 Roger Leigh <rleigh@debian.org
# Copyright © 2008      Timothy G Abbott <tabbott@mit.edu>
# Copyright © 2008      Simon McVittie <smcv@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, see
# <http://www.gnu.org/licenses/>.
#
#######################################################################

package main;

use strict;
use warnings;

use POSIX;
use Data::Dumper;
use Sbuild qw(isin check_group_membership);
use Sbuild::Conf qw();
use Sbuild::Log qw(open_log close_log);
use Sbuild::Sysconfig qw(%programs);
use Sbuild::Options;
use Sbuild::Build;

sub main ();
sub write_jobs_file ();
sub append_to_FINISHED ($);
sub status_trigger ($$);
sub shutdown ($);
sub dump_main_state ();

my $conf = Sbuild::Conf::new();
exit 1 if !defined($conf);
my $options = Sbuild::Options->new($conf, "sbuild", "1");
exit 1 if !defined($options);
check_group_membership();

umask(022);

# Job state
my %jobs = ();
my $current_job = undef;

main();

sub main () {
    my $dist = $conf->get('DISTRIBUTION');
    if (!defined($dist) || !$dist) {
	print STDERR "No distribution defined\n";
	exit(1);
    }

    print "Selected distribution " . $conf->get('DISTRIBUTION') . "\n"
	if $conf->get('DEBUG');
    print "Selected chroot " . $conf->get('CHROOT') . "\n"
	if $conf->get('DEBUG') and defined $conf->get('CHROOT');
    print "Selected architecture " . $conf->get('ARCH') . "\n"
	if $conf->get('DEBUG' && defined($conf->get('ARCH')));

    open_log($conf);

    $SIG{'INT'} = \&main::shutdown;
    $SIG{'TERM'} = \&main::shutdown;
    $SIG{'ALRM'} = \&main::shutdown;
    $SIG{'PIPE'} = \&main::shutdown;

    # If no arguments are supplied, assume we want to process the current dir.
    push @ARGV, '.' unless (@ARGV);

    # Create jobs
    foreach my $job (@ARGV) {
	$jobs{$job} = Sbuild::Build->new($job, $conf);
	$jobs{$job}->set('Pkg Status Trigger', \&status_trigger)
    }
    write_jobs_file(); # Will now update on trigger.

    # Run each job.  Potential for parallelising this step.
    foreach (keys %jobs) {
	my $jobname = $_;

	my $job = $jobs{$jobname};
	$current_job = $jobname;

	# Do the build
	$job->run();

	dump_main_state() if $conf->get('DEBUG');
    }

    close_log($conf);
    unlink($conf->get('JOB_FILE'))
	if $conf->get('BATCH_MODE');
    unlink("SBUILD-FINISHED") if $conf->get('BATCH_MODE');

    # Until buildd parses status info from sbuild output, skipped must
    # be treated as a failure.
    if (defined($current_job) && defined($jobs{$current_job})) {
	if ($jobs{$current_job}->get_status() eq "successful" ||
	    ($conf->get('SBUILD_MODE') ne "buildd" &&
	     $jobs{$current_job}->get_status() eq "skipped")) {
	    exit 0;
	} elsif ($jobs{$current_job}->get_status() eq "attempted") {
	    exit 2;
	} elsif ($jobs{$current_job}->get_status() eq "given-back") {
	    #Probably needs a give back:
	    exit 3;
	}
	# Unknown status - probably needs a give back, but needs to be
	# reported to the admin as failure:
	exit 1;
    }
}

# only called from main loop, but depends on job state.
sub write_jobs_file () {
    if ($conf->get('BATCH_MODE')) {

	my $file = $conf->get('JOB_FILE');
	local( *F );

	return if !open( F, ">$file" );
	foreach (keys %jobs) {
	    my $job = $jobs{$_};

	    print F $job->get('Package_OVersion') . ": " .
		$job->get_status() . "\n";
	}
	close( F );
    }
}

sub append_to_FINISHED ($) {
    my $job = shift;

    local( *F );

    if ($conf->get('BATCH_MODE')) {
	open(F, ">>SBUILD-FINISHED");
	print F $job->get('Package_OVersion');
	close(F);
    }
}

sub status_trigger ($$) {
    my $build = shift;
    my $status = shift;

    write_jobs_file();

    # Rewrite status if we need to give back or mark attempted
    # following failure.  Note that this must follow the above
    # function calls because set_status will recursively trigger.
    if ($status eq "failed" &&
	isin($build->get('Pkg Fail Stage'),
	     qw(fetch-src install-core install-essential install-deps
		unpack check-unpacked-version check-space hack-binNMU
		install-deps-env apt-get-clean apt-get-update
		apt-get-upgrade apt-get-distupgrade))) {
	$build->set_status('given-back');
    } elsif ($status eq "failed" &&
	     isin ($build->get('Pkg Fail Stage'),
		   qw(build arch-check))) {
	$build->set_status('attempted');
    }
}

sub shutdown ($) {
    my $job = undef;
    my $signame = shift;
    my(@npkgs,@pkgs);
    local( *F );

    $SIG{'INT'} = 'IGNORE';
    $SIG{'QUIT'} = 'IGNORE';
    $SIG{'TERM'} = 'IGNORE';
    $SIG{'ALRM'} = 'IGNORE';
    $SIG{'PIPE'} = 'IGNORE';

    $job = $jobs{$current_job} if (defined($current_job) &&
				   defined($jobs{$current_job}));

    # TODO: Use main log
    if (defined($job)) {
	$job->log("sbuild received SIG$signame -- shutting down\n");
    }

    if (defined($job)) {
	my $session = $job->get('Session');
	my $resolver = $job->get('Dependency Resolver');

	# Kill currently running command (if any)
	if ($job->get('Sub PID')) {
	    print "Killing " . $job->get('Sub Task') .
		" subprocess " . $job->get('Sub PID') . "\n";
	    $job->get('Session')->run_command(
		{ COMMAND => ['perl', '-e',
			      "\"kill( \\\"TERM\\\", " .
			      $job->get('Sub PID') .
			      " )\""],
			      USER => 'root',
			      PRIORITY => 0,
			      DIR => '/' });
	}

	if (defined($session) && $conf->get('BATCH_MODE') &&
	    !$conf->get('SBUILD_MODE') eq "buildd") {
	    # next: say which packages should be uninstalled
	    if (defined ($session->get('Session Purged')) && $session->get('Session Purged') == 1) {
		print "Not removing build depends: cloned chroot in use\n";
	    } else {
		@pkgs = keys %{$resolver->get('Changes')->{'installed'}};
		if (@pkgs) {
		    if (open( F, ">>NEED-TO-UNINSTALL" )) {
			print F "@pkgs\n";
			close( F );
		    }
		    print "The following packages still need to be uninstalled ",
		    "(--purge):\n@pkgs\n";
		}
	    }
	}

	# Close logs and send mails
	if (defined($session) &&
	    $conf->get('PURGE_BUILD_DIRECTORY') eq "always" &&
	    defined($job->get('Chroot Build Dir'))) {
	    $job->log("Purging " . $job->get('Chroot Build Dir') . "\n");
	    my $bdir = $job->get('Session')->strip_chroot_path($job->get('Chroot Build Dir'));
	    $job->get('Session')->run_command(
		{ COMMAND => ['rm', '-rf', $bdir],
		  USER => 'root',
		  PRIORITY => 0,
		  DIR => '/' });
	}

	if (defined($resolver)) {
	    $resolver->cleanup();
	}

	if (defined($session)) {
	    $session->unlock_chroot();
	    $session->end_session();
	    $job->set('Session', undef);
	}

	$job->close_build_log();
	$job->set('binNMU Name', undef);
    }

    close_log($conf);

    # Restore the signal handler to let a self-kill result in the appropriate
    # exit code.
    $SIG{$signame} = 'DEFAULT';
    kill($signame, $$) or die("self-kill failed");
}

sub dump_main_state () {
    print STDERR Data::Dumper->Dump([$current_job,
				     \%jobs],
				    [qw($current_job
					%jobs)] );
}
