#
# Build.pm: build library for sbuild
# Copyright © 2005      Ryan Murray <rmurray@debian.org>
# Copyright © 2005-2010 Roger Leigh <rleigh@debian.org>
# 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 Sbuild::Build;

use strict;
use warnings;

use English;
use POSIX;
use Errno qw(:POSIX);
use Fcntl;
use File::Temp     qw(tempdir);
use File::Basename qw(basename dirname);
use FileHandle;
use File::Copy qw();    # copy is already exported from Sbuild, so don't export
                        # anything.
use Dpkg::Arch;
use Dpkg::Control;
use Dpkg::Index;
use Dpkg::Version;
use Dpkg::Deps qw(deps_concat deps_parse);
use Dpkg::Changelog::Debian;
use Scalar::Util 'refaddr';

use MIME::Lite;
use Term::ANSIColor;

use Sbuild qw($devnull binNMU_version copy isin debug send_mail
  dsc_files dsc_pkgver strftime_c);
use Sbuild::Base;
use Sbuild::ChrootInfoSchroot;
use Sbuild::ChrootInfoUnshare;
use Sbuild::ChrootInfoAutopkgtest;
use Sbuild::ChrootRoot;
use Sbuild::Sysconfig qw($version $release_date);
use Sbuild::Sysconfig;
use Sbuild::Resolver qw(get_resolver);
use Sbuild::Exception;

use version;

BEGIN {
	use Exporter ();
	our (@ISA, @EXPORT);

	@ISA = qw(Exporter Sbuild::Base);

	@EXPORT = qw();
}

our $saved_stdout = undef;
our $saved_stderr = undef;

sub new {
	my $class = shift;
	my $dsc   = shift;
	my $conf  = shift;

	my $self = $class->SUPER::new($conf);
	bless($self, $class);

	$self->set('ABORT',              undef);
	$self->set('Job',                $dsc);
	$self->set('Build Dir',          '');
	$self->set('Max Lock Trys',      120);
	$self->set('Lock Interval',      5);
	$self->set('Pkg Status',         'pending');
	$self->set('Pkg Status Trigger', undef);
	$self->set('Pkg Start Time',     0);
	$self->set('Pkg End Time',       0);
	$self->set('Pkg Fail Stage',     'init');
	$self->set('Build Start Time',   0);
	$self->set('Build End Time',     0);
	$self->set('Install Start Time', 0);
	$self->set('Install End Time',   0);
	$self->set('This Time',          0);
	$self->set('This Space',         0);
	$self->set('Sub Task',           'initialisation');
	$self->set('Host', Sbuild::ChrootRoot->new($self->get('Config')));
	# Host execution defaults
	my $host_defaults = $self->get('Host')->get('Defaults');
	$host_defaults->{'USER'}            = $self->get_conf('USERNAME');
	$host_defaults->{'DIR'}             = $self->get_conf('HOME');
	$host_defaults->{'STREAMIN'}        = $devnull;
	$host_defaults->{'ENV'}->{'LC_ALL'} = 'C.UTF-8';
	$host_defaults->{'ENV'}->{'SHELL'}  = '/bin/sh';
	$host_defaults->{'ENV_FILTER'} = $self->get_conf('ENVIRONMENT_FILTER');
	# Note, this should never fail.  But, we should handle failure anyway.
	$self->get('Host')->begin_session();

	$self->set('Session',               undef);
	$self->set('Dependency Resolver',   undef);
	$self->set('Log File',              undef);
	$self->set('Log Stream',            undef);
	$self->set('Summary Stats',         {});
	$self->set('dpkg-buildpackage pid', undef);
	$self->set('Dpkg Version',          undef);

	# DSC, package and version information:
	$self->set('DSC Orig', $dsc);
	$self->set_dsc($dsc);

	# If the job name contains an underscore then it is either the filename of
	# a dsc or a pkgname_version string. In both cases we can already extract
	# the version number. Otherwise it is a bare source package name and the
	# version will initially be unknown.
	if ($dsc =~ m/_/) {
		$self->set_version($dsc);
	} else {
		$self->set('Package', $dsc);
	}

	return $self;
}

sub request_abort {
	my $self   = shift;
	my $reason = shift;

	$self->log_error("ABORT: $reason (requesting cleanup and shutdown)\n");
	$self->set('ABORT', $reason);

	# Send signal to dpkg-buildpackage immediately if it's running.
	if (defined $self->get('dpkg-buildpackage pid')) {
		# Handling ABORT in the loop reading from the stdout/stderr output of
		# dpkg-buildpackage is suboptimal because then the ABORT signal would
		# only be handled once the build process writes to stdout or stderr
		# which might not be immediately.
		my $pid = $self->get('dpkg-buildpackage pid');
		# Sending the pid negated to send to the whole process group.
		kill "TERM", -$pid;
	}
}

sub check_abort {
	my $self = shift;

	if ($self->get('ABORT')) {
		Sbuild::Exception::Build->throw(
			error     => "Aborting build: " . $self->get('ABORT'),
			failstage => "abort"
		);
	}
}

sub set_dsc {
	my $self = shift;
	my $dsc  = shift;

	debug("Setting DSC: $dsc\n");

	$self->set('DSC',        $dsc);
	$self->set('Source Dir', dirname($dsc));
	$self->set('DSC Base',   basename($dsc));

	debug("DSC = " . $self->get('DSC') . "\n");
	debug("Source Dir = " . $self->get('Source Dir') . "\n");
	debug("DSC Base = " . $self->get('DSC Base') . "\n");
}

sub set_version {
	my $self = shift;
	my $pkgv = shift;

	debug("Setting package version: $pkgv\n");

	my ($pkg, $version);
	if (-f $pkgv && -r $pkgv) {
		($pkg, $version) = dsc_pkgver($pkgv);
	} else {
		($pkg, $version) = split /_/, $pkgv;
	}
	my $pver = Dpkg::Version->new($version, check => 1);
	return if (!defined($pkg) || !defined($version) || !defined($pver));
	my ($o_version);
	$o_version = $pver->version();

	# Original version (no binNMU or other addition)
	my $oversion = $version;
	# Original version with stripped epoch
	my $osversion = $o_version;
	$osversion .= '-' . $pver->revision() unless $pver->{'no_revision'};

	# Add binNMU to version if needed.
	if (   $self->get_conf('BIN_NMU')
		|| $self->get_conf('APPEND_TO_VERSION')
		|| defined $self->get_conf('BIN_NMU_CHANGELOG')) {
		if (defined $self->get_conf('BIN_NMU_CHANGELOG')) {
			# extract the binary version from the custom changelog entry
			open(CLOGFH, '<', \$self->get_conf('BIN_NMU_CHANGELOG'));
			my $changes = Dpkg::Changelog::Debian->new();
			$changes->parse(*CLOGFH, "descr");
			my @data = $changes->get_range({ count => 1 });
			$version = $data[0]->get_version();
			close(CLOGFH);
		} else {
			# compute the binary version from the original version and the
			# requested binNMU and append-to-version parameters
			$version = binNMU_version(
				$version,
				$self->get_conf('BIN_NMU_VERSION'),
				$self->get_conf('APPEND_TO_VERSION'));
		}
	}

	my $bver = Dpkg::Version->new($version, check => 1);
	return if (!defined($bver));
	my ($b_epoch, $b_version, $b_revision);
	$b_epoch    = $bver->epoch();
	$b_epoch    = "" if $bver->{'no_epoch'};
	$b_version  = $bver->version();
	$b_revision = $bver->revision();
	$b_revision = "" if $bver->{'no_revision'};

	# Version with binNMU or other additions and stripped epoch
	my $sversion = $b_version;
	$sversion .= '-' . $b_revision if $b_revision ne '';

	$self->set('Package',           $pkg);
	$self->set('Version',           $version);
	$self->set('Package_Version',   "${pkg}_$version");
	$self->set('Package_OVersion',  "${pkg}_$oversion");
	$self->set('Package_OSVersion', "${pkg}_$osversion");
	$self->set('Package_SVersion',  "${pkg}_$sversion");
	$self->set('OVersion',          $oversion);
	$self->set('OSVersion',         $osversion);
	$self->set('SVersion',          $sversion);
	$self->set('VersionEpoch',      $b_epoch);
	$self->set('VersionUpstream',   $b_version);
	$self->set('VersionDebian',     $b_revision);
	$self->set('DSC File',          "${pkg}_${osversion}.dsc");

	if (length $self->get_conf('DSC_DIR')) {
		$self->set('DSC Dir', $self->get_conf('DSC_DIR'));
	} else {
		$self->set('DSC Dir', "${pkg}-${b_version}");
	}

	debug("Package = " . $self->get('Package') . "\n");
	debug("Version = " . $self->get('Version') . "\n");
	debug("Package_Version = " . $self->get('Package_Version') . "\n");
	debug("Package_OVersion = " . $self->get('Package_OVersion') . "\n");
	debug("Package_OSVersion = " . $self->get('Package_OSVersion') . "\n");
	debug("Package_SVersion = " . $self->get('Package_SVersion') . "\n");
	debug("OVersion = " . $self->get('OVersion') . "\n");
	debug("OSVersion = " . $self->get('OSVersion') . "\n");
	debug("SVersion = " . $self->get('SVersion') . "\n");
	debug("VersionEpoch = " . $self->get('VersionEpoch') . "\n");
	debug("VersionUpstream = " . $self->get('VersionUpstream') . "\n");
	debug("VersionDebian = " . $self->get('VersionDebian') . "\n");
	debug("DSC File = " . $self->get('DSC File') . "\n");
	debug("DSC Dir = " . $self->get('DSC Dir') . "\n");
}

sub set_status {
	my $self   = shift;
	my $status = shift;

	$self->set('Pkg Status', $status);
	if (defined($self->get('Pkg Status Trigger'))) {
		$self->get('Pkg Status Trigger')->($self, $status);
	}
}

sub get_status {
	my $self = shift;

	return $self->get('Pkg Status');
}

# This function is the main entry point into the package build.  It
# provides a top-level exception handler and does the initial setup
# including initiating logging and creating host chroot.  The nested
# run_ functions it calls are separate in order to permit running
# cleanup tasks in a strict order.
sub run {
	my $self = shift;

	eval {
		$self->check_abort();

		$self->set_status('building');

		$self->set('Pkg Start Time', time);
		$self->set('Pkg End Time',   $self->get('Pkg Start Time'));

		# Acquire the architectures we're building for and on.
		$self->set('Host Arch',      $self->get_conf('HOST_ARCH'));
		$self->set('Build Arch',     $self->get_conf('BUILD_ARCH'));
		$self->set('Build Profiles', $self->get_conf('BUILD_PROFILES'));

		# Acquire the build type in the nomenclature used by the --build
		# argument of dpkg-buildpackage
		my $buildtype;
		if ($self->get_conf('BUILD_SOURCE')) {
			if ($self->get_conf('BUILD_ARCH_ANY')) {
				if ($self->get_conf('BUILD_ARCH_ALL')) {
					$buildtype = "full";
				} else {
					$buildtype = "source,any";
				}
			} else {
				if ($self->get_conf('BUILD_ARCH_ALL')) {
					$buildtype = "source,all";
				} else {
					$buildtype = "source";
				}
			}
		} else {
			if ($self->get_conf('BUILD_ARCH_ANY')) {
				if ($self->get_conf('BUILD_ARCH_ALL')) {
					$buildtype = "binary";
				} else {
					$buildtype = "any";
				}
			} else {
				if ($self->get_conf('BUILD_ARCH_ALL')) {
					$buildtype = "all";
				} else {
					Sbuild::Exception::Build->throw(
						error =>
"Neither architecture specific nor architecture independent or source package specified to be built.",
						failstage => "init"
					);
				}
			}
		}
		$self->set('Build Type', $buildtype);

		my $dist = $self->get_conf('DISTRIBUTION');
		if (!defined($dist) || !$dist) {
			Sbuild::Exception::Build->throw(
				error     => "No distribution defined",
				failstage => "init"
			);
		}

		# TODO: Get package name from build object
		if (!$self->open_build_log()) {
			Sbuild::Exception::Build->throw(
				error     => "Failed to open build log",
				failstage => "init"
			);
		}

		# Set a chroot to run commands in host
		my $host = $self->get('Host');

		# Host execution defaults (set streams)
		my $host_defaults = $host->get('Defaults');
		$host_defaults->{'STREAMIN'}  = $devnull;
		$host_defaults->{'STREAMOUT'} = $self->get('Log Stream');
		$host_defaults->{'STREAMERR'} = $self->get('Log Stream');

		$self->check_abort();
		$self->run_chroot();
	};

	debug("Error run(): $@") if $@;

	my $e;
	if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
		if ($e->status) {
			$self->set_status($e->status);
		} else {
			$self->set_status("failed");
		}
		$self->set('Pkg Fail Stage', $e->failstage);
		$e->rethrow();
	}
}

# Pack up source if needed and then run the main chroot session.
# Close log during return/failure.
sub run_chroot {
	my $self = shift;

	eval {
		$self->check_abort();
		$self->run_chroot_session();
	};

	debug("Error run_chroot(): $@") if $@;

	# Log exception info and set status and fail stage prior to
	# closing build log.
	my $e;
	if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
		$self->log_error("$e\n");
		$self->log_info($e->info . "\n")
		  if ($e->info);
		if ($e->status) {
			$self->set_status($e->status);
		} else {
			$self->set_status("failed");
		}
		$self->set('Pkg Fail Stage', $e->failstage);
	}

	$self->close_build_log();

	if ($e) {
		$e->rethrow();
	}
}

# Create main chroot session and package resolver.  Creates a lock in
# the chroot to prevent concurrent chroot usage (only important for
# non-snapshot chroots).  Ends chroot session on return/failure.
sub run_chroot_session {
	my $self = shift;

	eval {
		$self->check_abort();
		my $chroot_info;
		if ($self->get_conf('CHROOT_MODE') eq 'schroot') {
			$chroot_info
			  = Sbuild::ChrootInfoSchroot->new($self->get('Config'));
		} elsif ($self->get_conf('CHROOT_MODE') eq 'autopkgtest') {
			$chroot_info
			  = Sbuild::ChrootInfoAutopkgtest->new($self->get('Config'));
		} elsif ($self->get_conf('CHROOT_MODE') eq 'unshare') {
			$chroot_info
			  = Sbuild::ChrootInfoUnshare->new($self->get('Config'));
		} else {
			Sbuild::Exception::Build->throw(
				error     => "CHROOT_MODE=sudo (or unset) is unsupported",
				failstage => "create-session"
			);
		}

		my $host = $self->get('Host');

		my $session = $chroot_info->create(
			'chroot',
			$self->get_conf('DISTRIBUTION'),
			$self->get_conf('CHROOT'),
			$self->get_conf('BUILD_ARCH'));
		if (!defined $session) {
			Sbuild::Exception::Build->throw(
				error     => "Error creating chroot",
				failstage => "create-session"
			);
		}

		$self->check_abort();
		if (!$session->begin_session()) {
			Sbuild::Exception::Build->throw(
				error => "Error creating chroot session: skipping "
				  . $self->get('Package'),
				failstage => "create-session"
			);
		}
		$self->log_info("Creating chroot session...\n");

		$self->set('Session', $session);

		$self->check_abort();
		my $chroot_arch = $self->chroot_arch();
		if ($self->get_conf('BUILD_ARCH') ne $chroot_arch) {
			Sbuild::Exception::Build->throw(
				error => "Requested build architecture ("
				  . $self->get_conf('BUILD_ARCH')
				  . ") and chroot architecture ("
				  . $chroot_arch
				  . ") do not match.  Skipping build.",
				info =>
"Please specify the correct architecture with --build, or use a chroot of the correct architecture",
				failstage => "create-session"
			);
		}

		if (length $self->get_conf('BUILD_PATH')) {
			my $build_path = $self->get_conf('BUILD_PATH');
			$self->set('Build Dir', $build_path);
			if (!($session->test_directory($build_path))) {
				if (!$session->mkdir($build_path, { PARENTS => 1 })) {
					Sbuild::Exception::Build->throw(
						error => "Buildpath: "
						  . $build_path
						  . " cannot be created",
						failstage => "create-session"
					);
				}
			} else {
				my $isempty = <<END;
if (opendir my \$dfh, "$build_path") {
    while (defined(my \$file=readdir \$dfh)) {
	next if \$file eq "." or \$file eq "..";
	closedir \$dfh;
	exit 1
    }
    closedir \$dfh;
    exit 0
}
exit 2
END
				$session->run_command({
					COMMAND => ['perl', '-e', $isempty],
					USER    => 'root',
					DIR     => '/'
				});
				if ($? == 1) {
					Sbuild::Exception::Build->throw(
						error => "Buildpath: " . $build_path . " is not empty",
						failstage => "create-session"
					);
				} elsif ($? == 2) {
					Sbuild::Exception::Build->throw(
						error => "Buildpath: "
						  . $build_path
						  . " cannot be read. Insufficient permissions?",
						failstage => "create-session"
					);
				}
			}
		} else {
			# we run mktemp within the chroot instead of using
			# File::Temp::tempdir because the user running sbuild might
			# not have permissions creating a directory in /build.
			# This happens when the chroot was extracted in a different
			# user namespace than the outer user
			$self->check_abort();
			my $tmpdir = $session->mktemp({
				TEMPLATE  => "/build/" . $self->get('Package') . '-XXXXXX',
				DIRECTORY => 1
			});
			if (!$tmpdir) {
				$self->log_error("unable to mktemp\n");
				Sbuild::Exception::Build->throw(
					error     => "unable to mktemp",
					failstage => "create-build-dir"
				);
			}
			$self->check_abort();
			$self->set('Build Dir', $tmpdir);
		}

		# Copy in external solvers if we are cross-building
		if ($self->get('Host Arch') ne $self->get('Build Arch')) {
			if (!$session->test_directory("/usr/lib/apt/solvers")) {
				if (!$session->mkdir("/usr/lib/apt/solvers", { PARENTS => 1 }))
				{
					Sbuild::Exception::Build->throw(
						error     => "/usr/lib/apt/solvers cannot be created",
						failstage => "create-session"
					);
				}
			}
			my $solver = 'sbuild-cross-resolver';
			if (
				!$session->test_regular_file_readable(
					"/usr/lib/apt/solvers/$solver")
			) {
				if (!-e "/usr/lib/apt/solvers/$solver") {
					Sbuild::Exception::Build->throw(
						error     => "/usr/lib/apt/solvers/$solver is missing",
						failstage => "create-session"
					);
				}
				if (
					!$session->copy_to_chroot(
						"/usr/lib/apt/solvers/$solver",
						"/usr/lib/apt/solvers/$solver"
					)
				) {
					Sbuild::Exception::Build->throw(
						error =>
						  "/usr/lib/apt/solvers/$solver cannot be copied",
						failstage => "create-session"
					);
				}
				if (!$session->chmod("/usr/lib/apt/solvers/$solver", "0755")) {
					Sbuild::Exception::Build->throw(
						error => "/usr/lib/apt/solvers/$solver cannot chmod",
						failstage => "create-session"
					);
				}
			}
		}

		# Run pre build external commands
		$self->check_abort();
		if (!$self->run_external_commands("pre-build-commands")) {
			Sbuild::Exception::Build->throw(
				error     => "Failed to execute pre-build-commands",
				failstage => "run-pre-build-commands"
			);
		}

		# Log colouring
		if ($self->get_conf('LOG_COLOUR')) {
			$self->log_info("Setting up log color...\n");
		}
		$self->build_log_colour('red',        '^E: ');
		$self->build_log_colour('yellow',     '^W: ');
		$self->build_log_colour('green',      '^I: ');
		$self->build_log_colour('red',        '^Status:');
		$self->build_log_colour('green',      '^Status: successful$');
		$self->build_log_colour('yellow',     '^Keeping session: ');
		$self->build_log_colour('green',      '^Lintian: successful');
		$self->build_log_colour('bright_red', '^Lintian: failed');
		$self->build_log_colour('red', '^Lintian: failed - policy violation');
		$self->build_log_colour('red', '^Piuparts:');
		$self->build_log_colour('green',  '^Piuparts: pass$');
		$self->build_log_colour('red',    '^Autopkgtest:');
		$self->build_log_colour('yellow', '^Autopkgtest: no tests$');
		$self->build_log_colour('green',  '^Autopkgtest: pass$');

		# Log filtering
		my $filter;
		$filter = $session->get('Location');
		$filter =~ s;^/;;;
		$self->build_log_filter($filter, 'CHROOT');

		# Need tempdir to be writable and readable by sbuild group.
		$self->check_abort();
		if (
			!$session->chown(
				$self->get('Build Dir'), $self->get_conf('BUILD_USER'),
				'sbuild'
			)
		) {
			Sbuild::Exception::Build->throw(
				error =>
				  "Failed to set sbuild group ownership on chroot build dir",
				failstage => "create-build-dir"
			);
		}
		$self->check_abort();
		if (!$session->chmod($self->get('Build Dir'), "ug=rwx,o=,a-s")) {
			Sbuild::Exception::Build->throw(
				error =>
				  "Failed to set sbuild group ownership on chroot build dir",
				failstage => "create-build-dir"
			);
		}

		$self->check_abort();
		# Needed so chroot commands log to build log
		$session->set('Log Stream', $self->get('Log Stream'));
		$host->set('Log Stream', $self->get('Log Stream'));

		# Chroot execution defaults
		my $chroot_defaults = $session->get('Defaults');
		$chroot_defaults->{'DIR'}             = $self->get('Build Dir');
		$chroot_defaults->{'STREAMIN'}        = $devnull;
		$chroot_defaults->{'STREAMOUT'}       = $self->get('Log Stream');
		$chroot_defaults->{'STREAMERR'}       = $self->get('Log Stream');
		$chroot_defaults->{'ENV'}->{'LC_ALL'} = 'C.UTF-8';
		$chroot_defaults->{'ENV'}->{'SHELL'}  = '/bin/sh';
		# Setting $HOME to /sbuild-nonexistent attempts to prevent commands
		# that get run on the system (not in the chroot) via run_command()
		# from making changes to the user's home directory. This can be
		# problematic for chroot backends which rely on access to the user's
		# $HOME like the autopkgtest podman backend. The workaround in that
		# case is to set XDG_CACHE_HOME, XDG_CONFIG_HOME and XDG_DATA_HOME to
		# their appropriate values and add them to the $environment_filter.
		# We do not just plainly disable the environment filter because
		# autopkgtest has its own environment filter and does not start with
		# a blank slate but might pass some variables on to the testbed, see
		# run_test() in lib/adt_testbed.py.
		$chroot_defaults->{'ENV'}->{'HOME'} = '/sbuild-nonexistent';
		$chroot_defaults->{'ENV_FILTER'}
		  = $self->get_conf('ENVIRONMENT_FILTER');

		my $resolver = get_resolver($self->get('Config'), $session, $host);
		$resolver->set('Log Stream',     $self->get('Log Stream'));
		$resolver->set('Arch',           $self->get_conf('ARCH'));
		$resolver->set('Host Arch',      $self->get_conf('HOST_ARCH'));
		$resolver->set('Build Arch',     $self->get_conf('BUILD_ARCH'));
		$resolver->set('Build Profiles', $self->get_conf('BUILD_PROFILES'));
		$resolver->set('Build Dir',      $self->get('Build Dir'));
		$self->set('Dependency Resolver', $resolver);

		# Lock chroot so it won't be tampered with during the build.
		$self->check_abort();
		my $jobname;
		# the version might not yet be known if the user only passed a package
		# name without a version to sbuild
		if ($self->get('Package_SVersion')) {
			$jobname = $self->get('Package_SVersion');
		} else {
			$jobname = $self->get('Package');
		}
		if (!$session->lock_chroot($jobname, $$, $self->get_conf('USERNAME')))
		{
			Sbuild::Exception::Build->throw(
				error => "Error locking chroot session: skipping "
				  . $self->get('Package'),
				failstage => "lock-session"
			);
		}

		$self->check_abort();
		$self->run_chroot_session_locked();
	};

	debug("Error run_chroot_session(): $@") if $@;

	# End chroot session
	my $session = $self->get('Session');
	if (defined $session) {
		my $end_session = (
			$self->get_conf('PURGE_SESSION') eq 'always'
			  || ( $self->get_conf('PURGE_SESSION') eq 'successful'
				&& $self->get_status() eq 'successful')) ? 1 : 0;
		if ($end_session) {
			$session->end_session();
		} else {
			if ($self->get_conf('CHROOT_MODE') ne 'unshare') {
				$self->log(
					"Keeping session: " . $session->get('Session ID') . "\n");
			}
		}
		$session = undef;
	}
	$self->set('Session', $session);

	my $e;
	if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
		$e->rethrow();
	}
}

# Run tasks in a *locked* chroot.  Update and upgrade packages.
# Unlocks chroot on return/failure.
sub run_chroot_session_locked {
	my $self = shift;

	eval {
		my $session  = $self->get('Session');
		my $resolver = $self->get('Dependency Resolver');

		# Run specified chroot setup commands
		$self->check_abort();
		if (!$self->run_external_commands("chroot-setup-commands")) {
			Sbuild::Exception::Build->throw(
				error     => "Failed to execute chroot-setup-commands",
				failstage => "run-chroot-setup-commands"
			);
		}

		$self->check_abort();

		$self->check_abort();
		$self->log_info("Setting up apt archive...\n");
		if (!$resolver->setup()) {
			Sbuild::Exception::Build->throw(
				error     => "resolver setup failed",
				failstage => "resolver setup"
			);
		}

		my $filter;
		$filter = $resolver->get('Dummy package path');
		$filter =~ s;^/;;;
		$self->build_log_filter($filter, 'RESOLVERDIR');

		$self->check_abort();
		$self->run_chroot_update();

		$self->check_abort();
		$self->run_fetch_install_packages();
	};

	debug("Error run_chroot_session_locked(): $@") if $@;

	my $session  = $self->get('Session');
	my $resolver = $self->get('Dependency Resolver');

	$resolver->cleanup();
	# Unlock chroot now it's cleaned up and ready for other users.
	$session->unlock_chroot();

	my $e;
	if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
		$e->rethrow();
	}
}

sub run_chroot_update {
	my $self     = shift;
	my $resolver = $self->get('Dependency Resolver');

	eval {
		if (   $self->get_conf('APT_CLEAN')
			|| $self->get_conf('APT_UPDATE')
			|| $self->get_conf('APT_DISTUPGRADE')
			|| $self->get_conf('APT_UPGRADE')) {
			$self->log_subsection_t('Update chroot', time);
		}

		# Clean APT cache.
		$self->check_abort();
		if ($self->get_conf('APT_CLEAN')) {
			if ($resolver->clean()) {
				# Since apt-clean was requested specifically, fail on
				# error when not in buildd mode.
				$self->log_error("apt-get clean failed\n");
				if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
					Sbuild::Exception::Build->throw(
						error     => "apt-get clean failed",
						failstage => "apt-get-clean"
					);
				}
			}
		}

		# Update APT cache.
		$self->check_abort();
		if ($self->get_conf('APT_UPDATE')) {
			if (!$resolver->update_archive(1)) {
				# Since apt-update was requested specifically, fail on
				# error when not in buildd mode.
				if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
					Sbuild::Exception::Build->throw(
						error     => "apt-get update failed",
						failstage => "apt-get-update"
					);
				}
			}
		} else {
			# If it was requested not to do an apt update, the build and host
			# architecture must already be part of the chroot. If they are not
			# and thus added during the sbuild run, issue a warning because
			# then the package build dependencies will likely fail to be
			# installable.
			#
			# The logic which checks which architectures are needed is in
			# ResolverBase.pm, so we just check whether any architectures
			# where added with 'dpkg --add-architecture' because if any were
			# added an update is most likely needed.
			if (keys %{ $resolver->get('Added Foreign Arches') }) {
				$self->log_warning(
"Additional architectures were added but apt update was disabled. Build dependencies might not be satisfiable.\n"
				);
			}
		}

		# Upgrade using APT.
		$self->check_abort();
		if ($self->get_conf('APT_DISTUPGRADE')) {
			if ($resolver->distupgrade()) {
				# Since apt-distupgrade was requested specifically, fail on
				# error when not in buildd mode.
				if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
					Sbuild::Exception::Build->throw(
						error     => "apt-get dist-upgrade failed",
						failstage => "apt-get-dist-upgrade"
					);
				}
			}
		} elsif ($self->get_conf('APT_UPGRADE')) {
			if ($resolver->upgrade()) {
				# Since apt-upgrade was requested specifically, fail on
				# error when not in buildd mode.
				if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
					Sbuild::Exception::Build->throw(
						error     => "apt-get upgrade failed",
						failstage => "apt-get-upgrade"
					);
				}
			}
		}
	};

	debug("Error run_chroot_update(): $@") if $@;

	my $e = Exception::Class->caught('Sbuild::Exception::Build');
	if ($e) {
		$self->run_external_commands("chroot-update-failed-commands");
		$e->rethrow();
	}
}

# Fetch sources, run setup, fetch and install core and package build
# deps, then run build.  Cleans up build directory and uninstalls
# build depends on return/failure.
sub run_fetch_install_packages {
	my $self = shift;

	$self->check_abort();
	eval {
		my $session  = $self->get('Session');
		my $resolver = $self->get('Dependency Resolver');

		$self->check_abort();
		if (!$self->fetch_source_files()) {
			Sbuild::Exception::Build->throw(
				error     => "Failed to fetch source files",
				failstage => "fetch-src"
			);
		}

		# Display message about chroot setup script option use being deprecated
		if ($self->get_conf('CHROOT_SETUP_SCRIPT')) {
			my $msg
			  = "setup-hook option is deprecated. It has been superseded by ";
			$msg
			  .= "the chroot-setup-commands feature. setup-hook script will be ";
			$msg .= "run via chroot-setup-commands.\n";
			$self->log_warning($msg);
		}

		$self->check_abort();
		$self->set('Install Start Time', time);
		$self->set('Install End Time',   $self->get('Install Start Time'));
		my @coredeps = @{ $self->get_conf('CORE_DEPENDS') };
		if ($self->get('Host Arch') ne $self->get('Build Arch')) {
			my $crosscoredeps = $self->get_conf('CROSSBUILD_CORE_DEPENDS');
			if (defined($crosscoredeps->{ $self->get('Host Arch') })) {
				push(@coredeps,
					@{ $crosscoredeps->{ $self->get('Host Arch') } });
			} else {
				push(@coredeps,
						'crossbuild-essential-'
					  . $self->get('Host Arch')
					  . ':native');
				# for /usr/lib/apt/solvers/apt which is used by the
				# sbuild-cross-resolver
				push(@coredeps, 'apt-utils:native');
				# Also add the following to work around bug #815172
				push(@coredeps,
					'libc-dev:' . $self->get('Host Arch'),
					'libstdc++-dev:' . $self->get('Host Arch'));
			}
		}

		my @snapshot = ();
		@snapshot = ("gcc-snapshot") if ($self->get_conf('GCC_SNAPSHOT'));

		$resolver->add_dependencies(
			'MAIN',
			join(", ",
				$self->get('Build Depends') // (),
				@{ $self->get_conf('MANUAL_DEPENDS') },
				@snapshot,
				@coredeps),
			join(", ",
				$self->get('Build Depends Arch') // (),
				@{ $self->get_conf('MANUAL_DEPENDS_ARCH') }),
			join(", ",
				$self->get('Build Depends Indep') // (),
				@{ $self->get_conf('MANUAL_DEPENDS_INDEP') }),
			join(", ",
				$self->get('Build Conflicts') // (),
				@{ $self->get_conf('MANUAL_CONFLICTS') }),
			join(", ",
				$self->get('Build Conflicts Arch') // (),
				@{ $self->get_conf('MANUAL_CONFLICTS_ARCH') }),
			join(", ",
				$self->get('Build Conflicts Indep') // (),
				@{ $self->get_conf('MANUAL_CONFLICTS_INDEP') }));

		$self->log_subsection_t("Install package build dependencies", time);

		$self->check_abort();
		if (!$resolver->install_deps(0, 'main', 'MAIN')) {
			Sbuild::Exception::Build->throw(
				error => "Package build dependencies not satisfied; skipping",
				failstage => "install-deps"
			);
		}
		$self->check_abort();
		if ($self->get_conf('PURGE_EXTRA_PACKAGES')) {
			if (!$resolver->purge_extra_packages($self->get('Package'))) {
				Sbuild::Exception::Build->throw(
					error => "Chroot could not be cleaned of extra packages",
					failstage => "install-deps"
				);
			}
		}
		$self->set('Install End Time', time);

		# the architecture check has to be done *after* build-essential is
		# installed because as part of the architecture check a perl script is
		# run inside the chroot which requires the Dpkg::Arch module which is
		# in libdpkg-perl which might not exist in the chroot but will get
		# installed by the build-essential package
		if (!$self->check_architectures()) {
			Sbuild::Exception::Build->throw(
				error     => "Architecture check failed",
				failstage => "check-architecture"
			);
		}

		$self->check_abort();
		my $dpkg_version = $resolver->dump_build_environment();
		$self->set('Dpkg Version', Dpkg::Version->new($dpkg_version));

		$self->check_abort();
		if ($self->build()) {
			$self->set_status('successful');
		} else {
			$self->set('Pkg Fail Stage', "build");
			$self->set_status('failed');
		}

		# We run it here and not inside build() because otherwise, we cannot
		# set the overall status to failed due to lintian errors
		if ($self->get('Pkg Status') eq "successful") {
			# Run lintian.
			$self->check_abort();
			my $ret = $self->run_lintian();
			if (!$ret && $self->get_conf('LINTIAN_REQUIRE_SUCCESS')) {
				$self->set('Pkg Fail Stage', "post-build");
				$self->set_status("failed");
			}
		}

		# Run specified chroot cleanup commands
		$self->check_abort();
		if (!$self->run_external_commands("chroot-cleanup-commands")) {
			Sbuild::Exception::Build->throw(
				error     => "Failed to execute chroot-cleanup-commands",
				failstage => "run-chroot-cleanup-commands"
			);
		}

		# piuparts and autopkgtest must be run while the chroot is still open
		# because they might need files that are not available on the host,
		# for example the .dsc which might have been downloaded
		if ($self->get('Pkg Status') eq "successful") {
			if (!grep { $_ eq "postbuild" }
				@{ $self->get_conf('LOG_HIDDEN_SECTIONS') }) {
				$self->log_subsection_t("Post Build", time);
			}

			# Run piuparts.
			$self->check_abort();
			my $ret = $self->run_piuparts();
			if (!$ret && $self->get_conf('PIUPARTS_REQUIRE_SUCCESS')) {
				$self->set('Pkg Fail Stage', "post-build");
				$self->set_status("failed");
			}

			# Run autopkgtest.
			$self->check_abort();
			$ret = $self->run_autopkgtest();
			if (!$ret && $self->get_conf('AUTOPKGTEST_REQUIRE_SUCCESS')) {
				$self->set('Pkg Fail Stage', "post-build");
				$self->set_status("failed");
			}

			# Run post build external commands
			$self->check_abort();
			if (!$self->run_external_commands("post-build-commands")) {
				Sbuild::Exception::Build->throw(
					error     => "Failed to execute post-build-commands",
					failstage => "run-post-build-commands"
				);
			}

		}
	};

	# If 'This Time' is still zero, then build() raised an exception and thus
	# the end time was never set. Thus, setting it here.
	# If we would set 'This Time' here unconditionally, then it would also
	# possibly include the times to run piuparts and autopkgtest.
	if ($self->get('This Time') == 0) {
		$self->set('This Time',
			$self->get('Pkg End Time') - $self->get('Pkg Start Time'));
		$self->set('This Time', 0) if $self->get('This Time') < 0;
	}
	# Same for 'This Space' which we must set here before everything gets
	# cleaned up.
	if ($self->get('This Space') == 0) {
		# Since the build apparently failed, we pass an empty list of the
		# build artifacts
		$self->set('This Space', $self->check_space());
	}

	debug("Error run_fetch_install_packages(): $@") if $@;

	# I catch the exception here and trigger the hook, if needed. Normally I'd
	# do this at the end of the function, but I want the hook to fire before we
	# clean up the environment. I re-throw the exception at the end, as usual
	my $e = Exception::Class->caught('Sbuild::Exception::Build');
	if ($e) {
		if ($e->status) {
			$self->set_status($e->status);
		} else {
			$self->set_status("failed");
		}
		$self->set('Pkg Fail Stage', $e->failstage);
	}
	if (!$self->get('ABORT') && defined $self->get('Pkg Fail Stage')) {
		if ($self->get('Pkg Fail Stage') eq 'build') {
			if (!$self->run_external_commands("build-failed-commands")) {
				Sbuild::Exception::Build->throw(
					error     => "Failed to execute build-failed-commands",
					failstage => "run-build-failed-commands"
				);
			}
		} elsif ($self->get('Pkg Fail Stage') eq 'install-deps') {
			my $could_not_explain = undef;

			if (   defined $self->get_conf('BD_UNINSTALLABLE_EXPLAINER')
				&& $self->get_conf('BD_UNINSTALLABLE_EXPLAINER') ne ''
				&& $self->get_conf('BD_UNINSTALLABLE_EXPLAINER') ne 'none') {
				if (!$self->explain_bd_uninstallable()) {
					$could_not_explain = 1;
				}
			}

			if (!$self->run_external_commands("build-deps-failed-commands")) {
				Sbuild::Exception::Build->throw(
					error => "Failed to execute build-deps-failed-commands",
					failstage => "run-build-deps-failed-commands"
				);
			}

			if ($could_not_explain) {
				Sbuild::Exception::Build->throw(
					error     => "Failed to explain bd-uninstallable",
					failstage => "explain-bd-uninstallable"
				);
			}
		}
	}

	if ($self->get('Pkg Status') ne "successful") {
		if (!$self->run_external_commands("post-build-failed-commands")) {
			Sbuild::Exception::Build->throw(
				error     => "Failed to execute post-build-commands",
				failstage => "run-post-build-failed-commands"
			);
		}
	}

	if (!grep { $_ eq "cleanup" } @{ $self->get_conf('LOG_HIDDEN_SECTIONS') })
	{
		$self->log_subsection_t("Cleanup", time);
	}
	my $session  = $self->get('Session');
	my $resolver = $self->get('Dependency Resolver');

	my $purge_build_directory = (
		$self->get_conf('PURGE_BUILD_DIRECTORY') eq 'always'
		  || ( $self->get_conf('PURGE_BUILD_DIRECTORY') eq 'successful'
			&& $self->get_status() eq 'successful')) ? 1 : 0;
	my $purge_build_deps = (
		$self->get_conf('PURGE_BUILD_DEPS') eq 'always'
		  || ( $self->get_conf('PURGE_BUILD_DEPS') eq 'successful'
			&& $self->get_status() eq 'successful')) ? 1 : 0;
	my $is_cloned_session = (defined($session->get('Session Purged'))
		  && $session->get('Session Purged') == 1) ? 1 : 0;

	if ($purge_build_directory) {
		# Purge package build directory
		$self->log("Purging " . $self->get('Build Dir') . "\n");
		if (!$self->get('Session')
			->unlink($self->get('Build Dir'), { RECURSIVE => 1 })) {
			$self->log_error("unable to remove build directory\n");
		}
	}

	# Purge non-cloned session
	if ($is_cloned_session) {
		$self->log("Not cleaning session: cloned chroot in use\n");
	} else {
		if ($purge_build_deps) {
			# Removing dependencies
			$resolver->uninstall_deps();
		} else {
			$self->log("Not removing build depends: as requested\n");
		}
	}

	# re-throw the previously-caught exception
	if ($e) {
		$e->rethrow();
	}
}

