summaryrefslogtreecommitdiffstats
path: root/lib/Sbuild/Build.pm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:46:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:46:56 +0000
commit8e79ad9f544d1c4a0476e0d96aef0496ca7fc741 (patch)
treecda1743f5820600fd8c638ac7f034f917ac8c381 /lib/Sbuild/Build.pm
parentInitial commit. (diff)
downloadsbuild-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/Build.pm')
-rw-r--r--lib/Sbuild/Build.pm3686
1 files changed, 3686 insertions, 0 deletions
diff --git a/lib/Sbuild/Build.pm b/lib/Sbuild/Build.pm
new file mode 100644
index 0000000..cca1c89
--- /dev/null
+++ b/lib/Sbuild/Build.pm
@@ -0,0 +1,3686 @@
+#
+# Build.pm: build library for sbuild
+# Copyright © 2005 Ryan Murray <rmurray@debian.org>
+# Copyright © 2005-2010 Roger Leigh <rleigh@debian.org>
+# Copyright © 2008 Simon McVittie <smcv@debian.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+#######################################################################
+
+package Sbuild::Build;
+
+use strict;
+use warnings;
+
+use English;
+use POSIX;
+use Errno qw(:POSIX);
+use Fcntl;
+use File::Temp qw(mkdtemp);
+use File::Basename qw(basename dirname);
+use File::Path qw(make_path);
+use FileHandle;
+use File::Copy qw(); # copy is already exported from Sbuild, so don't export
+ # anything.
+use Dpkg::Arch;
+use Dpkg::Control;
+use Dpkg::Index;
+use Dpkg::Version;
+use Dpkg::Deps qw(deps_concat deps_parse);
+use Dpkg::Changelog::Debian;
+use Scalar::Util 'refaddr';
+
+use MIME::Lite;
+use Term::ANSIColor;
+
+use Sbuild qw($devnull binNMU_version copy isin debug send_mail
+ dsc_files dsc_pkgver strftime_c);
+use Sbuild::Base;
+use Sbuild::ChrootInfoSchroot;
+use Sbuild::ChrootInfoUnshare;
+use Sbuild::ChrootInfoSudo;
+use Sbuild::ChrootInfoAutopkgtest;
+use Sbuild::ChrootRoot;
+use Sbuild::Sysconfig qw($version $release_date);
+use Sbuild::Sysconfig;
+use Sbuild::Resolver qw(get_resolver);
+use Sbuild::Exception;
+
+use version;
+
+BEGIN {
+ use Exporter ();
+ our (@ISA, @EXPORT);
+
+ @ISA = qw(Exporter Sbuild::Base);
+
+ @EXPORT = qw();
+}
+
+our $saved_stdout = undef;
+our $saved_stderr = undef;
+
+sub new {
+ my $class = shift;
+ my $dsc = shift;
+ my $conf = shift;
+
+ my $self = $class->SUPER::new($conf);
+ bless($self, $class);
+
+ $self->set('ABORT', undef);
+ $self->set('Job', $dsc);
+ $self->set('Build Dir', '');
+ $self->set('Max Lock Trys', 120);
+ $self->set('Lock Interval', 5);
+ $self->set('Pkg Status', 'pending');
+ $self->set('Pkg Status Trigger', undef);
+ $self->set('Pkg Start Time', 0);
+ $self->set('Pkg End Time', 0);
+ $self->set('Pkg Fail Stage', 'init');
+ $self->set('Build Start Time', 0);
+ $self->set('Build End Time', 0);
+ $self->set('Install Start Time', 0);
+ $self->set('Install End Time', 0);
+ $self->set('This Time', 0);
+ $self->set('This Space', 0);
+ $self->set('Sub Task', 'initialisation');
+ $self->set('Host', Sbuild::ChrootRoot->new($self->get('Config')));
+ # Host execution defaults
+ my $host_defaults = $self->get('Host')->get('Defaults');
+ $host_defaults->{'USER'} = $self->get_conf('USERNAME');
+ $host_defaults->{'DIR'} = $self->get_conf('HOME');
+ $host_defaults->{'STREAMIN'} = $devnull;
+ $host_defaults->{'ENV'}->{'LC_ALL'} = 'C.UTF-8';
+ $host_defaults->{'ENV'}->{'SHELL'} = '/bin/sh';
+ $host_defaults->{'ENV_FILTER'} = $self->get_conf('ENVIRONMENT_FILTER');
+ # Note, this should never fail. But, we should handle failure anyway.
+ $self->get('Host')->begin_session();
+
+ $self->set('Session', undef);
+ $self->set('Dependency Resolver', undef);
+ $self->set('Log File', undef);
+ $self->set('Log Stream', undef);
+ $self->set('Summary Stats', {});
+ $self->set('dpkg-buildpackage pid', undef);
+ $self->set('Dpkg Version', undef);
+
+ # DSC, package and version information:
+ $self->set_dsc($dsc);
+
+ # If the job name contains an underscore then it is either the filename of
+ # a dsc or a pkgname_version string. In both cases we can already extract
+ # the version number. Otherwise it is a bare source package name and the
+ # version will initially be unknown.
+ if ($dsc =~ m/_/) {
+ $self->set_version($dsc);
+ } else {
+ $self->set('Package', $dsc);
+ }
+
+ return $self;
+}
+
+sub request_abort {
+ my $self = shift;
+ my $reason = shift;
+
+ $self->log_error("ABORT: $reason (requesting cleanup and shutdown)\n");
+ $self->set('ABORT', $reason);
+
+ # Send signal to dpkg-buildpackage immediately if it's running.
+ if (defined $self->get('dpkg-buildpackage pid')) {
+ # Handling ABORT in the loop reading from the stdout/stderr output of
+ # dpkg-buildpackage is suboptimal because then the ABORT signal would
+ # only be handled once the build process writes to stdout or stderr
+ # which might not be immediately.
+ my $pid = $self->get('dpkg-buildpackage pid');
+ # Sending the pid negated to send to the whole process group.
+ kill "TERM", -$pid;
+ }
+}
+
+sub check_abort {
+ my $self = shift;
+
+ if ($self->get('ABORT')) {
+ Sbuild::Exception::Build->throw(error => "Aborting build: " .
+ $self->get('ABORT'),
+ failstage => "abort");
+ }
+}
+
+sub set_dsc {
+ my $self = shift;
+ my $dsc = shift;
+
+ debug("Setting DSC: $dsc\n");
+
+ $self->set('DSC', $dsc);
+ $self->set('Source Dir', dirname($dsc));
+ $self->set('DSC Base', basename($dsc));
+
+ debug("DSC = " . $self->get('DSC') . "\n");
+ debug("Source Dir = " . $self->get('Source Dir') . "\n");
+ debug("DSC Base = " . $self->get('DSC Base') . "\n");
+}
+
+sub set_version {
+ my $self = shift;
+ my $pkgv = shift;
+
+ debug("Setting package version: $pkgv\n");
+
+ my ($pkg, $version);
+ if (-f $pkgv && -r $pkgv) {
+ ($pkg, $version) = dsc_pkgver($pkgv);
+ } else {
+ ($pkg, $version) = split /_/, $pkgv;
+ }
+ my $pver = Dpkg::Version->new($version, check => 1);
+ return if (!defined($pkg) || !defined($version) || !defined($pver));
+ my ($o_version);
+ $o_version = $pver->version();
+
+ # Original version (no binNMU or other addition)
+ my $oversion = $version;
+ # Original version with stripped epoch
+ my $osversion = $o_version;
+ $osversion .= '-' . $pver->revision() unless $pver->{'no_revision'};
+
+ # Add binNMU to version if needed.
+ if ($self->get_conf('BIN_NMU') || $self->get_conf('APPEND_TO_VERSION')
+ || defined $self->get_conf('BIN_NMU_CHANGELOG')) {
+ if (defined $self->get_conf('BIN_NMU_CHANGELOG')) {
+ # extract the binary version from the custom changelog entry
+ open(CLOGFH, '<', \$self->get_conf('BIN_NMU_CHANGELOG'));
+ my $changes = Dpkg::Changelog::Debian->new();
+ $changes->parse(*CLOGFH, "descr");
+ my @data = $changes->get_range({count => 1});
+ $version = $data[0]->get_version();
+ close(CLOGFH);
+ } else {
+ # compute the binary version from the original version and the
+ # requested binNMU and append-to-version parameters
+ $version = binNMU_version($version,
+ $self->get_conf('BIN_NMU_VERSION'),
+ $self->get_conf('APPEND_TO_VERSION'));
+ }
+ }
+
+ my $bver = Dpkg::Version->new($version, check => 1);
+ return if (!defined($bver));
+ my ($b_epoch, $b_version, $b_revision);
+ $b_epoch = $bver->epoch();
+ $b_epoch = "" if $bver->{'no_epoch'};
+ $b_version = $bver->version();
+ $b_revision = $bver->revision();
+ $b_revision = "" if $bver->{'no_revision'};
+
+ # Version with binNMU or other additions and stripped epoch
+ my $sversion = $b_version;
+ $sversion .= '-' . $b_revision if $b_revision ne '';
+
+ $self->set('Package', $pkg);
+ $self->set('Version', $version);
+ $self->set('Package_Version', "${pkg}_$version");
+ $self->set('Package_OVersion', "${pkg}_$oversion");
+ $self->set('Package_OSVersion', "${pkg}_$osversion");
+ $self->set('Package_SVersion', "${pkg}_$sversion");
+ $self->set('OVersion', $oversion);
+ $self->set('OSVersion', $osversion);
+ $self->set('SVersion', $sversion);
+ $self->set('VersionEpoch', $b_epoch);
+ $self->set('VersionUpstream', $b_version);
+ $self->set('VersionDebian', $b_revision);
+ $self->set('DSC File', "${pkg}_${osversion}.dsc");
+ if (length $self->get_conf('DSC_DIR')) {
+ $self->set('DSC Dir', $self->get_conf('DSC_DIR'));
+ } else {
+ $self->set('DSC Dir', "${pkg}-${b_version}");
+ }
+
+ debug("Package = " . $self->get('Package') . "\n");
+ debug("Version = " . $self->get('Version') . "\n");
+ debug("Package_Version = " . $self->get('Package_Version') . "\n");
+ debug("Package_OVersion = " . $self->get('Package_OVersion') . "\n");
+ debug("Package_OSVersion = " . $self->get('Package_OSVersion') . "\n");
+ debug("Package_SVersion = " . $self->get('Package_SVersion') . "\n");
+ debug("OVersion = " . $self->get('OVersion') . "\n");
+ debug("OSVersion = " . $self->get('OSVersion') . "\n");
+ debug("SVersion = " . $self->get('SVersion') . "\n");
+ debug("VersionEpoch = " . $self->get('VersionEpoch') . "\n");
+ debug("VersionUpstream = " . $self->get('VersionUpstream') . "\n");
+ debug("VersionDebian = " . $self->get('VersionDebian') . "\n");
+ debug("DSC File = " . $self->get('DSC File') . "\n");
+ debug("DSC Dir = " . $self->get('DSC Dir') . "\n");
+}
+
+sub set_status {
+ my $self = shift;
+ my $status = shift;
+
+ $self->set('Pkg Status', $status);
+ if (defined($self->get('Pkg Status Trigger'))) {
+ $self->get('Pkg Status Trigger')->($self, $status);
+ }
+}
+
+sub get_status {
+ my $self = shift;
+
+ return $self->get('Pkg Status');
+}
+
+# This function is the main entry point into the package build. It
+# provides a top-level exception handler and does the initial setup
+# including initiating logging and creating host chroot. The nested
+# run_ functions it calls are separate in order to permit running
+# cleanup tasks in a strict order.
+sub run {
+ my $self = shift;
+
+ eval {
+ $self->check_abort();
+
+ $self->set_status('building');
+
+ $self->set('Pkg Start Time', time);
+ $self->set('Pkg End Time', $self->get('Pkg Start Time'));
+
+ # Acquire the architectures we're building for and on.
+ $self->set('Host Arch', $self->get_conf('HOST_ARCH'));
+ $self->set('Build Arch', $self->get_conf('BUILD_ARCH'));
+ $self->set('Build Profiles', $self->get_conf('BUILD_PROFILES'));
+
+ # Acquire the build type in the nomenclature used by the --build
+ # argument of dpkg-buildpackage
+ my $buildtype;
+ if ($self->get_conf('BUILD_SOURCE')) {
+ if ($self->get_conf('BUILD_ARCH_ANY')) {
+ if ($self->get_conf('BUILD_ARCH_ALL')) {
+ $buildtype = "full";
+ } else {
+ $buildtype = "source,any";
+ }
+ } else {
+ if ($self->get_conf('BUILD_ARCH_ALL')) {
+ $buildtype = "source,all";
+ } else {
+ $buildtype = "source";
+ }
+ }
+ } else {
+ if ($self->get_conf('BUILD_ARCH_ANY')) {
+ if ($self->get_conf('BUILD_ARCH_ALL')) {
+ $buildtype = "binary";
+ } else {
+ $buildtype = "any";
+ }
+ } else {
+ if ($self->get_conf('BUILD_ARCH_ALL')) {
+ $buildtype = "all";
+ } else {
+ Sbuild::Exception::Build->throw(error => "Neither architecture specific nor architecture independent or source package specified to be built.",
+ failstage => "init");
+ }
+ }
+ }
+ $self->set('Build Type', $buildtype);
+
+ my $dist = $self->get_conf('DISTRIBUTION');
+ if (!defined($dist) || !$dist) {
+ Sbuild::Exception::Build->throw(error => "No distribution defined",
+ failstage => "init");
+ }
+
+ # TODO: Get package name from build object
+ if (!$self->open_build_log()) {
+ Sbuild::Exception::Build->throw(error => "Failed to open build log",
+ failstage => "init");
+ }
+
+ # Set a chroot to run commands in host
+ my $host = $self->get('Host');
+
+ # Host execution defaults (set streams)
+ my $host_defaults = $host->get('Defaults');
+ $host_defaults->{'STREAMIN'} = $devnull;
+ $host_defaults->{'STREAMOUT'} = $self->get('Log Stream');
+ $host_defaults->{'STREAMERR'} = $self->get('Log Stream');
+
+ $self->check_abort();
+ $self->run_chroot();
+ };
+
+ debug("Error run(): $@") if $@;
+
+ my $e;
+ if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
+ if ($e->status) {
+ $self->set_status($e->status);
+ } else {
+ $self->set_status("failed");
+ }
+ $self->set('Pkg Fail Stage', $e->failstage);
+ $e->rethrow();
+ }
+}
+
+# Pack up source if needed and then run the main chroot session.
+# Close log during return/failure.
+sub run_chroot {
+ my $self = shift;
+
+ eval {
+ $self->check_abort();
+ $self->run_chroot_session();
+ };
+
+ debug("Error run_chroot(): $@") if $@;
+
+ # Log exception info and set status and fail stage prior to
+ # closing build log.
+ my $e;
+ if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
+ $self->log_error("$e\n");
+ $self->log_info($e->info."\n")
+ if ($e->info);
+ if ($e->status) {
+ $self->set_status($e->status);
+ } else {
+ $self->set_status("failed");
+ }
+ $self->set('Pkg Fail Stage', $e->failstage);
+ }
+
+ $self->close_build_log();
+
+ if ($e) {
+ $e->rethrow();
+ }
+}
+
+# Create main chroot session and package resolver. Creates a lock in
+# the chroot to prevent concurrent chroot usage (only important for
+# non-snapshot chroots). Ends chroot session on return/failure.
+sub run_chroot_session {
+ my $self=shift;
+
+ eval {
+ $self->check_abort();
+ my $chroot_info;
+ if ($self->get_conf('CHROOT_MODE') eq 'schroot') {
+ $chroot_info = Sbuild::ChrootInfoSchroot->new($self->get('Config'));
+ } elsif ($self->get_conf('CHROOT_MODE') eq 'autopkgtest') {
+ $chroot_info = Sbuild::ChrootInfoAutopkgtest->new($self->get('Config'));
+ } elsif ($self->get_conf('CHROOT_MODE') eq 'unshare') {
+ $chroot_info = Sbuild::ChrootInfoUnshare->new($self->get('Config'));
+ } else {
+ $chroot_info = Sbuild::ChrootInfoSudo->new($self->get('Config'));
+ }
+
+ my $host = $self->get('Host');
+
+ my $session = $chroot_info->create('chroot',
+ $self->get_conf('DISTRIBUTION'),
+ $self->get_conf('CHROOT'),
+ $self->get_conf('BUILD_ARCH'));
+ if (!defined $session) {
+ Sbuild::Exception::Build->throw(error => "Error creating chroot",
+ failstage => "create-session");
+ }
+
+ $self->check_abort();
+ if (!$session->begin_session()) {
+ Sbuild::Exception::Build->throw(error => "Error creating chroot session: skipping " .
+ $self->get('Package'),
+ failstage => "create-session");
+ }
+
+ $self->set('Session', $session);
+
+ $self->check_abort();
+ my $chroot_arch = $self->chroot_arch();
+ if ($self->get_conf('BUILD_ARCH') ne $chroot_arch) {
+ Sbuild::Exception::Build->throw(
+ error => "Requested build architecture (" .
+ $self->get_conf('BUILD_ARCH') .
+ ") and chroot architecture (" . $chroot_arch .
+ ") do not match. Skipping build.",
+ info => "Please specify the correct architecture with --build, or use a chroot of the correct architecture",
+ failstage => "create-session");
+ }
+
+ if (length $self->get_conf('BUILD_PATH')) {
+ my $build_path = $self->get_conf('BUILD_PATH');
+ $self->set('Build Dir', $build_path);
+ if (!($session->test_directory($build_path))) {
+ if (!$session->mkdir($build_path, { PARENTS => 1})) {
+ Sbuild::Exception::Build->throw(
+ error => "Buildpath: " . $build_path . " cannot be created",
+ failstage => "create-session");
+ }
+ } else {
+ my $isempty = <<END;
+if (opendir my \$dfh, "$build_path") {
+ while (defined(my \$file=readdir \$dfh)) {
+ next if \$file eq "." or \$file eq "..";
+ closedir \$dfh;
+ exit 1
+ }
+ closedir \$dfh;
+ exit 0
+}
+exit 2
+END
+ $session->run_command({
+ COMMAND => ['perl', '-e', $isempty],
+ USER => 'root',
+ DIR => '/'
+ });
+ if ($? == 1) {
+ Sbuild::Exception::Build->throw(
+ error => "Buildpath: " . $build_path . " is not empty",
+ failstage => "create-session");
+ }
+ elsif ($? == 2) {
+ Sbuild::Exception::Build->throw(
+ error => "Buildpath: " . $build_path . " cannot be read. Insufficient permissions?",
+ failstage => "create-session");
+ }
+ }
+ } else {
+ # we run mktemp within the chroot instead of using File::Temp::tempdir because the user
+ # running sbuild might not have permissions creating a directory in /build. This happens
+ # when the chroot was extracted in a different user namespace than the outer user
+ $self->check_abort();
+ my $tmpdir = $session->mktemp({
+ TEMPLATE => "/build/" . $self->get('Package') . '-XXXXXX',
+ DIRECTORY => 1});
+ if (!$tmpdir) {
+ $self->log_error("unable to mktemp\n");
+ Sbuild::Exception::Build->throw(error => "unable to mktemp",
+ failstage => "create-build-dir");
+ }
+ $self->check_abort();
+ $self->set('Build Dir', $tmpdir);
+ }
+
+ # Copy in external solvers if we are cross-building
+ if ($self->get('Host Arch') ne $self->get('Build Arch')) {
+ if (!$session->test_directory("/usr/lib/apt/solvers")) {
+ if (!$session->mkdir("/usr/lib/apt/solvers", { PARENTS => 1})) {
+ Sbuild::Exception::Build->throw(
+ error => "/usr/lib/apt/solvers cannot be created",
+ failstage => "create-session");
+ }
+ }
+ foreach my $solver ('apt', 'sbuild-cross-resolver') {
+ if ($session->test_regular_file_readable("/usr/lib/apt/solvers/$solver")) {
+ next;
+ }
+ if (! -e "/usr/lib/apt/solvers/$solver") {
+ Sbuild::Exception::Build->throw(
+ error => "/usr/lib/apt/solvers/$solver is missing",
+ failstage => "create-session");
+ }
+ if (! $session->copy_to_chroot("/usr/lib/apt/solvers/$solver", "/usr/lib/apt/solvers/$solver")) {
+ Sbuild::Exception::Build->throw(
+ error => "/usr/lib/apt/solvers/$solver cannot be copied",
+ failstage => "create-session");
+ }
+ if (! $session->chmod("/usr/lib/apt/solvers/$solver", "0755")) {
+ Sbuild::Exception::Build->throw(
+ error => "/usr/lib/apt/solvers/$solver cannot chmod",
+ failstage => "create-session");
+ }
+ }
+ }
+
+ # Run pre build external commands
+ $self->check_abort();
+ if(!$self->run_external_commands("pre-build-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute pre-build-commands",
+ failstage => "run-pre-build-commands");
+ }
+
+ # Log colouring
+ $self->build_log_colour('red', '^E: ');
+ $self->build_log_colour('yellow', '^W: ');
+ $self->build_log_colour('green', '^I: ');
+ $self->build_log_colour('red', '^Status:');
+ $self->build_log_colour('green', '^Status: successful$');
+ $self->build_log_colour('yellow', '^Keeping session: ');
+ $self->build_log_colour('red', '^Lintian:');
+ $self->build_log_colour('yellow', '^Lintian: warn$');
+ $self->build_log_colour('green', '^Lintian: pass$');
+ $self->build_log_colour('green', '^Lintian: info$');
+ $self->build_log_colour('red', '^Piuparts:');
+ $self->build_log_colour('green', '^Piuparts: pass$');
+ $self->build_log_colour('red', '^Autopkgtest:');
+ $self->build_log_colour('yellow', '^Autopkgtest: no tests$');
+ $self->build_log_colour('green', '^Autopkgtest: pass$');
+
+ # Log filtering
+ my $filter;
+ $filter = $session->get('Location');
+ $filter =~ s;^/;;;
+ $self->build_log_filter($filter , 'CHROOT');
+
+ # Need tempdir to be writable and readable by sbuild group.
+ $self->check_abort();
+ if (!$session->chown($self->get('Build Dir'), $self->get_conf('BUILD_USER'), 'sbuild')) {
+ Sbuild::Exception::Build->throw(error => "Failed to set sbuild group ownership on chroot build dir",
+ failstage => "create-build-dir");
+ }
+ $self->check_abort();
+ if (!$session->chmod($self->get('Build Dir'), "ug=rwx,o=,a-s")) {
+ Sbuild::Exception::Build->throw(error => "Failed to set sbuild group ownership on chroot build dir",
+ failstage => "create-build-dir");
+ }
+
+ $self->check_abort();
+ # Needed so chroot commands log to build log
+ $session->set('Log Stream', $self->get('Log Stream'));
+ $host->set('Log Stream', $self->get('Log Stream'));
+
+ # Chroot execution defaults
+ my $chroot_defaults = $session->get('Defaults');
+ $chroot_defaults->{'DIR'} = $self->get('Build Dir');
+ $chroot_defaults->{'STREAMIN'} = $devnull;
+ $chroot_defaults->{'STREAMOUT'} = $self->get('Log Stream');
+ $chroot_defaults->{'STREAMERR'} = $self->get('Log Stream');
+ $chroot_defaults->{'ENV'}->{'LC_ALL'} = 'C.UTF-8';
+ $chroot_defaults->{'ENV'}->{'SHELL'} = '/bin/sh';
+ $chroot_defaults->{'ENV'}->{'HOME'} = '/sbuild-nonexistent';
+ $chroot_defaults->{'ENV_FILTER'} = $self->get_conf('ENVIRONMENT_FILTER');
+
+ my $resolver = get_resolver($self->get('Config'), $session, $host);
+ $resolver->set('Log Stream', $self->get('Log Stream'));
+ $resolver->set('Arch', $self->get_conf('ARCH'));
+ $resolver->set('Host Arch', $self->get_conf('HOST_ARCH'));
+ $resolver->set('Build Arch', $self->get_conf('BUILD_ARCH'));
+ $resolver->set('Build Profiles', $self->get_conf('BUILD_PROFILES'));
+ $resolver->set('Build Dir', $self->get('Build Dir'));
+ $self->set('Dependency Resolver', $resolver);
+
+ # Lock chroot so it won't be tampered with during the build.
+ $self->check_abort();
+ my $jobname;
+ # the version might not yet be known if the user only passed a package
+ # name without a version to sbuild
+ if ($self->get('Package_SVersion')) {
+ $jobname = $self->get('Package_SVersion');
+ } else {
+ $jobname = $self->get('Package');
+ }
+ if (!$session->lock_chroot($jobname, $$, $self->get_conf('USERNAME'))) {
+ Sbuild::Exception::Build->throw(error => "Error locking chroot session: skipping " .
+ $self->get('Package'),
+ failstage => "lock-session");
+ }
+
+ $self->check_abort();
+ $self->run_chroot_session_locked();
+ };
+
+ debug("Error run_chroot_session(): $@") if $@;
+
+ if ($self->get('Pkg Status') ne "successful") {
+ if(!$self->run_external_commands("post-build-failed-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute post-build-commands",
+ failstage => "run-post-build-failed-commands");
+ }
+ }
+
+ # End chroot session
+ my $session = $self->get('Session');
+ if (defined $session) {
+ my $end_session =
+ ($self->get_conf('PURGE_SESSION') eq 'always' ||
+ ($self->get_conf('PURGE_SESSION') eq 'successful' &&
+ $self->get_status() eq 'successful')) ? 1 : 0;
+ if ($end_session) {
+ $session->end_session();
+ } else {
+ $self->log("Keeping session: " . $session->get('Session ID') . "\n");
+ }
+ $session = undef;
+ }
+ $self->set('Session', $session);
+
+ my $e;
+ if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
+ $e->rethrow();
+ }
+}
+
+# Run tasks in a *locked* chroot. Update and upgrade packages.
+# Unlocks chroot on return/failure.
+sub run_chroot_session_locked {
+ my $self = shift;
+
+ eval {
+ my $session = $self->get('Session');
+ my $resolver = $self->get('Dependency Resolver');
+
+ # Run specified chroot setup commands
+ $self->check_abort();
+ if(!$self->run_external_commands("chroot-setup-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute chroot-setup-commands",
+ failstage => "run-chroot-setup-commands");
+ }
+
+ $self->check_abort();
+
+
+ $self->check_abort();
+ if (!$resolver->setup()) {
+ Sbuild::Exception::Build->throw(error => "resolver setup failed",
+ failstage => "resolver setup");
+ }
+
+ my $filter;
+ $filter = $resolver->get('Dummy package path');
+ $filter =~ s;^/;;;
+ $self->build_log_filter($filter , 'RESOLVERDIR');
+
+ $self->check_abort();
+ $self->run_chroot_update();
+
+ $self->check_abort();
+ $self->run_fetch_install_packages();
+ };
+
+ debug("Error run_chroot_session_locked(): $@") if $@;
+
+ my $session = $self->get('Session');
+ my $resolver = $self->get('Dependency Resolver');
+
+ $resolver->cleanup();
+ # Unlock chroot now it's cleaned up and ready for other users.
+ $session->unlock_chroot();
+
+ my $e;
+ if ($e = Exception::Class->caught('Sbuild::Exception::Build')) {
+ $e->rethrow();
+ }
+}
+
+sub run_chroot_update {
+ my $self = shift;
+ my $resolver = $self->get('Dependency Resolver');
+
+ eval {
+ if ($self->get_conf('APT_CLEAN') || $self->get_conf('APT_UPDATE') ||
+ $self->get_conf('APT_DISTUPGRADE') || $self->get_conf('APT_UPGRADE')) {
+ $self->log_subsection('Update chroot');
+ }
+
+ # Clean APT cache.
+ $self->check_abort();
+ if ($self->get_conf('APT_CLEAN')) {
+ if ($resolver->clean()) {
+ # Since apt-clean was requested specifically, fail on
+ # error when not in buildd mode.
+ $self->log_error("apt-get clean failed\n");
+ if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
+ Sbuild::Exception::Build->throw(error => "apt-get clean failed",
+ failstage => "apt-get-clean");
+ }
+ }
+ }
+
+ # Update APT cache.
+ $self->check_abort();
+ if ($self->get_conf('APT_UPDATE')) {
+ if ($resolver->update()) {
+ # Since apt-update was requested specifically, fail on
+ # error when not in buildd mode.
+ if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
+ Sbuild::Exception::Build->throw(error => "apt-get update failed",
+ failstage => "apt-get-update");
+ }
+ }
+ } else {
+ # If it was requested not to do an apt update, the build and host
+ # architecture must already be part of the chroot. If they are not
+ # and thus added during the sbuild run, issue a warning because
+ # then the package build dependencies will likely fail to be
+ # installable.
+ #
+ # The logic which checks which architectures are needed is in
+ # ResolverBase.pm, so we just check whether any architectures
+ # where added with 'dpkg --add-architecture' because if any were
+ # added an update is most likely needed.
+ if (keys %{$resolver->get('Added Foreign Arches')}) {
+ $self->log_warning("Additional architectures were added but apt update was disabled. Build dependencies might not be satisfiable.\n");
+ }
+ }
+
+ # Upgrade using APT.
+ $self->check_abort();
+ if ($self->get_conf('APT_DISTUPGRADE')) {
+ if ($resolver->distupgrade()) {
+ # Since apt-distupgrade was requested specifically, fail on
+ # error when not in buildd mode.
+ if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
+ Sbuild::Exception::Build->throw(error => "apt-get dist-upgrade failed",
+ failstage => "apt-get-dist-upgrade");
+ }
+ }
+ } elsif ($self->get_conf('APT_UPGRADE')) {
+ if ($resolver->upgrade()) {
+ # Since apt-upgrade was requested specifically, fail on
+ # error when not in buildd mode.
+ if ($self->get_conf('SBUILD_MODE') ne 'buildd') {
+ Sbuild::Exception::Build->throw(error => "apt-get upgrade failed",
+ failstage => "apt-get-upgrade");
+ }
+ }
+ }
+ };
+
+ debug("Error run_chroot_update(): $@") if $@;
+
+ my $e = Exception::Class->caught('Sbuild::Exception::Build');
+ if ($e) {
+ $self->run_external_commands("chroot-update-failed-commands");
+ $e->rethrow();
+ }
+}
+
+# Fetch sources, run setup, fetch and install core and package build
+# deps, then run build. Cleans up build directory and uninstalls
+# build depends on return/failure.
+sub run_fetch_install_packages {
+ my $self = shift;
+
+ $self->check_abort();
+ eval {
+ my $session = $self->get('Session');
+ my $resolver = $self->get('Dependency Resolver');
+
+ $self->check_abort();
+ if (!$self->fetch_source_files()) {
+ Sbuild::Exception::Build->throw(error => "Failed to fetch source files",
+ failstage => "fetch-src");
+ }
+
+ # Display message about chroot setup script option use being deprecated
+ if ($self->get_conf('CHROOT_SETUP_SCRIPT')) {
+ my $msg = "setup-hook option is deprecated. It has been superseded by ";
+ $msg .= "the chroot-setup-commands feature. setup-hook script will be ";
+ $msg .= "run via chroot-setup-commands.\n";
+ $self->log_warning($msg);
+ }
+
+ $self->check_abort();
+ $self->set('Install Start Time', time);
+ $self->set('Install End Time', $self->get('Install Start Time'));
+ my @coredeps = @{$self->get_conf('CORE_DEPENDS')};
+ if ($self->get('Host Arch') ne $self->get('Build Arch')) {
+ my $crosscoredeps = $self->get_conf('CROSSBUILD_CORE_DEPENDS');
+ if (defined($crosscoredeps->{$self->get('Host Arch')})) {
+ push(@coredeps, @{$crosscoredeps->{$self->get('Host Arch')}});
+ } else {
+ push(@coredeps, 'crossbuild-essential-' . $self->get('Host Arch') . ':native');
+ # Also add the following to work around bug #815172
+ push(@coredeps, 'libc-dev:' . $self->get('Host Arch'),
+ 'libstdc++-dev:' . $self->get('Host Arch'));
+ }
+ }
+
+ my @snapshot = ();
+ @snapshot = ("gcc-snapshot") if ($self->get_conf('GCC_SNAPSHOT'));
+
+ $resolver->add_dependencies('MAIN',
+ join(", ", $self->get('Build Depends') // (),
+ @{$self->get_conf('MANUAL_DEPENDS')}, @snapshot, @coredeps),
+ join(", ", $self->get('Build Depends Arch') // (),
+ @{$self->get_conf('MANUAL_DEPENDS_ARCH')}),
+ join(", ", $self->get('Build Depends Indep') // (),
+ @{$self->get_conf('MANUAL_DEPENDS_INDEP')}),
+ join(", ", $self->get('Build Conflicts') // (),
+ @{$self->get_conf('MANUAL_CONFLICTS')}),
+ join(", ", $self->get('Build Conflicts Arch') // (),
+ @{$self->get_conf('MANUAL_CONFLICTS_ARCH')}),
+ join(", ", $self->get('Build Conflicts Indep') // (),
+ @{$self->get_conf('MANUAL_CONFLICTS_INDEP')}));
+
+ $self->log_subsection("Install package build dependencies");
+
+ $self->check_abort();
+ if (!$resolver->install_deps('main', 'MAIN')) {
+ Sbuild::Exception::Build->throw(error => "Package build dependencies not satisfied; skipping",
+ failstage => "install-deps");
+ }
+ $self->check_abort();
+ if ($self->get_conf('PURGE_EXTRA_PACKAGES')) {
+ if (!$resolver->purge_extra_packages($self->get('Package'))) {
+ Sbuild::Exception::Build->throw(error => "Chroot could not be cleaned of extra packages",
+ failstage => "install-deps");
+ }
+ }
+ $self->set('Install End Time', time);
+
+ # the architecture check has to be done *after* build-essential is
+ # installed because as part of the architecture check a perl script is
+ # run inside the chroot which requires the Dpkg::Arch module which is
+ # in libdpkg-perl which might not exist in the chroot but will get
+ # installed by the build-essential package
+ if(!$self->check_architectures()) {
+ Sbuild::Exception::Build->throw(error => "Architecture check failed",
+ failstage => "check-architecture");
+ }
+
+ $self->check_abort();
+ my $dpkg_version = $resolver->dump_build_environment();
+ $self->set('Dpkg Version',Dpkg::Version->new($dpkg_version));
+
+ $self->check_abort();
+ if ($self->build()) {
+ $self->set_status('successful');
+ } else {
+ $self->set('Pkg Fail Stage', "build");
+ $self->set_status('failed');
+ }
+
+ # We run it here and not inside build() because otherwise, we cannot
+ # set the overall status to failed due to lintian errors
+ if ($self->get('Pkg Status') eq "successful") {
+ # Run lintian.
+ $self->check_abort();
+ my $ret = $self->run_lintian();
+ if (!$ret && $self->get_conf('LINTIAN_REQUIRE_SUCCESS')) {
+ $self->set('Pkg Fail Stage', "post-build");
+ $self->set_status("failed");
+ }
+ }
+
+ # Run specified chroot cleanup commands
+ $self->check_abort();
+ if (!$self->run_external_commands("chroot-cleanup-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute chroot-cleanup-commands",
+ failstage => "run-chroot-cleanup-commands");
+ }
+
+ # piuparts and autopkgtest must be run while the chroot is still open
+ # because they might need files that are not available on the host,
+ # for example the .dsc which might have been downloaded
+ if ($self->get('Pkg Status') eq "successful") {
+ $self->log_subsection("Post Build");
+
+ # Run piuparts.
+ $self->check_abort();
+ my $ret = $self->run_piuparts();
+ if (!$ret && $self->get_conf('PIUPARTS_REQUIRE_SUCCESS')) {
+ $self->set('Pkg Fail Stage', "post-build");
+ $self->set_status("failed");
+ }
+
+ # Run autopkgtest.
+ $self->check_abort();
+ $ret = $self->run_autopkgtest();
+ if (!$ret && $self->get_conf('AUTOPKGTEST_REQUIRE_SUCCESS')) {
+ $self->set('Pkg Fail Stage', "post-build");
+ $self->set_status("failed");
+ }
+
+ # Run post build external commands
+ $self->check_abort();
+ if(!$self->run_external_commands("post-build-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute post-build-commands",
+ failstage => "run-post-build-commands");
+ }
+
+ }
+ };
+
+ # If 'This Time' is still zero, then build() raised an exception and thus
+ # the end time was never set. Thus, setting it here.
+ # If we would set 'This Time' here unconditionally, then it would also
+ # possibly include the times to run piuparts and autopkgtest.
+ if ($self->get('This Time') == 0) {
+ $self->set('This Time', $self->get('Pkg End Time') - $self->get('Pkg Start Time'));
+ $self->set('This Time', 0) if $self->get('This Time') < 0;
+ }
+ # Same for 'This Space' which we must set here before everything gets
+ # cleaned up.
+ if ($self->get('This Space') == 0) {
+ # Since the build apparently failed, we pass an empty list of the
+ # build artifacts
+ $self->set('This Space', $self->check_space());
+ }
+
+ debug("Error run_fetch_install_packages(): $@") if $@;
+
+ # I catch the exception here and trigger the hook, if needed. Normally I'd
+ # do this at the end of the function, but I want the hook to fire before we
+ # clean up the environment. I re-throw the exception at the end, as usual
+ my $e = Exception::Class->caught('Sbuild::Exception::Build');
+ if ($e) {
+ if ($e->status) {
+ $self->set_status($e->status);
+ } else {
+ $self->set_status("failed");
+ }
+ $self->set('Pkg Fail Stage', $e->failstage);
+ }
+ if (!$self->get('ABORT') && defined $self->get('Pkg Fail Stage')) {
+ if ($self->get('Pkg Fail Stage') eq 'build' ) {
+ if(!$self->run_external_commands("build-failed-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute build-failed-commands",
+ failstage => "run-build-failed-commands");
+ }
+ } elsif($self->get('Pkg Fail Stage') eq 'install-deps' ) {
+ my $could_not_explain = undef;
+
+ if (defined $self->get_conf('BD_UNINSTALLABLE_EXPLAINER')
+ && $self->get_conf('BD_UNINSTALLABLE_EXPLAINER') ne ''
+ && $self->get_conf('BD_UNINSTALLABLE_EXPLAINER') ne 'none') {
+ if (!$self->explain_bd_uninstallable()) {
+ $could_not_explain = 1;
+ }
+ }
+
+ if(!$self->run_external_commands("build-deps-failed-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute build-deps-failed-commands",
+ failstage => "run-build-deps-failed-commands");
+ }
+
+ if( $could_not_explain ) {
+ Sbuild::Exception::Build->throw(error => "Failed to explain bd-uninstallable",
+ failstage => "explain-bd-uninstallable");
+ }
+ }
+ }
+
+ $self->log_subsection("Cleanup");
+ my $session = $self->get('Session');
+ my $resolver = $self->get('Dependency Resolver');
+
+ my $purge_build_directory =
+ ($self->get_conf('PURGE_BUILD_DIRECTORY') eq 'always' ||
+ ($self->get_conf('PURGE_BUILD_DIRECTORY') eq 'successful' &&
+ $self->get_status() eq 'successful')) ? 1 : 0;
+ my $purge_build_deps =
+ ($self->get_conf('PURGE_BUILD_DEPS') eq 'always' ||
+ ($self->get_conf('PURGE_BUILD_DEPS') eq 'successful' &&
+ $self->get_status() eq 'successful')) ? 1 : 0;
+ my $is_cloned_session = (defined ($session->get('Session Purged')) &&
+ $session->get('Session Purged') == 1) ? 1 : 0;
+
+ if ($purge_build_directory) {
+ # Purge package build directory
+ $self->log("Purging " . $self->get('Build Dir') . "\n");
+ if (!$self->get('Session')->unlink($self->get('Build Dir'), { RECURSIVE => 1 })) {
+ $self->log_error("unable to remove build directory\n");
+ }
+ }
+
+ # Purge non-cloned session
+ if ($is_cloned_session) {
+ $self->log("Not cleaning session: cloned chroot in use\n");
+ } else {
+ if ($purge_build_deps) {
+ # Removing dependencies
+ $resolver->uninstall_deps();
+ } else {
+ $self->log("Not removing build depends: as requested\n");
+ }
+ }
+
+
+ # re-throw the previously-caught exception
+ if ($e) {
+ $e->rethrow();
+ }
+}
+
+sub copy_to_chroot {
+ my $self = shift;
+ my $source = shift;
+ my $chrootdest = shift;
+
+ my $session = $self->get('Session');
+
+ $self->check_abort();
+ if(!$session->copy_to_chroot($source, $chrootdest)) {
+ $self->log_error("Failed to copy $source to $chrootdest\n");
+ return 0;
+ }
+
+ if (!$session->chown($chrootdest, $self->get_conf('BUILD_USER'), 'sbuild')) {
+ $self->log_error("Failed to set sbuild group ownership on $chrootdest\n");
+ return 0;
+ }
+ if (!$session->chmod($chrootdest, "ug=rw,o=r,a-s")) {
+ $self->log_error("Failed to set 0644 permissions on $chrootdest\n");
+ return 0;
+ }
+
+ return 1;
+}
+sub fetch_source_files {
+ my $self = shift;
+
+ my $build_dir = $self->get('Build Dir');
+ my $host_arch = $self->get('Host Arch');
+ my $resolver = $self->get('Dependency Resolver');
+
+ my ($dscarchs, $dscpkg, $dscver, $dsc);
+
+ my $build_depends = "";
+ my $build_depends_arch = "";
+ my $build_depends_indep = "";
+ my $build_conflicts = "";
+ my $build_conflicts_arch = "";
+ my $build_conflicts_indep = "";
+ local( *F );
+
+ $self->log_subsection("Fetch source files");
+
+ $self->check_abort();
+ if ($self->get('DSC Base') =~ m/\.dsc$/) {
+ my $dir = $self->get('Source Dir');
+
+ # Work with a .dsc file.
+ my $file = $self->get('DSC');
+ $dsc = $self->get('DSC File');
+ if (! -f $file || ! -r $file) {
+ $self->log_error("Could not find $file\n");
+ return 0;
+ }
+ my @cwd_files = dsc_files($file);
+
+ # Copy the local source files into the build directory.
+ $self->log_subsubsection("Local sources");
+ $self->log("$file exists in $dir; copying to chroot\n");
+ if (! $self->copy_to_chroot("$file", "$build_dir/$dsc")) {
+ $self->log_error("Could not copy $file to $build_dir/$dsc\n");
+ return 0;
+ }
+ foreach (@cwd_files) {
+ if (! $self->copy_to_chroot("$dir/$_", "$build_dir/$_")) {
+ $self->log_error("Could not copy $dir/$_ to $build_dir/$_\n");
+ return 0;
+ }
+ }
+ } else {
+ my $pkg = $self->get('DSC');
+ my $ver;
+
+ if ($pkg =~ m/_/) {
+ ($pkg, $ver) = split /_/, $pkg;
+ }
+
+ # Use apt to download the source files
+ $self->log_subsubsection("Check APT");
+
+ my $indextargets;
+ {
+ my $pipe = $self->get('Session')->pipe_command(
+ {
+ COMMAND => [ 'apt-get', 'indextargets' ],
+ USER => $self->get_conf('BUILD_USER'),
+ }
+ );
+ if ( !$pipe ) {
+ $self->log_error("Can't open pipe to apt-get: $!\n");
+ return 0;
+ }
+ $indextargets =
+ Dpkg::Index->new( get_key_func => sub { return $_[0]->{URI}; } );
+
+ if ( !$indextargets->parse( $pipe, 'apt-get indextargets' ) ) {
+ $self->log_error(
+ "Cannot parse output of apt-get indextargets: $!\n");
+ return 0;
+ }
+ close($pipe);
+
+ if ($?) {
+ $self->log_error("apt-get indextargets exit status $?: $!\n");
+ return 0;
+ }
+ }
+ my $found_sources_entry = 0;
+ my $num_packages_entries = 0;
+ my $entry_uri = undef;
+ my $entry_codename = undef;
+ my $entry_component = undef;
+ foreach my $key ( $indextargets->get_keys() ) {
+ my $cdata = $indextargets->get_by_key($key);
+ my $createdby = $cdata->{"Created-By"} // "";
+ my $targetof = $cdata->{"Target-Of"} // "";
+ my $identifier = $cdata->{"Identifier"} // "";
+ if ( $createdby eq "Sources"
+ and $identifier eq "Sources"
+ and $targetof eq "deb-src" )
+ {
+ $found_sources_entry = 1;
+ }
+ if ( $createdby eq 'Packages'
+ and $identifier eq 'Packages'
+ and $targetof eq 'deb'
+ and length $cdata->{"Repo-URI"} > 0
+ and length $cdata->{"Codename"} > 0
+ and length $cdata->{"Label"} > 0
+ and length $cdata->{"Origin"} > 0
+ and length $cdata->{"Suite"} > 0
+ and $cdata->{"Repo-URI"} =~ /^file:\//
+ and $cdata->{"Codename"} eq 'invalid-sbuild-codename'
+ and $cdata->{'Label'} eq 'sbuild-build-depends-archive'
+ and $cdata->{'Origin'} eq 'sbuild-build-depends-archive'
+ and $cdata->{'Suite'} eq 'invalid-sbuild-suite' )
+ {
+ # do not count the sbuild dummy repository created by any
+ # --extra-package options
+ next;
+ }
+ if ( $createdby eq 'Packages'
+ and $identifier eq 'Packages'
+ and $targetof eq 'deb'
+ and length $cdata->{"Repo-URI"} > 0
+ and length $cdata->{"Codename"} > 0
+ and length $cdata->{"Component"} > 0 )
+ {
+ $num_packages_entries += 1;
+ $entry_uri = $cdata->{"Repo-URI"};
+ $entry_codename = $cdata->{"Codename"};
+ $entry_component = $cdata->{"Component"};
+ }
+ }
+ if ( !$found_sources_entry ) {
+ $self->log("There are no deb-src lines in your sources.list\n");
+ if ( $num_packages_entries == 0 ) {
+ $self->log("Cannot generate deb-src entry without deb entry\n");
+ }
+ elsif ( $num_packages_entries > 1 ) {
+ $self->log( "Cannot generate deb-src entry "
+ . "with more than one deb entry\n" );
+ }
+ else {
+ my $entry =
+ "deb-src $entry_uri $entry_codename $entry_component";
+ $self->log(
+ "Automatically adding to EXTRA_REPOSITORIES: $entry\n");
+ push @{ $self->get_conf('EXTRA_REPOSITORIES') }, $entry;
+ $resolver->add_extra_repositories();
+ $self->run_chroot_update();
+ }
+ }
+
+ $self->log("Checking available source versions...\n");
+
+ # We would like to call apt-cache with --only-source so that the
+ # result only contains source packages with the given name but this
+ # feature was only introduced in apt 1.1~exp10 so it is only available
+ # in Debian Stretch and later
+ my $pipe = $self->get('Dependency Resolver')->pipe_apt_command(
+ { COMMAND => [$self->get_conf('APT_CACHE'),
+ '-q', 'showsrc', $pkg],
+ USER => $self->get_conf('BUILD_USER'),
+ PRIORITY => 0,
+ DIR => '/'});
+ if (!$pipe) {
+ $self->log_error("Can't open pipe to ".$self->get_conf('APT_CACHE').": $!\n");
+ return 0;
+ }
+
+ my $key_func = sub {
+ return $_[0]->{Package} . '_' . $_[0]->{Version};
+ };
+
+ my $index = Dpkg::Index->new(get_key_func=>$key_func);
+
+ if (!$index->parse($pipe, 'apt-cache showsrc')) {
+ $self->log_error("Cannot parse output of apt-cache showsrc: $!\n");
+ return 0;
+ }
+
+ close($pipe);
+
+ if ($?) {
+ $self->log_error($self->get_conf('APT_CACHE') . " exit status $?: $!\n");
+ return 0;
+ }
+
+ my $highestversion;
+ my $highestdsc;
+
+ foreach my $key ($index->get_keys()) {
+ my $cdata = $index->get_by_key($key);
+ my $pkgname = $cdata->{"Package"};
+ if (not defined($pkgname)) {
+ $self->log_warning("apt-cache output without Package field\n");
+ next;
+ }
+ # Since we cannot run apt-cache with --only-source because that
+ # feature was only introduced with apt 1.1~exp10, the result can
+ # contain source packages that we didn't ask for (but which
+ # contain binary packages of the name we specified). Since we only
+ # are interested in source packages of the given name, we skip
+ # everything that is a different source package.
+ if ($pkg ne $pkgname) {
+ next;
+ }
+ my $pkgversion = $cdata->{"Version"};
+ if (not defined($pkgversion)) {
+ $self->log_warning("apt-cache output without Version field\n");
+ next;
+ }
+ if (defined($ver) and $ver ne $pkgversion) {
+ next;
+ }
+ my $checksums = Dpkg::Checksums->new();
+ $checksums->add_from_control($cdata, use_files_for_md5 => 1);
+ my @files = grep {/\.dsc$/} $checksums->get_files();
+ if (scalar @files != 1) {
+ $self->log_warning("apt-cache output with more than one .dsc\n");
+ next;
+ }
+ if (!defined $highestdsc) {
+ $highestdsc = $files[0];
+ $highestversion = $pkgversion;
+ } else {
+ if (version_compare($highestversion, $pkgversion) < 0) {
+ $highestdsc = $files[0];
+ $highestversion = $pkgversion;
+ }
+ }
+ }
+
+ if (!defined $highestdsc) {
+ $self->log_error($self->get_conf('APT_CACHE') .
+ " returned no information about $pkg source\n");
+ $self->log_error("Are there any deb-src lines in your /etc/apt/sources.list?\n");
+ return 0;
+ }
+
+ $self->set_dsc($highestdsc);
+ $dsc = $highestdsc;
+
+ $self->log_subsubsection("Download source files with APT");
+
+ my $pipe2 = $self->get('Dependency Resolver')->pipe_apt_command(
+ { COMMAND => [$self->get_conf('APT_GET'), '--only-source', '-q', '-d', 'source', "$pkg=$highestversion"],
+ USER => $self->get_conf('BUILD_USER'),
+ PRIORITY => 0}) || return 0;
+
+ while(<$pipe2>) {
+ $self->log($_);
+ }
+ close($pipe2);
+ if ($?) {
+ $self->log_error($self->get_conf('APT_GET') . " for sources failed\n");
+ return 0;
+ }
+ }
+
+ my $pipe = $self->get('Session')->get_read_file_handle("$build_dir/$dsc");
+ if (!$pipe) {
+ $self->log_error("unable to open pipe\n");
+ return 0;
+ }
+
+ my $pdsc = Dpkg::Control->new(type => CTRL_PKG_SRC);
+ $pdsc->set_options(allow_pgp => 1);
+ if (!$pdsc->parse($pipe, "$build_dir/$dsc")) {
+ $self->log_error("Error parsing $build_dir/$dsc\n");
+ return 0;
+ }
+
+ close($pipe);
+
+ $build_depends = $pdsc->{'Build-Depends'};
+ $build_depends_arch = $pdsc->{'Build-Depends-Arch'};
+ $build_depends_indep = $pdsc->{'Build-Depends-Indep'};
+ $build_conflicts = $pdsc->{'Build-Conflicts'};
+ $build_conflicts_arch = $pdsc->{'Build-Conflicts-Arch'};
+ $build_conflicts_indep = $pdsc->{'Build-Conflicts-Indep'};
+ $dscarchs = $pdsc->{'Architecture'};
+ $dscpkg = $pdsc->{'Source'};
+ $dscver = $pdsc->{'Version'};
+
+ $self->set_version("${dscpkg}_${dscver}");
+
+ $build_depends =~ s/\n\s+/ /g if defined $build_depends;
+ $build_depends_arch =~ s/\n\s+/ /g if defined $build_depends_arch;
+ $build_depends_indep =~ s/\n\s+/ /g if defined $build_depends_indep;
+ $build_conflicts =~ s/\n\s+/ /g if defined $build_conflicts;
+ $build_conflicts_arch =~ s/\n\s+/ /g if defined $build_conflicts_arch;
+ $build_conflicts_indep =~ s/\n\s+/ /g if defined $build_conflicts_indep;
+
+ $self->set('Build Depends', $build_depends);
+ $self->set('Build Depends Arch', $build_depends_arch);
+ $self->set('Build Depends Indep', $build_depends_indep);
+ $self->set('Build Conflicts', $build_conflicts);
+ $self->set('Build Conflicts Arch', $build_conflicts_arch);
+ $self->set('Build Conflicts Indep', $build_conflicts_indep);
+
+ $self->set('Dsc Architectures', $dscarchs);
+
+ # we set up the following filters this late because the user might only
+ # have specified a source package name to build without a version in which
+ # case we only get to know the final build directory now
+ my $filter;
+ $filter = $self->get('Build Dir') . '/' . $self->get('DSC Dir');
+ $filter =~ s;^/;;;
+ $self->build_log_filter($filter, 'PKGBUILDDIR');
+ $filter = $self->get('Build Dir');
+ $filter =~ s;^/;;;
+ $self->build_log_filter($filter, 'BUILDDIR');
+
+ return 1;
+}
+
+sub check_architectures {
+ my $self = shift;
+ my $resolver = $self->get('Dependency Resolver');
+ my $dscarchs = $self->get('Dsc Architectures');
+ my $build_arch = $self->get('Build Arch');
+ my $host_arch = $self->get('Host Arch');
+ my $session = $self->get('Session');
+
+ $self->log_subsection("Check architectures");
+ # Check for cross-arch dependencies
+ # parse $build_depends* for explicit :arch and add the foreign arches, as needed
+ #
+ # This check only looks at the immediate build dependencies. This could
+ # fail in a future where a foreign architecture direct build dependency of
+ # architecture X depends on another foreign architecture package of
+ # architecture Y. Architecture Y would not be added through this check as
+ # sbuild will not traverse the dependency graph. Doing so would be very
+ # complicated as new architectures would have to be added to a dependency
+ # solver like dose3 as the graph is traversed and new architectures are
+ # found.
+ sub get_explicit_arches
+ {
+ my $visited_deps = pop;
+ my @deps = @_;
+
+ my %set;
+ for my $dep (@deps)
+ {
+ # Break any recursion in the deps data structure (is this overkill?)
+ next if !defined $dep;
+ my $id = ref($dep) ? refaddr($dep) : "str:$dep";
+ next if $visited_deps->{$id};
+ $visited_deps->{$id} = 1;
+
+ if ( exists( $dep->{archqual} ) )
+ {
+ if ( $dep->{archqual} )
+ {
+ $set{$dep->{archqual}} = 1;
+ }
+ }
+ else
+ {
+ for my $key (get_explicit_arches($dep->get_deps,
+ $visited_deps)) {
+ $set{$key} = 1;
+ }
+ }
+ }
+
+ return keys %set;
+ }
+
+ # we don't need to look at build conflicts here because conflicting with a
+ # package of an explicit architecture does not mean that we need to enable
+ # that architecture in the chroot
+ my $build_depends_concat =
+ deps_concat( grep {defined $_} ($self->get('Build Depends'),
+ $self->get('Build Depends Arch'),
+ $self->get('Build Depends Indep')));
+ my $merged_depends = deps_parse( $build_depends_concat,
+ reduce_arch => 1,
+ host_arch => $self->get('Host Arch'),
+ build_arch => $self->get('Build Arch'),
+ build_dep => 1,
+ reduce_profiles => 1,
+ build_profiles => [ split / /, $self->get('Build Profiles') ]);
+ if( !defined $merged_depends ) {
+ my $msg = "Error! deps_parse() couldn't parse the Build-Depends '$build_depends_concat'";
+ $self->log_error("$msg\n");
+ return 0;
+ }
+
+ my @explicit_arches = get_explicit_arches($merged_depends, {});
+ my @foreign_arches = grep {$_ !~ /any|all|native/} @explicit_arches;
+ my $added_any_new;
+ for my $foreign_arch(@foreign_arches)
+ {
+ $resolver->add_foreign_architecture($foreign_arch);
+ $added_any_new = 1;
+ }
+
+ my @keylist=keys %{$resolver->get('Initial Foreign Arches')};
+ $self->log('Initial Foreign Architectures: ' . join ' ', @keylist, "\n")
+ if @keylist;
+ $self->log('Foreign Architectures in build-deps: '. join ' ', @foreign_arches, "\n\n")
+ if @foreign_arches;
+
+ $self->run_chroot_update() if $added_any_new;
+
+ # At this point, all foreign architectures should have been added to dpkg.
+ # Thus, we now examine, whether the packages passed via --extra-package
+ # can even be considered by dpkg inside the chroot with respect to their
+ # architecture.
+
+ # Retrieve all foreign architectures from the chroot. We need to do this
+ # step because the user might've added more foreign arches to the chroot
+ # beforehand.
+ my @all_foreign_arches = split /\s+/, $session->read_command({
+ COMMAND => ['dpkg', '--print-foreign-architectures'],
+ USER => $self->get_conf('USERNAME'),
+ });
+ # we use an anonymous subroutine so that the referenced variables are
+ # automatically rebound to their current values
+ my $check_deb_arch = sub {
+ my $pkg = shift;
+ # Investigate the Architecture field of the binary package
+ my $arch = $self->get('Host')->read_command({
+ COMMAND => ['dpkg-deb', '--field', Cwd::abs_path($pkg), 'Architecture'],
+ USER => $self->get_conf('USERNAME')
+ });
+ if (!defined $arch) {
+ $self->log_warning("Failed to run dpkg-deb on $pkg. Skipping...\n");
+ next;
+ }
+ chomp $arch;
+ # Only packages that are Architecture:all, the native architecture or
+ # one of the configured foreign architectures are allowed.
+ if ($arch ne 'all' and $arch ne $build_arch
+ and !isin($arch, @all_foreign_arches)) {
+ $self->log_warning("Extra package $pkg of architecture $arch cannot be installed in the chroot\n");
+ }
+ };
+ for my $deb (@{$self->get_conf('EXTRA_PACKAGES')}) {
+ if (-f $deb) {
+ &$check_deb_arch($deb);
+ } elsif (-d $deb) {
+ opendir(D, $deb);
+ while (my $f = readdir(D)) {
+ next if (! -f "$deb/$f");
+ next if ("$deb/$f" !~ /\.deb$/);
+ &$check_deb_arch("$deb/$f");
+ }
+ closedir(D);
+ } else {
+ $self->log_warning("$deb is neither a regular file nor a directory. Skipping...\n");
+ }
+ }
+
+ # Check package arch makes sense to build
+ if (!$dscarchs) {
+ $self->log_warning("dsc has no Architecture: field -- skipping arch check!\n");
+ } elsif ($self->get_conf('BUILD_SOURCE')) {
+ # If the source package is to be built, then we do not need to check
+ # if any of the source package's architectures can be built given the
+ # current host architecture because then no matter the Architectures
+ # field, at least the source package will end up getting built.
+ } else {
+ my $valid_arch;
+ for my $a (split(/\s+/, $dscarchs)) {
+ # Check architecture wildcard matching with dpkg inside the chroot
+ # to avoid situations in which dpkg outside the chroot doesn't
+ # know about a new architecture yet
+ my $command = <<"EOF";
+ use strict;
+ use warnings;
+ use Dpkg::Arch;
+ if (Dpkg::Arch::debarch_is('$host_arch', '$a')) {
+ exit 0;
+ }
+ exit 1;
+EOF
+ $session->run_command(
+ { COMMAND => ['perl',
+ '-e',
+ $command],
+ USER => 'root',
+ PRIORITY => 0,
+ DIR => '/' });
+ if ($? == 0) {
+ $valid_arch = 1;
+ last;
+ }
+ }
+ if ($dscarchs ne "any" && !($valid_arch) &&
+ !($dscarchs =~ /\ball\b/ && $self->get_conf('BUILD_ARCH_ALL')) ) {
+ my $msg = "dsc: $host_arch not in arch list or does not match any arch wildcards: $dscarchs -- skipping";
+ $self->log_error("$msg\n");
+ Sbuild::Exception::Build->throw(error => $msg,
+ status => "skipped",
+ failstage => "arch-check");
+ return 0;
+ }
+ }
+
+ $self->log("Arch check ok ($host_arch included in $dscarchs)\n");
+
+ return 1;
+}
+
+# Subroutine that runs any command through the system (i.e. not through the
+# chroot. It takes a string of a command with arguments to run along with
+# arguments whether to save STDOUT and/or STDERR to the log stream
+sub run_command {
+ my $self = shift;
+ my $command = shift;
+ my $log_output = shift;
+ my $log_error = shift;
+ my $chroot = shift;
+
+ # Used to determine if we are to log from commands
+ my ($out, $err, $defaults);
+
+ # Run the command and save the exit status
+ if (!$chroot)
+ {
+ $defaults = $self->get('Host')->{'Defaults'};
+ $out = $defaults->{'STREAMOUT'} if ($log_output);
+ $err = $defaults->{'STREAMERR'} if ($log_error);
+
+ my %args = (PRIORITY => 0,
+ STREAMOUT => $out,
+ STREAMERR => $err);
+ if(ref $command) {
+ $args{COMMAND} = \@{$command};
+ $args{COMMAND_STR} = "@{$command}";
+ } else {
+ $args{COMMAND} = [split('\s+', $command)];
+ $args{COMMAND_STR} = $command;
+ }
+
+ $self->get('Host')->run_command( \%args );
+ } else {
+ $defaults = $self->get('Session')->{'Defaults'};
+ $out = $defaults->{'STREAMOUT'} if ($log_output);
+ $err = $defaults->{'STREAMERR'} if ($log_error);
+
+ my %args = (USER => 'root',
+ PRIORITY => 0,
+ STREAMOUT => $out,
+ STREAMERR => $err);
+ if(ref $command) {
+ $args{COMMAND} = \@{$command};
+ $args{COMMAND_STR} = "@{$command}";
+ } else {
+ $args{COMMAND} = [split('\s+', $command)];
+ $args{COMMAND_STR} = $command;
+ }
+
+ $self->get('Session')->run_command( \%args );
+ }
+ my $status = $?;
+
+ # Check if the command failed
+ if ($status != 0) {
+ return 0;
+ }
+ return 1;
+}
+
+# Subroutine that processes external commands to be run during various stages of
+# an sbuild run. We also ask if we want to log any output from the commands
+sub run_external_commands {
+ my $self = shift;
+ my $stage = shift;
+
+ my $log_output = $self->get_conf('LOG_EXTERNAL_COMMAND_OUTPUT');
+ my $log_error = $self->get_conf('LOG_EXTERNAL_COMMAND_ERROR');
+
+ # Return success now unless there are commands to run
+ return 1 unless (${$self->get_conf('EXTERNAL_COMMANDS')}{$stage});
+
+ # Determine which set of commands to run based on the parameter $stage
+ my @commands = @{${$self->get_conf('EXTERNAL_COMMANDS')}{$stage}};
+ return 1 if !(@commands);
+
+ # Create appropriate log message and determine if the commands are to be
+ # run inside the chroot or not, and as root or not.
+ my $chroot = 0;
+ if ($stage eq "pre-build-commands") {
+ $self->log_subsection("Pre Build Commands");
+ } elsif ($stage eq "chroot-setup-commands") {
+ $self->log_subsection("Chroot Setup Commands");
+ $chroot = 1;
+ } elsif ($stage eq "chroot-update-failed-commands") {
+ $self->log_subsection("Chroot-update Install Failed Commands");
+ $chroot = 1;
+ } elsif ($stage eq "build-deps-failed-commands") {
+ $self->log_subsection("Build-Deps Install Failed Commands");
+ $chroot = 1;
+ } elsif ($stage eq "build-failed-commands") {
+ $self->log_subsection("Generic Build Failed Commands");
+ $chroot = 1;
+ } elsif ($stage eq "starting-build-commands") {
+ $self->log_subsection("Starting Timed Build Commands");
+ $chroot = 1;
+ } elsif ($stage eq "finished-build-commands") {
+ $self->log_subsection("Finished Timed Build Commands");
+ $chroot = 1;
+ } elsif ($stage eq "chroot-cleanup-commands") {
+ $self->log_subsection("Chroot Cleanup Commands");
+ $chroot = 1;
+ } elsif ($stage eq "post-build-commands") {
+ $self->log_subsection("Post Build Commands");
+ } elsif ($stage eq "post-build-failed-commands") {
+ $self->log_subsection("Post Build Failed Commands");
+ }
+
+ # Run each command, substituting the various percent escapes (like
+ # %SBUILD_DSC) from the commands to run with the appropriate subsitutions.
+ my $hostarch = $self->get('Host Arch');
+ my $buildarch = $self->get('Build Arch');
+ my $build_dir = $self->get('Build Dir');
+ my $shell_cmd = "bash -i </dev/tty >/dev/tty 2>/dev/tty";
+ my %percent = (
+ "%" => "%",
+ "a" => $hostarch, "SBUILD_HOST_ARCH" => $hostarch,
+ "SBUILD_BUILD_ARCH" => $buildarch,
+ "b" => $build_dir, "SBUILD_BUILD_DIR" => $build_dir,
+ "s" => $shell_cmd, "SBUILD_SHELL" => $shell_cmd,
+ );
+ if ($self->get('Changes File')) {
+ my $changes = $self->get('Changes File');
+ $percent{c} = $changes;
+ $percent{SBUILD_CHANGES} = $changes;
+ }
+ # In case set_version has not been run yet, we do not know the dsc file or
+ # directory yet. This can happen if the user only specified a source
+ # package name without a version on the command line.
+ if ($self->get('DSC Dir')) {
+ my $dsc = $self->get('DSC');
+ $percent{d} = $dsc;
+ $percent{SBUILD_DSC} = $dsc;
+ my $pkgbuild_dir = $build_dir . '/' . $self->get('DSC Dir');
+ $percent{p} = $pkgbuild_dir;
+ $percent{SBUILD_PKGBUILD_DIR} = $pkgbuild_dir;
+ }
+ if ($chroot == 0) {
+ my $chroot_dir = $self->get('Session')->get('Location');
+ $percent{r} = $chroot_dir;
+ $percent{SBUILD_CHROOT_DIR} = $chroot_dir;
+ # the %SBUILD_CHROOT_EXEC escape is only defined when the command is
+ # to be run outside the chroot
+ my $exec_string = $self->get('Session')->get_internal_exec_string();
+ $percent{e} = $exec_string;
+ $percent{SBUILD_CHROOT_EXEC} = $exec_string;
+ }
+ # Our escapes pattern, with longer escapes first, then sorted lexically.
+ my $keyword_pat = join("|",
+ sort {length $b <=> length $a || $a cmp $b} keys %percent);
+ my $returnval = 1;
+ foreach my $command (@commands) {
+
+ my $substitute = sub {
+ foreach(@_) {
+ if (/\%SBUILD_CHROOT_DIR/ || /\%r/) {
+ $self->log_warning("The %SBUILD_CHROOT_DIR and %r percentage escapes are deprecated and should not be used anymore. Please use %SBUILD_CHROOT_EXEC or %e instead.");
+ }
+ s{
+ # Match a percent followed by a valid keyword
+ \%($keyword_pat)
+ }{
+ # Substitute with the appropriate value only if it's defined
+ $percent{$1} || $&
+ }msxge;
+ }
+ };
+
+ my $command_str;
+ if( ref $command ) {
+ $substitute->(@{$command});
+ $command_str = join(" ", @{$command});
+ } else {
+ $substitute->($command);
+ $command_str = $command;
+ }
+
+ $self->log_subsubsection("$command_str");
+
+ $returnval = $self->run_command($command, $log_output, $log_error, $chroot);
+ $self->log("\n");
+ if (!$returnval) {
+ $self->log_error("Command '$command_str' failed to run.\n");
+ # do not run any other commands of this type after the first
+ # failure
+ last;
+ } else {
+ $self->log_info("Finished running '$command_str'.\n");
+ }
+ }
+ $self->log("\nFinished processing commands.\n");
+ $self->log_sep();
+ return $returnval;
+}
+
+sub run_lintian {
+ my $self = shift;
+ my $session = $self->get('Session');
+
+ return 1 unless ($self->get_conf('RUN_LINTIAN'));
+ $self->set('Lintian Reason', 'error');
+
+ if (!defined($session)) {
+ $self->log_error("Session is undef. Cannot run lintian.\n");
+ return 0;
+ }
+
+ $self->log_subsubsection("lintian");
+
+ my $build_dir = $self->get('Build Dir');
+ my $resolver = $self->get('Dependency Resolver');
+ my $lintian = $self->get_conf('LINTIAN');
+ my $changes = $self->get_changes();
+ if (!defined($changes)) {
+ $self->log_error(".changes is undef. Cannot run lintian.\n");
+ return 0;
+ }
+
+ my @lintian_command = ($lintian);
+ push @lintian_command, @{$self->get_conf('LINTIAN_OPTIONS')} if
+ ($self->get_conf('LINTIAN_OPTIONS'));
+ push @lintian_command, $changes;
+
+ # If the source package was not instructed to be built, then it will not
+ # be part of the .changes file and thus, the .dsc has to be passed to
+ # lintian in addition to the .changes file.
+ if (!$self->get_conf('BUILD_SOURCE')) {
+ my $dsc = $self->get('DSC File');
+ push @lintian_command, $dsc;
+ }
+
+ $resolver->add_dependencies('LINTIAN', 'lintian:native', "", "", "", "", "");
+ return 1 unless $resolver->install_deps('lintian', 'LINTIAN');
+
+ $self->log("Running lintian...\n");
+
+ # we are not using read_command() because we also need the output for
+ # non-zero exit codes
+ my $pipe = $session->pipe_command(
+ { COMMAND => \@lintian_command,
+ PRIORITY => 0,
+ DIR => $self->get('Build Dir'),
+ PIPE => "in"
+ });
+ if (!$pipe) {
+ $self->log_error("Failed to exec Lintian: $!\n");
+ return 0;
+ }
+ my $lintian_output = "";
+ while (my $line = <$pipe>) {
+ $self->log($line);
+ $lintian_output .= $line;
+ }
+ close $pipe;
+
+ $self->log("\n");
+ if ($?) {
+ my $status = $? >> 8;
+ my $why = "unknown reason";
+ $self->set('Lintian Reason', 'fail') if ($status == 1);
+ $why = "runtime error" if ($status == 2);
+ $why = "policy violation" if ($status == 1);
+ $why = "received signal " . $? & 127 if ($? & 127);
+ $self->log_error("Lintian run failed ($why)\n");
+
+ return 0;
+ } else {
+ $self->set('Lintian Reason', 'pass');
+ if ($lintian_output =~ m/^I: /m) {
+ $self->set('Lintian Reason', 'info');
+ }
+ if ($lintian_output =~ m/^W: /m) {
+ $self->set('Lintian Reason', 'warn');
+ }
+ }
+
+ $self->log_info("Lintian run was successful.\n");
+ return 1;
+}
+
+sub run_piuparts {
+ my $self = shift;
+
+ return 1 unless ($self->get_conf('RUN_PIUPARTS'));
+ $self->set('Piuparts Reason', 'fail');
+
+ $self->log_subsubsection("piuparts");
+
+ my $piuparts = $self->get_conf('PIUPARTS');
+ my @piuparts_command;
+ # The default value is the empty array.
+ # If the value is the default (empty array) prefix with 'sudo --'
+ # If the value is a non-empty array, prefix with its values except if the
+ # first value is an empty string in which case, prefix with nothing
+ # If the value is not an array, prefix with that scalar except if the
+ # scalar is the empty string in which case, prefix with nothing
+ if (ref($self->get_conf('PIUPARTS_ROOT_ARGS')) eq "ARRAY") {
+ if (scalar(@{$self->get_conf('PIUPARTS_ROOT_ARGS')})) {
+ if (@{$self->get_conf('PIUPARTS_ROOT_ARGS')}[0] ne '') {
+ push @piuparts_command, @{$self->get_conf('PIUPARTS_ROOT_ARGS')};
+ }
+ } else {
+ push @piuparts_command, 'sudo', '--';
+ }
+ } else {
+ if ($self->get_conf('PIUPARTS_ROOT_ARGS') ne '') {
+ push @piuparts_command, $self->get_conf('PIUPARTS_ROOT_ARGS');
+ }
+ }
+ push @piuparts_command, $piuparts;
+ push @piuparts_command, @{$self->get_conf('PIUPARTS_OPTIONS')} if
+ ($self->get_conf('PIUPARTS_OPTIONS'));
+ push @piuparts_command, $self->get('Changes File');
+ $self->get('Host')->run_command(
+ { COMMAND => \@piuparts_command,
+ PRIORITY => 0,
+ });
+ my $status = $? >> 8;
+
+ # We must check for Ctrl+C (and other aborting signals) directly after
+ # running the command so that we do not mark the piuparts run as successful
+ # (the exit status will be zero)
+ $self->check_abort();
+
+ $self->log("\n");
+
+ if ($status == 0) {
+ $self->set('Piuparts Reason', 'pass');
+ } else {
+ $self->log_error("Piuparts run failed.\n");
+ return 0;
+ }
+
+ $self->log_info("Piuparts run was successful.\n");
+ return 1;
+}
+
+sub run_autopkgtest {
+ my $self = shift;
+
+ return 1 unless ($self->get_conf('RUN_AUTOPKGTEST'));
+
+ $self->set('Autopkgtest Reason', 'fail');
+
+ $self->log_subsubsection("autopkgtest");
+
+ my $session = $self->get('Session');
+
+ # sbuild used to check whether debian/tests/control exists and would not
+ # run autopkgtest at all if it didn't. This is wrong behaviour because
+ # even packages without a debian/tests/control or packages without a
+ # Testsuite: field in debian/control might still have autopkgtests as they
+ # are generated by autodep8. We will not attempt to recreate the autodep8
+ # heuristics here and thus we will always run autopkgtest if
+ # RUN_AUTOPKGTEST was set to true. Also see
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=916924
+
+ my $autopkgtest = $self->get_conf('AUTOPKGTEST');
+ my @autopkgtest_command;
+ # The default value is the empty array.
+ # If the value is the default (empty array) prefix with 'sudo --'
+ # If the value is a non-empty array, prefix with its values except if the
+ # first value is an empty string in which case, prefix with nothing
+ # If the value is not an array, prefix with that scalar except if the
+ # scalar is the empty string in which case, prefix with nothing
+ if (ref($self->get_conf('AUTOPKGTEST_ROOT_ARGS')) eq "ARRAY") {
+ if (scalar(@{$self->get_conf('AUTOPKGTEST_ROOT_ARGS')}) == 0) {
+ push @autopkgtest_command, 'sudo', '--';
+ } elsif (@{$self->get_conf('AUTOPKGTEST_ROOT_ARGS')}[0] eq '') {
+ # do nothing if the first array element is the empty string
+ } else {
+ push @autopkgtest_command, @{$self->get_conf('AUTOPKGTEST_ROOT_ARGS')};
+ }
+ } elsif ($self->get_conf('AUTOPKGTEST_ROOT_ARGS') eq '') {
+ # do nothing if the configuration value is the empty string
+ } else {
+ push @autopkgtest_command, $self->get_conf('AUTOPKGTEST_ROOT_ARGS');
+ }
+ push @autopkgtest_command, $autopkgtest;
+ my $tmpdir;
+ my @cwd_files;
+ # If the source package was not instructed to be built, then it will not
+ # be part of the .changes file and thus, the .dsc has to be passed to
+ # autopkgtest in addition to the .changes file.
+ if (!$self->get_conf('BUILD_SOURCE')) {
+ my $dsc = $self->get('DSC');
+ # If the source package was downloaded by sbuild, then the .dsc
+ # and the files it references have to be made available to the
+ # host
+ if (! -f $dsc || ! -r $dsc) {
+ my $build_dir = $self->get('Build Dir');
+ $tmpdir = mkdtemp("/tmp/tmp.sbuild.XXXXXXXXXX");
+ if (!$session->copy_from_chroot("$build_dir/$dsc", "$tmpdir/$dsc")) {
+ $self->log_error("cannot copy .dsc from chroot\n");
+ rmdir $tmpdir;
+ return 0;
+ }
+ @cwd_files = dsc_files("$tmpdir/$dsc");
+ foreach (@cwd_files) {
+ if (!$session->copy_from_chroot("$build_dir/$_", "$tmpdir/$_")) {
+ $self->log_error("cannot copy $_ from chroot\n");
+ unlink "$tmpdir/$.dsc";
+ foreach (@cwd_files) {
+ unlink "$tmpdir/$_" if -f "$tmpdir/$_";
+ }
+ rmdir $tmpdir;
+ return 0;
+ }
+ }
+ $dsc = "$tmpdir/$dsc";
+ }
+ push @autopkgtest_command, $dsc;
+ }
+ push @autopkgtest_command, $self->get('Changes File');
+ if (scalar(@{$self->get_conf('AUTOPKGTEST_OPTIONS')})) {
+ push @autopkgtest_command, @{$self->get_conf('AUTOPKGTEST_OPTIONS')};
+ } else {
+ push @autopkgtest_command, '--', 'null';
+ }
+ $self->get('Host')->run_command(
+ { COMMAND => \@autopkgtest_command,
+ PRIORITY => 0,
+ });
+ my $status = $? >> 8;
+ # if the source package wasn't built and also initially downloaded by
+ # sbuild, then the temporary directory that was created must be removed
+ if (defined $tmpdir) {
+ my $dsc = $self->get('DSC');
+ unlink "$tmpdir/$dsc";
+ foreach (@cwd_files) {
+ unlink "$tmpdir/$_";
+ }
+ rmdir $tmpdir;
+ }
+
+ # We must check for Ctrl+C (and other aborting signals) directly after
+ # running the command so that we do not mark the autopkgtest as successful
+ # (the exit status will be zero)
+ # But we must check only after the temporary directory has been removed.
+ $self->check_abort();
+
+ $self->log("\n");
+
+ if ($status == 0 || $status == 2) { # 2 is "at least one test was skipped (or at least one flaky test failed)"
+ $self->set('Autopkgtest Reason', 'pass');
+ } elsif ($status == 8) {
+ $self->set('Autopkgtest Reason', 'no tests');
+ } else {
+ # fail if neither all tests passed nor was the package without tests
+ $self->log_error("Autopkgtest run failed.\n");
+ return 0;
+ }
+
+ $self->log_info("Autopkgtest run was successful.\n");
+ return 1;
+}
+
+sub explain_bd_uninstallable {
+ my $self = shift;
+
+ my $resolver = $self->get('Dependency Resolver');
+
+ my $dummy_pkg_name = $resolver->get_sbuild_dummy_pkg_name('main');
+
+ if (!defined $self->get_conf('BD_UNINSTALLABLE_EXPLAINER')) {
+ return 0;
+ } elsif ($self->get_conf('BD_UNINSTALLABLE_EXPLAINER') eq '') {
+ return 0;
+ } elsif ($self->get_conf('BD_UNINSTALLABLE_EXPLAINER') eq 'apt') {
+ my (@instd, @rmvd);
+ my @apt_args = ('--simulate', \@instd, \@rmvd, 'install', $dummy_pkg_name,
+ '-oDebug::pkgProblemResolver=true', '-oDebug::pkgDepCache::Marker=1',
+ '-oDebug::pkgDepCache::AutoInstall=1', '-oDebug::BuildDeps=1'
+ );
+ $resolver->run_apt(@apt_args);
+ } elsif ($self->get_conf('BD_UNINSTALLABLE_EXPLAINER') eq 'dose3') {
+ # To retrieve all Packages files apt knows about we use "apt-get
+ # indextargets" and "apt-helper cat-file". The former is able to
+ # report the filesystem path of all input Packages files. The latter
+ # is able to decompress the files if necessary.
+ #
+ # We do not use "apt-cache dumpavail" or convert the EDSP output to a
+ # Packages file because that would make the package selection subject
+ # to apt pinning. This limitation would be okay if there was only the
+ # apt resolver but since there also exists the aptitude and aspcud
+ # resolvers which are able to find solution without pinning
+ # restrictions, we don't want to limit ourselves by it. In cases where
+ # apt cannot find a solution, this check is supposed to allow the user
+ # to know that choosing a different resolver might fix the problem.
+ $resolver->add_dependencies('DOSE3', 'dose-distcheck:native', "", "", "", "", "");
+ if (!$resolver->install_deps('dose3', 'DOSE3')) {
+ return 0;
+ }
+
+ my $session = $self->get('Session');
+ my $pipe_apt = $session->pipe_command({
+ COMMAND => [ 'apt-get', 'indextargets', '--format', '$(FILENAME)', 'Created-By: Packages' ],
+ USER => $self->get_conf('BUILD_USER'),
+ });
+ if (!$pipe_apt) {
+ $self->log_error("cannot open reading pipe from apt-get indextargets\n");
+ return 0;
+ }
+
+ my $host = $self->get_conf('HOST_ARCH');
+ my $build = $self->get_conf('BUILD_ARCH');
+ my @debforeignarg = ();
+ if ($build ne $host) {
+ @debforeignarg = ('--deb-foreign-archs', $host);
+ }
+
+ # - We run dose-debcheck instead of dose-builddebcheck because we want
+ # to check the dummy binary package created by sbuild instead of the
+ # original source package Build-Depends.
+ # - We use dose-debcheck instead of dose-distcheck because we cannot
+ # use the deb:// prefix on data from standard input.
+ my $pipe_dose = $session->pipe_command({
+ COMMAND => ['dose-debcheck',
+ '--checkonly', "$dummy_pkg_name:$host", '--verbose',
+ '--failures', '--successes', '--explain',
+ '--deb-native-arch', $self->get_conf('BUILD_ARCH'), @debforeignarg ],
+ PRIORITY => 0,
+ USER => $self->get_conf('BUILD_USER'),
+ PIPE => 'out'
+ });
+ if (!$pipe_dose) {
+ $self->log_error("cannot open writing pipe to dose-debcheck\n");
+ return 0;
+ }
+
+ # We parse file by file instead of concatenating all files because if
+ # there are many files, we might exceed the maximum command length and
+ # it avoids having to have the data from all Packages files in memory
+ # all at once. Working with a smaller Dpkg::Index structure should
+ # also result in faster store and retrieval times.
+ while (my $fname = <$pipe_apt>) {
+ chomp $fname;
+ my $pipe_cat = $session->pipe_command({
+ COMMAND => [ '/usr/lib/apt/apt-helper', 'cat-file', $fname ],
+ USER => $self->get_conf('BUILD_USER'),
+ });
+ if (!$pipe_cat) {
+ $self->log_error("cannot open reading pipe from apt-helper\n");
+ return 0;
+ }
+
+ # For native compilation we just pipe the output of apt-helper to
+ # dose3. For cross compilation we need to filter foreign
+ # architecture packages that are Essential:yes or
+ # Multi-Arch:foreign or otherwise dose3 might present a solution
+ # that installs foreign architecture Essential:yes or
+ # Multi-Arch:foreign packages.
+ if ($build eq $host) {
+ File::Copy::copy $pipe_cat, $pipe_dose;
+ } else {
+ my $key_func = sub {
+ return $_[0]->{Package} . ' ' . $_[0]->{Version} . ' ' . $_[0]->{Architecture};
+ };
+
+ my $index = Dpkg::Index->new(get_key_func=>$key_func);
+
+ if (!$index->parse($pipe_cat, 'apt-helper cat-file')) {
+ $self->log_error("Cannot parse output of apt-helper cat-file: $!\n");
+ return 0;
+ }
+
+ foreach my $key ($index->get_keys()) {
+ my $cdata = $index->get_by_key($key);
+ my $arch = $cdata->{'Architecture'} // '';
+ my $ess = $cdata->{'Essential'} // '';
+ my $ma = $cdata->{'Multi-Arch'} // '';
+ if ($arch ne 'all' && $arch ne $build
+ && ($ess eq 'yes' || $ma eq 'foreign')) {
+ next;
+ }
+ $cdata->output($pipe_dose);
+ print $pipe_dose "\n";
+ }
+ }
+
+ close($pipe_cat);
+ if (($? >> 8) != 0) {
+ $self->log_error("apt-helper failed\n");
+ return 0;
+ }
+ }
+
+ close $pipe_dose;
+ # - We expect an exit code of less than 64 of dose-debcheck. Any other
+ # exit code indicates abnormal program termination.
+ if (($? >> 8) >= 64) {
+ $self->log_error("dose-debcheck failed\n");
+ return 0;
+ }
+
+ close $pipe_apt;
+ if (($? >> 8) != 0) {
+ $self->log_error("apt-get indextargets failed\n");
+ return 0;
+ }
+
+
+ }
+
+ return 1;
+}
+
+sub build {
+ my $self = shift;
+
+ my $dscfile = $self->get('DSC File');
+ my $dscdir = $self->get('DSC Dir');
+ my $pkg = $self->get('Package');
+ my $build_dir = $self->get('Build Dir');
+ my $host_arch = $self->get('Host Arch');
+ my $build_arch = $self->get('Build Arch');
+ my $session = $self->get('Session');
+
+ my( $rv, $changes );
+ local( *PIPE, *F, *F2 );
+
+ $self->log_subsection("Build");
+ $self->set('This Space', 0);
+
+ my $tmpunpackdir = $dscdir;
+ $tmpunpackdir =~ s/-.*$/.orig.tmp-nest/;
+ $tmpunpackdir =~ s/_/-/;
+ $tmpunpackdir = "$build_dir/$tmpunpackdir";
+
+ $dscdir = "$build_dir/$dscdir";
+
+ $self->log_subsubsection("Unpack source");
+ if ($session->test_directory($dscdir) && $session->test_symlink($dscdir)) {
+ # if the package dir already exists but is a symlink, complain
+ $self->log_error("Cannot unpack source: a symlink to a directory with the\n".
+ "same name already exists.\n");
+ return 0;
+ }
+ my $dsccontent = $session->read_file("$build_dir/$dscfile");
+ if (!$dsccontent) {
+ $self->log_error("Cannot read $build_dir/$dscfile\n");
+ } else {
+ $self->log($dsccontent);
+ $self->log("\n");
+ }
+ if (!$session->test_directory($dscdir)) {
+ $self->set('Sub Task', "dpkg-source");
+ $session->run_command({
+ COMMAND => [$self->get_conf('DPKG_SOURCE'),
+ '-x', $dscfile, $dscdir],
+ USER => $self->get_conf('BUILD_USER'),
+ DIR => $build_dir,
+ PRIORITY => 0});
+ if ($?) {
+ $self->log_error("FAILED [dpkg-source died]\n");
+ Sbuild::Exception::Build->throw(error => "FAILED [dpkg-source died]",
+ failstage => "unpack");
+ }
+
+ if (!$session->chmod($dscdir, 'g-s,go+rX', { RECURSIVE => 1 })) {
+ $self->log_error("chmod -R g-s,go+rX $dscdir failed.\n");
+ Sbuild::Exception::Build->throw(error => "chmod -R g-s,go+rX $dscdir failed",
+ failstage => "unpack");
+ }
+ }
+ else {
+ $self->log_subsubsection("Check unpacked source");
+ # check if the unpacked tree is really the version we need
+ my $clog = $session->read_command(
+ { COMMAND => ['dpkg-parsechangelog'],
+ USER => $self->get_conf('BUILD_USER'),
+ PRIORITY => 0,
+ DIR => $dscdir});
+ if (!$clog) {
+ $self->log_error("unable to read from dpkg-parsechangelog\n");
+ Sbuild::Exception::Build->throw(error => "unable to read from dpkg-parsechangelog",
+ failstage => "check-unpacked-version");
+ }
+ $self->set('Sub Task', "dpkg-parsechangelog");
+
+ if ($clog !~ /^Version:\s*(.+)\s*$/mi) {
+ $self->log_error("dpkg-parsechangelog didn't print Version:\n");
+ Sbuild::Exception::Build->throw(error => "dpkg-parsechangelog didn't print Version:",
+ failstage => "check-unpacked-version");
+ }
+ }
+
+ $self->log_subsubsection("Check disk space");
+ chomp(my $current_usage = $session->read_command({ COMMAND => ["du", "-k", "-s", "$dscdir"]}));
+ if ($?) {
+ $self->log_error("du exited with non-zero exit status $?\n");
+ Sbuild::Exception::Build->throw(error => "du exited with non-zero exit status $?", failstage => "check-space");
+ }
+ $current_usage =~ /^(\d+)/;
+ $current_usage = $1;
+ if ($current_usage) {
+ my $pipe = $session->pipe_command({ COMMAND => ["df", "-k", "$dscdir"]});
+ my $free;
+ while (<$pipe>) {
+ $free = (split /\s+/)[3];
+ }
+ close $pipe;
+ if ($?) {
+ $self->log_error("df exited with non-zero exit status $?\n");
+ Sbuild::Exception::Build->throw(error => "df exited with non-zero exit status $?", failstage => "check-space");
+ }
+ if ($free < 2*$current_usage && $self->get_conf('CHECK_SPACE')) {
+ Sbuild::Exception::Build->throw(error => "Disk space is probably not sufficient for building.",
+ info => "Source needs $current_usage KiB, while $free KiB is free.)",
+ failstage => "check-space");
+ } else {
+ $self->log("Sufficient free space for build\n");
+ }
+ }
+
+ my $clogpipe = $session->pipe_command(
+ { COMMAND => ['dpkg-parsechangelog'],
+ USER => $self->get_conf('BUILD_USER'),
+ PRIORITY => 0,
+ DIR => $dscdir });
+ if (!$clogpipe) {
+ $self->log_error("unable to read from dpkg-parsechangelog\n");
+ Sbuild::Exception::Build->throw(error => "unable to read from dpkg-parsechangelog",
+ failstage => "check-unpacked-version");
+ }
+
+ my $clog = Dpkg::Control->new(type => CTRL_CHANGELOG);
+ if (!$clog->parse($clogpipe, "$dscdir/debian/changelog")) {
+ $self->log_error("unable to parse debian/changelog\n");
+ Sbuild::Exception::Build->throw(error => "unable to parse debian/changelog",
+ failstage => "check-unpacked-version");
+ }
+
+ close($clogpipe);
+
+ my $name = $clog->{Source};
+ my $version = $clog->{Version};
+ my $dists = $clog->{Distribution};
+ my $urgency = $clog->{Urgency};
+
+ if ($dists ne $self->get_conf('DISTRIBUTION')) {
+ $self->build_log_colour('yellow',
+ "^Distribution: " . $self->get_conf('DISTRIBUTION') . "\$");
+ }
+
+ if ($self->get_conf('BIN_NMU') || $self->get_conf('APPEND_TO_VERSION')
+ || defined $self->get_conf('BIN_NMU_CHANGELOG')) {
+ $self->log_subsubsection("Hack binNMU version");
+
+ my $text = $session->read_file("$dscdir/debian/changelog");
+
+ if (!$text) {
+ $self->log_error("Can't open debian/changelog -- no binNMU hack!\n");
+ Sbuild::Exception::Build->throw(error => "Can't open debian/changelog -- no binNMU hack: $!!",
+ failstage => "hack-binNMU");
+ }
+
+ my $NMUversion = $self->get('Version');
+
+ my $clogpipe = $session->get_write_file_handle("$dscdir/debian/changelog");
+
+ if (!$clogpipe) {
+ $self->log_error("Can't open debian/changelog for binNMU hack: $!\n");
+ Sbuild::Exception::Build->throw(error => "Can't open debian/changelog for binNMU hack: $!",
+ failstage => "hack-binNMU");
+ }
+ if (defined $self->get_conf('BIN_NMU_CHANGELOG')) {
+ my $clogentry = $self->get_conf('BIN_NMU_CHANGELOG');
+ # trim leading and trailing whitespace and linebreaks
+ $clogentry =~ s/^\s+|\s+$//g;
+ print $clogpipe $clogentry . "\n\n";
+ } else {
+ if (!$self->get_conf('MAINTAINER_NAME')) {
+ Sbuild::Exception::Build->throw(error => "No maintainer specified.",
+ info => 'When making changelog additions for a binNMU or appending a version suffix, a maintainer must be specified for the changelog entry e.g. using $maintainer_name, $uploader_name or $key_id, (or the equivalent command-line options)',
+ failstage => "check-space");
+ }
+
+ $dists = $self->get_conf('DISTRIBUTION');
+
+ print $clogpipe "$name ($NMUversion) $dists; urgency=low, binary-only=yes\n\n";
+ if ($self->get_conf('APPEND_TO_VERSION')) {
+ print $clogpipe " * Append ", $self->get_conf('APPEND_TO_VERSION'),
+ " to version number; no source changes\n";
+ }
+ if ($self->get_conf('BIN_NMU')) {
+ print $clogpipe " * Binary-only non-maintainer upload for $host_arch; ",
+ "no source changes.\n";
+ print $clogpipe " * ", join( " ", split( "\n", $self->get_conf('BIN_NMU') )), "\n";
+ }
+ print $clogpipe "\n";
+
+ # Earlier implementations used the date of the last changelog
+ # entry for the new entry so that Multi-Arch:same packages would
+ # be co-installable (their shared changelogs had to match). This
+ # is not necessary anymore as binNMU changelogs are now written
+ # into architecture specific paths. Re-using the date of the last
+ # changelog entry has the disadvantage that this will effect
+ # SOURCE_DATE_EPOCH which in turn will make the timestamps of the
+ # files in the new package equal to the last version which can
+ # confuse backup programs. By using the build date for the new
+ # binNMU changelog timestamp we make sure that the timestamps of
+ # changed files inside the new package advanced in comparison to
+ # the last version.
+ #
+ # The timestamp format has to follow Debian Policy §4.4
+ # https://www.debian.org/doc/debian-policy/ch-source.html#s-dpkgchangelog
+ # which is the same format as `date -R`
+ my $date;
+ if (defined $self->get_conf('BIN_NMU_TIMESTAMP')) {
+ if ($self->get_conf('BIN_NMU_TIMESTAMP') =~ /^\+?[1-9]\d*$/) {
+ $date = strftime_c "%a, %d %b %Y %H:%M:%S +0000",
+ gmtime($self->get_conf('BIN_NMU_TIMESTAMP'));
+ } else {
+ $date = $self->get_conf('BIN_NMU_TIMESTAMP');
+ }
+ } else {
+ $date = strftime_c "%a, %d %b %Y %H:%M:%S +0000", gmtime();
+ }
+ print $clogpipe " -- " . $self->get_conf('MAINTAINER_NAME') . " $date\n\n";
+ }
+ print $clogpipe $text;
+ close($clogpipe);
+ $self->log("Created changelog entry for binNMU version $NMUversion\n");
+ }
+
+ if ($session->test_regular_file("$dscdir/debian/files")) {
+ local( *FILES );
+ my @lines;
+ my $FILES = $session->get_read_file_handle("$dscdir/debian/files");
+ chomp( @lines = <$FILES> );
+ close( $FILES );
+
+ $self->log_warning("After unpacking, there exists a file debian/files with the contents:\n");
+
+ $self->log_sep();
+ foreach (@lines) {
+ $self->log($_);
+ }
+ $self->log_sep();
+ $self->log("\n");
+
+ $self->log_info("This should be reported as a bug.\n");
+ $self->log_info("The file has been removed to avoid dpkg-genchanges errors.\n");
+
+ unlink "$dscdir/debian/files";
+ }
+
+ # Build tree not writable during build (except for the sbuild
+ # user performing the build).
+ if (!$session->chmod($self->get('Build Dir'), 'go-w', { RECURSIVE => 1 })) {
+ $self->log_error("chmod og-w " . $self->get('Build Dir') . " failed.\n");
+ return 0;
+ }
+
+ if (!$self->run_external_commands("starting-build-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute starting-build-commands",
+ failstage => "run-starting-build-commands");
+ }
+
+ $self->set('Build Start Time', time);
+ $self->set('Build End Time', $self->get('Build Start Time'));
+
+ if ($session->test_regular_file("/etc/ld.so.conf") &&
+ ! $session->test_regular_file_readable("/etc/ld.so.conf")) {
+ $session->chmod('/etc/ld.so.conf', 'a+r');
+
+ $self->log_subsubsection("Fix ld.so");
+ $self->log("ld.so.conf was not readable! Fixed.\n");
+ }
+
+ my $buildcmd = [];
+ if (length $self->get_conf('BUILD_ENV_CMND') ) {
+ push( @{$buildcmd}, $self->get_conf('BUILD_ENV_CMND') );
+ }
+ push (@{$buildcmd}, 'dpkg-buildpackage');
+
+ 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 = $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);
+ }
+ }
+ # since dpkg 1.20.0
+ # will reset environment and umask to their vendor specific defaults
+ if ($dpkgversion >= "1.20.0") {
+ push (@{$buildcmd}, '--sanitize-env');
+ }
+
+ if ($host_arch ne $build_arch) {
+ push (@{$buildcmd}, '-a' . $host_arch);
+ }
+
+ if (length $self->get_conf('BUILD_PROFILES')) {
+ my $profiles = $self->get_conf('BUILD_PROFILES');
+ $profiles =~ tr/ /,/;
+ push (@{$buildcmd}, '-P' . $profiles);
+ }
+
+ if (defined $self->get_conf('PGP_OPTIONS')) {
+ if (ref($self->get_conf('PGP_OPTIONS')) eq 'ARRAY') {
+ push (@{$buildcmd}, @{$self->get_conf('PGP_OPTIONS')});
+ } elsif (length $self->get_conf('PGP_OPTIONS')) {
+ push (@{$buildcmd}, $self->get_conf('PGP_OPTIONS'));
+ }
+ }
+
+ if (defined $self->get_conf('SIGNING_OPTIONS')) {
+ if (ref($self->get_conf('SIGNING_OPTIONS')) eq 'ARRAY') {
+ push (@{$buildcmd}, @{$self->get_conf('SIGNING_OPTIONS')});
+ } elsif (length $self->get_conf('SIGNING_OPTIONS')) {
+ push (@{$buildcmd}, $self->get_conf('SIGNING_OPTIONS'));
+ }
+ }
+
+ use constant dpkgopt => [[["", "-B"], ["-A", "-b" ]], [["-S", "-G"], ["-g", ""]]];
+ my $binopt = dpkgopt->[$self->get_conf('BUILD_SOURCE')]
+ [$self->get_conf('BUILD_ARCH_ALL')]
+ [$self->get_conf('BUILD_ARCH_ANY')];
+ push (@{$buildcmd}, $binopt) if $binopt;
+ push (@{$buildcmd}, "-r" . $self->get_conf('FAKEROOT'));
+
+ if ($self->get_conf('DPKG_FILE_SUFFIX')) {
+ my $dpkg_version_ok = Dpkg::Version->new("1.18.11");
+ if ($self->get('Dpkg Version') >= $dpkg_version_ok) {
+ my $changes = $self->get_changes();
+ push (@{$buildcmd}, "--changes-option=-O../$changes");
+ my $buildinfo = $self->get_buildinfo();
+ push (@{$buildcmd}, "--buildinfo-option=-O../$buildinfo");
+ } else {
+ $self->log("Ignoring dpkg file suffix: dpkg version too old\n");
+ $self->set_conf('DPKG_FILE_SUFFIX',undef);
+ }
+ }
+
+ if (defined $self->get_conf('DPKG_BUILDPACKAGE_USER_OPTIONS')) {
+ push (@{$buildcmd}, @{$self->get_conf('DPKG_BUILDPACKAGE_USER_OPTIONS')});
+ }
+
+ # Set up additional build environment variables.
+ my %buildenv = %{$self->get_conf('BUILD_ENVIRONMENT')};
+ $buildenv{'PATH'} = $self->get_conf('PATH');
+ $buildenv{'LD_LIBRARY_PATH'} = $self->get_conf('LD_LIBRARY_PATH')
+ if defined($self->get_conf('LD_LIBRARY_PATH'));
+
+ # Add cross environment config
+ if ($host_arch ne $build_arch) {
+ $buildenv{'CONFIG_SITE'} = "/etc/dpkg-cross/cross-config." . $host_arch;
+ # when cross-building, only set "nocheck" if DEB_BUILD_OPTIONS
+ # was not already set. This allows overwriting the default by
+ # setting the DEB_BUILD_OPTIONS environment variable
+ if (!defined($ENV{'DEB_BUILD_OPTIONS'})) {
+ $ENV{'DEB_BUILD_OPTIONS'} = "nocheck";
+ }
+ }
+
+ # Explicitly add any needed environment to the environment filter
+ # temporarily for dpkg-buildpackage.
+ my @env_filter;
+ foreach my $envvar (keys %buildenv) {
+ push(@env_filter, "^$envvar\$");
+ }
+
+ # Dump build environment
+ $self->log_subsubsection("User Environment");
+ {
+ my $envcmd = $session->read_command(
+ { COMMAND => ['env'],
+ ENV => \%buildenv,
+ ENV_FILTER => \@env_filter,
+ USER => $self->get_conf('BUILD_USER'),
+ SETSID => 1,
+ PRIORITY => 0,
+ DIR => $dscdir
+ });
+ if (!$envcmd) {
+ $self->log_error("unable to open pipe\n");
+ Sbuild::Exception::Build->throw(error => "unable to open pipe",
+ failstage => "dump-build-env");
+ }
+
+ my @lines=sort(split /\n/, $envcmd);
+ foreach my $line (@lines) {
+ $self->log("$line\n");
+ }
+ }
+
+ $self->log_subsubsection("dpkg-buildpackage");
+ $self->log("Command: " . join(' ', @{$buildcmd}) . "\n");
+
+ my $command = {
+ COMMAND => $buildcmd,
+ ENV => \%buildenv,
+ ENV_FILTER => \@env_filter,
+ USER => $self->get_conf('BUILD_USER'),
+ SETSID => 1,
+ PRIORITY => 0,
+ DIR => $dscdir,
+ STREAMERR => \*STDOUT,
+ DISABLE_NETWORK => 1,
+ };
+
+ my $pipe = $session->pipe_command($command);
+ if (!$pipe) {
+ $self->log_error("unable to open pipe\n");
+ Sbuild::Exception::Build->throw(error => "unable to open pipe",
+ failstage => "dpkg-buildpackage");
+ }
+
+ $self->set('dpkg-buildpackage pid', $command->{'PID'});
+ $self->set('Sub Task', "dpkg-buildpackage");
+
+ my $timeout = $self->get_conf('INDIVIDUAL_STALLED_PKG_TIMEOUT')->{$pkg} ||
+ $self->get_conf('STALLED_PKG_TIMEOUT');
+ $timeout *= 60;
+ my $timed_out = 0;
+ my(@timeout_times, @timeout_sigs, $last_time);
+
+ local $SIG{'ALRM'} = sub {
+ my $pid = $self->get('dpkg-buildpackage pid');
+ my $signal = ($timed_out > 0) ? "KILL" : "TERM";
+ # negative pid to send to whole process group
+ kill "$signal", -$pid;
+
+ $timeout_times[$timed_out] = time - $last_time;
+ $timeout_sigs[$timed_out] = $signal;
+ $timed_out++;
+ $timeout = 5*60; # only wait 5 minutes until next signal
+ };
+
+ alarm($timeout);
+ # We do not use a while(<$pipe>) {} loop because that one would only read
+ # full lines (until $/ is reached). But we do not want to tie "activity"
+ # to receiving complete lines on standard output and standard error.
+ # Receiving any data should be sufficient for a process to signal that it
+ # is still active. Thus, instead of reading lines, we use sysread() which
+ # will return us data once it is available even if the data is not
+ # terminated by a newline. To still print correctly to the log, we collect
+ # unterminated strings into an accumulator and print them to the log once
+ # the newline shows up. This has the added advantage that we can now not
+ # only treat \n as producing new lines ($/ is limited to a single
+ # character) but can also produce new lines when encountering a \r as it
+ # is common for progress-meter output of long-running processes.
+ my $acc = "";
+ while(1) {
+ alarm($timeout);
+ $last_time = time;
+ # The buffer size is really arbitrary and just makes sure not to call
+ # this function too often if lots of data is produced by the build.
+ # The function will immediately return even with less data than the
+ # buffer size once it is available.
+ my $ret = sysread($pipe, my $buf, 1024);
+ # sysread failed - this for example happens when the build timeouted
+ # and is killed as a result
+ if (!defined $ret) {
+ last;
+ }
+ # A return value of 0 signals EOF
+ if ($ret == 0) {
+ last;
+ }
+ # We choose that lines shall not only be terminated by \n but that new
+ # log lines are also produced after encountering a \r.
+ # A negative limit is used to also produce trailing empty fields if
+ # required (think of multiple trailing empty lines).
+ my @parts = split /\r|\n/, $buf, -1;
+ my $numparts = scalar @parts;
+ if ($numparts == 1) {
+ # line terminator was not found
+ $acc .= $buf;
+ } elsif ($numparts >= 2) {
+ # first match needs special treatment as it needs to be
+ # concatenated with $acc
+ my $first = shift @parts;
+ $self->log($acc . $first . "\n");
+ my $last = pop @parts;
+ for (my $i = 0; $i < $numparts - 2; $i++) {
+ $self->log($parts[$i] . "\n");
+ }
+ # the last part is put into the accumulator. This might
+ # just be the empty string if $buf ended in a line
+ # terminator
+ $acc = $last;
+ }
+ }
+ # If the output didn't end with a line terminator, just print out the rest
+ # as we have it.
+ if ($acc ne "") {
+ $self->log($acc . "\n");
+ }
+ close($pipe);
+ alarm(0);
+ $rv = $?;
+ $self->set('dpkg-buildpackage pid', undef);
+
+ my $i;
+ for( $i = 0; $i < $timed_out; ++$i ) {
+ $self->log_error("Build killed with signal " . $timeout_sigs[$i] .
+ " after " . int($timeout_times[$i]/60) .
+ " minutes of inactivity\n");
+ }
+ $self->set('Build End Time', time);
+ $self->set('Pkg End Time', time);
+ $self->set('This Time', $self->get('Pkg End Time') - $self->get('Pkg Start Time'));
+ $self->set('This Time', 0) if $self->get('This Time') < 0;
+
+ $self->write_stats('build-time',
+ $self->get('Build End Time')-$self->get('Build Start Time'));
+ $self->write_stats('install-download-time',
+ $self->get('Install End Time')-$self->get('Install Start Time'));
+ my $finish_date = strftime_c "%FT%TZ", gmtime($self->get('Build End Time'));
+ $self->log_sep();
+ $self->log("Build finished at $finish_date\n");
+
+
+ if (!$self->run_external_commands("finished-build-commands")) {
+ Sbuild::Exception::Build->throw(error => "Failed to execute finished-build-commands",
+ failstage => "run-finished-build-commands");
+ }
+
+ my @space_files = ();
+
+ $self->log_subsubsection("Finished");
+ if ($rv) {
+ Sbuild::Exception::Build->throw(error => "Build failure (dpkg-buildpackage died)",
+ failstage => "build");
+ } else {
+ $self->log_info("Built successfully\n");
+
+ if ($session->test_regular_file_readable("$dscdir/debian/files")) {
+ my @files = $self->debian_files_list("$dscdir/debian/files");
+
+ foreach (@files) {
+ if (!$session->test_regular_file("$build_dir/$_")) {
+ $self->log_error("Package claims to have built ".basename($_).", but did not. This is a bug in the packaging.\n");
+ next;
+ }
+ if (/_all.u?deb$/ and not $self->get_conf('BUILD_ARCH_ALL')) {
+ $self->log_error("Package builds ".basename($_)." when binary-indep target is not called. This is a bug in the packaging.\n");
+ $session->unlink("$build_dir/$_");
+ next;
+ }
+ }
+ }
+
+ # Restore write access to build tree now build is complete.
+ if (!$session->chmod($self->get('Build Dir'), 'g+w', { RECURSIVE => 1 })) {
+ $self->log_error("chmod g+w " . $self->get('Build Dir') . " failed.\n");
+ return 0;
+ }
+
+ $self->log_subsection("Changes");
+
+ # we use an anonymous subroutine so that the referenced variables are
+ # automatically rebound to their current values
+ my $copy_changes = sub {
+ my $changes = shift;
+
+ my $F = $session->get_read_file_handle("$build_dir/$changes");
+ if (!$F) {
+ $self->log_error("cannot get read file handle for $build_dir/$changes\n");
+ Sbuild::Exception::Build->throw(error => "cannot get read file handle for $build_dir/$changes",
+ failstage => "parse-changes");
+ }
+ my $pchanges = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
+ if (!$pchanges->parse($F, "$build_dir/$changes")) {
+ $self->log_error("cannot parse $build_dir/$changes\n");
+ Sbuild::Exception::Build->throw(error => "cannot parse $build_dir/$changes",
+ failstage => "parse-changes");
+ }
+ close($F);
+
+
+ if ($self->get_conf('OVERRIDE_DISTRIBUTION')) {
+ $pchanges->{Distribution} = $self->get_conf('DISTRIBUTION');
+ }
+
+ my $sys_build_dir = $self->get_conf('BUILD_DIR');
+ my $F2 = $session->get_write_file_handle("$build_dir/$changes.new");
+ if (!$F2) {
+ $self->log("Cannot create $build_dir/$changes.new\n");
+ $self->log("Distribution field may be wrong!!!\n");
+ if ($build_dir) {
+ if(!$session->copy_from_chroot("$build_dir/$changes", ".")) {
+ $self->log_error("Could not copy $build_dir/$changes to .\n");
+ }
+ }
+ } else {
+ $pchanges->output(\*STDOUT);
+ $pchanges->output(\*$F2);
+
+ close( $F2 );
+
+ $session->rename("$build_dir/$changes.new", "$build_dir/$changes");
+ if ($?) {
+ $self->log("$build_dir/$changes.new could not be " .
+ "renamed to $build_dir/$changes: $?\n");
+ $self->log("Distribution field may be wrong!!!");
+ }
+ if ($build_dir) {
+ if (!$session->copy_from_chroot("$build_dir/$changes", "$sys_build_dir")) {
+ $self->log("Could not copy $build_dir/$changes to $sys_build_dir");
+ }
+ }
+ }
+
+ return $pchanges;
+ };
+
+ $changes = $self->get_changes();
+ if (!defined($changes)) {
+ $self->log_error(".changes is undef. Cannot copy build results.\n");
+ return 0;
+ }
+ my @cfiles;
+ if ($session->test_regular_file_readable("$build_dir/$changes")) {
+ my(@do_dists, @saved_dists);
+ $self->log_subsubsection("$changes:");
+
+ my $pchanges = &$copy_changes($changes);
+ $self->set('Changes File', $self->get_conf('BUILD_DIR') . "/$changes");
+
+ my $checksums = Dpkg::Checksums->new();
+ $checksums->add_from_control($pchanges);
+
+ push(@cfiles, $checksums->get_files());
+
+ }
+ else {
+ $self->log_error("Can't find $changes -- can't dump info\n");
+ }
+
+ if ($self->get_conf('SOURCE_ONLY_CHANGES')) {
+ my $so_changes = $self->get('Package_SVersion') . "_source.changes";
+ $self->log_subsubsection("$so_changes:");
+ my $genchangescmd = ['dpkg-genchanges', '--build=source'];
+ if (defined $self->get_conf('SIGNING_OPTIONS')) {
+ if (ref($self->get_conf('SIGNING_OPTIONS')) eq 'ARRAY') {
+ push (@{$genchangescmd}, @{$self->get_conf('SIGNING_OPTIONS')});
+ } elsif (length $self->get_conf('SIGNING_OPTIONS')) {
+ push (@{$genchangescmd}, $self->get_conf('SIGNING_OPTIONS'));
+ }
+ }
+ my $changes_opts = $self->get_changes_opts();
+ if ($changes_opts) {
+ push (@{$genchangescmd}, @{$changes_opts});
+ }
+ my $cfile = $session->read_command(
+ { COMMAND => $genchangescmd,
+ USER => $self->get_conf('BUILD_USER'),
+ PRIORITY => 0,
+ DIR => $dscdir});
+ if (!$cfile) {
+ $self->log_error("dpkg-genchanges --build=source failed\n");
+ Sbuild::Exception::Build->throw(error => "dpkg-genchanges --build=source failed",
+ failstage => "source-only-changes");
+ }
+ if (!$session->write_file("$build_dir/$so_changes", $cfile)) {
+ $self->log_error("cannot write content to $build_dir/$so_changes\n");
+ Sbuild::Exception::Build->throw(error => "cannot write content to $build_dir/$so_changes",
+ failstage => "source-only-changes");
+ }
+
+ my $pchanges = &$copy_changes($so_changes);
+ }
+
+ $self->log_subsection("Buildinfo");
+
+ foreach (@cfiles) {
+ my $deb = "$build_dir/$_";
+ next if $deb !~ /\.buildinfo$/;
+ my $buildinfo = $session->read_file($deb);
+ if (!$buildinfo) {
+ $self->log_error("Cannot read $deb\n");
+ } else {
+ $self->log($buildinfo);
+ $self->log("\n");
+ }
+ }
+
+ $self->log_subsection("Package contents");
+
+ my @debcfiles = @cfiles;
+ foreach (@debcfiles) {
+ my $deb = "$build_dir/$_";
+ next if $deb !~ /(\Q$host_arch\E|all)\.(udeb|deb)$/;
+
+ $self->log_subsubsection("$_");
+ my $dpkg_info = $session->read_command({COMMAND => ["dpkg", "--info", $deb]});
+ if (!$dpkg_info) {
+ $self->log_error("Can't spawn dpkg: $! -- can't dump info\n");
+ }
+ else {
+ $self->log($dpkg_info);
+ }
+ $self->log("\n");
+ my $dpkg_contents = $session->read_command({COMMAND => ["sh", "-c", "dpkg --contents $deb 2>&1 | sort -k6"]});
+ if (!$dpkg_contents) {
+ $self->log_error("Can't spawn dpkg: $! -- can't dump info\n");
+ }
+ else {
+ $self->log($dpkg_contents);
+ }
+ $self->log("\n");
+ }
+
+ foreach (@cfiles) {
+ push( @space_files, $self->get_conf('BUILD_DIR') . "/$_");
+ if (!$session->copy_from_chroot("$build_dir/$_", $self->get_conf('BUILD_DIR'))) {
+ $self->log_error("Could not copy $build_dir/$_ to " . $self->get_conf('BUILD_DIR') . "\n");
+ }
+ }
+ }
+
+ $self->set('This Space', $self->check_space(@space_files));
+
+ return $rv == 0 ? 1 : 0;
+}
+
+# Produce a hash suitable for ENV export
+sub get_env ($$) {
+ my $self = shift;
+ my $prefix = shift;
+
+ sub _env_loop ($$$$) {
+ my ($env,$ref,$keysref,$prefix) = @_;
+
+ foreach my $key (keys( %{ $keysref } )) {
+ my $value = $ref->get($key);
+ next if (!defined($value));
+ next if (ref($value));
+ my $name = "${prefix}${key}";
+ $name =~ s/ /_/g;
+ $env->{$name} = $value;
+ }
+ }
+
+ my $envlist = {};
+ _env_loop($envlist, $self, $self, $prefix);
+ _env_loop($envlist, $self->get('Config'), $self->get('Config')->{'KEYS'}, "${prefix}CONF_");
+ return $envlist;
+}
+
+sub get_build_filename {
+ my $self=shift;
+ my $filetype=shift;
+ my $changes = $self->get('Package_SVersion');
+
+ if ($self->get_conf('BUILD_ARCH_ANY')) {
+ $changes .= '_' . $self->get('Host Arch');
+ } elsif ($self->get_conf('BUILD_ARCH_ALL')) {
+ $changes .= "_all";
+ } elsif ($self->get_conf('BUILD_SOURCE')) {
+ $changes .= "_source";
+ }
+
+ my $suffix = $self->get_conf('DPKG_FILE_SUFFIX');
+ $changes .= $suffix if ($suffix);
+
+ $changes .= '.' . $filetype;
+
+ return $changes;
+}
+
+sub get_changes {
+ my $self=shift;
+ return $self->get_build_filename("changes");
+}
+
+sub get_buildinfo {
+ my $self=shift;
+ return $self->get_build_filename("buildinfo");
+}
+
+sub check_space {
+ my $self = shift;
+ my @files = @_;
+ my $sum = 0;
+
+ my $dscdir = $self->get('DSC Dir');
+ return -1 unless (defined $dscdir);
+
+ my $build_dir = $self->get('Build Dir');
+ my $pkgbuilddir = "$build_dir/$dscdir";
+
+ # if the source package was not yet unpacked, we will not attempt to compute
+ # the required space.
+ return -1 unless ($self->get('Session')->test_directory($pkgbuilddir));
+
+ my ($space, $spacenum);
+
+ # get the required space for the unpacked source package in the chroot
+ $space = $self->get('Session')->read_command(
+ { COMMAND => ['du', '-k', '-s', $pkgbuilddir],
+ USER => $self->get_conf('USERNAME'),
+ PRIORITY => 0,
+ DIR => '/'});
+
+ if (!$space) {
+ $self->log_error("Cannot determine space needed for $pkgbuilddir (du failed)\n");
+ return -1;
+ }
+ # remove the trailing path from the du output
+ if (($spacenum) = $space =~ /^(\d+)/) {
+ $sum += $spacenum;
+ } else {
+ $self->log_error("Cannot determine space needed for $pkgbuilddir (unexpected du output): $space\n");
+ return -1;
+ }
+
+ # get the required space for all produced build artifacts on the host
+ # running sbuild
+ foreach my $file (@files) {
+ $space = $self->get('Host')->read_command(
+ { COMMAND => ['du', '-k', '-s', $file],
+ USER => $self->get_conf('USERNAME'),
+ PRIORITY => 0,
+ DIR => '/'});
+
+ if (!$space) {
+ $self->log_error("Cannot determine space needed for $file (du failed): $!\n");
+ return -1;
+ }
+ # remove the trailing path from the du output
+ if (($spacenum) = $space =~ /^(\d+)/) {
+ $sum += $spacenum;
+ } else {
+ $self->log_error("Cannot determine space needed for $file (unexpected du output): $space\n");
+ return -1;
+ }
+ }
+
+ return $sum;
+}
+
+sub lock_file {
+ my $self = shift;
+ my $file = shift;
+ my $for_srcdep = shift;
+ my $lockfile = "$file.lock";
+ my $try = 0;
+
+ repeat:
+ if (!sysopen( F, $lockfile, O_WRONLY|O_CREAT|O_TRUNC|O_EXCL, 0644 )){
+ if ($! == EEXIST) {
+ # lock file exists, wait
+ goto repeat if !open( F, "<$lockfile" );
+ my $line = <F>;
+ my ($pid, $user);
+ close( F );
+ if ($line !~ /^(\d+)\s+([\w\d.-]+)$/) {
+ $self->log_warning("Bad lock file contents ($lockfile) -- still trying\n");
+ }
+ else {
+ ($pid, $user) = ($1, $2);
+ if (kill( 0, $pid ) == 0 && $! == ESRCH) {
+ # process doesn't exist anymore, remove stale lock
+ $self->log_warning("Removing stale lock file $lockfile ".
+ "(pid $pid, user $user)\n");
+ unlink( $lockfile );
+ goto repeat;
+ }
+ }
+ ++$try;
+ if (!$for_srcdep && $try > $self->get_conf('MAX_LOCK_TRYS')) {
+ $self->log_warning("Lockfile $lockfile still present after " .
+ $self->get_conf('MAX_LOCK_TRYS') *
+ $self->get_conf('LOCK_INTERVAL') .
+ " seconds -- giving up\n");
+ return;
+ }
+ $self->log("Another sbuild process ($pid by $user) is currently installing or removing packages -- waiting...\n")
+ if $for_srcdep && $try == 1;
+ sleep $self->get_conf('LOCK_INTERVAL');
+ goto repeat;
+ }
+ $self->log_warning("Can't create lock file $lockfile: $!\n");
+ }
+
+ my $username = $self->get_conf('USERNAME');
+ F->print("$$ $username\n");
+ F->close();
+}
+
+sub unlock_file {
+ my $self = shift;
+ my $file = shift;
+ my $lockfile = "$file.lock";
+
+ unlink( $lockfile );
+}
+
+sub add_stat {
+ my $self = shift;
+ my $key = shift;
+ my $value = shift;
+
+ $self->get('Summary Stats')->{$key} = $value;
+}
+
+sub generate_stats {
+ my $self = shift;
+ my $resolver = $self->get('Dependency Resolver');
+
+ $self->add_stat('Job', $self->get('Job'));
+ $self->add_stat('Package', $self->get('Package'));
+ # If the package fails early, then the version might not yet be known.
+ # This can happen if the user only specified a source package name on the
+ # command line and then the version will only be known after the source
+ # package was successfully downloaded.
+ if ($self->get('Version')) {
+ $self->add_stat('Version', $self->get('Version'));
+ }
+ if ($self->get('OVersion')) {
+ $self->add_stat('Source-Version', $self->get('OVersion'));
+ }
+ $self->add_stat('Machine Architecture', $self->get_conf('ARCH'));
+ $self->add_stat('Host Architecture', $self->get('Host Arch'));
+ $self->add_stat('Build Architecture', $self->get('Build Arch'));
+ $self->add_stat('Build Profiles', $self->get('Build Profiles'))
+ if $self->get('Build Profiles');
+ $self->add_stat('Build Type', $self->get('Build Type'));
+ my @keylist;
+ if (defined $resolver) {
+ @keylist=keys %{$resolver->get('Initial Foreign Arches')};
+ push @keylist, keys %{$resolver->get('Added Foreign Arches')};
+ }
+ my $foreign_arches = join ' ', @keylist;
+ $self->add_stat('Foreign Architectures', $foreign_arches )
+ if $foreign_arches;
+ $self->add_stat('Distribution', $self->get_conf('DISTRIBUTION'));
+ if ($self->get('This Space') >= 0) {
+ $self->add_stat('Space', $self->get('This Space'));
+ } else {
+ $self->add_stat('Space', "n/a");
+ }
+ $self->add_stat('Build-Time',
+ $self->get('Build End Time')-$self->get('Build Start Time'));
+ $self->add_stat('Install-Time',
+ $self->get('Install End Time')-$self->get('Install Start Time'));
+ $self->add_stat('Package-Time',
+ $self->get('Pkg End Time')-$self->get('Pkg Start Time'));
+ if ($self->get('This Space') >= 0) {
+ $self->add_stat('Build-Space', $self->get('This Space'));
+ } else {
+ $self->add_stat('Build-Space', "n/a");
+ }
+ $self->add_stat('Status', $self->get_status());
+ $self->add_stat('Fail-Stage', $self->get('Pkg Fail Stage'))
+ if ($self->get_status() ne "successful");
+ $self->add_stat('Lintian', $self->get('Lintian Reason'))
+ if $self->get('Lintian Reason');
+ $self->add_stat('Piuparts', $self->get('Piuparts Reason'))
+ if $self->get('Piuparts Reason');
+ $self->add_stat('Autopkgtest', $self->get('Autopkgtest Reason'))
+ if $self->get('Autopkgtest Reason');
+}
+
+sub log_stats {
+ my $self = shift;
+ foreach my $stat (sort keys %{$self->get('Summary Stats')}) {
+ $self->log("${stat}: " . $self->get('Summary Stats')->{$stat} . "\n");
+ }
+}
+
+sub print_stats {
+ my $self = shift;
+ foreach my $stat (sort keys %{$self->get('Summary Stats')}) {
+ print STDOUT "${stat}: " . $self->get('Summary Stats')->{$stat} . "\n";
+ }
+}
+
+sub write_stats {
+ my $self = shift;
+
+ return if (!$self->get_conf('BATCH_MODE'));
+
+ my $stats_dir = $self->get_conf('STATS_DIR');
+
+ return if not defined $stats_dir;
+
+ if (! -d $stats_dir &&
+ !mkdir $stats_dir) {
+ $self->log_warning("Could not create $stats_dir: $!\n");
+ return;
+ }
+
+ my ($cat, $val) = @_;
+ local( *F );
+
+ $self->lock_file($stats_dir, 0);
+ open( F, ">>$stats_dir/$cat" );
+ print F "$val\n";
+ close( F );
+ $self->unlock_file($stats_dir);
+}
+
+sub debian_files_list {
+ my $self = shift;
+ my $files = shift;
+
+ my @list;
+
+ debug("Parsing $files\n");
+ my $session = $self->get('Session');
+
+ my $pipe = $session->get_read_file_handle($files);
+ if ($pipe) {
+ while (<$pipe>) {
+ chomp;
+ my $f = (split( /\s+/, $_ ))[0];
+ push( @list, "$f" );
+ debug(" $f\n");
+ }
+ close( $pipe ) or $self->log_error("Failed to close $files\n") && return 1;
+ }
+
+ return @list;
+}
+
+# Figure out chroot architecture
+sub chroot_arch {
+ my $self = shift;
+
+ chomp(my $chroot_arch = $self->get('Session')->read_command(
+ { COMMAND => ['dpkg', '--print-architecture'],
+ USER => $self->get_conf('BUILD_USER'),
+ PRIORITY => 0,
+ DIR => '/' }));
+
+ if (!$chroot_arch) {
+ Sbuild::Exception::Build->throw(error => "Can't determine architecture of chroot: $!",
+ failstage => "chroot-arch")
+ }
+
+ return $chroot_arch;
+}
+
+sub build_log_filter {
+ my $self = shift;
+ my $text = shift;
+ my $replacement = shift;
+
+ if ($self->get_conf('LOG_FILTER')) {
+ $self->log($self->get('FILTER_PREFIX') . $text . ':' . $replacement . "\n");
+ }
+}
+
+sub build_log_colour {
+ my $self = shift;
+ my $regex = shift;
+ my $colour = shift;
+
+ if ($self->get_conf('LOG_COLOUR')) {
+ $self->log($self->get('COLOUR_PREFIX') . $colour . ':' . $regex . "\n");
+ }
+}
+
+sub open_build_log {
+ my $self = shift;
+
+ my $date = strftime_c "%FT%TZ", gmtime($self->get('Pkg Start Time'));
+
+ my $filter_prefix = '__SBUILD_FILTER_' . $$ . ':';
+ $self->set('FILTER_PREFIX', $filter_prefix);
+ my $colour_prefix = '__SBUILD_COLOUR_' . $$ . ':';
+ $self->set('COLOUR_PREFIX', $colour_prefix);
+
+ my $filename = $self->get_conf('LOG_DIR') . '/';
+ # we might not know the pkgname_ver string if the user only specified a
+ # package name without version
+ if ($self->get('Package_SVersion')) {
+ $filename .= $self->get('Package_SVersion');
+ } else {
+ $filename .= $self->get('Package');
+ }
+ $filename .= '_' . $self->get('Host Arch') . "-$date";
+ $filename .= ".build" if $self->get_conf('SBUILD_MODE') ne 'buildd';
+
+ open($saved_stdout, ">&STDOUT") or warn "Can't redirect stdout\n";
+ open($saved_stderr, ">&STDERR") or warn "Can't redirect stderr\n";
+
+ my $PLOG;
+
+ my $pid;
+ ($pid = open($PLOG, "|-"));
+ if (!defined $pid) {
+ warn "Cannot open pipe to '$filename': $!\n";
+ } elsif ($pid == 0) {
+ $SIG{'INT'} = 'IGNORE';
+ $SIG{'TERM'} = 'IGNORE';
+ $SIG{'QUIT'} = 'IGNORE';
+ $SIG{'PIPE'} = 'IGNORE';
+
+ $saved_stdout->autoflush(1);
+ if (!$self->get_conf('NOLOG') &&
+ $self->get_conf('LOG_DIR_AVAILABLE')) {
+ unlink $filename; # To prevent opening symlink to elsewhere
+ open( CPLOG, ">$filename" ) or
+ Sbuild::Exception::Build->throw(error => "Failed to open build log $filename: $!",
+ failstage => "init");
+ CPLOG->autoflush(1);
+
+ # Create 'current' symlinks
+ if ($self->get_conf('SBUILD_MODE') eq 'buildd') {
+ $self->log_symlink($filename,
+ $self->get_conf('BUILD_DIR') . '/current-' .
+ $self->get_conf('DISTRIBUTION'));
+ } else {
+ my $symlinktarget = $filename;
+ # if symlink target is in the same directory as the symlink
+ # itself, make it a relative link instead of an absolute one
+ if (Cwd::abs_path($self->get_conf('BUILD_DIR')) eq Cwd::abs_path(dirname($filename))) {
+ $symlinktarget = basename($filename)
+ }
+ my $symlinkname = $self->get_conf('BUILD_DIR') . '/';
+ # we might not know the pkgname_ver string if the user only specified a
+ # package name without version
+ if ($self->get('Package_SVersion')) {
+ $symlinkname .= $self->get('Package_SVersion');
+ } else {
+ $symlinkname .= $self->get('Package');
+ }
+ $symlinkname .= '_' . $self->get('Host Arch') . ".build";
+ $self->log_symlink($symlinktarget, $symlinkname);
+ }
+ }
+
+ # Cache vars to avoid repeated hash lookups.
+ my $nolog = $self->get_conf('NOLOG');
+ my $log = $self->get_conf('LOG_DIR_AVAILABLE');
+ my $verbose = $self->get_conf('VERBOSE');
+ my $log_colour = $self->get_conf('LOG_COLOUR');
+ my @filter = ();
+ my @colour = ();
+ my ($text, $replacement);
+ my $filter_regex = "^$filter_prefix(.*):(.*)\$";
+ my $colour_regex = "^$colour_prefix(.*):(.*)\$";
+ my @ignore = ();
+
+ while (<STDIN>) {
+ # Add a replacement pattern to filter (sent from main
+ # process in log stream).
+ if (m/$filter_regex/) {
+ ($text,$replacement)=($1,$2);
+ $replacement = "<<$replacement>>";
+ push (@filter, [$text, $replacement]);
+ $_ = "I: NOTICE: Log filtering will replace '$text' with '$replacement'\n";
+ } elsif (m/$colour_regex/) {
+ my ($colour, $regex);
+ ($colour,$regex)=($1,$2);
+ push (@colour, [$colour, $regex]);
+# $_ = "I: NOTICE: Log colouring will colour '$regex' in $colour\n";
+ next;
+ } else {
+ # Filter out any matching patterns
+ foreach my $pattern (@filter) {
+ ($text,$replacement) = @{$pattern};
+ s/\Q$text\E/$replacement/g;
+ }
+ }
+ if (m/Deprecated key/ || m/please update your configuration/) {
+ my $skip = 0;
+ foreach my $ignore (@ignore) {
+ $skip = 1 if ($ignore eq $_);
+ }
+ next if $skip;
+ push(@ignore, $_);
+ }
+
+ if ($nolog || $verbose) {
+ my $colour = 'reset';
+ if (-t $saved_stdout && $log_colour) {
+ foreach my $pattern (@colour) {
+ if (m/$$pattern[0]/) {
+ $colour = $$pattern[1];
+ }
+ }
+ if ($colour ne 'reset') {
+ print $saved_stdout color $colour;
+ }
+ }
+
+ print $saved_stdout $_;
+ if (-t $saved_stdout && $log_colour && $colour ne 'reset') {
+ print $saved_stdout color 'reset';
+ }
+ }
+ if (!$nolog && $log) {
+ print CPLOG $_;
+ }
+ }
+
+ close CPLOG;
+ exit 0;
+ }
+
+ $PLOG->autoflush(1);
+ open(STDOUT, '>&', $PLOG) or warn "Can't redirect stdout\n";
+ open(STDERR, '>&', $PLOG) or warn "Can't redirect stderr\n";
+ $self->set('Log File', $filename);
+ $self->set('Log Stream', $PLOG);
+
+ my $hostname = $self->get_conf('HOSTNAME');
+ $self->log("sbuild (Debian sbuild) $version ($release_date) on $hostname\n");
+
+ my $arch_string = $self->get('Host Arch');
+ my $head1 = $self->get('Package');
+ if ($self->get('Version')) {
+ $head1 .= ' ' . $self->get('Version');
+ }
+ $head1 .= ' (' . $arch_string . ') ';
+ my $head2 = strftime_c "%a, %d %b %Y %H:%M:%S +0000",
+ gmtime($self->get('Pkg Start Time'));
+ my $head = $head1;
+ # If necessary, insert spaces so that $head1 is left aligned and $head2 is
+ # right aligned. If the sum of the length of both is greater than the
+ # available space of 76 characters, then no additional padding is
+ # inserted.
+ if (length($head1) + length($head2) <= 76) {
+ $head .= ' ' x (76 - length($head1) - length($head2));
+ }
+ $head .= $head2;
+ $self->log_section($head);
+
+ $self->log("Package: " . $self->get('Package') . "\n");
+ if (defined $self->get('Version')) {
+ $self->log("Version: " . $self->get('Version') . "\n");
+ }
+ if (defined $self->get('OVersion')) {
+ $self->log("Source Version: " . $self->get('OVersion') . "\n");
+ }
+ $self->log("Distribution: " . $self->get_conf('DISTRIBUTION') . "\n");
+ $self->log("Machine Architecture: " . $self->get_conf('ARCH') . "\n");
+ $self->log("Host Architecture: " . $self->get('Host Arch') . "\n");
+ $self->log("Build Architecture: " . $self->get('Build Arch') . "\n");
+ $self->log("Build Profiles: " . $self->get('Build Profiles') . "\n") if $self->get('Build Profiles');
+ $self->log("Build Type: " . $self->get('Build Type') . "\n");
+ $self->log("\n");
+}
+
+sub close_build_log {
+ my $self = shift;
+
+ my $time = $self->get('Pkg End Time');
+ if ($time == 0) {
+ $time = time;
+ }
+ my $date = strftime_c "%FT%TZ", gmtime($time);
+
+ my $hours = int($self->get('This Time')/3600);
+ my $minutes = int(($self->get('This Time')%3600)/60),
+ my $seconds = int($self->get('This Time')%60),
+ my $space = "no";
+ if ($self->get('This Space') >= 0) {
+ $space = sprintf("%dk", $self->get('This Space'));
+ }
+
+ my $filename = $self->get('Log File');
+
+ # building status at this point means failure.
+ if ($self->get_status() eq "building") {
+ $self->set_status('failed');
+ }
+
+ $self->log_subsection('Summary');
+ $self->generate_stats();
+ $self->log_stats();
+
+ $self->log_sep();
+ $self->log("Finished at ${date}\n");
+ $self->log(sprintf("Build needed %02d:%02d:%02d, %s disk space\n",
+ $hours, $minutes, $seconds, $space));
+
+ if ($self->get_status() eq "successful") {
+ if (length $self->get_conf('KEY_ID')) {
+ my $key_id = $self->get_conf('KEY_ID');
+ my $build_dir = $self->get_conf('BUILD_DIR');
+ my $changes;
+ $self->log(sprintf("Signature with key '%s' requested:\n", $key_id));
+ $changes = $self->get_changes();
+ if (!defined($changes)) {
+ $self->log_error(".changes is undef. Cannot sign .changes.\n");
+ } else {
+ system('debsign', '--re-sign', "-k$key_id", '--', "$build_dir/$changes");
+ }
+ if ($self->get_conf('SOURCE_ONLY_CHANGES')) {
+ # We would like to run debsign with --no-re-sign so that a file
+ # referenced by the normal changes file and was already signed
+ # there does not get changed here by re-signing. Otherwise, the
+ # checksum from the normal changes file might not match
+ # anymore. https://bugs.debian.org/977674
+ #
+ # The problem is, that with --no-re-sign, debsign will see a
+ # signed buildinfo file and skip signing the dsc.
+ # https://bugs.debian.org/981021
+ my $so_changes = $build_dir . '/' . $self->get('Package_SVersion') . "_source.changes";
+ if (-r $so_changes) {
+ system('debsign', '--re-sign', "-k$key_id", '--', "$so_changes");
+ } else {
+ $self->log_error("$so_changes unreadable. Cannot sign .changes.\n");
+ }
+ }
+ }
+ }
+
+ my $subject = "Log for " . $self->get_status() . " build of ";
+ if ($self->get('Package_Version')) {
+ $subject .= $self->get('Package_Version');
+ } else {
+ $subject .= $self->get('Package');
+ }
+
+ if ($self->get_conf('BUILD_SOURCE') && !$self->get_conf('BUILD_ARCH_ALL') && !$self->get_conf('BUILD_ARCH_ANY')) {
+ $subject .= " source";
+ }
+ if ($self->get_conf('BUILD_ARCH_ALL') && !$self->get_conf('BUILD_ARCH_ANY')) {
+ $subject .= " on all";
+ } elsif ($self->get('Host Arch')) {
+ $subject .= " on " . $self->get('Host Arch');
+ }
+ if ($self->get_conf('ARCHIVE')) {
+ $subject .= " (" . $self->get_conf('ARCHIVE') . "/" . $self->get_conf('DISTRIBUTION') . ")";
+ }
+ else {
+ $subject .= " (dist=" . $self->get_conf('DISTRIBUTION') . ")";
+ }
+
+ open(STDERR, '>&', $saved_stderr) or warn "Can't redirect stderr\n"
+ if defined($saved_stderr);
+ open(STDOUT, '>&', $saved_stdout) or warn "Can't redirect stdout\n"
+ if defined($saved_stdout);
+ $saved_stderr->close();
+ undef $saved_stderr;
+ $saved_stdout->close();
+ undef $saved_stdout;
+ $self->set('Log File', undef);
+ if (defined($self->get('Log Stream'))) {
+ $self->get('Log Stream')->close(); # Close child logger process
+ $self->set('Log Stream', undef);
+ }
+
+ $self->send_build_log($self->get_conf('MAILTO'), $subject, $filename)
+ if (defined($filename) && -f $filename &&
+ $self->get_conf('MAILTO'));
+}
+
+sub send_build_log {
+ my $self = shift;
+ my $to = shift;
+ my $subject = shift;
+ my $filename = shift;
+
+ my $conf = $self->get('Config');
+
+ if ($conf->get('MIME_BUILD_LOG_MAILS')) {
+ return $self->send_mime_build_log($to, $subject, $filename);
+ } else {
+ return send_mail($conf, $to, $subject, $filename);
+ }
+}
+
+sub send_mime_build_log {
+ my $self = shift;
+ my $to = shift;
+ my $subject = shift;
+ my $filename = shift;
+
+ my $conf = $self->get('Config');
+ my $tmp; # Needed for gzip, here for proper scoping.
+
+ my $msg = MIME::Lite->new(
+ From => $conf->get('MAILFROM'),
+ To => $to,
+ Subject => $subject,
+ Type => 'multipart/mixed'
+ );
+
+ # Add the GPG key ID to the mail if present so that it's clear if the log
+ # still needs signing or not.
+ if (length $self->get_conf('KEY_ID')) {
+ $msg->add('Key-ID', $self->get_conf('KEY_ID'));
+ }
+
+ if (!$conf->get('COMPRESS_BUILD_LOG_MAILS')) {
+ my $log_part = MIME::Lite->new(
+ Type => 'text/plain',
+ Path => $filename,
+ Filename => basename($filename)
+ );
+ $log_part->attr('content-type.charset' => 'UTF-8');
+ $msg->attach($log_part);
+ } else {
+ local( *F, *GZFILE );
+
+ if (!open( F, "<$filename" )) {
+ warn "Cannot open $filename for mailing: $!\n";
+ return 0;
+ }
+
+ $tmp = File::Temp->new();
+ tie *GZFILE, 'IO::Zlib', $tmp->filename, 'wb';
+
+ while( <F> ) {
+ print GZFILE $_;
+ }
+ untie *GZFILE;
+
+ close F;
+ close GZFILE;
+
+ $msg->attach(
+ Type => 'application/x-gzip',
+ Path => $tmp->filename,
+ Filename => basename($filename) . '.gz'
+ );
+ }
+ my $build_dir = $self->get_conf('BUILD_DIR');
+ my $changes = $self->get_changes();
+ if ($self->get_status() eq 'successful' && -r "$build_dir/$changes") {
+ my $log_part = MIME::Lite->new(
+ Type => 'text/plain',
+ Path => "$build_dir/$changes",
+ Filename => basename($changes)
+ );
+ $log_part->attr('content-type.charset' => 'UTF-8');
+ $msg->attach($log_part);
+ }
+
+ my $stats = '';
+ foreach my $stat (sort keys %{$self->get('Summary Stats')}) {
+ $stats .= sprintf("%s: %s\n", $stat, $self->get('Summary Stats')->{$stat});
+ }
+ $msg->attach(
+ Type => 'text/plain',
+ Filename => basename($filename) . '.summary',
+ Data => $stats
+ );
+
+ local $SIG{'PIPE'} = 'IGNORE';
+
+ if (!open( MAIL, "|" . $conf->get('MAILPROG') . " -oem $to" )) {
+ warn "Could not open pipe to " . $conf->get('MAILPROG') . ": $!\n";
+ close( F );
+ return 0;
+ }
+
+ $msg->print(\*MAIL);
+
+ if (!close( MAIL )) {
+ warn $conf->get('MAILPROG') . " failed (exit status $?)\n";
+ return 0;
+ }
+ return 1;
+}
+
+sub log_symlink {
+ my $self = shift;
+ my $log = shift;
+ my $dest = shift;
+
+ unlink $dest; # Don't return on failure, since the symlink will fail.
+ symlink $log, $dest;
+}
+
+sub get_changes_opts {
+ my $self = shift;
+ my @changes_opts = ();
+ foreach (@{$self->get_conf('DPKG_BUILDPACKAGE_USER_OPTIONS')}) {
+ if (/^--changes-option=(.*)$/) {
+ push @changes_opts, $1;
+ } elsif (/^-s[iad]$/) {
+ push @changes_opts, $_;
+ } elsif (/^--build=.*$/) {
+ push @changes_opts, $_;
+ } elsif (/^-m.*$/) {
+ push @changes_opts, $_;
+ } elsif (/^-e.*$/) {
+ push @changes_opts, $_;
+ } elsif (/^-v.*$/) {
+ push @changes_opts, $_;
+ } elsif (/^-C.*$/) {
+ push @changes_opts, $_;
+ }
+ }
+
+ return \@changes_opts;
+}
+
+1;