diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:46:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:46:56 +0000 |
commit | 8e79ad9f544d1c4a0476e0d96aef0496ca7fc741 (patch) | |
tree | cda1743f5820600fd8c638ac7f034f917ac8c381 /lib/Sbuild/ResolverBase.pm | |
parent | Initial commit. (diff) | |
download | sbuild-8e79ad9f544d1c4a0476e0d96aef0496ca7fc741.tar.xz sbuild-8e79ad9f544d1c4a0476e0d96aef0496ca7fc741.zip |
Adding upstream version 0.85.6.upstream/0.85.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/Sbuild/ResolverBase.pm')
-rw-r--r-- | lib/Sbuild/ResolverBase.pm | 1685 |
1 files changed, 1685 insertions, 0 deletions
diff --git a/lib/Sbuild/ResolverBase.pm b/lib/Sbuild/ResolverBase.pm new file mode 100644 index 0000000..dff1905 --- /dev/null +++ b/lib/Sbuild/ResolverBase.pm @@ -0,0 +1,1685 @@ +# 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(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"; + } + + 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 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("Build environment"); + $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') ) { + # 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) + $session->run_command( + { COMMAND => ['env', 'NO_PKG_MANGLE=1', 'dpkg-deb', '--build', $dummy_pkg_dir, $dummy_deb], + 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; |