sub copy_to_chroot {
	my $self       = shift;
	my $source     = shift;
	my $chrootdest = shift;

	my $session = $self->get('Session');

	$self->check_abort();
	if (!$session->copy_to_chroot($source, $chrootdest)) {
		$self->log_error("Failed to copy $source to $chrootdest\n");
		return 0;
	}

	if (!$session->chown($chrootdest, $self->get_conf('BUILD_USER'), 'sbuild'))
	{
		$self->log_error(
			"Failed to set sbuild group ownership on $chrootdest\n");
		return 0;
	}
	if (!$session->chmod($chrootdest, "ug=rw,o=r,a-s")) {
		$self->log_error("Failed to set 0644 permissions on $chrootdest\n");
		return 0;
	}

	return 1;
}

sub fetch_source_files {
	my $self = shift;

	my $build_dir = $self->get('Build Dir');
	my $host_arch = $self->get('Host Arch');
	my $resolver  = $self->get('Dependency Resolver');

	my ($dscarchs, $dscpkg, $dscver, $dsc);

	my $build_depends         = "";
	my $build_depends_arch    = "";
	my $build_depends_indep   = "";
	my $build_conflicts       = "";
	my $build_conflicts_arch  = "";
	my $build_conflicts_indep = "";
	local (*F);

	$self->log_subsection_t("Fetch source files", time);

	$self->check_abort();
	if ($self->get('DSC Base') =~ m/\.dsc$/) {
		my $dir = $self->get('Source Dir');

		# Work with a .dsc file.
		my $file = $self->get('DSC');
		$dsc = $self->get('DSC File');
		if (!-f $file || !-r $file) {
			$self->log_error("Could not find $file\n");
			return 0;
		}
		my @cwd_files = dsc_files($file);

		# Copy the local source files into the build directory.
		$self->log_subsubsection("Local sources");
		$self->log("$file exists in $dir; copying to chroot\n");
		if (!$self->copy_to_chroot("$file", "$build_dir/$dsc")) {
			$self->log_error("Could not copy $file to $build_dir/$dsc\n");
			return 0;
		}
		foreach (@cwd_files) {
			if (!$self->copy_to_chroot("$dir/$_", "$build_dir/$_")) {
				$self->log_error("Could not copy $dir/$_ to $build_dir/$_\n");
				return 0;
			}
		}
	} else {
		my $pkg = $self->get('DSC');
		my $ver;

		if ($pkg =~ m/_/) {
			($pkg, $ver) = split /_/, $pkg;
		}

		# Use apt to download the source files
		$self->log_subsubsection("Check APT");

		my $indextargets;
		{
			my $pipe = $self->get('Session')->pipe_command({
				COMMAND => ['apt-get', 'indextargets'],
				USER    => $self->get_conf('BUILD_USER'),
			});
			if (!$pipe) {
				$self->log_error("Can't open pipe to apt-get: $!\n");
				return 0;
			}
			$indextargets
			  = Dpkg::Index->new(get_key_func => sub { return $_[0]->{URI}; });

			if (!$indextargets->parse($pipe, 'apt-get indextargets')) {
				$self->log_error(
					"Cannot parse output of apt-get indextargets: $!\n");
				return 0;
			}
			close($pipe);

			if ($?) {
				$self->log_error("apt-get indextargets exit status $?: $!\n");
				return 0;
			}
		}
		my $found_sources_entry = 0;
		my %unique_sources      = ();
		foreach my $key ($indextargets->get_keys()) {
			my $cdata      = $indextargets->get_by_key($key);
			my $createdby  = $cdata->{"Created-By"} // "";
			my $targetof   = $cdata->{"Target-Of"}  // "";
			my $identifier = $cdata->{"Identifier"} // "";
			if (    $createdby eq "Sources"
				and $identifier eq "Sources"
				and $targetof eq "deb-src") {
				$found_sources_entry = 1;
				last;
			}
			if (    $createdby eq 'Packages'
				and $identifier eq 'Packages'
				and $targetof eq 'deb'
				and length $cdata->{"Repo-URI"} > 0
				and length $cdata->{"Codename"} > 0
				and length $cdata->{"Label"} > 0
				and length $cdata->{"Origin"} > 0
				and length $cdata->{"Suite"} > 0
				and $cdata->{"Repo-URI"} =~ /^file:\//
				and $cdata->{"Codename"} eq 'invalid-sbuild-codename'
				and $cdata->{'Label'} eq 'sbuild-build-depends-archive'
				and $cdata->{'Origin'} eq 'sbuild-build-depends-archive'
				and $cdata->{'Suite'} eq 'invalid-sbuild-suite') {
				# do not count the sbuild dummy repository created by any
				# --extra-package options
				next;
			}
			if (    $createdby eq 'Packages'
				and $identifier eq 'Packages'
				and $targetof eq 'deb'
				and length $cdata->{"Repo-URI"} > 0
				and length $cdata->{"Codename"} > 0
				and length $cdata->{"Component"} > 0) {
				$unique_sources{
					join "\n",            $cdata->{"Repo-URI"},
					$cdata->{"Codename"}, $cdata->{"Component"} } = 1;
			}
		}
		if (!$found_sources_entry) {
			$self->log("There are no deb-src lines in your sources.list\n");
			if (scalar(keys %unique_sources) == 0) {
				$self->log(
					"Cannot generate deb-src entry without deb entry\n");
			} else {
				foreach my $unique_source (keys %unique_sources) {
					my ($entry_uri, $entry_codename, $entry_component)
					  = split /\n/, $unique_source, 3;
					my $entry
					  = "deb-src $entry_uri $entry_codename $entry_component";
					$self->log(
						"Automatically adding to EXTRA_REPOSITORIES: $entry\n"
					);
					push @{ $self->get_conf('EXTRA_REPOSITORIES') }, $entry;
				}
				$resolver->add_extra_repositories();
				$self->run_chroot_update();
			}
		}

		$self->log("Checking available source versions...\n");

		# We would like to call apt-cache with --only-source so that the
		# result only contains source packages with the given name but this
		# feature was only introduced in apt 1.1~exp10 so it is only available
		# in Debian Stretch and later
		my $pipe = $self->get('Dependency Resolver')->pipe_apt_command({
			COMMAND =>
			  [$self->get_conf('APT_CACHE'), '-q', 'showsrc', $pkg],
			USER     => $self->get_conf('BUILD_USER'),
			PRIORITY => 0,
			DIR      => '/'
		});
		if (!$pipe) {
			$self->log_error("Can't open pipe to "
				  . $self->get_conf('APT_CACHE')
				  . ": $!\n");
			return 0;
		}

		my $key_func = sub {
			return $_[0]->{Package} . '_' . $_[0]->{Version};
		};

		my $index = Dpkg::Index->new(get_key_func => $key_func);

		if (!$index->parse($pipe, 'apt-cache showsrc')) {
			$self->log_error("Cannot parse output of apt-cache showsrc: $!\n");
			return 0;
		}

		close($pipe);

		if ($?) {
			$self->log_error(
				$self->get_conf('APT_CACHE') . " exit status $?: $!\n");
			return 0;
		}

		my $highestversion;
		my $highestdsc;

		foreach my $key ($index->get_keys()) {
			my $cdata   = $index->get_by_key($key);
			my $pkgname = $cdata->{"Package"};
			if (not defined($pkgname)) {
				$self->log_warning("apt-cache output without Package field\n");
				next;
			}
			# Since we cannot run apt-cache with --only-source because that
			# feature was only introduced with apt 1.1~exp10, the result can
			# contain source packages that we didn't ask for (but which
			# contain binary packages of the name we specified). Since we only
			# are interested in source packages of the given name, we skip
			# everything that is a different source package.
			if ($pkg ne $pkgname) {
				next;
			}
			my $pkgversion = $cdata->{"Version"};
			if (not defined($pkgversion)) {
				$self->log_warning("apt-cache output without Version field\n");
				next;
			}
			if (defined($ver) and $ver ne $pkgversion) {
				next;
			}
			my $checksums = Dpkg::Checksums->new();
			$checksums->add_from_control($cdata, use_files_for_md5 => 1);
			my @files = grep { /\.dsc$/ } $checksums->get_files();
			if (scalar @files != 1) {
				$self->log_warning(
					"apt-cache output with more than one .dsc\n");
				next;
			}
			if (!defined $highestdsc) {
				$highestdsc     = $files[0];
				$highestversion = $pkgversion;
			} else {
				if (version_compare($highestversion, $pkgversion) < 0) {
					$highestdsc     = $files[0];
					$highestversion = $pkgversion;
				}
			}
		}

		if (!defined $highestdsc) {
			my $pkgname = $pkg;
			if (defined $ver) {
				$pkgname = "$pkg=$ver";
			}
			$self->log_error($self->get_conf('APT_CACHE')
				  . " returned no information about $pkgname source\n");
			$self->log_error(
				"Are there any deb-src lines in your /etc/apt/sources.list?\n"
			);
			return 0;
		}

		$self->set_dsc($highestdsc);
		$dsc = $highestdsc;

		$self->log_subsubsection("Download source files with APT");

		my $pipe2 = $self->get('Dependency Resolver')->pipe_apt_command({
				COMMAND => [
					$self->get_conf('APT_GET'),
					'--only-source', '-q', '-d', 'source',
					"$pkg=$highestversion"
				],
				USER     => $self->get_conf('BUILD_USER'),
				PRIORITY => 0
			}) || return 0;

		while (<$pipe2>) {
			$self->log($_);
		}
		close($pipe2);
		if ($?) {
			$self->log_error(
				$self->get_conf('APT_GET') . " for sources failed\n");
			return 0;
		}
	}

	my $pipe = $self->get('Session')->get_read_file_handle("$build_dir/$dsc");
	if (!$pipe) {
		$self->log_error("unable to open pipe\n");
		return 0;
	}

	my $pdsc = Dpkg::Control->new(type => CTRL_PKG_SRC);
	$pdsc->set_options(allow_pgp => 1);
	if (!$pdsc->parse($pipe, "$build_dir/$dsc")) {
		$self->log_error("Error parsing $build_dir/$dsc\n");
		return 0;
	}

	close($pipe);

	$build_depends         = $pdsc->{'Build-Depends'};
	$build_depends_arch    = $pdsc->{'Build-Depends-Arch'};
	$build_depends_indep   = $pdsc->{'Build-Depends-Indep'};
	$build_conflicts       = $pdsc->{'Build-Conflicts'};
	$build_conflicts_arch  = $pdsc->{'Build-Conflicts-Arch'};
	$build_conflicts_indep = $pdsc->{'Build-Conflicts-Indep'};
	$dscarchs              = $pdsc->{'Architecture'};
	$dscpkg                = $pdsc->{'Source'};
	$dscver                = $pdsc->{'Version'};

	$self->set_version("${dscpkg}_${dscver}");

	$build_depends         =~ s/\n\s+/ /g if defined $build_depends;
	$build_depends_arch    =~ s/\n\s+/ /g if defined $build_depends_arch;
	$build_depends_indep   =~ s/\n\s+/ /g if defined $build_depends_indep;
	$build_conflicts       =~ s/\n\s+/ /g if defined $build_conflicts;
	$build_conflicts_arch  =~ s/\n\s+/ /g if defined $build_conflicts_arch;
	$build_conflicts_indep =~ s/\n\s+/ /g if defined $build_conflicts_indep;

	$self->set('Build Depends',         $build_depends);
	$self->set('Build Depends Arch',    $build_depends_arch);
	$self->set('Build Depends Indep',   $build_depends_indep);
	$self->set('Build Conflicts',       $build_conflicts);
	$self->set('Build Conflicts Arch',  $build_conflicts_arch);
	$self->set('Build Conflicts Indep', $build_conflicts_indep);

	$self->set('Dsc Architectures', $dscarchs);

	# we set up the following filters this late because the user might only
	# have specified a source package name to build without a version in which
	# case we only get to know the final build directory now
	my $filter;
	$filter = $self->get('Build Dir') . '/' . $self->get('DSC Dir');
	$filter =~ s;^/;;;
	$self->build_log_filter($filter, 'PKGBUILDDIR');
	$filter = $self->get('Build Dir');
	$filter =~ s;^/;;;
	$self->build_log_filter($filter, 'BUILDDIR');

	return 1;
}

sub check_deb_arch {
	my $self               = shift;
	my $pkg                = shift;
	my @all_foreign_arches = @_;
	my $build_arch         = $self->get('Build Arch');
	# Investigate the Architecture field of the binary package
	my $arch = $self->get('Host')->read_command({
			COMMAND =>
			  ['dpkg-deb', '--field', Cwd::abs_path($pkg), 'Architecture'],
			USER => $self->get_conf('USERNAME') });
	if (!defined $arch) {
		$self->log_warning("Failed to run dpkg-deb on $pkg. Skipping...\n");
		return 0;
	}
	chomp $arch;
	# Only packages that are Architecture:all, the native architecture or
	# one of the configured foreign architectures are allowed.
	if (    $arch ne 'all'
		and $arch ne $build_arch
		and !isin($arch, @all_foreign_arches)) {
		$self->log_warning("Extra package $pkg of architecture $arch"
			  . "cannot be installed in the chroot\n");
		return 0;
	}
	return 1;
}

sub check_architectures {
	my $self       = shift;
	my $resolver   = $self->get('Dependency Resolver');
	my $dscarchs   = $self->get('Dsc Architectures');
	my $build_arch = $self->get('Build Arch');
	my $host_arch  = $self->get('Host Arch');
	my $session    = $self->get('Session');

	$self->log_subsection_t("Check architectures", time);
	# Check for cross-arch dependencies
	# parse $build_depends* for explicit :arch and add the foreign arches,
	# as needed
	#
	# This check only looks at the immediate build dependencies. This could
	# fail in a future where a foreign architecture direct build dependency of
	# architecture X depends on another foreign architecture package of
	# architecture Y. Architecture Y would not be added through this check as
	# sbuild will not traverse the dependency graph. Doing so would be very
	# complicated as new architectures would have to be added to a dependency
	# solver like dose3 as the graph is traversed and new architectures are
	# found.
	sub get_explicit_arches {
		my $visited_deps = pop;
		my @deps         = @_;

		my %set;
		for my $dep (@deps) {
			# Break any recursion in the deps data structure
			# (is this overkill?)
			next if !defined $dep;
			my $id = ref($dep) ? refaddr($dep) : "str:$dep";
			next if $visited_deps->{$id};
			$visited_deps->{$id} = 1;

			if (exists($dep->{archqual})) {
				if ($dep->{archqual}) {
					$set{ $dep->{archqual} } = 1;
				}
			} else {
				for
				  my $key (get_explicit_arches($dep->get_deps, $visited_deps))
				{
					$set{$key} = 1;
				}
			}
		}

		return keys %set;
	}

	# we don't need to look at build conflicts here because conflicting with a
	# package of an explicit architecture does not mean that we need to enable
	# that architecture in the chroot
	my $build_depends_concat = deps_concat(
		grep { defined $_ } (
			$self->get('Build Depends'),
			$self->get('Build Depends Arch'),
			$self->get('Build Depends Indep')));
	my $merged_depends = deps_parse(
		$build_depends_concat,
		reduce_arch     => 1,
		host_arch       => $self->get('Host Arch'),
		build_arch      => $self->get('Build Arch'),
		build_dep       => 1,
		reduce_profiles => 1,
		build_profiles  => [split / /, $self->get('Build Profiles')]);
	if (!defined $merged_depends) {
		my $msg
		  = "Error! deps_parse() couldn't parse the Build-Depends '$build_depends_concat'";
		$self->log_error("$msg\n");
		return 0;
	}

	my @explicit_arches = get_explicit_arches($merged_depends, {});
	my @foreign_arches  = grep { $_ !~ /any|all|native/ } @explicit_arches;
	my $added_any_new;
	for my $foreign_arch (@foreign_arches) {
		$resolver->add_foreign_architecture($foreign_arch);
		$added_any_new = 1;
	}

	my @keylist = keys %{ $resolver->get('Initial Foreign Arches') };
	$self->log('Initial Foreign Architectures: ' . join ' ', @keylist, "\n")
	  if @keylist;
	$self->log('Foreign Architectures in build-deps: ' . join ' ',
		@foreign_arches, "\n\n")
	  if @foreign_arches;

	$self->run_chroot_update() if $added_any_new;

	# At this point, all foreign architectures should have been added to dpkg.
	# Thus, we now examine, whether the packages passed via --extra-package
	# can even be considered by dpkg inside the chroot with respect to their
	# architecture.

	# Retrieve all foreign architectures from the chroot. We need to do this
	# step because the user might've added more foreign arches to the chroot
	# beforehand.
	my @all_foreign_arches = split /\s+/,
	  $session->read_command({
		  COMMAND => ['dpkg', '--print-foreign-architectures'],
		  USER    => $self->get_conf('BUILD_USER'),
	  });
	if ($? != 0) {
		$self->log_error("dpkg --print-foreign-architectures failed\n");
		return 0;
	}
	for my $deb (@{ $self->get_conf('EXTRA_PACKAGES') }) {
		if (-f $deb) {
			$self->check_deb_arch($deb, @all_foreign_arches);
		} elsif (-d $deb) {
			opendir(D, $deb);
			while (my $f = readdir(D)) {
				next if (!-f "$deb/$f");
				next if ("$deb/$f" !~ /\.deb$/);
				$self->check_deb_arch("$deb/$f", @all_foreign_arches);
			}
			closedir(D);
		} else {
			$self->log_warning(
				"$deb is neither a regular file nor a directory. Skipping...\n"
			);
		}
	}

	# Check package arch makes sense to build
	if (!$dscarchs) {
		$self->log_warning(
			"dsc has no Architecture: field -- skipping arch check!\n");
	} elsif ($self->get_conf('BUILD_SOURCE')) {
		# If the source package is to be built, then we do not need to check
		# if any of the source package's architectures can be built given the
		# current host architecture because then no matter the Architectures
		# field, at least the source package will end up getting built.
	} else {
		my $valid_arch;
		for my $a (split(/\s+/, $dscarchs)) {
			# Check architecture wildcard matching with dpkg inside the chroot
			# to avoid situations in which dpkg outside the chroot doesn't
			# know about a new architecture yet
			my $command = <<"EOF";
		use strict;
		use warnings;
		use Dpkg::Arch;
		if (Dpkg::Arch::debarch_is('$host_arch', '$a')) {
		    exit 0;
		}
		exit 1;
EOF
			$session->run_command({
				COMMAND  => ['perl', '-e', $command],
				USER     => 'root',
				PRIORITY => 0,
				DIR      => '/'
			});
			if ($? == 0) {
				$valid_arch = 1;
				last;
			}
		}
		if (   $dscarchs ne "any"
			&& !($valid_arch)
			&& !($dscarchs =~ /\ball\b/ && $self->get_conf('BUILD_ARCH_ALL')))
		{
			my $msg
			  = "dsc: $host_arch not in arch list or does not match any arch wildcards: $dscarchs -- skipping";
			$self->log_error("$msg\n");
			Sbuild::Exception::Build->throw(
				error     => $msg,
				status    => "skipped",
				failstage => "arch-check"
			);
			return 0;
		}
	}

	$self->log("Arch check ok ($host_arch included in $dscarchs)\n");

	return 1;
}

# Subroutine that runs any command through the system (i.e. not through the
# chroot. It takes a string of a command with arguments to run along with
# arguments whether to save STDOUT and/or STDERR to the log stream
sub run_command {
	my $self       = shift;
	my $command    = shift;
	my $log_output = shift;
	my $log_error  = shift;
	my $chroot     = shift;

	# Used to determine if we are to log from commands
	my ($out, $err, $defaults);

	# Run the command and save the exit status
	if (!$chroot) {
		$defaults = $self->get('Host')->{'Defaults'};
		$out      = $defaults->{'STREAMOUT'} if ($log_output);
		$err      = $defaults->{'STREAMERR'} if ($log_error);

		my %args = (
			PRIORITY  => 0,
			STREAMOUT => $out,
			STREAMERR => $err
		);
		if (ref $command) {
			$args{COMMAND}     = \@{$command};
			$args{COMMAND_STR} = "@{$command}";
		} else {
			$args{COMMAND}     = [split('\s+', $command)];
			$args{COMMAND_STR} = $command;
		}

		$self->get('Host')->run_command(\%args);
	} else {
		$defaults = $self->get('Session')->{'Defaults'};
		$out      = $defaults->{'STREAMOUT'} if ($log_output);
		$err      = $defaults->{'STREAMERR'} if ($log_error);

		my %args = (
			USER      => 'root',
			PRIORITY  => 0,
			STREAMOUT => $out,
			STREAMERR => $err
		);
		if (ref $command) {
			$args{COMMAND}     = \@{$command};
			$args{COMMAND_STR} = "@{$command}";
		} else {
			$args{COMMAND}     = [split('\s+', $command)];
			$args{COMMAND_STR} = $command;
		}

		$self->get('Session')->run_command(\%args);
	}
	my $status = $?;

	# Check if the command failed
	if ($status != 0) {
		return 0;
	}
	return 1;
}

# Subroutine that processes external commands to be run during various stages of
# an sbuild run. We also ask if we want to log any output from the commands
sub run_external_commands {
	my $self  = shift;
	my $stage = shift;

	my $log_output = $self->get_conf('LOG_EXTERNAL_COMMAND_OUTPUT');
	my $log_error  = $self->get_conf('LOG_EXTERNAL_COMMAND_ERROR');

	# Return success now unless there are commands to run
	return 1 unless (${ $self->get_conf('EXTERNAL_COMMANDS') }{$stage});

	# Determine which set of commands to run based on the parameter $stage
	my @commands = @{ ${ $self->get_conf('EXTERNAL_COMMANDS') }{$stage} };
	return 1 if !(@commands);

	# Create appropriate log message and determine if the commands are to be
	# run inside the chroot or not, and as root or not.
	my $chroot = 0;
	if ($stage eq "pre-build-commands") {
		$self->log_subsection_t("Pre Build Commands", time);
	} elsif ($stage eq "chroot-setup-commands") {
		$self->log_subsection_t("Chroot Setup Commands", time);
		$chroot = 1;
	} elsif ($stage eq "chroot-update-failed-commands") {
		$self->log_subsection_t("Chroot-update Install Failed Commands", time);
		$chroot = 1;
	} elsif ($stage eq "build-deps-failed-commands") {
		$self->log_subsection_t("Build-Deps Install Failed Commands", time);
		$chroot = 1;
	} elsif ($stage eq "build-failed-commands") {
		$self->log_subsection_t("Generic Build Failed Commands", time);
		$chroot = 1;
	} elsif ($stage eq "starting-build-commands") {
		$self->log_subsection_t("Starting Time Build Commands", time);
		$chroot = 1;
	} elsif ($stage eq "finished-build-commands") {
		$self->log_subsection_t("Finished Time Build Commands", time);
		$chroot = 1;
	} elsif ($stage eq "chroot-cleanup-commands") {
		$self->log_subsection_t("Chroot Cleanup Commands", time);
		$chroot = 1;
	} elsif ($stage eq "post-build-commands") {
		$self->log_subsection_t("Post Build Commands", time);
	} elsif ($stage eq "post-build-failed-commands") {
		$self->log_subsection_t("Post Build Failed Commands", time);
	}

	# Run each command, substituting the various percent escapes (like
	# %SBUILD_DSC) from the commands to run with the appropriate subsitutions.
	my $hostarch  = $self->get('Host Arch');
	my $buildarch = $self->get('Build Arch');
	my $build_dir = $self->get('Build Dir');
	my $shell_cmd = "bash -i </dev/tty >/dev/tty 2>/dev/tty";
	my $log_dir   = $self->get_conf('LOG_DIR');
	my %percent   = (
		"%"                 => "%",
		"a"                 => $hostarch,
		"SBUILD_HOST_ARCH"  => $hostarch,
		"SBUILD_BUILD_ARCH" => $buildarch,
		"b"                 => $build_dir,
		"SBUILD_BUILD_DIR"  => $build_dir,
		"s"                 => $shell_cmd,
		"SBUILD_SHELL"      => $shell_cmd,
		"SBUILD_LOG_DIR"    => $log_dir,
	);
	if ($self->get('Changes File')) {
		my $changes = $self->get('Changes File');
		$percent{c}              = $changes;
		$percent{SBUILD_CHANGES} = $changes;
	}
	# In case set_version has not been run yet, we do not know the dsc file or
	# directory yet. This can happen if the user only specified a source
	# package name without a version on the command line.
	if ($self->get('DSC Dir')) {
		my $dsc = $self->get('DSC');
		$percent{d}          = $dsc;
		$percent{SBUILD_DSC} = $dsc;
		my $pkgbuild_dir = $build_dir . '/' . $self->get('DSC Dir');
		$percent{p}                   = $pkgbuild_dir;
		$percent{SBUILD_PKGBUILD_DIR} = $pkgbuild_dir;
	}
	my $log_basename = $self->get('Log File Basename');
	if ($log_basename) {
		$percent{SBUILD_LOG_BASENAME} = $log_basename;
	}
	my $log_path = $self->get('Log File');
	if ($log_path) {
		# nb, 'Log File' is (usually) set evn if --nolog is used. It
		# is only unset when neither Package nor Package_SVersion were
		# defined
		$percent{SBUILD_LOG_PATH} = $log_path;
	}
	my $srcpackage = $self->get('Package');
	if ($srcpackage) {
		$percent{SRCPACKAGE} = $srcpackage;
	}
	my $srcpackage_ver = $self->get('Package_SVersion');
	if ($srcpackage_ver) {
		# not set if user only specified a package name
		$percent{SRCPACKAGE_VERSION} = $srcpackage_ver;
	}

	if ($chroot == 0) {
		my $chroot_dir = $self->get('Session')->get('Location');
		$percent{r}                 = $chroot_dir;
		$percent{SBUILD_CHROOT_DIR} = $chroot_dir;
		# the %SBUILD_CHROOT_EXEC escape is only defined when the command is
		# to be run outside the chroot
		my $exec_string = $self->get('Session')->get_internal_exec_string();
		$percent{e}                  = $exec_string;
		$percent{SBUILD_CHROOT_EXEC} = $exec_string;
	}
	# Our escapes pattern, with longer escapes first, then sorted lexically.
	my $keyword_pat
	  = join("|", sort { length $b <=> length $a || $a cmp $b } keys %percent);
	my $returnval = 1;
	foreach my $command (@commands) {

		my $substitute = sub {
			foreach (@_) {
				s{
		     # Match a percent followed by a valid keyword
		     \%($keyword_pat)
	     }{
		 # Substitute with the appropriate value only if it's defined
		 $percent{$1} || $&
	     }msxge;
			}
		};

		my $command_str;
		if (ref $command) {
			$substitute->(@{$command});
			$command_str = join(" ", @{$command});
		} else {
			$substitute->($command);
			$command_str = $command;
		}

		$self->log_subsubsection("$command_str");

		$returnval
		  = $self->run_command($command, $log_output, $log_error, $chroot);
		$self->log("\n");
		if (!$returnval) {
			$self->log_error("Command '$command_str' failed to run.\n");
			# do not run any other commands of this type after the first
			# failure
			last;
		} else {
			$self->log_info("Finished running '$command_str'.\n");
		}
	}
	$self->log("\nFinished processing commands.\n");
	$self->log_sep();
	return $returnval;
}

sub run_lintian {
	my $self    = shift;
	my $session = $self->get('Session');

	return 1 unless ($self->get_conf('RUN_LINTIAN'));
	$self->set('Lintian Reason', 'error');

	if (!defined($session)) {
		$self->log_error("Session is undef. Cannot run lintian.\n");
		return 0;
	}

	$self->log_subsubsection("lintian");

	my $build_dir = $self->get('Build Dir');
	my $resolver  = $self->get('Dependency Resolver');
	my $lintian   = $self->get_conf('LINTIAN');
	my $changes   = $self->get_changes();
	if (!defined($changes)) {
		$self->log_error(".changes is undef. Cannot run lintian.\n");
		return 0;
	}

	my @lintian_command = ($lintian);
	push @lintian_command, @{ $self->get_conf('LINTIAN_OPTIONS') }
	  if ($self->get_conf('LINTIAN_OPTIONS'));
	push @lintian_command, $changes;

	# If the source package was not instructed to be built, then it will not
	# be part of the .changes file and thus, the .dsc has to be passed to
	# lintian in addition to the .changes file.
	if (!$self->get_conf('BUILD_SOURCE')) {
		my $dsc = $self->get('DSC File');
		push @lintian_command, $dsc;
	}

	$resolver->add_dependencies('LINTIAN', 'lintian:native', "", "", "", "",
		"");
	return 1 unless $resolver->install_deps(1, 'lintian', 'LINTIAN');

	$self->log("Running lintian...\n");

	# we are not using read_command() because we also need the output for
	# non-zero exit codes
	my $pipe = $session->pipe_command({
		COMMAND  => \@lintian_command,
		PRIORITY => 0,
		DIR      => $self->get('Build Dir'),
		PIPE     => "in"
	});
	if (!$pipe) {
		$self->log_error("Failed to exec Lintian: $!\n");
		return 0;
	}
	my $lintian_output = "";
	while (my $line = <$pipe>) {
		$self->log($line);
		$lintian_output .= $line;
	}
	close $pipe;

	$self->log("\n");
	# find worst non-overridden tag from lintian (these might be
	# present in a successful run, depending on the lintian options
	# used)
	my $lintian_worst_message = 'no issues';
	if ($lintian_output =~ m/^E: /m) {
		$lintian_worst_message = 'error';
	} elsif ($lintian_output =~ m/^W: /m) {
		$lintian_worst_message = 'warn';
	} elsif ($lintian_output =~ m/^I: /m) {
		$lintian_worst_message = 'info';
	} elsif ($lintian_output =~ m/^P: /m) {
		$lintian_worst_message = 'pedantic';
	} elsif ($lintian_output =~ m/^X: /m) {
		$lintian_worst_message = 'experimental';
	}

	my $status = $? >> 8;
	my $result = "unknown";
	if ($? & 127) {
		$result = "failed - received signal " . $? & 127;
	} elsif ($status == 0) {
		$result = "successful";
	} elsif ($status == 1) {
		$result = "failed - runtime error";
	} elsif ($status == 2) {
		$result = "failed - policy violation";
	}

	if ($lintian_worst_message ne 'no issues') {
		$result .= " (worst tag: $lintian_worst_message)";
	}

	$self->set('Lintian Reason', "$result");

	if ($status == 0) {
		$self->log_info("Lintian run $result\n");
		return 1;
	} else {
		$self->log_error("Lintian run $result\n");
		return 0;
	}
}

sub run_piuparts {
	my $self = shift;

	return 1 unless ($self->get_conf('RUN_PIUPARTS'));
	$self->set('Piuparts Reason', 'fail');

	$self->log_subsubsection("piuparts");

	my $piuparts = $self->get_conf('PIUPARTS');
	my @piuparts_command;
	# The default value is the empty array.
	# If the value is the default (empty array) prefix with 'sudo --' unless
	# sbuild is run in unshare mode.
	# If the value is a non-empty array, prefix with its values except if the
	# first value is an empty string in which case, prefix with nothing
	# If the value is not an array, prefix with that scalar except if the
	# scalar is the empty string in which case, prefix with nothing
	if (ref($self->get_conf('PIUPARTS_ROOT_ARGS')) eq "ARRAY") {
		if (scalar(@{ $self->get_conf('PIUPARTS_ROOT_ARGS') }) == 0) {
			if ($self->get_conf('CHROOT_MODE') ne 'unshare') {
				push @piuparts_command, 'sudo', '--';
			}
		} elsif (@{ $self->get_conf('PIUPARTS_ROOT_ARGS') }[0] eq '') {
			# do nothing if the first array element is the empty string
		} else {
			push @piuparts_command, @{ $self->get_conf('PIUPARTS_ROOT_ARGS') };
		}
	} elsif ($self->get_conf('PIUPARTS_ROOT_ARGS') eq '') {
		# do nothing if the configuration value is the empty string
	} else {
		push @piuparts_command, $self->get_conf('PIUPARTS_ROOT_ARGS');
	}
	push @piuparts_command, $piuparts;
	push @piuparts_command, @{ $self->get_conf('PIUPARTS_OPTIONS') }
	  if ($self->get_conf('PIUPARTS_OPTIONS'));
	push @piuparts_command, $self->get('Changes File');
	$self->get('Host')->run_command({
		COMMAND    => \@piuparts_command,
		ENV_FILTER => ['^TMPDIR$'],
		PRIORITY   => 0,
	});
	my $status = $? >> 8;

	# We must check for Ctrl+C (and other aborting signals) directly after
	# running the command so that we do not mark the piuparts run as successful
	# (the exit status will be zero)
	$self->check_abort();

	$self->log("\n");

	if ($status == 0) {
		$self->set('Piuparts Reason', 'pass');
	} else {
		$self->log_error("Piuparts run failed.\n");
		return 0;
	}

	$self->log_info("Piuparts run was successful.\n");
	return 1;
}

