Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:adrianSuSE
build
PBuild.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
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!!!
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor