#! /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-2008 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 conf;

use Sbuild::Conf;


package main;

use strict;
use warnings;

use POSIX;
use File::Basename qw(basename dirname);
use IO::Handle;
use FileHandle;
use Sbuild qw(binNMU_version version_compare copy);
use Data::Dumper;
use File::Temp qw(tempdir);
use Sbuild::Chroot;
use Sbuild::Log qw(open_log close_log);
use Sbuild::Sysconfig qw($arch $version);
use Sbuild::Options;
use Sbuild::Build;

sub main ();
sub shutdown ($);
sub check_group_membership ();
sub dump_main_state ();

sub main () {
# Be verbose by default if on a tty
    if (-t STDIN && -t STDOUT && $Sbuild::Conf::verbose == 0) {
	$conf::verbose = 1;
    }

    my $options = Sbuild::Options::new();
    exit 1 if !defined($options);

    print "Selected distribution " . $options->get('Distribution') . "\n"
	if $conf::debug;
    print "Selected chroot " . $options->get('Chroot') . "\n"
	if $conf::debug and defined $options->get('Chroot');
    print "Selected architecture " . $options->get('User Arch') . "\n"
	if $conf::debug;

    $conf::mailto = $conf::mailto{$options->get('Distribution')}
    if $conf::mailto{$options->get('Distribution')};

# see debsign for priorities, we will follow the same order
    $options->set('Signing Options',
		  "-m\"".$conf::maintainer_name."\"")
	if defined $conf::maintainer_name;
    $options->set('Signing Options',
		  "-e\"".$conf::uploader_name."\"")
	if defined $conf::uploader_name;
    $options->set('Signing Options',
		  "-k\"".$conf::key_id."\"")
	if defined $conf::key_id;
    $conf::maintainer_name=$conf::uploader_name if defined $conf::uploader_name;
    $conf::maintainer_name=$conf::key_id if defined $conf::key_id;

    if (!defined($conf::maintainer_name) &&
	$options->get('binNMU')) {
	die "A maintainer name, uploader name or key ID must be specified in .sbuildrc,\nor use -m, -e or -k, when performing a binNMU\n";
    }

# variables for scripts:
    open_log($options->get('Distribution'));
    $SIG{'INT'} = \&shutdown;
    $SIG{'TERM'} = \&shutdown;
    $SIG{'ALRM'} = \&shutdown;
    $SIG{'PIPE'} = \&shutdown;

    my $dscfile;
    foreach $dscfile (@ARGV) {

	# TODO: Append to build list, to allow parallel builds.
	my $build = Sbuild::Build::new($dscfile, $options);
	$main::build_object = $build;

	$build->{'Pkg Start Time'} = time;

	$main::build_object->write_jobs_file("");
	$build->parse_manual_srcdeps(map { m,(?:.*/)?([^_/]+)[^/]*, } @ARGV);

	if ($build->{'Invalid Source'}) {
	    print PLOG "Invalid source: $dscfile\n";
	    print PLOG "Skipping " . $build->{'Package'} . " \n";
	    $build->{'Pkg Status'} = "skipped";
	    goto cleanup_skip;
	}

	{
	    my $tpkg = basename($build->{'Package_Version'});
	    # TODO: This should be 'Pkg Start Time', set in build().
	    my $date = strftime("%Y%m%d-%H%M",localtime);

	    if ($options->get('binNMU')) {
		$tpkg =~ /^([^_]+)_([^_]+)(.*)$/;
		$tpkg = $1 . "_" . binNMU_version($2,$options->get('binNMU Version'));
		$options->set('binNMU Name', $tpkg);
		$tpkg .= $3;
	    }

	    # TODO: Get package name from build object
	    next if !$build->open_build_log($tpkg);
	}

	$build->{'Pkg Status'} = "failed"; # assume for now
	$main::current_job = $build->{'Package_Version'};
	$build->{'Additional Deps'} = [];
	$main::build_object->write_jobs_file("currently building");
	if ($build->should_skip()) {
	    $build->{'Pkg Status'} = "skipped";
	    goto cleanup_close;
	}

	my $session = Sbuild::Chroot::new($options->get('Distribution'),
					  $options->get('Chroot'),
					  $options->get('User Arch'));
	$build->{'Session'} = $session;

	if (!$session->begin_session()) {
	    print PLOG "Error creating schroot session: skipping " .
		$build->{'Package'} . "\n";
	    $build->{'Pkg Status'} = "skipped";
	    goto cleanup_close;
	}
	$build->{'Chroot Dir'} = $session->{'Location'};
	$build->{'Chroot Build Dir'} =
	    tempdir("$Sbuild::Conf::username-" . 
		    $build->{'Package_SVersion'} .
		    "-$arch-XXXXXX",
		    DIR => $session->{'Build Location'});
	# TODO: Don't hack the build location in; add a means to customise
	# the chroot directly.
	$session->{'Build Location'} = $build->{'Chroot Build Dir'};

	$build->{'Arch'} = $build->chroot_arch();

	# Update APT cache.
	if ($conf::apt_update) {
	    if (!open(PIPE, $session->get_apt_command("$conf::apt_get", "-q update", "root", 1, '/') . " 2>&1 |")) {
		print PLOG "Can't open pipe to apt-get: $!\n";
		return 0;
	    }
	    while(<PIPE>) {
		print PLOG $_;
	    }
	    close(PIPE);
	    if ($?) {
		print PLOG "apt-get update failed\n" ;
		$build->{'Pkg Status'} = "skipped";
		goto cleanup_close;
	    }
	}

	$build->{'Pkg Fail Stage'} = "fetch-src";
	if (!$build->fetch_source_files()) {
	    goto cleanup_close;
	}

	$build->{'Pkg Fail Stage'} = "install-deps";
	if (!$build->install_deps()) {
	    print PLOG "Source-dependencies not satisfied; skipping " .
		$build->{'Package'} . "\n";
	    goto cleanup_packages;
	}

	$build->{'Pkg Status'} = "successful"
	    # TODO: Don't pass version info
	    if $build->build();
	$main::build_object->write_jobs_file($build->{'Pkg Status'});
	$build->append_to_FINISHED();

      cleanup_packages:
	if (defined ($session->{'Session Purged'}) &&
	    $session->{'Session Purged'} == 1) {
	    print PLOG "Not removing build depends: cloned chroot in use\n";
	} else {
	    $build->uninstall_deps();
	}
	$build->remove_srcdep_lock_file();
      cleanup_close:
	$build->analyze_fail_stage();
	$main::build_object->write_jobs_file($build->{'Pkg Status'});

	$session->end_session();
	$session = undef;
	$build->{'Session'} = undef;

	$build->close_build_log();

      cleanup_skip:
	undef $build->{'binNMU Name'};
	$main::current_job = "";
	if ( $options->get('Batch Mode') and (-f "$conf::HOME/EXIT-DAEMON-PLEASE") ) {
	    main::shutdown("NONE (flag file exit)");
	}
	dump_main_state() if $conf::debug;
	$main::build_object->write_jobs_file("");
    }

    close_log();
    unlink( $main::build_object->{'Jobs File'} )
	if $options->get('Batch Mode');
    unlink( "SBUILD-FINISHED" ) if $options->get('Batch Mode');
    if ($conf::sbuild_mode eq "user" && defined($main::build_object)) {
	    exit ($main::build_object->{'Pkg Status'} ne "successful") ? 1 : 0;
    }
    exit 0;
}

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

    $SIG{'INT'} = 'IGNORE';
    $SIG{'QUIT'} = 'IGNORE';
    $SIG{'TERM'} = 'IGNORE';
    $SIG{'ALRM'} = 'IGNORE';
    $SIG{'PIPE'} = 'IGNORE';
    print PLOG "sbuild received SIG$signame -- shutting down\n";

    goto not_ni_shutdown if !$main::build_object->get_option('Batch Mode');

    # most important: dump out names of unfinished jobs to REDO
    foreach $job (@ARGV) {
	my $job2 = $job;
	$job2 = $main::build_object->fixup_pkgv($job2);
	push( @npkgs, $job2 )
	    if !$main::job_state{$job} || $job eq $main::current_job;
    }
    print LOG "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 $job (@npkgs) {
	    next if grep( /^\Q$job\E\s/, @pkgs );
	    if (not defined $main::build_object->get_option('binNMU Version')) {
		print F "$job $main::build_object->get_option('Distribution')\n";
	    } else {
		print F "$job " .
		    $main::build_object->get_option({'Distribution'})
		    . " " .
		    $main::build_object->get_option('binNMU Version')
		    . " " .
		    $main::build_object->get_option('binNMU') . "\n";
	    }
	}
	close( F );
    }
    else {
	print "Cannot open $f: $!\n";
    }
    open( F, ">SBUILD-REDO-DUMPED" );
    close( F );
    print LOG "SBUILD-REDO-DUMPED created\n";
    unlink( "SBUILD-FINISHED" );

    # next: say which packages should be uninstalled
    @pkgs = keys %{$main::build_object->{'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";
    }

  not_ni_shutdown:
    # next: kill currently running command (if one)
    if ($main::build_object->{'Sub PID'}) {
	print "Killing $main::build_object->{'Sub Task'} subprocess $main::build_object->{'Sub PID'}\n";
	$main::build_object->{'Session'}->run_command("perl -e \"kill( \\\"TERM\\\", $main::build_object->{'Sub PID'} )\"", "root", 1, 0, '/');
    }
    $main::build_object->remove_srcdep_lock_file();

    # close logs and send mails
    if ( $main::current_job && defined($main::build_object->{'Session'})) {
	if ($conf::purge_build_directory eq "always") {
	    print PLOG "Purging $main::build_object->{'Chroot Build Dir'}\n";
	    my $bdir = $main::build_object->{'Session'}->strip_chroot_path($main::build_object->{'Chroot Build Dir'});
	    $main::build_object->{'Session'}->run_command("rm -rf '$bdir'", "root", 1, 0, '/');
	}

	$main::current_job =
	    $main::build_object->fixup_pkgv($main::current_job);

	$main::build_object->{'Session'}->end_session();
	undef $main::build_object->{'Session'};

	$main::build_object->close_build_log();
	undef $main::build_object->{'binNMU Name'};
    }
    close_log();
    unlink( $main::build_object->{'Jobs File'} ) if $main::build_object->{'Batch Mode'};
    $? = 0; $! = 0;
    if ($conf::sbuild_mode eq "user") {
	exit 1;
    }
    exit 0;
}

sub check_group_membership () {
    my $user = getpwuid($<);
    my ($name,$passwd,$gid,$members) = getgrnam("sbuild");

    if (!$gid) {
	die "Group sbuild does not exist";
    }

    my $in_group = 0;
    foreach (split(' ', $members)) {
	$in_group = 1 if $_ eq $Sbuild::Conf::username;
    }

    if (!$in_group) {
	print STDERR "User $user is not a member of group $name\n";
	print STDERR "See \"User Setup\" in sbuild-setup(7)\n";
	exit(1);
    }

    return;
}

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


$ENV{'LC_ALL'} = "POSIX";
$ENV{'SHELL'} = "/bin/sh";

# avoid intermixing of stdout and stderr
$| = 1;
# in case the terminal disappears, the build should continue
$SIG{'HUP'} = 'IGNORE';

# A file representing /dev/null
if (!open(main::DEVNULL, '+<', '/dev/null')) {
    die "Cannot open /dev/null: $!\n";;
}

check_group_membership();

umask(022);

$main::build_object = undef;

main();