sub run_autopkgtest {
	my $self = shift;

	return 1 unless ($self->get_conf('RUN_AUTOPKGTEST'));

	$self->set('Autopkgtest Reason', 'fail');

	$self->log_subsubsection("autopkgtest");

	my $session = $self->get('Session');

	# sbuild used to check whether debian/tests/control exists and would not
	# run autopkgtest at all if it didn't. This is wrong behaviour because
	# even packages without a debian/tests/control or packages without a
	# Testsuite: field in debian/control might still have autopkgtests as they
	# are generated by autodep8. We will not attempt to recreate the autodep8
	# heuristics here and thus we will always run autopkgtest if
	# RUN_AUTOPKGTEST was set to true. Also see
	# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=916924

	my $autopkgtest = $self->get_conf('AUTOPKGTEST');
	my @autopkgtest_command;
	# The default value is the empty array.
	# If the value is the default (empty array) prefix with 'sudo --' unless
	# sbuild is run in unshare mode.
	# If the value is a non-empty array, prefix with its values except if the
	# first value is an empty string in which case, prefix with nothing
	# If the value is not an array, prefix with that scalar except if the
	# scalar is the empty string in which case, prefix with nothing
	if (ref($self->get_conf('AUTOPKGTEST_ROOT_ARGS')) eq "ARRAY") {
		if (scalar(@{ $self->get_conf('AUTOPKGTEST_ROOT_ARGS') }) == 0) {
			if ($self->get_conf('CHROOT_MODE') ne 'unshare') {
				push @autopkgtest_command, 'sudo', '--';
			}
		} elsif (@{ $self->get_conf('AUTOPKGTEST_ROOT_ARGS') }[0] eq '') {
			# do nothing if the first array element is the empty string
		} else {
			push @autopkgtest_command,
			  @{ $self->get_conf('AUTOPKGTEST_ROOT_ARGS') };
		}
	} elsif ($self->get_conf('AUTOPKGTEST_ROOT_ARGS') eq '') {
		# do nothing if the configuration value is the empty string
	} else {
		push @autopkgtest_command, $self->get_conf('AUTOPKGTEST_ROOT_ARGS');
	}
	push @autopkgtest_command, $autopkgtest;
	my $tmpdir;
	my @cwd_files;
	# If the source package was not instructed to be built, then it will not
	# be part of the .changes file and thus, the .dsc has to be passed to
	# autopkgtest in addition to the .changes file.
	if (!$self->get_conf('BUILD_SOURCE')) {
		my $dsc;
		if ($self->get('DSC Orig') =~ m/\.dsc$/) {
			# sbuild got passed a dsc file from the outside
			$dsc = Cwd::abs_path($self->get('DSC'));
		} else {
			$dsc = $self->get('DSC');
			# sbuild got passed a source package name and downloaded that
			# itself, so it must be made available to the host
			my $build_dir = $self->get('Build Dir');
			$tmpdir = tempdir("tmp.sbuild.XXXXXXXXXX", TMPDIR => 1);
			if (!$session->copy_from_chroot("$build_dir/$dsc", "$tmpdir/$dsc"))
			{
				$self->log_error("cannot copy .dsc from chroot\n");
				rmdir $tmpdir;
				return 0;
			}
			@cwd_files = dsc_files("$tmpdir/$dsc");
			foreach (@cwd_files) {
				if (!$session->copy_from_chroot("$build_dir/$_", "$tmpdir/$_"))
				{
					$self->log_error("cannot copy $_ from chroot\n");
					unlink "$tmpdir/$.dsc";
					foreach (@cwd_files) {
						unlink "$tmpdir/$_" if -f "$tmpdir/$_";
					}
					rmdir $tmpdir;
					return 0;
				}
			}
			$dsc = "$tmpdir/$dsc";
		}
		push @autopkgtest_command, $dsc;
	}
	push @autopkgtest_command, $self->get('Changes File');

	# Retrieve all foreign architectures from the chroot. We need to do this
	# step because the user might've added more foreign arches to the chroot
	# beforehand.
	my @all_foreign_arches = split /\s+/,
	  $session->read_command({
		  COMMAND => ['dpkg', '--print-foreign-architectures'],
		  USER    => $self->get_conf('BUILD_USER'),
	  });
	if ($? != 0) {
		$self->log_error("dpkg --print-foreign-architectures failed\n");
		return 0;
	}
	for my $deb (@{ $self->get_conf('EXTRA_PACKAGES') }) {
		$deb = Cwd::abs_path($deb);
		if (-f $deb) {
			next if !$self->check_deb_arch($deb, @all_foreign_arches);
			push @autopkgtest_command, $deb;
		} elsif (-d $deb) {
			opendir(D, $deb);
			while (my $f = readdir(D)) {
				next if (!-f "$deb/$f");
				next if ("$deb/$f" !~ /\.deb$/);
				next if !$self->check_deb_arch("$deb/$f", @all_foreign_arches);
				push @autopkgtest_command, $deb;
			}
			closedir(D);
		} else {
			$self->log_warning(
				"$deb is neither a regular file nor a directory. Skipping...\n"
			);
		}
	}

	if (scalar(@{ $self->get_conf('AUTOPKGTEST_OPTIONS') })) {
		push @autopkgtest_command, @{ $self->get_conf('AUTOPKGTEST_OPTIONS') };
	} else {
		push @autopkgtest_command, '--', 'null';
	}
	$self->get('Host')->run_command({
		COMMAND    => \@autopkgtest_command,
		ENV_FILTER => ['^TMPDIR$'],
		PRIORITY   => 0,
	});
	my $status = $? >> 8;
	# if the source package wasn't built and also initially downloaded by
	# sbuild, then the temporary directory that was created must be removed
	if (defined $tmpdir) {
		my $dsc = $self->get('DSC');
		unlink "$tmpdir/$dsc";
		foreach (@cwd_files) {
			unlink "$tmpdir/$_";
		}
		rmdir $tmpdir;
	}

	# We must check for Ctrl+C (and other aborting signals) directly after
	# running the command so that we do not mark the autopkgtest as successful
	# (the exit status will be zero)
	# But we must check only after the temporary directory has been removed.
	$self->check_abort();

	$self->log("\n");

	if ($status == 0 || $status == 2)
	{ # 2 is "at least one test was skipped (or at least one flaky test failed)"
		$self->set('Autopkgtest Reason', 'pass');
	} elsif ($status == 8) {
		$self->set('Autopkgtest Reason', 'no tests');
	} else {
		# fail if neither all tests passed nor was the package without tests
		$self->log_error("Autopkgtest run failed.\n");
		return 0;
	}

	$self->log_info("Autopkgtest run was successful.\n");
	return 1;
}

sub explain_bd_uninstallable {
	my $self = shift;

	my $resolver = $self->get('Dependency Resolver');

	my $dummy_pkg_name = $resolver->get_sbuild_dummy_pkg_name('main');

	if (!defined $self->get_conf('BD_UNINSTALLABLE_EXPLAINER')) {
		return 0;
	} elsif ($self->get_conf('BD_UNINSTALLABLE_EXPLAINER') eq '') {
		return 0;
	} elsif ($self->get_conf('BD_UNINSTALLABLE_EXPLAINER') eq 'apt') {
		my (@instd, @rmvd);
		my @apt_args
		  = ('--simulate', \@instd, \@rmvd, 'install', $dummy_pkg_name);
		if ($self->get_conf('HOST_ARCH') eq $self->get_conf('BUILD_ARCH')) {
			# when building natively, pass the debug options to apt directly
			push @apt_args,
			  (
				'-oDebug::pkgProblemResolver=true',
				'-oDebug::pkgDepCache::Marker=1',
				'-oDebug::pkgDepCache::AutoInstall=1'
			  );
		} else {
			# When cross-building, the sbuild-cross-resolver is used, which
			# will ignore options passed to apt via --option. We use the
			# Preferences field in the EDSP request to enable debug output.
			push @apt_args,
			  '-oAPT::Solver::sbuild-cross-resolver::Preferences=debug';
		}
		$resolver->run_apt(@apt_args);
	} elsif ($self->get_conf('BD_UNINSTALLABLE_EXPLAINER') eq 'dose3') {
		# To retrieve all Packages files apt knows about we use "apt-get
		# indextargets" and "apt-helper cat-file". The former is able to
		# report the filesystem path of all input Packages files. The latter
		# is able to decompress the files if necessary.
		#
		# We do not use "apt-cache dumpavail" or convert the EDSP output to a
		# Packages file because that would make the package selection subject
		# to apt pinning. This limitation would be okay if there was only the
		# apt resolver but since there also exists the aptitude and aspcud
		# resolvers which are able to find solution without pinning
		# restrictions, we don't want to limit ourselves by it. In cases where
		# apt cannot find a solution, this check is supposed to allow the user
		# to know that choosing a different resolver might fix the problem.
		#
		# FIXME: dose3 will ignore apt pinning. One could write a parser for
		# files in /etc/apt/preferences.d/ and filter the Packages files
		# accordingly. Until then, if pinning values are relevant (for example
		# in the sbuild autopkgtest where packages are pinned to unstable
		# to test transitions to testing) use the apt explainer instead.
		$resolver->add_dependencies('DOSE3', 'dose-distcheck:native', "", "",
			"", "", "");
		if (!$resolver->install_deps(0, 'dose3', 'DOSE3')) {
			return 0;
		}

		my $session  = $self->get('Session');
		my $pipe_apt = $session->pipe_command({
				COMMAND => [
					'apt-get',  'indextargets',
					'--format', '$(FILENAME)',
					'Created-By: Packages'
				],
				USER => $self->get_conf('BUILD_USER'),
			});
		if (!$pipe_apt) {
			$self->log_error(
				"cannot open reading pipe from apt-get indextargets\n");
			return 0;
		}

		my $host          = $self->get_conf('HOST_ARCH');
		my $build         = $self->get_conf('BUILD_ARCH');
		my @debforeignarg = ();
		if ($build ne $host) {
			@debforeignarg = ('--deb-foreign-archs', $host);
		}

		# - We run dose-debcheck instead of dose-builddebcheck because we want
		#   to check the dummy binary package created by sbuild instead of the
		#   original source package Build-Depends.
		# - We use dose-debcheck instead of dose-distcheck because we cannot
		#   use the deb:// prefix on data from standard input.
		my $pipe_dose = $session->pipe_command({
				COMMAND => [
					'dose-debcheck',               '--checkonly',
					"$dummy_pkg_name:$host",       '--verbose',
					'--failures',                  '--successes',
					'--explain',                   '--deb-native-arch',
					$self->get_conf('BUILD_ARCH'), @debforeignarg
				],
				PRIORITY => 0,
				USER     => $self->get_conf('BUILD_USER'),
				PIPE     => 'out'
			});
		if (!$pipe_dose) {
			$self->log_error("cannot open writing pipe to dose-debcheck\n");
			return 0;
		}

		# We parse file by file instead of concatenating all files because if
		# there are many files, we might exceed the maximum command length and
		# it avoids having to have the data from all Packages files in memory
		# all at once. Working with a smaller Dpkg::Index structure should
		# also result in faster store and retrieval times.
		while (my $fname = <$pipe_apt>) {
			chomp $fname;
			my $pipe_cat = $session->pipe_command({
				COMMAND => ['/usr/lib/apt/apt-helper', 'cat-file', $fname],
				USER    => $self->get_conf('BUILD_USER'),
			});
			if (!$pipe_cat) {
				$self->log_error("cannot open reading pipe from apt-helper\n");
				return 0;
			}

			# For native compilation we just pipe the output of apt-helper to
			# dose3. For cross compilation we need to filter foreign
			# architecture packages that are Essential:yes or
			# Multi-Arch:foreign or otherwise dose3 might present a solution
			# that installs foreign architecture Essential:yes or
			# Multi-Arch:foreign packages.
			if ($build eq $host) {
				File::Copy::copy $pipe_cat, $pipe_dose;
			} else {
				my $key_func = sub {
					return
						$_[0]->{Package} . ' '
					  . $_[0]->{Version} . ' '
					  . $_[0]->{Architecture};
				};

				my $index = Dpkg::Index->new(get_key_func => $key_func);

				if (!$index->parse($pipe_cat, 'apt-helper cat-file')) {
					$self->log_error(
						"Cannot parse output of apt-helper cat-file: $!\n");
					return 0;
				}

				foreach my $key ($index->get_keys()) {
					my $cdata = $index->get_by_key($key);
					my $arch  = $cdata->{'Architecture'} // '';
					my $ess   = $cdata->{'Essential'}    // '';
					my $ma    = $cdata->{'Multi-Arch'}   // '';
					if (   $arch ne 'all'
						&& $arch ne $build
						&& ($ess eq 'yes' || $ma eq 'foreign')) {
						next;
					}
					$cdata->output($pipe_dose);
					print $pipe_dose "\n";
				}
			}

			close($pipe_cat);
			if (($? >> 8) != 0) {
				$self->log_error("apt-helper failed\n");
				return 0;
			}
		}

		close $pipe_dose;
		# - We expect an exit code of less than 64 of dose-debcheck. Any other
		#   exit code indicates abnormal program termination.
		if (($? >> 8) >= 64) {
			$self->log_error("dose-debcheck failed\n");
			return 0;
		}

		close $pipe_apt;
		if (($? >> 8) != 0) {
			$self->log_error("apt-get indextargets failed\n");
			return 0;
		}

	}

	return 1;
}

