# Resolver.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::ResolverBase;

use strict;
use warnings;
use POSIX;
use Fcntl;
use File::Temp qw(mktemp);
use File::Basename qw(basename);
use File::Copy;
use MIME::Base64;

use Dpkg::Deps;
use Sbuild::Base;
use Sbuild qw($devnull isin debug debug2);

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

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

    @EXPORT = qw();
}

sub new {
    my $class = shift;
    my $conf = shift;
    my $session = shift;
    my $host = shift;

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

    $self->set('Session', $session);
    $self->set('Host', $host);
    $self->set('Changes', {});
    $self->set('AptDependencies', {});
    $self->set('Split', $self->get_conf('CHROOT_SPLIT'));
    # Typically set by Sbuild::Build, but not outside a build context.
    $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'));
    $self->set('Multiarch Support', 1);
    $self->set('Initial Foreign Arches', {});
    $self->set('Added Foreign Arches', {});

    my $dummy_archive_list_file =
        '/etc/apt/sources.list.d/sbuild-build-depends-archive.list';
    $self->set('Dummy archive list file', $dummy_archive_list_file);

    my $extra_repositories_archive_list_file =
        '/etc/apt/sources.list.d/sbuild-extra-repositories.list';
    $self->set('Extra repositories archive list file', $extra_repositories_archive_list_file);

    my $extra_packages_archive_list_file =
        '/etc/apt/sources.list.d/sbuild-extra-packages-archive.list';
    $self->set('Extra packages archive list file', $extra_packages_archive_list_file);

    return $self;
}

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

    # Add specified extra repositories into /etc/apt/sources.list.d/.
    # This has to be done this early so that the early apt
    # update/upgrade/distupgrade steps also consider the extra repositories.
    # If this step would be done too late, extra repositories would only be
    # considered when resolving build dependencies but not for upgrading the
    # base chroot.
    if (scalar @{$self->get_conf('EXTRA_REPOSITORIES')} > 0) {
	my $extra_repositories_archive_list_file = $self->get('Extra repositories archive list file');
	if ($session->test_regular_file($extra_repositories_archive_list_file)) {
	    $self->log_error("$extra_repositories_archive_list_file exists - will not write extra repositories to it\n");
	} else {
	    my $tmpfilename = $session->mktemp();

	    my $tmpfh = $session->get_write_file_handle($tmpfilename);
	    if (!$tmpfh) {
		$self->log_error("Cannot open pipe: $!\n");
		return 0;
	    }
	    for my $repospec (@{$self->get_conf('EXTRA_REPOSITORIES')}) {
		print $tmpfh "$repospec\n";
	    }
	    close $tmpfh;
	    # List file needs to be moved with root.
	    if (!$session->chmod($tmpfilename, '0644')) {
		$self->log("Failed to create apt list file for dummy archive.\n");
		$session->unlink($tmpfilename);
		return 0;
	    }
	    if (!$session->rename($tmpfilename, $extra_repositories_archive_list_file)) {
		$self->log("Failed to create apt list file for dummy archive.\n");
		$session->unlink($tmpfilename);
		return 0;
	    }
	}
    }
}

sub setup {
    my $self = shift;

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

    #Set up dpkg config
    $self->setup_dpkg();

    my $aptconf = "/var/lib/sbuild/apt.conf";
    $self->set('APT Conf', $aptconf);

    my $chroot_aptconf = $session->get('Location') . "/$aptconf";
    $self->set('Chroot APT Conf', $chroot_aptconf);

    my $tmpaptconf = $session->mktemp({ TEMPLATE => "$aptconf.XXXXXX"});
    if (!$tmpaptconf) {
	$self->log_error("Can't create $chroot_aptconf.XXXXXX: $!\n");
	return 0;
    }

    my $F = $session->get_write_file_handle($tmpaptconf);
    if (!$F) {
	$self->log_error("Cannot open pipe: $!\n");
	return 0;
    }

    # Always write out apt.conf, because it may become outdated.
    if ($self->get_conf('APT_ALLOW_UNAUTHENTICATED')) {
	print $F qq(APT::Get::AllowUnauthenticated "true";\n);
    }
    print $F qq(APT::Install-Recommends "false";\n);
    print $F qq(APT::AutoRemove::SuggestsImportant "false";\n);
    print $F qq(APT::AutoRemove::RecommendsImportant "false";\n);
    print $F qq(Acquire::Languages "none";\n); # do not download translations

    if ($self->get_conf('APT_KEEP_DOWNLOADED_PACKAGES')) {
	print $F qq(APT::Keep-Downloaded-Packages "true";\n);
    } else {
	# remove packages from /var/cache/apt/archive/*.deb after installation
	print $F qq(APT::Keep-Downloaded-Packages "false";\n);
    }

    if ($self->get('Split')) {
	print $F "Dir \"$chroot_dir\";\n";
    }

# The cross build profile indicates that we cannot run host architecture binaries.
# In that case, forbid installing host architecture binaries.
    if (grep { $_ eq 'cross' } (split /\s+/, $self->get_conf('BUILD_PROFILES'))) {
	my $host_arch = $self->get_conf('HOST_ARCH');
	print $F qq(APT::BarbarianArchitectures {"$host_arch"};\n);
    }

    close $F;

    if (!$session->rename($tmpaptconf, $aptconf)) {
	$self->log_error("Can't rename $tmpaptconf to $aptconf: $!\n");
	return 0;
    }

    if (!$session->chown($aptconf, $self->get_conf('BUILD_USER'), 'sbuild')) {
	$self->log_error("Failed to set " . $self->get_conf('BUILD_USER') .
			 ":sbuild ownership on apt.conf at $aptconf\n");
	return 0;
    }
    if (!$session->chmod($aptconf, '0664')) {
	$self->log_error("Failed to set 0664 permissions on apt.conf at $aptconf\n");
	return 0;
    }

    # unsplit mode uses an absolute path inside the chroot, rather
    # than on the host system.
    if ($self->get('Split')) {
	$self->set('APT Options',
		   ['-o', "Dir::State::status=$chroot_dir/var/lib/dpkg/status",
		    '-o', "DPkg::Options::=--root=$chroot_dir",
		    '-o', "DPkg::Run-Directory=$chroot_dir"]);

	$self->set('Aptitude Options',
		   ['-o', "Dir::State::status=$chroot_dir/var/lib/dpkg/status",
		    '-o', "DPkg::Options::=--root=$chroot_dir",
		    '-o', "DPkg::Run-Directory=$chroot_dir"]);

	# sudo uses an absolute path on the host system.
	$session->get('Defaults')->{'ENV'}->{'APT_CONFIG'} =
	    $self->get('Chroot APT Conf');
    } else { # no split
	$self->set('APT Options', []);
	$self->set('Aptitude Options', []);
	$session->get('Defaults')->{'ENV'}->{'APT_CONFIG'} =
	    $self->get('APT Conf');
    }

    $self->add_extra_repositories();

    # Create an internal repository for packages given via --extra-package
    # If this step would be done too late, extra packages would only be
    # considered when resolving build dependencies but not for upgrading the
    # base chroot.
    if (scalar @{$self->get_conf('EXTRA_PACKAGES')} > 0) {
	my $extra_packages_archive_list_file = $self->get('Extra packages archive list file');
	if ($session->test_regular_file($extra_packages_archive_list_file)) {
	    $self->log_error("$extra_packages_archive_list_file exists - will not write extra packages archive list to it\n");
	} else {
	    #Prepare a path to place the extra packages
	    if (! defined $self->get('Extra packages path')) {
		my $tmpdir = $session->mktemp({ TEMPLATE => $self->get('Build Dir') . '/resolver-XXXXXX', DIRECTORY => 1});
		if (!$tmpdir) {
		    $self->log_error("mktemp -d " . $self->get('Build Dir') . '/resolver-XXXXXX failed\n');
		    return 0;
		}
		$self->set('Extra packages path', $tmpdir);
	    }
	    if (!$session->chown($self->get('Extra packages path'), $self->get_conf('BUILD_USER'), 'sbuild')) {
		$self->log_error("Failed to set " . $self->get_conf('BUILD_USER') .
		    ":sbuild ownership on extra packages dir\n");
		return 0;
	    }
	    if (!$session->chmod($self->get('Extra packages path'), '0770')) {
		$self->log_error("Failed to set 0770 permissions on extra packages dir\n");
		return 0;
	    }
	    my $extra_packages_dir = $self->get('Extra packages path');
	    my $extra_packages_archive_dir = $extra_packages_dir . '/apt_archive';
	    my $extra_packages_release_file = $extra_packages_archive_dir . '/Release';

	    $self->set('Extra packages archive directory', $extra_packages_archive_dir);
	    $self->set('Extra packages release file', $extra_packages_release_file);
	    my $extra_packages_archive_list_file = $self->get('Extra packages archive list file');

	    if (!$session->test_directory($extra_packages_dir)) {
		$self->log_warning('Could not create build-depends extra packages dir ' . $extra_packages_dir . ': ' . $!);
		return 0;
	    }
	    if (!($session->test_directory($extra_packages_archive_dir) || $session->mkdir($extra_packages_archive_dir, { MODE => "00775"}))) {
		$self->log_warning('Could not create build-depends extra packages archive dir ' . $extra_packages_archive_dir . ': ' . $!);
		return 0;
	    }

	    # Copy over all the extra binary packages from the host into the
	    # chroot
	    for my $deb (@{$self->get_conf('EXTRA_PACKAGES')}) {
		if (-f $deb) {
		    my $base_deb = basename($deb);
		    if ($session->test_regular_file("$extra_packages_archive_dir/$base_deb")) {
			$self->log_warning("$base_deb already exists in $extra_packages_archive_dir inside the chroot. Skipping...\n");
			next;
		    }
		    $self->log("Copying $deb to " . $session->get('Location') . "...\n");
		    $session->copy_to_chroot($deb, $extra_packages_archive_dir);
		} elsif (-d $deb) {
		    opendir(D, $deb);
		    while (my $f = readdir(D)) {
			next if (! -f "$deb/$f");
			next if ("$deb/$f" !~ /\.deb$/);
			if ($session->test_regular_file("$extra_packages_archive_dir/$f")) {
			    $self->log_warning("$f already exists in $extra_packages_archive_dir inside the chroot. Skipping...\n");
			    next;
			}
			$self->log("Copying $deb/$f to " . $session->get('Location') . "...\n");
			$session->copy_to_chroot("$deb/$f", $extra_packages_archive_dir);
		    }
		    closedir(D);
		} else {
		    $self->log_warning("$deb is neither a regular file nor a directory. Skipping...\n");
		}
	    }

	    # Do code to run apt-ftparchive
	    if (!$self->run_apt_ftparchive($self->get('Extra packages archive directory'))) {
		$self->log("Failed to run apt-ftparchive.\n");
		return 0;
	    }

	    # Write a list file for the extra packages archive if one not create yet.
	    if (!$session->test_regular_file($extra_packages_archive_list_file)) {
		my $tmpfilename = $session->mktemp();

		if (!$tmpfilename) {
		    $self->log_error("Can't create tempfile\n");
		    return 0;
		}

		my $tmpfh = $session->get_write_file_handle($tmpfilename);
		if (!$tmpfh) {
		    $self->log_error("Cannot open pipe: $!\n");
		    return 0;
		}

		# We always trust the extra packages apt repositories.
		print $tmpfh 'deb [trusted=yes] file://' . $extra_packages_archive_dir . " ./\n";
		print $tmpfh 'deb-src [trusted=yes] file://' . $extra_packages_archive_dir . " ./\n";

		close($tmpfh);
		# List file needs to be moved with root.
		if (!$session->chmod($tmpfilename, '0644')) {
		    $self->log("Failed to create apt list file for extra packages archive.\n");
		    $session->unlink($tmpfilename);
		    return 0;
		}
		if (!$session->rename($tmpfilename, $extra_packages_archive_list_file)) {
		    $self->log("Failed to create apt list file for extra packages archive.\n");
		    $session->unlink($tmpfilename);
		    return 0;
		}
	    }

	}
    }

    # Now, we'll add in any provided OpenPGP keys into the archive, so that
    # builds can (optionally) trust an external key for the duration of the
    # build.
    #
    # Keys have to be in a format that apt expects to land in
    # /etc/apt/trusted.gpg.d as they are just copied to there. We could also
    # support more formats by first importing them using gpg and then
    # exporting them but that would require gpg to be installed inside the
    # chroot.
    if (@{$self->get_conf('EXTRA_REPOSITORY_KEYS')}) {
	my $host = $self->get('Host');
	# remember whether running gpg worked or not
	my $has_gpg = 1;
	for my $repokey (@{$self->get_conf('EXTRA_REPOSITORY_KEYS')}) {
	    debug("Adding archive key: $repokey\n");
	    if (!-f $repokey) {
		$self->log("Failed to add archive key '${repokey}' - it doesn't exist!\n");
		return 0;
	    }
	    # key might be armored but apt requires keys in binary format
	    # We first try to run gpg from the host to convert the key into
	    # binary format (this works even when the key already is in binary
	    # format).
	    my $tmpfilename = mktemp("/tmp/tmp.XXXXXXXXXX");
	    if ($has_gpg == 1) {
		$host->run_command({
			COMMAND => ['gpg', '--yes', '--batch', '--output', $tmpfilename, '--dearmor', $repokey],
			USER => $self->get_conf('BUILD_USER'),
		    });
		if ($?) {
		    # don't try to use gpg again in later loop iterations
		    $has_gpg = 0;
		}
	    }
	    # If that doesn't work, then we manually convert the key
	    # as it is just base64 encoded data with a header and footer.
	    #
	    # The decoding of armored gpg keys can even be done from a shell
	    # script by using:
	    #
	    #    awk '/^$/{ x = 1; } /^[^=-]/{ if (x) { print $0; } ; }' | base64 -d
	    #
	    # As explained by dkg here: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=831409#67
	    if ($has_gpg == 0) {
		# Test if we actually have an armored key. Otherwise, no
		# conversion is needed.
		open my $fh, '<', $repokey;
		read $fh, my $first_line, 36;
		if ($first_line eq "-----BEGIN PGP PUBLIC KEY BLOCK-----") {
		    # Read the remaining part of the line until the newline.
		    # We do it like this because the line might contain
		    # additional whitespace characters or \r\n newlines.
		    <$fh>;
		    open my $out, '>', $tmpfilename;
		    # the file is an armored gpg key, so we convert it to the
		    # binary format
		    my $header = 1;
		    while( my $line = <$fh>) {
			chomp $line;
			# an empty line marks the end of the header
			if ($line eq "") {
			    $header = 0;
			    next;
			}
			if ($header == 1) {
			    next;
			}
			# the footer might contain lines starting with an
			# equal sign or minuses
			if ($line =~ /^[=-]/) {
			    last;
			}
			print $out (decode_base64($line));
		    }
		    close $out;
		}
		close $fh;
	    }
	    # we could use incrementing integers to number the extra
	    # repository keys but mktemp will also make sure that the new name
	    # doesn't exist yet and avoids the complexity of an additional
	    # variable
	    my $keyfilename = $session->mktemp({TEMPLATE => "/etc/apt/trusted.gpg.d/sbuild-extra-repository-XXXXXXXXXX.gpg"});
	    if (!$keyfilename) {
		$self->log_error("Can't create tempfile for external repository key\n");
		$session->unlink($keyfilename);
		unlink $tmpfilename;
		return 0;
	    }
	    if (!$session->copy_to_chroot($tmpfilename, $keyfilename)) {
		$self->log_error("Failed to copy external repository key $repokey into chroot $keyfilename\n");
		$session->unlink($keyfilename);
		unlink $tmpfilename;
		return 0;
	    }
	    unlink $tmpfilename;
	    if (!$session->chmod($keyfilename, '0644')) {
		$self->log_error("Failed to chmod $keyfilename inside the chroot\n");
		$session->unlink($keyfilename);
		return 0;
	    }
	}

    }

    # We have to do this early so that we can setup log filtering for the RESOLVERDIR
    # We only set it up, if 'Build Dir' was set. It is not when the resolver
    # is used by sbuild-createchroot, for example.
    #Prepare a path to build a dummy package containing our deps:
    if (! defined $self->get('Dummy package path') && defined $self->get('Build Dir')) {
	my $tmpdir = $session->mktemp({ TEMPLATE => $self->get('Build Dir') . '/resolver-XXXXXX', DIRECTORY => 1});
	if (!$tmpdir) {
	    $self->log_error("mktemp -d " . $self->get('Build Dir') . '/resolver-XXXXXX failed\n');
	    return 0;
	}
	$self->set('Dummy package path', $tmpdir);
    }

    return 1;
}

sub get_foreign_architectures {
    my $self = shift;

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

    $session->run_command({ COMMAND => ['dpkg', '--assert-multi-arch'],
                            USER => 'root'});
    if ($?)
    {
        $self->set('Multiarch Support', 0);
        $self->log_error("dpkg does not support multi-arch\n");
        return {};
    }

    my $foreignarchs = $session->read_command({ COMMAND => ['dpkg', '--print-foreign-architectures'], USER => 'root' });

    if (!defined($foreignarchs)) {
        $self->set('Multiarch Support', 0);
        $self->log_error("dpkg does not support multi-arch\n");
        return {};
    }

    if (!$foreignarchs)
    {
        debug("There are no foreign architectures configured\n");
        return {};
    }

    my %set;
    foreach my $arch (split /\s+/, $foreignarchs) {
	chomp $arch;
	next unless $arch;
	$set{$arch} = 1;
    }

    return \%set;
}

sub add_foreign_architecture {

    my $self = shift;
    my $arch = shift;

    # just skip if dpkg is to old for multiarch
    if (! $self->get('Multiarch Support')) {
	debug("not adding $arch because of no multiarch support\n");
	return 1;
    };

    # if we already have this architecture, we're done
    if ($self->get('Initial Foreign Arches')->{$arch}) {
	debug("not adding $arch because it is an initial arch\n");
	return 1;
    }
    if ($self->get('Added Foreign Arches')->{$arch}) {
	debug("not adding $arch because it has already been aded");
	return 1;
    }

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

    # FIXME - allow for more than one foreign arch
    $session->run_command(
                          # This is the Ubuntu dpkg 1.16.0~ubuntuN interface;
                          # we ought to check (or configure) which to use with
                          # check_dpkg_version:
                          #	{ COMMAND => ['sh', '-c', 'echo "foreign-architecture ' . $self->get('Host Arch') . '" > /etc/dpkg/dpkg.cfg.d/sbuild'],
                          #	  USER => 'root' });
                          # This is the Debian dpkg >= 1.16.2 interface:
                          { COMMAND => ['dpkg', '--add-architecture', $arch],
                            USER => 'root' });
    if ($?)
    {
        $self->log_error("Failed to set dpkg foreign-architecture config\n");
        return 0;
    }
    debug("Added foreign arch: $arch\n") if $arch;

    $self->get('Added Foreign Arches')->{$arch} = 1;
    return 1;
}

sub cleanup_foreign_architectures {
    my $self = shift;

    # just skip if dpkg is to old for multiarch
    if (! $self->get('Multiarch Support')) { return 1 };

    my $added_foreign_arches = $self->get('Added Foreign Arches');

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

    if (defined ($session->get('Session Purged')) && $session->get('Session Purged') == 1) {
	debug("Not removing foreign architectures: cloned chroot in use\n");
	return;
    }

    foreach my $arch (keys %{$added_foreign_arches}) {
        $self->log("Removing foreign architecture $arch\n");
        $session->run_command({ COMMAND => ['dpkg', '--remove-architecture', $arch],
                                USER => 'root',
                                DIR => '/'});
        if ($?)
        {
            $self->log_error("Failed to remove dpkg foreign-architecture $arch\n");
            return;
        }
    }
}

sub setup_dpkg {
    my $self = shift;

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

    # Record initial foreign arch state so it can be restored
    $self->set('Initial Foreign Arches', $self->get_foreign_architectures());

    if ($self->get('Host Arch') ne $self->get('Build Arch')) {
	$self->add_foreign_architecture($self->get('Host Arch'))
    }
}

sub cleanup {
    my $self = shift;

    #cleanup dpkg cross-config
    # rm /etc/dpkg/dpkg.cfg.d/sbuild
    $self->cleanup_apt_archive();
    $self->cleanup_foreign_architectures();
}

sub update {
    my $self = shift;

    $self->run_apt_command(
	{ COMMAND => [$self->get_conf('APT_GET'), 'update'],
	  ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	  USER => 'root',
	  DIR => '/' });
    return $?;
}

sub update_archive {
    my $self = shift;

    if (!$self->get_conf('APT_UPDATE_ARCHIVE_ONLY')) {
	# Update with apt-get; causes complete archive update
	$self->run_apt_command(
	    { COMMAND => [$self->get_conf('APT_GET'), 'update'],
	      ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	      USER => 'root',
	      DIR => '/' });
    } else {
	my $session = $self->get('Session');
	# Create an empty sources.list.d directory that we can set as
	# Dir::Etc::sourceparts to suppress the real one. /dev/null
	# works in recent versions of apt, but not older ones (we want
	# 448eaf8 in apt 0.8.0 and af13d14 in apt 0.9.3). Since this
	# runs against the target chroot's apt, be conservative.
	my $dummy_sources_list_d = $self->get('Dummy package path') . '/sources.list.d';
	if (!($session->test_directory($dummy_sources_list_d) || $session->mkdir($dummy_sources_list_d, { MODE => "00700"}))) {
	    $self->log_warning('Could not create build-depends dummy sources.list directory ' . $dummy_sources_list_d . ': ' . $!);
	    return 0;
	}

	# Run apt-get update pointed at our dummy archive list file, and
	# the empty sources.list.d directory, so that we only update
	# this one source. Since apt doesn't have all the sources
	# available to it in this run, any caches it generates are
	# invalid, so we then need to run gencaches with all sources
	# available to it. (Note that the tempting optimization to run
	# apt-get update -o pkgCacheFile::Generate=0 is broken before
	# 872ed75 in apt 0.9.1.)
	for my $list_file ($self->get('Dummy archive list file'),
			   $self->get('Extra packages archive list file'),
			   $self->get('Extra repositories archive list file')) {
	    if (!$session->test_regular_file_readable($list_file)) {
		next;
	    }
	    $self->run_apt_command(
		{ COMMAND => [$self->get_conf('APT_GET'), 'update',
			'-o', 'Dir::Etc::sourcelist=' . $list_file,
			'-o', 'Dir::Etc::sourceparts=' . $dummy_sources_list_d,
			'--no-list-cleanup'],
		    ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
		    USER => 'root',
		    DIR => '/' });
	    if ($? != 0) {
		return 0;
	    }
	}

	$self->run_apt_command(
	    { COMMAND => [$self->get_conf('APT_CACHE'), 'gencaches'],
	      ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	      USER => 'root',
	      DIR => '/' });
    }

    if ($? != 0) {
	return 0;
    }

    return 1;
}

sub upgrade {
    my $self = shift;

    $self->run_apt_command(
	{ COMMAND => [$self->get_conf('APT_GET'), '-uy', '-o', 'Dpkg::Options::=--force-confold', 'upgrade'],
	  ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	  USER => 'root',
	  DIR => '/' });
    return $?;
}

sub distupgrade {
    my $self = shift;

    $self->run_apt_command(
	{ COMMAND => [$self->get_conf('APT_GET'), '-uy', '-o', 'Dpkg::Options::=--force-confold', 'dist-upgrade'],
	  ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	  USER => 'root',
	  DIR => '/' });
    return $?;
}

sub clean {
    my $self = shift;

    $self->run_apt_command(
	{ COMMAND => [$self->get_conf('APT_GET'), '-y', 'clean'],
	  ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	  USER => 'root',
	  DIR => '/' });
    return $?;
}

sub autoclean {
    my $self = shift;

    $self->run_apt_command(
	{ COMMAND => [$self->get_conf('APT_GET'), '-y', 'autoclean'],
	  ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	  USER => 'root',
	  DIR => '/' });
    return $?;
}

sub autoremove {
    my $self = shift;

    $self->run_apt_command(
	{ COMMAND => [$self->get_conf('APT_GET'), '-y', 'autoremove'],
	  ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	  USER => 'root',
	  DIR => '/' });
    return $?;
}

sub add_dependencies {
    my $self = shift;
    my $pkg = shift;
    my $build_depends = shift;
    my $build_depends_arch = shift;
    my $build_depends_indep = shift;
    my $build_conflicts = shift;
    my $build_conflicts_arch = shift;
    my $build_conflicts_indep = shift;

    debug("Build-Depends: $build_depends\n") if $build_depends;
    debug("Build-Depends-Arch: $build_depends_arch\n") if $build_depends_arch;
    debug("Build-Depends-Indep: $build_depends_indep\n") if $build_depends_indep;
    debug("Build-Conflicts: $build_conflicts\n") if $build_conflicts;
    debug("Build-Conflicts-Arch: $build_conflicts_arch\n") if $build_conflicts_arch;
    debug("Build-Conflicts-Indep: $build_conflicts_indep\n") if $build_conflicts_indep;

    my $deps = {
	'Build Depends' => $build_depends,
	'Build Depends Arch' => $build_depends_arch,
	'Build Depends Indep' => $build_depends_indep,
	'Build Conflicts' => $build_conflicts,
	'Build Conflicts Arch' => $build_conflicts_arch,
	'Build Conflicts Indep' => $build_conflicts_indep
    };

    $self->get('AptDependencies')->{$pkg} = $deps;
}

sub uninstall_deps {
    my $self = shift;

    my( @pkgs, @instd, @rmvd );

    @pkgs = keys %{$self->get('Changes')->{'removed'}};
    debug("Reinstalling removed packages: @pkgs\n");
    $self->log("Failed to reinstall removed packages!\n")
	if !$self->run_apt("-y", \@instd, \@rmvd, 'install', @pkgs);
    debug("Installed were: @instd\n");
    debug("Removed were: @rmvd\n");
    $self->unset_removed(@instd);
    $self->unset_installed(@rmvd);

    @pkgs = keys %{$self->get('Changes')->{'installed'}};
    debug("Removing installed packages: @pkgs\n");
    $self->log("Failed to remove installed packages!\n")
	if !$self->run_apt("-y", \@instd, \@rmvd, 'remove', @pkgs);
    $self->unset_removed(@instd);
    $self->unset_installed(@rmvd);
}

sub set_installed {
    my $self = shift;

    foreach (@_) {
	$self->get('Changes')->{'installed'}->{$_} = 1;
    }
    debug("Added to installed list: @_\n");
}

sub set_removed {
    my $self = shift;
    foreach (@_) {
	$self->get('Changes')->{'removed'}->{$_} = 1;
	if (exists $self->get('Changes')->{'installed'}->{$_}) {
	    delete $self->get('Changes')->{'installed'}->{$_};
	    $self->get('Changes')->{'auto-removed'}->{$_} = 1;
	    debug("Note: $_ was installed\n");
	}
    }
    debug("Added to removed list: @_\n");
}

sub unset_installed {
    my $self = shift;
    foreach (@_) {
	delete $self->get('Changes')->{'installed'}->{$_};
    }
    debug("Removed from installed list: @_\n");
}

sub unset_removed {
    my $self = shift;
    foreach (@_) {
	delete $self->get('Changes')->{'removed'}->{$_};
	if (exists $self->get('Changes')->{'auto-removed'}->{$_}) {
	    delete $self->get('Changes')->{'auto-removed'}->{$_};
	    $self->get('Changes')->{'installed'}->{$_} = 1;
	    debug("Note: revived $_ to installed list\n");
	}
    }
    debug("Removed from removed list: @_\n");
}

sub get_dpkg_version {
    my $self = shift;

    my $dpkgversion = version->new(0);
    {
        # we use pipe_command instead of read_command because we want to
        # ignore non-zero exit code without printing an error message from
        # dpkg versions before 1.20 which didn't have --robot
        my $pipe = $self->get('Session')->pipe_command(
            {
                COMMAND   => [ 'dpkg', '--robot', '--version' ],
                STREAMERR => $devnull
            }
        );
        chomp(
            my $content = do { local $/; <$pipe> }
        );
        close $pipe;
        if ( $? == 0 and $content =~ /^([0-9.]+)( .*)?$/ ) {
            # dpkg is new enough for the --robot option
            $dpkgversion = version->new($1);
        }
    }
    return $dpkgversion;
}

sub dump_build_environment {
    my $self = shift;

    my $status = $self->get_dpkg_status();

    my $arch = $self->get('Arch');
    my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
    $self->log_subsection_t("Build environment", time);
    $self->log("Kernel: $sysname $release $version $arch ($machine)\n");

    $self->log("Toolchain package versions:");
    foreach my $name (sort keys %{$status}) {
        foreach my $regex (@{$self->get_conf('TOOLCHAIN_REGEX')}) {
	    if ($name =~ m,^$regex, && defined($status->{$name}->{'Version'})) {
		$self->log(' ' . $name . '_' . $status->{$name}->{'Version'});
	    }
	}
    }
    $self->log("\n");

    $self->log("Package versions:");
    foreach my $name (sort keys %{$status}) {
	if (defined($status->{$name}->{'Version'})) {
	    $self->log(' ' . $name . '_' . $status->{$name}->{'Version'});
	}
    }
    $self->log("\n");

    return $status->{'dpkg-dev'}->{'Version'};
}

sub run_apt {
    my $self = shift;
    my $mode = shift;
    my $inst_ret = shift;
    my $rem_ret = shift;
    my $action = shift;
    my @packages = @_;
    my( $msgs, $status, $pkgs, $rpkgs );

    $msgs = "";
    # redirection of stdin from /dev/null so that conffile question
    # are treated as if RETURN was pressed.
    # dpkg since 1.4.1.18 issues an error on the conffile question if
    # it reads EOF -- hardwire the new --force-confold option to avoid
    # the questions.
    my @apt_command = ($self->get_conf('APT_GET'), '--purge',
	'-o', 'DPkg::Options::=--force-confold',
	'-o', 'DPkg::Options::=--refuse-remove-essential',
	'-o', 'APT::Install-Recommends=false',
	'-o', 'Dpkg::Use-Pty=false',
	'-q');
    push @apt_command, '--allow-unauthenticated' if
	($self->get_conf('APT_ALLOW_UNAUTHENTICATED'));
    if ( $self->get('Host Arch') ne $self->get('Build Arch') ) {
        my $session = $self->get('Session');
        if (!$session->test_regular_file('/usr/lib/apt/solvers/apt')) {
            $self->log_info("Not using sbuild-cross-resolver because apt-utils"
                  . " is not installed inside the chroot\n");
        } else {
            # drop m-a:foreign and essential:yes packages that are not arch:all
            # and not arch:native
            push @apt_command, '--solver', 'sbuild-cross-resolver';
        }
    }
    push @apt_command, "$mode", $action, @packages;
    my $pipe =
	$self->pipe_apt_command(
	    { COMMAND => \@apt_command,
	      ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	      USER => 'root',
	      PRIORITY => 0,
	      DIR => '/' });
    if (!$pipe) {
	$self->log("Can't open pipe to apt-get: $!\n");
	return 0;
    }

    while(<$pipe>) {
	$msgs .= $_;
	$self->log($_) if $mode ne "-s" || debug($_);
    }
    close($pipe);
    $status = $?;

    $pkgs = $rpkgs = "";
    if ($msgs =~ /NEW packages will be installed:\n((^[ 	].*\n)*)/mi) {
	($pkgs = $1) =~ s/^[ 	]*((.|\n)*)\s*$/$1/m;
	$pkgs =~ s/\*//g;
    }
    if ($msgs =~ /packages will be REMOVED:\n((^[ 	].*\n)*)/mi) {
	($rpkgs = $1) =~ s/^[ 	]*((.|\n)*)\s*$/$1/m;
	$rpkgs =~ s/\*//g;
    }
    @$inst_ret = split( /\s+/, $pkgs );
    @$rem_ret = split( /\s+/, $rpkgs );

    $self->log("apt-get failed.\n") if $status && $mode ne "-s";
    return $mode eq "-s" || $status == 0;
}

sub run_xapt {
    my $self = shift;
    my $mode = shift;
    my $inst_ret = shift;
    my $rem_ret = shift;
    my $action = shift;
    my @packages = @_;
    my( $msgs, $status, $pkgs, $rpkgs );

    $msgs = "";
    # redirection of stdin from /dev/null so that conffile question
    # are treated as if RETURN was pressed.
    # dpkg since 1.4.1.18 issues an error on the conffile question if
    # it reads EOF -- hardwire the new --force-confold option to avoid
    # the questions.
    my @xapt_command = ($self->get_conf('XAPT'));
    my $pipe =
	$self->pipe_xapt_command(
	    { COMMAND => \@xapt_command,
	      ENV => {'DEBIAN_FRONTEND' => 'noninteractive'},
	      USER => 'root',
	      PRIORITY => 0,
	      DIR => '/' });
    if (!$pipe) {
	$self->log("Can't open pipe to xapt: $!\n");
	return 0;
    }

    while(<$pipe>) {
	$msgs .= $_;
	$self->log($_) if $mode ne "-s" || debug($_);
    }
    close($pipe);
    $status = $?;

    $pkgs = $rpkgs = "";
    if ($msgs =~ /NEW packages will be installed:\n((^[ 	].*\n)*)/mi) {
	($pkgs = $1) =~ s/^[ 	]*((.|\n)*)\s*$/$1/m;
	$pkgs =~ s/\*//g;
    }
    if ($msgs =~ /packages will be REMOVED:\n((^[ 	].*\n)*)/mi) {
	($rpkgs = $1) =~ s/^[ 	]*((.|\n)*)\s*$/$1/m;
	$rpkgs =~ s/\*//g;
    }
    @$inst_ret = split( /\s+/, $pkgs );
    @$rem_ret = split( /\s+/, $rpkgs );

    $self->log("xapt failed.\n") if $status && $mode ne "-s";
    return $mode eq "-s" || $status == 0;
}

sub format_deps {
    my $self = shift;

    return join( ", ",
		 map { join( "|",
			     map { ($_->{'Neg'} ? "!" : "") .
				       $_->{'Package'} .
				       ($_->{'Rel'} ? " ($_->{'Rel'} $_->{'Version'})":"")}
			     scalar($_), @{$_->{'Alternatives'}}) } @_ );
}

sub get_dpkg_status {
    my $self = shift;
    my @interest = @_;
    my %result;

    debug("Requesting dpkg status for packages: @interest\n");
    my $STATUS = $self->get('Session')->get_read_file_handle('/var/lib/dpkg/status');
    if (!$STATUS) {
	$self->log("Can't open /var/lib/dpkg/status inside chroot: $!\n");
	return ();
    }
    local( $/ ) = "";
    while( <$STATUS> ) {
	my( $pkg, $status, $version, $provides );
	/^Package:\s*(.*)\s*$/mi and $pkg = $1;
	/^Status:\s*(.*)\s*$/mi and $status = $1;
	/^Version:\s*(.*)\s*$/mi and $version = $1;
	/^Provides:\s*(.*)\s*$/mi and $provides = $1;
	if (!$pkg) {
	    $self->log_error("parse error in /var/lib/dpkg/status: no Package: field\n");
	    next;
	}
	if (defined($version)) {
	    debug("$pkg ($version) status: $status\n") if $self->get_conf('DEBUG') >= 2;
	} else {
	    debug("$pkg status: $status\n") if $self->get_conf('DEBUG') >= 2;
	}
	if (!$status) {
	    $self->log_error("parse error in /var/lib/dpkg/status: no Status: field for package $pkg\n");
	    next;
	}
	if ($status !~ /\sinstalled$/) {
	    $result{$pkg}->{'Installed'} = 0
		if !(exists($result{$pkg}) &&
		     $result{$pkg}->{'Version'} eq '~*=PROVIDED=*=');
	    next;
	}
	if (!defined $version || $version eq "") {
	    $self->log_error("parse error in /var/lib/dpkg/status: no Version: field for package $pkg\n");
	    next;
	}
	$result{$pkg} = { Installed => 1, Version => $version }
	    if (isin( $pkg, @interest ) || !@interest);
	if ($provides) {
	    foreach (split( /\s*,\s*/, $provides )) {
		$result{$_} = { Installed => 1, Version => '~*=PROVIDED=*=' }
		if isin( $_, @interest ) and (not exists($result{$_}) or
					      ($result{$_}->{'Installed'} == 0));
	    }
	}
    }
    close( $STATUS );
    return \%result;
}

