File PBuild.obscpio of Package build
07070100000000000081a400000000000000000000000163e504b900002368000000000000000000000000000000000000001300000000PBuild/AssetMgr.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::AssetMgr;
use strict;
use Digest::MD5 ();
use PBuild::Util;
use PBuild::Source;
use PBuild::RemoteAssets;
use PBuild::Cpio;
our $goproxy_default = 'https://proxy.golang.org';
#
# Create the asset manager
#
sub create {
my ($assetdir) = @_;
return bless { 'asset_dir' => $assetdir, 'handlers' => [] };
}
#
# Add a new asset resource to the manager
#
sub add_assetshandler {
my ($assetmgr, $assetsurl) = @_;
my $type = '';
$type = $1 if $assetsurl =~ s/^([a-zA-Z0-9_]+)\@//;
if ($type eq 'fedpkg') {
push @{$assetmgr->{'handlers'}}, { 'url' => $assetsurl, 'type' => $type };
} elsif ($type eq 'goproxy') {
push @{$assetmgr->{'handlers'}}, { 'url' => $assetsurl, 'type' => $type };
} else {
die("unsupported assets url '$assetsurl'\n");
}
}
#
# Calculate the asset id used to cache the asset on-disk
#
sub get_assetid {
my ($file, $asset) = @_;
return $asset->{'assetid'} if $asset->{'assetid'};
my $digest = $asset->{'digest'};
if ($digest) {
return Digest::MD5::md5_hex("$digest $file");
} elsif ($asset->{'url'}) {
return Digest::MD5::md5_hex("$asset->{'url'} $file");
} else {
die("$file: asset must either have a digest or an url\n");
}
}
#
# calculate an id that identifies an mutable asset
#
sub calc_mutable_id {
my ($assetmgr, $asset) = @_;
my $assetid = $asset->{'assetid'};
my $adir = "$assetmgr->{'asset_dir'}/".substr($assetid, 0, 2);
my $fd;
if (open($fd, '<', "$adir/$assetid")) {
# already have it, use md5sum to track content
my $ctx = Digest::MD5->new;
$ctx->addfile($fd);
close $fd;
return $ctx->hexdigest();
}
# not available yet, use "download on demand" placeholder
return 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0';
}
#
# Add the asset information to the package's srcmd5
#
sub update_srcmd5 {
my ($assetmgr, $p) = @_;
my $old_srcmd5 = $p->{'srcmd5'};
return 0 unless $old_srcmd5;
my $asset_files = $p->{'asset_files'};
return 0 unless %{$asset_files || {}};
my %files = %{$p->{'files'}};
for my $file (sort keys %$asset_files) {
my $asset = $asset_files->{$file};
die unless $asset->{'assetid'};
# use first part of digest if we have one
if ($asset->{'digest'} && $asset->{'digest'} =~ /:([a-f0-9]{32})/) {
$files{$file} = $1;
} elsif ($asset->{'immutable'}) {
$files{$file} = substr($asset->{'assetid'}, 0, 32);
} else {
$files{$file} = calc_mutable_id($assetmgr, $asset);
}
}
$p->{'srcmd5'} = PBuild::Source::calc_srcmd5(\%files);
return $p->{'srcmd5'} eq $old_srcmd5 ? 0 : 1;
}
#
# Merge assets into the asset_files hash
#
sub merge_assets {
my ($assetmgr, $p, $assets) = @_;
my $files = $p->{'files'};
for my $asset (@{$assets || []}) {
my $file = $asset->{'file'};
if (!$assetmgr->{'keep_all_assets'}) {
# ignore asset if present in source list
next if $files->{$file} || $files->{"$file/"};
}
$asset->{'assetid'} ||= get_assetid($file, $asset);
$p->{'asset_files'}->{$file} = $asset;
}
}
#
# Generate asset information from the package source
#
sub find_assets {
my ($assetmgr, $p) = @_;
my $bt = $p->{'buildtype'} || '';
my @assets;
push @assets, @{$p->{'source_assets'} || []};
push @assets, PBuild::RemoteAssets::fedpkg_parse($p) if $p->{'files'}->{'sources'};
push @assets, PBuild::RemoteAssets::golang_parse($p) if $p->{'files'}->{'go.sum'};
push @assets, PBuild::RemoteAssets::recipe_parse($p) if $bt eq 'spec' || $bt eq 'kiwi' || $bt eq 'arch' || $bt eq 'docker';
merge_assets($assetmgr, $p, \@assets);
update_srcmd5($assetmgr, $p) if $p->{'asset_files'};
}
#
# Does a package have assets that may change over time?
#
sub has_mutable_assets {
my ($assetmgr, $p) = @_;
for my $asset (values %{$p->{'asset_files'} || {}}) {
return 1 unless $asset->{'digest'} || $asset->{'immutable'};
}
return 0;
}
#
# remove the assets that we have cached on-disk
#
sub prune_cached_assets {
my ($assetmgr, @assets) = @_;
my $assetdir = $assetmgr->{'asset_dir'};
my @pruned;
for my $asset (@assets) {
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
push @pruned, $asset unless -e "$adir/$assetid";
}
return @pruned;
}
#
# Make sure that we have all remote assets in our on-disk cache
#
sub getremoteassets {
my ($assetmgr, $p) = @_;
my $asset_files = $p->{'asset_files'};
return unless $asset_files;
my $assetdir = $assetmgr->{'asset_dir'};
my %assetid_seen;
my @assets;
# unify over the assetid
for my $asset (map {$asset_files->{$_}} sort keys %$asset_files) {
push @assets, $asset unless $assetid_seen{$asset->{'assetid'}}++;
}
@assets = prune_cached_assets($assetmgr, @assets);
for my $handler (@{$assetmgr->{'handlers'}}) {
last unless @assets;
if ($handler->{'type'} eq 'fedpkg') {
PBuild::RemoteAssets::fedpkg_fetch($p, $assetdir, \@assets, $handler->{'url'});
} elsif ($handler->{'type'} eq 'goproxy') {
PBuild::RemoteAssets::golang_fetch($p, $assetdir, \@assets, $handler->{'url'});
} else {
die("unsupported assets type $handler->{'type'}\n");
}
@assets = prune_cached_assets($assetmgr, @assets);
}
if (grep {($_->{'type'} || '') eq 'ipfs'} @assets) {
PBuild::RemoteAssets::ipfs_fetch($p, $assetdir, \@assets);
@assets = prune_cached_assets($assetmgr, @assets);
}
if (grep {($_->{'type'} || '') eq 'golang'} @assets) {
if (!grep {$_->{'type'} eq 'goproxy'} (@{$assetmgr->{'handlers'}})) {
PBuild::RemoteAssets::golang_fetch($p, $assetdir, \@assets, $goproxy_default);
@assets = prune_cached_assets($assetmgr, @assets);
}
}
if (grep {($_->{'type'} || '') eq 'url'} @assets) {
PBuild::RemoteAssets::url_fetch($p, $assetdir, \@assets);
@assets = prune_cached_assets($assetmgr, @assets);
}
if (@assets) {
my @missing = sort(map {$_->{'file'}} @assets);
print "missing assets: @missing\n";
$p->{'error'} = "missing assets: @missing";
return;
}
update_srcmd5($assetmgr, $p) if has_mutable_assets($assetmgr, $p);
}
sub unpack_obscpio_asset {
my ($assetmgr, $obscpio, $srcdir, $file) = @_;
PBuild::Cpio::cpio_extract($obscpio, sub {
my $name = $_[0]->{'name'};
!$_[1] && ($name eq $file || $name =~ /^\Q$file\E\//) ? "$srcdir/$name" : undef
}, 'postpone_symlinks' => 1, 'set_mode' => 1, 'set_mtime' => 1);
}
#
# Copy the assets from our cache to the build root
#
sub copy_assets {
my ($assetmgr, $p, $srcdir, $unpack) = @_;
my $assetdir = $assetmgr->{'asset_dir'};
my $asset_files = $p->{'asset_files'};
for my $file (sort keys %{$asset_files || {}}) {
my $asset = $asset_files->{$file};
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
die("asset $assetid is gone\n") unless -e "$adir/$assetid";
if ($asset->{'isdir'} && $unpack) {
unpack_obscpio_asset($assetmgr, "$adir/$assetid", $srcdir, $file);
next;
}
PBuild::Util::cp("$adir/$assetid", $asset->{'isdir'} ? "$srcdir/$file.obscpio" : "$srcdir/$file");
}
if (has_mutable_assets($assetmgr, $p) && update_srcmd5($assetmgr, $p)) {
copy_assets($assetmgr, $p, $srcdir); # had a race, copy again
}
}
#
# Move the assets from our cache to the build root, destroying the cache
#
sub move_assets {
my ($assetmgr, $p, $srcdir, $unpack) = @_;
my $assetdir = $assetmgr->{'asset_dir'};
my $asset_files = $p->{'asset_files'};
for my $file (sort keys %{$asset_files || {}}) {
my $asset = $asset_files->{$file};
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
die("asset $assetid is gone\n") unless -e "$adir/$assetid";
if ($asset->{'isdir'}) {
if ($unpack && ! -d "$adir/$assetid") {
unpack_obscpio_asset($assetmgr, "$adir/$assetid", $srcdir, $file);
next;
}
if (!$unpack && -d "$adir/$assetid") {
die("packing of assets is not supported\n");
}
$file .= ".obscpio" if !$unpack;
}
rename("$adir/$assetid", "$srcdir/$file") || die("rename $adir/$assetid $srcdir/$file: $!\n");
}
if (has_mutable_assets($assetmgr, $p) && update_srcmd5($assetmgr, $p)) {
die("had a race in move_assets\n");
}
}
1;
07070100000001000081a400000000000000000000000163e504b900001c74000000000000000000000000000000000000001600000000PBuild/BuildResult.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::BuildResult;
use strict;
use Digest::MD5 ();
use Build;
use Build::SimpleXML;
use PBuild::Util;
use PBuild::Verify;
use PBuild::Container;
my @binsufs = qw{rpm deb pkg.tar.gz pkg.tar.xz pkg.tar.zst};
my $binsufsre = join('|', map {"\Q$_\E"} @binsufs);
#
# create the .bininfo file that contains information about all binaries
# in the build artifacts
#
sub read_bininfo {
my ($dir, $withid) = @_;
my $bininfo;
my @bininfo_s;
local *BI;
if (open(BI, '<', "$dir/.bininfo")) {
@bininfo_s = stat(BI);
$bininfo = PBuild::Util::retrieve(\*BI, 1) if @bininfo_s && $bininfo_s[7];
close BI;
if ($bininfo) {
$bininfo->{'.bininfo'} = {'id' => "$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]"} if $withid;
return $bininfo;
}
}
$bininfo = {};
@bininfo_s = ();
for my $file (PBuild::Util::ls($dir)) {
$bininfo->{'.nosourceaccess'} = {} if $file eq '.nosourceaccess';
if ($file !~ /\.(?:$binsufsre)$/) {
if ($file eq '.channelinfo' || $file eq 'updateinfo.xml') {
$bininfo->{'.nouseforbuild'} = {};
} elsif ($file =~ /\.obsbinlnk$/) {
my @s = stat("$dir/$file");
my $d = PBuild::Util::retrieve("$dir/$file", 1);
next unless @s && $d;
my $r = {%$d, 'filename' => $file, 'id' => "$s[9]/$s[7]/$s[1]"};
$bininfo->{$file} = $r;
} elsif ($file =~ /[-.]appdata\.xml$/) {
local *F;
open(F, '<', "$dir/$file") || next;
my @s = stat(F);
next unless @s;
my $ctx = Digest::MD5->new;
$ctx->addfile(*F);
close F;
$bininfo->{$file} = {'md5sum' => $ctx->hexdigest(), 'filename' => $file, 'id' => "$s[9]/$s[7]/$s[1]"};
}
next;
}
my @s = stat("$dir/$file");
next unless @s;
my $id = "$s[9]/$s[7]/$s[1]";
my $data;
eval {
my $leadsigmd5;
die("$dir/$file: no hdrmd5\n") unless Build::queryhdrmd5("$dir/$file", \$leadsigmd5);
$data = Build::query("$dir/$file", 'evra' => 1, 'conflicts' => 1, 'weakdeps' => 1, 'addselfprovides' => 1, 'filedeps' => 1, 'normalizedeps' => 1);
die("$dir/$file: query failed\n") unless $data;
PBuild::Verify::verify_nevraquery($data);
$data->{'leadsigmd5'} = $leadsigmd5 if $leadsigmd5;
};
if ($@) {
warn($@);
next;
}
$data->{'filename'} = $file;
$data->{'id'} = $id;
$bininfo->{$file} = $data;
}
eval {
PBuild::Util::store("$dir/.bininfo.new", "$dir/.bininfo", $bininfo);
@bininfo_s = stat("$dir/.bininfo");
$bininfo->{'.bininfo'} = {'id' => "$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]"} if @bininfo_s && $withid;
};
warn($@) if $@;
return $bininfo;
}
#
# copy build artifacts from the build root to the destination
#
sub integrate_build_result {
my ($p, $result, $dst) = @_;
# delete old files
for my $file (sort(PBuild::Util::ls($dst))) {
next if $file eq '_meta' || $file eq '_meta.success' || $file eq '_meta.fail';
next if $file eq '_log' || $file eq '_log.success';
next if $file eq '_repository';
unlink("$dst/$file");
}
# copy new stuff over
for my $file (sort keys %$result) {
next if $file =~ /\.obsbinlnk$/s;
if ($file =~ /(.*)\.containerinfo$/) {
# create an obsbinlnk file from the containerinfo
my $prefix = $1;
die unless $result->{$file} =~ /^(.*)\/([^\/]+)$/;
my $obsbinlnk = PBuild::Container::containerinfo2obsbinlnk($1, $2, $p->{'pkg'});
PBuild::Util::store("$dst/$prefix.obsbinlnk", undef, $obsbinlnk) if $obsbinlnk;
}
PBuild::Util::cp($result->{$file}, "$dst/$file");
}
# create new bininfo
my $bininfo = read_bininfo($dst, 1);
return $bininfo;
}
#
# process a finished build job: copy artifacts, write extra
# information
#
sub integrate_job {
my ($builddir, $job, $code, $result) = @_;
my $p = $job->{'pdata'};
my $packid = $p->{'pkg'};
my $dst = "$builddir/$packid";
PBuild::Util::mkdir_p($dst);
unlink("$dst/_meta");
unlink("$dst/_log");
my $bininfo;
if ($code eq 'succeeded') {
$bininfo = integrate_build_result($p, $result, $dst);
PBuild::Util::writestr("$dst/._meta.$$", "$dst/_meta", join("\n", @{$job->{'meta'}})."\n");
unlink("$dst/_log.success");
unlink("$dst/_meta.success");
unlink("$dst/_meta.success");
link("$dst/_log", "$dst/_log.success");
link("$dst/_meta", "$dst/_meta.success");
unlink("$dst/_meta.fail");
} else {
PBuild::Util::cp($result->{'_log'}, "$dst/_log");
PBuild::Util::writestr("$dst/._meta.$$", "$dst/_meta", join("\n", @{$job->{'meta'}})."\n");
unlink("$dst/_meta.fail");
link("$dst/_meta", "$dst/_meta.fail");
}
unlink("$dst/_reason");
my $reason = $job->{'reason'};
if ($reason) {
$reason = PBuild::Util::clone($reason);
$reason->{'time'} = $job->{'readytime'};
$reason->{'_order'} = [ 'explain', 'time', 'oldsource', 'packagechange' ];
for (qw{explain time oldsource}) {
$reason->{$_} = [ $reason->{$_} ] if exists $reason->{$_};
}
my $reasonxml = Build::SimpleXML::unparse( { 'reason' => [ $reason ] });
PBuild::Util::writestr("$dst/._reason.$$", "$dst/_reason", $reasonxml);
}
return $bininfo;
}
#
# Create job data
#
sub makejobhist {
my ($p, $code, $readytime, $starttime, $endtime, $reason, $hostarch) = @_;
my $jobhist = {};
$jobhist->{'package'} = $p->{'pkg'};
$jobhist->{'code'} = $code;
$jobhist->{'readytime'} = $readytime;
$jobhist->{'starttime'} = $starttime;
$jobhist->{'endtime'} = $endtime;
$jobhist->{'srcmd5'} = $p->{'srcmd5'};
$jobhist->{'verifymd5'} = $p->{'verifymd5'} if $p->{'verifymd5'} && $p->{'verifymd5'} ne $p->{'srcmd5'};
$jobhist->{'reason'} = $reason->{'explain'} if $reason && $reason->{'explain'};
$jobhist->{'hostarch'} = $hostarch if $hostarch;
return $jobhist;
}
#
# Add job data to the _jobhistory file
#
sub addjobhist {
my ($builddir, $jobhist) = @_;
my $fd;
local $jobhist->{'_order'} = [ qw{package rev srcmd5 versrel bcnt readytime starttime endtime code uri workerid hostarch reason verifymd5} ];
my $jobhistxml = Build::SimpleXML::unparse({ 'jobhistlist' => [ { 'jobhist' => [ $jobhist ] } ] });
if (-s "$builddir/_jobhistory") {
open($fd, '+<', "$builddir/_jobhistory") || die("$builddir/_jobhistory: $!\n");
seek($fd, -15, 2);
print $fd substr($jobhistxml, 14);
close($fd);
} else {
PBuild::Util::writestr("$builddir/_jobhistory", undef, $jobhistxml);
}
}
1;
07070100000002000081a400000000000000000000000163e504b9000010b7000000000000000000000000000000000000001000000000PBuild/Cando.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Cando;
#
# the cando table maps the host architecture to the repository architectures
# that can be built on the host.
#
our %cando = (
# aarch64_ilp32: is just a software architecure convention
'aarch64' => [ 'aarch64', 'aarch64_ilp32', 'armv8l:linux32', 'armv7l:linux32', 'armv7hl:linux32', 'armv6l:linux32', 'armv6hl:linux32' ],
'aarch64_ilp32' => [ 'aarch64_ilp32', 'aarch64' ],
'armv4l' => [ 'armv4l' ],
'armv5l' => [ 'armv4l', 'armv5l' , 'armv5el' ],
'armv6l' => [ 'armv4l', 'armv5l', 'armv6l' , 'armv5el', 'armv6el' ],
'armv7l' => [ 'armv4l', 'armv5l', 'armv6l', 'armv7l', 'armv5el', 'armv6el', 'armv6hl', 'armv7el', 'armv7hl', 'armv8el', 'armv8hl', 'armv8l' ], # armv8* is a subset of armv7hl
'armv8l' => [ 'armv4l', 'armv5l', 'armv6l', 'armv7l', 'armv5el', 'armv6el', 'armv6hl', 'armv7el', 'armv7hl', 'armv8el', 'armv8hl', 'armv8l' ], # armv8l & armv7l are considered to be identical on rpm level
'sh4' => [ 'sh4' ],
'i586' => [ 'i586' ],
'i686' => [ 'i586', 'i686' ],
'x86_64' => [ 'x86_64', 'i586:linux32', 'i686:linux32' ],
'k1om' => [ 'k1om' ],
'parisc' => [ 'hppa', 'hppa64:linux64' ],
'parisc64'=> [ 'hppa64', 'hppa:linux32' ],
'ppc' => [ 'ppc' ],
'ppc64' => [ 'ppc64le', 'ppc64', 'ppc:linux32' ],
'ppc64p7' => [ 'ppc64le', 'ppc64p7', 'ppc:linux32' ],
'ppc64le' => [ 'ppc64le', 'ppc64', 'ppc:linux32' ],
'ia64' => [ 'ia64' ],
'riscv64' => [ 'riscv64' ],
's390' => [ 's390' ],
's390x' => [ 's390x', 's390:linux32' ],
'sparc' => [ 'sparcv8', 'sparc' ],
'sparc64' => [ 'sparc64v', 'sparc64', 'sparcv9v', 'sparcv9', 'sparcv8:linux32', 'sparc:linux32' ],
'mips' => [ 'mips' ],
'mips64' => [ 'mips64', 'mips:mips32' ],
'm68k' => [ 'm68k' ],
'local' => [ 'local' ],
);
our %knownarch;
for my $harch (keys %cando) {
for my $arch (@{$cando{$harch} || []}) {
if ($arch =~ /^(.*):/) {
$knownarch{$1}->{$harch} = $arch;
} else {
$knownarch{$arch}->{$harch} = $arch;
}
}
}
# keep in sync with extend_build_arch() in common_functions!
my %archfilter_extra = (
'aarch64' => [ 'aarch64_ilp32', 'armv8l' ],
'aarch64_ilp32' => [ 'aarch64', 'armv8l' ],
'armv8hl' => [ 'armv8l', 'armv7hl', 'armv7l', 'armv6hl', 'armv6l', 'armv5tel' ],
'armv8l' => [ 'armv7hl', 'armv7l', 'armv6hl', 'armv6l', 'armv5tel' ],
'armv7hl' => [ 'armv7l', 'armv6hl', 'armv6l', 'armv5tel' ],
'armv7l' => [ 'armv6l', 'armv5tel' ],
'armv6hl' => [ 'armv6l', 'armv5tel' ],
'armv6l' => [ 'armv5tel' ],
'mips64' => [ 'mips' ],
'i686' => [ 'i586', 'i486', 'i386' ],
'i586' => [ 'i486', 'i386' ],
'i486' => [ 'i386' ],
'parisc64' => [ 'hppa64', 'hppa' ],
'parisc' => [ 'hppa' ],
'ppc64' => [ 'ppc' ],
's390x' => [ 's390' ],
'sparc64v' => [ 'sparc64', 'sparcv9v', 'sparcv9', 'sparcv8', 'sparc' ],
'sparc64' => [ 'sparcv9', 'sparcv8', 'sparc' ],
'sparcv9v' => [ 'sparcv9', 'sparcv8', 'sparc' ],
'sparcv9' => [ 'sparcv8', 'sparc' ],
'sparcv8' => [ 'sparc' ],
'x86_64' => [ 'i686', 'i586', 'i486', 'i386' ],
);
sub archfilter {
my ($hostarch) = @_;
return $hostarch, @{$archfilter_extra{$hostarch} || []};
}
1;
07070100000003000081a400000000000000000000000163e504b9000060ae000000000000000000000000000000000000001200000000PBuild/Checker.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Checker;
use strict;
use Digest::MD5 ();
use Data::Dumper;
use PBuild::Expand;
use PBuild::Depsort;
use PBuild::Meta;
use PBuild::Util;
use PBuild::Job;
use PBuild::RepoMgr;
use PBuild::AssetMgr;
#
# Create a new package status checker
#
sub create {
my ($bconf, $arch, $buildtype, $pkgsrc, $builddir, $opts, $repomgr, $assetmgr) = @_;
my $genmetaalgo = $bconf->{'buildflags:genmetaalgo'};
$genmetaalgo = 1 unless defined $genmetaalgo;
my $ctx = {
'bconf' => $bconf,
'arch' => $arch,
'buildtype' => $buildtype,
'opts' => $opts,
'pkgsrc' => $pkgsrc,
'builddir' => $builddir,
'block' => undef, # block strategy (all,never)
'rebuild' => undef, # rebuild strategy (transitive,direct,local)
'debuginfo' => 1, # create debug packages?
'genmetaalgo' => $genmetaalgo,
'lastcheck' => {},
'metacache' => {},
'repomgr' => $repomgr,
'assetmgr' => $assetmgr,
};
$ctx->{'rebuild'} = $opts->{'buildtrigger'} if $opts->{'buildtrigger'};
return bless $ctx;
}
#
# Configure the repositories used for package building
#
sub prepare {
my ($ctx, $repos, $hostrepos) = @_;
my $dep2pkg = PBuild::Expand::configure_repos($ctx->{'bconf'}, $repos);
my %dep2src;
my %subpacks;
for my $n (sort keys %$dep2pkg) {
my $bin = $dep2pkg->{$n};
my $sn = $bin->{'source'};
$sn = $n unless defined $n;
$dep2src{$n} = $sn;
}
push @{$subpacks{$dep2src{$_}}}, $_ for keys %dep2src;
$ctx->{'dep2src'} = \%dep2src;
$ctx->{'dep2pkg'} = $dep2pkg;
$ctx->{'subpacks'} = \%subpacks;
PBuild::Meta::setgenmetaalgo($ctx->{'genmetaalgo'});
$ctx->{'dep2pkg_host'} = PBuild::Expand::configure_repos($ctx->{'bconf_host'}, $hostrepos) if $ctx->{'bconf_host'};
}
#
# Expand the package dependencies of all packages
#
sub pkgexpand {
my ($ctx, @pkgs) = @_;
my $bconf = $ctx->{'bconf'};
my $bconf_host = $ctx->{'bconf_host'};
if (($bconf_host || $bconf)->{'expandflags:preinstallexpand'}) {
my $err = Build::expandpreinstalls($bconf_host || $bconf);
die("cannot expand preinstalls: $err\n") if $err;
}
my $pkgsrc = $ctx->{'pkgsrc'};
my $subpacks = $ctx->{'subpacks'};
my $cross = $bconf_host ? 1 : 0;
for my $pkg (@pkgs) {
my $p = $pkgsrc->{$pkg};
if ($p->{'native'}) {
PBuild::Expand::expand_deps($p, $bconf_host, $subpacks);
} else {
PBuild::Expand::expand_deps($p, $bconf, $subpacks, $cross);
}
}
}
#
# Sort the packages by dependencies
#
sub pkgsort {
my ($ctx, @pkgs) = @_;
my %pdeps;
my %pkg2src;
my $pkgsrc = $ctx->{'pkgsrc'};
for my $pkg (@pkgs) {
my $p = $pkgsrc->{$pkg};
$pdeps{$pkg} = $p->{'dep_expanded'} || [];
$pkg2src{$pkg} = $p->{'name'} || $p->{'pkg'};
}
my @cycles;
my @sccs;
@pkgs = PBuild::Depsort::depsort2(\%pdeps, $ctx->{'dep2src'}, \%pkg2src, \@cycles, \@sccs, @pkgs);
my %cychash;
for my $cyc (@sccs) {
next if @$cyc < 2; # just in case
my @c = map {@{$cychash{$_} || [ $_ ]}} @$cyc;
@c = PBuild::Util::unify(sort(@c));
$cychash{$_} = \@c for @c;
}
#if (@sccs) {
# print " sccs:\n";
# print " - @{[sort @$_]}\n" for @sccs;
#}
#if (%cychash) {
# print " cycle components:\n";
# for (PBuild::Util::unify(sort(map {$_->[0]} values %cychash))) {
# print " - @{$cychash{$_}}\n";
# }
#}
$ctx->{'cychash'} = \%cychash;
return @pkgs;
}
#
# Check all packages if they need to be rebuilt
#
sub pkgcheck {
my ($ctx, $builders, @pkgs) = @_;
my %packstatus;
my %packdetails;
$ctx->{'building'} = {}; # building
$ctx->{'building'}->{$_->{'job'}->{'pdata'}->{'pkg'}} = $_->{'job'} for grep {$_->{'job'}} @$builders;
$ctx->{'notready'} = {}; # building or blocked
$ctx->{'nharder'} = 0;
my $builddir = $ctx->{'builddir'};
my $pkgsrc = $ctx->{'pkgsrc'};
my $cychash = $ctx->{'cychash'};
my %cycpass;
my @cpacks = @pkgs;
# now check every package
while (@cpacks) {
my $packid = shift @cpacks;
# cycle handling code
my $incycle = 0;
if ($cychash->{$packid}) {
($packid, $incycle) = handlecycle($ctx, $packid, \@cpacks, \%cycpass, \%packstatus);
next unless $incycle;
}
my $p = $pkgsrc->{$packid};
if ($p->{'error'}) {
if ($p->{'error'} =~ /^(excluded|disabled|locked)(?::(.*))?$/) {
$packstatus{$packid} = $1;
$packdetails{$packid} = $2 if $2;
next;
}
$packstatus{$packid} = 'broken';
$packdetails{$packid} = $p->{'error'};
next;
}
if ($p->{'dep_experror'}) {
$packstatus{$packid} = 'unresolvable';
$packdetails{$packid} = $p->{'dep_experror'};
next;
}
if ($ctx->{'building'}->{$packid}) {
my $job = $ctx->{'building'}->{$packid};
$packstatus{$packid} = 'building';
$packdetails{$packid} = "on builder $job->{'name'}" if $job->{'nbuilders'} > 1;
$ctx->{'notready'}->{$p->{'name'} || $p->{'pkg'}} = 1 if $p->{'useforbuildenabled'};
next;
}
recheck_package:
my ($status, $error) = check($ctx, $p, $incycle);
#printf("%s -> %s%s", $packid, $status, $error && $status ne 'scheduled' ? " ($error)" : '');
if ($status eq 'scheduled') {
my $builder;
for (@$builders) {
next if $_->{'job'};
$builder = $_;
last;
}
if (!$builder) {
($status, $error) = ('waiting', undef);
} else {
($status, $error) = build($ctx, $p, $error, $builder);
}
goto recheck_package if $status eq 'recheck'; # assets changed
if ($status eq 'building') {
my $job = $error;
$error = undef;
$error = "on builder $job->{'name'}" if $job->{'nbuilders'} > 1;
my $bid = ($builder->{'nbuilders'} || 1) > 1 ? "$builder->{'name'}: " : '';
if ($p->{'native'}) {
print "${bid}building $p->{'pkg'}/$p->{'recipe'} (native)\n";
} else {
print "${bid}building $p->{'pkg'}/$p->{'recipe'}\n";
}
$ctx->{'building'}->{$packid} = $builder->{'job'};
}
#printf("%s -> %s%s", $packid, $status, $error ? " ($error)" : '');
} elsif ($status eq 'done') {
# map done to succeeded/failed
if (-e "$builddir/$packid/_meta.fail") {
$status = 'failed';
} else {
$status = 'succeeded';
}
}
if ($status eq 'blocked' || $status eq 'building' || $status eq 'waiting') {
$ctx->{'notready'}->{$p->{'name'} || $p->{'pkg'}} = 1 if $p->{'useforbuildenabled'};
}
$packstatus{$packid} = $status;
$packdetails{$packid} = $error if defined $error;
}
my %result;
for my $packid (sort keys %packstatus) {
my $r = { 'code' => $packstatus{$packid} };
$r->{'details'} = $packdetails{$packid} if defined $packdetails{$packid};
$result{$packid} = $r;
}
return \%result;
}
#
# Generate the dependency tracking data for a image/container
#
sub genmeta_image {
my ($ctx, $p, $edeps) = @_;
if ($p->{'buildtype'} eq 'preinstallimage') {
my @pdeps = Build::get_preinstalls($ctx->{'bconf'});
my @vmdeps = Build::get_vminstalls($ctx->{'bconf'});
$edeps = [ PBuild::Util::unify(@$edeps, @pdeps, @vmdeps) ];
}
my $dep2pkg = $p->{'native'} ? $ctx->{'dep2pkg_host'} : $ctx->{'dep2pkg'};
my @new_meta;
for my $bin (@$edeps) {
my $q = $dep2pkg->{$bin};
push @new_meta, (($q || {})->{'hdrmd5'} || 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0')." $bin";
}
@new_meta = sort {substr($a, 34) cmp substr($b, 34) || $a cmp $b} @new_meta;
unshift @new_meta, ($p->{'verifymd5'} || $p->{'srcmd5'})." $p->{'pkg'}";
return \@new_meta;
}
#
# Generate the dependency tracking data for a package
#
sub genmeta {
my ($ctx, $p, $edeps, $hdeps) = @_;
my $buildtype = $p->{'buildtype'};
return genmeta_image($ctx, $p, $edeps) if $buildtype eq 'kiwi' || $buildtype eq 'docker' || $buildtype eq 'preinstallimage';
my $dep2pkg = $p->{'native'} ? $ctx->{'dep2pkg_host'} : $ctx->{'dep2pkg'};
my $metacache = $ctx->{'metacache'};
my @new_meta;
my $builddir = $ctx->{'builddir'};
for my $bin (@$edeps) {
my $q = $dep2pkg->{$bin};
my $binpackid = $q->{'packid'};
if (!defined $binpackid) {
# use the hdrmd5 for non-local packages
push @new_meta, ($q->{'hdrmd5'} || 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0')." $bin";
next;
}
# meta file is not in cache, read it
if (!exists $metacache->{$binpackid}) {
my $mf = "$builddir/$q->{'packid'}/_meta.success";
my $mfd;
die("$mf: $!\n") unless open($mfd, '<', $mf);
local $/ = undef;
$metacache->{$binpackid} = <$mfd>;
close($mfd);
die("$mf: bad meta\n") unless length($metacache->{$binpackid}) > 34;
}
PBuild::Meta::add_meta(\@new_meta, $metacache->{$binpackid}, $bin, $p->{'pkg'});
}
if ($hdeps) {
my $dep2pkg_host = $ctx->{'dep2pkg_host'};
my $hostarch = $ctx->{'hostarch'};
for my $bin (@$hdeps) {
my $q = $dep2pkg_host->{$bin};
push @new_meta, ($q->{'hdrmd5'} || 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0')." $hostarch:$bin";
}
}
@new_meta = PBuild::Meta::gen_meta($ctx->{'subpacks'}->{$p->{'name'}} || [], @new_meta);
unshift @new_meta, ($p->{'verifymd5'} || $p->{'srcmd5'})." $p->{'pkg'}";
return \@new_meta;
}
#
# Check the status of a single image/container
#
sub check_image {
my ($ctx, $p) = @_;
my $edeps = $p->{'dep_expanded'} || [];
my $notready = $ctx->{'notready'};
my $dep2src = $ctx->{'dep2src'};
my @blocked = grep {$notready->{$dep2src->{$_}}} @$edeps;
@blocked = () if $ctx->{'block'} && $ctx->{'block'} eq 'never';
if (@blocked) {
splice(@blocked, 10, scalar(@blocked), '...') if @blocked > 10;
return ('blocked', join(', ', @blocked));
}
my $new_meta = genmeta($ctx, $p, $edeps);
my $packid = $p->{'pkg'};
my $dst = "$ctx->{'builddir'}/$packid";
my @meta;
my $mfp;
if (open($mfp, '<', "$dst/_meta")) {
@meta = <$mfp>;
close $mfp;
chomp @meta;
}
return ('scheduled', [ { 'explain' => 'new build' } ]) if !@meta;
return ('scheduled', [ { 'explain' => 'source change', 'oldsource' => substr($meta[0], 0, 32) } ]) if $meta[0] ne $new_meta->[0];
return ('scheduled', [ { 'explain' => 'forced rebuild' } ]) if $p->{'force_rebuild'};
my $rebuildmethod = $ctx->{'rebuild'} || 'transitive';
if ($rebuildmethod eq 'local') {
return ('scheduled', [ { 'explain' => 'rebuild counter sync' } ]) if $ctx->{'relsynctrigger'}->{$packid};
return ('done');
}
if (@meta == @$new_meta && join('\n', @meta) eq join('\n', @$new_meta)) {
return ('scheduled', [ { 'explain' => 'rebuild counter sync' } ]) if $ctx->{'relsynctrigger'}->{$packid};
return ('done');
}
my @diff = PBuild::Meta::diffsortedmd5(\@meta, $new_meta);
my $reason = PBuild::Meta::sortedmd5toreason(@diff);
return ('scheduled', [ { 'explain' => 'meta change', 'packagechange' => $reason } ] );
}
#
# Check the status of a single package
#
sub check {
my ($ctx, $p, $incycle) = @_;
my $buildtype = $p->{'buildtype'};
return check_image($ctx, $p) if $buildtype eq 'kiwi' || $buildtype eq 'docker' || $buildtype eq 'preinstallimage';
my $packid = $p->{'pkg'};
my $notready = $ctx->{'notready'};
my $dep2src = $ctx->{'dep2src'};
my $edeps = $p->{'dep_expanded'} || [];
my $dst = "$ctx->{'builddir'}/$packid";
# calculate if we're blocked
my @blocked = grep {$notready->{$dep2src->{$_}}} @$edeps;
@blocked = () if $ctx->{'block'} && $ctx->{'block'} eq 'never';
# check if cycle builds are in progress
if ($incycle && $incycle == 3) {
push @blocked, 'cycle' unless @blocked;
splice(@blocked, 10, scalar(@blocked), '...') if @blocked > 10;
return ('blocked', join(', ', @blocked));
}
# prune cycle packages from blocked
if ($incycle) {
my $pkgsrc = $ctx->{'pkgsrc'};
my %cycs = map {(($pkgsrc->{$_} || {})->{'name'} || $_) => 1} @{$ctx->{'cychash'}->{$packid}};
@blocked = grep {!$cycs{$dep2src->{$_}}} @blocked;
}
if (@blocked) {
# print " - $packid ($buildtype)\n";
# print " blocked\n";
splice(@blocked, 10, scalar(@blocked), '...') if @blocked > 10;
return ('blocked', join(', ', @blocked));
}
# expand host deps
my $hdeps;
if ($ctx->{'bconf_host'} && !$p->{'native'}) {
my $subpacks = $ctx->{'subpacks'};
$hdeps = [ @{$p->{'dep_host'} || $p->{'dep'} || []}, @{$p->{'dep_native'} || []} ];
@$hdeps = Build::get_deps($ctx->{'bconf_host'}, $subpacks->{$p->{'name'}}, @$hdeps);
if (!shift @$hdeps) {
return ('unresolvable', 'host: '.join(', ', @$hdeps));
}
}
my $reason;
my @meta_s = stat("$dst/_meta");
# we store the lastcheck data in one string instead of an array
# with 4 elements to save precious memory
# srcmd5.metamd5.hdrmetamd5.statdata (32+32+32+x)
my $lastcheck = $ctx->{'lastcheck'};
my $mylastcheck = $lastcheck->{$packid};
my @meta;
if (!@meta_s || !$mylastcheck || substr($mylastcheck, 96) ne "$meta_s[9]/$meta_s[7]/$meta_s[1]") {
if (open(F, '<', "$dst/_meta")) {
@meta_s = stat F;
@meta = <F>;
close F;
chomp @meta;
$mylastcheck = substr($meta[0], 0, 32);
if (@meta == 2 && $meta[1] =~ /^fake/) {
$mylastcheck .= 'fakefakefakefakefakefakefakefake';
} else {
$mylastcheck .= Digest::MD5::md5_hex(join("\n", @meta));
}
$mylastcheck .= 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; # fake hdrmetamd5
$mylastcheck .= "$meta_s[9]/$meta_s[7]/$meta_s[1]";
$lastcheck->{$packid} = $mylastcheck;
} else {
delete $lastcheck->{$packid};
undef $mylastcheck;
}
}
if (!$mylastcheck) {
return ('scheduled', [ { 'explain' => 'new build' }, $hdeps ]);
} elsif (substr($mylastcheck, 0, 32) ne ($p->{'verifymd5'} || $p->{'srcmd5'})) {
return ('scheduled', [ { 'explain' => 'source change', 'oldsource' => substr($mylastcheck, 0, 32) }, $hdeps ]);
} elsif ($p->{'force_rebuild'}) {
return ('scheduled', [ { 'explain' => 'forced rebuild' }, $hdeps ]);
} elsif (substr($mylastcheck, 32, 32) eq 'fakefakefakefakefakefakefakefake') {
my @s = stat("$dst/_meta");
if (!@s || $s[9] + 14400 > time()) {
return ('failed')
}
return ('scheduled', [ { 'explain' => 'retrying bad build' }, $hdeps ]);
} else {
my $rebuildmethod = $ctx->{'rebuild'} || 'transitive';
if ($rebuildmethod eq 'local' || $p->{'hasbuildenv'}) {
# rebuild on src changes only
goto relsynccheck;
}
# more work, check if dep rpm changed
if ($incycle == 1) {
# print " - $packid ($buildtype)\n";
# print " in cycle, no source change...\n";
return ('done');
}
my $check = substr($mylastcheck, 32, 32); # metamd5
my $dep2pkg = $p->{'native'} ? $ctx->{'dep2pkg_host'} : $ctx->{'dep2pkg'};
my $dep2pkg_host = $ctx->{'dep2pkg_host'};
$check .= $ctx->{'genmetaalgo'} if $ctx->{'genmetaalgo'};
$check .= $rebuildmethod;
$check .= $dep2pkg->{$_}->{'hdrmd5'} || 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0' for sort @$edeps;
$check .= $dep2pkg_host->{$_}->{'hdrmd5'} || 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0' for sort @{$hdeps || []};
$check = Digest::MD5::md5_hex($check);
if ($check eq substr($mylastcheck, 64, 32)) {
# print " - $packid ($buildtype)\n";
# print " nothing changed\n";
goto relsynccheck;
}
substr($mylastcheck, 64, 32) = $check; # substitute new hdrmetamd5
# even more work, generate new meta, check if it changed
my $new_meta = genmeta($ctx, $p, $edeps, $hdeps);
if (Digest::MD5::md5_hex(join("\n", @$new_meta)) eq substr($mylastcheck, 32, 32)) {
# print " - $packid ($buildtype)\n";
# print " nothing changed (looked harder)\n";
$ctx->{'nharder'}++;
$lastcheck->{$packid} = $mylastcheck;
goto relsynccheck;
}
# something changed, read in old meta (if not already done)
if (!@meta && open(F, '<', "$dst/_meta")) {
@meta = <F>;
close F;
chomp @meta;
}
if ($rebuildmethod eq 'direct') {
@meta = grep {!/\//} @meta;
@$new_meta = grep {!/\//} @$new_meta;
}
if (@meta == @$new_meta && join('\n', @meta) eq join('\n', @$new_meta)) {
# print " - $packid ($buildtype)\n";
# print " nothing changed (looked harder)\n";
$ctx->{'nharder'}++;
if ($rebuildmethod eq 'direct') {
$lastcheck->{$packid} = $mylastcheck;
} else {
# should not happen, delete lastcheck cache
delete $lastcheck->{$packid};
}
goto relsynccheck;
}
my @diff = PBuild::Meta::diffsortedmd5(\@meta, $new_meta);
my $reason = PBuild::Meta::sortedmd5toreason(@diff);
return ('scheduled', [ { 'explain' => 'meta change', 'packagechange' => $reason }, $hdeps ] );
}
relsynccheck:
if ($ctx->{'relsynctrigger'}->{$packid}) {
return ('scheduled', [ { 'explain' => 'rebuild counter sync' }, $hdeps ] );
}
return ('done');
}
#
# Build dependency cycle handling
#
sub handlecycle {
my ($ctx, $packid, $cpacks, $cycpass, $packstatus) = @_;
my $incycle = 0;
my $cychash = $ctx->{'cychash'};
return ($packid, 0) unless $cychash->{$packid};
# do every package in the cycle twice:
# pass1: only build source changes
# pass2: normal build, but block if a pass1 package is building
# pass3: ignore
$incycle = $cycpass->{$packid};
if (!$incycle) {
# starting pass 1 (incycle == 1)
my @cycp = @{$cychash->{$packid}};
unshift @$cpacks, $cycp[0]; # pass3
unshift @$cpacks, @cycp; # pass2
unshift @$cpacks, @cycp; # pass1
$packid = shift @$cpacks;
$incycle = 1;
$cycpass->{$_} = $incycle for @cycp;
$cycpass->{$packid} = -1; # pass1 ended
} elsif ($incycle == -1) {
# starting pass 2 (incycle will be 2 or 3)
my @cycp = @{$cychash->{$packid}};
$incycle = (grep {$ctx->{'building'}->{$_}} @cycp) ? 3 : 2;
$cycpass->{$_} = $incycle for @cycp;
$cycpass->{$packid} = -2; # pass2 ended
} elsif ($incycle == -2) {
# starting pass 3 (incycle == 4)
my @cycp = @{$cychash->{$packid}};
$incycle = 4;
$cycpass->{$_} = $incycle for @cycp;
# propagate notready to all cycle packages
my $notready = $ctx->{'notready'};
my $pkgsrc = $ctx->{'pkgsrc'};
if (grep {$notready->{($pkgsrc->{$_} || {})->{'name'} || $_}} @cycp) {
$notready->{($pkgsrc->{$_} || {})->{'name'} || $_} ||= 1 for @cycp;
}
}
return ($packid, undef) if $incycle == 4; # ignore after pass1/2
return ($packid, undef) if $packstatus->{$packid} && $packstatus->{$packid} ne 'done' && $packstatus->{$packid} ne 'succeeded' && $packstatus->{$packid} ne 'failed'; # already decided
return ($packid, $incycle);
}
#
# Convert binary names to binary objects
#
sub dep2bins {
my ($ctx, @deps) = @_;
my $dep2pkg = $ctx->{'dep2pkg'};
for (@deps) {
my $q = $dep2pkg->{$_};
die("unknown binary $_\n") unless $q;
$_ = $q;
}
return \@deps;
}
sub dep2bins_host {
my ($ctx, @deps) = @_;
my $dep2pkg = $ctx->{'dep2pkg_host'} || $ctx->{'dep2pkg'};
for (@deps) {
my $q = $dep2pkg->{$_};
die("unknown binary $_\n") unless $q;
$_ = $q;
}
return \@deps;
}
#
# Start the build of a package
#
sub build {
my ($ctx, $p, $data, $builder) = @_;
my $packid = $p->{'pkg'};
my $reason = $data->[0];
my $hdeps = $data->[1];
#print Dumper($reason);
my %jobopts;
$jobopts{'nounchanged'} = 1 if $packid && $ctx->{'cychash'}->{$packid};
my @btdeps;
my $edeps = $p->{'dep_expanded'} || [];
my $bconf = $ctx->{'bconf_host'} || $ctx->{'bconf'};
my $buildtype = $p->{'buildtype'};
$buildtype = 'kiwi-image' if $buildtype eq 'kiwi';
my $kiwimode;
$kiwimode = $buildtype if $buildtype eq 'kiwi-image' || $buildtype eq 'kiwi-product' || $buildtype eq 'docker' || $buildtype eq 'fissile';
if ($p->{'buildtimeservice'}) {
for my $service (@{$p->{'buildtimeservice'} || []}) {
if ($bconf->{'substitute'}->{"obs-service:$service"}) {
push @btdeps, @{$bconf->{'substitute'}->{"obs-service:$service"}};
} else {
my $pkgname = "obs-service-$service";
$pkgname =~ s/_/-/g if $bconf->{'binarytype'} eq 'deb';
push @btdeps, $pkgname;
}
}
@btdeps = PBuild::Util::unify(@btdeps);
}
my @sysdeps = @btdeps;
unshift @sysdeps, grep {/^kiwi-.*:/} @{$p->{'dep'} || []} if $buildtype eq 'kiwi-image';
if (@sysdeps) {
@sysdeps = Build::get_sysbuild($bconf, $buildtype, [ @sysdeps ]); # cannot cache...
} else {
$ctx->{"sysbuild_$buildtype"} ||= [ Build::get_sysbuild($bconf, $buildtype) ];
@sysdeps = @{$ctx->{"sysbuild_$buildtype"}};
}
@btdeps = () if @sysdeps; # already included in sysdeps
my $genbuildreqs = $p->{'genbuildreqs'};
my @bdeps = grep {!/^\// || $bconf->{'fileprovides'}->{$_}} @{$p->{'prereq'} || []};
unshift @bdeps, '--directdepsend--' if @bdeps;
unshift @bdeps, @{$p->{'dep_native'} || []};
unshift @bdeps, @{$genbuildreqs->[1]} if $genbuildreqs;
if (!$kiwimode && $ctx->{'bconf_host'}) {
unshift @bdeps, @{$p->{'dep_host'} || $p->{'dep'} || []}, @btdeps;
} else {
unshift @bdeps, @{$p->{'dep'} || []}, @btdeps;
}
push @bdeps, '--ignoreignore--' if @sysdeps || $buildtype eq 'simpleimage';
my $opts = $ctx->{'opts'};
if ($opts->{'ccache'} && ($buildtype eq 'arch' || $buildtype eq 'spec' || $buildtype eq 'dsc')) {
my $opackid = $packid;
$opackid = $p->{'releasename'} if $p->{'releasename'};
if (!exists($bconf->{'buildflags:useccache'}) || grep {$_ eq "useccache:$opackid" || $_ eq "useccache:$packid"} @{$bconf->{'buildflags'} || []}) {
my $ccache_type = $opts->{'ccache-type'} || 'ccache';
push @bdeps, @{$bconf->{'substitute'}->{"build-packages:$ccache_type"} || [ $ccache_type ] };
$jobopts{'ccache'} = 1;
$jobopts{'ccache-type'} = $opts->{'ccache-type'};
}
}
if ($kiwimode || $buildtype eq 'buildenv' || $buildtype eq 'preinstallimage') {
@bdeps = (1, @$edeps); # reuse edeps packages, no need to expand again
} else {
@bdeps = Build::get_build($bconf, $ctx->{'subpacks'}->{$p->{'name'}}, @bdeps);
}
if (!shift(@bdeps)) {
return ('unresolvable', join(', ', @bdeps));
}
if (@sysdeps && !shift(@sysdeps)) {
return ('unresolvable', 'sysdeps:' . join(', ', @sysdeps));
}
my $dep2pkg = $ctx->{'dep2pkg_host'} || $ctx->{'dep2pkg'};
my @pdeps = Build::get_preinstalls($bconf);
my @vmdeps = Build::get_vminstalls($bconf);
my @missing = grep {!$dep2pkg->{$_}} (@pdeps, @vmdeps);
if (@missing) {
my $missing = join(', ', sort(PBuild::Util::unify(@missing)));
return ('unresolvable', "missing pre/vminstalls: $missing");
}
my $tdeps;
$tdeps = [ @$edeps ] if !$kiwimode && !$p->{'native'} && $ctx->{'bconf_host'};
my $oldsrcmd5 = $p->{'srcmd5'};
$ctx->{'assetmgr'}->getremoteassets($p);
return ('recheck', 'assets changed') if $p->{'srcmd5'} ne $oldsrcmd5;
return ('broken', $p->{'error'}) if $p->{'error'}; # missing assets
my $bins;
if ($kiwimode && $ctx->{'bconf_host'}) {
$bins = dep2bins_host($ctx, PBuild::Util::unify(@pdeps, @vmdeps, @sysdeps));
push @$bins, @{dep2bins($ctx, PBuild::Util::unify(@bdeps))};
} else {
$bins = dep2bins_host($ctx, PBuild::Util::unify(@pdeps, @vmdeps, @sysdeps, @bdeps));
push @$bins, @{dep2bins($ctx, PBuild::Util::unify(@$tdeps))} if $tdeps;
}
$ctx->{'repomgr'}->getremotebinaries($bins);
my $readytime = time();
my $job;
eval {
$job = PBuild::Job::createjob($ctx, $builder->{'name'}, $builder->{'nbuilders'}, $builder->{'root'}, $p, \@bdeps, \@pdeps, \@vmdeps, \@sysdeps, $tdeps, \%jobopts);
};
if ($@) {
chomp $@;
return ('broken', $@);
}
$job->{'readytime'} = $readytime;
$job->{'reason'} = $reason;
$job->{'hostarch'} = $ctx->{'hostarch'};
# calculate meta (again) as remote binaries have been replaced
$job->{'meta'} = genmeta($ctx, $p, $edeps, $hdeps);
$builder->{'job'} = $job;
return ('building', $job);
}
1;
07070100000004000081a400000000000000000000000163e504b900000e4f000000000000000000000000000000000000001400000000PBuild/Container.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Container;
use Digest::MD5 ();
use PBuild::Util;
use PBuild::Verify;
eval { require JSON::XS };
*JSON::XS::decode_json = sub {die("JSON::XS is not available\n")} unless defined &JSON::XS::decode_json;
use strict;
sub containerinfo2nevra {
my ($d) = @_;
my $lnk = {};
$lnk->{'name'} = "container:$d->{'name'}";
$lnk->{'version'} = defined($d->{'version'}) ? $d->{'version'} : '0';
$lnk->{'release'} = defined($d->{'release'}) ? $d->{'release'} : '0';
$lnk->{'arch'} = defined($d->{'arch'}) ? $d->{'arch'} : 'noarch';
return $lnk;
}
sub containerinfo2obsbinlnk {
my ($dir, $containerinfo, $packid) = @_;
my $d = readcontainerinfo($dir, $containerinfo);
return unless $d;
my $lnk = containerinfo2nevra($d);
# need to have a source so that it goes into the :full tree
$lnk->{'source'} = $lnk->{'name'};
# add self-provides
push @{$lnk->{'provides'}}, "$lnk->{'name'} = $lnk->{'version'}";
for my $tag (@{$d->{tags}}) {
push @{$lnk->{'provides'}}, "container:$tag" unless "container:$tag" eq $lnk->{'name'};
}
eval { PBuild::Verify::verify_nevraquery($lnk) };
return undef if $@;
local *F;
if ($d->{'tar_md5sum'}) {
# this is a normalized container
$lnk->{'hdrmd5'} = $d->{'tar_md5sum'};
$lnk->{'lnk'} = $d->{'file'};
return $lnk;
}
return undef unless open(F, '<', "$dir/$d->{'file'}");
my $ctx = Digest::MD5->new;
$ctx->addfile(*F);
close F;
$lnk->{'hdrmd5'} = $ctx->hexdigest();
$lnk->{'lnk'} = $d->{'file'};
return $lnk;
}
sub readcontainerinfo {
my ($dir, $containerinfo) = @_;
return undef unless -e "$dir/$containerinfo";
return undef unless (-s _) < 100000;
my $m = PBuild::Util::readstr("$dir/$containerinfo");
my $d;
eval { $d = JSON::XS::decode_json($m); };
return undef unless $d && ref($d) eq 'HASH';
my $tags = $d->{'tags'};
$tags = [] unless $tags && ref($tags) eq 'ARRAY';
for (@$tags) {
$_ = undef unless defined($_) && ref($_) eq '';
}
@$tags = grep {defined($_)} @$tags;
my $name = $d->{'name'};
$name = undef unless defined($name) && ref($name) eq '';
if (!defined($name) && @$tags) {
# no name specified, get it from first tag
$name = $tags->[0];
$name =~ s/[:\/]/-/g;
}
$d->{name} = $name;
my $file = $d->{'file'};
$d->{'file'} = $file = undef unless defined($file) && ref($file) eq '';
delete $d->{'disturl'} unless defined($d->{'disturl'}) && ref($d->{'disturl'}) eq '';
delete $d->{'buildtime'} unless defined($d->{'buildtime'}) && ref($d->{'buildtime'}) eq '';
delete $d->{'imageid'} unless defined($d->{'imageid'}) && ref($d->{'imageid'}) eq '';
return undef unless defined($name) && defined($file);
eval {
PBuild::Verify::verify_simple($file);
PBuild::Verify::verify_filename($file);
};
return undef if $@;
return $d;
}
1;
07070100000005000081a400000000000000000000000163e504b900001f85000000000000000000000000000000000000000f00000000PBuild/Cpio.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Cpio;
use PBuild::Util;
use strict;
# cpiotype: 1=pipe 2=char 4=dir 6=block 8=file 10=symlink 12=socket
sub cpio_make {
my ($ent, $s) = @_;
return ("07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!\0\0\0\0") if !$ent;
my $name = $ent->{'name'};
my $mode = $ent->{'mode'};
my $cpiotype = $ent->{'cpiotype'};
my $ino = $ent->{'inode'};
my $mtime = $ent->{'mtime'};
my $size = $ent->{'size'};
$cpiotype = (($mode || 0) >> 12) || 8 unless defined $cpiotype;
$mode = $cpiotype == 4 ? 0x1ed : 0x1a4 unless defined $mode;
$mode = ($mode & ~0xf000) | ($cpiotype << 12);
$mtime = $s ? $s->[9] : time() unless defined $mtime;
$size = $s ? $s->[7] : 0 unless defined $size;
$ino = ($ino || 0) & 0xffffffff;
my $h = sprintf("070701%08x%08x000000000000000000000001%08x", $ino, $mode, $mtime);
if ($size >= 0xffffffff) {
# build service extension, size is in rmajor/rminor
my $top = int($s->[7] / 4294967296);
$size -= $top * 4294967296;
$h .= sprintf("ffffffff0000000000000000%08x%08x", $top, $size);
} else {
$h .= sprintf("%08x00000000000000000000000000000000", $size);
}
$h .= sprintf("%08x", length($name) + 1);
$h .= "00000000$name\0";
$h .= substr("\0\0\0\0", (length($h) & 3)) if length($h) & 3;
my $pad = $size % 4 ? substr("\0\0\0\0", $size % 4) : '';
return ($h, $pad);
}
sub copyout {
my ($ofd, $file, $size) = @_;
my $fd;
open($fd, '<', $file) || die("$file: $!\n");
while ($size > 0) {
my $d;
sysread($fd, $d, $size > 8192 ? 8192 : $size, 0);
die("$file: unexpected EOF\n") unless length($d);
print $ofd $d or die("cpio write: $!\n");
$size -= length($d);
}
close($fd);
}
sub copyin {
my ($ifd, $file, $size) = @_;
my $fd;
open($fd, '>', $file) || die("$file: $!\n");
while ($size > 0) {
my $chunk = cpio_read($ifd, $size > 65536 ? 65536 : $size);
print $fd $chunk or die("$file write: $!\n");
$size -= length($chunk);
}
close($fd) || die("$file: $!\n");
}
sub skipin {
my ($ifd, $size) = @_;
while ($size > 0) {
my $chunk = cpio_read($ifd, $size > 65536 ? 65536 : $size);
$size -= length($chunk);
}
}
sub cpio_create {
my ($fd, $dir, %opts) = @_;
my @todo;
my $prefix = defined($opts{'prefix'}) ? $opts{'prefix'} : '';
my $prefixdir;
if ($prefix =~ /(.+)\/$/) {
$prefixdir = $1;
my @s = stat("$dir/.");
die("$dir: $!\n") unless @s;
$s[7] = 0;
unshift @todo, [ '', @s ];
}
if ($opts{'dircontent'}) {
unshift @todo, @{$opts{'dircontent'}};
} else {
unshift @todo, sort(PBuild::Util::ls($dir));
}
my $ino = 0;
while (@todo) {
my $name = shift @todo;
my @s;
if (!ref($name)) {
@s = lstat("$dir/$name");
die("$dir/$name: $!\n") unless @s;
}
my $ent;
if (ref($name)) {
$ent = { 'cpiotype' => 4, 'size' => 0 };
($name, @s) = @$name;
} elsif (-l _) {
my $lnk = readlink("$dir/$name");
die("readlink $dir/$name: $!\n") unless defined $lnk;
$ent = { 'cpiotype' => 10, 'size' => length($lnk), 'data' => $lnk };
} elsif (-d _) {
unshift @todo, [ $name, @s ];
unshift @todo, map {"$name/$_"} sort(PBuild::Util::ls("$dir/$name"));
next;
} elsif (-f _) {
$ent = { 'cpiotype' => 8, 'size' => $s[7] };
} else {
die("unsupported file type $s[2]: $dir/$name\n");
}
$ent->{'mode'} = $s[2] & 0xfff;
$ent->{'name'} = $name eq '' ? $prefixdir : "$prefix$name";
$ent->{'mtime'} = $opts{'mtime'} if defined $opts{'mtime'};
$ent->{'inode'} = $ino++;
my ($h, $pad) = cpio_make($ent, \@s);
print $fd $h;
print $fd $ent->{'data'} if defined $ent->{'data'};
copyout($fd, "$dir/$name", $ent->{'size'}) if $ent->{'cpiotype'} == 8 && $ent->{'size'};
print $fd $pad or die("cpio write: $!\n");
}
print $fd cpio_make() or die("cpio write: $!\n");
}
sub cpio_read {
my ($fd, $l) = @_;
my $r = '';
die("bad cpio file\n") unless !$l || (read($fd, $r, $l) || 0) == $l;
return $r;
}
sub cpio_parse {
my ($cpiohead) = @_;
die("not a 'SVR4 no CRC ascii' cpio\n") unless substr($cpiohead, 0, 6) eq '070701';
my $mode = hex(substr($cpiohead, 14, 8));
my $mtime = hex(substr($cpiohead, 46, 8));
my $size = hex(substr($cpiohead, 54, 8));
my $pad = (4 - ($size % 4)) % 4;
my $namesize = hex(substr($cpiohead, 94, 8));
my $namepad = (6 - ($namesize % 4)) % 4;
if ($size == 0xffffffff) {
# build service extension, size is in rmajor/rminor
$size = hex(substr($cpiohead, 86, 8));
$pad = (4 - ($size % 4)) % 4;
$size += hex(substr($cpiohead, 78, 8)) * 4294967296;
die("bad size extension\n") if $size < 0xffffffff;
}
die("ridiculous long filename\n") if $namesize > 8192;
my $ent = { 'namesize' => $namesize , 'size' => $size, 'mtime' => $mtime, 'mode' => $mode, 'cpiotype' => ($mode >> 12 & 0xf) };
return ($ent, $namesize, $namepad, $size, $pad);
}
sub set_mode_mtime {
my ($ent, $outfile, $opts) = @_;
if ($opts->{'set_mode'}) {
chmod($ent->{'mode'} & 07777, $outfile);
}
if ($opts->{'set_mtime'}) {
utime($ent->{'mtime'}, $ent->{'mtime'}, $outfile);
}
}
sub cpio_extract {
my ($cpiofile, $out, %opts) = @_;
my $fd;
my $extract = $opts{'extract'};
open($fd, '<', $cpiofile) || die("$cpiofile: $!\n");
my %symlinks;
while (1) {
my $cpiohead = cpio_read($fd, 110);
my ($ent, $namesize, $namepad, $size, $pad) = cpio_parse($cpiohead);
my $name = substr(cpio_read($fd, $namesize + $namepad), 0, $namesize);
$name =~ s/\0.*//s;
if (!$size && $name eq 'TRAILER!!!') {
die("$cpiofile: no $extract entry\n") if defined($extract) && !$opts{'missingok'};
last;
}
$name =~ s/^\.\///s;
$ent->{'name'} = $name;
my $outfile = "$out/$name";
$outfile = $ent->{'cpiotype'} == 8 && $name eq $extract ? $out : undef if defined $extract;
$outfile = $out->($ent, undef) if defined($outfile) && ref($out) eq 'CODE';
if (!defined($outfile)) {
skipin($fd, $size + $pad);
next;
}
PBuild::Util::mkdir_p($1) if $name =~ /\// && $outfile =~ /(.*)\//;
if ($ent->{'cpiotype'} == 4) {
if (-l $outfile || ! -d _) {
mkdir($outfile, 0755) || die("mkdir $outfile: $!\n");
}
} elsif ($ent->{'cpiotype'} == 10) {
die("illegal symlink size\n") if $size > 65535;
my $lnk = cpio_read($fd, $size);
unlink($outfile);
if ($opts{'postpone_symlinks'}) {
$symlinks{$outfile} = $lnk;
} else {
symlink($lnk, $outfile) || die("symlink $lnk $outfile: $!\n");
}
} elsif ($ent->{'cpiotype'} == 8) {
unlink($outfile);
copyin($fd, $outfile, $size);
} else {
die("unsupported cpio type $ent->{'cpiotype'}\n");
}
set_mode_mtime($ent, $outfile, \%opts) if $ent->{'cpiotype'} != 10;
if (ref($out) eq 'CODE') {
last if $out->($ent, $outfile);
}
last if defined $extract;
cpio_read($fd, $pad) if $pad;
}
for my $outfile (sort {$b cmp $a} keys %symlinks) {
unlink($outfile);
symlink($symlinks{$outfile}, $outfile) || die("symlink $symlinks{$outfile} $outfile: $!\n");
}
close($fd);
}
1;
07070100000006000081a400000000000000000000000163e504b900001547000000000000000000000000000000000000001200000000PBuild/Depsort.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Depsort;
use strict;
# Tarjan's SCC algorithm using a stack instead of recursion
sub find_sccs {
my ($vert, $edg) = @_;
my %low;
my @sccs;
my $idx = 1;
my @todo = map {($_, 0)} @$vert;
while (@todo) {
my ($node, $myidx) = splice(@todo, 0, 2);
if (!$myidx) {
next if $low{$node};
$low{$node} = $myidx = $idx++;
}
my @notyet = grep {!$low{$_}} @{$edg->{$node} || []};
if (@notyet) {
unshift @todo, (map {($_, 0)} @notyet), $node, $myidx;
next;
}
for (map {$low{$_}} @{$edg->{$node} || []}) {
$low{$node} = $_ if $_ > 0 && $_ < $low{$node};
}
if ($low{$node} == $myidx) {
my @collect = $node;
my @scc;
while (@collect) {
my $n = shift @collect;
next if $low{$n} < 0;
$low{$n} = -1;
push @scc, $n;
unshift @collect, grep {$low{$_} > 0} @{$edg->{$n} || []};
}
push @sccs, \@scc if @scc > 1;
}
}
return @sccs;
}
#
# Sort packages by dependencies
#
sub depsort {
my ($depsp, $mapp, $cycp, $sccs, @packs) = @_;
return @packs if @packs < 2;
my %deps;
my %rdeps;
my %needed;
# map and unify dependencies, create rdeps and needed
my %known = map {$_ => 1} @packs;
die("sortpacks: input not unique\n") if @packs != keys(%known);
for my $p (@packs) {
my @fdeps = @{$depsp->{$p} || []};
@fdeps = map {$mapp->{$_} || $_} @fdeps if $mapp;
@fdeps = grep {$known{$_}} @fdeps;
my %fdeps = ($p => 1); # no self reference
@fdeps = grep {!$fdeps{$_}++} @fdeps;
$deps{$p} = \@fdeps;
$needed{$p} = @fdeps;
push @{$rdeps{$_}}, $p for @fdeps;
}
undef %known; # free memory
@packs = sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @packs;
my @good;
my @res;
# the big sort loop
while (@packs) {
@good = grep {$needed{$_} == 0} @packs;
if (@good) {
@packs = grep {$needed{$_}} @packs;
push @res, @good;
for my $p (@good) {
$needed{$_}-- for @{$rdeps{$p}};
}
next;
}
die unless @packs > 1;
# uh oh, cycle alert. find and remove all cycles.
if ($sccs) {
push @$sccs, find_sccs(\@packs, \%deps);
undef $sccs;
}
my %notdone = map {$_ => 1} @packs;
$notdone{$_} = 0 for @res; # already did those
my @todo = @packs;
while (@todo) {
my $v = shift @todo;
if (ref($v)) {
$notdone{$$v} = 0; # finished this one
next;
}
my $s = $notdone{$v};
next unless $s;
my @e = grep {$notdone{$_}} @{$deps{$v}};
if (!@e) {
$notdone{$v} = 0; # all deps done, mark as finished
next;
}
if ($s == 1) {
$notdone{$v} = 2; # now under investigation
unshift @todo, @e, \$v;
next;
}
# reached visited package, found a cycle!
my @cyc = ();
my $cycv = $v;
# go back till $v is reached again
while(1) {
die unless @todo;
$v = shift @todo;
next unless ref($v);
$v = $$v;
$notdone{$v} = 1 if $notdone{$v} == 2;
unshift @cyc, $v;
last if $v eq $cycv;
}
unshift @todo, $cycv;
# print "cycle: ".join(' -> ', @cyc)."\n";
push @$cycp, [ @cyc ] if $cycp;
my $breakv = (sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @cyc)[0];
push @cyc, $cyc[0];
shift @cyc while $cyc[0] ne $breakv;
$v = $cyc[1];
# print " breaking with $breakv -> $v\n";
$deps{$breakv} = [ grep {$_ ne $v} @{$deps{$breakv}} ];
$rdeps{$v} = [ grep {$_ ne $breakv} @{$rdeps{$v}} ];
$needed{$breakv}--;
}
}
return @res;
}
#
# Sort packages by dependencies mapped to source packages
#
sub depsort2 {
my ($deps, $dep2src, $pkg2src, $cycles, $sccs, @packs) = @_;
my %src2pkg = reverse(%$pkg2src);
my %pkgdeps;
my @dups;
if (keys(%src2pkg) != keys (%$pkg2src)) {
@dups = grep {$src2pkg{$pkg2src->{$_}} ne $_} reverse(keys %$pkg2src);
}
if (@dups) {
push @dups, grep {defined($_)} map {delete $src2pkg{$pkg2src->{$_}}} @dups;
@dups = sort(@dups);
#print "src2pkg dups: @dups\n";
push @{$src2pkg{$pkg2src->{$_}}}, $_ for @dups;
for my $pkg (keys %$deps) {
$pkgdeps{$pkg} = [ map {ref($_) ? @$_ : $_} map { $src2pkg{$dep2src->{$_} || $_} || $dep2src->{$_} || $_} @{$deps->{$pkg}} ];
}
} else {
for my $pkg (keys %$deps) {
$pkgdeps{$pkg} = [ map { $src2pkg{$dep2src->{$_} || $_} || $dep2src->{$_} || $_} @{$deps->{$pkg}} ];
}
}
return depsort(\%pkgdeps, undef, $cycles, $sccs, @packs);
}
1;
07070100000007000081a400000000000000000000000163e504b900000711000000000000000000000000000000000000001100000000PBuild/Distro.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Distro;
use strict;
use Build;
sub guess_distro_from_rpm {
my ($arch) = @_;
my $distribution;
my @requires;
my $fd;
open($fd, '-|', 'rpm', '-q', '--qf', 'Distribution = %{DISTRIBUTION}\n', '--requires', 'rpm') || die;
while (<$fd>) {
chomp;
if (/^Distribution = (.*)$/ && !defined($distribution)) {
$distribution = $1;
} else {
push @requires, $_;
}
}
close($fd);
my $dist = Build::dist_canon($distribution, $arch);
# need some extra work for sles11 and sles15 :(
if ($dist =~ /^sles11-/) {
$dist =~ s/^sles11-/sles11sp2-/ if grep {/^liblzma/} @requires;
}
if ($dist =~ /^sles15-/) {
$dist =~ s/^sles15-/sles15sp2-/ if grep {/^libgcrypt/} @requires;
}
return $dist;
}
sub guess_distro {
my ($arch) = @_;
my $dist;
if (-x '/bin/rpm' || -x '/usr/bin/rpm') {
$dist = guess_distro_from_rpm($arch);
}
die("could not determine local dist\n") unless $dist;
return $dist;
}
1;
07070100000008000081a400000000000000000000000163e504b900001182000000000000000000000000000000000000001100000000PBuild/Expand.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Expand;
use strict;
use Build;
use PBuild::Modules;
#
# configure the expander with the available repos
#
sub configure_repos {
my ($bconf, $repos) = @_;
my %packs;
my %packs_done;
my $binarytype = $bconf->{'binarytype'} || '';
my $verscmp = $binarytype eq 'deb' ? \&Build::Deb::verscmp : \&Build::Rpm::verscmp;
# this is what perl-BSSolv does. It is different to the
# code in expanddeps!
for my $repo (@$repos) {
my $bins = $repo->{'bins'} || [];
if (@$bins && $bins->[-1]->{'name'} eq 'moduleinfo:' && $bins->[-1]->{'data'}) {
my $err = PBuild::Modules::missingmodules($bconf->{'modules'}, $bins->[-1]->{'data'});
die("module configuration error: $err\n") if $err;
$bins = PBuild::Modules::prune_to_modules($bconf->{'modules'}, $bins->[-1]->{'data'}, $bins);
}
for my $bin (@$bins) {
my $n = $bin->{'name'};
next if $packs_done{$n};
my $obin = $packs{$n};
if ($obin) {
my $evr = $bin->{'version'};
$evr = "$bin->{'epoch'}:$evr" if $bin->{'epoch'};
$evr .= "-$bin->{'release'}" if defined $bin->{'release'};
my $oevr = $obin->{'version'};
$oevr = "$obin->{'epoch'}:$oevr" if $obin->{'epoch'};
$oevr .= "-$obin->{'release'}" if defined $obin->{'release'};
my $arch = $bin->{'arch'} || '';
$arch = 'noarch' if !$arch || $arch eq 'all' || $arch eq 'any';
my $oarch = $obin->{'arch'} || '';
$oarch = 'noarch' if !$oarch || $oarch eq 'all' || $oarch eq 'any';
if ($oevr ne $evr) {
next if ($verscmp->($oevr, $evr) || $oevr cmp $evr) >= 0;
} elsif ($arch ne $oarch) {
next if $arch eq 'noarch' && $oarch ne 'noarch';
next if !($oarch eq 'noarch' && $arch ne 'noarch') && ($oarch cmp $arch) >= 0;
}
}
$packs{$n} = $bin;
}
%packs_done = %packs;
}
delete $packs{'moduleinfo:'};
Build::forgetdeps($bconf); # free mem first
Build::readdeps($bconf, undef, \%packs);
return \%packs;
}
#
# expand dependencies of a single package (image case)
#
sub expand_deps_image {
my ($p, $bconf, $subpacks, $cross) = @_;
delete $p->{'dep_experror'};
if ($p->{'error'}) {
$p->{'dep_expanded'} = [];
return;
}
my @deps = @{$p->{'dep'} || []};
push @deps, '--ignoreignore--' unless ($p->{'buildtype'} || '') eq 'preinstallimage';
my ($ok, @edeps) = Build::get_build($bconf, [], @deps);
if (!$ok) {
delete $p->{'dep_expanded'};
$p->{'dep_experror'} = join(', ', @edeps);
} else {
$p->{'dep_expanded'} = \@edeps;
}
}
#
# expand dependencies of a single package
#
sub expand_deps {
my ($p, $bconf, $subpacks, $cross) = @_;
my $buildtype = $p->{'buildtype'} || '';
return expand_deps_image($p, $bconf, $subpacks, $cross) if $buildtype eq 'kiwi' || $buildtype eq 'docker' || $buildtype eq 'fissile' || $buildtype eq 'preinstallimage';
delete $p->{'dep_experror'};
if ($p->{'error'}) {
$p->{'dep_expanded'} = [];
return;
}
my @deps = @{$p->{'dep'} || []};
if ($buildtype eq 'aggregate' || $buildtype eq 'patchinfo') {
$p->{'dep_expanded'} = \@deps;
return;
}
if ($p->{'genbuildreqs'}) {
push @deps, @{$p->{'genbuildreqs'}};
}
my @edeps;
if ($cross) {
my @native;
@edeps = Build::get_sysroot($bconf, $subpacks->{$p->{'name'}}, '--extractnative--', \@native, @deps);
$p->{'dep_native'} = \@native;
} else {
@edeps = Build::get_deps($bconf, $subpacks->{$p->{'name'}}, @deps);
}
if (!shift @edeps) {
delete $p->{'dep_expanded'};
$p->{'dep_experror'} = join(', ', @edeps);
} else {
$p->{'dep_expanded'} = \@edeps;
}
}
1;
07070100000009000081a400000000000000000000000163e504b900000ba1000000000000000000000000000000000000001700000000PBuild/ExportFilter.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::ExportFilter;
use strict;
use PBuild::Verify;
my %default_exportfilters = (
'i586' => {
'\.x86_64\.rpm$' => [ 'x86_64' ],
'\.ia64\.rpm$' => [ 'ia64' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'x86_64' => {
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'ppc' => {
'\.ppc64\.rpm$' => [ 'ppc64' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'ppc64' => {
'\.ppc\.rpm$' => [ 'ppc' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'sparc' => {
# discard is intended - sparcv9 target is better suited for 64-bit baselibs
'\.sparc64\.rpm$' => [],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'sparcv8' => {
# discard is intended - sparcv9 target is better suited for 64-bit baselibs
'\.sparc64\.rpm$' => [],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'sparcv9' => {
'\.sparc64\.rpm$' => [ 'sparc64' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'sparcv9v' => {
'\.sparc64v\.rpm$' => [ 'sparc64v' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'sparc64' => {
'\.sparcv9\.rpm$' => [ 'sparcv9' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
'sparc64v' => {
'\.sparcv9v\.rpm$' => [ 'sparcv9v' ],
'-debuginfo-.*\.rpm$' => [],
'-debugsource-.*\.rpm$' => [],
},
);
sub compile_exportfilter {
my ($filter) = @_;
return undef unless $filter;
my @res;
for my $f (@$filter) {
eval {
$_ eq '.' || PBuild::Verify::verify_arch($_) for @{$f->[1] || []};
push @res, [ qr/$f->[0]/, $f->[1] ];
};
}
return \@res;
}
sub calculate_exportfilter {
my ($bconf, $arch) = @_;
my $filter = $bconf->{'exportfilter'};
undef $filter if $filter && !%$filter;
$arch = 'i586' if $arch eq 'i686';
$filter ||= $default_exportfilters{$arch};
$filter = [ map {[$_, $filter->{$_}]} reverse sort keys %$filter ] if $filter;
return compile_exportfilter($filter);
}
1;
0707010000000a000081a400000000000000000000000163e504b900003e0b000000000000000000000000000000000000000e00000000PBuild/Job.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Job;
use strict;
use Time::HiRes ();
use PBuild::Util;
use PBuild::Cando;
use PBuild::Verify;
use PBuild::RepoMgr;
#
# Fork and exec the build tool
#
sub forkjob {
my ($opts, $args, $stderrfile) = @_;
if (!$opts->{'shell'}) {
my $pid = PBuild::Util::xfork();
return $pid if $pid;
if (!$opts->{'showlog'}) {
open(STDIN, '<', '/dev/null');
open(STDOUT, '>', '/dev/null');
if ($stderrfile) {
open(STDERR, '>', $stderrfile) || die("$stderrfile: $!\n");
} else {
open(STDERR, '>&STDOUT');
}
}
}
exec(@$args);
die("$args->[0]: $!\n");
}
#
# Update the logfile_lines element by counting lines in the logfile
#
sub updatelines {
my ($job) = @_;
my $logfile = $job->{'logfile'};
return unless $logfile;
my @s = stat($logfile);
my $newsize = @s ? $s[7] : 0;
my $oldsize = $job->{'logfile_oldsize'} || 0;
return if $oldsize < 0 || $newsize <= $oldsize;
my $fd;
if (open($fd, '<', $logfile)) {
sysseek($fd, $oldsize, 0);
while ($oldsize < $newsize) {
my $b = '';
my $r = sysread($fd, $b, $newsize - $oldsize > 8192 ? 8192 : $newsize - $oldsize);
last if $r < 0;
$oldsize += $r;
$job->{'logfile_lines'} += $b =~ tr/\n/\n/;
}
close $fd;
} else {
$oldsize = -1;
}
$job->{'logfile_oldsize'} = $oldsize;
}
#
# Wait for one or more build jobs to finish
#
sub waitjob {
my ($opts, @jobs) = @_;
local $| = 1;
my $oldmsg;
while (1) {
Time::HiRes::sleep(.2);
my $msg = '[';
for my $job (@jobs) {
updatelines($job);
if (($job->{'nbuilders'} || 1) > 1) {
$msg .= " $job->{'name'}:$job->{'logfile_lines'}";
} else {
$msg .= " $job->{'logfile_lines'}";
}
my $r = waitpid($job->{'pid'}, POSIX::WNOHANG);
next unless $r && $r == $job->{'pid'};
my $waitstatus = $?;
$waitstatus = $waitstatus & 255 ? -1 : $waitstatus >> 8;
$job->{'waitstatus'} = $waitstatus;
$job->{'endtime'} = time();
delete $job->{'pid'};
print "\n" if $oldmsg;
return $job;
}
$msg .= ' ]';
print "\r$msg" if !$opts->{'showlog'} && (!$oldmsg || $oldmsg ne $msg);
$oldmsg = $msg;
}
}
#
# Search for build artifacts
#
sub collect_result {
my ($p, $buildroot) = @_;
my @d;
push @d, map {"RPMS/$_"} sort(PBuild::Util::ls("$buildroot/.build.packages/RPMS"));
push @d, 'SRPMS';
@d = ('DEBS') if $p->{'recipe'} =~ /(?:\.dsc|build\.collax)$/ || $p->{'recipe'} eq 'debian/control' || $p->{'recipe'} eq 'debian.control';
if (-d "$buildroot/.build.packages/SDEBS") {
@d = map {"DEBS/$_"} sort(PBuild::Util::ls("$buildroot/.build.packages/DEBS")); # assume debbuild
push @d, 'SDEBS';
}
@d = ('ARCHPKGS') if $p->{'recipe'} =~ /PKGBUILD$/;
@d = ('KIWI') if $p->{'recipe'} =~ /\.kiwi$/;
@d = ('DOCKER') if $p->{'recipe'} =~ /Dockerfile$/;
@d = ('FISSILE') if $p->{'recipe'} =~ /fissile\.yml$/;
@d = ('HELM') if $p->{'recipe'} =~ /Chart\.yaml$/;
push @d, 'OTHER';
my @send;
for my $d ('.', @d) {
my @files = sort(PBuild::Util::ls("$buildroot/.build.packages/$d"));
@files = grep {$_ ne 'same_result_marker' && $_ ne '.kiwitree'} @files;
@files = grep {! -l "$buildroot/.build.packages/$d/$_" && -f _} @files;
push @send, map {"$buildroot/.build.packages/$d/$_"} @files;
}
my %send = map {(split('/', $_))[-1] => $_} @send;
for my $f (sort keys %send) {
if ($f =~ /^\./) {
delete $send{$f};
next;
}
if ($f =~ /^_/) {
next if $f eq '_statistics';
next if $f eq '_ccache.tar';
delete $send{$f};
next;
}
}
delete $send{'_log'};
return \%send;
}
#
# Copy the package sources into the build root
#
sub copy_sources {
my ($p, $srcdir) = @_;
# for kiwi/docker we need to copy the sources to $buildroot/.build-srcdir
# so that we can set up the "repos" and "containers" directories
PBuild::Util::rm_rf($srcdir);
PBuild::Util::mkdir_p($srcdir);
for (sort keys %{$p->{'files'}}) {
if (/(.*)\/$/) {
PBuild::Util::cp_a("$p->{'dir'}/$1", "$srcdir/$1");
} else {
PBuild::Util::cp("$p->{'dir'}/$_", "$srcdir/$_");
}
}
}
#
# Export upstream tarballs for git-buildpackage
#
sub export_origtar {
my ($p, $srcdir, $opts) = @_;
return unless -d "$p->{'dir'}/.git";
system("$opts->{'libbuild'}/export_debian_orig_from_git", $p->{'dir'}, "$srcdir/build.origtar") && die("export_orig_from_git $p->{'dir'} $srcdir/build.origtar: $?\n");
}
#
# Create a new build job
#
# ctx usage: opts hostarch bconf arch repos dep2pkg buildconfig debuginfo
#
sub createjob {
my ($ctx, $jobname, $nbuilders, $buildroot, $p, $bdeps, $pdeps, $vmdeps, $sysdeps, $tdeps, $jobopts) = @_;
my $opts = { %{$ctx->{'opts'}} }; # create copy so we can modify
my $hostarch = $opts->{'hostarch'};
my $arch = $p->{'native'} ? $hostarch : $ctx->{'arch'};
my $bconf = $ctx->{'bconf'};
my $helperarch = $bconf->{'hostarch'} || $arch;
die("don't know how to build arch $helperarch\n") unless $PBuild::Cando::knownarch{$helperarch};
my $helper = '';
/^\Q$helperarch\E:(.*)$/ && ($helper = $1) for @{$PBuild::Cando::cando{$hostarch}};
my %runscripts = map {$_ => 1} Build::get_runscripts($bconf);
my %bdeps = map {$_ => 1} @$bdeps;
my %pdeps = map {$_ => 1} @$pdeps;
my %vmdeps = map {$_ => 1} @$vmdeps;
my %sysdeps = map {$_ => 1} @$sysdeps;
my @alldeps;
my $kiwimode = $p->{'buildtype'} eq 'kiwi' || $p->{'buildtype'} eq 'docker' || $p->{'buildtype'} eq 'fissile' ? $p->{'buildtype'} : undef;
if ($kiwimode) {
@alldeps = PBuild::Util::unify(@$pdeps, @$vmdeps, @$sysdeps);
} else {
@alldeps = PBuild::Util::unify(@$pdeps, @$vmdeps, @$bdeps, @$sysdeps);
}
my @rpmlist;
my $binlocations = $ctx->{'repomgr'}->getbinarylocations($ctx->dep2bins_host(@alldeps));
for my $bin (@alldeps) {
push @rpmlist, "$bin $binlocations->{$bin}";
}
if (@{$tdeps || []}) {
$binlocations = $ctx->{'repomgr'}->getbinarylocations($ctx->dep2bins(@$tdeps));
for my $bin (@$tdeps) {
push @rpmlist, "sysroot: $bin $binlocations->{$bin}";
}
}
push @rpmlist, "preinstall: ".join(' ', @$pdeps);
push @rpmlist, "vminstall: ".join(' ', @$vmdeps);
push @rpmlist, "runscripts: ".join(' ', grep {$runscripts{$_}} (@$pdeps, @$vmdeps));
if (@$sysdeps && !$kiwimode) {
push @rpmlist, "noinstall: ".join(' ', grep {!($sysdeps{$_} || $vmdeps{$_} || $pdeps{$_})} @$bdeps);
push @rpmlist, "installonly: ".join(' ', grep {!$bdeps{$_}} @$sysdeps);
}
PBuild::Util::mkdir_p($buildroot);
PBuild::Util::writestr("$buildroot/.build.rpmlist", undef, join("\n", @rpmlist)."\n");
PBuild::Util::writestr("$buildroot/.build.config", undef, $ctx->{'buildconfig'});
my $needsbinariesforbuild;
my $needobspackage;
my $needsslcert;
my $needappxsslcert;
if ($p->{'buildtype'} ne 'kiwi') {
my $fd;
if (open($fd, '<', "$p->{'dir'}/$p->{'recipe'}")) {
while(<$fd>) {
chomp;
$needsbinariesforbuild = 1 if /^#\s*needsbinariesforbuild\s*$/s;
$needobspackage = 1 if /\@OBS_PACKAGE\@/;
$needsslcert = 1 if /^(?:#|Obs:)\s*needsslcertforbuild\s*$/s;
$needappxsslcert = 1 if /^(?:#|Obs:)\s*needsappxsslcertforbuild\s*$/s;
}
close($fd);
}
}
my $srcdir = $p->{'dir'};
my $copy_sources_asis;
# for kiwi/docker we need to copy the sources to $buildroot/.build-srcdir
# so that we can set up the "repos" and "containers" directories
if ($kiwimode || $p->{'asset_files'} || grep {/\/$/} keys %{$p->{'files'} || {}}) {
$srcdir = "$buildroot/.build-srcdir";
copy_sources($p, $srcdir);
$ctx->{'assetmgr'}->copy_assets($p, $srcdir) if $p->{'asset_files'};
export_origtar($p, $srcdir, $opts) if $p->{'recipe'} eq 'debian/control';
$copy_sources_asis = 1;
}
my $oldresultdir = "$ctx->{'builddir'}/$p->{'pkg'}";
my @args;
push @args, $helper if $helper;
push @args, "$opts->{'libbuild'}/build";
my $vm = $opts->{'vm-type'} || '';
if ($vm =~ /(xen|kvm|zvm|emulator|pvm|qemu)/) {
# allow setting the filesystem type with the build config
$opts->{'vm-disk-filesystem'} ||= $bconf->{'buildflags:vmfstype'} if $bconf->{'buildflags:vmfstype'};
$opts->{'vm-disk-filesystem-options'} ||= $bconf->{'buildflags:vmfsoptions'} if $bconf->{'buildflags:vmfsoptions'};
mkdir("$buildroot/.mount") unless -d "$buildroot/.mount";
push @args, "--root=$buildroot/.mount";
for my $opt (qw{vm-type vm-disk vm-swap vm-emulator-script vm-memory vm-kernel vm-initrd vm-custom-opt vm-disk-size vm-swap-size vm-disk-filesystem vm-disk-filesystem-options vm-disk-mount-options vm-disk-clean vm-hugetlbfs vm-worker vm-worker-no vm-enable-console vm-net vm-netdev vm-device vm-network}) {
next unless defined $opts->{$opt};
if ($opt eq 'vm-disk-clean' || $opt eq 'vm-enable-console') {
push @args, "--$opt",
} elsif (ref($opts->{$opt})) {
push @args, map {"--$opt=$_"} @{$opts->{$opt}};
} else {
push @args, "--$opt=$opts->{$opt}";
}
}
push @args, '--statistics';
push @args, '--vm-watchdog' unless $opts->{'shell'};
} elsif ($vm eq 'openstack') {
mkdir("$buildroot/.mount") unless -d "$buildroot/.mount";
push @args, "--root=$buildroot/.mount";
for my $opt (qw{vm-type vm-disk vm-swap vm-server vm-worker vm-kernel vm-openstack-flavor}) {
push @args, "--$opt=$opts->{$opt}" if defined $opts->{$opt},
}
} elsif ($vm eq 'lxc' || $vm eq 'docker') {
push @args, "--root=$buildroot";
for my $opt (qw{vm-type vm-memory}) {
push @args, "--$opt=$opts->{$opt}" if defined $opts->{$opt},
}
} else {
push @args, "--root=$buildroot";
push @args, "--vm-type=$vm" if $vm;
}
push @args, '--clean' unless $opts->{'noclean'};
push @args, '--nochecks' if $opts->{'nochecks'};
push @args, '--shell' if $opts->{'shell'};
push @args, '--shell-after-fail' if $opts->{'shell-after-fail'};
push @args, '--no-timestamps' if $opts->{'no-timestamps'};
push @args, '--skip-bundle';
push @args, '--changelog';
#push @args, '--oldpackages', $oldresultdir if -d $oldresultdir;
push @args, '--ccache' if $jobopts->{'ccache'};
push @args, '--ccache-create-archive' if $jobopts->{'ccache'};
push @args, '--ccache-type', $jobopts->{'ccache-type'} if $jobopts->{'ccache'} && $jobopts->{'ccache-type'};
if ($jobopts->{'ccache'} && -s "$oldresultdir/_ccache.tar") {
PBuild::Util::mkdir_p("$buildroot/.build.oldpackages");
PBuild::Util::cp("$oldresultdir/_ccache.tar", "$buildroot/.build.oldpackages/_ccache.tar");
push @args, "--ccache-archive", "$buildroot/.build.oldpackages/_ccache.tar";
}
push @args, '--dist', "$buildroot/.build.config";
push @args, '--rpmlist', "$buildroot/.build.rpmlist";
push @args, '--logfile', "$buildroot/.build.log";
#push @args, '--release', "$release" if defined $release;
push @args, '--debuginfo' if $ctx->{'debuginfo'} || $opts->{'debuginfo'};
push @args, "--arch=$arch";
push @args, '--jobs', $opts->{'jobs'} if $opts->{'jobs'};
push @args, '--threads', $opts->{'threads'} if $opts->{'threads'};
push @args, "--buildflavor=$p->{'flavor'}" if $p->{'flavor'};
push @args, "--obspackage=".($p->{'originpackage'} || $p->{'pkg'}) if $needobspackage;
push @args, "--copy-sources-asis" if $copy_sources_asis;
push @args, "--rpm-recipe-in-subdir" if $p->{'recipe'} =~ /^(?:package|dist)\/.*\.spec$/;
push @args, "$srcdir/$p->{'recipe'}";
if ($kiwimode) {
# now setup the repos/containers directories
$ctx->{'repomgr'}->copyimagebinaries($ctx->dep2bins(@$bdeps), $srcdir);
# tell kiwi how to use them
if ($p->{'buildtype'} eq 'kiwi') {
my @kiwiargs;
push @kiwiargs, '--ignore-repos';
push @kiwiargs, '--add-repo', 'dir://./repos/pbuild/pbuild';
push @kiwiargs, '--add-repotype', 'rpm-md';
push @kiwiargs, '--add-repoprio', '1';
if (-d "$srcdir/containers") {
for my $containerfile (grep {/\.tar$/} sort(PBuild::Util::ls("$srcdir/containers"))) {
push @kiwiargs, "--set-container-derived-from=dir://./containers/$containerfile";
}
}
push @args, map {"--kiwi-parameter=$_"} @kiwiargs;
}
}
unlink("$buildroot/.build.log");
#print "building $p->{'pkg'}/$p->{'recipe'}\n";
my $pid = forkjob($opts, \@args, "$buildroot/.build.log");
return { 'name' => $jobname, 'nbuilders' => $nbuilders, 'pid' => $pid, 'buildroot' => $buildroot, 'vm_type' => $vm, 'pdata' => $p, 'logfile' => "$buildroot/.build.log", 'logfile_lines' => 0, 'starttime' => time() };
}
#
# Move the build result into the correct place if we used a
# subdirectory for building
#
sub rename_build_result {
my ($vm, $buildroot) = @_;
return unless $vm =~ /(xen|kvm|zvm|emulator|pvm|qemu|openstack)/;
# get rid of old results
unlink("$buildroot/.build.packages");
if (-d "$buildroot/.build.packages") {
PBuild::Util::cleandir("$buildroot/.build.packages");
rmdir("$buildroot/.build.packages");
}
rename("$buildroot/.mount/.build.packages", "$buildroot/.build.packages") || die("final rename failed: $!\n");
# XXX: extracted cpio is flat but code below expects those directories...
symlink('.', "$buildroot/.build.packages/SRPMS");
symlink('.', "$buildroot/.build.packages/DEBS");
symlink('.', "$buildroot/.build.packages/KIWI");
}
#
# Return the last lines of a logfile
#
sub get_last_lines {
my ($logfile) = @_;
my $fd;
open($fd, '<', $logfile) || return '';
if (-s $fd > 8192) {
defined(sysseek($fd, -8192, 2)) || return '';
}
my $buf = '';
sysread($fd, $buf, 8192);
close $fd;
my @buf = split(/\r?\n/, $buf);
shift @buf if length($buf) == 8192 && @buf > 1;
@buf = splice(@buf, -5) if @buf > 5;
return @buf ? join("\n", @buf)."\n" : '';
}
#
# Finalize a build job after the build process has finished
#
sub finishjob {
my ($job) = @_;
die("job is still building\n") if $job->{'pid'};
my $buildroot = $job->{'buildroot'};
my $vm = $job->{'vm_type'};
my $p = $job->{'pdata'};
my $ret = $job->{'waitstatus'};
die("waitstatus not set\n") unless defined $ret;
$ret = 1 if $ret < 0;
# 1: build failure
# 2: unchanged build
# 3: badhost
# 4: fatal build error
# 9: genbuildreqs
if ($ret == 4) {
my $ll = get_last_lines("$buildroot/.build.log");
print $ll if $ll;
die("fatal build error, see $buildroot/.build.log for more information\n");
}
if ($ret == 3) {
$ret = 1;
}
if ($ret == 9) {
rename_build_result($vm, $buildroot);
die("XXX: dynamic buildreqs not implemented yet");
}
if (!$ret && (-l "$buildroot/.build.log" || ! -s _)) {
unlink("$buildroot/.build.log");
PBuild::Util::writestr("$buildroot/.build.log", undef, "build created no logfile!\n");
$ret = 1;
}
if ($ret) {
my $result = { '_log' => "$buildroot/.build.log" };
return ('failed', $result);
}
rename_build_result($vm, $buildroot);
my $result = collect_result($p, $buildroot);
$result->{'_log'} = "$buildroot/.build.log";
return ('succeeded', $result);
}
1;
0707010000000b000081a400000000000000000000000163e504b900000b61000000000000000000000000000000000000000f00000000PBuild/Link.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Link;
use strict;
use PBuild::Structured;
use PBuild::Util;
my $dtd_link = [
'link' =>
'project',
'package',
'baserev',
'missingok',
[ 'patches' => [[ '' => [] ]] ],
];
sub expand_single_link {
my ($pkgs, $pkg) = @_;
my @todo = ($pkg);
while (@todo) {
my $pkg = shift @todo;
my $p = $pkgs->{$pkg};
next if $p->{'error'} && $p->{'error'} =~ /^link expansion:/;
my $files = $p->{'files'} || {};
next unless $files->{'_link'};
my $link = PBuild::Structured::readxml("$p->{'dir'}/_link", $dtd_link, 1, 1);
if (!defined($link)) {
$p->{'error'} = 'link expansion: bad _link xml';
next;
}
if (exists $link->{'project'}) {
$p->{'error'} = 'link expansion: only local links allowed';
next;
}
if (!$link->{'package'}) {
$p->{'error'} = 'link expansion: no package attribute';
next;
}
if ((exists($link->{'patches'}) && exists($link->{'patches'}->{''})) || keys(%$files) != 1) {
$p->{'error'} = 'link expansion: only simple links supported';
next;
}
my $tpkg = $link->{'package'};
my $tp = $pkgs->{$tpkg};
if (!$tp) {
$p->{'error'} = "link expansion: target package '$tpkg' does not exist";
next;
}
if ($tp->{'error'} && $tp->{'error'} =~ /^link expansion:(.*)/) {
$p->{'error'} = "link expansion: $tpkg: $1";
$p->{'error'} = "link expansion: $1" if $1 eq 'cyclic link';
next;
}
if (($tp->{'files'} || {})->{'_link'}) {
if (grep {$_ eq $tpkg} @todo) {
$p->{'error'} = "link expansion: cyclic link";
} else {
unshift @todo, $tpkg, $pkg;
}
next;
}
$pkgs->{$pkg} = { %$tp, 'pkg' => $pkg };
}
}
sub expand_links {
my ($pkgs) = @_;
for my $pkg (sort keys %$pkgs) {
my $p = $pkgs->{$pkg};
my $files = $p->{'files'} || {};
expand_single_link($pkgs, $pkg) if $files->{'_link'};
}
}
sub count_links {
my ($pkgs) = @_;
return 0 + (grep {($_->{'files'} || {})->{'_link'}} values %$pkgs);
}
1;
0707010000000c000081a400000000000000000000000163e504b900001771000000000000000000000000000000000000001400000000PBuild/LocalRepo.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::LocalRepo;
use strict;
use PBuild::Verify;
use PBuild::Util;
use PBuild::BuildResult;
use PBuild::ExportFilter;
my @binsufs = qw{rpm deb pkg.tar.gz pkg.tar.xz pkg.tar.zst};
my $binsufsre = join('|', map {"\Q$_\E"} @binsufs);
my $binsufsre_binlnk = join('|', map {"\Q$_\E"} (@binsufs, 'obsbinlnk'));
#
# Collect all build artifact information of the packages into a single
# global datastructure and store it in .pbuild/_bininfo
#
sub read_gbininfo {
my ($builddir, $pkgs) = @_;
my $old_gbininfo = PBuild::Util::retrieve("$builddir/.pbuild/_bininfo", 1);
my $gbininfo = {};
for my $pkg (@$pkgs) {
next unless -d "$builddir/$pkg";
if ($old_gbininfo->{$pkg} && $old_gbininfo->{$pkg}->{'id'}) {
my @s = stat("$builddir/$pkg/.bininfo");
if (@s && "$s[9]/$s[7]/$s[1]" eq $old_gbininfo->{$pkg}->{'id'}) {
$gbininfo->{$pkg} = $old_gbininfo->{$pkg};
next;
}
}
$gbininfo->{$pkg} = PBuild::BuildResult::read_bininfo("$builddir/$pkg", 1);
}
PBuild::Util::mkdir_p("$builddir/.pbuild");
PBuild::Util::store("$builddir/.pbuild/._bininfo.$$", "$builddir/.pbuild/_bininfo", $gbininfo);
return $gbininfo;
}
#
# Update the global build artifact data with the result of a new succeeded
# build
#
sub update_gbininfo {
my ($builddir, $pkg, $bininfo) = @_;
my $gbininfo = PBuild::Util::retrieve("$builddir/.pbuild/_bininfo");
if (defined($bininfo)) {
$gbininfo->{$pkg} = $bininfo;
} else {
delete $gbininfo->{$pkg};
}
PBuild::Util::store("$builddir/.pbuild/._bininfo.$$", "$builddir/.pbuild/_bininfo", $gbininfo);
}
sub orderpackids {
my ($pkgs) = @_;
return sort @$pkgs;
}
sub set_suf_and_filter_exports {
my ($arch, $bininfo, $filter) = @_;
my %n;
for my $rp (sort keys %$bininfo) {
my $r = $bininfo->{$rp};
delete $r->{'suf'};
next unless $r->{'source'}; # no src in full tree
next unless $r->{'name'}; # need binary name
my $suf;
$suf = $1 if $rp =~ /\.($binsufsre_binlnk)$/;
next unless $suf; # need a valid suffix
$r->{'suf'} = $suf;
my $nn = $rp;
$nn =~ s/.*\///;
if ($filter) {
my $skip;
for (@$filter) {
if ($nn =~ /$_->[0]/) {
$skip = $_->[1];
last;
}
}
if ($skip) {
my $myself;
for my $exportarch (@$skip) {
if ($exportarch eq '.' || $exportarch eq $arch) {
$myself = 1;
next;
}
}
next unless $myself;
}
}
$n{$nn} = $r;
}
return %n;
}
#
# Calculate the binaries that are to be used in subsequent builds from
# the global build artifact information
#
sub gbininfo2full {
my ($gbininfo, $arch, $useforbuild, $filter) = @_;
my @packids = orderpackids([ keys %$gbininfo ]);
# construct new full
my %full;
for my $packid (@packids) {
next unless $useforbuild->{$packid};
my $bininfo = $gbininfo->{$packid};
next if $bininfo->{'.nouseforbuild'}; # channels/patchinfos don't go into the full tree
my %f = set_suf_and_filter_exports($arch, $bininfo, $filter);
for my $fn (sort { ($f{$a}->{'imported'} || 0) <=> ($f{$b}->{'imported'} || 0) || $a cmp $b} keys %f) {
my $r = $f{$fn};
$r->{'packid'} = $packid;
$r->{'filename'} = $fn;
$r->{'location'} = "$packid/$fn";
my $or = $full{$r->{'name'}};
$full{$r->{'name'}} = $r if $or && $or->{'packid'} eq $packid && volatile_cmp($r, $or);
$full{$r->{'name'}} ||= $r; # first one wins
}
}
return %full;
}
#
# get metadata of build artifacts that are to be used in subsequent builds
#
sub fetchrepo {
my ($bconf, $arch, $builddir, $pkgsrc, $pkgs) = @_;
my @pkgs = sort @{$pkgs || [ sort keys %$pkgsrc ] };
my $gbininfo = read_gbininfo($builddir, \@pkgs);
my $filter = PBuild::ExportFilter::calculate_exportfilter($bconf, $arch);
my %useforbuild = map {$_ => $pkgsrc->{$_}->{'useforbuildenabled'}} @pkgs;
for (@pkgs) {
my $p = $pkgsrc->{$_};
delete $useforbuild{$_} if ($p->{'error'} || '') eq 'excluded';
}
my %full = gbininfo2full($gbininfo, $arch, \%useforbuild, $filter);
my $bins = [ sort { $a->{'name'} cmp $b->{'name'} } values %full ];
my $repofile = "$builddir/.pbuild/_metadata";
PBuild::Util::store("$builddir/.pbuild/._metadata.$$", $repofile, $bins);
return $bins;
}
#
# Delete no obsolete entries in the builddir
#
sub cleanup_builddir {
my ($builddir, $pkgsrc) = @_;
my @pkgs = sort keys %$pkgsrc;
my @d = PBuild::Util::ls($builddir);
my %obsolete;
for my $d (@d) {
next if $d eq '.pbuild' || $d =~ /^_/;
my $p = $pkgsrc->{$d};
$obsolete{$d} = 1 if !$p || ($p->{'error'} || '') eq 'excluded';
}
return unless %obsolete;
my $gbininfo = PBuild::Util::retrieve("$builddir/.pbuild/_bininfo", 1);
if ($gbininfo) {
delete $gbininfo->{$_} for keys %obsolete;
PBuild::Util::store("$builddir/.pbuild/._bininfo.$$", "$builddir/.pbuild/_bininfo", $gbininfo);
}
for my $d (sort keys %obsolete) {
next unless -d "$builddir/$d";
PBuild::Util::cleandir("$builddir/$d");
rmdir("$builddir/$d");
}
}
1;
0707010000000d000081a400000000000000000000000163e504b900001177000000000000000000000000000000000000000f00000000PBuild/Meta.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Meta;
use strict;
my $genmetaalgo = 0;
sub add_meta {
my ($new_meta, $m, $bin, $packid) = @_;
my $oldlen = @$new_meta;
for (split("\n", ref($m) ? $m->[0] : $m)) {
s/ / $bin\//;
push @$new_meta, $_;
}
if (@$new_meta != $oldlen) {
if (defined($packid) && $new_meta->[$oldlen] =~ /\/\Q$packid\E$/) {
# do not include our own build results
splice(@$new_meta, $oldlen);
} else {
# fixup first line, it contains the package name and not the binary name
$new_meta->[$oldlen] =~ s/ .*/ $bin/;
}
}
}
sub gen_meta {
my ($subp, @deps) = @_;
my @subp = @{$subp || []};
#print "gen_meta @subp\n";
my $subpackre = '';
for (@subp) {
$subpackre .= "|/\Q$_\E/";
}
if ($subpackre) {
$subpackre = substr($subpackre, 1);
$subpackre = qr/$subpackre/;
}
# setup helpers
my (%helper1, %helper2, %helper3, %cycle);
for (@deps) {
$helper1{$_} = tr/\///; # count '/'
/^([^ ]+ )((?:.*\/)?([^\/]*))$/ or die("bad dependency line: $_\n");
$helper2{$_} = $2; # path
$helper3{$_} = "$1$3"; # md5 lastpkg
if ($subpackre && "/$2/" =~ /$subpackre/) {
/ ([^\/]+)/ or die("bad dependency line: $_\n");
$cycle{$1} = 1; # detected a cycle!
}
}
# sort
@deps = sort {$helper1{$a} <=> $helper1{$b} || $helper2{$a} cmp $helper2{$b} || $a cmp $b} @deps;
undef $subpackre unless %cycle; # speed things up a bit
# ignore self-cycles
if (%cycle) {
delete $cycle{$_} for @subp;
# print "CYC: ".join(' ', keys %cycle)."\n";
}
# handle cycles
my %cycdepseen;
if (%cycle) {
my $cyclere = '';
$cyclere .= "|\Q/$_/\E" for sort keys %cycle;
$cyclere = substr($cyclere, 1);
$cyclere = qr/$cyclere/;
if (!$genmetaalgo) {
# kill all deps that use a package that we see directly
@deps = grep {"$_/" !~ /$cyclere/} @deps;
} else {
for my $d (grep {"$_/" =~ /$cyclere/} @deps) {
$cycdepseen{$helper3{$d}} ||= $helper1{$d};
}
}
}
# prune
my %depseen;
my @meta;
for my $d (@deps) {
next if $depseen{$helper3{$d}}; # skip if we already have this pkg with this md5
next if $subpackre && "/$helper2{$d}/" =~ /$subpackre/;
$depseen{$helper3{$d}} = 1;
push @meta, $d;
}
# do extra cycle pruning
if (%cycdepseen) {
@meta = grep {!$cycdepseen{$helper3{$_}} || $helper1{$_} < $cycdepseen{$helper3{$_}}} @meta;
}
return @meta;
}
sub setgenmetaalgo {
my ($algo) = @_;
$algo = 1 if $algo < 0;
die("BSBuild::setgenmetaalgo: unsupported algo $algo\n") if $algo > 1;
$genmetaalgo = $algo;
return $algo;
}
sub sortedmd5toreason {
my @res;
for my $line (@_) {
my $tag = substr($line, 0, 1); # just the first char
$tag = 'md5sum' if $tag eq '!';
$tag = 'added' if $tag eq '+';
$tag = 'removed' if $tag eq '-';
push @res, { 'change' => $tag, 'key' => substr($line, 1) };
}
return \@res;
}
sub diffsortedmd5 {
my ($fromp, $top) = @_;
my @ret;
my @from = map {[$_, substr($_, 34)]} @$fromp;
my @to = map {[$_, substr($_, 34)]} @$top;
@from = sort {$a->[1] cmp $b->[1] || $a->[0] cmp $b->[0]} @from;
@to = sort {$a->[1] cmp $b->[1] || $a->[0] cmp $b->[0]} @to;
for my $f (@from) {
if (@to && $f->[1] eq $to[0]->[1]) {
push @ret, "!$f->[1]" if $f->[0] ne $to[0]->[0];
shift @to;
next;
}
if (!@to || $f->[1] lt $to[0]->[1]) {
push @ret, "-$f->[1]";
next;
}
while (@to && $f->[1] gt $to[0]->[1]) {
push @ret, "+$to[0]->[1]";
shift @to;
}
redo;
}
push @ret, "+$_->[1]" for @to;
return @ret;
}
1;
0707010000000e000081a400000000000000000000000163e504b900000f4c000000000000000000000000000000000000001200000000PBuild/Modules.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Modules;
use strict;
use PBuild::Util;
#
# reduce the binaries to the ones selected by the given module list
#
sub prune_to_modules {
my ($modules, $data, $bins) = @_;
my %modules = map {$_ => 1} @{$modules || []};
# expand modules to streams if we have the data
my $moduleinfo = $data->{'/moduleinfo'};
if ($moduleinfo) {
my %pmodules = %modules;
for (keys %pmodules) {
$pmodules{$_} = 1 if /^(.*)-/; # also provide without the stream suffix
}
my %xmodules;
for my $mi (@$moduleinfo) {
next unless $modules{$mi->{'name'}};
my @req = grep {$_ ne 'platform' && !/^platform-/} @{$mi->{'requires'} || []};
next if grep {!$pmodules{$_}} @req;
$xmodules{"$mi->{'name'}\@$mi->{'context'}"} = 1;
}
%modules = %xmodules;
}
# now get rid of all packages not in a module
my @nbins;
my @notmod;
my %inmod;
for my $bin (@$bins) {
my $evr = $bin->{'epoch'} ? "$bin->{'epoch'}:$bin->{'version'}" : $bin->{'version'};
$evr .= "-$bin->{'release'}" if defined $bin->{'release'};
my $nevra = "$bin->{'name'}-$evr.$bin->{'arch'}";
if ($data->{$nevra}) {
next unless grep {$modules{$_}} @{$data->{$nevra}};
$inmod{$bin->{'name'}} = 1;
} else {
# not in a module
next if $bin->{'release'} && $bin->{'release'} =~ /\.module_/; # hey!
push @notmod, $bin;
}
push @nbins, $bin;
}
for (@notmod) {
$_ = undef if $inmod{$_->{'name'}};
}
@nbins = grep {defined($_)} @nbins;
return \@nbins;
}
#
# return the modules a package belongs to
#
sub getmodules {
my ($data, $bin) = @_;
my $moduleinfo = $data->{'/moduleinfo'};
my $evr = $bin->{'epoch'} ? "$bin->{'epoch'}:$bin->{'version'}" : $bin->{'version'};
$evr .= "-$bin->{'release'}" if defined $bin->{'release'};
my $nevra = "$bin->{'name'}-$evr.$bin->{'arch'}";
return @{$data->{$nevra} || []};
}
#
# return which modules are missing from the module config
#
sub missingmodules {
my ($modules, $data) = @_;
my $moduleinfo = $data->{'/moduleinfo'};
return () unless $moduleinfo && @{$modules || []};
my %modules = map {$_ => 1} @$modules;
my %pmodules = %modules;
for (keys %pmodules) {
$pmodules{$_} = 1 if /^(.*)-/; # also provide without the stream suffix
}
my %missingmods;
for my $mi (@$moduleinfo) {
my $n = $mi->{'name'};
next unless $modules{$n};
next if exists($missingmods{$n}) && !$missingmods{$n};
my @req = grep {$_ ne 'platform' && !/^platform-/} @{$mi->{'requires'} || []};
my $bad;
for (grep {!$pmodules{$_}} @req) {
push @{$missingmods{$n}}, $_;
$bad = 1;
}
$missingmods{$n} = undef unless $bad;
}
delete $missingmods{$_} for grep {!$missingmods{$_}} keys %missingmods;
return undef unless %missingmods;
my $msg = '';
for my $mod (sort keys %missingmods) {
my @m = sort(PBuild::Util::unify(@{$missingmods{$mod}}));
if (@m > 1) {
$msg .= ", $mod needs one of ".join(',', @m);
} else {
$msg .= ", $mod needs $m[0]";
}
}
return substr($msg, 2);
}
1;
0707010000000f000081a400000000000000000000000163e504b9000009c5000000000000000000000000000000000000001500000000PBuild/Multibuild.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Multibuild;
use strict;
use PBuild::Structured;
use PBuild::Verify;
my $dtd_multibuild = [
'multibuild' =>
[ 'package' ], # obsolete
[ 'flavor' ],
];
sub find_mbname {
my ($files) = @_;
my $mbname = '_multibuild';
# support service generated multibuild files, see findfile
if ($files->{'_service'}) {
for (sort keys %$files) {
next unless /^_service:.*:(.*?)$/s;
$mbname = $_ if $1 eq '_multibuild';
}
}
return $mbname;
}
sub readmbxml {
my ($xmlfile) = @_;
my $mb = PBuild::Structured::readxml($xmlfile, $dtd_multibuild);
PBuild::Verify::verify_multibuild($mb);
return $mb;
}
sub getmultibuild_fromfiles {
my ($srcdir, $files) = @_;
my $mbname = find_mbname($files);
my $mb;
if ($files->{$mbname}) {
eval { $mb = readmbxml("$srcdir/$mbname") };
if ($@) {
warn("$srcdir/$mbname: $@");
return undef;
}
$mb->{'_md5'} = $files->{$mbname} if $mb;
}
return $mb;
}
sub expand_multibuilds {
my ($pkgs) = @_;
for my $pkg (sort keys %$pkgs) {
my $p = $pkgs->{$pkg};
my $mb = getmultibuild_fromfiles($p->{'dir'}, $p->{'files'});
next unless $mb;
my @mbp = @{$mb->{'flavor'} || $mb->{'package'} || []};
for my $flavor (@mbp) {
my $mpkg = "$pkg:$flavor";
$pkgs->{$mpkg} = { %$p, 'pkg' => $mpkg, 'flavor' => $flavor, 'originpackage' => $pkg };
}
}
}
sub count_multibuilds {
my ($pkgs) = @_;
my $c = 0;
for (values %$pkgs) {
my $files = $_->{'files'} || {};
next unless $files->{'_multibuild'} || ($files->{'_service'} && $files->{find_mbname($files)});
$c++;
}
return $c;
}
1;
07070100000010000081a400000000000000000000000163e504b9000024b2000000000000000000000000000000000000000e00000000PBuild/OBS.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::OBS;
use strict;
use Build::Download;
use PBuild::Util;
use PBuild::Cpio;
use PBuild::Structured;
my @binsufs = qw{rpm deb pkg.tar.gz pkg.tar.xz pkg.tar.zst};
my $binsufsre = join('|', map {"\Q$_\E"} @binsufs);
my @dtd_disableenable = (
[[ 'disable' =>
'arch',
'repository',
]],
[[ 'enable' =>
'arch',
'repository',
]],
);
my $dtd_repo = [
'repository' =>
'name',
'rebuild',
'block',
'linkedbuild',
[[ 'path' =>
'project',
'repository',
]],
[ 'arch' ],
];
my $dtd_proj = [
'project' =>
'name',
'kind',
[],
[[ 'link' =>
'project',
'vrevmode',
]],
[ 'lock' => @dtd_disableenable ],
[ 'build' => @dtd_disableenable ],
[ 'publish' => @dtd_disableenable ],
[ 'debuginfo' => @dtd_disableenable ],
[ 'useforbuild' => @dtd_disableenable ],
[ 'binarydownload' => @dtd_disableenable ],
[ 'sourceaccess' => @dtd_disableenable ],
[ 'access' => @dtd_disableenable ],
[ $dtd_repo ],
];
#
# get the project data from an OBS project
#
sub fetch_proj {
my ($projid, $baseurl) = @_;
my $projid2 = PBuild::Util::urlencode($projid);
my ($projxml) = Build::Download::fetch("${baseurl}source/$projid2/_meta");
return PBuild::Structured::fromxml($projxml, $dtd_proj, 0, 1);
}
#
# get the config from an OBS project
#
sub fetch_config {
my ($prp, $baseurl) = @_;
my ($projid, $repoid) = split('/', $prp, 2);
my $projid2 = PBuild::Util::urlencode($projid);
my ($config) = Build::Download::fetch("${baseurl}source/$projid2/_config", 'missingok' => 1);
$config = '' unless defined $config;
$config = "\n### from $projid\n%define _repository $repoid\n$config" if $config;
return $config;
}
#
# expand the path for an OBS project/repository
#
sub expand_path {
my ($prp, $baseurl) = @_;
my %done;
my @ret;
my @path = ($prp);
while (@path) {
my $t = shift @path;
push @ret, $t unless $done{$t};
$done{$prp} = 1;
if (!@path) {
last if $done{"/$t"};
my ($tprojid, $trepoid) = split('/', $t, 2);
my $proj = fetch_proj($tprojid, $baseurl);
$done{"/$t"} = 1;
my $repo = (grep {$_->{'name'} eq $trepoid} @{$proj->{'repository'} || []})[0];
next unless $repo;
for (@{$repo->{'path'} || []}) {
push @path, "$_->{'project'}/$_->{'repository'}";
}
}
}
return @ret;
}
#
# get the configs/repo urls for an OBS project/repository
# expand the path if $islast is true
#
sub fetch_all_configs {
my ($url, $opts, $islast) = @_;
die("bad obs: reference\n") unless $url =~ /^obs:\/{1,3}([^\/]+\/[^\/]+)\/?$/;
my $prp = PBuild::Util::urldecode($1);
die("please specify the build service url with the --obs option\n") unless $opts->{'obs'};
my $baseurl = $opts->{'obs'};
$baseurl .= '/' unless $baseurl =~ /\/$/;
my @prps;
if ($islast) {
@prps = expand_path($prp, $baseurl);
} else {
@prps = ($prp);
}
my @configs;
for my $xprp (@prps) {
my $config = fetch_config($xprp, $baseurl);
push @configs, $config if $config;
}
my @repourls;
push @repourls, "obs:\/".PBuild::Util::urlencode($_) for @prps;
return (\@configs, \@repourls);
}
#
# parse a dependency in libsolv's testcase style
#
my %testcaseops = (
'&' => 1,
'|' => 2,
'<IF>' => 3,
'<UNLESS' => 4,
'<ELSE>' => 5,
'+' => 6,
'-' => 7,
);
sub parse_testcasedep_rec {
my ($dep, $chainop) = @_;
no warnings 'recursion';
my $d = $dep;
$chainop ||= 0;
my ($r, $r2);
$d =~ s/^\s+//;
if ($d =~ s/^\(//) {
($d, $r) = parse_testcasedep_rec($d);
return ($d, undef) unless $r && $d =~ s/\s*^\)//;
} else {
return ($d, undef) if $d eq '' || $d =~ /^\)/;
return ($d, undef) unless $d =~ s/([^\s\)]+)//;
$r = $1;
$r .= ')' if $d =~ /^\)/ && $r =~ /\([^\)]+$/ && $d =~ s/^\)//;
$r = "$r$1" if $d =~ s/^( (?:<|<=|>|>=|<=>|=) [^\s\)]+)//;
$r =~ s/\\([A-Fa-f2-9][A-Fa-f0-9])/chr(hex($1))/sge;
$r = [0, $r];
}
$d =~ s/^\s+//;
return ($d, $r) if $d eq '' || $d =~ /^\)/;
return ($d, undef) unless $d =~ s/([^\s\)]+)//;
my $op = $testcaseops{$1};
return ($d, undef) unless $op;
return ($d, undef) if $op == 5 && $chainop != 3 && $chainop != 4;
$chainop = 0 if $op == 5;
return ($d, undef) if $chainop && (($chainop != 1 && $chainop != 2 && $chainop != 6) || $op != $chainop);
($d, $r2) = parse_testcasedep_rec($d, $op);
return ($d, undef) unless $r2;
if (($op == 3 || $op == 4) && $r2->[0] == 5) {
$r = [$op, $r, $r2->[1], $r2->[2]];
} else {
$r = [$op, $r, $r2];
}
return ($d, $r);
}
#
# convert a parsed dependency to rpm's rich dep style
#
my @rpmops = ('', 'and', 'or', 'if', 'unless', 'else', 'with', 'without');
sub rpmdepformat_rec {
my ($r, $addparens) = @_;
no warnings 'recursion';
my $op = $r->[0];
return $r->[1] unless $op;
my $top = $rpmops[$op];
my $r1 = rpmdepformat_rec($r->[1], 1);
if (($op == 3 || $op == 4) && @$r == 4) {
$r1 = "$r1 $top " . rpmdepformat_rec($r->[2], 1);
$top = 'else';
}
my $addparens2 = 1;
$addparens2 = 0 if $r->[2]->[0] == $op && ($op == 1 || $op == 2 || $op == 6);
my $r2 = rpmdepformat_rec($r->[-1], $addparens2);
return $addparens ? "($r1 $top $r2)" : "$r1 $top $r2";
}
#
# recode the dependencies in a binary from testcaseformat to native
#
sub recode_deps {
my ($b) = @_;
for my $dep (@{$b->{'requires'} || []}, @{$b->{'conflicts'} || []}, @{$b->{'recommends'} || []}, @{$b->{'supplements'} || []}) {
next unless $dep =~ / (?:<[A-Z]|[\-\+\|\&\.])/;
my ($d, $r) = parse_testcasedep_rec($dep);
next if !$r || $d ne '';
$dep = rpmdepformat_rec($r, 1); # currently only rpm supported
}
}
#
# Extract a binary from the cpio archive downloaded by fetchbinaries
#
sub fetch_binaries_cpioextract {
my ($ent, $xfile, $repodir, $names, $callback) = @_;
return undef unless $ent->{'cpiotype'} == 8;
my $name = $ent->{'name'};
if (!defined($xfile)) {
return undef unless $name =~ s/\.($binsufsre)$//;
my $suf = $1;
return undef unless $names->{$name};
my $tmpname = $names->{$name}->[0];
return undef unless $tmpname =~ /\.\Q$suf\E$/;
return "$repodir/$tmpname"; # ok, extract this one!
}
die unless $name =~ s/\.($binsufsre)$//;
die unless $names->{$name};
$callback->($repodir, @{$names->{$name}});
return undef; # continue extracting
}
#
# Download binaries in batches from a remote obs instance
#
sub fetch_binaries {
my ($url, $repodir, $names, $callback, $ua) = @_;
my @names = sort keys %$names;
while (@names) {
$ua ||= Build::Download::create_ua();
my @nchunk = splice(@names, 0, 100);
my $chunkurl = "$url/_repository?view=cpio";
$chunkurl .= "&binary=".PBuild::Util::urlencode($_) for @nchunk;
my $tmpcpio = "$repodir/.$$.binaries.cpio";
Build::Download::download($chunkurl, $tmpcpio, undef, 'ua' => $ua, 'retry' => 3);
PBuild::Cpio::cpio_extract($tmpcpio, sub {fetch_binaries_cpioextract($_[0], $_[1], $repodir, $names, $callback)});
unlink($tmpcpio);
}
}
#
# Get the repository metadata for an OBS repo
#
sub fetch_repodata {
my ($url, $tmpdir, $arch, $opts, $modules) = @_;
die("bad obs: reference\n") unless $url =~ /^obs:\/{1,3}([^\/]+\/[^\/]+)(?:\/([^\/]*))?$/;
my $prp = $1;
$arch = $2 if $2;
die("please specify the build service url with the --obs option\n") unless $opts->{'obs'};
my $baseurl = $opts->{'obs'};
$baseurl .= '/' unless $baseurl =~ /\/$/;
my $requrl .= "${baseurl}build/$prp/$arch/_repository?view=cache";
$requrl .= "&module=".PBuild::Util::urlencode($_) for @{$modules || []};
Build::Download::download($requrl, "$tmpdir/repository.cpio", undef, 'retry' => 3);
unlink("$tmpdir/repository.data");
PBuild::Cpio::cpio_extract("$tmpdir/repository.cpio", "$tmpdir/repository.data", 'extract' => 'repositorycache', 'missingok' => 1);
my $rdata;
$rdata = PBuild::Util::retrieve("$tmpdir/repository.data") if -s "$tmpdir/repository.data";
my @bins = grep {ref($_) eq 'HASH' && defined($_->{'name'})} values %{$rdata || {}};
for (@bins) {
if ($_->{'path'} =~ /^\.\.\/([^\/\.][^\/]*\/[^\/\.][^\/]*)$/s) {
$_->{'location'} = "${baseurl}build/$prp/$arch/$1"; # obsbinlink to package
} else {
$_->{'location'} = "${baseurl}build/$prp/$arch/_repository/$_->{'path'}";
}
recode_deps($_); # recode deps from testcase format to rpm
}
return \@bins;
}
1;
07070100000011000081a400000000000000000000000163e504b9000017a3000000000000000000000000000000000000001200000000PBuild/Options.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Options;
use Build::Options;
use strict;
my $pbuild_options = {
'h' => 'help',
'help' => '',
'preset' => ':',
'list-presets' => '',
'listpresets' => 'list-presets',
'reponame' => ':',
'noclean' => '',
'no-clean' => 'noclean',
'nochecks' => '',
'no-checks' => 'nochecks',
'arch' => ':',
'hostarch' => ':',
'host-arch' => 'hostarch:',
'target' => ':',
'jobs' => ':',
'threads' => ':',
'buildjobs' => ':',
'root' => ':',
'dist' => '::',
'configdir' => ':',
'repo' => '::',
'repository' => 'repo::',
'registry' => '::',
'assets' => '::',
'obs' => ':',
'hostrepo' => '::',
'hostrepository' => 'hostrepo::',
'result' => \&result_rebuild_special,
'result-pkg' => '::',
'result-code' => '::',
'details' => '',
'terse' => '',
'rebuild' => \&result_rebuild_special,
'rebuild-pkg' => '::',
'rebuild-code' => '::',
'buildtrigger' => ':',
'repoquery' => '::',
'repoquery-host' => '::',
'no-repo-refresh' => '',
'xen' => \&vm_type_special,
'kvm' => \&vm_type_special,
'uml' => \&vm_type_special,
'qemu' => \&vm_type_special,
'emulator' => \&vm_type_special,
'zvm' => \&vm_type_special,
'lxc' => \&vm_type_special,
'vm-type' => ':',
'vm-worker' => ':',
'vm-worker-no' => ':',
'vm-worker-nr' => 'vm-worker-no:',
'vm-server' => ':',
'vm-region' => 'vm-server:',
'vm-disk' => ':',
'vm-swap' => ':',
'swap' => 'vm-swap:',
'vm-memory' => ':',
'memory' => 'vm-memory:',
'vm-kernel' => ':',
'vm-initrd' => ':',
'vm-disk-size' => ':',
'vmdisk-rootsize' => 'vm-disk-size:',
'vm-swap-size' => ':',
'vmdisk-swapsize' => 'vm-swap-size:',
'vm-disk-filesystem' => ':',
'vmdisk-filesystem' => 'vm-disk-filesystem:',
'vm-disk-filesystem-options' => ':',
'vmdisk-filesystem-options' => 'vm-disk-filesystem-options:',
'vm-disk-mount-options' => ':',
'vmdisk-mount-options' => 'vm-disk-mount-options:',
'vm-disk-clean' => '',
'vmdisk-clean' => 'vm-disk-clean',
'vm-hugetlbfs' => ':',
'hugetlbfs' => 'vm-hugetlbfs:',
'vm-watchdog' => '',
'vm-user' => ':',
'vm-enable-console' => '',
'vm-telnet' => ':',
'vm-net' => '::',
'vm-network' => '',
'vm-netdev' => '::',
'vm-device' => '::',
'vm-custom-opt' => ':',
'vm-openstack-flavor' => ':',
'openstack-flavor' => 'vm-openstack-flavor:',
'vm-emulator-script' => ':',
'debuginfo' => '',
'debug' => 'debuginfo',
'emulator-script' => 'vm-emulator-script:',
'single' => ':',
'single-flavor' => ':',
'shell' => '',
'shell-after-fail' => '',
'no-timestamps' => '',
'showlog' => '',
'ccache' => \&ccache_special,
'ccache-type' => '',
};
sub vm_type_special {
my ($opts, $opt, $origopt, $args) = @_;
my $arg;
$arg = Build::Options::getarg($origopt, $args, 1) unless $opt eq 'zvm' || $opt eq 'lxc';
$opts->{'vm-disk'} = $arg if defined $arg;
$opts->{'vm-type'} = $opt;
}
sub ccache_special {
my ($opts, $opt, $origopt, $args) = @_;
my $arg;
$arg = Build::Options::getarg($origopt, $args) if @$args && ref($args->[0]);
$opts->{'ccache'} = 1;
$opts->{'ccache-type'} = $arg if $arg;
}
my @codes = qw{broken succeeded failed unresolvable blocked scheduled waiting building excluded disabled locked};
my %known_codes = map {$_ => 1} @codes;
sub result_rebuild_special {
my ($opts, $opt, $origopt, $args) = @_;
my $arg;
$arg = Build::Options::getarg($origopt, $args, 1) if @$args && (ref($args->[0]) || $args->[0] !~ /\//);
if (!defined($arg) || $arg eq 'all') {
push @{$opts->{"$opt-code"}}, 'all';
} elsif ($known_codes{$arg}) {
push @{$opts->{"$opt-code"}}, $arg;
} else {
push @{$opts->{"$opt-pkg"}}, $arg;
}
}
sub parse_options {
return Build::Options::parse_options($pbuild_options, @_);
}
sub usage {
my ($exitstatus) = @_;
print <<'EOS';
Usage: pbuild [options] [dir]
Build all packages in the directory 'dir'.
Important options (see the man page for a full list):
--dist known_dist|url|file
distribution to build for
--repo url
repository to use, can be given multiple times
--registry url
registry to use, can be given multiple times
--preset name
specify a preset defined in the project
--list-presets
list all known presets
--reponame name
name of the destination dir
defaults to "_build.<dist>.<arch>"
--buildjobs number
build in parallel with 'number' jobs
--root rootdir
do the build in the 'rootdir' directory
defaults to '/var/tmp/build-root'
--arch arch
build for architecture 'arch'
defaults to the host architecture
--obs url
open build service instance for obs:/ type urls
--vm-*, --xen, --kvm, ...
passed to the build tool, see the build(1) manpage
--result
show the package build status
EOS
exit($exitstatus) if defined $exitstatus;
}
sub merge_old_options {
my ($opts, $oldopts) = @_;
my $newopts = {};
for (qw{preset dist repo hostrepo registry assets obs configdir root jobs threads buildjobs}) {
$opts->{$_} = $oldopts->{$_} if !exists($opts->{$_}) && exists($oldopts->{$_});
$newopts->{$_} = $opts->{$_} if exists($opts->{$_});
}
return $newopts;
}
1;
07070100000012000081a400000000000000000000000163e504b900000c71000000000000000000000000000000000000001100000000PBuild/Preset.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Preset;
use PBuild::Structured;
use PBuild::Util;
use strict;
my $dtd_pbuild = [
'pbuild' =>
[[ 'preset' =>
'name',
'default',
'arch',
[ 'config' ],
[ 'repo' ],
[ 'registry' ],
[ 'assets' ],
'obs',
[ 'hostrepo' ],
]],
];
# read presets, take default if non given.
sub read_presets {
my ($dir, $presetname) = @_;
if (-f "$dir/_pbuild") {
my $pbuild = PBuild::Structured::readxml("$dir/_pbuild", $dtd_pbuild);
for my $preset (@{$pbuild->{'preset'} || []}) {
next unless $preset->{'name'};
if (defined($presetname)) {
# check for selected preset
return $preset if $presetname eq $preset->{'name'};
} else {
# check for default
return $preset if exists $preset->{'default'};
}
}
}
die("unknown preset '$presetname'\n") if defined $presetname;
return undef;
}
# get a list of defined presets
sub known_presets {
my ($dir) = @_;
my @presetnames;
if (-f "$dir/_pbuild") {
my $pbuild = PBuild::Structured::readxml("$dir/_pbuild", $dtd_pbuild);
for my $d (@{$pbuild->{'preset'} || []}) {
push @presetnames, $d->{'name'} if defined $d->{'name'};
}
@presetnames = PBuild::Util::unify(@presetnames);
}
return @presetnames;
}
# show presets
sub list_presets {
my ($dir) = @_;
my @presetnames = known_presets($dir);
if (@presetnames) {
print "Known presets:\n";
print " - $_\n" for @presetnames;
} else {
print "No presets defined\n";
}
}
# get reponame/dist/repo/registry options from preset
sub apply_preset {
my ($opts, $preset) = @_;
$opts->{'arch'} = $preset->{'arch'} if $preset->{'arch'} && !$opts->{'arch'};
$opts->{'reponame'} = $preset->{'name'} if $preset->{'name'} && !$opts->{'reponame'};
push @{$opts->{'dist'}}, @{$preset->{'config'}} if $preset->{'config'} && !$opts->{'dist'};
push @{$opts->{'repo'}}, @{$preset->{'repo'}} if $preset->{'repo'} && !$opts->{'repo'};
push @{$opts->{'registry'}}, @{$preset->{'registry'}} if $preset->{'registry'} && !$opts->{'registry'};
push @{$opts->{'assets'}}, @{$preset->{'assets'}} if $preset->{'assets'} && !$opts->{'assets'};
$opts->{'obs'} = $preset->{'obs'} if $preset->{'obs'} && !$opts->{'obs'};
push @{$opts->{'hostrepo'}}, @{$preset->{'hostrepo'}} if $preset->{'hostrepo'} && !$opts->{'hostrepo'};
}
1;
07070100000013000081a400000000000000000000000163e504b900001dfa000000000000000000000000000000000000001100000000PBuild/Recipe.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Recipe;
use strict;
use Build;
use PBuild::Service;
use PBuild::Util;
$Build::Kiwi::urlmapper = 0; # disable url -> prp mapping
#
# Select a recipe file from the source files
#
sub find_recipe {
my ($p, $type) = @_;
my %files = map {$_ => $_} keys %{$p->{'files'} || {}};
return $files{'_preinstallimage'} if $type ne 'kiwi' && keys(%files) == 1 && $files{'_preinstallimage'};
return $files{'simpleimage'} if $files{'simpleimage'};
return $files{'snapcraft.yaml'} if $type eq 'snapcraft' && $files{'snapcraft.yaml'};
return $files{'appimage.yml'} if $type eq 'appimage' && $files{'appimage.yml'};
return $files{'Dockerfile'} if $type eq 'docker' && $files{'Dockerfile'};
return $files{'fissile.yml'} if $type eq 'fissile' && $files{'fissile.yml'};
return $files{'Chart.yaml'} if $type eq 'helm' && $files{'Chart.yaml'};
return (grep {/flatpak\.(?:ya?ml|json)$/} sort keys %files)[0] if $type eq 'flatpak';
return $files{'PKGBUILD'} ? $files{'PKGBUILD'} : undef if $type eq 'arch';
my $pkg = $p->{'pkg'};
$pkg = $p->{'flavor'} if $p->{'flavor'};
return $files{"$pkg.$type"} if $files{"$pkg.$type"};
# try again without last components
return $files{"$1.$type"} if $pkg =~ /^(.*?)\./ && $files{"$1.$type"};
my @files = grep {/\.$type$/} keys %files;
@files = grep {/^\Q$pkg\E/i} @files if @files > 1;
return $files{$files[0]} if @files == 1;
if (@files > 1) {
@files = sort @files;
return $files{$files[0]};
}
return $files{'debian.control'} if $type eq 'dsc' && $files{'debian.control'};
return $files{'debian/control'} if $type eq 'dsc' && $files{'debian/control'};
# as last resort ignore the type for image/container building
if ($type ne 'docker') {
return $files{'Dockerfile'} if $files{'Dockerfile'};
}
if ($type ne 'kiwi') {
@files = grep {/\.kiwi$/} keys %files;
@files = grep {/^\Q$pkg\E/i} @files if @files > 1;
return $files{$files[0]} if @files == 1;
if (@files > 1) {
@files = sort @files;
return $files{$files[0]};
}
}
if (1) {
@files = grep {/mkosi\.$/} keys %files;
return $files{$files[0]} if @files == 1;
if (@files > 1) {
@files = sort @files;
return $files{$files[0]};
}
}
if ($type eq 'spec' && $files{'package/'}) {
my @specs = sort(grep {/\.spec/} PBuild::Util::ls("$p->{'dir'}/package"));
return "package/$specs[0]" if @specs;
}
if ($type eq 'spec' && $files{'dist/'}) {
my @specs = sort(grep {/\.spec/} PBuild::Util::ls("$p->{'dir'}/dist"));
return "dist/$specs[0]" if @specs;
}
return undef;
}
#
# Find and parse a recipe file
#
sub parse {
my ($bconf, $p, $buildtype, $arch, $bconf_host, $arch_host) = @_;
if ($p->{'pkg'} eq '_product') {
$p->{'error'} = 'excluded';
return;
}
my $recipe = find_recipe($p, $buildtype);
if (!$recipe) {
$p->{'error'} = "no recipe found for buildtype $buildtype";
return;
}
$p->{'recipe'} = $recipe;
my $bt = Build::recipe2buildtype($recipe);
if (!$bt) {
$p->{'error'} = "do not know how to build $recipe";
return;
}
$p->{'buildtype'} = $bt;
my $d;
local $bconf->{'buildflavor'} = $p->{'flavor'};
eval {
$d = Build::parse_typed($bconf, "$p->{'dir'}/$recipe", $bt);
die("can not parse $recipe\n") unless $d;
if ($bconf_host && $d->{'nativebuild'}) {
$p->{'native'} = 1;
local $bconf_host->{'buildflavor'} = $p->{'flavor'};
$d = Build::parse_typed($bconf_host, "$p->{'dir'}/$recipe", $bt);
die("can not parse $recipe\n") unless $d;
$arch = $arch_host;
}
die("can not parse name from $recipe\n") unless $d->{'name'};
};
if ($@) {
$p->{'error'} = $@;
$p->{'error'} =~ s/\n.*//s;;
return;
}
my $version = defined($d->{'version'}) ? $d->{'version'} : 'unknown';
$p->{'version'} = $version;
$p->{'name'} = $d->{'name'};
$p->{'dep'} = $d->{'deps'};
$p->{'onlynative'} = $d->{'onlynative'} if $d->{'onlynative'};
$p->{'alsonative'} = $d->{'alsonative'} if $d->{'alsonative'};
if ($d->{'prereqs'}) {
my %deps = map {$_ => 1} (@{$d->{'deps'} || []}, @{$d->{'subpacks'} || []});
my @prereqs = grep {!$deps{$_} && !/^%/} @{$d->{'prereqs'}};
$p->{'prereq'} = \@prereqs if @prereqs;
}
if ($p->{'files'}->{'_service'}) {
push @{$p->{'buildtimeservice'}}, $_ for PBuild::Service::get_buildtimeservices($p);
}
my $imagetype = $bt eq 'kiwi' && $d->{'imagetype'} ? ($d->{'imagetype'}->[0] || '') : '';
if ($bt eq 'kiwi' && $imagetype eq 'product') {
$p->{'nodbgpkgs'} = 1 if defined($d->{'debugmedium'}) && $d->{'debugmedium'} <= 0;
$p->{'nosrcpkgs'} = 1 if defined($d->{'sourcemedium'}) && $d->{'sourcemedium'} <= 0;
}
my $myarch = $bconf->{'target'} ? (split('-', $bconf->{'target'}))[0] : $arch;
$p->{'error'} = 'excluded' if $d->{'exclarch'} && !grep {$_ eq $myarch} @{$d->{'exclarch'}};
$p->{'error'} = 'excluded' if $d->{'badarch'} && grep {$_ eq $myarch} @{$d->{'badarch'}};
$p->{'imagetype'} = $d->{'imagetype'} if $d->{'imagetype'};
$p->{'remoteassets'} = $d->{'remoteassets'} if $d->{'remoteassets'};
# check if we can build this
if ($bt eq 'kiwi' && $imagetype eq 'product') {
$p->{'error'} = 'cannot build kiwi products yet';
return;
}
}
# split host deps from deps if cross building
sub split_hostdeps {
my ($p, $bconf) = @_;
return if $p->{'buildtype'} eq 'kiwi' || $p->{'buildtype'} eq 'docker' || $p->{'buildtype'} eq 'fissile';
return if $p->{'native'};
return unless @{$p->{'dep'} || []};
my %onlynative = map {$_ => 1} @{$bconf->{'onlynative'} || []};
my %alsonative = map {$_ => 1} @{$bconf->{'alsonative'} || []};
for (@{$p->{'onlynative'} || []}) {
if (/^!(.*)/) {
delete $onlynative{$1};
} else {
$onlynative{$_} = 1;
}
}
for (@{$p->{'alsonative'} || []}) {
if (/^!(.*)/) {
delete $alsonative{$1};
} else {
$alsonative{$_} = 1;
}
}
my @hdep;
$p->{'dep_host'} = \@hdep;
return unless %onlynative || %alsonative;
@hdep = grep {$onlynative{$_} || $alsonative{$_}} @{$p->{'dep'}};
$p->{'dep'} = [ grep {!$onlynative{$_}} @{$p->{'dep'}}] if @hdep && %onlynative;
}
sub looks_like_packagedir {
my ($dir) = @_;
return 0 if -d "$dir/.pbuild";
return 0 if -d "$dir/_pbuild";
return 0 if -d "$dir/_config";
return 1 if -d "$dir/../.pbuild";
return 1 if -d "$dir/../_pbuild";
return 1 if -d "$dir/../_config";
my @files = PBuild::Util::ls($dir);
return 0 if grep {/^_build\./} @files;
for my $file (@files) {
return 1 if $file =~ /\.(?:spec|dsc|kiwi)$/;
return 1 if $file =~ /^mkosi\./;
return 1 if $file eq 'snapcraft.yaml' || $file eq 'appimage.yml';
return 1 if $file eq 'Dockerfile' || $file eq 'fissile.yml' || $file eq 'Chart.yml';
return 1 if $file eq 'PKGBUILD';
}
return 0;
}
1;
07070100000014000081a400000000000000000000000163e504b900002bb8000000000000000000000000000000000000001700000000PBuild/RemoteAssets.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::RemoteAssets;
use POSIX;
use Digest::MD5 ();
use Digest::SHA ();
use MIME::Base64 ();
use Build::Download;
use PBuild::Util;
use PBuild::Cpio;
use PBuild::Zip;
use strict;
#
# rename a file unless the target already exists
#
sub rename_unless_present {
my ($old, $new) = @_;
die("link $old $new: $!\n") if !link($old, $new) && $! != POSIX::EEXIST;
unlink($old);
}
#
# create a obscpio asset from a directory
#
sub create_asset_from_dir {
my ($assetdir, $asset, $dir, $mtime) = @_;
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
PBuild::Util::mkdir_p($adir);
my $fd;
open($fd, '>', "$adir/.$assetid.$$") || die("$adir/.$assetid.$$: $!");
PBuild::Cpio::cpio_create($fd, $dir, 'mtime' => $mtime);
close($fd) || die("$adir/.$assetid.$$: $!");
rename_unless_present("$adir/.$assetid.$$", "$adir/$assetid");
}
#
# Recipe file remote asset handling
#
sub recipe_parse {
my ($p) = @_;
my @assets;
for my $s (@{$p->{'remoteassets'} || []}) {
my $url = $s->{'url'};
if ($url && $url =~ /^git(?:\+https?)?:.*\/([^\/]+?)(?:.git)?(?:\#[^\#\/]+)?$/) {
my $file = $1;
next if $p->{'files'}->{$file};
push @assets, { 'file' => $file, 'url' => $url, 'type' => 'url', 'isdir' => 1 };
next;
}
my $file = $s->{'file'};
if (($s->{'type'} || '' eq 'webcache')) {
next unless $s->{'url'};
$file = 'build-webcache-'.Digest::SHA::sha256_hex($s->{'url'});
}
next unless $s->{'url'} =~ /(?:^|\/)([^\.\/][^\/]+)$/s;
$file = $1 unless defined $file;
undef $url unless $url =~ /^https?:\/\/.*\/([^\.\/][^\/]+)$/s;
my $digest = $s->{'digest'};
next unless $digest || $url;
next unless $file =~ /^([^\.\/][^\/]+)$/s;
my $asset = { 'file' => $file };
$asset->{'digest'} = $digest if $digest;
$asset->{'url'} = $url if $url;
$asset->{'type'} = 'url' if $url;
push @assets, $asset;
}
return @assets;
}
# Go module proxy support
#
# Parse the go.sum file to find module information
#
sub golang_parse {
my ($p) = @_;
return unless $p->{'files'}->{'go.sum'} && !$p->{'files'}->{'vendor/'};
my $fd;
my @assets;
open ($fd, '<', "$p->{'dir'}/go.sum") || die("$p->{'dir'}/go.sum: $!\n");
my %mods;
while (<$fd>) {
chomp;
my @s = split(' ', $_);
next unless @s >= 3;
next unless $s[1] =~ /^v./;
next unless $s[2] =~ /^h1:[a-zA-Z0-9\+\/=]+/;
if ($s[1] =~ s/\/go.mod$//) {
$mods{"$s[0]/$s[1]"}->{'mod'} = $s[2];
} else {
$mods{"$s[0]/$s[1]"}->{'zip'} = $s[2];
$mods{"$s[0]/$s[1]"}->{'info'} = undef; # not protected by a checksum
}
}
for my $mod (sort keys %mods) {
my $l = $mods{$mod};
next unless $l->{'mod'}; # need at least the go.mod file
my $k = "$mod";
$k .= " $_ $mods{$mod}->{$_}" for sort keys %{$mods{$mod}};
my $file = "build-gomodcache/$mod";
$file =~ s/\//:/g;
my $assetid = Digest::MD5::md5_hex($k);
my $asset = { 'type' => 'golang', 'file' => $file, 'mod' => $mod, 'parts' => $mods{$mod}, 'isdir' => 1, 'immutable' => 1, 'assetid' => $assetid };
push @assets, $asset;
}
close $fd;
return @assets;
}
#
# Verify a file with the go module h1 checksum
#
sub verify_golang_h1 {
my ($file, $part, $h1) = @_;
my $fd;
open($fd, '<', $file) || die("file: $!\n");
my %content;
if ($part eq 'mod') {
$content{'go.mod'} = {};
} elsif ($part eq 'zip') {
my $l = PBuild::Zip::zip_list($fd);
for (@$l) {
next if $_->{'ziptype'} != 8; # only plain files
die("file $_->{'name'} exceeds size limit\n") if $_->{'size'} >= 500000000;
$content{$_->{'name'}} = $_;
}
}
die("$file: no content\n") unless %content;
my $data = '';
for my $file (sort keys %content) {
my $ctx = Build::Download::digest2ctx("sha256:");
if ($part eq 'zip') {
PBuild::Zip::zip_extract($fd, $content{$file}, 'writer' => sub {$ctx->add($_[0])});
} else {
my $chunk;
$ctx->add($chunk) while read($fd, $chunk, 65536);
}
$data .= $ctx->hexdigest()." $file\n";
}
close($fd);
die("not a h1 checksum: $h1\n") unless $h1 =~ /^h1:/;
my $digest = "sha256:".unpack("H*", MIME::Base64::decode_base64(substr($h1, 3)));
Build::Download::checkdigest($data, $digest);
}
#
# Fetch golang assets from a go module proxy
#
sub golang_fetch {
my ($p, $assetdir, $assets, $url) = @_;
my @assets = grep {$_->{'type'} eq 'golang'} @$assets;
return unless @assets;
print "fetching ".PBuild::Util::plural(scalar(@assets), 'asset')." from $url\n";
for my $asset (@assets) {
my $tmpdir = "$assetdir/.tmpdir.$$";
PBuild::Util::rm_rf($tmpdir);
my $mod = $asset->{'mod'};
my $moddir = $mod;
$moddir =~ s/\/[^\/]+$//;
$moddir =~ s/([A-Z])/'!'.lc($1)/ge;
my $vers = $mod;
$vers =~ s/.*\///;
$vers =~ s/([A-Z])/'!'.lc($1)/ge;
my $cname = "build-gomodcache";
PBuild::Util::mkdir_p("$tmpdir/$cname/$moddir/\@v");
my $parts = $asset->{'parts'};
for my $part (sort keys %$parts) {
my $proxyurl = "$url/$moddir/\@v/$vers.$part";
my $maxsize = $part eq 'zip' ? 500000000 : 16000000;
Build::Download::download($proxyurl, "$tmpdir/$cname/$moddir/\@v/$vers.$part", undef, 'retry' => 3, 'maxsize' => $maxsize);
my $h1 = $parts->{$part};
verify_golang_h1("$tmpdir/$cname/$moddir/\@v/$vers.$part", $part, $h1) if defined $h1;
}
my $mtime = 0; # we want reproducible cpio archives
create_asset_from_dir($assetdir, $asset, $tmpdir, $mtime);
PBuild::Util::rm_rf($tmpdir);
}
}
# Fedora FedPkg / lookaside cache support
#
# Parse a fedora "sources" asset reference file
#
sub fedpkg_parse {
my ($p) = @_;
return unless $p->{'files'}->{'sources'};
my $fd;
my @assets;
open ($fd, '<', "$p->{'dir'}/sources") || die("$p->{'dir'}/sources: $!\n");
while (<$fd>) {
chomp;
my $asset;
if (/^(\S+) \((.*)\) = ([0-9a-fA-F]{32,})$/s) {
$asset = { 'file' => $2, 'digest' => lc("$1:$3") };
} elsif (/^([0-9a-fA-F]{32}) (.*)$/) {
$asset = { 'file' => $2, 'digest' => lc("md5:$1") };
} else {
warn("unparsable line in 'sources' file: $_\n");
next;
}
push @assets, $asset if $asset->{'file'} =~ /^[^\.\/][^\/]*$/s;
}
close $fd;
return @assets;
}
#
# Get missing assets from a fedora lookaside cache server
#
sub fedpkg_fetch {
my ($p, $assetdir, $assets, $url) = @_;
my @assets = grep {$_->{'digest'}} @$assets;
return unless @assets;
die("need a parsed name to download fedpkg assets\n") unless $p->{'name'};
print "fetching ".PBuild::Util::plural(scalar(@assets), 'asset')." from $url\n";
for my $asset (@assets) {
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
PBuild::Util::mkdir_p($adir);
# $url/<name>/<file>/<hashtype>/<hash>/<file>
my $path = $asset->{'digest'};
$path =~ s/:/\//;
$path = "$p->{'name'}/$asset->{'file'}/$path/$asset->{'file'}";
$path = PBuild::Util::urlencode($path);
my $fedpkg_url = $url;
$fedpkg_url =~ s/\/?$/\/$path/;
if (Build::Download::download($fedpkg_url, "$adir/.$assetid.$$", undef, 'retry' => 3, 'digest' => $asset->{'digest'}, 'missingok' => 1)) {
rename_unless_present("$adir/.$assetid.$$", "$adir/$assetid");
}
}
}
# IPFS asset support (parsing is done in the source handler)
#
# Get missing assets from the InterPlanetary File System
#
sub ipfs_fetch {
my ($p, $assetdir, $assets) = @_;
my @assets = grep {($_->{'type'} || '') eq 'ipfs'} @$assets;
return unless @assets;
print "fetching ".PBuild::Util::plural(scalar(@assets), 'asset')." from the InterPlanetary File System\n";
# for now assume /ipfs is mounted...
die("/ipfs is not available\n") unless -d '/ipfs';
for my $asset (@assets) {
my $assetid = $asset->{'assetid'};
die("need a CID to download IPFS assets\n") unless $asset->{'cid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
PBuild::Util::mkdir_p($adir);
PBuild::Util::cp($asset->{'cid'}, "$adir/.$assetid.$$");
rename_unless_present("$adir/.$assetid.$$", "$adir/$assetid");
}
}
# Generic url asset support
sub fetch_git_asset {
my ($assetdir, $asset) = @_;
my $tmpdir = "$assetdir/.tmpdir.$$";
PBuild::Util::rm_rf($tmpdir);
PBuild::Util::mkdir_p($tmpdir);
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
my $file = $asset->{'file'};
PBuild::Util::mkdir_p($adir);
my $url = $asset->{'url'};
die unless $url =~ /^git(?:\+https?)?:/;
$url =~ s/^git\+//;
my @cmd = ('git', 'clone', '-q', '--recurse-submodules');
push @cmd, '-b', $1 if $url =~ s/#([^#]+)$//;
push @cmd, '--', $url, "$tmpdir/$file";
system(@cmd) && die("git clone failed: $!\n");
# get timestamp of last commit
my $pfd;
open($pfd, '-|', 'git', '-C', "$tmpdir/$file", 'log', '--pretty=format:%ct', '-1') || die("open: $!\n");
my $t = <$pfd>;
close($pfd);
chomp $t;
$t = undef unless $t && $t > 0;
if ($asset->{'donotpack'}) {
rename("$tmpdir/$file", "$adir/$assetid") || die("rename $tmpdir $adir/$assetid: $!\n");
} else {
create_asset_from_dir($assetdir, $asset, $tmpdir, $t);
}
PBuild::Util::rm_rf($tmpdir);
}
#
# generic resource fetcher
#
sub url_fetch {
my ($p, $assetdir, $assets) = @_;
my %tofetch_hosts;
# classify by hosts
my %tofetch_host;
for my $asset (grep {($_->{'type'} || '') eq 'url'} @$assets) {
my $url = $asset->{'url'};
die("need a url to download an asset\n") unless $url;
die("weird download url '$url' for asset\n") unless $url =~ /^(.*?\/\/.*?)\//;
push @{$tofetch_host{$1}}, $asset;
}
for my $hosturl (sort keys %tofetch_host) {
my $tofetch = $tofetch_host{$hosturl};
print "fetching ".PBuild::Util::plural(scalar(@$tofetch), 'asset')." from $hosturl\n";
for my $asset (@$tofetch) {
if ($asset->{'url'} =~ /^git(?:\+https?)?:/) {
fetch_git_asset($assetdir, $asset);
next;
}
my $assetid = $asset->{'assetid'};
my $adir = "$assetdir/".substr($assetid, 0, 2);
PBuild::Util::mkdir_p($adir);
if (Build::Download::download($asset->{'url'}, "$adir/.$assetid.$$", undef, 'retry' => 3, 'digest' => $asset->{'digest'}, 'missingok' => 1)) {
rename_unless_present("$adir/.$assetid.$$", "$adir/$assetid");
}
}
}
}
1;
07070100000015000081a400000000000000000000000163e504b900002952000000000000000000000000000000000000001900000000PBuild/RemoteRegistry.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::RemoteRegistry;
use strict;
use LWP::UserAgent;
use URI;
use Build::Download;
use Build::SimpleJSON;
use PBuild::Verify;
eval { require JSON::XS };
*JSON::XS::decode_json = sub {die("JSON::XS is not available\n")} unless defined &JSON::XS::decode_json;
#
# mime types
#
my $mt_docker_manifest = 'application/vnd.docker.distribution.manifest.v2+json';
my $mt_docker_manifestlist = 'application/vnd.docker.distribution.manifest.list.v2+json';
my $mt_oci_manifest = 'application/vnd.oci.image.manifest.v1+json';
my $mt_oci_index = 'application/vnd.oci.image.index.v1+json';
#
# simple anon bearer authenticator
#
sub bearer_authenticate {
my($class, $ua, $proxy, $auth_param, $response, $request, $arg, $size) = @_;
return $response if $ua->{'bearer_authenticate_norecurse'};
local $ua->{'bearer_authenticate_norecurse'} = 1;
my $realm = $auth_param->{'realm'};
die("bearer auth did not provide a realm\n") unless $realm;
die("bearer realm is not http/https\n") unless $realm =~ /^https?:\/\//i;
my $auri = URI->new($realm);
my @afields;
for ('service', 'scope') {
push @afields, $_, $auth_param->{$_} if defined $auth_param->{$_};
}
print "requesting bearer auth from $realm [@afields]\n";
$auri->query_form($auri->query_form, @afields);
my $ares = $ua->get($auri);
return $response unless $ares->is_success;
my $reply = JSON::XS::decode_json($ares->decoded_content);
my $token = $reply->{'token'} || $reply->{'access_token'};
return $response unless $token;
my $url = $proxy ? $request->{proxy} : $request->uri_canonical;
my $host_port = $url->host_port;
my $h = $ua->get_my_handler('request_prepare', 'm_host_port' => $host_port, sub {
$_[0]{callback} = sub { $_[0]->header('Authorization' => "Bearer $token") };
});
return $ua->request($request->clone, $arg, $size, $response);
}
*LWP::Authen::Bearer::authenticate = \&bearer_authenticate;
#
# convert arch to goarch/govariant
#
sub arch2goarch {
my ($arch) = @_;
return ('amd64') if $arch eq 'x86_64';
return ('386') if $arch =~ /^i[3456]86$/;
return ('arm64', 'v8') if $arch eq 'aarch64';
return ('arm', "v$1") if $arch =~ /^armv(\d+)/;
return $arch;
}
#
# select a matching manifest from a manifest index (aka fat manifest)
#
sub select_manifest {
my ($arch, $manifests) = @_;
my ($goarch, $govariant) = arch2goarch($arch);
for my $m (@{$manifests || []}) {
next unless $m->{'digest'};
if ($m->{'platform'}) {
next if $m->{'platform'}->{'architecture'} ne $goarch;
next if $m->{'platform'}->{'variant'} && $govariant && $m->{'platform'}->{'variant'} ne $govariant;
}
return $m;
}
return undef;
}
sub fetch_manifest {
my ($repodir, $registry, @args) = @_;
return Build::Download::fetch(@args) if $registry !~ /docker.io\/?$/;
my ($data, $ct);
eval { ($data, $ct) = Build::Download::head(@args) };
die($@) if $@ && $@ !~ /401 Unauthorized/; # sigh, docker returns 401 for not-existing repositories
return undef unless $data;
my $digest = $data->{'docker-content-digest'};
return Build::Download::fetch(@args) unless $digest;
my $content = PBuild::Util::readstr("$repodir/manifest.$digest", 1);
if ($content) {
Build::Download::checkdigest($content, $digest);
return ($content, $ct);
}
($content, $ct) = Build::Download::fetch(@args);
return undef unless $content;
Build::Download::checkdigest($content, $digest);
PBuild::Util::mkdir_p($repodir);
PBuild::Util::writestr("$repodir/.manifest.$digest.$$", "$repodir/manifest.$digest", $content);
return ($content, $ct);
}
#
# query a registry about a container
#
sub queryremotecontainer {
my ($ua, $arch, $repodir, $registry, $repotag) = @_;
$repotag .= ":latest" unless $repotag =~ /:[^\/:]+$/;
die unless $repotag =~ /^(.*):([^\/:]+)$/;
my ($repository, $tag) = ($1, $2);
$repository = "library/$repository" if $repository !~ /\// && $registry =~ /docker.io\/?$/;
# strip domain part if it matches the registry url
my $registrydomain = $registry;
$registrydomain =~ s/^[^\/]+\/\///;
$registrydomain =~ s/\/.*//;
$repository = $2 if $repository =~ /^([^\/]+)\/(.+)$/s && $1 eq $registrydomain;
my @accept = ($mt_docker_manifestlist, $mt_docker_manifest, $mt_oci_index, $mt_oci_manifest);
my ($data, $ct) = fetch_manifest($repodir, $registry, "$registry/v2/$repository/manifests/$tag",
'ua' => $ua, 'accept' => \@accept, 'missingok' => 1);
return undef unless defined $data;
die("no content type set in answer\n") unless $ct;
if ($ct eq $mt_docker_manifestlist || $ct eq $mt_oci_index) {
# fat manifest, select the one we want
my $r = JSON::XS::decode_json($data);
my $manifest = select_manifest($arch, $r->{'manifests'} || []);
return undef unless $manifest;
@accept = ($mt_docker_manifest, $mt_oci_manifest);
($data, $ct) = fetch_manifest($repodir, $registry, "$registry/v2/$repository/manifests/$manifest->{'digest'}",
'ua' => $ua, 'accept' => \@accept);
die("no content type set in answer\n") unless $ct;
}
die("unknown content type\n") unless $ct eq $mt_docker_manifest || $ct eq $mt_oci_manifest;
my $r = JSON::XS::decode_json($data);
my @blobs;
die("manifest has no config\n") unless $r->{'config'};
push @blobs, $r->{'config'};
push @blobs, @{$r->{'layers'} || []};
PBuild::Verify::verify_digest($_->{'digest'}) for @blobs;
my $id = $blobs[0]->{'digest'};
$id =~ s/.*://;
$id = substr($id, 0, 32);
my $name = $repotag;
$name =~ s/[:\/]/-/g;
$name = "container:$name";
my $version = 0;
my @provides = ("$name = $version");
push @provides, "container:$repotag" unless $name eq "container:$repotag";
my $q = {
'name' => $name,
'version' => $version,
'arch' => 'noarch',
'source' => $name,
'provides' => \@provides,
'hdrmd5' => $id,
'location' => $repository,
'blobs' => \@blobs,
'containertags' => [ $repotag ],
};
return $q;
}
#
# get data from a registry for a set of containers
#
sub fetchrepo {
my ($bconf, $arch, $repodir, $url, $repotags) = @_;
my @bins;
my $ua = Build::Download::create_ua();
for my $repotag (@{$repotags || []}) {
my $bin = queryremotecontainer($ua, $arch, $repodir, $url, $repotag);
push @bins, $bin if $bin;
}
return \@bins;
}
#
# download the blobs needed to reconstruct a container
#
sub fetchbinaries {
my ($repo, $bins) = @_;
my $repodir = $repo->{'dir'};
my $url = $repo->{'url'};
my $nbins = @$bins;
die("bad repo\n") unless $url;
my %tofetch;
for my $bin (@$bins) {
my $blobs = $bin->{'blobs'};
die unless $blobs;
for my $blob (@$blobs) {
my $digest = $blob->{'digest'};
die unless $digest;
next if -s "$repodir/blob.$digest";
$tofetch{"$bin->{'location'}/$digest"} = 1;
}
}
return unless %tofetch;
my @tofetch = sort keys %tofetch;
print "fetching ".PBuild::Util::plural(scalar(@tofetch), 'container blob')." from $url\n";
my $ua = Build::Download::create_ua();
PBuild::Util::mkdir_p($repodir);
for my $tofetch (@tofetch) {
next unless $tofetch =~ /^(.*)\/(.*)?$/;
my ($repository, $digest) = ($1, $2);
next if -s "$repodir/blob.$digest";
Build::Download::download("$url/v2/$repository/blobs/$digest", "$repodir/.blob.$digest.$$", "$repodir/blob.$digest", 'digest' => $digest, 'ua' => $ua);
}
}
#
# create the head/pad data for a tar file entry
#
sub maketarhead {
my ($name, $size, $mtime) = @_;
my $h = "\0\0\0\0\0\0\0\0" x 64;
my $pad = '';
return ("$h$h") unless defined $name;
my $tartype = '0';
die("name too big\n") if length($name) > 100;
my $mode = sprintf("%07o", 0x81a4);
my $fsize = sprintf("%011o", $size);
my $fmtime = sprintf("%011o", $mtime);
substr($h, 0, length($name), $name);
substr($h, 100, length($mode), $mode);
substr($h, 108, 15, "0000000\0000000000"); # uid/gid
substr($h, 124, length($fsize), $fsize);
substr($h, 136, length($fmtime), $fmtime);
substr($h, 148, 8, ' ');
substr($h, 156, 1, $tartype);
substr($h, 257, 8, "ustar\00000"); # magic/version
substr($h, 329, 15, "0000000\0000000000"); # major/minor
substr($h, 148, 7, sprintf("%06o\0", unpack("%16C*", $h)));
$pad = "\0" x (512 - $size % 512) if $size % 512;
return ($h, $pad);
}
#
# reconstruct a container from blobs
#
sub construct_containertar {
my ($repodir, $q, $dst) = @_;
die("construct_containertar: $q->{'name'}: not a container\n") unless $q->{'name'} =~ /^container:/;
my $fd;
open ($fd, '>', $dst) || die("$dst: $!\n");
my $mtime = time();
my $blobs = $q->{'blobs'};
die unless $blobs;
for my $blob (@$blobs) {
my $digest = $blob->{'digest'};
die unless $digest;
my $bfd;
open ($bfd, '<', "$repodir/blob.$digest") || die("$repodir/blob.$digest: $!\n");
my @s = stat($bfd);
die unless @s;
my $size = $s[7];
my ($head, $pad) = maketarhead($digest, $size, $mtime);
print $fd $head;
while ($size > 0) {
my $chunk = $size > 16384 ? 16384 : $size;
my $b = '';
die("unexpected read error in blob\n") unless sysread($bfd, $b, $chunk);
print $fd $b;
$size -= length($b);
}
print $fd $pad;
close($bfd);
}
my @digests = map {$_->{'digest'}} @$blobs;
my $configdigest = shift @digests;
my $manifest = {
'Config' => $configdigest,
'Layers' => \@digests,
'RepoTags' => $q->{'containertags'},
'_order' => [ 'Config', 'RepoTags', 'Layers' ],
};
my $manifest_json = Build::SimpleJSON::unparse([ $manifest ], 'ugly' => 1);
my ($head, $pad) = maketarhead('manifest.json', length($manifest_json), $mtime);
print $fd "$head$manifest_json$pad".maketarhead();
close($fd) || die;
}
1;
07070100000016000081a400000000000000000000000163e504b900004574000000000000000000000000000000000000001500000000PBuild/RemoteRepo.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::RemoteRepo;
use strict;
use Encode;
use IO::Uncompress::Gunzip ();
use Digest::MD5 ();
use Build;
use Build::Rpmmd;
use Build::Archrepo;
use Build::Debrepo;
use Build::Deb;
use Build::Susetags;
use Build::Zypp;
use Build::Modules;
use Build::Download;
use PBuild::Util;
use PBuild::Verify;
use PBuild::OBS;
use PBuild::Cando;
my @binsufs = qw{rpm deb pkg.tar.gz pkg.tar.xz pkg.tar.zst};
my $binsufsre = join('|', map {"\Q$_\E"} @binsufs);
sub download_zypp {
my ($url, $dest, $digest) = @_;
die("do not know how to download $url\n") unless $url =~ m#^zypp://([^/]+)/((?:.*/)?([^/]+)\.rpm)$#;
my ($repo, $path, $pkg) = ($1, $2, $3);
die("bad dest $dest for $pkg\n") if $dest !~ /^(.*)\/[^\/]*\Q$pkg\E\.rpm$/;
my $dir = $1;
system('/usr/bin/zypper', '--no-refresh', '-q', '--pkg-cache-dir', $dir, 'download', '-r', $repo, $pkg)
&& die("zypper download $pkg failed\n");
die("zypper download of $pkg did not create $dir/$repo/$path\n") unless -f "$dir/$repo/$path";
Build::Download::checkfiledigest("$dir/$repo/$path", $digest) if $digest;
rename("$dir/$repo/$path", $dest) || die("rename $dir/$repo/$path $dest: $!\n");
}
sub download {
my ($url, $dest, $destfinal, $digest, $ua) = @_;
return download_zypp($url, $destfinal || $dest, $digest) if $url =~ /^zypp:\/\//;
Build::Download::download($url, $dest, $destfinal, 'digest' => $digest, 'ua' => $ua, 'retry' => 3);
}
sub addpkg {
my ($bins, $pkg, $locprefix, $archfilter) = @_;
return unless defined($pkg->{'name'}) && defined($pkg->{'arch'});
return if $pkg->{'arch'} eq 'src' || $pkg->{'arch'} eq 'nosrc';
return if $archfilter && !$archfilter->{$pkg->{'arch'}};
$locprefix = '' unless defined $locprefix;
$pkg->{'location'} = "$locprefix$pkg->{'location'}" if defined $locprefix;
delete $pkg->{'filename'}; # just in case
delete $pkg->{'packid'}; # just in case
push @$bins, $pkg;
}
sub fetchrepo_arch {
my ($url, $tmpdir, %opts) = @_;
die("could not determine reponame from url $url\n") unless "/$url/" =~ /.*\/([^\/]+)\/os\//;
my $reponame = $1;
$url .= '/' unless $url =~ /\/$/;
download("$url$reponame.db", "$tmpdir/repo.db");
my @bins;
Build::Archrepo::parse("$tmpdir/repo.db", sub { addpkg(\@bins, $_[0], $url) }, 'addselfprovides' => 1, 'normalizedeps' => 1);
return \@bins;
}
sub fetchrepo_debian {
my ($url, $tmpdir, %opts) = @_;
my ($baseurl, $disturl, $components) = Build::Debrepo::parserepourl($url);
die("fetchrepo_debian needs an architecture\n") unless $opts{'arch'};
my $basearch = Build::Deb::basearch($opts{'arch'});
my @bins;
for my $component (@$components) {
unlink("$tmpdir/Packages.gz");
if ($component eq '.') {
download("${disturl}Packages.gz", "$tmpdir/Packages.gz");
die("Packages.gz missing\n") unless -s "$tmpdir/Packages.gz";
} else {
download("$disturl$component/binary-$basearch/Packages.gz", "$tmpdir/Packages.gz");
die("Packages.gz missing for basearch $basearch, component $component\n") unless -s "$tmpdir/Packages.gz";
}
Build::Debrepo::parse("$tmpdir/Packages.gz", sub { addpkg(\@bins, $_[0], $baseurl) }, 'addselfprovides' => 1, 'withchecksum' => 1, 'normalizedeps' => 1);
}
return \@bins;
}
sub open_uncompressed {
my ($filename) = @_;
my $fh;
open($fh, '<', $filename) or die("Error opening $filename: $!\n");
if ($filename =~ /\.gz$/) {
$fh = IO::Uncompress::Gunzip->new($fh) or die("Error opening $filename: $IO::Uncompress::Gunzip::GunzipError\n");
}
return $fh;
}
sub fetchrepo_rpmmd {
my ($url, $tmpdir, %opts) = @_;
my $baseurl = $url;
$baseurl .= '/' unless $baseurl =~ /\/$/;
my @resources;
download("${baseurl}repodata/repomd.xml", "$tmpdir/repomd.xml") unless $opts{'iszypp'};
my $cookie = Digest::MD5::md5_hex(PBuild::Util::readstr("$tmpdir/repomd.xml"));
my $oldrepo = $opts{'oldrepo'};
return $oldrepo if $oldrepo && $oldrepo->{'cookie'} && $cookie eq $oldrepo->{'cookie'};
Build::Rpmmd::parse_repomd("$tmpdir/repomd.xml", \@resources);
my @primaryfiles = grep {$_->{'type'} eq 'primary' && defined($_->{'location'})} @resources;
my $archfilter = $opts{'archfilter'};
my @bins;
for my $f (@primaryfiles) {
my $u = "$f->{'location'}";
utf8::downgrade($u);
next unless $u =~ /(primary\.xml(?:\.gz)?)$/s;
my $fn = $1;
if ($opts{'iszypp'}) {
$fn = $u;
$fn =~ s/.*\///s;
die("zypp repo $url is not up to date, please refresh first\n") unless -s "$tmpdir/$fn";
} else {
die("primary file $u does not have a checksum\n") unless $f->{'checksum'} && $f->{'checksum'} =~ /:(.*)/;
$fn = "$1-$fn";
download("${baseurl}/$f->{'location'}", "$tmpdir/$fn", undef, $f->{'checksum'});
}
my $fh = open_uncompressed("$tmpdir/$fn");
Build::Rpmmd::parse($fh, sub { addpkg(\@bins, $_[0], $baseurl, $archfilter) }, 'addselfprovides' => 1, 'withchecksum' => 1);
last;
}
my @moduleinfofiles = grep {$_->{'type'} eq 'modules' && defined($_->{'location'})} @resources;
for my $f (@moduleinfofiles) {
my $u = "$f->{'location'}";
utf8::downgrade($u);
next unless $u =~ /(modules\.yaml(?:\.gz)?)$/s;
my $fn = $1;
die("zypp:// repos do not support module data\n") if $opts{'iszypp'};
die("modules file $u does not have a checksum\n") unless $f->{'checksum'} && $f->{'checksum'} =~ /:(.*)/;
$fn = "$1-$fn";
download("${baseurl}/$f->{'location'}", "$tmpdir/$fn", undef, $f->{'checksum'});
my $fh = open_uncompressed("$tmpdir/$fn");
my $moduleinfo = {};
Build::Modules::parse($fh, $moduleinfo);
push @bins, { 'name' => 'moduleinfo:', 'data' => $moduleinfo };
last;
}
return { 'bins' => \@bins, 'cookie' => $cookie };
}
sub fetchrepo_susetags {
my ($url, $tmpdir, %opts) = @_;
my $descrdir = 'suse/setup/descr';
my $datadir = 'suse';
my $baseurl = $url;
$baseurl .= '/' unless $baseurl =~ /\/$/;
download("${baseurl}$descrdir/packages.gz", "$tmpdir/packages.gz") unless $opts{'iszypp'};
my $archfilter = $opts{'archfilter'};
my @bins;
Build::Susetags::parse("$tmpdir/packages.gz", sub {
my $xurl = $baseurl;
$xurl =~ s/1\/$/$_[0]->{'medium'}/ if $_[0]->{'medium'};
$xurl .= "$datadir/" if $datadir;
addpkg(\@bins, $_[0], $xurl, $archfilter)
}, 'addselfprovides' => 1, 'withchecksum' => 1);
return \@bins;
}
sub fetchrepo_zypp {
my ($url, $tmpdir, %opts) = @_;
die("zypp repo must start with zypp://\n") unless $url =~ /^zypp:\/\/([^\/]*)/;
my $repo = Build::Zypp::parserepo($1);
my $type = $repo->{'type'};
my $zyppcachedir = "/var/cache/zypp/raw/$repo->{'name'}";
if (!$type) {
$type = 'yast2' if -e "$zyppcachedir/suse/setup/descr";
$type = 'rpm-md' if -e "$zyppcachedir/repodata";
}
die("could not determine repo type for '$repo->{'name'}'\n") unless $type;
if($type eq 'rpm-md') {
die("zypp repo $url is not up to date, please refresh first\n") unless -s "$zyppcachedir/repodata/repomd.xml";
return fetchrepo_rpmmd("zypp://$repo->{'name'}", "$zyppcachedir/repodata", %opts, 'iszypp' => 1);
} else {
die("zypp repo $url is not up to date, please refresh first\n") unless -s "$zyppcachedir/suse/setup/descr/packages.gz";
return fetchrepo_susetags("zypp://$repo->{'name'}", "$zyppcachedir/suse/setup/descr", %opts, 'iszypp' => 1);
}
}
sub fetchrepo_obs {
my ($url, $tmpdir, %opts) = @_;
my $modules = $opts{'modules'};
my $bins = PBuild::OBS::fetch_repodata($url, $tmpdir, $opts{'arch'}, $opts{'opts'}, $modules);
@$bins = sort {$a->{'name'} cmp $b->{'name'}} @$bins;
for (@$bins) {
delete $_->{'filename'}; # just in case
delete $_->{'packid'}; # just in case
}
push @$bins, { 'name' => 'moduleinfo:', 'modules' => $modules } if @{$modules || []};
return $bins;
}
#
# Generate the on-disk filename from the metadata
#
sub calc_binname {
my ($bin) = @_;
my $suf;
if ($bin->{'name'} =~ /^container:/) {
$suf = 'tar';
} else {
die("bad location: $bin->{'location'}\n") unless $bin->{'location'} =~ /\.($binsufsre)$/;
$suf = $1;
}
my $binname = $bin->{'version'};
$binname = "$bin->{'epoch'}:$binname" if $bin->{'epoch'};
$binname .= "-$bin->{'release'}" if defined $bin->{'release'};
$binname .= ".$bin->{'arch'}" if $bin->{'arch'};
$binname = "$bin->{'name'}-$binname.$suf";
$binname = "$bin->{'hdrmd5'}-$binname" if $binname =~ s/^container:// && $bin->{'hdrmd5'};
return $binname;
}
#
# Replace already downloaded entries in the metadata
#
sub replace_with_local {
my ($repodir, $bins) = @_;
my $bad;
my %files = map {$_ => 1} PBuild::Util::ls($repodir);
delete $files{'_metadata'};
delete $files{'.tmp'};
for my $bin (@$bins) {
next if $bin->{'name'} eq 'moduleinfo:';
my $file = $bin->{'filename'};
if (defined $file) {
if (!$files{$file}) {
$bad = 1;
next;
}
$files{$file} = 2;
next;
}
$file = calc_binname($bin);
next unless $files{$file};
if ($bin->{'name'} =~ /^container:/) {
delete $bin->{'id'};
$bin->{'filename'} = $file;
next;
}
eval {
my $q = querybinary($repodir, $file);
%$bin = %$q;
$files{$file} = 2;
};
if ($@) {
warn($@);
unlink($file);
}
}
for my $file (grep {$files{$_} == 1} sort keys %files) {
unlink("$repodir/$file");
}
return $bad ? 0 : 1;
}
#
# Guess the repotype from the build config
#
sub guess_repotype {
my ($bconf, $buildtype) = @_;
return undef unless $bconf;
for (@{$bconf->{'repotype'} || []}) {
return $_ if $_ eq 'arch' || $_ eq 'debian' || $_ eq 'hdlist2' || $_ eq 'rpm-md';
}
return 'arch' if ($bconf->{'binarytype'} || '') eq 'arch';
return 'debian' if ($bconf->{'binarytype'} || '') eq 'deb';
$buildtype ||= $bconf->{'type'};
return 'rpm-md' if ($buildtype || '') eq 'spec';
return 'debian' if ($buildtype || '') eq 'dsc';
return 'arch' if ($buildtype || '') eq 'arch';
return undef;
}
#
# Get repository metadata for a remote repository
#
sub fetchrepo {
my ($bconf, $arch, $repodir, $url, $buildtype, $opts) = @_;
my $repotype;
$repotype = 'zypp' if $url =~ /^zypp:/;
$repotype = 'obs' if $url =~ /^obs:/;
my $archfilter;
if ($url =~ /^(arch|debian|hdlist2|rpmmd|rpm-md|suse)(?:\+archfilter=([^\@\/]+))?\@(.*)$/) {
$repotype = $1;
$archfilter = [ split(',', $2) ] if $2;
$url = $3;
}
$repotype ||= guess_repotype($bconf, $buildtype) || 'rpmmd';
$archfilter ||= [ PBuild::Cando::archfilter($arch) ] if $repotype ne 'obs';
if ($archfilter) {
$archfilter = { map {$_ => 1} @$archfilter };
$archfilter->{$_} = 1 for qw{all any noarch};
}
my $modules = [ PBuild::Util::unify(sort(@{$bconf->{'modules'} || []})) ];
my $repofile = "$repodir/_metadata";
my $cookie;
my $oldrepo;
if (-s $repofile) {
$oldrepo = PBuild::Util::retrieve($repofile, 1);
undef $oldrepo unless ref($oldrepo) eq 'HASH' && $oldrepo->{'bins'};
if ($oldrepo && $repotype eq 'obs') {
# obs repo data changes with the modules, so be careful
my $oldbins = $oldrepo->{'bins'};
my $repomodules = [];
if (@$oldbins && $oldbins->[-1]->{'name'} eq 'moduleinfo:') {
$repomodules = $oldbins->[-1]->{'modules'} || [];
}
undef $oldrepo if join(',', @$modules) ne join(',', @$repomodules);
}
undef $oldrepo if $oldrepo && !replace_with_local($repodir, $oldrepo->{'bins'});
return $oldrepo->{'bins'} if $oldrepo && $opts->{'no-repo-refresh'};
}
my $tmpdir = "$repodir/.tmp";
PBuild::Util::cleandir($tmpdir) if -e $tmpdir;
PBuild::Util::mkdir_p($tmpdir);
my $repo;
my %opts = ( 'arch' => $arch, 'archfilter' => $archfilter, 'modules' => $modules, 'oldrepo' => $oldrepo , 'opts' => $opts);
if ($repotype eq 'rpmmd' || $repotype eq 'rpm-md') {
$repo = fetchrepo_rpmmd($url, $tmpdir, %opts);
} elsif ($repotype eq 'debian') {
$repo = fetchrepo_debian($url, $tmpdir, %opts);
} elsif ($repotype eq 'arch') {
$repo = fetchrepo_arch($url, $tmpdir, %opts);
} elsif ($repotype eq 'suse') {
$repo = fetchrepo_susetags($url, $tmpdir, %opts);
} elsif ($repotype eq 'zypp') {
$repo = fetchrepo_zypp($url, $tmpdir, %opts);
} elsif ($repotype eq 'obs') {
$repo = fetchrepo_obs($url, $tmpdir, %opts);
} else {
die("unsupported repotype '$repotype'\n");
}
$repo = { 'bins' => $repo } if $repo && ref($repo) ne 'HASH';
die unless $repo && $repo->{'bins'};
replace_with_local($repodir, $repo->{'bins'}) unless $repo == $oldrepo;
PBuild::Util::store("$repodir/._metadata.$$", $repofile, $repo);
return $repo->{'bins'};
}
#
# Expand the special zypp:// repo to all enabled zypp repositories
#
sub expand_zypp_repo {
my ($repos) = @_;
return unless grep {/^zypp:\/{0,2}$/} @{$repos || []};
my @r;
for my $url (@$repos) {
if ($url =~ /^zypp:\/{0,2}$/) {
for my $r (Build::Zypp::parseallrepos()) {
push @r, "zypp://$r->{'name'}" if $r->{'enabled'};
}
} else {
push @r, $url;
}
}
@$repos = @r;
}
#
# Check if the downloaded package matches the repository metadata
#
sub is_matching_binary {
my ($b1, $b2) = @_;
return 0 if $b1->{'name'} ne $b2->{'name'};
return 0 if $b1->{'arch'} ne $b2->{'arch'};
return 0 if $b1->{'version'} ne $b2->{'version'};
return 0 if ($b1->{'epoch'} || 0) ne ($b2->{'epoch'} || 0);
return 0 if (defined $b1->{'release'} ? $b1->{'release'} : '__undef__') ne (defined $b2->{'release'} ? $b2->{'release'} : '__undef__');
return 1;
}
#
# Query dependencies of a downloaded binary package
#
sub querybinary {
my ($dir, $file) = @_;
my @s = stat("$dir/$file");
die("$dir/$file: $!\n") unless @s;
my $id = "$s[9]/$s[7]/$s[1]";
my $data;
my $leadsigmd5;
die("$dir/$file: no hdrmd5\n") unless Build::queryhdrmd5("$dir/$file", \$leadsigmd5);
$data = Build::query("$dir/$file", 'evra' => 1, 'conflicts' => 1, 'weakdeps' => 1, 'addselfprovides' => 1, 'filedeps' => 1, 'normalizedeps' => 1);
die("$dir/$file: query failed\n") unless $data;
PBuild::Verify::verify_nevraquery($data);
$data->{'leadsigmd5'} = $leadsigmd5 if $leadsigmd5;
$data->{'filename'} = $file;
$data->{'id'} = $id;
return $data;
}
#
# Check if the downloaded binary matches and replace the stub with it
#
sub fetchbinaries_replace {
my ($repodir, $tmpname, $binname, $bin) = @_;
Build::Download::checkfiledigest("$repodir/$tmpname", $bin->{'checksum'}) if $bin->{'checksum'};
my $q = querybinary($repodir, $tmpname);
die("downloaded binary $binname does not match repository metadata\n") unless is_matching_binary($bin, $q);
rename("$repodir/$tmpname", "$repodir/$binname") || die("rename $repodir/$tmpname $repodir/$binname\n");
$q->{'filename'} = $binname;
%$bin = %$q; # inline replace!
}
#
# Download missing binaries in batches from a remote obs instance
#
sub fetchbinaries_obs {
my ($repo, $bins, $ua) = @_;
my $url;
my %names;
for my $bin (@$bins) {
next if $bin->{'filename'};
my $location = $bin->{'location'};
die("missing location for binary $bin->{'name'}\n") unless $location;
next if $location =~ /^zypp:/ || $location !~ /(.+)\/_repository\//;
my $binname = calc_binname($bin);
PBuild::Verify::verify_filename($binname);
$url = $1 unless defined $url;
next if $1 ne $url;
$names{$bin->{'name'}} = [ ".$$.$binname", $binname, $bin ];
}
return unless %names;
my $repodir = $repo->{'dir'};
PBuild::Util::mkdir_p($repodir);
PBuild::OBS::fetch_binaries($url, $repodir, \%names, \&fetchbinaries_replace, $ua);
}
#
# Download missing binaries from a remote repository
#
sub fetchbinaries {
my ($repo, $bins) = @_;
my $repodir = $repo->{'dir'};
my $url = $repo->{'url'};
die("bad repo\n") unless $url;
print "fetching ".PBuild::Util::plural(scalar(@$bins), 'binary')." from $url\n";
PBuild::Util::mkdir_p($repodir);
my $ua = Build::Download::create_ua();
fetchbinaries_obs($repo, $bins, $ua) if $url =~ /^obs:/;
for my $bin (@$bins) {
next if $bin->{'filename'};
my $location = $bin->{'location'};
die("missing location for binary $bin->{'name'}\n") unless $location;
die("bad location: $location\n") unless $location =~ /^(?:https?|zypp):\/\//;
my $binname = calc_binname($bin);
PBuild::Verify::verify_filename($binname);
my $tmpname = ".$$.$binname";
if ($bin->{'name'} =~ /^container:/) {
# we cannot query containers, just download and set the filename
die("container has no hdrmd5\n") unless $bin->{'hdrmd5'};
download($location, "$repodir/$tmpname", "$repodir/$binname", "md5:$bin->{'hdrmd5'}", $ua);
delete $bin->{'id'};
$bin->{'filename'} = $binname;
next;
}
download($location, "$repodir/$tmpname", undef, undef, $ua);
fetchbinaries_replace($repodir, $tmpname, $binname, $bin);
}
# update _metadata
PBuild::Util::store("$repodir/._metadata.$$", "$repodir/_metadata", $repo->{'bins'});
}
1;
07070100000017000081a400000000000000000000000163e504b9000017c7000000000000000000000000000000000000001200000000PBuild/RepoMgr.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::RepoMgr;
use strict;
use PBuild::Util;
use PBuild::RemoteRepo;
use PBuild::RemoteRegistry;
use PBuild::Verify;
#
# Create the repo manager
#
sub create {
return bless {};
}
#
# Add a remote repository to the manager
#
sub addremoterepo {
my ($repos, $bconf, $myarch, $builddir, $repourl, $buildtype, $opts) = @_;
return addemptyrepo($repos) if $repourl =~ /^empty:/;
my $id = Digest::MD5::md5_hex("$myarch/$repourl");
return $repos->{$id} if $repos->{$id};
my $repodir = "$builddir/.pbuild/_base/$id";
my $bins = PBuild::RemoteRepo::fetchrepo($bconf, $myarch, $repodir, $repourl, $buildtype, $opts);
$_->{'repoid'} = $id for @$bins;
my $repo = { 'dir' => $repodir, 'bins' => $bins, 'url' => $repourl, 'arch' => $myarch, 'type' => 'repo', 'repoid' => $id };
$repos->{$id} = $repo;
return $repo;
}
#
# Add a remote registry to the manager
#
sub addremoteregistry {
my ($repos, $bconf, $myarch, $builddir, $registry, $tags) = @_;
my $repourl = $registry;
$repourl = "https://$repourl" unless $repourl =~ /^[^\/]+\/\//;
my $id = Digest::MD5::md5_hex("$myarch/$repourl");
return $repos->{$id} if $repos->{$id};
my $repodir = "$builddir/.pbuild/_base/$id";
my $bins = PBuild::RemoteRegistry::fetchrepo($bconf, $myarch, $repodir, $repourl, $tags);
$_->{'repoid'} = $id for @$bins;
my $repo = { 'dir' => $repodir, 'bins' => $bins, 'url' => $repourl, 'arch' => $myarch, 'type' => 'registry', 'repoid' => $id };
$repos->{$id} = $repo;
return $repo;
}
#
# Add a local repository to the manager
#
sub addlocalrepo {
my ($repos, $bconf, $myarch, $builddir, $pkgsrc, $pkgs) = @_;
my $id = "$myarch/local";
die("local repo already added\n") if $repos->{$id};
my $bins = PBuild::LocalRepo::fetchrepo($bconf, $myarch, $builddir, $pkgsrc, $pkgs);
$_->{'repoid'} = $id for @$bins;
my $repo = { 'dir' => $builddir, 'bins' => $bins, 'arch' => $myarch, 'type' => 'local', 'repoid' => $id };
$repos->{$id} = $repo;
return $repo;
}
#
# Add an emptt repository to the manager
#
sub addemptyrepo {
my ($repos) = @_;
my $id = 'empty';
return $repos->{$id} if $repos->{$id};
my $repo = { 'bins' => [], 'type' => 'empty', 'repoid' => $id };
$repos->{$id} = $repo;
return $repo;
}
#
# Update the local reposiory with new binary data
#
sub updatelocalrepo {
my ($repos, $bconf, $myarch, $builddir, $pkgsrc, $pkgs) = @_;
my $id = "$myarch/local";
my $repo = $repos->{$id};
die("local repo does not exist\n") unless $repo;
my $bins = PBuild::LocalRepo::fetchrepo($bconf, $myarch, $builddir, $pkgsrc, $pkgs);
$_->{'repoid'} = $id for @$bins;
$repo->{'bins'} = $bins;
}
#
# Fetch missing binaries from a remote repo/registry
#
sub getremotebinaries {
my ($repos, $bins) = @_;
my %tofetch;
for my $q (@$bins) {
push @{$tofetch{$q->{'repoid'}}}, $q unless $q->{'filename'};
}
for my $repoid (sort {$a cmp $b} keys %tofetch) {
my $repo = $repos->{$repoid};
die("bad repoid $repoid\n") unless $repo;
if ($repo->{'type'} eq 'repo') {
PBuild::RemoteRepo::fetchbinaries($repo, $tofetch{$repoid});
} elsif ($repo->{'type'} eq 'registry') {
PBuild::RemoteRegistry::fetchbinaries($repo, $tofetch{$repoid});
} else {
die("unknown repo type $repo->{'type'}\n");
}
$_->{'repoid'} = $repoid for @{$tofetch{$repoid}};
}
}
#
# Setup the repo/containers directories used for image/container builds
#
sub copyimagebinaries {
my ($repos, $bins, $dstdir) = @_;
PBuild::Util::mkdir_p("$dstdir/repos/pbuild/pbuild");
for my $q (@$bins) {
my $repo = $repos->{$q->{'repoid'}};
die("package $q->{'name'} has no repo\n") unless $repo;
my $to;
if ($q->{'name'} =~ /^container:/) {
PBuild::Util::mkdir_p("$dstdir/containers");
$to = "$q->{'name'}.tar";
$to =~ s/^container://;
$to =~ s/[\/:]/_/g;
PBuild::Verify::verify_filename($to);
$to = "$dstdir/containers/$to";
} else {
die("package $q->{'name'} is not available\n") unless $q->{'filename'};
PBuild::Verify::verify_filename($q->{'filename'});
$to = "$dstdir/repos/pbuild/pbuild/$q->{'filename'}";
}
if ($repo->{'type'} eq 'registry') {
PBuild::RemoteRegistry::construct_containertar($repo->{'dir'}, $q, $to);
next;
}
die("package $q->{'name'} is not available\n") unless $q->{'filename'};
PBuild::Verify::verify_filename($q->{'filename'});
my $from = "$repo->{'dir'}/$q->{'filename'}";
$from = "$repo->{'dir'}/$q->{'packid'}/$q->{'filename'}" if $q->{'packid'};
$from = "$repo->{'dir'}/$q->{'packid'}/$q->{'lnk'}" if $q->{'packid'} && $q->{'lnk'}; # obsbinlnk
PBuild::Util::cp($from, $to);
}
}
#
# Return the on-disk locations for a set of binary names
#
sub getbinarylocations {
my ($repos, $bins) = @_;
my %locations;
for my $q (@$bins) {
my $repo = $repos->{$q->{'repoid'}};
die("package $q->{'name'} has no repo\n") unless $repo;
die("package $q->{'name'} is not available\n") unless $q->{'filename'};
if ($q->{'packid'}) {
$locations{$q->{'name'}} = "$repo->{'dir'}/$q->{'packid'}/$q->{'filename'}";
} else {
$locations{$q->{'name'}} = "$repo->{'dir'}/$q->{'filename'}";
}
}
return \%locations;
}
1;
07070100000018000081a400000000000000000000000163e504b900001dc7000000000000000000000000000000000000001400000000PBuild/Repoquery.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Repoquery;
use strict;
use Build;
use Build::Rpm;
use PBuild::Expand;
use PBuild::Modules;
#
# match a parsed complex dependency against a set of provides
#
sub matchdeps_cplx {
my ($pp, $r, $binarytype) = @_;
if ($r->[0] == 0) {
for my $p (@$pp) {
return 1 if Build::matchsingledep($p, $r->[1], $binarytype);
}
} elsif ($r->[0] == 1 || $r->[0] == 2) { # and or
return 1 if matchdeps_cplx($pp, $r->[1], $binarytype);
return 1 if matchdeps_cplx($pp, $r->[2], $binarytype);
} elsif ($r->[0] == 3 || $r->[0] == 4) { # if unless
return 1 if matchdeps_cplx($pp, $r->[1], $binarytype);
return 1 if @$r == 4 && matchdeps_cplx($pp, $r->[3], $binarytype);
} elsif ($r->[0] == 6) { # with
return 1 if matchdeps_cplx($pp, $r->[1], $binarytype) && matchdeps_cplx($pp, $r->[2], $binarytype);
} elsif ($r->[0] == 7) { # without
return 1 if matchdeps_cplx($pp, $r->[1], $binarytype) && !matchdeps_cplx($pp, $r->[2], $binarytype);
}
return 0;
}
#
# match a dependency against a single provides
#
sub matchdep {
my ($p, $d, $binarytype) = @_;
if ($d =~ /\|/) {
# debian or
for my $od (split(/\s*\|\s*/, $d)) {
return 1 if Build::matchsingledep($p, $od, $binarytype);
}
return 0;
}
return Build::matchsingledep($p, $d, $binarytype);
}
#
# compare to packages by epoch/version/release
#
sub evrcmp {
my ($obin, $bin, $verscmp) = @_;
my $evr = $bin->{'version'};
$evr = "$bin->{'epoch'}:$evr" if $bin->{'epoch'};
$evr .= "-$bin->{'release'}" if defined $bin->{'release'};
my $oevr = $obin->{'version'};
$oevr = "$obin->{'epoch'}:$oevr" if $obin->{'epoch'};
$oevr .= "-$obin->{'release'}" if defined $obin->{'release'};
return 0 if $oevr eq $evr;
return $verscmp->($oevr, $evr) || $oevr cmp $evr;
}
#
# compare to packages by architecure (noarch > otherarch)
#
sub archcmp {
my ($obin, $bin) = @_;
my $arch = $bin->{'arch'} || '';
$arch = 'noarch' if !$arch || $arch eq 'all' || $arch eq 'any';
my $oarch = $obin->{'arch'} || '';
$oarch = 'noarch' if !$oarch || $oarch eq 'all' || $oarch eq 'any';
return -1 if $arch eq 'noarch' && $oarch ne 'noarch';
return 1 if $oarch eq 'noarch' && $arch ne 'noarch';
return $oarch cmp $arch;
}
#
# return true if a package is not selected by the configured modules
#
sub ispruned {
my ($modules, $moduledata, $bin) = @_;
return 0 unless $moduledata;
my @modules = PBuild::Modules::getmodules($moduledata, $bin);
return 0 unless @modules;
my $pruned = PBuild::Modules::prune_to_modules($modules, $moduledata, [ $bin ]);
return @$pruned ? 0 : 1;
}
#
# match the available packages against a given query
#
sub repoquery {
my ($bconf, $myarch, $repos, $query, $opts) = @_;
my @query = @{$query || []};
die("Please specify a query\n") unless @query;
for (@query) {
if (/^(name|requires|provides|conflicts|recommends|supplements|obsoletes):(.*)$/) {
$_ = [ $1, $2 ];
} else {
$_ = [ 'provides', $_ ];
}
if ($_->[1] =~ /^\(.*\)$/) {
$_->[3] = Build::Rpm::parse_rich_dep($_->[1]);
} elsif ($_->[1] =~ /\|/) {
$_->[2] = undef; # debian or
} elsif ($_->[1] !~ /^(.*?)\s*([<=>]{1,2})\s*(.*?)$/) {
$_->[2] = $_->[1];
} else {
$_->[2] = $1;
}
if ($_->[0] ne 'name' && $_->[0] ne 'provides') {
die("provides '$_->[1]' cannot be complex\n") if $_->[3];
die("provides '$_->[1]' cannot use debian or\n") if $_->[1] =~ /\|/;
}
}
my $binarytype = $bconf->{'binarytype'};
my $verscmp = $binarytype eq 'deb' ? \&Build::Deb::verscmp : \&Build::Rpm::verscmp;
my $packs = PBuild::Expand::configure_repos($bconf, $repos);
for my $repo (@$repos) {
my $bins = $repo->{'bins'} || [];
my $moduledata;
$moduledata = $bins->[-1]->{'data'} if @$bins && $bins->[-1]->{'name'} eq 'moduleinfo:';
my $repoprinted;
for my $bin (@$bins) {
next if $bin->{'name'} eq 'moduleinfo:';
my $match;
for my $q (@query) {
my $dd;
if ($q->[0] eq 'name') {
my $evr = $bin->{'version'};
$evr = "$bin->{'epoch'}:$evr" if $bin->{'epoch'};
$evr .= "-$bin->{'release'}" if defined $bin->{'release'};
$dd = [ "$bin->{'name'} = $evr" ];
} else {
$dd = $bin->{$q->[0]};
}
next unless $dd;
if ($q->[0] eq 'name' || $q->[0] eq 'provides') {
if ($q->[3]) {
if (matchdeps_cplx($dd, $q->[3], $binarytype)) {
$match = 1;
last;
}
next;
}
if ($q->[1] =~ /\|/) {
# debian or, cannot pre-filter
for my $d (@$dd) {
if (matchdep($d, $q->[1], $binarytype)) {
$match = 1;
last;
}
}
next;
}
for my $d (grep {/^\Q$q->[2]\E/} @$dd) {
if (matchdep($d, $q->[1], $binarytype)) {
$match = 1;
last;
}
}
} else {
for my $d (grep {/^\Q$q->[2]\E/} @$dd) {
if (matchdep($q->[1], $d, $binarytype)) {
$match = 1;
last;
}
}
}
last if $match;
}
next unless $match;
my $excluded;
my $taken = $packs->{$bin->{'name'}};
if (($taken || 0) != $bin) {
$excluded = 'unknown' unless $taken;
$excluded = 'unselected module' if !$excluded && ispruned($bconf->{'modules'}, $moduledata, $bin);
$excluded = 'repo layering' if !$excluded && $taken->{'repoid'} ne $bin->{'repoid'};
$excluded = 'smaller version' if !$excluded && evrcmp($taken, $bin, $verscmp) >= 0;
$excluded = 'smaller architecture' if !$excluded && archcmp($taken, $bin) >= 0;
$excluded ||= 'unknown';
}
my $evr = $bin->{'version'};
$evr = "$bin->{'epoch'}:$evr" if $bin->{'epoch'};
$evr .= "-$bin->{'release'}" if defined $bin->{'release'};
my $nevra = "$bin->{'name'}-$evr.$bin->{'arch'}";
my $from = $repo->{'type'} eq 'local' ? 'build result' : $repo->{'url'};
if ($opts->{'details'}) {
print "$nevra\n";
print " repo: $from\n";
print " excluded: $excluded\n" if $excluded;
my @modules;
@modules = PBuild::Modules::getmodules($moduledata, $bin) if $moduledata;
if (@modules) {
print " modules:\n";
print " - $_\n" for @modules;
}
for my $d (qw{provides requires conflicts obsoletes recommends supplements suggests enhances}) {
my $dd = $bin->{$d};
next unless @{$dd || []};
print " $d:\n";
print " - $_\n" for @$dd;
}
} else {
if (!$repoprinted) {
print "repo: $from\n";
$repoprinted = 1;
}
if ($excluded) {
$excluded = '<v' if $excluded eq 'smaller version';
$excluded = '<a' if $excluded eq 'smaller architecture';
$excluded = '<r' if $excluded eq 'repo layering';
$excluded = '!m' if $excluded eq 'unselected module';
$excluded = '??' if length($excluded) != 2;
} else {
$excluded = ' ';
}
print " $excluded $nevra\n";
}
}
}
}
1;
07070100000019000081a400000000000000000000000163e504b900000ae1000000000000000000000000000000000000001100000000PBuild/Result.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Result;
use strict;
use PBuild::Util;
my @code_order = qw{broken succeeded failed unresolvable blocked scheduled waiting building excluded disabled locked};
my %code_failures = map {$_ => 1} qw{broken failed unresolvable};
sub print_result {
my ($opts, $builddir) = @_;
my $r = PBuild::Util::retrieve("$builddir/.pbuild/_result", 1);
die("pbuild has not run yet for $builddir\n") unless $r;
$opts->{'result-pkg'} = [ $opts->{'single'} ] if $opts->{'single'};
my %codefilter = map {$_ => 1} @{$opts->{'result-code'} || []};
my %pkgfilter = map {$_ => 1} @{$opts->{'result-pkg'} || []};
my $found_failures = 0;
my %codes_seen;
for my $pkg (sort keys %$r) {
next if %pkgfilter && !$pkgfilter{$pkg};
my $code = $r->{$pkg}->{'code'} || 'unknown';
$found_failures = 1 if $code_failures{$code};
next if %codefilter && !$codefilter{'all'} && !$codefilter{$code};
push @{$codes_seen{$code}}, $pkg;
}
my @codes_seen;
for (@code_order) {
push @codes_seen, $_ if $codes_seen{$_};
}
@codes_seen = PBuild::Util::unify(@codes_seen, sort keys %codes_seen);
for my $code (@codes_seen) {
my $ncode = @{$codes_seen{$code}};
printf "%-10s %d\n", "$code:", $ncode;
next if ($code eq 'disabled' || $code eq 'excluded') && !$codefilter{$code};
next unless $opts->{'result-code'} || $opts->{'result-pkg'};
for my $pkg (@{$codes_seen{$code}}) {
if (!$opts->{'terse'}) {
my $details = $r->{$pkg}->{'details'};
if ($details) {
print " $pkg ($details)\n";
} else {
print " $pkg\n";
}
} else {
print " $pkg\n";
}
}
}
return $found_failures;
}
sub has_failed {
my ($opts, $builddir, $pkg) = @_;
my $r = PBuild::Util::retrieve("$builddir/.pbuild/_result", 1);
die("pbuild has not run yet for $builddir\n") unless $r;
my $code = $r->{$pkg}->{'code'} || 'unknown';
return $code_failures{$code} ? 1 : 0;
}
1;
0707010000001a000081a400000000000000000000000163e504b90000073e000000000000000000000000000000000000001200000000PBuild/Service.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Service;
use strict;
use PBuild::Util;
use PBuild::Structured;
my $dtd_services = [
'services' =>
[[ 'service' =>
'name',
'mode', # "localonly" is skipping this service on server side, "trylocal" is trying to merge changes directly in local files, "disabled" is just skipping it
[[ 'param' =>
'name',
'_content'
]],
]],
];
#
# Parse a _service file from a package
#
sub parse_service {
my ($p) = @_;
return undef unless $p->{'files'}->{'_service'};
return PBuild::Structured::readxml("$p->{'dir'}/_service", $dtd_services, 1, 1);
}
#
# return the buildtime services of a package
#
sub get_buildtimeservices {
my ($p) = @_;
return () unless $p->{'files'}->{'_service'};
my @bt;
my $services = parse_service($p);
for my $service (@{$services->{'service'} || []}) {
push @bt, $service->{'name'} if ($service->{'mode'} || '') eq 'buildtime';
}
return sort(PBuild::Util::unify(@bt));
}
1;
0707010000001b000081a400000000000000000000000163e504b9000010a0000000000000000000000000000000000000001100000000PBuild/Source.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Source;
use strict;
use Digest::MD5;
use PBuild::Util;
sub find_packages {
my ($dir) = @_;
my @pkgs;
for my $pkg (sort(PBuild::Util::ls($dir))) {
next if $pkg =~ /^[\._]/;
next unless -d "$dir/$pkg";
push @pkgs, $pkg;
}
return @pkgs;
}
sub gendigest {
my ($fn) = @_;
my $fd;
open($fd, '<', $fn) || die("$fn: $!\n");
my $ctx = Digest::MD5->new;
$ctx->addfile($fd);
close $fd;
return $ctx->hexdigest();
}
sub genlnkdigest {
my ($fn) = @_;
my $lnk = readlink($fn);
die("$fn: $!\n") unless defined $lnk;
return Digest::MD5::md5_hex($lnk);
}
sub gendirdigest {
my ($dir) = @_;
my %files;
for my $file (sort(PBuild::Util::ls($dir))) {
my @s = lstat("$dir/$file");
if (!@s) {
warn("$dir: $!\n");
next;
}
if (-l _) {
$files{$file} = genlnkdigest("$dir/$file");
} elsif (-d _) {
$files{"$file/"} = gendirdigest("$dir/$file");
} elsif (-f _) {
$files{$file} = gendigest("$dir/$file");
}
}
return calc_srcmd5(\%files);
}
sub get_scm_controlled {
my ($dir) = @_;
return {} unless -d "$dir/.git" || -d "$dir/../.git";
my $fd;
my @controlled;
#open($fd, '-|', 'git', '-C', $dir, 'ls-files', '-z') || die("git: $!\n");
open($fd, '-|', 'git', '-C', $dir, 'ls-tree', '--name-only', '-z', 'HEAD') || die("git: $!\n");
my $d = '';
1 while sysread($fd, $d, 8192, length($d));
close($fd) || die("git ls-tree failed: $?\n");
my @d = split("\0", $d);
s/\/.*// for @d;
return { map {$_ => 1} @d };
}
sub is_subdir_build {
my ($dir, $files) = @_;
my @sd = grep {$_ eq 'dist' || $_ eq 'package'} @$files;
return 0 unless @sd;
return 0 if grep {/\.spec$/} @$files;
for my $sd (@sd) {
return 1 if grep {/\.spec$/} PBuild::Util::ls("$dir/$sd");
}
}
sub list_package {
my ($dir) = @_;
my %files;
my @assets;
my $controlled;
my @all = sort(PBuild::Util::ls($dir));
for my $file (@all) {
next if $file eq '_meta' || $file eq '.git';
next if $file =~ /\n/;
my @s = lstat("$dir/$file");
die("$dir/$file: $!\n") unless @s;
my $lnk;
if (-l _) {
$lnk = readlink("$dir/$file");
die("readlink $dir/$file: $!\n") unless defined $lnk;
if ($lnk =~ /^(\/ipfs\/.+)$/s) {
my $assetid = Digest::MD5::md5_hex($1);
push @assets, { 'file' => $file, 'cid' => $1, 'assetid' => $assetid, 'immutable' => 1, 'type' => 'ipfs' };
next;
}
}
if (-l _ || -d _ || $file =~ /^\./) {
if (!$controlled) {
$controlled = get_scm_controlled($dir);
if (!%$controlled && is_subdir_build($dir, \@all)) {
$controlled = { map {$_ => 1} @all };
}
# redo stat because get_scm_controlled changed it
lstat("$dir/$file") || die("$dir/$file: $!\n");
}
if (!$controlled->{$file}) {
next if -d _ || $file =~ /^\./;
@s = stat("$dir/$file");
next unless @s && -f _; # follow links to files
}
}
if (-l _) {
$files{"$file/"} = genlnkdigest("$dir/$file");
} elsif (-d _) {
$files{"$file/"} = gendirdigest("$dir/$file");
} elsif (-f _) {
$files{$file} = gendigest("$dir/$file");
}
}
$files{"debian/control"} = gendigest("$dir/debian/control") if $files{'debian/'} && -s "$dir/debian/control";
return \%files, \@assets;
}
sub calc_srcmd5 {
my ($files) = @_;
my $meta = '';
$meta .= "$files->{$_} $_\n" for sort keys %$files;
return Digest::MD5::md5_hex($meta);
}
1;
0707010000001c000081a400000000000000000000000163e504b900001168000000000000000000000000000000000000001500000000PBuild/Structured.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Structured;
use Build::SimpleXML;
use PBuild::Util;
use strict;
#
# Convert dtd to a hash mapping elements/attributes to multi/subdtd tupels
#
sub _toknown {
my ($me, @dtd) = @_;
my %known = map {ref($_) ? (!@$_ ? () : (ref($_->[0]) ? $_->[0]->[0] : $_->[0] => $_)) : ($_=> $_)} @dtd;
for my $v (values %known) {
if (!ref($v)) {
$v = 0; # string
} elsif (@$v == 1 && !ref($v->[0])) {
$v = 1; # array of strings
} elsif (@$v == 1) {
$v = [1, _toknown(@{$v->[0]}) ]; # array of sub-elements
} else {
$v = [0, _toknown(@$v) ]; # sub-element
}
}
$known{'.'} = $me;
return \%known;
}
#
# Process a single element
#
sub _workin {
my ($known, $out, $in, $allowunknown) = @_;
die("bad input\n") unless ref($in) eq 'HASH';
for my $x (sort keys %$in) {
my $k = $known->{$x};
if ($x eq '_content') {
$in->{$x} =~ s/^\s+//s;
$in->{$x} =~ s/\s+$//s;
next if $in->{$x} eq '';
}
if (!defined($k) && defined($known->{''})) {
$k = $known->{''};
die("bad dtd\n") unless ref($k);
if (!$k->[0]) {
die("element '' must be singleton\n") if exists $out->{''};
$out = $out->{''} = {};
} else {
push @{$out->{''}}, {};
$out = $out->{''}->[-1];
}
$known= $k->[1];
$k = $known->{$x};
}
if (!defined($k)) {
next if $allowunknown;
die("unknown element: $x\n");
}
my $v = $in->{$x};
if (ref($v) eq '') {
# attribute
if (ref($k)) {
die("attribute '$x' must be element\n") if @{$known->{$x}} > 1 || ref($known->{$x}->[0]);
push @{$out->{$x}}, $v;
} else {
die("attribute '$x' must be singleton\n") if exists $out->{$x};
$out->{$x} = $v;
}
next;
}
die("bad input\n") unless ref($v) eq 'ARRAY';
for (@$v) {
die("bad element '$x'\n") if ref($_) ne 'HASH';
}
if (!ref($k)) {
for (@$v) {
die("element '$x' has subelements\n") if %$_ && (!exists($_->{'_content'}) || keys(%$_) != 1);
}
if (!$k) {
die("element '$x' must be singleton\n") unless @$v == 1 && !exists($out->{$x});
$out->{$x} = $v->[0]->{'_content'};
} else {
push @{$out->{$x}}, map {$_->{'_content'}} @$v;
}
} else {
if (!$k->[0]) {
die("element '$x' must be singleton\n") unless @$v == 1 && !exists($out->{$x});
$out->{$x} = {};
_workin($k->[1], $out->{$x}, $v->[0], $allowunknown);
} else {
for (@$v) {
push @{$out->{$x}}, {};
_workin($k->[1], $out->{$x}->[-1], $_, $allowunknown);
}
}
}
}
}
#
# Postprocess parsed xml data by matching it to a dtd
#
sub xmlpostprocess {
my ($d, $dtd, $allowunknown) = @_;
my $me = $dtd->[0];
my $known = {$me => [ 0, _toknown(@$dtd) ] };
my $out = {};
_workin($known, $out, $d, $allowunknown);
die("xml is not a '$me' element\n") unless defined $out->{$me};
return $out->{$me};
}
#
# Convert data from xml
#
sub fromxml {
my ($d, $dtd, $nonfatal, $allowunknown) = @_;
eval {
$d = Build::SimpleXML::parse($d, 'notrim' => 1);
$d = xmlpostprocess($d, $dtd, $allowunknown);
};
if ($@) {
return undef if $nonfatal;
die($@);
}
return $d;
}
#
# Read a file containing XML, parse and postprocess it according to the provided dtd
#
sub readxml {
my ($fn, $dtd, $nonfatal, $allowunknown) = @_;
my $d = PBuild::Util::readstr($fn, $nonfatal);
return $d unless defined $d;
eval {
$d = Build::SimpleXML::parse($d, 'notrim' => 1);
$d = xmlpostprocess($d, $dtd, $allowunknown);
};
if ($@) {
return undef if $nonfatal;
die("$fn: $@");
}
return $d;
}
1;
0707010000001d000081a400000000000000000000000163e504b9000016ea000000000000000000000000000000000000000f00000000PBuild/Util.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Util;
use strict;
use POSIX;
use Storable ();
sub unify {
my %h = map {$_ => 1} @_;
return grep(delete($h{$_}), @_);
}
sub clone {
return Storable::dclone($_[0]);
}
sub writestr {
my ($fn, $fnf, $d) = @_;
my $f;
open($f, '>', $fn) || die("$fn: $!\n");
if (length($d)) {
(syswrite($f, $d) || 0) == length($d) || die("$fn write: $!\n");
}
close($f) || die("$fn close: $!\n");
return unless defined $fnf;
rename($fn, $fnf) || die("rename $fn $fnf: $!\n");
}
sub readstr {
my ($fn, $nonfatal) = @_;
my $f;
if (!open($f, '<', $fn)) {
die("$fn: $!\n") unless $nonfatal;
return undef;
}
my $d = '';
1 while sysread($f, $d, 8192, length($d));
close $f;
return $d;
}
sub touch($) {
my ($file) = @_;
if (-e $file) {
my $t = time();
utime($t, $t, $file);
} else {
# create new file, mtime is anyway current
my $f;
open($f, '>>', $file) || die("$file: $!\n");
close($f) || die("$file close: $!\n");
}
}
sub ls {
my $d;
opendir($d, $_[0]) || return ();
my @r = grep {$_ ne '.' && $_ ne '..'} readdir($d);
closedir $d;
return @r;
}
sub mkdir_p {
my ($dir) = @_;
return 1 if -d $dir;
my $pdir;
if ($dir =~ /^(.+)\//) {
$pdir = $1;
mkdir_p($pdir) || return undef;
}
while (!mkdir($dir, 0777)) {
my $e = $!;
return 1 if -d $dir;
if (defined($pdir) && ! -d $pdir) {
mkdir_p($pdir) || return undef;
next;
}
$! = $e;
warn("mkdir: $dir: $!\n");
return undef;
}
return 1;
}
sub cleandir {
my ($dir) = @_;
my $ret = 1;
return 1 unless -d $dir;
for my $c (ls($dir)) {
if (! -l "$dir/$c" && -d _) {
cleandir("$dir/$c");
$ret = undef unless rmdir("$dir/$c");
} else {
$ret = undef unless unlink("$dir/$c");
}
}
return $ret;
}
sub rm_rf {
my ($file) = @_;
my @s = lstat($file);
if (@s && -d _) {
cleandir($file);
die("rmdir $file: $!\n") unless rmdir($file);
} elsif (@s) {
unlink($file) || die("unlink $file: $!\n");
}
}
sub xfork {
while (1) {
my $pid = fork();
return $pid if defined $pid;
die("fork: $!\n") if $! != POSIX::EAGAIN;
sleep(5);
}
}
sub cp {
my ($from, $to, $tof) = @_;
my ($f, $t);
open($f, '<', $from) || die("$from: $!\n");
open($t, '>', $to) || die("$to: $!\n");
my $buf;
while (sysread($f, $buf, 8192)) {
(syswrite($t, $buf) || 0) == length($buf) || die("$to write: $!\n");
}
close($f);
close($t) || die("$to: $!\n");
if (defined($tof)) {
rename($to, $tof) || die("rename $to $tof: $!\n");
}
}
sub cp_a {
my ($from, $to, $tof) = @_;
rm_rf($to);
system('cp', '-a', '--', $from, $to) && die("cp $from $to: $?\n");
if (defined($tof)) {
rename($to, $tof) || die("rename $to $tof: $!\n");
}
}
sub store {
my ($fn, $fnf, $dd) = @_;
if (!Storable::nstore($dd, $fn)) {
die("nstore $fn: $!\n");
}
return unless defined $fnf;
$! = 0;
rename($fn, $fnf) || die("rename $fn $fnf: $!\n");
}
sub retrieve {
my ($fn, $nonfatal) = @_;
my $dd;
if (!$nonfatal) {
$dd = ref($fn) ? Storable::fd_retrieve($fn) : Storable::retrieve($fn);
die("retrieve $fn: $!\n") unless $dd;
} else {
eval {
$dd = ref($fn) ? Storable::fd_retrieve($fn) : Storable::retrieve($fn);
};
if (!$dd && $nonfatal == 2) {
if ($@) {
warn($@);
} else {
warn("retrieve $fn: $!\n");
}
}
}
return $dd;
}
sub identical {
my ($d1, $d2, $except, $subexcept) = @_;
if (!defined($d1)) {
return defined($d2) ? 0 : 1;
}
return 0 unless defined($d2);
my $r = ref($d1);
return 0 if $r ne ref($d2);
if ($r eq '') {
return 0 if $d1 ne $d2;
} elsif ($r eq 'HASH') {
my %k = (%$d1, %$d2);
for my $k (keys %k) {
next if $except && $except->{$k};
return 0 unless identical($d1->{$k}, $d2->{$k}, $subexcept, $subexcept);
}
} elsif ($r eq 'ARRAY') {
return 0 unless @$d1 == @$d2;
for (my $i = 0; $i < @$d1; $i++) {
return 0 unless identical($d1->[$i], $d2->[$i], $subexcept, $subexcept);
}
} else {
return 0;
}
return 1;
}
sub store_unless_identical {
my ($fn, $fnf, $dd, $olddd) = @_;
$olddd ||= retrieve(defined($fnf) ? $fnf : $fn, 1);
store($fn, $fnf, $dd) unless identical($dd, $olddd);
}
sub isotime {
my ($t) = @_;
my @lt = localtime($t || time());
return sprintf "%04d-%02d-%02d %02d:%02d:%02d", $lt[5] + 1900, $lt[4] + 1, @lt[3,2,1,0];
}
sub urlencode {
my ($str, $iscgi) = @_;
if ($iscgi) {
$str =~ s/([\000-\037<>;\"#\?&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/sge;
$str =~ tr/ /+/;
} else {
$str =~ s/([\000-\040<>;\"#\?&\+=%[\177-\377])/sprintf("%%%02X",ord($1))/sge;
}
return $str;
}
sub urldecode {
my ($str, $iscgi) = @_;
$str =~ tr/+/ / if $iscgi;
$str =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/sge;
return $str;
}
sub plural {
my ($n, $what) = @_;
return "$n $what" if $n == 1;
return "$n ${what}ies" if $what =~ s/y$//;
return "$n ${what}s";
}
1;
0707010000001e000081a400000000000000000000000163e504b900000df9000000000000000000000000000000000000001100000000PBuild/Verify.pm################################################################
#
# Copyright (c) 2021 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Verify;
use strict;
sub verify_simple {
my $name = $_[0];
die("illegal characters\n") if $name =~ /[^\-+=\.,0-9:%{}\@#%A-Z_a-z~\200-\377]/s;
}
sub verify_filename {
my $filename = $_[0];
die("filename is empty\n") unless defined($filename) && $filename ne '';
die("filename '$filename' is illegal\n") if $filename =~ /[\/\000-\037]/;
die("filename '$filename' is illegal\n") if $filename =~ /^\./;
}
sub verify_arch {
my $arch = $_[0];
die("arch is empty\n") unless defined($arch) && $arch ne '';
die("arch '$arch' is illegal\n") if $arch =~ /[\/:\.\000-\037]/;
die("arch '$arch' is illegal\n") unless $arch;
die("arch '$arch' is too long\n") if length($arch) > 200;
verify_simple($arch);
}
sub verify_packid {
my $packid = $_[0];
die("packid is empty\n") unless defined($packid) && $packid ne '';
die("packid '$packid' is too long\n") if length($packid) > 200;
if ($packid =~ /(?<!^_product)(?<!^_patchinfo):./) {
# multibuild case: first part must be a vaild package, second part simple label
die("packid '$packid' is illegal\n") unless $packid =~ /\A([^:]+):([^:]+)\z/s;
my ($p1, $p2) = ($1, $2);
die("packid '$packid' is illegal\n") if $p1 eq '_project' || $p1 eq '_pattern';
verify_packid($p1);
die("packid '$packid' is illegal\n") unless $p2 =~ /\A[^_\.\/:\000-\037][^\/:\000-\037]*\z/;
return;
}
return if $packid =~ /\A(?:_product|_pattern|_project|_patchinfo)\z/;
return if $packid =~ /\A(?:_product:|_patchinfo:)[^_\.\/:\000-\037][^\/:\000-\037]*\z/;
die("packid '$packid' is illegal\n") if $packid =~ /[\/:\000-\037]/;
die("packid '$packid' is illegal\n") if $packid =~ /^[_\.]/;
die("packid '$packid' is illegal\n") unless $packid;
}
sub verify_digest {
my $digest = $_[0];
die("digest is empty\n") unless defined($digest) && $digest ne '';
die("digest '$digest' is illegal\n") unless $digest =~ /^(?:[a-zA-Z0-9]+:)?[a-fA-F0-9]+$/s;
}
sub verify_nevraquery {
my ($q) = @_;
verify_arch($q->{'arch'});
die("binary has no name\n") unless defined $q->{'name'};
die("binary has no version\n") unless defined $q->{'version'};
my $f = "$q->{'name'}-$q->{'version'}";
$f .= "-$q->{'release'}" if defined $q->{'release'};
verify_filename($f);
verify_simple($f);
}
sub verify_multibuild {
my ($mb) = @_;
die("multibuild cannot have both package and flavor elements\n") if $mb->{'package'} && $mb->{'flavor'};
for my $packid (@{$mb->{'package'} || []}) {
verify_packid($packid);
die("packid $packid is illegal in multibuild\n") if $packid =~ /:/;
}
for my $packid (@{$mb->{'flavor'} || []}) {
verify_packid($packid);
die("flavor $packid is illegal in multibuild\n") if $packid =~ /:/;
}
}
1;
0707010000001f000081a400000000000000000000000163e504b900001cfe000000000000000000000000000000000000000e00000000PBuild/Zip.pm################################################################
#
# Copyright (c) 2022 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
package PBuild::Zip;
use strict;
sub readbytes {
my ($handle, $size, $pos) = @_;
die("zip_list: file too small\n") if defined($pos) && $pos < 0;
return '' if $size == 0;
die("zip readbytes: invalid size $size\n") if $size < 0 || $size >= 0x1000000;
seek($handle, $pos, 0) || die("seek: $!\n") if defined($pos);
my $d;
my $r = read($handle, $d, $size);
die("zip read: $!\n") unless defined $r;
die("zip read: unexpeced EOF ($r != $size)\n") unless $r == $size;
return $d;
}
sub quadit {
die("quad overflow\n") if $_[1] >= 65536;
$_[0] = $_[0] + $_[1] * 65536 * 65536;
}
sub extract_stored {
my ($handle, $size, $csize, $writer) = @_;
while ($size > 0) {
my $chunksize = $size > 65536 ? 65536 : $size;
$writer->(readbytes($handle, $chunksize));
$size -= $chunksize;
}
}
sub extract_inflate {
my ($handle, $size, $csize, $writer) = @_;
return unless $size > 0;
require Compress::Raw::Zlib unless defined &Compress::Raw::Zlib::Inflate;
my ($decomp, $status) = Compress::Raw::Zlib::Inflate->new('-WindowBits' => -Compress::Raw::Zlib::MAX_WBITS(), '-Bufsize' => 65536);
die("Compress::Raw::Zlib::Inflate::new failed\n") unless $decomp && $status == Compress::Raw::Zlib::Z_OK();
while ($size > 0 || $csize > 0) {
die("unexpected EOF\n") unless $csize > 0;
my $chunksize = $csize > 65536 ? 65536 : $csize;
my $chunk = readbytes($handle, $chunksize);
$csize -= $chunksize;
my $infchunk = '';
($status) = $decomp->inflate($chunk, $infchunk);
die("decompression error\n") unless $status == Compress::Raw::Zlib::Z_OK() || $status == Compress::Raw::Zlib::Z_STREAM_END();
die("decompression returned too much data\n") if length($infchunk) > $size;
$writer->($infchunk) if length($infchunk);
$size -= length($infchunk);
last if $status == Compress::Raw::Zlib::Z_STREAM_END();
}
die("decompressed returned too few data\n") if $size;
}
sub zip_extract {
my ($handle, $ent, %opts) = @_;
die("cannot extract this type of entry\n") if defined($ent->{'ziptype'}) && $ent->{'ziptype'} != 8 && $ent->{'ziptype'} != 10;
my $data = '';
return $data if $ent->{'size'} == 0;
my $writer = $opts{'writer'} || sub { $data .= $_[0] };
die("missing local header offset\n") unless defined $ent->{'lhdroffset'};
my $lfh = readbytes($handle, 30, $ent->{'lhdroffset'});
my ($lfh_magic, $lfh_vneed, $lfh_bits, $lfh_comp, $lfh_time, $lfh_date, $lfh_crc, $lfh_csize, $lfh_size, $lfh_fnsize, $lfh_extrasize) = unpack('VvvvvvVVVvv', $lfh);
die("missing local file header\n") unless $lfh_magic == 0x04034b50;
readbytes($handle, $lfh_fnsize + $lfh_extrasize); # verify file name?
# can't use lfh size values because they may be in the trailing data descriptor
if ($lfh_comp == 8) {
extract_inflate($handle, $ent->{'size'}, $ent->{'csize'}, $writer);
} elsif ($lfh_comp == 0) {
extract_stored($handle, $ent->{'size'}, $ent->{'csize'}, $writer);
} else {
die("unsupported compression type $lfh_comp\n");
}
return $data;
}
sub zip_list {
my ($handle) = @_;
my @s = stat($handle);
die("zip_list: $!\n") unless @s;
die("zip_list: only files supported\n") unless -f _;
my $zipsize = $s[7];
my $eocd = readbytes($handle, 22, $zipsize - 22);
my ($eocd_magic, $eocd_disk, $eocd_cddisk, $eocd_cdcnt, $eocd_tcdcnt, $cd_size, $cd_offset, $eocd_commentsize) = unpack('VvvvvVVv', $eocd);
die("not a (commentless) zip archive\n") unless $eocd_magic == 0x06054b50 && $eocd_commentsize == 0;
if ($eocd_cdcnt == 0xffff || $eocd_tcdcnt == 0xffff || $cd_size == 0xffffffff || $cd_offset == 0xffffffff) {
my $eocd64l = readbytes($handle, 20, $zipsize - 42);
my ($eocd64l_magic, $eocd64l_cddisk, $eocd64_offset, $eocd64_offset_hi, $eocd64l_ndisk) = unpack('VVVVV', $eocd64l);
die("missing end of central directory locator\n") unless $eocd64l_magic == 0x07064b50;
quadit($eocd64_offset, $eocd64_offset_hi);
die("multidisc zip archive\n") unless $eocd64l_cddisk == 0;
die("invalid eocd64 offset\n") if $eocd64_offset > $zipsize - (20 + 22) || $zipsize - (20 + 22) - $eocd64_offset >= 0x10000 || $zipsize - (20 + 22) - $eocd64_offset < 56;
$eocd = readbytes($handle, 56, $eocd64_offset);
my ($eocd_cdcnt_hi, $eocd_tcdcnt_hi, $cd_offset_hi, $cd_size_hi);
($eocd_magic, undef, undef, undef, undef, $eocd_disk, $eocd_cddisk, $eocd_cdcnt, $eocd_cdcnt_hi, $eocd_tcdcnt, $eocd_tcdcnt_hi, $cd_size, $cd_size_hi, $cd_offset, $cd_offset_hi) = unpack('VVVvvVVVVVVVVVV');
die("missing zip64 end of central directory record\n") unless $eocd_magic == 0x06064b50;
quadit($eocd_cdcnt, $eocd_cdcnt_hi);
quadit($eocd_tcdcnt, $eocd_tcdcnt_hi);
quadit($cd_offset, $cd_offset_hi);
die("invalid cd offset\n") if $cd_offset >= $eocd64_offset;
die("central directory size mismatch\n") if $cd_size != $eocd64_offset - $cd_offset;
} else {
die("central directory size mismatch\n") if $cd_size != $zipsize - 22 - $cd_offset;
}
die("multidisc zip archive\n") unless $eocd_disk == 0 && $eocd_cddisk == 0;
die("central directory too big\n") if $cd_size >= 0x1000000;
my $left = $cd_size;
my @l;
while ($left > 0) {
die("bad directory entry\n") if $left < 46;
my $ent = readbytes($handle, 46, !@l ? $cd_offset : undef);
my ($ent_magic, $ent_vmade, $ent_vneed, $ent_bits, $ent_comp, $ent_time, $ent_date, $ent_crc, $ent_csize, $ent_size, $ent_fnsize, $ent_extrasize, $ent_commentsize, $ent_diskno, $ent_iattr, $ent_xattr, $ent_lhdr) = unpack('VvvvvvvVVVvvvvvVV', $ent);
die("bad directory entry\n") if $left < 46 + $ent_fnsize + $ent_extrasize + $ent_commentsize;
my $name = readbytes($handle, $ent_fnsize);
my $extra = readbytes($handle, $ent_extrasize);
my $comment = readbytes($handle, $ent_commentsize);
my $ziptype;
my $ent_system = $ent_vmade >> 8;
my $ent_mode;
if ($ent_system == 3) {
$ziptype = $ent_xattr >> 28;
$ent_mode = ($ent_xattr >> 16) & 07777;
} else {
$ziptype = 8;
$ziptype = 4 if $name =~ /\/$/;
$ent_mode = $ziptype == 8 ? 0644 : 0755;
$ent_mode &= 0555 if $ent_system == 0 && ($ent_xattr & 1) != 0;
}
$name =~ s/\/+$//;
$name =~ s/^\/+//;
$name = '.' if $name eq '';
push @l, { 'name' => $name, 'size' => $ent_size, 'csize' => $ent_csize, 'comp' => $ent_comp, 'lhdroffset' => $ent_lhdr , 'extra' => $extra, 'comment' => $comment, 'bits' => $ent_bits, 'ziptype' => $ziptype, 'mode' => $ent_mode, 'crc' => $ent_crc };
$left -= 46 + $ent_fnsize + $ent_extrasize + $ent_commentsize;
}
return \@l;
}
1;
07070100000020000041ed00000000000000000000000163e504b900000000000000000000000000000000000000000000000700000000PBuild07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!