sub build {
	my $self = shift;

	my $dscfile    = $self->get('DSC File');
	my $dscdir     = $self->get('DSC Dir');
	my $pkg        = $self->get('Package');
	my $build_dir  = $self->get('Build Dir');
	my $host_arch  = $self->get('Host Arch');
	my $build_arch = $self->get('Build Arch');
	my $session    = $self->get('Session');
	my $resolver   = $self->get('Dependency Resolver');

	my ($rv, $changes);
	local (*PIPE, *F, *F2);

	$self->log_subsection_t("Build", time);
	$self->set('This Space', 0);

	my $tmpunpackdir = $dscdir;
	$tmpunpackdir =~ s/-.*$/.orig.tmp-nest/;
	$tmpunpackdir =~ s/_/-/;
	$tmpunpackdir = "$build_dir/$tmpunpackdir";

	$dscdir = "$build_dir/$dscdir";

	$self->log_subsubsection("Unpack source");
	if ($session->test_directory($dscdir) && $session->test_symlink($dscdir)) {
		# if the package dir already exists but is a symlink, complain
		$self->log_error(
				"Cannot unpack source: a symlink to a directory with the\n"
			  . "same name already exists.\n");
		return 0;
	}
	my $dsccontent = $session->read_file("$build_dir/$dscfile");
	if (!$dsccontent) {
		$self->log_error("Cannot read $build_dir/$dscfile\n");
	} else {
		$self->log($dsccontent);
		$self->log("\n");
	}
	if (!$session->test_directory($dscdir)) {
		$self->set('Sub Task', "dpkg-source");
		$session->run_command({
			COMMAND =>
			  [$self->get_conf('DPKG_SOURCE'), '-x', $dscfile, $dscdir],
			USER     => $self->get_conf('BUILD_USER'),
			DIR      => $build_dir,
			PRIORITY => 0
		});
		if ($?) {
			$self->log_error("FAILED [dpkg-source died]\n");
			Sbuild::Exception::Build->throw(
				error     => "FAILED [dpkg-source died]",
				failstage => "unpack"
			);
		}

		if (!$session->chmod($dscdir, 'g-s,go+rX', { RECURSIVE => 1 })) {
			$self->log_error("chmod -R g-s,go+rX $dscdir failed.\n");
			Sbuild::Exception::Build->throw(
				error     => "chmod -R g-s,go+rX $dscdir failed",
				failstage => "unpack"
			);
		}
	} else {
		$self->log_subsubsection("Check unpacked source");
		if (   $self->get_conf('CHROOT_MODE') eq 'schroot'
			&& $self->get_conf('BUILD_PATH') eq '/build/reproducible-path'
			&& $self->get_conf('PURGE_BUILD_DIRECTORY') ne 'always') {
			$session->run_command({
				COMMAND => ['mountpoint', '-q', '/build'],
				USER    => 'root'
			});
			if ($? == 0) {
				$self->log_warning(
"The unpacked source directory already exists, you are using schroot mode, PURGE_BUILD_DIRECTORY is not 'always' and /build is a mountpoint. To prevent clobbering an existing unpacked source directory, either:\n - use a randomized build path by setting \$build_path = undef;\n - purge the build directory by setting \$purge_build_directory = 'always'\n - avoid persistent mount on /build\n - switch from schroot mode to unshare mode by using \$chroot_mode = 'unshare';\n"
				);
			}
		}
		# check if the unpacked tree is really the version we need
		my $clog = $session->read_command({
			COMMAND  => ['dpkg-parsechangelog'],
			USER     => $self->get_conf('BUILD_USER'),
			PRIORITY => 0,
			DIR      => $dscdir
		});
		if (!$clog) {
			$self->log_error("unable to read from dpkg-parsechangelog\n");
			Sbuild::Exception::Build->throw(
				error     => "unable to read from dpkg-parsechangelog",
				failstage => "check-unpacked-version"
			);
		}
		$self->set('Sub Task', "dpkg-parsechangelog");

		if ($clog !~ /^Version:\s*(.+)\s*$/mi) {
			$self->log_error("dpkg-parsechangelog didn't print Version:\n");
			Sbuild::Exception::Build->throw(
				error     => "dpkg-parsechangelog didn't print Version:",
				failstage => "check-unpacked-version"
			);
		}
	}

	{
		my $install_fakeroot = 0;
		# is-rootless was added in 1.22.12
		my $dpkg_version_ok = Dpkg::Version->new("1.22.12");
		if ($self->get('Dpkg Version') >= $dpkg_version_ok) {
			$session->run_command({
				COMMAND  => ['dpkg-buildtree', 'is-rootless'],
				USER     => 'root',
				PRIORITY => 0,
				DIR      => $dscdir
			});
			if ($? != 0) {
				$install_fakeroot = 1;
			}
		} else {
			$install_fakeroot = 1;
		}

		if ($install_fakeroot) {
			$self->log_subsubsection("Install fakeroot");
			$resolver->add_dependencies('FAKEROOT', 'fakeroot:native');
			if (!$resolver->install_deps(0, 'fakeroot', 'FAKEROOT')) {
				Sbuild::Exception::Build->throw(
					error =>
					  "Package build dependencies not satisfied; skipping",
					failstage => "install-deps"
				);
			}
		}
	}

	if ($self->get_conf('CLEAN_APT_CACHE')) {
		$self->log_subsubsection("clean up apt cache");
		$session->run_command({
			COMMAND => ["apt-get", "distclean"],
			USER    => 'root',
		});
		if ($?) {
			$self->log_error("cleaning the apt cache failed with $?\n");
		   # Don't fail as distclean was only introduced in apt 2.7.8 (trixie).
		}
	}

	$self->log_subsubsection("Check disk space");
	chomp(my $current_usage
		  = $session->read_command(
			{ COMMAND => ["du", "-k", "-s", "$dscdir"] }));
	if ($?) {
		$self->log_error("du exited with non-zero exit status $?\n");
		Sbuild::Exception::Build->throw(
			error     => "du exited with non-zero exit status $?",
			failstage => "check-space"
		);
	}
	$current_usage =~ /^(\d+)/;
	$current_usage = $1;
	if ($current_usage) {
		my $pipe
		  = $session->pipe_command({ COMMAND => ["df", "-k", "$dscdir"] });
		my $free;
		while (<$pipe>) {
			$free = (split /\s+/)[3];
		}
		close $pipe;
		if ($?) {
			$self->log_error("df exited with non-zero exit status $?\n");
			Sbuild::Exception::Build->throw(
				error     => "df exited with non-zero exit status $?",
				failstage => "check-space"
			);
		}
		if ($free < 2 * $current_usage && $self->get_conf('CHECK_SPACE')) {
			my $config_path = '~/.config/sbuild/config.pl';
			if (length($ENV{'XDG_CONFIG_HOME'})) {
				$config_path = $ENV{'XDG_CONFIG_HOME'} . '/sbuild/config.pl';
			}
			Sbuild::Exception::Build->throw(
				error => "Disk space is probably not sufficient for building.",
				info  => (
					"Unpacked source needs $current_usage KiB (according to "
					  . "du -k) and you have $free KiB free (according to df -k).\n"
					  . "I: Please make enough room for twice the unpacked source "
					  . "size or disable this check by  setting \$check_space "
					  . "to 0 in your $config_path.\n"
					  . "I: For more information see CHECK_SPACE in sbuild.conf(5)."
				),
				failstage => "check-space"
			);
		} else {
			$self->log("Sufficient free space for build\n");
		}
	}

	my $clogpipe = $session->pipe_command({
		COMMAND  => ['dpkg-parsechangelog'],
		USER     => $self->get_conf('BUILD_USER'),
		PRIORITY => 0,
		DIR      => $dscdir
	});
	if (!$clogpipe) {
		$self->log_error("unable to read from dpkg-parsechangelog\n");
		Sbuild::Exception::Build->throw(
			error     => "unable to read from dpkg-parsechangelog",
			failstage => "check-unpacked-version"
		);
	}

	my $clog = Dpkg::Control->new(type => CTRL_CHANGELOG);
	if (!$clog->parse($clogpipe, "$dscdir/debian/changelog")) {
		$self->log_error("unable to parse debian/changelog\n");
		Sbuild::Exception::Build->throw(
			error     => "unable to parse debian/changelog",
			failstage => "check-unpacked-version"
		);
	}

	close($clogpipe);

	my $name    = $clog->{Source};
	my $version = $clog->{Version};
	my $dists   = $clog->{Distribution};
	my $urgency = $clog->{Urgency};

	if ($dists ne $self->get_conf('DISTRIBUTION')) {
		$self->build_log_colour('yellow',
			"^Distribution: " . $self->get_conf('DISTRIBUTION') . "\$");
	}

	if (   $self->get_conf('BIN_NMU')
		|| $self->get_conf('APPEND_TO_VERSION')
		|| defined $self->get_conf('BIN_NMU_CHANGELOG')) {
		$self->log_subsubsection("Hack binNMU version");

		my $text = $session->read_file("$dscdir/debian/changelog");

		if (!$text) {
			$self->log_error(
				"Can't open debian/changelog -- no binNMU hack!\n");
			Sbuild::Exception::Build->throw(
				error => "Can't open debian/changelog -- no binNMU hack: $!!",
				failstage => "hack-binNMU"
			);
		}

		my $NMUversion = $self->get('Version');

		my $clogpipe
		  = $session->get_write_file_handle("$dscdir/debian/changelog");

		if (!$clogpipe) {
			$self->log_error(
				"Can't open debian/changelog for binNMU hack: $!\n");
			Sbuild::Exception::Build->throw(
				error     => "Can't open debian/changelog for binNMU hack: $!",
				failstage => "hack-binNMU"
			);
		}
		if (defined $self->get_conf('BIN_NMU_CHANGELOG')) {
			# Use the changelog entry supplied via --binNMU-changelog
			my $clogentry = $self->get_conf('BIN_NMU_CHANGELOG');
			# trim leading and trailing whitespace and linebreaks
			$clogentry =~ s/^\s+|\s+$//g;
			print $clogpipe $clogentry . "\n\n";
		} else {
			# Craft our own debian/changelog entry
			if (!$self->get_conf('MAINTAINER_NAME')) {
				Sbuild::Exception::Build->throw(
					error => "No maintainer specified.",
					info  =>
'When making changelog additions for a binNMU or appending a version suffix, a maintainer must be specified for the changelog entry e.g. using $maintainer_name (or the --maintainer option)',
					failstage => "check-space"
				);
			}

			$dists = $self->get_conf('DISTRIBUTION');

			print $clogpipe "$name ($NMUversion) $dists; urgency=low";
			if ((defined $self->get_conf('BIN_NMU_VERSION'))
				&& $self->get_conf('BIN_NMU_VERSION')) {
				# Do not make it binary-only=yes if --binNMU=0.
				# Only if BIN_NMU_VERSION is defined and neither empty nor
				# zero is the +bN suffix appended.
				print $clogpipe ", binary-only=yes";
			}
			print $clogpipe "\n\n";
			if ($self->get_conf('APPEND_TO_VERSION')) {
				print $clogpipe "  * Append ",
				  $self->get_conf('APPEND_TO_VERSION'),
				  " to version number; no source changes\n";
			}
			if ($self->get_conf('BIN_NMU')) {
				print $clogpipe
				  "  * Binary-only non-maintainer upload for $host_arch; ",
				  "no source changes.\n";
				print $clogpipe "  * ",
				  join("    ", split("\n", $self->get_conf('BIN_NMU'))), "\n";
			}
			print $clogpipe "\n";

			# Earlier implementations used the date of the last changelog
			# entry for the new entry so that Multi-Arch:same packages would
			# be co-installable (their shared changelogs had to match). This
			# is not necessary anymore as binNMU changelogs are now written
			# into architecture specific paths. Re-using the date of the last
			# changelog entry has the disadvantage that this will effect
			# SOURCE_DATE_EPOCH which in turn will make the timestamps of the
			# files in the new package equal to the last version which can
			# confuse backup programs.  By using the build date for the new
			# binNMU changelog timestamp we make sure that the timestamps of
			# changed files inside the new package advanced in comparison to
			# the last version.
			#
			# The timestamp format has to follow Debian Policy §4.4 which is
			# the same format as `date -R`

			my $date;
			if (defined $self->get_conf('BIN_NMU_TIMESTAMP')) {
				if ($self->get_conf('BIN_NMU_TIMESTAMP') =~ /^\+?[1-9]\d*$/) {
					$date = strftime_c "%a, %d %b %Y %H:%M:%S +0000",
					  gmtime($self->get_conf('BIN_NMU_TIMESTAMP'));
				} else {
					$date = $self->get_conf('BIN_NMU_TIMESTAMP');
				}
			} else {
				$date = strftime_c "%a, %d %b %Y %H:%M:%S +0000",
				  gmtime($self->get('Pkg Start Time'));
			}
			print $clogpipe " -- "
			  . $self->get_conf('MAINTAINER_NAME')
			  . "  $date\n\n";
		}
		print $clogpipe $text;
		close($clogpipe);
		$self->log("Created changelog entry for binNMU version $NMUversion\n");
	}

	if ($session->test_regular_file("$dscdir/debian/files")) {
		local (*FILES);
		my @lines;
		my $FILES = $session->get_read_file_handle("$dscdir/debian/files");
		chomp(@lines = <$FILES>);
		close($FILES);

		$self->log_warning(
"After unpacking, there exists a file debian/files with the contents:\n"
		);

		$self->log_sep();
		foreach (@lines) {
			$self->log($_);
		}
		$self->log_sep();
		$self->log("\n");

		$self->log_info("This should be reported as a bug.\n");
		$self->log_info(
			"The file has been removed to avoid dpkg-genchanges errors.\n");

		unlink "$dscdir/debian/files";
	}

	# Build tree not writable during build (except for the sbuild
	# user performing the build).
	if (!$session->chmod($self->get('Build Dir'), 'go-w', { RECURSIVE => 1 }))
	{
		$self->log_error(
			"chmod og-w " . $self->get('Build Dir') . " failed.\n");
		return 0;
	}

	if (!$self->run_external_commands("starting-build-commands")) {
		Sbuild::Exception::Build->throw(
			error     => "Failed to execute starting-build-commands",
			failstage => "run-starting-build-commands"
		);
	}

	$self->set('Build Start Time', time);
	$self->set('Build End Time',   $self->get('Build Start Time'));

	if ($session->test_regular_file("/etc/ld.so.conf")
		&& !$session->test_regular_file_readable("/etc/ld.so.conf")) {
		$session->chmod('/etc/ld.so.conf', 'a+r');

		$self->log_subsubsection("Fix ld.so");
		$self->log("ld.so.conf was not readable! Fixed.\n");
	}

	my $buildcmd = [];
	if (length $self->get_conf('BUILD_ENV_CMND')) {
		push(@{$buildcmd}, $self->get_conf('BUILD_ENV_CMND'));
	}
	push(@{$buildcmd}, 'dpkg-buildpackage');

	# since dpkg 1.20.0
	# will reset environment and umask to their vendor specific defaults
	if ($resolver->get_dpkg_version() >= "1.20.0") {
		push(@{$buildcmd}, '--sanitize-env');
	}

	if ($host_arch ne $build_arch) {
		push(@{$buildcmd}, '-a' . $host_arch);
	}

	if (length $self->get_conf('BUILD_PROFILES')) {
		my $profiles = $self->get_conf('BUILD_PROFILES');
		$profiles =~ tr/ /,/;
		push(@{$buildcmd}, '-P' . $profiles);
	}

	if (defined $self->get_conf('PGP_OPTIONS')) {
		if (ref($self->get_conf('PGP_OPTIONS')) eq 'ARRAY') {
			push(@{$buildcmd}, @{ $self->get_conf('PGP_OPTIONS') });
		} elsif (length $self->get_conf('PGP_OPTIONS')) {
			push(@{$buildcmd}, $self->get_conf('PGP_OPTIONS'));
		}
	}

	if (defined $self->get_conf('SIGNING_OPTIONS')) {
		if (ref($self->get_conf('SIGNING_OPTIONS')) eq 'ARRAY') {
			push(@{$buildcmd}, @{ $self->get_conf('SIGNING_OPTIONS') });
		} elsif (length $self->get_conf('SIGNING_OPTIONS')) {
			push(@{$buildcmd}, $self->get_conf('SIGNING_OPTIONS'));
		}
	}

	use constant dpkgopt =>
	  [[["", "-B"], ["-A", "-b"]], [["-S", "-G"], ["-g", ""]]];
	my $binopt = dpkgopt->[$self->get_conf('BUILD_SOURCE')]
	  [$self->get_conf('BUILD_ARCH_ALL')][$self->get_conf('BUILD_ARCH_ANY')];
	push(@{$buildcmd}, $binopt) if $binopt;

	if ($self->get_conf('DPKG_FILE_SUFFIX')) {
		my $dpkg_version_ok = Dpkg::Version->new("1.18.11");
		if ($self->get('Dpkg Version') >= $dpkg_version_ok) {
			my $changes = $self->get_changes();
			push(@{$buildcmd}, "--changes-option=-O../$changes");
			my $buildinfo = $self->get_buildinfo();
			push(@{$buildcmd}, "--buildinfo-option=-O../$buildinfo");
		} else {
			$self->log("Ignoring dpkg file suffix: dpkg version too old\n");
			$self->set_conf('DPKG_FILE_SUFFIX', undef);
		}
	}

	if (defined $self->get_conf('DPKG_BUILDPACKAGE_USER_OPTIONS')) {
		push(
			@{$buildcmd},
			@{ $self->get_conf('DPKG_BUILDPACKAGE_USER_OPTIONS') });
	}

	# Set up additional build environment variables.
	my %buildenv = %{ $self->get_conf('BUILD_ENVIRONMENT') };
	$buildenv{'PATH'}            = $self->get_conf('PATH');
	$buildenv{'LD_LIBRARY_PATH'} = $self->get_conf('LD_LIBRARY_PATH')
	  if defined($self->get_conf('LD_LIBRARY_PATH'));

	# Add cross environment config
	if ($host_arch ne $build_arch) {
		$buildenv{'CONFIG_SITE'}
		  = "/etc/dpkg-cross/cross-config." . $host_arch;
		# when cross-building, only set "nocheck" if DEB_BUILD_OPTIONS
		# was not already set. This allows overwriting the default by
		# setting the DEB_BUILD_OPTIONS environment variable
		if (!defined($ENV{'DEB_BUILD_OPTIONS'})) {
			$ENV{'DEB_BUILD_OPTIONS'} = "nocheck";
		}
	}

	# Explicitly add any needed environment to the environment filter
	# temporarily for dpkg-buildpackage.
	my @env_filter;
	foreach my $envvar (keys %buildenv) {
		push(@env_filter, "^$envvar\$");
	}

	# Dump build environment
	$self->log_subsubsection("User Environment");
	{
		my $envcmd = $session->read_command({
			COMMAND    => ['env'],
			ENV        => \%buildenv,
			ENV_FILTER => \@env_filter,
			USER       => $self->get_conf('BUILD_USER'),
			SETSID     => 1,
			PRIORITY   => 0,
			DIR        => $dscdir
		});
		if (!$envcmd) {
			$self->log_error("unable to open pipe\n");
			Sbuild::Exception::Build->throw(
				error     => "unable to open pipe",
				failstage => "dump-build-env"
			);
		}

		my @lines = sort(split /\n/, $envcmd);
		foreach my $line (@lines) {
			$self->log("$line\n");
		}
	}

	$self->log_subsubsection("dpkg-buildpackage");
	$self->log("Command: " . join(' ', @{$buildcmd}) . "\n");

	my $dpkg_build_user = $self->get_conf('BUILD_USER');
	if ($self->get_conf('BUILD_AS_ROOT_WHEN_NEEDED')) {
		# is-rootless was added in 1.22.12
		my $dpkg_version_ok = Dpkg::Version->new("1.22.12");
		if ($self->get('Dpkg Version') >= $dpkg_version_ok) {
			$session->run_command({
				COMMAND  => ['dpkg-buildtree', 'is-rootless'],
				USER     => 'root',
				PRIORITY => 0,
				DIR      => $dscdir
			});
			if ($? != 0) {
				$dpkg_build_user = "root";
				# root needs access to the build path
				# this fixes the build of smartlist 3.15-28
				$session->chmod($self->get_conf('BUILD_PATH'), "a+rx");
			}
		} else {
			# temporary detect R³ manually until all affected
			# packages have been rebuild with a new dpkg.
			# This does not catch cases like dpkg-build-api (= 1)
			# so the dpkg-buildtree version above is more generic
			# but not always available.
			my $F = $session->get_read_file_handle("$dscdir/debian/control");
			if (!$F) {
				$self->log_error(
					"cannot get read file handle for $dscdir/debian/control\n"
				);
				Sbuild::Exception::Build->throw(
					error =>
					  "cannot get read file handle for $dscdir/debian/control",
					failstage => "parse-control"
				);
			}
			while (my $line = <$F>) {
				if ($line =~ /^Rules-Requires-Root:\s*binary-targets/i) {
					$dpkg_build_user = "root";
				}
			}
			close $F;
		}
	}

	my $command = {
		COMMAND        => $buildcmd,
		ENV            => \%buildenv,
		ENV_FILTER     => \@env_filter,
		USER           => $dpkg_build_user,
		SETSID         => 1,
		PRIORITY       => 0,
		DIR            => $dscdir,
		STREAMERR      => \*STDOUT,
		ENABLE_NETWORK => $self->get_conf('ENABLE_NETWORK'),
	};

	my $pipe = $session->pipe_command($command);
	if (!$pipe) {
		$self->log_error("unable to open pipe\n");
		Sbuild::Exception::Build->throw(
			error     => "unable to open pipe",
			failstage => "dpkg-buildpackage"
		);
	}

	$self->set('dpkg-buildpackage pid', $command->{'PID'});
	$self->set('Sub Task',              "dpkg-buildpackage");

	my $timeout = $self->get_conf('INDIVIDUAL_STALLED_PKG_TIMEOUT')->{$pkg}
	  || $self->get_conf('STALLED_PKG_TIMEOUT');
	$timeout *= 60;
	my $timed_out = 0;
	my (@timeout_times, @timeout_sigs, $last_time);

	local $SIG{'ALRM'} = sub {
		my $pid    = $self->get('dpkg-buildpackage pid');
		my $signal = ($timed_out > 0) ? "KILL" : "TERM";
		# negative pid to send to whole process group
		kill "$signal", -$pid;

		$timeout_times[$timed_out] = time - $last_time;
		$timeout_sigs[$timed_out]  = $signal;
		$timed_out++;
		$timeout = 5 * 60;    # only wait 5 minutes until next signal
	};

	alarm($timeout);
	# We do not use a while(<$pipe>) {} loop because that one would only read
	# full lines (until $/ is reached). But we do not want to tie "activity"
	# to receiving complete lines on standard output and standard error.
	# Receiving any data should be sufficient for a process to signal that it
	# is still active. Thus, instead of reading lines, we use sysread() which
	# will return us data once it is available even if the data is not
	# terminated by a newline. To still print correctly to the log, we collect
	# unterminated strings into an accumulator and print them to the log once
	# the newline shows up. This has the added advantage that we can now not
	# only treat \n as producing new lines ($/ is limited to a single
	# character) but can also produce new lines when encountering a \r as it
	# is common for progress-meter output of long-running processes.
	my $acc = "";
	while (1) {
		alarm($timeout);
		$last_time = time;
		# The buffer size is really arbitrary and just makes sure not to call
		# this function too often if lots of data is produced by the build.
		# The function will immediately return even with less data than the
		# buffer size once it is available.
		my $ret = sysread($pipe, my $buf, 1024);
		# sysread failed - this for example happens when the build timeouted
		# and is killed as a result
		if (!defined $ret) {
			last;
		}
		# A return value of 0 signals EOF
		if ($ret == 0) {
			last;
		}
		# We choose that lines shall not only be terminated by \n but that new
		# log lines are also produced after encountering a \r.
		# A negative limit is used to also produce trailing empty fields if
		# required (think of multiple trailing empty lines).
		my @parts    = split /\r|\n/, $buf, -1;
		my $numparts = scalar @parts;
		if ($numparts == 1) {
			# line terminator was not found
			$acc .= $buf;
		} elsif ($numparts >= 2) {
			# first match needs special treatment as it needs to be
			# concatenated with $acc
			my $first = shift @parts;
			$self->log($acc . $first . "\n");
			my $last = pop @parts;
			for (my $i = 0 ; $i < $numparts - 2 ; $i++) {
				$self->log($parts[$i] . "\n");
			}
			# the last part is put into the accumulator. This might
			# just be the empty string if $buf ended in a line
			# terminator
			$acc = $last;
		}
	}
	# If the output didn't end with a line terminator, just print out the rest
	# as we have it.
	if ($acc ne "") {
		$self->log($acc . "\n");
	}
	close($pipe);
	alarm(0);
	$rv = $?;
	$self->set('dpkg-buildpackage pid', undef);

	my $i;
	for ($i = 0 ; $i < $timed_out ; ++$i) {
		$self->log_error("Build killed with signal "
			  . $timeout_sigs[$i]
			  . " after "
			  . int($timeout_times[$i] / 60)
			  . " minutes of inactivity\n");
	}
	$self->set('Build End Time', time);
	$self->set('Pkg End Time',   time);
	$self->set('This Time',
		$self->get('Pkg End Time') - $self->get('Pkg Start Time'));
	$self->set('This Time', 0) if $self->get('This Time') < 0;

	$self->write_stats('build-time',
		$self->get('Build End Time') - $self->get('Build Start Time'));
	$self->write_stats('install-download-time',
		$self->get('Install End Time') - $self->get('Install Start Time'));
	my $finish_date = strftime_c "%FT%TZ",
	  gmtime($self->get('Build End Time'));
	$self->log_sep();
	$self->log("Build finished at $finish_date\n");

	if (!$self->run_external_commands("finished-build-commands")) {
		Sbuild::Exception::Build->throw(
			error     => "Failed to execute finished-build-commands",
			failstage => "run-finished-build-commands"
		);
	}

	my @space_files = ();

	$self->log_subsubsection("Finished");

	if ($rv != 0) {
		my $msg = '';
		if (POSIX::WIFEXITED($rv)) {
			my $ret = POSIX::WEXITSTATUS($rv);
			$msg = "exit $ret";
		} elsif (POSIX::WIFSIGNALED($rv)) {
			my $sig = POSIX::WTERMSIG($rv);
			$msg = "signal $sig";
		} else {
			$msg = "unknown status $rv";
		}
		Sbuild::Exception::Build->throw(
			error     => "Build failure (dpkg-buildpackage died with $msg)",
			failstage => "build"
		);
		$self->set('This Space', $self->check_space(@space_files));
		return 0;
	}

	$self->log_info("Built successfully\n");

	if ($session->test_regular_file_readable("$dscdir/debian/files")) {
		my @files = $self->debian_files_list("$dscdir/debian/files");

		foreach (@files) {
			if (!$session->test_regular_file("$build_dir/$_")) {
				$self->log_error("Package claims to have built "
					  . basename($_)
					  . ", but did not.  This is a bug in the packaging.\n");
				next;
			}
			if (/_all.u?deb$/ and not $self->get_conf('BUILD_ARCH_ALL')) {
				$self->log_error("Package builds "
					  . basename($_)
					  . " when binary-indep target is not called.  This is a bug in the packaging.\n"
				);
				$session->unlink("$build_dir/$_");
				next;
			}
		}
	}

	# Restore write access to build tree now build is complete.
	if (!$session->chmod($self->get('Build Dir'), 'g+w', { RECURSIVE => 1 })) {
		$self->log_error(
			"chmod g+w " . $self->get('Build Dir') . " failed.\n");
		return 0;
	}

	if (!grep { $_ eq "changes" } @{ $self->get_conf('LOG_HIDDEN_SECTIONS') })
	{
		$self->log_subsection_t("Changes", time);
	}

	# we use an anonymous subroutine so that the referenced variables are
	# automatically rebound to their current values
	my $copy_changes = sub {
		my $changes = shift;

		my $F = $session->get_read_file_handle("$build_dir/$changes");
		if (!$F) {
			$self->log_error(
				"cannot get read file handle for $build_dir/$changes\n");
			Sbuild::Exception::Build->throw(
				error => "cannot get read file handle for $build_dir/$changes",
				failstage => "parse-changes"
			);
		}
		my $pchanges = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
		if (!$pchanges->parse($F, "$build_dir/$changes")) {
			$self->log_error("cannot parse $build_dir/$changes\n");
			Sbuild::Exception::Build->throw(
				error     => "cannot parse $build_dir/$changes",
				failstage => "parse-changes"
			);
		}
		close($F);

		if ($self->get_conf('OVERRIDE_DISTRIBUTION')) {
			$pchanges->{Distribution} = $self->get_conf('DISTRIBUTION');
		}

		my $sys_build_dir = $self->get_conf('BUILD_DIR');
		my $F2 = $session->get_write_file_handle("$build_dir/$changes.new");
		if (!$F2) {
			$self->log("Cannot create $build_dir/$changes.new\n");
			$self->log("Distribution field may be wrong!!!\n");
			if ($build_dir) {
				if (!$session->copy_from_chroot("$build_dir/$changes", ".")) {
					$self->log_error(
						"Could not copy $build_dir/$changes to .\n");
				}
			}
		} else {
			if (!grep { $_ eq "changes" }
				@{ $self->get_conf('LOG_HIDDEN_SECTIONS') }) {
				$pchanges->output(\*STDOUT);
			}
			$pchanges->output(\*$F2);

			close($F2);

			$session->rename("$build_dir/$changes.new", "$build_dir/$changes");
			if ($?) {
				$self->log("$build_dir/$changes.new could not be "
					  . "renamed to $build_dir/$changes: $?\n");
				$self->log("Distribution field may be wrong!!!");
			}
			if ($build_dir) {
				if (
					!$session->copy_from_chroot(
						"$build_dir/$changes", "$sys_build_dir"
					)
				) {
					$self->log(
						"Could not copy $build_dir/$changes to $sys_build_dir"
					);
				}
			}
		}

		return $pchanges;
	};

	$changes = $self->get_changes();
	if (!defined($changes)) {
		$self->log_error(".changes is undef. Cannot copy build results.\n");
		return 0;
	}
	my @cfiles;
	if ($session->test_regular_file_readable("$build_dir/$changes")) {
		my (@do_dists, @saved_dists);
		if (!grep { $_ eq "changes" }
			@{ $self->get_conf('LOG_HIDDEN_SECTIONS') }) {
			$self->log_subsubsection("$changes:");
		}

		my $pchanges = &$copy_changes($changes);
		$self->set('Changes File', $self->get_conf('BUILD_DIR') . "/$changes");

		my $checksums = Dpkg::Checksums->new();
		$checksums->add_from_control($pchanges);

		push(@cfiles, $checksums->get_files());

	} else {
		$self->log_error("Can't find $changes -- can't dump info\n");
	}

	if ($self->get_conf('SOURCE_ONLY_CHANGES')) {
		my $so_changes = $self->get('Package_SVersion') . "_source.changes";
		$self->log_subsubsection("$so_changes:");
		my $genchangescmd = ['dpkg-genchanges', '--build=source'];
		if (defined $self->get_conf('SIGNING_OPTIONS')) {
			if (ref($self->get_conf('SIGNING_OPTIONS')) eq 'ARRAY') {
				push(
					@{$genchangescmd},
					@{ $self->get_conf('SIGNING_OPTIONS') });
			} elsif (length $self->get_conf('SIGNING_OPTIONS')) {
				push(@{$genchangescmd}, $self->get_conf('SIGNING_OPTIONS'));
			}
		}
		my $changes_opts = $self->get_changes_opts();
		if ($changes_opts) {
			push(@{$genchangescmd}, @{$changes_opts});
		}
		my $cfile = $session->read_command({
			COMMAND  => $genchangescmd,
			USER     => $self->get_conf('BUILD_USER'),
			PRIORITY => 0,
			DIR      => $dscdir
		});
		if (!$cfile) {
			$self->log_error("dpkg-genchanges --build=source failed\n");
			Sbuild::Exception::Build->throw(
				error     => "dpkg-genchanges --build=source failed",
				failstage => "source-only-changes"
			);
		}
		if (!$session->write_file("$build_dir/$so_changes", $cfile)) {
			$self->log_error(
				"cannot write content to $build_dir/$so_changes\n");
			Sbuild::Exception::Build->throw(
				error     => "cannot write content to $build_dir/$so_changes",
				failstage => "source-only-changes"
			);
		}

		my $pchanges = &$copy_changes($so_changes);
	}

	if (!grep { $_ eq "buildinfo" }
		@{ $self->get_conf('LOG_HIDDEN_SECTIONS') }) {
		$self->log_subsection_t("Buildinfo", time);

		foreach (@cfiles) {
			my $deb = "$build_dir/$_";
			next if $deb !~ /\.buildinfo$/;
			my $buildinfo = $session->read_file($deb);
			if (!$buildinfo) {
				$self->log_error("Cannot read $deb\n");
			} else {
				$self->log($buildinfo);
				$self->log("\n");
			}
		}
	}

	if (!grep { $_ eq "contents" } @{ $self->get_conf('LOG_HIDDEN_SECTIONS') })
	{
		$self->log_subsection_t("Package contents", time);

		my @debcfiles = @cfiles;
		foreach (@debcfiles) {
			my $deb = "$build_dir/$_";
			next if $deb !~ /(\Q$host_arch\E|all)\.(udeb|deb)$/;

			$self->log_subsubsection("$_");
			my $dpkg_info
			  = $session->read_command(
				{ COMMAND => ["dpkg", "--info", $deb] });
			if (!$dpkg_info) {
				$self->log_error("Can't spawn dpkg: $! -- can't dump info\n");
			} else {
				$self->log($dpkg_info);
			}
			$self->log("\n");
			my $dpkg_contents = $session->read_command({
					COMMAND =>
					  ["sh", "-c", "dpkg --contents $deb 2>&1 | sort -k6"] });
			if (!$dpkg_contents) {
				$self->log_error("Can't spawn dpkg: $! -- can't dump info\n");
			} else {
				$self->log($dpkg_contents);
			}
			$self->log("\n");
		}
	}

	foreach (@cfiles) {
		push(@space_files, $self->get_conf('BUILD_DIR') . "/$_");
		if (
			!$session->copy_from_chroot(
				"$build_dir/$_", $self->get_conf('BUILD_DIR'))
		) {
			$self->log_error("Could not copy $build_dir/$_ to "
				  . $self->get_conf('BUILD_DIR')
				  . "\n");
		}
	}

	$self->set('This Space', $self->check_space(@space_files));

	return 1;
}