# Create an apt archive. Add to it if one exists.
sub setup_apt_archive {
    my $self = shift;
    my $dummy_pkg_name = shift;
    my @pkgs = @_;

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


    if (!$session->chown($self->get('Dummy package path'), $self->get_conf('BUILD_USER'), 'sbuild')) {
	$self->log_error("Failed to set " . $self->get_conf('BUILD_USER') .
			 ":sbuild ownership on dummy package dir\n");
	return 0;
    }
    if (!$session->chmod($self->get('Dummy package path'), '0770')) {
	$self->log_error("Failed to set 0770 permissions on dummy package dir\n");
	return 0;
    }
    my $dummy_dir = $self->get('Dummy package path');
    my $dummy_gpghome = $dummy_dir . '/gpg';
    my $dummy_archive_dir = $dummy_dir . '/apt_archive';
    my $dummy_release_file = $dummy_archive_dir . '/Release';
    my $dummy_archive_seckey = $dummy_archive_dir . '/sbuild-key.sec';
    my $dummy_archive_pubkey = $dummy_archive_dir . '/sbuild-key.pub';

    $self->set('Dummy archive directory', $dummy_archive_dir);
    $self->set('Dummy Release file', $dummy_release_file);
    my $dummy_archive_list_file = $self->get('Dummy archive list file');

    if (!$session->test_directory($dummy_dir)) {
        $self->log_warning('Could not create build-depends dummy dir ' . $dummy_dir . ': ' . $!);
        return 0;
    }
    if (!($session->test_directory($dummy_gpghome) || $session->mkdir($dummy_gpghome, { MODE => "00700"}))) {
        $self->log_warning('Could not create build-depends dummy gpg home dir ' . $dummy_gpghome . ': ' . $!);
        return 0;
    }
    if (!$session->chown($dummy_gpghome, $self->get_conf('BUILD_USER'), 'sbuild')) {
	$self->log_error('Failed to set ' . $self->get_conf('BUILD_USER') .
			 ':sbuild ownership on $dummy_gpghome\n');
	return 0;
    }
    if (!($session->test_directory($dummy_archive_dir) || $session->mkdir($dummy_archive_dir, { MODE => "00775"}))) {
        $self->log_warning('Could not create build-depends dummy archive dir ' . $dummy_archive_dir . ': ' . $!);
        return 0;
    }

    my $dummy_pkg_dir = $dummy_dir . '/' . $dummy_pkg_name;
    my $dummy_deb = $dummy_archive_dir . '/' . $dummy_pkg_name . '.deb';
    my $dummy_dsc = $dummy_archive_dir . '/' . $dummy_pkg_name . '.dsc';

    if (!($session->mkdir("$dummy_pkg_dir", { MODE => "00775"}))) {
	$self->log_warning('Could not create build-depends dummy dir ' . $dummy_pkg_dir . $!);
	return 0;
    }

    if (!($session->mkdir("$dummy_pkg_dir/DEBIAN", { MODE => "00775"}))) {
	$self->log_warning('Could not create build-depends dummy dir ' . $dummy_pkg_dir . '/DEBIAN: ' . $!);
	return 0;
    }

    my $DUMMY_CONTROL = $session->get_write_file_handle("$dummy_pkg_dir/DEBIAN/control");
    if (!$DUMMY_CONTROL) {
	$self->log_warning('Could not open ' . $dummy_pkg_dir . '/DEBIAN/control for writing: ' . $!);
	return 0;
    }

    my $arch = $self->get('Host Arch');
    print $DUMMY_CONTROL <<"EOF";
Package: $dummy_pkg_name
Version: 0.invalid.0
Architecture: $arch
EOF

    my @positive;
    my @negative;
    my @positive_arch;
    my @negative_arch;
    my @positive_indep;
    my @negative_indep;

    for my $pkg (@pkgs) {
	my $deps = $self->get('AptDependencies')->{$pkg};

	push(@positive, $deps->{'Build Depends'})
	    if (defined($deps->{'Build Depends'}) &&
		$deps->{'Build Depends'} ne "");
	push(@negative, $deps->{'Build Conflicts'})
	    if (defined($deps->{'Build Conflicts'}) &&
		$deps->{'Build Conflicts'} ne "");
	if ($self->get_conf('BUILD_ARCH_ANY')) {
	    push(@positive_arch, $deps->{'Build Depends Arch'})
		if (defined($deps->{'Build Depends Arch'}) &&
		    $deps->{'Build Depends Arch'} ne "");
	    push(@negative_arch, $deps->{'Build Conflicts Arch'})
		if (defined($deps->{'Build Conflicts Arch'}) &&
		    $deps->{'Build Conflicts Arch'} ne "");
	}
	if ($self->get_conf('BUILD_ARCH_ALL')) {
	    push(@positive_indep, $deps->{'Build Depends Indep'})
		if (defined($deps->{'Build Depends Indep'}) &&
		    $deps->{'Build Depends Indep'} ne "");
	    push(@negative_indep, $deps->{'Build Conflicts Indep'})
		if (defined($deps->{'Build Conflicts Indep'}) &&
		    $deps->{'Build Conflicts Indep'} ne "");
	}
    }

    my $positive_build_deps = join(", ", @positive,
				   @positive_arch, @positive_indep);
    my $positive = deps_parse($positive_build_deps,
			      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 $positive ) {
        my $msg = "Error! deps_parse() couldn't parse the positive Build-Depends '$positive_build_deps'";
        $self->log_error("$msg\n");
        return 0;
    }

    my $negative_build_deps = join(", ", @negative,
				   @negative_arch, @negative_indep);
    my $negative = deps_parse($negative_build_deps,
			      reduce_arch => 1,
			      host_arch => $self->get('Host Arch'),
			      build_arch => $self->get('Build Arch'),
			      build_dep => 1,
			      union => 1,
			      reduce_profiles => 1,
			      build_profiles => [ split / /, $self->get('Build Profiles') ]);
    if( !defined $negative ) {
        my $msg = "Error! deps_parse() couldn't parse the negative Build-Depends '$negative_build_deps'";
        $self->log_error("$msg\n");
        return 0;
    }


    # sbuild turns build dependencies into the dependencies of a dummy binary
    # package. Since binary package dependencies do not support :native the
    # architecture qualifier, these have to either be removed during native
    # compilation or replaced by the build (native) architecture during cross
    # building
    my $handle_native_archqual = sub {
        my ($dep) = @_;
        if ($dep->{archqual} && $dep->{archqual} eq "native") {
            if ($self->get('Host Arch') eq $self->get('Build Arch')) {
                $dep->{archqual} = undef;
            } else {
                $dep->{archqual} = $self->get('Build Arch');
            }
        }
        return 1;
    };
    deps_iterate($positive, $handle_native_archqual);
    deps_iterate($negative, $handle_native_archqual);

    $self->log("Merged Build-Depends: $positive\n") if $positive;
    $self->log("Merged Build-Conflicts: $negative\n") if $negative;

    # Filter out all but the first alternative except in special
    # cases.
    if (!$self->get_conf('RESOLVE_ALTERNATIVES')) {
	my $positive_filtered = Dpkg::Deps::AND->new();
	foreach my $item ($positive->get_deps()) {
	    my $alt_filtered = Dpkg::Deps::OR->new();
	    my @alternatives = $item->get_deps();
	    my $first = shift @alternatives;
	    $alt_filtered->add($first) if defined $first;
	    # Allow foo (rel x) | foo (rel y) as the only acceptable
	    # form of alternative.  i.e. where the package is the
	    # same, but different relations are needed, since these
	    # are effectively a single logical dependency.
	    foreach my $alt (@alternatives) {
		if ($first->{'package'} eq $alt->{'package'}) {
		    $alt_filtered->add($alt);
		} else {
		    last;
		}
	    }
	    $positive_filtered->add($alt_filtered);
	}
	$positive = $positive_filtered;
    }

    if ($positive ne "") {
	print $DUMMY_CONTROL 'Depends: ' . $positive . "\n";
    }
    if ($negative ne "") {
	print $DUMMY_CONTROL 'Conflicts: ' . $negative . "\n";
    }

    $self->log("Filtered Build-Depends: $positive\n") if $positive;
    $self->log("Filtered Build-Conflicts: $negative\n") if $negative;

    print $DUMMY_CONTROL <<"EOF";
Maintainer: Debian buildd-tools Developers <buildd-tools-devel\@lists.alioth.debian.org>
Description: Dummy package to satisfy dependencies with apt - created by sbuild
 This package was created automatically by sbuild and should never appear on
 a real system. You can safely remove it.
EOF
    close ($DUMMY_CONTROL);

    foreach my $path ($dummy_pkg_dir . '/DEBIAN/control',
		      $dummy_pkg_dir . '/DEBIAN',
		      $dummy_pkg_dir,
		      $dummy_archive_dir) {
	if (!$session->chown($path, $self->get_conf('BUILD_USER'), 'sbuild')) {
	    $self->log_error("Failed to set " . $self->get_conf('BUILD_USER')
			   . ":sbuild ownership on $path\n");
	    return 0;
	}
    }

    # Now build the package:
    # NO_PKG_MANGLE=1 disables https://launchpad.net/pkgbinarymangler (only used on Ubuntu)
    my @command = ( 'env', 'NO_PKG_MANGLE=1', 'dpkg-deb' );

    # --root-owner-group was added in 1.19.0
    if ($self->get_dpkg_version() >= "1.19.0") {
	push (@command, '--root-owner-group');
    }

    push(@command, ('--build',  $dummy_pkg_dir, $dummy_deb));


    $session->run_command({
            COMMAND => \@command,
            USER     => $self->get_conf('BUILD_USER'),
            PRIORITY => 0
        });
    if ($?) {
	$self->log("Dummy package creation failed\n");
	return 0;
    }

    # Write the dummy dsc file.
    my $dummy_dsc_fh = $session->get_write_file_handle($dummy_dsc);
    if (!$dummy_dsc_fh) {
        $self->log_warning('Could not open ' . $dummy_dsc . ' for writing: ' . $!);
        return 0;
    }

    print $dummy_dsc_fh <<"EOF";
Format: 1.0
Source: $dummy_pkg_name
Binary: $dummy_pkg_name
Architecture: any
Version: 0.invalid.0
Maintainer: Debian buildd-tools Developers <buildd-tools-devel\@lists.alioth.debian.org>
EOF
    if (scalar(@positive)) {
       print $dummy_dsc_fh 'Build-Depends: ' . join(", ", @positive) . "\n";
    }
    if (scalar(@negative)) {
       print $dummy_dsc_fh 'Build-Conflicts: ' . join(", ", @negative) . "\n";
    }
    if (scalar(@positive_arch)) {
       print $dummy_dsc_fh 'Build-Depends-Arch: ' . join(", ", @positive_arch) . "\n";
    }
    if (scalar(@negative_arch)) {
       print $dummy_dsc_fh 'Build-Conflicts-Arch: ' . join(", ", @negative_arch) . "\n";
    }
    if (scalar(@positive_indep)) {
       print $dummy_dsc_fh 'Build-Depends-Indep: ' . join(", ", @positive_indep) . "\n";
    }
    if (scalar(@negative_indep)) {
       print $dummy_dsc_fh 'Build-Conflicts-Indep: ' . join(", ", @negative_indep) . "\n";
    }
    print $dummy_dsc_fh "\n";
    close $dummy_dsc_fh;

    # Do code to run apt-ftparchive
    if (!$self->run_apt_ftparchive($self->get('Dummy archive directory'))) {
        $self->log("Failed to run apt-ftparchive.\n");
        return 0;
    }

    # Write a list file for the dummy archive if one not create yet.
    if (!$session->test_regular_file($dummy_archive_list_file)) {
	my $tmpfilename = $session->mktemp();

	if (!$tmpfilename) {
	    $self->log_error("Can't create tempfile\n");
	    return 0;
	}

	my $tmpfh = $session->get_write_file_handle($tmpfilename);
	if (!$tmpfh) {
	    $self->log_error("Cannot open pipe: $!\n");
	    return 0;
	}

	# We always trust the dummy apt repositories by setting trusted=yes.
	#
	# We use copy:// instead of file:// as URI because the latter will make
	# apt use symlinks in /var/lib/apt/lists. These symlinks will become
	# broken after the dummy archive is removed. This in turn confuses
	# launchpad-buildd which directly tries to access
	# /var/lib/apt/lists/*_Packages and cannot use `apt-get indextargets` as
	# that apt feature is too new for it.
        print $tmpfh 'deb [trusted=yes] copy://' . $dummy_archive_dir . " ./\n";
        print $tmpfh 'deb-src [trusted=yes] copy://' . $dummy_archive_dir . " ./\n";

        close($tmpfh);
        # List file needs to be moved with root.
        if (!$session->chmod($tmpfilename, '0644')) {
            $self->log("Failed to create apt list file for dummy archive.\n");
	    $session->unlink($tmpfilename);
            return 0;
        }
        if (!$session->rename($tmpfilename, $dummy_archive_list_file)) {
            $self->log("Failed to create apt list file for dummy archive.\n");
	    $session->unlink($tmpfilename);
            return 0;
        }
    }

    return 1;
}

# Remove the apt archive.
sub cleanup_apt_archive {
    my $self = shift;

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

    if (defined $self->get('Dummy package path')) {
	$session->unlink($self->get('Dummy package path'), { RECURSIVE => 1, FORCE => 1 });
    }

    if (defined $self->get('Extra packages path')) {
	$session->unlink($self->get('Extra packages path'), { RECURSIVE => 1, FORCE => 1 });
    }

    $session->unlink($self->get('Dummy archive list file'), { FORCE => 1 });

    $session->unlink($self->get('Extra repositories archive list file'), { FORCE => 1 });

    $session->unlink($self->get('Extra packages archive list file'), { FORCE => 1 });

    $self->set('Extra packages path', undef);
    $self->set('Extra packages archive directory', undef);
    $self->set('Extra packages release file', undef);
    $self->set('Dummy archive directory', undef);
    $self->set('Dummy Release file', undef);
}

# Function that runs apt-ftparchive
sub run_apt_ftparchive {
    my $self = shift;
    my $dummy_archive_dir = shift;

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

    # We create the Packages, Sources and Release file inside the chroot.
    # We cannot use Digest::MD5, or Digest::SHA because
    # they are not available inside a chroot with only Essential:yes and apt
    # installed.
    # We cannot use apt-ftparchive as this is not available inside the chroot.
    # Apt-ftparchive outside the chroot might not have access to the files
    # inside the chroot (for example when using qemu or ssh backends).
    # The only alternative would've been to set up the archive outside the
    # chroot using apt-ftparchive and to then copy Packages, Sources and
    # Release into the chroot.
    # We do not do this to avoid copying files from and to the chroot.
    # At the same time doing it like this has the advantage to have less
    # dependencies of sbuild itself (no apt-ftparchive needed).
    # The disadvantage of doing it this way is that we now have to maintain
    # our own code creating the Release file which might break in the future.
    my $packagessourcescmd = <<'SCRIPTEND';
use strict;
use warnings;

use POSIX qw(strftime);
use POSIX qw(locale_h);

# Execute a command without /bin/sh but plain execvp while redirecting its
# standard output to a file given as the first argument.
# Using "print $fh `my_command`" has the disadvantage that "my_command" might
# be executed through /bin/sh (depending on the characters used) or that the
# output of "my_command" is very long.

sub hash_file($$)
{
	my ($filename, $util) = @_;
	my $output = `$util $filename`;
	my ($hash, undef) = split /\s+/, $output;
	return $hash;
}

{
    opendir(my $dh, '.') or die "Can't opendir('.'): $!";
    open my $out, '>', 'Packages';
    while (my $entry = readdir $dh) {
	next if $entry !~ /\.deb$/;
	open my $in, '-|', 'dpkg-deb', '-I', $entry, 'control' or die "cannot fork dpkg-deb";
	while (my $line = <$in>) {
	    print $out $line;
	}
	close $in;
	my $size = -s $entry;
	my $md5 = hash_file($entry, 'md5sum');
	my $sha1 = hash_file($entry, 'sha1sum');
	my $sha256 = hash_file($entry, 'sha256sum');
	print $out "Size: $size\n";
	print $out "MD5sum: $md5\n";
	print $out "SHA1: $sha1\n";
	print $out "SHA256: $sha256\n";
	print $out "Filename: ./$entry\n";
	print $out "\n";
    }
    close $out;
    closedir($dh);
}
{
    opendir(my $dh, '.') or die "Can't opendir('.'): $!";
    open my $out, '>', 'Sources';
    while (my $entry = readdir $dh) {
	next if $entry !~ /\.dsc$/;
	my $size = -s $entry;
	my $md5 = hash_file($entry, 'md5sum');
	my $sha1 = hash_file($entry, 'sha1sum');
	my $sha256 = hash_file($entry, 'sha256sum');
	my ($sha1_printed, $sha256_printed, $files_printed) = (0, 0, 0);
	open my $in, '<', $entry or die "cannot open $entry";
	while (my $line = <$in>) {
	    next if $line eq "\n";
	    $line =~ s/^Source:/Package:/;
	    print $out $line;
	    if ($line eq "Checksums-Sha1:\n") {
		print $out " $sha1 $size $entry\n";
		$sha1_printed = 1;
	    } elsif ($line eq "Checksums-Sha256:\n") {
		print $out " $sha256 $size $entry\n";
		$sha256_printed = 1;
	    } elsif ($line eq "Files:\n") {
		print $out " $md5 $size $entry\n";
		$files_printed = 1;
	    }
	}
	close $in;
	if ($sha1_printed == 0) {
	    print $out "Checksums-Sha1:\n";
	    print $out " $sha1 $size $entry\n";
	}
	if ($sha256_printed == 0) {
	    print $out "Checksums-Sha256:\n";
	    print $out " $sha256 $size $entry\n";
	}
	if ($files_printed == 0) {
	    print $out "Files:\n";
	    print $out " $md5 $size $entry\n";
	}
	print $out "Directory: .\n";
	print $out "\n";
    }
    close $out;
    closedir($dh);
}

my $packages_md5 = hash_file('Packages', 'md5sum');
my $sources_md5 = hash_file('Sources', 'md5sum');

my $packages_sha1 = hash_file('Packages', 'sha1sum');
my $sources_sha1 = hash_file('Sources', 'sha1sum');

my $packages_sha256 = hash_file('Packages', 'sha256sum');
my $sources_sha256 = hash_file('Sources', 'sha256sum');

my $packages_size = -s 'Packages';
my $sources_size = -s 'Sources';

# The timestamp format of release files is documented here:
#   https://wiki.debian.org/RepositoryFormat#Date.2CValid-Until
# It is specified to be the same format as described in Debian Policy §4.4
#   https://www.debian.org/doc/debian-policy/ch-source.html#s-dpkgchangelog
# or the same as in debian/changelog or the Date field in .changes files.
# or the same format as `date -R`
# To adhere to the specified format, the C or C.UTF-8 locale must be used.
my $old_locale = setlocale(LC_TIME);
setlocale(LC_TIME, "C.UTF-8");
my $datestring = strftime "%a, %d %b %Y %H:%M:%S +0000", gmtime();
setlocale(LC_TIME, $old_locale);

open(my $releasefh, '>', 'Release') or die "cannot open Release for writing: $!";

print $releasefh <<"END";
Codename: invalid-sbuild-codename
Date: $datestring
Description: Sbuild Build Dependency Temporary Archive
Label: sbuild-build-depends-archive
Origin: sbuild-build-depends-archive
Suite: invalid-sbuild-suite
MD5Sum:
 $packages_md5 $packages_size Packages
 $sources_md5 $sources_size Sources
SHA1:
 $packages_sha1 $packages_size Packages
 $sources_sha1 $sources_size Sources
SHA256:
 $packages_sha256 $packages_size Packages
 $sources_sha256 $sources_size Sources
END

close $releasefh;

SCRIPTEND

    # Instead of using $(perl -e) and passing $packagessourcescmd as a command
    # line argument, feed perl from standard input because otherwise the
    # command line will be too long for certain backends (like the autopkgtest
    # qemu backend).
    my $pipe = $session->pipe_command(
	{ COMMAND => ['perl'],
	    USER => "root",
	    DIR => $dummy_archive_dir,
	    PIPE => 'out',
	});
    if (!$pipe) {
	$self->log_error("cannot open pipe\n");
	return 0;
    }
    print $pipe $packagessourcescmd;
    close $pipe;
    if ($? ne 0) {
	$self->log_error("cannot create dummy archive\n");
	return 0;
    }

    return 1;
}

