#! /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);
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 should_skip ($);
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);
$conf->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;

    # Create jobs
    foreach (@ARGV) {
	$jobs{$_} = Sbuild::Build->new($_, $conf);
	$jobs{$_}->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
	if (should_skip($job)) {
	    $job->set_status('skipped');
	} else {
	    $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');
    if (defined($current_job) && defined($jobs{$current_job})) {
	if ($conf->get('SBUILD_MODE') eq "user") {
	    exit ($jobs{$current_job}->get_status() ne "successful") ? 1 : 0;
	} elsif ($conf->get('SBUILD_MODE') eq "buildd") {
	    if ($jobs{$current_job}->get_status() eq "successful") {
		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;
	}
    }
    exit 0;
}

# 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 should_skip ($) {
    my $build = shift;

    if ($conf->get('BATCH_MODE')) {
	my $pkgv = $build->get('Package_OVersion');

	$pkgv = $build->fixup_pkgv($pkgv);
	$build->lock_file("SKIP", 0);
	goto unlock if !open( F, "SKIP" );
	my @pkgs = <F>;
	close( F );

	if (!open( F, ">SKIP" )) {
	    print "Can't open SKIP for writing: $!\n",
	    "Would write: @pkgs\nminus $pkgv\n";
	    goto unlock;
	}
	my $found = 0;
	foreach (@pkgs) {
	    if (/^\Q$pkgv\E$/) {
		++$found;
		print "$pkgv found in SKIP file -- skipping building it\n";
	    }
	    else {
		print F $_;
	    }
	}
	close( F );
      unlock:
	$build->unlock_file("SKIP");
	return $found;
	}
}

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-deps unpack check-unpacked-version
		check-space hack-binNMU install-deps-env
		apt-get-update))) {
	$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 $signame = shift;
    my(@npkgs,@pkgs);
    local( *F );

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

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

    if ($conf->get('BATCH_MODE') && !$conf->get('SBUILD_MODE') eq "buildd") {
	# Dump names of unfinished jobs to REDO
	foreach (keys %jobs) {
	    my $job = $jobs{$_};

	    push(@npkgs, $job->get('Package_OVersion'))
		if $job->get_status() ne "successful";
	}
	print "The following jobs were not finished: @npkgs\n";

	my $f = "REDO";
	if (-f "REDO.lock") {
	    # if lock file exists, write to a different file -- timing may
	    # be critical
	    $f = "REDO2";
	}
	if (open(F, "<$f")) {
	    @pkgs = <F>;
	    close(F);
	}
	if (open(F, ">>$f")) {
	    foreach (@npkgs) {
		next if grep( /^\Q$_\E\s/, @pkgs );
		print F "$_ " .
		    $jobs{$current_job}->get_conf('DISTRIBUTION');
		print F " " . $jobs{$current_job}->get_conf('BIN_NMU_VERSION')
		    . " " . $jobs{$current_job}->get_conf('BIN_NMU')
		    if (defined $jobs{$current_job}->get_conf('BIN_NMU_VERSION'));
		print F "\n";
	    }
	    close(F);
	}
	else {
	    print "Cannot open $f: $!\n";
	}
	open(F, ">SBUILD-REDO-DUMPED");
	close(F);
	print "SBUILD-REDO-DUMPED created\n";

	unlink("SBUILD-FINISHED");

	# next: say which packages should be uninstalled
	my $session = $jobs{$current_job}->get('Session');
	if (defined ($session->get('Session Purged')) && $session->get('Session Purged') == 1) {
	    print "Not removing build depends: cloned chroot in use\n";
	} else {
	    @pkgs = keys %{$jobs{$current_job}->get('Dependency 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";
	    }
	}
    }

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

    if (defined($current_job) && $jobs{$current_job}->get('Dependency Resolver')) {
	$jobs{$current_job}->get('Dependency Resolver')->remove_srcdep_lock_file();
    }

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

	$jobs{$current_job}->get('Session')->end_session();
	$jobs{$current_job}->set('Session', undef);

	$jobs{$current_job}->close_build_log();
	$jobs{$current_job}->set('binNMU Name', undef);
    }
    close_log($conf);
    unlink( $jobs{$current_job}->get('Jobs File') ) if $conf->get('BATCH_MODE');
    $? = 0; $! = 0;
    if ($conf->get('SBUILD_MODE') eq "user") {
	exit 1;
    }

    # 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)] );
}