# Produce a hash suitable for ENV export
sub get_env ($$) {
	my $self   = shift;
	my $prefix = shift;

	sub _env_loop ($$$$) {
		my ($env, $ref, $keysref, $prefix) = @_;

		foreach my $key (keys(%{$keysref})) {
			my $value = $ref->get($key);
			next if (!defined($value));
			next if (ref($value));
			my $name = "${prefix}${key}";
			$name =~ s/ /_/g;
			$env->{$name} = $value;
		}
	}

	my $envlist = {};
	_env_loop($envlist, $self, $self, $prefix);
	_env_loop(
		$envlist,
		$self->get('Config'),
		$self->get('Config')->{'KEYS'},
		"${prefix}CONF_"
	);
	return $envlist;
}

sub get_build_filename {
	my $self     = shift;
	my $filetype = shift;
	my $changes  = $self->get('Package_SVersion');

	if ($self->get_conf('BUILD_ARCH_ANY')) {
		$changes .= '_' . $self->get('Host Arch');
	} elsif ($self->get_conf('BUILD_ARCH_ALL')) {
		$changes .= "_all";
	} elsif ($self->get_conf('BUILD_SOURCE')) {
		$changes .= "_source";
	}

	my $suffix = $self->get_conf('DPKG_FILE_SUFFIX');
	$changes .= $suffix if ($suffix);

	$changes .= '.' . $filetype;

	return $changes;
}

sub get_changes {
	my $self = shift;
	return $self->get_build_filename("changes");
}

sub get_buildinfo {
	my $self = shift;
	return $self->get_build_filename("buildinfo");
}

sub check_space {
	my $self  = shift;
	my @files = @_;
	my $sum   = 0;

	my $dscdir = $self->get('DSC Dir');
	return -1 unless (defined $dscdir);

	my $build_dir   = $self->get('Build Dir');
	my $pkgbuilddir = "$build_dir/$dscdir";

   # if the source package was not yet unpacked, we will not attempt to compute
   # the required space.
	return -1 unless ($self->get('Session')->test_directory($pkgbuilddir));

	my ($space, $spacenum);

	# get the required space for the unpacked source package in the chroot
	$space = $self->get('Session')->read_command({
		COMMAND  => ['du', '-k', '-s', $pkgbuilddir],
		USER     => $self->get_conf('BUILD_USER'),
		PRIORITY => 0,
		DIR      => '/'
	});

	if (!$space) {
		$self->log_error(
			"Cannot determine space needed for $pkgbuilddir (du failed)\n");
		return -1;
	}
	# remove the trailing path from the du output
	if (($spacenum) = $space =~ /^(\d+)/) {
		$sum += $spacenum;
	} else {
		$self->log_error(
"Cannot determine space needed for $pkgbuilddir (unexpected du output): $space\n"
		);
		return -1;
	}

	# get the required space for all produced build artifacts on the host
	# running sbuild
	foreach my $file (@files) {
		$space = $self->get('Host')->read_command({
			COMMAND  => ['du', '-k', '-s', $file],
			USER     => $self->get_conf('USERNAME'),
			PRIORITY => 0,
			DIR      => '/'
		});

		if (!$space) {
			$self->log_error(
				"Cannot determine space needed for $file (du failed): $!\n");
			return -1;
		}
		# remove the trailing path from the du output
		if (($spacenum) = $space =~ /^(\d+)/) {
			$sum += $spacenum;
		} else {
			$self->log_error(
"Cannot determine space needed for $file (unexpected du output): $space\n"
			);
			return -1;
		}
	}

	return $sum;
}

sub lock_file {
	my $self       = shift;
	my $file       = shift;
	my $for_srcdep = shift;
	my $lockfile   = "$file.lock";
	my $try        = 0;

  repeat:
	if (!sysopen(F, $lockfile, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0644)) {
		if ($! == EEXIST) {
			# lock file exists, wait
			goto repeat if !open(F, "<$lockfile");
			my $line = <F>;
			my ($pid, $user);
			close(F);
			if ($line !~ /^(\d+)\s+([\w\d.-]+)$/) {
				$self->log_warning(
					"Bad lock file contents ($lockfile) -- still trying\n");
			} else {
				($pid, $user) = ($1, $2);
				if (kill(0, $pid) == 0 && $! == ESRCH) {
					# process doesn't exist anymore, remove stale lock
					$self->log_warning("Removing stale lock file $lockfile "
						  . "(pid $pid, user $user)\n");
					unlink($lockfile);
					goto repeat;
				}
			}
			++$try;
			if (!$for_srcdep && $try > $self->get_conf('MAX_LOCK_TRYS')) {
				$self->log_warning("Lockfile $lockfile still present after "
					  . $self->get_conf('MAX_LOCK_TRYS')
					  * $self->get_conf('LOCK_INTERVAL')
					  . " seconds -- giving up\n");
				return;
			}
			$self->log(
"Another sbuild process ($pid by $user) is currently installing or removing packages -- waiting...\n"
			) if $for_srcdep && $try == 1;
			sleep $self->get_conf('LOCK_INTERVAL');
			goto repeat;
		}
		$self->log_warning("Can't create lock file $lockfile: $!\n");
	}

	my $username = $self->get_conf('USERNAME');
	F->print("$$ $username\n");
	F->close();
}

sub unlock_file {
	my $self     = shift;
	my $file     = shift;
	my $lockfile = "$file.lock";

	unlink($lockfile);
}

sub add_stat {
	my $self  = shift;
	my $key   = shift;
	my $value = shift;

	$self->get('Summary Stats')->{$key} = $value;
}

sub generate_stats {
	my $self     = shift;
	my $resolver = $self->get('Dependency Resolver');

	$self->add_stat('Job',     $self->get('Job'));
	$self->add_stat('Package', $self->get('Package'));
	# If the package fails early, then the version might not yet be known.
	# This can happen if the user only specified a source package name on the
	# command line and then the version will only be known after the source
	# package was successfully downloaded.
	if ($self->get('Version')) {
		$self->add_stat('Version', $self->get('Version'));
	}
	if ($self->get('OVersion')) {
		$self->add_stat('Source-Version', $self->get('OVersion'));
	}
	$self->add_stat('Machine Architecture', $self->get_conf('ARCH'));
	$self->add_stat('Host Architecture',    $self->get('Host Arch'));
	$self->add_stat('Build Architecture',   $self->get('Build Arch'));
	$self->add_stat('Build Profiles',       $self->get('Build Profiles'))
	  if $self->get('Build Profiles');
	$self->add_stat('Build Type', $self->get('Build Type'));
	my @keylist;
	if (defined $resolver) {
		@keylist = keys %{ $resolver->get('Initial Foreign Arches') };
		push @keylist, keys %{ $resolver->get('Added Foreign Arches') };
	}
	my $foreign_arches = join ' ', @keylist;
	$self->add_stat('Foreign Architectures', $foreign_arches)
	  if $foreign_arches;
	$self->add_stat('Distribution', $self->get_conf('DISTRIBUTION'));
	if ($self->get('This Space') >= 0) {
		$self->add_stat('Space', $self->get('This Space'));
	} else {
		$self->add_stat('Space', "n/a");
	}
	$self->add_stat('Build-Time',
		$self->get('Build End Time') - $self->get('Build Start Time'));
	$self->add_stat('Install-Time',
		$self->get('Install End Time') - $self->get('Install Start Time'));
	$self->add_stat('Package-Time',
		$self->get('Pkg End Time') - $self->get('Pkg Start Time'));
	if ($self->get('This Space') >= 0) {
		$self->add_stat('Build-Space', $self->get('This Space'));
	} else {
		$self->add_stat('Build-Space', "n/a");
	}
	$self->add_stat('Status',     $self->get_status());
	$self->add_stat('Fail-Stage', $self->get('Pkg Fail Stage'))
	  if ($self->get_status() ne "successful");
	$self->add_stat('Lintian', $self->get('Lintian Reason'))
	  if $self->get('Lintian Reason');
	$self->add_stat('Piuparts', $self->get('Piuparts Reason'))
	  if $self->get('Piuparts Reason');
	$self->add_stat('Autopkgtest', $self->get('Autopkgtest Reason'))
	  if $self->get('Autopkgtest Reason');
}

sub log_stats {
	my $self = shift;
	foreach my $stat (sort keys %{ $self->get('Summary Stats') }) {
		$self->log("${stat}: " . $self->get('Summary Stats')->{$stat} . "\n");
	}
}