sub get_apt_command_internal {
    my $self = shift;
    my $options = shift;

    my $command = $options->{'COMMAND'};
    my $apt_options = $self->get('APT Options');

    debug2("APT Options: ", join(" ", @$apt_options), "\n")
	if defined($apt_options);

    my @aptcommand = ();
    if (defined($apt_options)) {
	push(@aptcommand, @{$command}[0]);
	push(@aptcommand, @$apt_options);
	if ($#$command > 0) {
	    push(@aptcommand, @{$command}[1 .. $#$command]);
	}
    } else {
	@aptcommand = @$command;
    }

    debug2("APT Command: ", join(" ", @aptcommand), "\n");

    $options->{'INTCOMMAND'} = \@aptcommand;
}

sub run_apt_command {
    my $self = shift;
    my $options = shift;

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

    # Set modfied command
    $self->get_apt_command_internal($options);

    if ($self->get('Split')) {
	return $host->run_command_internal($options);
    } else {
	return $session->run_command_internal($options);
    }
}

sub pipe_apt_command {
    my $self = shift;
    my $options = shift;

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

    # Set modfied command
    $self->get_apt_command_internal($options);

    if ($self->get('Split')) {
	return $host->pipe_command_internal($options);
    } else {
	return $session->pipe_command_internal($options);
    }
}

sub pipe_xapt_command {
    my $self = shift;
    my $options = shift;

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

    # Set modfied command
    $self->get_apt_command_internal($options);

    if ($self->get('Split')) {
	return $host->pipe_command_internal($options);
    } else {
	return $session->pipe_command_internal($options);
    }
}

sub get_aptitude_command_internal {
    my $self = shift;
    my $options = shift;

    my $command = $options->{'COMMAND'};
    my $apt_options = $self->get('Aptitude Options');

    debug2("Aptitude Options: ", join(" ", @$apt_options), "\n")
	if defined($apt_options);

    my @aptcommand = ();
    if (defined($apt_options)) {
	push(@aptcommand, @{$command}[0]);
	push(@aptcommand, @$apt_options);
	if ($#$command > 0) {
	    push(@aptcommand, @{$command}[1 .. $#$command]);
	}
    } else {
	@aptcommand = @$command;
    }

    debug2("APT Command: ", join(" ", @aptcommand), "\n");

    $options->{'INTCOMMAND'} = \@aptcommand;
}

sub run_aptitude_command {
    my $self = shift;
    my $options = shift;

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

    # Set modfied command
    $self->get_aptitude_command_internal($options);

    if ($self->get('Split')) {
	return $host->run_command_internal($options);
    } else {
	return $session->run_command_internal($options);
    }
}

sub pipe_aptitude_command {
    my $self = shift;
    my $options = shift;

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

    # Set modfied command
    $self->get_aptitude_command_internal($options);

    if ($self->get('Split')) {
	return $host->pipe_command_internal($options);
    } else {
	return $session->pipe_command_internal($options);
    }
}

sub get_sbuild_dummy_pkg_name {
    my $self = shift;
    my $name = shift;

    return 'sbuild-build-depends-' . $name. '-dummy';
}

1;