sub print_stats {
	my $self = shift;
	foreach my $stat (sort keys %{ $self->get('Summary Stats') }) {
		print STDOUT "${stat}: " . $self->get('Summary Stats')->{$stat} . "\n";
	}
}

sub write_stats {
	my $self = shift;

	return if (!$self->get_conf('BATCH_MODE'));

	my $stats_dir = $self->get_conf('STATS_DIR');

	return if not defined $stats_dir;

	if (   !-d $stats_dir
		&& !mkdir $stats_dir) {
		$self->log_warning("Could not create $stats_dir: $!\n");
		return;
	}

	my ($cat, $val) = @_;
	local (*F);

	$self->lock_file($stats_dir, 0);
	open(F, ">>$stats_dir/$cat");
	print F "$val\n";
	close(F);
	$self->unlock_file($stats_dir);
}

sub debian_files_list {
	my $self  = shift;
	my $files = shift;

	my @list;

	debug("Parsing $files\n");
	my $session = $self->get('Session');

	my $pipe = $session->get_read_file_handle($files);
	if ($pipe) {
		while (<$pipe>) {
			chomp;
			my $f = (split(/\s+/, $_))[0];
			push(@list, "$f");
			debug("  $f\n");
		}
		close($pipe)
		  or $self->log_error("Failed to close $files\n") && return 1;
	}

	return @list;
}

# Figure out chroot architecture
sub chroot_arch {
	my $self = shift;

	chomp(
		my $chroot_arch = $self->get('Session')->read_command({
				COMMAND  => ['dpkg', '--print-architecture'],
				USER     => $self->get_conf('BUILD_USER'),
				PRIORITY => 0,
				DIR      => '/'
			}));

	if (!$chroot_arch) {
		Sbuild::Exception::Build->throw(
			error     => "Can't determine architecture of chroot: $!",
			failstage => "chroot-arch"
		);
	}

	return $chroot_arch;
}

sub build_log_filter {
	my $self        = shift;
	my $text        = shift;
	my $replacement = shift;

	if ($self->get_conf('LOG_FILTER')) {
		$self->log(
			$self->get('FILTER_PREFIX') . $text . ':' . $replacement . "\n");
	}
}

sub build_log_colour {
	my $self   = shift;
	my $regex  = shift;
	my $colour = shift;

	if ($self->get_conf('LOG_COLOUR')) {
		$self->log(
			$self->get('COLOUR_PREFIX') . $colour . ':' . $regex . "\n");
	}
}

# Define, and return the full path of the log file (on the host), and
# store its basename (to support %SBUILD_LOG_BASENAME).
# The format of the basename is:
#    %SRCPACKAGE[_VERSION]_%SBUILD_HOST_ARCH-STARTTIME[.build]
# where:
#    START_TIME is when the build started (YYYY-MM-DDTHH:MM:SS<TZ>)
#    %SRCPACKAGE[_VERSION] is the source package being built: if this
#     is not defined then this function returns undef.
# The source VERSION is included if known (omitted if the user only
# specified a package to download), and the '.build' extension is
# omitted in buildd mode.
sub log_file {
	my $self = shift;

	my $date = strftime_c $self->get_conf('LOG_FILENAME_TIMESTAMP_FORMAT'),
	  gmtime($self->get('Pkg Start Time'));
	my $filename = $self->get_conf('LOG_DIR') . '/';
	my $basename = "";

	# we might not know the pkgname_ver string if the user only specified a
	# package name without version
	if ($self->get('Package_SVersion')) {
		$basename .= $self->get('Package_SVersion');
	} else {
		if (!defined $self->get('Package')) {
			warn "W: source package name is not defined";
			return undef;
		}
		$basename .= $self->get('Package');
	}
	$basename .= '_' . $self->get('Host Arch') . "-$date";
	$basename .= ".build" if $self->get_conf('SBUILD_MODE') ne 'buildd';

	$self->set('Log File Basename', $basename);
	$filename .= $basename;
	return $filename;
}

sub open_build_log {
	my $self = shift;

	my $filter_prefix = '__SBUILD_FILTER_' . $$ . ':';
	$self->set('FILTER_PREFIX', $filter_prefix);
	my $colour_prefix = '__SBUILD_COLOUR_' . $$ . ':';
	$self->set('COLOUR_PREFIX', $colour_prefix);

	my $filename = $self->log_file();
	if (!$filename) {
		return 0;
	}

	open($saved_stdout, ">&STDOUT") or warn "Can't redirect stdout\n";
	open($saved_stderr, ">&STDERR") or warn "Can't redirect stderr\n";

	my $PLOG;

	my $pid;
	($pid = open($PLOG, "|-"));
	if (!defined $pid) {
		warn "Cannot open pipe to '$filename': $!\n";
	} elsif ($pid == 0) {
		$SIG{'INT'}  = 'IGNORE';
		$SIG{'TERM'} = 'IGNORE';
		$SIG{'QUIT'} = 'IGNORE';
		$SIG{'PIPE'} = 'IGNORE';

		$saved_stdout->autoflush(1);
		if (  !$self->get_conf('NOLOG')
			&& $self->get_conf('LOG_DIR_AVAILABLE')) {
			unlink $filename;    # To prevent opening symlink to elsewhere
			open(CPLOG, ">$filename")
			  or Sbuild::Exception::Build->throw(
				error     => "Failed to open build log $filename: $!",
				failstage => "init"
			  );
			CPLOG->autoflush(1);

			# Create 'current' symlinks
			if ($self->get_conf('SBUILD_MODE') eq 'buildd') {
				$self->log_symlink($filename,
						$self->get_conf('BUILD_DIR')
					  . '/current-'
					  . $self->get_conf('DISTRIBUTION'));
			} else {
				my $symlinktarget = $filename;
				# if symlink target is in the same directory as the symlink
				# itself, make it a relative link instead of an absolute one
				if (Cwd::abs_path($self->get_conf('BUILD_DIR')) eq
					Cwd::abs_path(dirname($filename))) {
					$symlinktarget = basename($filename);
				}
				my $symlinkname = $self->get_conf('BUILD_DIR') . '/';
		# we might not know the pkgname_ver string if the user only specified a
		# package name without version
				if ($self->get('Package_SVersion')) {
					$symlinkname .= $self->get('Package_SVersion');
				} else {
					$symlinkname .= $self->get('Package');
				}
				$symlinkname .= '_' . $self->get('Host Arch') . ".build";
				$self->log_symlink($symlinktarget, $symlinkname);
			}
		}

		# Cache vars to avoid repeated hash lookups.
		my $nolog      = $self->get_conf('NOLOG');
		my $log        = $self->get_conf('LOG_DIR_AVAILABLE');
		my $verbose    = $self->get_conf('VERBOSE');
		my $log_colour = $self->get_conf('LOG_COLOUR');
		my @filter     = ();
		my @colour     = ();
		my ($text, $replacement);
		my $filter_regex = "^$filter_prefix(.*):(.*)\$";
		my $colour_regex = "^$colour_prefix(.*):(.*)\$";
		my @ignore       = ();

		while (<STDIN>) {
			# Add a replacement pattern to filter (sent from main
			# process in log stream).
			if (m/$filter_regex/) {
				($text, $replacement) = ($1, $2);
				$replacement = "<<$replacement>>";
				push(@filter, [$text, $replacement]);
				$_
				  = "I: NOTICE: Log filtering will replace '$text' with '$replacement'\n";
			} elsif (m/$colour_regex/) {
				my ($colour, $regex);
				($colour, $regex) = ($1, $2);
				push(@colour, [$colour, $regex]);
		  #		$_ = "I: NOTICE: Log colouring will colour '$regex' in $colour\n";
				next;
			} else {
				# Filter out any matching patterns
				foreach my $pattern (@filter) {
					($text, $replacement) = @{$pattern};
					s/\Q$text\E/$replacement/g;
				}
			}
			if (m/Deprecated key/ || m/please update your configuration/) {
				my $skip = 0;
				foreach my $ignore (@ignore) {
					$skip = 1 if ($ignore eq $_);
				}
				next if $skip;
				push(@ignore, $_);
			}

			if ($nolog || $verbose) {
				my $colour = 'reset';
				if (-t $saved_stdout && $log_colour) {
					foreach my $pattern (@colour) {
						if (m/$$pattern[0]/) {
							$colour = $$pattern[1];
						}
					}
					if ($colour ne 'reset') {
						print $saved_stdout color $colour;
					}
				}

				print $saved_stdout $_;
				if (-t $saved_stdout && $log_colour && $colour ne 'reset') {
					print $saved_stdout color 'reset';
				}
			}
			if (!$nolog && $log) {
				print CPLOG $_;
			}
		}

		close CPLOG;
		exit 0;
	}

	$PLOG->autoflush(1);
	open(STDOUT, '>&', $PLOG) or warn "Can't redirect stdout\n";
	open(STDERR, '>&', $PLOG) or warn "Can't redirect stderr\n";
	$self->set('Log File',   $filename);
	$self->set('Log Stream', $PLOG);

	my $hostname = $self->get_conf('HOSTNAME');
	$self->log(
		"sbuild (Debian sbuild) $version ($release_date) on $hostname\n");

	my $arch_string = $self->get('Host Arch');
	my $head        = $self->get('Package');
	if ($self->get('Version')) {
		$head .= ' ' . $self->get('Version');
	}
	$head .= ' (' . $arch_string . ') ';
	$self->log_section_t($head, $self->get('Pkg Start Time'));

	$self->log("Package: " . $self->get('Package') . "\n");
	if (defined $self->get('Version')) {
		$self->log("Version: " . $self->get('Version') . "\n");
	}
	if (defined $self->get('OVersion')) {
		$self->log("Source Version: " . $self->get('OVersion') . "\n");
	}
	$self->log("Distribution: " . $self->get_conf('DISTRIBUTION') . "\n");
	$self->log("Machine Architecture: " . $self->get_conf('ARCH') . "\n");
	$self->log("Host Architecture: " . $self->get('Host Arch') . "\n");
	$self->log("Build Architecture: " . $self->get('Build Arch') . "\n");
	$self->log("Build Profiles: " . $self->get('Build Profiles') . "\n")
	  if $self->get('Build Profiles');
	$self->log("Build Type: " . $self->get('Build Type') . "\n");
	$self->log("\n");
}

sub close_build_log {
	my $self = shift;

	my $time = $self->get('Pkg End Time');
	if ($time == 0) {
		$time = time;
	}
	my $date = strftime_c "%FT%TZ", gmtime($time);

	my $hours   = int($self->get('This Time') / 3600);
	my $minutes = int(($self->get('This Time') % 3600) / 60),
	  my $seconds = int($self->get('This Time') % 60),
	  my $space = "no";
	if ($self->get('This Space') >= 0) {
		$space = sprintf("%dk", $self->get('This Space'));
	}

	my $filename = $self->get('Log File');

	# building status at this point means failure.
	if ($self->get_status() eq "building") {
		$self->set_status('failed');
	}

	if (!grep { $_ eq "summary" } @{ $self->get_conf('LOG_HIDDEN_SECTIONS') })
	{
		$self->log_subsection_t('Summary', time);
		$self->generate_stats();
		$self->log_stats();

		$self->log_sep();
		$self->log("Finished at ${date}\n");
		$self->log(
			sprintf(
				"Build needed %02d:%02d:%02d, %s disk space\n",
				$hours, $minutes, $seconds, $space
			));
	}

	if ($self->get_status() eq "successful") {
		if (length $self->get_conf('KEY_ID')) {
			my $key_id    = $self->get_conf('KEY_ID');
			my $build_dir = $self->get_conf('BUILD_DIR');
			my $changes;
			$self->log(
				sprintf("Signature with key '%s' requested:\n", $key_id));
			$changes = $self->get_changes();
			if (!defined($changes)) {
				$self->log_error(".changes is undef. Cannot sign .changes.\n");
			} else {
				system(
					'debsign',   '--re-sign',
					"-k$key_id", '--',
					"$build_dir/$changes"
				);
			}
			if ($self->get_conf('SOURCE_ONLY_CHANGES')) {
				# We would like to run debsign with --no-re-sign so that a file
				# referenced by the normal changes file and was already signed
				# there does not get changed here by re-signing. Otherwise, the
				# checksum from the normal changes file might not match
				# anymore. https://bugs.debian.org/977674
				#
				# The problem is, that with --no-re-sign, debsign will see a
				# signed buildinfo file and skip signing the dsc.
				# https://bugs.debian.org/981021
				my $so_changes
				  = $build_dir . '/'
				  . $self->get('Package_SVersion')
				  . "_source.changes";
				if (-r $so_changes) {
					system(
						'debsign', '--re-sign', "-k$key_id",
						'--',      "$so_changes"
					);
				} else {
					$self->log_error(
						"$so_changes unreadable. Cannot sign .changes.\n");
				}
			}
		}
	}

	my $subject = "Log for " . $self->get_status() . " build of ";
	if ($self->get('Package_Version')) {
		$subject .= $self->get('Package_Version');
	} else {
		$subject .= $self->get('Package');
	}

	if (   $self->get_conf('BUILD_SOURCE')
		&& !$self->get_conf('BUILD_ARCH_ALL')
		&& !$self->get_conf('BUILD_ARCH_ANY')) {
		$subject .= " source";
	}
	if ($self->get_conf('BUILD_ARCH_ALL')
		&& !$self->get_conf('BUILD_ARCH_ANY')) {
		$subject .= " on all";
	} elsif ($self->get('Host Arch')) {
		$subject .= " on " . $self->get('Host Arch');
	}
	if ($self->get_conf('ARCHIVE')) {
		$subject
		  .= " ("
		  . $self->get_conf('ARCHIVE') . "/"
		  . $self->get_conf('DISTRIBUTION') . ")";
	} else {
		$subject .= " (dist=" . $self->get_conf('DISTRIBUTION') . ")";
	}

	open(STDERR, '>&', $saved_stderr)
	  or warn "Can't redirect stderr\n"
	  if defined($saved_stderr);
	open(STDOUT, '>&', $saved_stdout)
	  or warn "Can't redirect stdout\n"
	  if defined($saved_stdout);
	$saved_stderr->close();
	undef $saved_stderr;
	$saved_stdout->close();
	undef $saved_stdout;
	$self->set('Log File', undef);

	if (defined($self->get('Log Stream'))) {
		$self->get('Log Stream')->close();    # Close child logger process
		$self->set('Log Stream', undef);
	}

	$self->send_build_log($self->get_conf('MAILTO'), $subject, $filename)
	  if ( defined($filename)
		&& -f $filename
		&& $self->get_conf('MAILTO'));
}

sub send_build_log {
	my $self     = shift;
	my $to       = shift;
	my $subject  = shift;
	my $filename = shift;

	my $conf = $self->get('Config');

	if ($conf->get('MIME_BUILD_LOG_MAILS')) {
		return $self->send_mime_build_log($to, $subject, $filename);
	} else {
		return send_mail($conf, $to, $subject, $filename);
	}
}

sub send_mime_build_log {
	my $self     = shift;
	my $to       = shift;
	my $subject  = shift;
	my $filename = shift;

	my $conf = $self->get('Config');
	my $tmp;    # Needed for gzip, here for proper scoping.

	my $msg = MIME::Lite->new(
		From    => $conf->get('MAILFROM'),
		To      => $to,
		Subject => $subject,
		Type    => 'multipart/mixed'
	);

	# Add the GPG key ID to the mail if present so that it's clear if the log
	# still needs signing or not.
	if (length $self->get_conf('KEY_ID')) {
		$msg->add('Key-ID', $self->get_conf('KEY_ID'));
	}

	if (!$conf->get('COMPRESS_BUILD_LOG_MAILS')) {
		my $log_part = MIME::Lite->new(
			Type     => 'text/plain',
			Path     => $filename,
			Filename => basename($filename));
		$log_part->attr('content-type.charset' => 'UTF-8');
		$msg->attach($log_part);
	} else {
		local (*F, *GZFILE);

		if (!open(F, "<$filename")) {
			warn "Cannot open $filename for mailing: $!\n";
			return 0;
		}

		$tmp = File::Temp->new();
		tie *GZFILE, 'IO::Zlib', $tmp->filename, 'wb';

		while (<F>) {
			print GZFILE $_;
		}
		untie *GZFILE;

		close F;
		close GZFILE;

		$msg->attach(
			Type     => 'application/x-gzip',
			Path     => $tmp->filename,
			Filename => basename($filename) . '.gz'
		);
	}
	my $build_dir = $self->get_conf('BUILD_DIR');
	my $changes   = $self->get_changes();
	if ($self->get_status() eq 'successful' && -r "$build_dir/$changes") {
		my $log_part = MIME::Lite->new(
			Type     => 'text/plain',
			Path     => "$build_dir/$changes",
			Filename => basename($changes));
		$log_part->attr('content-type.charset' => 'UTF-8');
		$msg->attach($log_part);
	}

	my $stats = '';
	foreach my $stat (sort keys %{ $self->get('Summary Stats') }) {
		$stats
		  .= sprintf("%s: %s\n", $stat, $self->get('Summary Stats')->{$stat});
	}
	$msg->attach(
		Type     => 'text/plain',
		Filename => basename($filename) . '.summary',
		Data     => $stats
	);

	local $SIG{'PIPE'} = 'IGNORE';

	if (!open(MAIL, "|" . $conf->get('MAILPROG') . " -oem $to")) {
		warn "Could not open pipe to " . $conf->get('MAILPROG') . ": $!\n";
		close(F);
		return 0;
	}

	$msg->print(\*MAIL);

	if (!close(MAIL)) {
		warn $conf->get('MAILPROG') . " failed (exit status $?)\n";
		return 0;
	}
	return 1;
}

sub log_symlink {
	my $self = shift;
	my $log  = shift;
	my $dest = shift;

	unlink $dest;    # Don't return on failure, since the symlink will fail.
	symlink $log, $dest;
}

sub get_changes_opts {
	my $self         = shift;
	my @changes_opts = ();
	foreach (@{ $self->get_conf('DPKG_BUILDPACKAGE_USER_OPTIONS') }) {
		if (/^--changes-option=(.*)$/) {
			push @changes_opts, $1;
		} elsif (/^-s[iad]$/) {
			push @changes_opts, $_;
		} elsif (/^--build=.*$/) {
			push @changes_opts, $_;
		} elsif (/^-m.*$/) {
			push @changes_opts, $_;
		} elsif (/^-e.*$/) {
			push @changes_opts, $_;
		} elsif (/^-v.*$/) {
			push @changes_opts, $_;
		} elsif (/^-C.*$/) {
			push @changes_opts, $_;
		}
	}

	return \@changes_opts;
}

1;
