#!/usr/bin/perl -w
#
# Copyright (c) 2006, 2007 Michael Schroeder, Novell Inc.
# Copyright (c) 2008 Adrian Schroeter, Novell Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 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
#
################################################################
#
# The Scheduler. One big chunk of code for now.
#

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC,  "$wd/build";
  unshift @INC,  "$wd";
}

use Digest::MD5 ();
use Data::Dumper;
use Storable ();
use XML::Structured ':bytes';
use POSIX;
use Fcntl qw(:DEFAULT :flock);

use BSConfig;
use BSRPC ':https';
use BSUtil;
use BSFileDB;
use BSXML;
use BSDBIndex;
use BSBuild;
use BSVerify;
use Build;
use BSDB;
use Meta;
use BSSolv;
use BSCando;

use strict;

my $testprojid;
my $testmode;
my $asyncmode;
my $startupmode;

$asyncmode = $BSConfig::sched_asyncmode if $BSConfig::sched_asyncmode;
$startupmode = $BSConfig::sched_startupmode if $BSConfig::sched_startupmode;

my $bsdir = $BSConfig::bsdir || "/srv/obs";

my @binsufs = qw{rpm deb pkg.tar.gz pkg.tar.xz};
my $binsufsre = join('|', map {"\Q$_\E"} @binsufs);

BSUtil::mkdir_p_chown($bsdir, $BSConfig::bsuser, $BSConfig::bsgroup);
BSUtil::drop_privs_to($BSConfig::bsuser, $BSConfig::bsgroup);

my $sign;
$sign = $BSConfig::sign if defined($BSConfig::sign);

my $proxy;
$proxy = $BSConfig::proxy if defined($BSConfig::proxy);

BSUtil::set_fdatasync_before_rename() unless $BSConfig::disable_data_sync || $BSConfig::disable_data_sync;

my $reporoot = "$bsdir/build";
my $jobsdir = "$bsdir/jobs";
my $eventdir = "$bsdir/events";
my $extrepodir = "$bsdir/repos";
my $extrepodir_sync = "$bsdir/repos_sync";
my $extrepodb = "$bsdir/db/published";
my $uploaddir = "$bsdir/upload";
my $rundir = $BSConfig::rundir || "$bsdir/run";
my $infodir = "$bsdir/info";
my $remotecache = "$BSConfig::bsdir/remotecache";

if (@ARGV && $ARGV[0] eq '--testmode') {
  $testmode = 1;
  shift @ARGV;
}
if (@ARGV && ($ARGV[0] eq '--exit' || $ARGV[0] eq '--stop')) {
  $testmode = 'exit';
  shift @ARGV;
} elsif (@ARGV && $ARGV[0] eq '--restart') {
  $testmode = 'restart';
  shift @ARGV;
}

my $myarch = $ARGV[0] || 'i586';

if (!$BSCando::knownarch{$myarch}) {
  die("Architecture '$myarch' is unknown, please adapt BSCando.pm\n");
}

my $myjobsdir = "$jobsdir/$myarch";
my $myeventdir = "$eventdir/$myarch";

my $historylay = [qw{versrel bcnt srcmd5 rev time}];

my %remoteprojs;	# remote project cache

# Create directory on first start
mkdir_p($infodir) || die ("Failed to create ".$infodir);

my $buildavg = 1200; # start not at 0, but with 20min for the average ounter


sub unify {
  my %h = map {$_ => 1} @_;
  return grep(delete($h{$_}), @_);
}

sub sendevent {
  my ($ev, $arch, $evname) = @_;

  mkdir_p("$eventdir/$arch");
  $evname = "$ev->{'type'}:::".Digest::MD5::md5_hex($evname) if length($evname) > 200;
  writexml("$eventdir/$arch/.$evname$$", "$eventdir/$arch/$evname", $ev, $BSXML::event);
  local *F;
  if (sysopen(F, "$eventdir/$arch/.ping", POSIX::O_WRONLY|POSIX::O_NONBLOCK)) {
    syswrite(F, 'x');
    close(F);
  }
}

#
# input: depsp  -> hash of arrays
#        mapp   -> hash of strings
#
# 
sub sortpacks {
  my ($depsp, $mapp, $cycp, @packs) = @_;

  return @packs if @packs < 2;
  my @cycs;
  @packs = BSSolv::depsort($depsp, $mapp, \@cycs, @packs);
  if (@cycs) {
    @$cycp = @cycs if $cycp;
    print "cycle: ".join(' -> ', @$_)."\n" for @cycs;
  }
  return @packs;
}

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;
}

sub findbins_dir {
  my ($dir, $cache) = @_;
  my @bins;
  if (ref($dir)) {
    @bins = grep {/\.(?:$binsufsre)$/} @$dir;
  } else {
    @bins = ls($dir);
    @bins = map {"$dir/$_"} grep {/\.(?:$binsufsre|raw|raw\.install)$/} sort @bins;
  }
  my $repobins = {};
  for my $bin (@bins) {
    my @s = stat($bin);
    next unless @s;
    my $id = "$s[9]/$s[7]/$s[1]";
    my $data;
    if ($cache && $cache->{$id}) {
      $data = { %{$cache->{$id}} };
    } else {
      $data = Build::query($bin, 'evra' => 1);	# need arch
      next unless $data;
    }
    eval {
      BSVerify::verify_nevraquery($data);
    };
    next if $@;
    delete $data->{'disttag'};
    $data->{'id'} = $id;
    $repobins->{$bin} = $data;
  }
  return $repobins;
}

sub writesolv {
  my ($fn, $fnf, $repo) = @_;
  if (defined($fnf) && $BSUtil::fdatasync_before_rename) {
    local *F;
    open(F, '>', $fn) || die("$fn: $!\n");
    $repo->tofile_fd(fileno(F));
    BSUtil::do_fdatasync(fileno(F));
    close(F) || die("$fn close: $!\n");
  } else {
    $repo->tofile($fn);
  }
  return unless defined $fnf;
  $! = 0;
  rename($fn, $fnf) || die("rename $fn $fnf: $!\n");
}

my $projpacks;		# global project/package data

#  'lastscan'   last time we scanned
#  'meta'       meta cache
#  'solv'       solv data cache (for remote repos)
my %repodatas;		# our repository knowledge
my %repodatas_alien;	# repositories from other archs

my %remotegbininfos;
my %remotepackstatus;
my %remotepackstatus_cleanup;

# add :full repo to pool
sub addrepo {
  my ($ctx, $pool, $prp) = @_;

  my $now = time();
  if ($repodatas{$prp} && $repodatas{$prp}->{'lastscan'} && $repodatas{$prp}->{'lastscan'} > $now - 24*3600) {
    if (exists $repodatas{$prp}->{'solv'}) {
      my $r;
      eval {$r = $pool->repofromstr($prp, $repodatas{$prp}->{'solv'});};
      return $r if $r;
      delete $repodatas{$prp}->{'solv'};
    }
    my $solvfile = $repodatas{$prp}->{'solvfile'} || "$reporoot/$prp/$myarch/:full.solv";
    if (-s $solvfile) {
      my $r;
      if ($repodatas{$prp}->{'solvfile'}) {
	my @s = stat _;
	utime time(), $s[9], $solvfile;	# update atime
      }
      eval {$r = $pool->repofromfile($prp, $solvfile);};
      return $r if $r;
    }
    if ($repodatas{$prp}->{'error'}) {
      return undef;
    }
  }
  delete $repodatas{$prp}->{'solv'};
  delete $repodatas{$prp}->{'lastscan'};
  delete $repodatas{$prp}->{'solvfile'};
  delete $repodatas{$prp}->{'error'};
  my ($projid, $repoid) = split('/', $prp, 2);
  if ($remoteprojs{$projid}) {
    return addrepo_remote($ctx, $pool, $prp, $myarch, $remoteprojs{$projid});
  }
  return addrepo_scan($pool, $prp);
}

# add :full repo to pool, make sure repo is up-to-data by
# scanning the directory
sub addrepo_scan {
  my ($pool, $prp) = @_;

  print "    scanning repo $prp...\n";
  my $dir = "$reporoot/$prp/$myarch/:full";
  my $cache;
  my $dirty;
  if (-s "$dir.solv") {
    eval {$cache = $pool->repofromfile($prp, "$dir.solv");};
    warn($@) if $@;
    if ($cache && $cache->isexternal()) {
      $repodatas{$prp}->{'lastscan'} = time();
      return $cache;
    }
  } elsif ($BSConfig::enable_download_on_demand) {
    my ($projid) = split('/', $prp, 2);
    my @doddata = grep {$_->{'arch'} && $_->{'arch'} eq $myarch} @{$projpacks->{$projid}->{'download'} || []};
    if (@doddata) {
      my $doddata = $doddata[0];
      eval {$cache = Meta::parse("$dir/$doddata->{'metafile'}", $doddata->{'mtype'}, { 'arch' => [ $myarch ] })};
      if ($@) {
	print "    download on demand: cannot read metadata: $@\n";
	return undef;
      } elsif (!$cache) {
        print "    download on demand: cannot read metadata: unknown mtype attribute\n";
        return undef;
      }
      for (values %$cache) {
	$_->{'id'} = 'dod';
	$_->{'hdrmd5'} = 'd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0';
      }
      $cache->{'/url'} = $doddata->{'baseurl'};
      $cache = $pool->repofromdata($prp, $cache);
      $dirty = 1;
    }
  }
  my @bins;
  local *D;
  if (opendir(D, $dir)) {
    @bins = grep {/\.(?:$binsufsre)$/} readdir(D);
    closedir D;
    if (!@bins && -s "$dir.subdirs") {
      for my $subdir (split(' ', readstr("$dir.subdirs"))) {
        push @bins, map {"$subdir/$_"} grep {/\.(?:$binsufsre)$/} ls("$dir/$subdir");
      }
    }
  } else {
    if (!$cache) {
      # return in-core empty repo
      my $r = $pool->repofrombins($prp, $dir);
      $repodatas{$prp}->{'solv'} = $r->tostr();
      $repodatas{$prp}->{'lastscan'} = time();
      return $r;
    }
  }
  for (splice @bins) {
    my @s = stat("$dir/$_");
    next unless @s;
    push @bins, $_, "$s[9]/$s[7]/$s[1]";
  }
  if ($cache) {
    my $updated = $cache->updatefrombins($dir, @bins);
    print "    (dirty: $updated)\n" if $updated;
    $dirty = 1 if $updated;
  } else {
    $cache = $pool->repofrombins($prp, $dir, @bins);
    $dirty = 1;
  }
  if ($dirty && $cache && !$repodatas{$prp}->{'dontwrite'}) {
    writesolv("$dir.solv.new", "$dir.solv", $cache);
  }
  $repodatas{$prp}->{'lastscan'} = time() unless $dirty && $repodatas{$prp}->{'dontwrite'};
  return $cache;
}


sub enabled {
  my ($repoid, $disen, $default) = @_;
  return BSUtil::enabled($repoid, $disen, $default, $myarch);
}



# this is basically getconfig from the source server
# we do not need any macros, just the config
sub getconfig {
  my ($arch, $path) = @_;
  my $config = '';
  if (@$path) {
    my ($p, $r) = split('/', $path->[0], 2);
    $config .= "%define _project $p\n";
  }
  for my $prp (reverse @$path) {
    my ($p, $r) = split('/', $prp, 2);
    my $c;
    if ($remoteprojs{$p}) {
      $c = fetchremoteconfig($p); 
      return undef unless defined $c;
    } elsif ($projpacks->{$p}) {
      $c = $projpacks->{$p}->{'config'};
    }
    next unless defined $c;
    $config .= "\n### from $p\n";
    $config .= "%define _repository $r\n";
    # get rid of the Macros sections
    my $s1 = '^\s*macros:\s*$.*?^\s*:macros\s*$';
    my $s2 = '^\s*macros:\s*$.*\Z';
    $c =~ s/$s1//gmsi;
    $c =~ s/$s2//gmsi;
    $config .= $c;
  }
  # it's an error if we have no config at all
  return undef unless $config ne '';
  # now we got the combined config, parse it
  my @c = split("\n", $config);
  my $c = Build::read_config($arch, \@c);
  $c->{'repotype'} = [ 'rpm-md' ] unless @{$c->{'repotype'}};
  # compatibility with older build versions
  if (!$c->{'binarytype'}) {
    $c->{'binarytype'} = 'rpm' if $c->{'type'} eq 'spec' || $c->{'type'} eq 'kiwi';
    $c->{'binarytype'} = 'deb' if $c->{'type'} eq 'dsc';
    $c->{'binarytype'} = 'arch' if $c->{'type'} eq 'arch';
    $c->{'binarytype'} ||= 'UNKNWON';
  }
  return $c;
}


#######################################################################
#######################################################################
##
## Job management functions
##

# scheduled jobs (does not need to be exact)
my %ourjobs = map {$_ => 1} grep {!/(?::dir|:status)$/} ls($myjobsdir);

#
# killjob - kill a single build job
#
# input: $job - job identificator
#
sub killjob {
  my ($job) = @_;

  local *F;
  if (! -e "$myjobsdir/$job:status") {
    # create locked status
    my $js = {'code' => 'deleting'};
    if (BSUtil::lockcreatexml(\*F, "$myjobsdir/.sched.$$", "$myjobsdir/$job:status", $js, $BSXML::jobstatus)) {
      print "        (job was not building)\n";
      if (-d "$myjobsdir/$job:dir") {
	unlink("$myjobsdir/$job:dir/$_") for ls("$myjobsdir/$job:dir");
	rmdir("$myjobsdir/$job:dir");
      }
      unlink("$myjobsdir/$job");
      unlink("$myjobsdir/$job:status");
      close F;
      delete $ourjobs{$job};
      return;
    }
  }
  my $js = BSUtil::lockopenxml(\*F, '<', "$myjobsdir/$job:status", $BSXML::jobstatus, 1);
  if (!$js) {
    # can't happen actually
    print "        (job was not building)\n";
    unlink("$myjobsdir/$job");
    delete $ourjobs{$job};
    return;
  }
  if ($js->{'code'} eq 'building') {
    print "        (job was building on $js->{'workerid'})\n";
    my $req = {
      'uri' => "$js->{'uri'}/discard",
      'timeout' => 60,
    };
    eval {
      BSRPC::rpc($req, undef, "jobid=$js->{'jobid'}");
    };
    warn("kill $job: $@") if $@;
  }
  if (-d "$myjobsdir/$job:dir") {
    unlink("$myjobsdir/$job:dir/$_") for ls("$myjobsdir/$job:dir");
    rmdir("$myjobsdir/$job:dir");
  }
  unlink("$myjobsdir/$job");
  unlink("$myjobsdir/$job:status");
  close(F);
  delete $ourjobs{$job};
}

#
# killjob - kill a single build job if it is scheduled but not building
#
# input: $job - job identificator
#
sub killscheduled {
  my ($job) = @_;

  return if -e "$myjobsdir/$job:status";
  local *F;
  my $js = {'code' => 'deleting'};
  if (BSUtil::lockcreatexml(\*F, "$myjobsdir/.sched.$$", "$myjobsdir/$job:status", $js, $BSXML::jobstatus)) {
    if (-d "$myjobsdir/$job:dir") {
      unlink("$myjobsdir/$job:dir/$_") for ls("$myjobsdir/$job:dir");
      rmdir("$myjobsdir/$job:dir");
    }
    unlink("$myjobsdir/$job");
    unlink("$myjobsdir/$job:status");
    close F;
    delete $ourjobs{$job};
  }
}

#
# jobname - create first part job job identifcation
#
# input:  $prp    - prp the job belongs to
#         $packid - package we are building
# output: first part of job identification
#
# append srcmd5 for full identification
#
sub jobname {
  my ($prp, $packid) = @_;
  my $job = "$prp/$packid";
  $job =~ s/\//::/g;
  $job = ':'.Digest::MD5::md5_hex($prp).'::'.(length($packid) > 160 ? ':'.Digest::MD5::md5_hex($packid) : $packid) if length($job) > 200;
  return $job;
}

#
# killbuilding - kill build jobs 
#
# - used if a project/package got deleted to kill all running
#   jobs
# 
# input: $prp    - prp we are working on
#        $packid - just kill the builds of the package
#           
sub killbuilding {
  my ($prp, $packid) = @_;
  my @jobs;
  if (defined $packid) {
    my $f = jobname($prp, $packid);
    @jobs = grep {$_ eq $f || /^\Q$f\E-[0-9a-f]{32}$/} ls($myjobsdir);
  } else {
    my $f = jobname($prp, '');
    @jobs = grep {/^\Q$f\E/} ls($myjobsdir);
    @jobs = grep {!/(?::dir|:status)$/} @jobs;
  }
  for my $job (@jobs) {
    print "        killing obsolete job $job\n";
    killjob($job);
  }
}

sub add_crossmarker {
  my ($bconf, $job) = @_;
  my $hostarch = $bconf->{'hostarch'};
  return if $hostarch eq $myarch;
  return unless $BSCando::knownarch{$hostarch};
  my $marker = "$jobsdir/$hostarch/$job:$myarch:cross";
  return if -e $marker;
  mkdir_p("$jobsdir/$hostarch");
  BSUtil::touch($marker);
}

#
# set_building  - create a new build job
#
# input:  $projid        - project this package belongs to
#         $repoid        - repository we are building for
#         $packid        - package to be built
#         $pdata         - package data
#         $info          - file and dependency information
#         $bconf         - project configuration
#         $subpacks      - all subpackages of this package we know of
#         $edeps         - expanded build dependencies
#         $prpsearchpath - build repository search path
#         $reason        - what triggered the build
#         $relsyncmax    - bcnt sync data
#         $needed        - packages blocked by this job
#
# output: $job           - the job identifier
#         $error         - in case we could not start the job
#
# check if this job is already building, if yes, do nothing.
# otherwise calculate and expand build dependencies, kill all
# other jobs of the same prp/package, write status and job info.
# not that hard, was it?
#
sub set_building {
  my ($projid, $repoid, $packid, $pdata, $info, $bconf, $subpacks, $edeps, $prpsearchpath, $reason, $relsyncmax, $needed) = @_;

  my $prp = "$projid/$repoid";
  my $srcmd5 = $pdata->{'srcmd5'};
  my $job = jobname($prp, $packid);
  if (-s "$myjobsdir/$job-$srcmd5") {
    add_crossmarker($bconf, "$job-$srcmd5") if $bconf->{'hostarch'};
    return "$job-$srcmd5";
  }
  return $job if -s "$myjobsdir/$job";	# obsolete
  my @otherjobs = grep {/^\Q$job\E-[0-9a-f]{32}$/} ls($myjobsdir);
  $job = "$job-$srcmd5";

  # a new one. expand usedforbuild. write info file.
  my $prptype = $bconf->{'type'};
  $info->{'file'} =~ /\.(spec|dsc|kiwi)$/;
  my $packtype = $1 || 'spec';
  $packtype = 'arch' if $info->{'file'} eq 'PKGBUILD';
  $packtype = 'preinstallimage' if $info->{'file'} eq '_preinstallimage';

  my $searchpath = [];
  my $syspath;
  if ($packtype eq 'kiwi') {
    if ($prpsearchpath) {
      $syspath = [];
      for (@$prpsearchpath) {
	my @pr = split('/', $_, 2);
	if ($remoteprojs{$pr[0]}) {
	  push @$syspath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
	} else {
	  push @$syspath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
	}
      }
    }
    $prpsearchpath = [ map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []} ];
  }
  for (@$prpsearchpath) {
    my @pr = split('/', $_, 2);
    if ($remoteprojs{$pr[0]}) {
      push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
    } else {
      push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
    }
  }

  # calculate packages needed for building
  my @bdeps = grep {!/^\// || $bconf->{'fileprovides'}->{$_}} @{$info->{'prereq'} || []};
  unshift @bdeps, @{$info->{'dep'} || []};

  if ($packtype eq 'kiwi') {
    # packages used for build environment setup
    @bdeps = @{$bconf->{'substitute'}->{'kiwi-setup:image'} || []};
    @bdeps = ('kiwi', 'createrepo', 'tar') unless @bdeps;	# default
    push @bdeps, grep {/^kiwi-.*:/} @{$info->{'dep'} || []};
  }

  my $eok;
  ($eok, @bdeps) = Build::get_build($bconf, $subpacks, @bdeps);
  if (!$eok) {
    print "        unresolvables:\n";
    print "          $_\n" for @bdeps;
    return (undef, "unresolvable: ".join(', ', @bdeps));
  }

  # find the last build count we used for this version/release
  mkdir_p("$reporoot/$prp/$myarch/$packid");
  my $h;
  if (-e "$reporoot/$prp/$myarch/$packid/history") {
    $h = BSFileDB::fdb_getmatch("$reporoot/$prp/$myarch/$packid/history", $historylay, 'versrel', $pdata->{'versrel'}, 1);
  }
  $h = {'bcnt' => 0} unless $h;

  # max with sync data
  my $tag = $pdata->{'bcntsynctag'} || $packid;
  if ($relsyncmax && $relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
    if ($h->{'bcnt'} + 1 < $relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
      $h->{'bcnt'} = $relsyncmax->{"$tag/$pdata->{'versrel'}"} - 1;
    }
  }

  # kill those ancient other jobs
  for my $otherjob (@otherjobs) {
    print "        killing old job $otherjob\n";
    killjob($otherjob);
  }

  # jay! ready for building, write status and job info
  my $now = time();
  writexml("$reporoot/$prp/$myarch/$packid/.status", "$reporoot/$prp/$myarch/$packid/status", { 'status' => 'scheduled', 'readytime' => $now, 'job' => $job}, $BSXML::buildstatus);
  # And store reason and time
  $reason->{'time'} = $now;
  writexml("$reporoot/$prp/$myarch/$packid/.reason", "$reporoot/$prp/$myarch/$packid/reason", $reason, $BSXML::buildreason);

  my @pdeps = Build::get_preinstalls($bconf);
  my @vmdeps = Build::get_vminstalls($bconf);
  my @cbpdeps = Build::get_cbpreinstalls($bconf); # crossbuild preinstall
  my @cbdeps = Build::get_cbinstalls($bconf);  # crossbuild install
  my %runscripts = map {$_ => 1} Build::get_runscripts($bconf);
  my %bdeps = map {$_ => 1} @bdeps;
  my %pdeps = map {$_ => 1} @pdeps;
  my %vmdeps = map {$_ => 1} @vmdeps;
  my %cbpdeps = map {$_ => 1} @cbpdeps;
  my %cbdeps = map {$_ => 1} @cbdeps;
  my %edeps = map {$_ => 1} @$edeps;
  @bdeps = unify(@pdeps, @vmdeps, @$edeps, @bdeps, @cbpdeps, @cbdeps);
  for (@bdeps) {
    $_ = {'name' => $_};
    $_->{'preinstall'} = 1 if $pdeps{$_->{'name'}};
    $_->{'vminstall'} = 1 if $vmdeps{$_->{'name'}};
    $_->{'cbpreinstall'} = 1 if $cbpdeps{$_->{'name'}};
    $_->{'cbinstall'} = 1 if $cbdeps{$_->{'name'}};
    $_->{'runscripts'} = 1 if $runscripts{$_->{'name'}};
    $_->{'notmeta'} = 1 unless $edeps{$_->{'name'}};
    $_->{'noinstall'} = 1 if $packtype eq 'kiwi' && $edeps{$_->{'name'}} && !($bdeps{$_->{'name'}} || $vmdeps{$_->{'name'}} || $pdeps{$_->{'name'}});
  }
  if ($info->{'extrasource'}) {
    push @bdeps, map {{
      'name' => $_->{'file'}, 'version' => '', 'repoarch' => 'src',
      'project' => $_->{'project'}, 'package' => $_->{'package'}, 'srcmd5' => $_->{'srcmd5'},
    }} @{$info->{'extrasource'}};
  }

  my $vmd5 = $pdata->{'verifymd5'} || $pdata->{'srcmd5'};
  my $binfo = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => $packid,
    'srcserver' => $BSConfig::srcserver,
    'reposerver' => $BSConfig::reposerver,
    'job' => $job,
    'arch' => $myarch,
    'reason' => $reason->{'explain'},
    'readytime' => $now,
    'srcmd5' => $pdata->{'srcmd5'},
    'verifymd5' => $vmd5,
    'rev' => $pdata->{'rev'},
    'file' => $info->{'file'},
    'versrel' => $pdata->{'versrel'},
    'bcnt' => $h->{'bcnt'} + 1,
    'subpack' => ($subpacks || []),
    'bdep' => \@bdeps,
    'path' => $searchpath,
    'needed' => $needed,
  };
  $binfo->{'syspath'} = $syspath if $syspath;
  $binfo->{'hostarch'} = $bconf->{'hostarch'} if $bconf->{'hostarch'};
  $binfo->{'constraintsmd5'} = $pdata->{'constraintsmd5'} if $pdata->{'constraintsmd5'};
  $binfo->{'prjconfconstraint'} = $bconf->{'constraint'} if @{$bconf->{'constraint'} || []};
  if ($pdata->{'revtime'}) {
    $binfo->{'revtime'} = $pdata->{'revtime'};
    # use max of revtime for interproject links
    for (@{$pdata->{'linked'} || []}) {
      last if $_->{'project'} ne $projid || !$projpacks->{$projid}->{'package'};
      my $lpdata = $projpacks->{$projid}->{'package'}->{$_->{'package'}} || {};
      $binfo->{'revtime'} = $lpdata->{'revtime'} if ($lpdata->{'revtime'} || 0) > $binfo->{'revtime'};
    }
  }
  $binfo->{'imagetype'} = $info->{'imagetype'} if $info->{'imagetype'};
  my $release = $pdata->{'versrel'};
  $release = '0' unless defined $release;
  $release =~ s/.*-//;
  my $bcnt = $h->{'bcnt'} + 1;
  if (defined($bconf->{'release'})) {
    $binfo->{'release'} = $bconf->{'release'};
    $binfo->{'release'} =~ s/\<CI_CNT\>/$release/g;
    $binfo->{'release'} =~ s/\<B_CNT\>/$bcnt/g;
  }
  my $debuginfo = $bconf->{'debuginfo'};
  $debuginfo = enabled($repoid, $projpacks->{$projid}->{'debuginfo'}, $debuginfo);
  $debuginfo = enabled($repoid, $pdata->{'debuginfo'}, $debuginfo);
  $binfo->{'debuginfo'} = 1 if $debuginfo;

  writexml("$myjobsdir/.$job", "$myjobsdir/$job", $binfo, $BSXML::buildinfo);
  add_crossmarker($bconf, $job) if $bconf->{'hostarch'};
  # all done. the dispatcher will now pick up the job and send it
  # to a worker.
  $ourjobs{$job} = 1;
  return $job;
}


#######################################################################
#######################################################################
##
## Repository management functions
##

sub checkaccess {
  my ($type, $projid, $packid, $repoid) = @_;
  my $access = 1;
  if ($projpacks->{$projid}) {
    my $pdata;
    $pdata = ($projpacks->{$projid}->{'package'} || {})->{$packid} if defined $packid;
    $access = enabled($repoid, $projpacks->{$projid}->{$type}, $access);
    $access = enabled($repoid, $pdata->{$type}, $access) if $pdata;
  } else {
    # remote project access checks are handled by the remote server
    $access = 0 unless $remoteprojs{$projid};
  }
  return $access;
}

# check if every user from oprojid may access projid
sub checkroles {
  my ($type, $projid, $packid, $oprojid, $opackid) = @_;
  my $proj = $projpacks->{$projid};
  my $oproj = $projpacks->{$oprojid};
  return 0 unless $proj && $oproj;
  if ($projid eq $oprojid) {
    return 1 if !defined $opackid;
    return 1 if ($packid || '') eq ($opackid || '');
  }
  my @roles;
  if (defined($packid)) {
    my $pdata = ($proj->{'package'} || {})->{$packid} || {};
    push @roles, @{$pdata->{'person'} || []}, @{$pdata->{'group'} || []};
  }
  push @roles, @{$proj->{'person'} || []}, @{$proj->{'group'} || []};
  while ($projid =~ /^(.+):/) {
    $projid = $1;
    $proj = $projpacks->{$projid} || {};
    push @roles, @{$proj->{'person'} || []}, @{$proj->{'group'} || []};
  }
  my @oroles;
  if (defined($opackid)) {
    my $pdata = ($oproj->{'package'} || {})->{$opackid} || {};
    push @oroles, @{$pdata->{'person'} || []}, @{$pdata->{'group'} || []};
  }
  push @oroles, @{$oproj->{'person'} || []}, @{$oproj->{'group'} || []};
  while ($oprojid =~ /^(.+):/) {
    $oprojid = $1;
    $oproj = $projpacks->{$oprojid} || {};
    push @oroles, @{$oproj->{'person'} || []}, @{$oproj->{'group'} || []};
  }
  # make sure every user from oprojid can also access projid
  # XXX: check type and roles
  for my $r (@oroles) {
    next if $r->{'role'} eq 'bugowner';
    my @rx; 
    if (exists $r->{'userid'}) {
      push @rx, grep {exists($_->{'userid'}) && $_->{'userid'} eq $r->{'userid'}} @roles;
    } elsif (exists $r->{'groupid'}) {
      push @rx, grep {exists($_->{'groupid'}) && $_->{'groupid'} eq $r->{'groupid'}} @roles;
    }
    return 0 unless grep {$_->{'role'} eq $r->{'role'} || $_->{'role'} eq 'maintainer'} @rx;
  }
  return 1;
}

# check if we may access repo $aprp from repo $prp
sub checkprpaccess {
  my ($aprp, $prp) = @_;
  return 1 if $aprp eq $prp;
  my ($aprojid, $arepoid) = split('/', $aprp, 2);
  # ok if aprp is not protected
  return 1 if checkaccess('access', $aprojid, undef, $arepoid);
  my ($projid, $repoid) = split('/', $prp, 2);
  # not ok if prp is unprotected
  return 0 if checkaccess('access', $projid, undef, $repoid);
  # both prp and aprp are proteced.
  return 1 if $aprojid eq $projid;	# they hopefully know what they are doing
  # check if publishing flags match unless aprojid is remote
  if (!$remoteprojs{$aprojid} && !checkaccess('publish', $aprojid, undef, $arepoid)) {
    return 0 if checkaccess('publish', $projid, undef, $repoid);
  }
  # check if the roles match
  return checkroles('access', $aprojid, undef, $projid, undef);
}

#
# sendpublishevent - send a publish event to the publisher
#
# input: $prp - prp to be published
#
sub sendpublishevent {
  my ($prp) = @_;

  my ($projid, $repoid) = split('/', $prp, 2);
  my $ev = {
    'type' => 'publish',
    'project' => $projid,
    'repository' => $repoid,
  };
  sendevent($ev, 'publish', "${projid}::$repoid");
}

sub sendrepochangeevent {
  my ($prp, $type) = @_;

  my ($projid, $repoid) = split('/', $prp, 2);
  my $ev = {
    'type' => ($type || 'repository'),
    'project' => $projid,
    'repository' => $repoid,
    'arch' => $myarch,
  };
  sendevent($ev, 'repository', "${projid}::${repoid}::${myarch}");
}

sub set_repo_state {
  my ($prp, $state, $details) = @_;

  unlink("$reporoot/$prp/$myarch/:schedulerstate.dirty") if $state eq "scheduling";
  
  $state .= " $details" if $details;
  writestr("$reporoot/$prp/$myarch/.:schedulerstate", "$reporoot/$prp/$myarch/:schedulerstate", $state) if -d "$reporoot/$prp/$myarch";
}


# create a delta job
#
# output: $job           - the job identifier
#         $error         - in case we could not start the job
sub createdeltajob {
  my ($projid, $repoid, $packid, $bconf, $prpsearchpath, $suffix, $needdelta) = @_;

  my $job = jobname("$projid/$repoid", $packid);
  $job .= "-$suffix" if defined $suffix;
  if (-e "$myjobsdir/$job") {
    return undef, 'building'; # delta creation already in progress
  }
  my $srcmd5 = '';
  $srcmd5 .= $_->[2] for @$needdelta;
  $srcmd5 = Digest::MD5::md5_hex($srcmd5);
  my $jobdatadir = "$myjobsdir/$job:dir";
  mkdir_p($jobdatadir);
  BSUtil::cleandir($jobdatadir);
  return (undef, "could not create jobdir") unless -d $jobdatadir;
  for my $delta (@$needdelta) {
    #print Dumper($delta);
    my $deltaid = $delta->[2];
    link($delta->[0], "$jobdatadir/$deltaid.old") || return (undef, "link error: $!");
    link($delta->[1], "$jobdatadir/$deltaid.new") || return (undef, "link error: $!");
    my $qold = Build::Rpm::query("$jobdatadir/$deltaid.old", 'evra' => 1);
    my $qnew = Build::Rpm::query("$jobdatadir/$deltaid.new", 'evra' => 1);
    return (undef, "bad rpms") unless $qold && $qnew;
    return (undef, "name/arch mismatch") if $qold->{'name'} ne $qnew->{'name'} || $qold->{'arch'} ne $qnew->{'arch'};
    $qold->{'epoch'} = '' unless defined $qold->{'epoch'};
    $qnew->{'epoch'} = '' unless defined $qnew->{'epoch'};
    my $info = '';
    $info .= ucfirst($_).": $qnew->{$_}\n" for qw{name epoch version release arch};
    $info .= "Old".ucfirst($_).": $qold->{$_}\n" for qw{name epoch version release arch};
    writestr("$jobdatadir/$deltaid.info", undef, $info);
  }
  # create job
  my $prptype = $bconf->{'type'};
  my ($eok, @bdeps) = Build::get_build($bconf, [], "deltarpm");
  if (!$eok) {
    print "        unresolvables:\n";
    print "          $_\n" for @bdeps;
    return (undef, "unresolvable: ".join(', ', @bdeps));
  }
  my $now = time();
  my @pdeps = Build::get_preinstalls($bconf);
  my @vmdeps = Build::get_vminstalls($bconf);
  my %runscripts = map {$_ => 1} Build::get_runscripts($bconf);
  my %bdeps = map {$_ => 1} @bdeps;
  my %pdeps = map {$_ => 1} @pdeps;
  my %vmdeps = map {$_ => 1} @vmdeps;
  @bdeps = unify(@pdeps, @vmdeps, @bdeps);
  for (@bdeps) {
    $_ = {'name' => $_}; 
    $_->{'preinstall'} = 1 if $pdeps{$_->{'name'}};
    $_->{'vminstall'} = 1 if $vmdeps{$_->{'name'}};
    $_->{'runscripts'} = 1 if $runscripts{$_->{'name'}};
    $_->{'notmeta'} = 1;
  }
  my $searchpath = [];
  for (@$prpsearchpath) {
    my @pr = split('/', $_, 2);
    if ($remoteprojs{$pr[0]}) {
      push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
    } else {
      push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
    }
  }

  my $binfo = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => $packid,
    'file' => '_delta',
    'srcmd5' => $srcmd5,
    'reason' => 'source change',
    'srcserver' => $BSConfig::srcserver,
    'reposerver' => $BSConfig::reposerver,
    'job' => $job,
    'arch' => $myarch,
    'readytime' => $now,
    'bdep' => \@bdeps,
    'path' => $searchpath,
    'needed' => 0,
  };
  $binfo->{'hostarch'} = $bconf->{'hostarch'} if $bconf->{'hostarch'};

  writexml("$myjobsdir/.$job", "$myjobsdir/$job", $binfo, $BSXML::buildinfo);
  print "    created deltajob...\n";
  return $job;
}

# make sure that we have all of the deltas we need
# create a deltajob if some are missing
# note that we must have the repo lock so that $extrep does not change!
sub makedeltas {
  my ($prp, $packs, $pubenabled, $bconf, $prpsearchpath) = @_;

  my ($projid, $repoid) = split('/', $prp, 2);
  my $rdir = "$reporoot/$prp/$myarch/:repo";
  my $ddir = "$reporoot/$prp/$myarch/_deltas";

  my %oldbins;

  my %havedelta;
  my @needdelta;
  my %deltaids;

  my $partial_job;
  my $unfinished;
  my $jobsize = 0;

  my $suffix;
  my $running_jobs = 0;
  my $maxjobs = 100;

  my %running_ids;
  if ($maxjobs > 1) {
    $suffix = 0;
    my $jobprefix = jobname($prp, '_deltas');
    for my $job (grep {$_ eq $jobprefix || /^\Q$jobprefix-\E\d+$/} ls($myjobsdir)) {
      $running_jobs++;
      if ($job =~ /(\d+)$/) {
        $suffix = $1 if $1 > $suffix;
      }
      $running_ids{$_} = 1 for grep {s/\.info$//} ls("$myjobsdir/$job:dir");
    }
  }

  my %ddir = map {$_ => 1} ls($ddir);

  # first collect all binary names
  my %binarchnames;
  for my $packid (@{$packs || []}) {
    next if $pubenabled && !$pubenabled->{$packid};
    my $pdir = "$reporoot/$prp/$myarch/$packid";
    my @all = sort(ls($pdir));
    my $nosourceaccess = grep {$_ eq '.nosourceaccess'} @all;
    @all = grep {/\.rpm$/} @all;
    next unless @all;
    for my $bin (@all) {
      next if $bin =~ /\.(?:no)?src\.rpm$/;	# no source deltas
      if ($nosourceaccess) {
	next if $bin =~ /-debug(:?info|source).*\.rpm$/;
      }
      next unless $bin =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\/\.\-]*)\.rpm$/;
      my $binname = $1;
      my $binarch = $2;
      push @{$binarchnames{"$binarch/$binname"}}, [ $bin, $packid ];
    }
  }

  for my $binarchname (sort keys %binarchnames) {
    # we only want deltas against the highest version
    my @binxs = sort {Build::Rpm::verscmp($b->[0], $a->[0])} @{$binarchnames{$binarchname}};
    my $didbin;
    for my $binx (@binxs) {
      my ($bin, $packid) = @$binx;
      last if $didbin && Build::Rpm::verscmp($didbin, $bin) > 0;
      my $pdir = "$reporoot/$prp/$myarch/$packid";
      my @binstat = stat("$pdir/$bin");
      next unless @binstat;
      $didbin = $bin;
      my ($binarch, $binname) = split('/', $binarchname, 2);

      # find all delta candidates for this package. we currently just
      # use the searchpath, this may be configurable in a later version
      my @aprp = @$prpsearchpath;
      for my $aprp (@aprp) {
	# look in the *published* repos. We allow a special
	# extradeltarepos override in the config.
	if (!$oldbins{"$aprp/$binarch"}) {
	  my $aextrep = $aprp;
	  $aextrep =~ s/:/:\//g;
	  $aextrep = "$extrepodir/$aextrep";
	  $aextrep = $BSConfig::publishredirect->{$aprp} if $BSConfig::publishredirect && defined($BSConfig::publishredirect->{$aprp});
	  $aextrep = $BSConfig::extradeltarepos->{$aprp} if $BSConfig::extradeltarepos && defined($BSConfig::extradeltarepos->{$aprp});
	  $oldbins{$aprp} = $aextrep;
	  $oldbins{"$aprp/$binarch"} = {};
	  for my $obin (sort(ls("$aextrep/$binarch"))) {
	    next unless $obin =~ /^(.+)-[^-]+-[^-]+\.(?:[a-zA-Z][^\/\.\-]*)\.rpm$/;
	    push @{$oldbins{"$aprp/$binarch"}->{$1}}, $obin;
	  }
	}
	my @cand = grep {$_ ne $bin} @{$oldbins{"$aprp/$binarch"}->{$binname}};
	next unless @cand;

	# sort and delete everything newer than bin
	# FIXME: what about the epoch? use file mtime instead?
	push @cand, $bin;
	@cand = sort { Build::Rpm::verscmp($b, $a) || ($a eq $bin ? 1 : $b eq $bin ? -1 : 0) } @cand;
	shift @cand while $cand[0] ne $bin;
	shift @cand;
	next unless @cand;

	# make this configurable
	@cand = splice(@cand, 0, 1);
	for my $obin (@cand) {
	  my $aextrep = $oldbins{$aprp};
	  my @s = stat("$aextrep/$binarch/$obin");
	  next unless @s;
	  my $deltaid = Digest::MD5::md5_hex("$packid/$bin/$aprp/$obin/$s[9]/$s[7]/$s[1]");
	  $deltaids{$deltaid} = 1;
          if ($ddir{$deltaid}) {
	    next unless $ddir{"$deltaid.dseq"};		# delta was too big
	    # make sure we don't already have this one
	    if (!grep {$_->[1] eq $obin} @{$havedelta{"$packid/$bin"} || []}) {
	      push @{$havedelta{"$packid/$bin"}}, [ $deltaid, $obin ];
	    }
            next;
          }
	  $unfinished = 1;
	  next if $running_ids{$deltaid};
	  push @needdelta, [ "$aextrep/$binarch/$obin", "$pdir/$bin", $deltaid ];
	  $jobsize += $s[7] + $binstat[7];
	  if ($jobsize > 500000000) {
	    # flush the job
	    if ($running_jobs >= $maxjobs) {
	      print "    too many delta jobs running\n";
	      $partial_job = 1;
	      last;
	    }
	    $suffix++ if defined $suffix;
	    my ($job, $joberror) = createdeltajob($projid, $repoid, '_deltas', $bconf, $prpsearchpath, $suffix, \@needdelta);
	    return (undef, $joberror) if $joberror;
	    $running_jobs++ if $job;
	    @needdelta = ();
	    $jobsize = 0;
	  }
	}
	last if $partial_job;
      }
      last if $partial_job;
    }
    last if $partial_job;
  }

  if (@needdelta && $running_jobs < $maxjobs) {
    $suffix++ if defined $suffix;
    my ($job, $joberror) = createdeltajob($projid, $repoid, '_deltas', $bconf, $prpsearchpath, $suffix, \@needdelta);
    return (undef, $joberror) if $joberror;
  }

  if ($unfinished) {
    print "    waiting for deltajobs to finish\n";
    return (undef, 'building');
  }

  # ddir maintenance
  my @ddir = sort(ls($ddir));
  for my $deltaid (grep {!$deltaids{$_} && !/\.dseq$/} @ddir) {
    next if $deltaid eq 'logfile';
    unlink("$ddir/$deltaid");		# no longer need this one
    unlink("$ddir/$deltaid.dseq");	# no longer need this one
  }
  return \%havedelta;
}

sub mkdeltaname {
  my ($old, $new) = @_;
  # name-version-release.arch.rpm
  my $newtail = '';
  if ($old =~ /^(.*)(\.[^\.]+\.rpm$)/) {
    $old = $1;
  }
  if ($new =~ /^(.*)(\.[^\.]+\.rpm$)/) {
    $new = $1;
    $newtail = $2;
  }
  my @old = split('-', $old);
  my @new = split('-', $new);
  my @out;
  while (@old || @new) {
    $old = shift @old;
    $new = shift @new;
    $old = '' unless defined $old;
    $new = '' unless defined $new;
    if ($old eq $new) {
      push @out, $old;
    } else {
      push @out, "${old}_${new}";
    }
  }
  my $ret = join('-', @out).$newtail;
  $ret =~ s/\.rpm$//;
  return "$ret.drpm";
}

#
# prpfinished  - publish a prp
#
# updates :repo and sends an event to the publisher
#
# input:  $prp        - the finished prp
#         $packs      - packages in project
#
# prpfinished  - publish a prp
#
# updates :repo and sends an event to the publisher
#
# input:  $prp        - the finished prp
#         $packs      - packages in project
#                       undef -> arch no longer builds this repository
#         $pubenabled - only publish those packages
#                       undef -> publish all packages
#         $bconf      - the config for this prp
#

sub compile_publishfilter {
  my ($filter) = @_;
  return undef unless $filter;
  my @res;
  for (@$filter) {
    eval {
      push @res, qr/$_/;
    };
  }
  return \@res;
}

#my $default_publishfilter = [
#  '-debuginfo-.*\.rpm$',
#  '-debugsource-.*\.rpm$',
#];

my $default_publishfilter;

sub publishdelta {
  my ($prp, $delta, $bin, $rdir, $rbin, $origin, $packid) = @_;

  my @s = stat("$reporoot/$prp/$myarch/_deltas/$delta->[0]");
  return 0 unless @s && $s[7];		# zero size means skip it
  return 0 unless -s "$reporoot/$prp/$myarch/_deltas/$delta->[0].dseq";	# need dseq file
  my $deltaname = mkdeltaname($delta->[1], $bin);
  my $deltaseqname = $deltaname;
  $deltaseqname =~ s/\.drpm$//;
  $deltaseqname .= '.dseq';
  my @sr = stat("$rdir/${rbin}::$deltaname");
  my $changed;
  if (!@sr || "$s[9]/$s[7]/$s[1]" ne "$sr[9]/$sr[7]/$sr[1]") {
    print @sr ? "      ! :repo/${rbin}::$deltaname\n" : "      + :repo/${rbin}::$deltaname\n";
    unlink("$rdir/${rbin}::$deltaname");
    unlink("$rdir/${rbin}::$deltaseqname");
    link("$reporoot/$prp/$myarch/_deltas/$delta->[0]", "$rdir/${rbin}::$deltaname") || die("link $reporoot/$prp/$myarch/_deltas/$delta->[0] $rdir/${rbin}::$deltaname: $!");
    link("$reporoot/$prp/$myarch/_deltas/$delta->[0].dseq", "$rdir/${rbin}::$deltaseqname") || die("link $reporoot/$prp/$myarch/_deltas/$delta->[0].dseq $rdir/${rbin}::$deltaseqname: $!");
    $changed = 1;
  }
  $origin->{"${rbin}::$deltaname"} = $packid;
  $origin->{"${rbin}::$deltaseqname"} = $packid;
  return $changed;
}

sub prpfinished {
  my ($prp, $packs, $pubenabled, $bconf, $prpsearchpath) = @_;

  print "    prp $prp is finished...\n";

  my ($projid, $repoid) = split('/', $prp, 2);
  local *F;
  open(F, '>', "$reporoot/$prp/.finishedlock") || die("$reporoot/$prp/.finishedlock: $!\n");
  if (!flock(F, LOCK_EX | LOCK_NB)) {
    print "    waiting for lock...\n";
    flock(F, LOCK_EX) || die("flock: $!\n");
    print "    got the lock...\n";
  }
  if (!$packs) {
    # delete all in :repo
    my $r = "$reporoot/$prp/$myarch/:repo";
    unlink("${r}info");
    if (-d $r) {
      BSUtil::cleandir($r);
      rmdir($r) || die("rmdir $r: $!\n");
    } else {
      print "    nothing to delete...\n";
      close(F);
      return '';
    }
    # release lock
    close(F);
    sendpublishevent($prp);
    return '';
  }

  # make all the deltas we need
  my $needdeltas;
  $needdeltas = 1 if grep {"$_:" =~ /:(?:deltainfo|prestodelta):/} @{$bconf->{'repotype'} || []};
  my ($deltas, $err) = makedeltas($prp, $needdeltas ? $packs : undef, $pubenabled, $bconf, $prpsearchpath);
  if (!$deltas) {
      close(F);
      $err ||= 'internal error';
      $err = "delta generation: $err";
      return $err;
  }

  my $rdir = "$reporoot/$prp/$myarch/:repo";

  my $rinfo;
  my $rinfo_packid2bins;

  # link all packages into :repo
  my %origin;
  my $changed;
  my $filter;
  $filter = $bconf->{'publishfilter'} if $bconf;
  undef $filter if $filter && !@$filter;
  $filter ||= $default_publishfilter;
  $filter = compile_publishfilter($filter);

  for my $packid (@$packs) {
    if ($pubenabled && !$pubenabled->{$packid}) {
      # publishing of this package is disabled, copy binary list from old info
      if (!$rinfo) {
	$rinfo = {};
	$rinfo = BSUtil::retrieve("${rdir}info") if -s "${rdir}info";
	$rinfo->{'binaryorigins'} ||= {};
	# create package->binaries helper hash
	$rinfo_packid2bins = {};
	my $rb = $rinfo->{'binaryorigins'};
	for (keys %$rb) {
	  push @{$rinfo_packid2bins->{$rb->{$_}}}, $_;
	}
      }
      print "        $packid: publishing disabled\n";
      for my $bin (@{$rinfo_packid2bins->{$packid} || []}) {
        next if exists $origin{$bin};	# first one wins
        $origin{$bin} = $packid;
      }
      next;
    }
    my $pdir = "$reporoot/$prp/$myarch/$packid";
    my @all = sort(ls($pdir));
    my $debian = grep {/\.dsc$/} @all;
    next if grep {$_ eq '.preinstallimage'} @all;
    my $nosourceaccess = grep {$_ eq '.nosourceaccess'} @all;
    @all = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne '.bininfo' && $_ ne 'reason' && $_ ne '.nosourceaccess' && $_ ne '.checksums' && !/^\.waiting_for_/} @all;
    for my $bin (@all) {
      next if $bin eq '.updateinfodata';
      my $rbin = $bin;
      # XXX: should be source name instead?
      $rbin = "${packid}::$bin" if $debian || $bin eq 'updateinfo.xml';
      next if exists $origin{$rbin};	# first one wins
      if ($nosourceaccess) {
        next if $bin =~ /\.(?:no)?src\.rpm$/;
	next if $bin =~ /-debug(:?info|source).*\.rpm$/;
        next if $debian && ($bin !~ /\.deb$/);
      }
      if ($filter) {
	my $bad;
	for (@$filter) {
	  next unless $bin =~ /$_/;
	  $bad = 1;
	  last;
	}
	next if $bad;
      }
      $origin{$rbin} = $packid;
      # link from package dir (pdir) to repo dir (rdir)
      my @sr = lstat("$rdir/$rbin");
      if (@sr) {
	my $risdir = -d _ ? 1 : 0;
        my @s = lstat("$pdir/$bin");
	my $pisdir = -d _ ? 1 : 0;
        next unless @s;
	if ("$s[9]/$s[7]/$s[1]" eq "$sr[9]/$sr[7]/$sr[1]") {
	  # unchanged file, check deltas
	  if ($deltas->{"$packid/$bin"}) {
	    for my $delta (@{$deltas->{"$packid/$bin"}}) {
	      $changed = 1 if publishdelta($prp, $delta, $bin, $rdir, $rbin, \%origin, $packid);
	    }
	  }
	  next;
	}
	if ($risdir && $pisdir) {
	  my $rdirinfo = BSUtil::treeinfo("$rdir/$rbin");
	  my $pdirinfo = BSUtil::treeinfo("$pdir/$bin");
	  next if join(',', @$rdirinfo) eq join(',', @$pdirinfo);
	}
        print "      ! :repo/$rbin ($packid)\n";
	if ($risdir) {
	  BSUtil::cleandir("$rdir/$rbin");
          rmdir("$rdir/$rbin");
	} else {
          unlink("$rdir/$rbin");
	}
      } else {
        print "      + :repo/$rbin ($packid)\n";
        mkdir_p($rdir) unless -d $rdir;
      }
      if (! -l "$pdir/$bin" && -d _) {
	BSUtil::linktree("$pdir/$bin", "$rdir/$rbin");
      } else {
        link("$pdir/$bin", "$rdir/$rbin") || die("link $pdir/$bin $rdir/$rbin: $!\n");
	if ($deltas->{"$packid/$bin"}) {
	  for my $delta (@{$deltas->{"$packid/$bin"}}) {
	    publishdelta($prp, $delta, $bin, $rdir, $rbin, \%origin, $packid);
	  }
	}
      }
      $changed = 1;
    }
  }
  undef $rinfo_packid2bins;	# no longer needed
  for my $rbin (sort(ls($rdir))) {
    next if exists $origin{$rbin};
    if (0) {
      if (!$rinfo) {
	$rinfo = {};
	$rinfo = BSUtil::retrieve("${rdir}info") if -s "${rdir}info";
	$rinfo->{'binaryorigins'} ||= {};
      }
      $origin{$rbin} = $rinfo->{'binaryorigins'}->{$rbin} if $rinfo->{'binaryorigins'}->{$rbin};
      next;
    }
    print "      - :repo/$rbin\n";
    if (! -l "$rdir/$rbin" && -d _) {
      BSUtil::cleandir("$rdir/$rbin");
      rmdir("$rdir/$rbin") || die("rmdir $rdir/$rbin: $!\n");
    } else {
      if (-f "$rdir/$rbin") {
       unlink("$rdir/$rbin") || die("unlink $rdir/$rbin: $!\n");
      }
    }
    $changed = 1;
  }

  # write new rpminfo
  $rinfo = {'binaryorigins' => \%origin};
  BSUtil::store("${rdir}info.new", "${rdir}info", $rinfo);

  # release lock and ping publisher
  close(F);
  sendpublishevent($prp);
  return '';
}

my $exportcnt = 0;

sub createexportjob {
  my ($prp, $arch, $jobrepo, $dst, $oldrepo, $meta, @exports) = @_;

  # create unique id
  my $job = "import-".Digest::MD5::md5_hex("$exportcnt.$$.$myarch.".time());
  $exportcnt++;

  local *F;
  my $jobstatus = {
    'code' => 'finished',
    'result' => 'succeeded',
  };
  mkdir_p("$jobsdir/$arch") unless -d "$jobsdir/$arch";
  if (!BSUtil::lockcreatexml(\*F, "$jobsdir/$arch/.$job", "$jobsdir/$arch/$job:status", $jobstatus, $BSXML::jobstatus)) {
    print "job lock failed!\n";
    return;
  }

  my ($projid, $repoid) = split('/', $prp, 2);
  my $info = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => ':import',
    'arch' => $arch,
    'job' => $job,
  };
  writexml("$jobsdir/$arch/.$job", "$jobsdir/$arch/$job", $info, $BSXML::buildinfo);
  my $dir = "$jobsdir/$arch/$job:dir";
  mkdir_p($dir);
  if ($meta) {
    link($meta, "$meta.dup");
    rename("$meta.dup", "$dir/meta");
    unlink("$meta.dup");
  }
  my %seen;
  while (@exports) {
    my ($rp, $r) = splice(@exports, 0, 2);
    next unless $r->{'source'};
    link("$dst/$rp", "$dir/$rp") || warn("link $dst/$rp $dir/$rp: $!\n");
    $seen{$r->{'id'}} = 1;
  }
  my @replaced;
  for my $rp (sort keys %$oldrepo) {
    my $r = $oldrepo->{$rp};
    next unless $r->{'source'};	# no src rpms in full tree
    next if $seen{$r->{'id'}};
    my $suf;
    $suf = $1 if $rp =~ /\.($binsufsre)$/;
    push @replaced, {'name' => "$r->{'name'}.$suf", 'id' => $r->{'id'}} if $suf;
  }
  if (@replaced) {
    writexml("$dir/replaced.xml", undef, {'name' => 'replaced', 'entry' => \@replaced}, $BSXML::dir);
  }
  close F;
  my $ev = {
    'type' => 'import',
    'job' => $job,
  };
  sendevent($ev, $arch, "import.$job");
}


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 (@$filter) {
    eval {
      push @res, [ qr/$_->[0]/, $_->[1] ];
    };
  }
  return \@res;
}

sub read_bininfo {
  my ($dir, $withid) = @_;
  my $bininfo;
  my @bininfo_s;
  local *BI;
  if (open(BI, '<', "$dir/.bininfo")) {
    @bininfo_s = stat(BI);
    $bininfo = BSUtil::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;
    }
  }
  # old style bininfo or no bininfo, create it
  $bininfo = {};
  @bininfo_s = ();
  for my $file (ls($dir)) {
    $bininfo->{'.nosourceaccess'} = {} if $file eq '.nosourceaccess';
    if ($file !~ /\.(?:$binsufsre)$/) {
      if ($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);
      die("$dir/$file: queury failed\n") unless $data;
      BSVerify::verify_nevraquery($data);
      $data->{'leadsigmd5'} = $leadsigmd5 if $leadsigmd5;
    };
    if ($@) {
      warn($@);
      next;
    }
    $data->{'filename'} = $file;
    $data->{'id'} = $id;
    $bininfo->{$file} = $data;
  }
  eval {
    BSUtil::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;
}

# alien: gbininfo is from another scheduler
sub read_gbininfo {
  my ($dir, $alien) = @_;

  return {} unless -d $dir;
  my $gbininfo = BSUtil::retrieve("$dir/:bininfo", 1);
  my $gbininfo_m;
  if ($gbininfo) {
    return $gbininfo unless -e "$dir/:bininfo.merge";
    $gbininfo_m = BSUtil::retrieve("$dir/:bininfo.merge", 1);
  }
  if ($gbininfo && $gbininfo_m) {
    for (keys %$gbininfo_m) {
      if ($gbininfo_m->{$_}) {
	$gbininfo->{$_} = $gbininfo_m->{$_};
      } else {
	delete $gbininfo->{$_};
      }
    }
  } else {
    return undef if $alien;
    $gbininfo = {};
    my @dir = split('/', $dir);
    print "    rebuilding project repoinfo for $dir[-3]/$dir[-2]...\n";
    for my $packid (grep {!/^[:\.]/} ls($dir)) {
      next if $packid eq '_deltas';
      next unless -d "$dir/$packid";
      my $bininfo = read_bininfo("$dir/$packid", 1);
      if ($bininfo) {
	for (values %$bininfo) {
	  delete $_->{'provides'};
	  delete $_->{'requires'};
	}
	$gbininfo->{$packid} = $bininfo;
      }
    }
  }
  return $gbininfo if $alien;
  eval {
    BSUtil::store("$dir/.:bininfo", "$dir/:bininfo", $gbininfo);
  };
  warn($@) if $@;
  unlink("$dir/:bininfo.merge");
  return $gbininfo;
}

#
# moves binary packages from jobrepo to dst and updates full repository
#

sub update_dst_full {
  my ($prp, $packid, $dst, $jobdir, $meta, $useforbuildenabled, $prpsearchpath, $fullcache) = @_;

  my ($projid, $repoid) = split('/', $prp, 2);

  # do extra preinstallimage processing
  if ((defined($dst) && -e "$dst/.preinstallimage") || (defined($jobdir) && -e "$jobdir/.preinstallimage")) {
    update_preinstallimage($prp, $packid, $dst, $jobdir);
  }

  # check for lock and patchinfo
  if ($projpacks->{$projid} && $projpacks->{$projid}->{'package'} && $projpacks->{$projid}->{'package'}->{$packid}) {
    my $locked = 0;
    $locked = enabled($repoid, $projpacks->{$projid}->{'lock'}, $locked) if $projpacks->{$projid}->{'lock'};
    my $pdata = $projpacks->{$projid}->{'package'}->{$packid};
    $locked = enabled($repoid, $pdata->{'lock'}, $locked) if $pdata->{'lock'};
    if ($locked) {
      print "    package is locked\n";
      return;
    }
    $useforbuildenabled = 0 if $pdata->{'patchinfo'};
  }

  my $jobrepo;
  my @jobfiles;
  my $jobbininfo;
  if (defined($jobdir)) {
    @jobfiles = sort(ls($jobdir));
    @jobfiles = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne 'reason' && $_ ne '.bininfo'} @jobfiles;
    $jobbininfo = BSUtil::retrieve("$jobdir/.bininfo", 1);
    if ($jobbininfo && !($dst && $jobdir eq $dst) && !$jobbininfo->{'.bininfo'}) {
      # old style jobdir bininfo, ignore
      unlink("$jobdir/.bininfo");
      undef $jobbininfo;
    }
    $jobbininfo ||= read_bininfo($jobdir);
    delete $jobbininfo->{'.bininfo'};	# delete new version marker
    my $cache = { map {$_->{'id'} => $_} grep {$_->{'id'}} values %$jobbininfo };
    $jobrepo = findbins_dir([ map {"$jobdir/$_"} grep {/\.(?:$binsufsre)$/ && !/\.delta\.rpm$/} @jobfiles ], $cache);
  } else {
    $jobrepo = {};
  }

  ##################################################################
  # part 1: move files into package directory ($dst)

  my $gdst = "$reporoot/$prp/$myarch";

  my $oldrepo;
  my $isimport;
  my $bininfo;

  if ($dst && $jobdir && $dst eq $jobdir) {
    # a "refresh" operation, nothing to do here
    $oldrepo = $jobrepo;
    $bininfo = $jobbininfo;
    $bininfo->{'.nosourceaccess'} = {} if -e "$dst/.nosourceaccess";
  } elsif ($dst) {
    # get old state
    my @oldfiles = sort(ls($dst));
    @oldfiles = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne 'reason' && $_ ne '.bininfo'} @oldfiles;
    $oldrepo = findbins_dir([ map {"$dst/$_"} grep {/\.(?:$binsufsre)$/ && !/\.delta\.rpm$/} @oldfiles ]);

    # move files over
    mkdir_p($dst);
    my %new;
    for my $f (@jobfiles) {
      if (! -l "$dst/$f" && -d _) {
	BSUtil::cleandir("$dst/$f");
	rmdir("$dst/$f");
      }
      rename("$jobdir/$f", "$dst/$f") || die("rename $jobdir/$f $dst/$f: $!\n");
      $new{$f} = 1;
      $bininfo->{$f} = $jobbininfo->{$f} if $jobbininfo->{$f};
    }
    for my $f (grep {!$new{$_}} @oldfiles) {
      if (! -l "$dst/$f" && -d _) {
	BSUtil::cleandir("$dst/$f");
	rmdir("$dst/$f");
      } else {
	unlink("$dst/$f") ;
      }
    }
    # we only check 'sourceaccess', not 'access' here. 'access' has
    # to be handled anyway, so we don't gain anything by limiting
    # source access.
    if (!checkaccess('sourceaccess', $projid, $packid, $repoid)) {
      BSUtil::touch("$dst/.nosourceaccess");
      $bininfo->{'.nosourceaccess'} = {};
    }
  } else {
    # dst = undef is true for importevents
    $isimport = 1;
    my $replaced = (readxml("$jobdir/replaced.xml", $BSXML::dir, 1) || {})->{'entry'};
    $oldrepo = {};
    for (@{$replaced || []}) {
      my $rp = $_->{'name'};
      $_->{'name'} =~ s/\.[^\.]*$//;
      $_->{'source'} = 1;
      $oldrepo->{$rp} = $_;
    }
    $dst = $jobdir;	# get em from the jobdir
  }

  # write .bininfo file and update :bininfo.merge (jobdir is undef for package deletion)
  if (!$isimport) {
    my @bininfo_s;
    if (defined($jobdir) && defined($bininfo)) {
      BSUtil::store("$dst/.bininfo.new", "$dst/.bininfo", $bininfo);
      @bininfo_s = stat("$dst/.bininfo");
      $bininfo->{'.bininfo'} = {'id' => "$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]"} if @bininfo_s;
    } else {
      unlink("$dst/.bininfo");
    }

    my $gbininfo = {};
    $gbininfo = BSUtil::retrieve("$gdst/:bininfo.merge", 1) if -e "$gdst/:bininfo.merge";
    if ($gbininfo) {
      if (defined($jobdir) && $bininfo) {
	# currently not needed, maybe later
	for (values %$bininfo) {
	  delete $_->{'provides'};
	  delete $_->{'requires'};
	}
	$gbininfo->{$packid} = $bininfo;
      } else {
        $gbininfo->{$packid} = undef;
      }
      BSUtil::store("$gdst/.:bininfo.merge", "$gdst/:bininfo.merge", $gbininfo);
    } else {
      writestr("$gdst/.:bininfo.merge", "$gdst/:bininfo.merge", '');	# corrupt file, mark
    }
    delete $bininfo->{'.bininfo'} if $bininfo;
  }

  ##################################################################
  # part 2: link needed binaries into :full tree

  my $filter;
  # argh, this slows us down a bit
  my $bconf;
  if ($fullcache) {
    sync_fullcache($fullcache) if $fullcache->{'prp'} && $fullcache->{'prp'} ne $prp;
    $fullcache->{'prp'} = $prp;
  }
  if ($prpsearchpath) {
    $bconf = $fullcache->{'config'} if $fullcache && $fullcache->{'config'};
    $bconf ||= getconfig($myarch, $prpsearchpath);
    $fullcache->{'config'} = $bconf if $fullcache;
  }
  $filter = $bconf->{'exportfilter'} if $bconf;
  undef $filter if $filter && !%$filter;
  $filter ||= $default_exportfilters{$myarch};
  $filter = [ map {[$_, $filter->{$_}]} reverse sort keys %$filter ] if $filter;
  $filter = compile_exportfilter($filter);

  # link new ones into full, delete old ones no longer in use
  my %exports;

  my %new;
  for my $rp (sort keys %$jobrepo) {
    my $nn = $rp;
    $nn =~ s/.*\///;
    $new{$nn} = $jobrepo->{$rp};
  }

  # find destination for all new binaries
  my @movetofull;
  for my $rp (sort keys %new) {
    my $r = $new{$rp};
    next unless $r->{'source'};	# no src in full tree

    if ($filter) {
      my $skip;
      for (@$filter) {
	if ($rp =~ /$_->[0]/) {
	  $skip = $_->[1];
	  last;
	}
      }
      if ($skip) {
	my $myself;
        for my $exportarch (@$skip) {
	  if ($exportarch eq '.' || $exportarch eq $myarch) {
	    $myself = 1;
	    next;
	  }
	  next if $isimport;	# no re-exports
	  push @{$exports{$exportarch}}, $rp, $r;
	}
        next unless $myself;
      }
    }
    push @movetofull, $rp;
  }
  if ($filter && !$isimport) {
    # need also to check old entries
    for my $rp (sort keys %$oldrepo) {
      my $r = $oldrepo->{$rp};
      next unless $r->{'source'};	# no src rpms in full tree
      my $rn = $rp;
      $rn =~ s/.*\///;
      my $skip;
      for (@$filter) {
	if ($rn =~ /$_->[0]/) {
	  $skip = $_->[1];
	  last;
	}
      }
      if ($skip) {
        for my $exportarch (@$skip) {
	  $exports{$exportarch} ||= [] if $exportarch ne '.' && $exportarch ne $myarch;
	}
      }
    }
  }

  if ($filter && !$isimport) {
    # we always export, the other schedulers are free to reject the job
    # if move to full is also disabled for them
    for my $exportarch (sort keys %exports) {
      # check if this prp supports the arch
      next unless $projpacks->{$projid};
      my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
      if ($repo && grep {$_ eq $exportarch} @{$repo->{'arch'} || []}) {
	print "    sending filtered packages to $exportarch\n";
	createexportjob($prp, $exportarch, $jobrepo, $dst, $oldrepo, $meta, @{$exports{$exportarch}});
      }
    }
  }

  if (!$useforbuildenabled) {
    print "    move to :full is disabled\n";
    return;
  }

  my $pool;
  my $satrepo;
  my %old;
  my $metacache;
  my $metacache_ismerge;
  if ($fullcache && $fullcache->{'old'}) {
    $pool = $fullcache->{'pool'};
    $satrepo = $fullcache->{'satrepo'};
    %old = %{$fullcache->{'old'}};
    $metacache = $fullcache->{'metacache'} || {};
    $metacache_ismerge = $fullcache->{'metacache_ismerge'};
  } else {
    $pool = BSSolv::pool->new();
    eval { $satrepo = $pool->repofromfile($prp, "$gdst/:full.solv"); };
    %old = $satrepo->getpathid() if $satrepo;
    if (((-s "$gdst/:full.metacache") || 0) < 16384 && ! -e "$gdst/:full.metacache.merge") {
      $metacache = BSUtil::retrieve("$gdst/:full.metacache", 1) || {};
    } else {
      $metacache = BSUtil::retrieve("$gdst/:full.metacache.merge", 1) || {};
      $metacache_ismerge = 1;
    }
  }

  # move em over into :full
  mkdir_p("$gdst/:full") if @movetofull && ! -d "$gdst/:full";
  my %fnew;
  my $dep2meta;
  $dep2meta = $repodatas{$prp}->{'meta'} if $repodatas{$prp} && $repodatas{$prp}->{'meta'};
  my $linkedmeta = 0;
  my $metamd5;
  my $metaid;
  for my $rp (@movetofull) {
    my $r = $new{$rp};
    my $suf;
    $suf = $1 if $rp =~ /\.($binsufsre)$/;
    next unless $suf;
    my $n = $r->{'name'};
    my @s = stat("$dst/$rp");
    next unless @s;
    print "      + :full/$n.$suf ($rp)\n";
    # link gives an error if the dest exists, so we dup
    # and rename instead.
    # when the dest is the same file, rename doesn't do
    # anything, so we need the unlink after the rename
    unlink("$dst/$rp.dup");
    link("$dst/$rp", "$dst/$rp.dup");
    rename("$dst/$rp.dup", "$gdst/:full/$n.$suf") || die("rename $dst/$rp.dup $gdst/:full/$n.$suf: $!\n");
    unlink("$dst/$rp.dup");
    $old{"$n.$suf"} = "$s[9]/$s[7]/$s[1]";
    for my $osuf (@binsufs) {
      next if $suf eq $osuf;
      unlink("$gdst/:full/$n.$osuf");
      delete $old{"$n.$osuf"};
    }
    if ($meta) {
      if (!$metamd5) {
	local *F;
        $metamd5 = '0' x 32;
	$metaid = '0/0/0';
	if (open(F, '<', $meta)) {
	  my @s = stat(F);
	  $metaid = "$s[9]/$s[7]/$s[1]" if @s;
	  my $ctx = Digest::MD5->new;
	  $ctx->addfile(*F);
	  close F;
	  $metamd5 = $ctx->hexdigest();
	}
      }
      if ($BSConfig::maxmetahardlink && ++$linkedmeta >= $BSConfig::maxmetahardlink) {
	# workaround for btrfs hardlink limitation. sigh.
	writestr("$meta.dup", $meta, readstr($meta));
	my @s = stat($meta);
	$metaid = "$s[9]/$s[7]/$s[1]" if @s;
	$linkedmeta = 0;
      }
      link($meta, "$meta.dup");
      rename("$meta.dup", "$gdst/:full/$n.meta") || die("rename $meta.dup $gdst/:full/$n.meta: $!\n");
      unlink("$meta.dup");
      $metacache->{$n} = [$metaid, $metamd5];
    } else {
      unlink("$gdst/:full/$n.meta");
      delete $metacache->{$n};
      $metacache->{$n} = undef if $metacache_ismerge;
    }
    delete $dep2meta->{$n} if $dep2meta;

    $fnew{$n} = 1;
  }

  for my $rp (sort keys %$oldrepo) {
    my $r = $oldrepo->{$rp};
    next unless $r->{'source'};	# no src rpms in full tree
    my $suf;
    $suf = $1 if $rp =~ /\.($binsufsre)$/;
    next unless $suf;
    my $n = $r->{'name'};
    next if $fnew{$n};		# got new version, already deleted old

    my @s = stat("$gdst/:full/$n.$suf");

    # don't delete package if not ours
    next unless @s && $r->{'id'} eq "$s[9]/$s[7]/$s[1]";
    # package no longer built, kill full entry
    print "      - :full/$n.$suf\n";
    for my $osuf (@binsufs) {
      unlink("$gdst/:full/$n.$osuf");
      delete $old{"$n.$osuf"};
    }
    unlink("$gdst/:full/$n.iso");
    unlink("$gdst/:full/$n.meta");
    unlink("$gdst/:full/$n-MD5SUMS.meta");
    delete $dep2meta->{$n} if $dep2meta;
    delete $metacache->{$n};
    $metacache->{$n} = undef if $metacache_ismerge;
  }
  
  mkdir_p($gdst) unless -d $gdst;
  if ($fullcache) {
    # delayed writing of the solv file, just update the fullcache
    $fullcache->{'prp'} = $prp;
    $fullcache->{'pool'} = $pool;
    $fullcache->{'satrepo'} = $satrepo if $satrepo;
    $fullcache->{'old'} = \%old;
    $fullcache->{'metacache'} = $metacache;
    $fullcache->{'metacache_ismerge'} = $metacache_ismerge;
  } else {
    if ($satrepo) {
      $satrepo->updatefrombins("$gdst/:full", %old);
    } else {
      $satrepo = $pool->repofrombins($prp, "$gdst/:full", %old);
    }
    writesolv("$gdst/:full.solv.new", "$gdst/:full.solv", $satrepo);
    if ($metacache_ismerge) {
      BSUtil::store("$gdst/.:full.metacache.merge", "$gdst/:full.metacache.merge", $metacache);
    } else {
      BSUtil::store("$gdst/.:full.metacache", "$gdst/:full.metacache", $metacache);
    }
  }
  delete $repodatas{$prp}->{'solv'};
}

sub update_preinstallimage {
  my ($prp, $packid, $dst, $jobdir) = @_;
  my $gdst = "$reporoot/$prp/$myarch";
  my $dirty;
  # wipe old
  my $imagedata = BSUtil::retrieve("$gdst/:preinstallimages", 1) || [];
  my $newimagedata = [ grep {$_->{'package'} ne $packid} @$imagedata ];
  if (@$newimagedata != @$imagedata) {
    $dirty = 1;
    $imagedata = $newimagedata;
  }
  my @all;
  @all = grep {/(?:\.tar\.xz|\.tar\.gz|\.info)$/} grep {!/^\./} sort(ls($jobdir)) if $jobdir;
  my %all = map {$_ => 1} @all;
  my @imgs = grep {s/\.info$//} @all;
  for my $img (@imgs) {
    my $tar;
    next if (-s "$jobdir/$img.info") > 100000;
    if (-f "$jobdir/$img.tar.xz") {
      $tar = "$img.tar.xz";
    } elsif (-f "$jobdir/$img.tar.gz") {
      $tar = "$img.tar.gz";
    }
    next unless $tar;
    my @s = stat("$jobdir/$tar");
    next unless @s;
    my $info = readstr("$jobdir/$img.info", 1);
    next unless $info;
    my $id = Digest::MD5::md5_hex("$info/$s[9]/$s[7]/$s[1]");
    # calculate bitstring
    my $b = "\0" x 512;
    my @hdrmd5s;
    my @bins;
    for (split("\n", readstr("$jobdir/$img.info", 1))) {
      next unless /^([0-9a-f]{32})  ([^ ]+)$/s;
      vec($b, hex(substr($1, 0, 3)), 1) = 1;
      push @hdrmd5s, $1;
      push @bins, $2;
    }
    unlink("$jobdir/.preinstallimage.$id");
    link("$jobdir/$tar", "$jobdir/.preinstallimage.$id") || die("link $jobdir/$tar $jobdir/.preinstallimage.$id");
    if ($dst && $dst ne $jobdir) {
      unlink("$dst/.preinstallimage.$id");
      link("$jobdir/.preinstallimage.$id", "$dst/.preinstallimage.$id") || die("link $jobdir/.$id $dst/.preinstallimage.$id");
    }
    my $sizek = int(($s[7] + 1023) / 1024);
    push @$imagedata, {'package' => $packid, 'hdrmd5' => $id, 'file' => $tar, 'sizek' => $sizek, 'bitstring' => $b, 'hdrmd5s' => \@hdrmd5s, 'bins' => \@bins};
    $dirty = 1;
  }
  if ($dirty) {
    if (@$imagedata) {
      BSUtil::store("$gdst/.:preinstallimages", "$gdst/:preinstallimages", $imagedata);
    } else {
      unlink("$gdst/:preinstallimages");
    }
  }
}

sub sync_fullcache {
  my ($fullcache) = @_;

  return unless $fullcache;
  if (!$fullcache->{'old'}) {
    %$fullcache = ();
    return;
  }
  my $prp = $fullcache->{'prp'};
  my $gdst = "$reporoot/$prp/$myarch";
  mkdir_p($gdst) unless -d $gdst;
  my $pool = $fullcache->{'pool'};
  my $satrepo = $fullcache->{'satrepo'};
  my %old = %{$fullcache->{'old'}};
  if ($satrepo) {
    $satrepo->updatefrombins("$gdst/:full", %old);
  } else {
    $satrepo = $pool->repofrombins($prp, "$gdst/:full", %old);
  }
  writesolv("$gdst/:full.solv.new", "$gdst/:full.solv", $satrepo);
  delete $repodatas{$prp}->{'solv'};
  if ($fullcache->{'metacache'}) {
    if ($fullcache->{'metacache_ismerge'}) {
      BSUtil::store("$gdst/.:full.metacache.merge", "$gdst/:full.metacache.merge", $fullcache->{'metacache'});
    } else {
      BSUtil::store("$gdst/.:full.metacache", "$gdst/:full.metacache", $fullcache->{'metacache'});
    }
  }
  %$fullcache = ();
}

sub addjobhist {
  my ($prp, $info, $status, $js, $code) = @_;
  my $jobhist = {};
  $jobhist->{'code'} = $code;
  $jobhist->{$_} = $js->{$_} for qw{readytime starttime endtime uri workerid hostarch};
  $jobhist->{$_} = $info->{$_} for qw{package rev srcmd5 versrel bcnt reason};
  $jobhist->{'verifymd5'} = $info->{'verifymd5'} if $info->{'verifymd5'};
  $jobhist->{'readytime'} ||= $status->{'readytime'};	# backward compat
  mkdir_p("$reporoot/$prp/$myarch");
  BSFileDB::fdb_add("$reporoot/$prp/$myarch/:jobhistory", $BSXML::jobhistlay, $jobhist);
}


####################################################################
####################################################################
##
##  project/package data collection functions
##

my @prps;		# all prps(project-repositories-sorted) we have to schedule, sorted
my %prpsearchpath;	# maps prp => [ prp, prp, ...]
                        # build packages with the packages of the prps
my %prpdeps;		# searchpath plus aggregate deps plus kiwi deps
			# maps prp => [ prp, prp ... ]
			# used for sorting
my %prpnoleaf;		# is this prp referenced by another prp?
my @projpacks_linked;	# data of all linked sources

my %watchremote;	# remote_url => { eventdescr => projid }
my %watchremote_start;	# remote_url => lasteventno

my %repounchanged;
my %prpfinished;
my %prpnotready;	# maps prp => { packid => 1, ... }

my %watchremoteprojs;	# tmp, only set in addwatchremote

my @retryevents;
my %iswaiting;		# waiting for some RPC to return
my %iswaiting_server;

my $maxserverload = 1;
$maxserverload = $BSConfig::sched_maxserverload if $BSConfig::sched_maxserverload;
my %iswaiting_serverload;
my %iswaiting_serverload_low;

sub xrpc {
  my ($ctx, $resource, $param, @args) = @_;

  return BSRPC::rpc($param, @args) unless $param->{'async'};

  if (!$iswaiting{$resource}) {
    my $server = $param->{'uri'};
    $server =~ s/.*?\/\///;
    $server =~ s/\/.*//;
    my $serverload = scalar(keys %{$iswaiting_server{$server} || {}});
    if ($serverload >= $maxserverload) {
      my $handle = { '_xrpc_data' => [$ctx, $resource, $param, @args] };
      if (($ctx->{'changetype'} || '') ne 'low') {
        push @{$iswaiting_serverload{$server}}, $handle;
      } else {
        push @{$iswaiting_serverload_low{$server}}, $handle;
      }
      $handle->{'_iswaiting'} = $resource;
      $handle->{'_server'} = $server;
      $iswaiting{$resource} = $handle;
      return $handle;
    }
    my $handle = BSRPC::rpc($param, @args);
    $handle->{'_iswaiting'} = $resource;
    $handle->{'_changetype'} = $ctx->{'changetype'} if $ctx->{'changetype'};
    $handle->{'_server'} = $server;
    $handle->{'_ctx'} = $ctx;
    $handle->{$_} = $param->{'async'}->{$_} for keys %{$param->{'async'}};
    $iswaiting{$resource} = $handle;
    $iswaiting_server{$server}->{$resource} = 1;
    return $handle;
  }
  my $rhandle = $iswaiting{$resource};
  if ($ctx->{'prp'}) {
    my $handle = { '_changetype' => $ctx->{'changetype'} };
    $handle->{$_} = $param->{'async'}->{$_} for keys %{$param->{'async'}};
    push @{$rhandle->{'_wakeup'}}, [ $ctx->{'prp'}, $handle ];
    return $handle;
  }
  # project rpcs, we need to run them in order
  my $handle = { '_xrpc_data' => [$ctx, $resource, $param, @args] };
  push @{$rhandle->{'_nextxrpc'}}, $handle;
  return $handle;
}

my $resumed_highload = 0;

sub xrpc_resume {
  my ($handle) = @_;

  # iswaiting rpc, finish...
  my $iw = $handle->{'_iswaiting'};
  my $server = $handle->{'_server'};
  print "response from RPC $iw ($handle->{'uri'})\n";
  delete $iswaiting{$iw};
  delete $iswaiting_server{$server}->{$iw};

  # fire up rpcs delayed because of server load
  if ($iswaiting_serverload{$server} || $iswaiting_serverload_low{$server}) {
    my @loadrpcs;
    my @loadrpcs_low;
    @loadrpcs = @{delete $iswaiting_serverload{$server}} if $iswaiting_serverload{$server};
    @loadrpcs_low = @{delete $iswaiting_serverload_low{$server}} if $iswaiting_serverload_low{$server};
    while (@loadrpcs || @loadrpcs_low) {
      my $nextrpc;
      if ((@loadrpcs && $resumed_highload++ < 2) || !@loadrpcs_low) {
	$nextrpc = shift @loadrpcs;
      } else {
	$resumed_highload = 0;
	$nextrpc = shift @loadrpcs_low;
      }
      my $resource = $nextrpc->{'_iswaiting'};
      delete $iswaiting{$resource};
      my $nhandle = xrpc(@{$nextrpc->{'_xrpc_data'}});
      for (keys %$nextrpc) {
        $nhandle->{$_} = $nextrpc->{$_} if $_ ne '_xrpc_data';
      }
      last if $iswaiting_serverload{$server} || $iswaiting_serverload_low{$server};
    }
    push @{$iswaiting_serverload{$server}}, @loadrpcs;
    push @{$iswaiting_serverload_low{$server}}, @loadrpcs_low;
  }

  my $ret;
  eval { $ret = BSRPC::rpc($handle) };
  my $error;
  if ($@) {
    warn $@;
    $error = $@;
    chomp $error;
  }
  # run result handler
  die("no _resume set in handler $handle\n") unless $handle->{'_resume'};
  $handle->{'_resume'}->($handle, $error, $ret);

  # fire up waiting rpcs
  for my $nextrpc (@{$handle->{'_nextxrpc'} || []}) {
    my $nhandle = xrpc(@{$nextrpc->{'_xrpc_data'}});
    for (keys %$nextrpc) {
      $nhandle->{$_} = $nextrpc->{$_} if $_ ne '_xrpc_data';
    }
  }
  if (@{$handle->{'_wakeup'} || []}) {
    my %did;
    for my $wakeup (unify(@{$handle->{'_wakeup'} || []})) {
      my $changetype = $wakeup->[1]->{'_changetype'} || 'high';
      my $changelevel = $wakeup->[1]->{'_changelevel'} || 1;
      next if $did{"$wakeup->[0]/$changetype/$changelevel"};
      $did{"$wakeup->[0]/$changetype/$changelevel"} = 1;
      xrpc_setchanged($handle, $wakeup->[0], $changetype, $changelevel);
    }
  }
}

sub xrpc_setchanged {
  my ($handle, $what, $changetype, $changelevel) = @_;

  my $ctx = $handle->{'_ctx'};
  die("no context in handle\n") unless $ctx;
  $what ||= $ctx->{'prp'};
  $changetype ||= $handle->{'_changetype'} || 'high';
  $changelevel ||= $handle->{'_changelevel'} || 1;
  my $gctx = $ctx->{'gctx'};
  die("no gctx in handle context\n") unless $gctx;
  my $changed = $gctx->{"changed_$changetype"};
  my $changed_dirty = $gctx->{'changed_dirty'};
  my ($projid, $repoid) = split('/', $what, 2);
  if (defined($repoid)) {
    my $prp = $what;
    @{$gctx->{"lookat_$changetype"}} = grep {$_ ne $prp} @{$gctx->{"lookat_$changetype"}};
    unshift(@{$gctx->{"lookat_$changetype"}}, $prp);
    if ($changetype eq 'low') {
      # we don't use changed2lookat to prevent infinte looping
      for my $dprp (@prps) {
	$changed->{$dprp} = 1 if grep {$_ eq $prp} @{$prpdeps{$dprp}};
      }
    } else {
      if ($changelevel == 2) {
        $changed->{$prp} = 2;
      } else {
        $changed->{$prp} ||= 1;
      }
    }
    $changed_dirty->{$prp} = 1;
    return;
  }
  my @cprps;
  for my $prp (@prps) {
    push @cprps, $prp if (split('/', $prp, 2))[0] eq $projid;
  }
  my %cprps = map {$_ => 1} @cprps;
  @{$gctx->{"lookat_$changetype"}} = grep {!$cprps{$_}} @{$gctx->{"lookat_$changetype"}};
  if ($changelevel == 2) {
    for my $prp (@cprps) {
      unshift(@{$gctx->{"lookat_$changetype"}}, $prp);
      $changed->{$prp} = 2;
      $changed_dirty->{$prp} = 1;
    }
    $changed->{$projid} = 2;
  } else {
    for my $prp (@cprps) {
      unshift(@{$gctx->{"lookat_$changetype"}}, $prp);
      $changed->{$prp} ||= 1;
      $changed_dirty->{$prp} = 1;
    }
    $changed->{$projid} ||= 1;
  }
}

sub checkbuildrepoid {
  my ($projpacksin) = @_;
  die("ERROR: source server did not report a repoid") unless $projpacksin->{'repoid'};
  my $buildrepoid = readstr("$reporoot/_repoid", 1);
  if (!$buildrepoid) {
    # set the repoid on first run
    $buildrepoid = $projpacksin->{'repoid'};
    mkdir_p($reporoot) unless -d "$reporoot";
    writestr("$reporoot/._repoid$$", "$reporoot/_repoid", $buildrepoid);
  }
  die("ERROR: My repository id($buildrepoid) has wrong length(".length($buildrepoid).")") unless length($buildrepoid) == 9;
  die("ERROR: source server repository id($projpacksin->{'repoid'}) does not match my repository id($buildrepoid)") unless $buildrepoid eq $projpacksin->{'repoid'};
}


# async RPC bottom part of get_projpacks
sub get_projpacks_resume {
  my ($handle, $error, $projpacksin) = @_;

  # what we asked about
  my $projid = $handle->{'_projid'};
  if ($error) {
    chomp $error;
    warn("$error\n");
    addretryevent({'type' => 'project', 'project' => $projid});
    return;
  }
  my $packids = $handle->{'_packids'};	# we only requested those
  my $oldprojdata = $packids ? clone_projpacks_part($projid, $packids) : undef;
  update_projpacks($projpacksin, $projid, $packids);
  get_projpacks_postprocess() if !$packids || postprocess_needed_check($projid, $oldprojdata);

  # do some upgrades if the project is gone and we fetched all packages (i.e. project event case)
  if (!$packids && !$projpacks->{$projid}) {
    $handle->{'_dolink'} = 1;
    $handle->{'_changetype'} = 'high';
  }

  # fetch linked packages with lower prio
  if ($handle->{'_dolink'}) {
    my $linked = find_linked_sources($projid, $packids);
    for my $lprojid (sort keys %$linked) {
      my %lpackids = map {$_ => 1} @{$linked->{$lprojid}};
      my $async = {'_changetype' => $packids ? 'med' : 'low'};
      get_projpacks($handle->{'_ctx'}, $async, $lprojid, sort keys %lpackids);
    }
  }
  xrpc_setchanged($handle, $projid);
}

#
# get_projpacks:  get/update project/package information
#
# input:  $projid: update just this project
#         @packids: update just these packages
# output: $projpacks (global)
#

sub get_projpacks {
  my ($ctx, $doasync, $projid, @packids) = @_;

  undef $projid unless $projpacks;
  @packids = () unless defined $projid;
  @packids = grep {defined $_} @packids;

  if (!@packids) {
    if (defined($projid)) {
      delete $remoteprojs{$projid};
    } else {
      %remoteprojs = ();
    }
  }

  $projid ||= $testprojid;

  my @args;
  if (@packids) {
    print "getting data for project '$projid' package '".join("', '", @packids)."' from $BSConfig::srcserver\n";
    push @args, "project=$projid";
    for my $packid (@packids) {
      push @args, "package=$packid";
    }
  } elsif (defined($projid)) {
    print "getting data for project '$projid' from $BSConfig::srcserver\n";
    push @args, "project=$projid";
  } else {
    print "getting data for all projects from $BSConfig::srcserver\n";
  }
  my $projpacksin;
  while (1) {
    push @args, 'nopackages' if $testprojid && $projid ne $testprojid;
    for my $tries (4, 3, 2, 1, 0) {
      my $param = {
	'uri' => "$BSConfig::srcserver/getprojpack",
      };
      if ($doasync) {
	$param->{'async'} = { %$doasync, '_resume' => \&get_projpacks_resume, '_projid' => $projid };
	$param->{'async'}->{'_packids'} = [ @packids ] if @packids;
      }
      eval {
        $projpacksin = xrpc($ctx, $projid, $param, $BSXML::projpack, 'withsrcmd5', 'withdeps', 'withrepos', 'withconfig', 'withremotemap', "arch=$myarch", @args);
      };
      return 0 if !$@ && $projpacksin && $param->{'async'};
      last unless $@ || !$projpacksin;
      last unless $tries && defined($projid);
      print $@ if $@;
      print "retrying...\n";
      sleep(60);
    }
    if ($@ || !$projpacksin) {
      print $@ if $@;
      if (@args) {
        print "retrying...\n";
        get_projpacks(undef, undef);
        get_projpacks_postprocess();	# just in case...
        return;
      }
      die("could not get project/package information, aborting due to testmode\n") if $testmode;
      printf("could not get project/package information, sleeping 1 minute\n");
      sleep(60);
      print "retrying...\n";
      next;
    }
    last;
  }

  update_projpacks($projpacksin, $projid, \@packids);

  if ($testprojid) {
    my $proj = $projpacks->{$projid};
    for my $repo (@{$proj->{'repository'} || []}) {
      for my $path (@{$repo->{'path'} || []}) {
        next if $path->{'project'} eq $testprojid;
	next if $projid ne $testprojid && $projpacks->{$path->{'project'}};
	get_projpacks(undef, undef, $path->{'project'});
      }
    }
  }
}

# incorporate all the new data from projpacksin into our projpacks data
sub update_projpacks {
  my ($projpacksin, $projid, $packids) = @_;

  checkbuildrepoid($projpacksin);
  # free old data
  if (!defined($projid)) {
    $projpacks = {};
  } elsif (!($packids && @$packids)) {
    delete $projpacks->{$projid};
  } elsif ($projpacks->{$projid} && $projpacks->{$projid}->{'package'}) {
    for my $packid (@$packids) {
      delete $projpacks->{$projid}->{'package'}->{$packid};
    }
  }
  for my $proj (@{$projpacksin->{'project'} || []}) {
    if ($packids && @$packids) {
      die("bad projpack answer\n") unless $proj->{'name'} eq $projid;
      if ($projpacks->{$projid}) {
	# do not delete the missingpackages flag if we just update single packages
	$proj->{'missingpackages'} = 1 if $projpacks->{$projid}->{'missingpackages'};
	# use all packages/configs from old projpacks
	my $opackage = $projpacks->{$projid}->{'package'} || {};
	for (keys %$opackage) {
	  $opackage->{$_}->{'name'} = $_;
	  push @{$proj->{'package'}}, $opackage->{$_};
	}
	if (!$proj->{'patternmd5'} && $projpacks->{$projid}->{'patternmd5'}) {
	  $proj->{'patternmd5'} = $projpacks->{$projid}->{'patternmd5'} unless grep {$_ eq '_pattern'} @$packids;
	}
      }
    }
    $projpacks->{$proj->{'name'}} = $proj;
    delete $proj->{'name'};
    my $packages = {};
    for my $pack (@{$proj->{'package'} || []}) {
      $packages->{$pack->{'name'}} = $pack;
      delete $pack->{'name'};
    }
    if (%$packages) {
      $proj->{'package'} = $packages;
    } else {
      delete $proj->{'package'};
    }
  }
  remotemap2remoteprojs($projpacksin->{'remotemap'});
}

# update remoteprojs with the remotemap data
sub remotemap2remoteprojs {
  my ($remotemap) = @_;

  for my $proj (@{$remotemap || []}) {
    my $projid = delete $proj->{'project'};
    my $oproj = $remoteprojs{$projid};
    undef $oproj if $oproj && ($oproj->{'remoteurl'} ne $proj->{'remoteurl'} || $oproj->{'remoteproject'} ne $proj->{'remoteproject'});
    my $c = $proj->{'config'};
    $c = $oproj->{'config'} if !defined($c) && $oproj;
    my $error = $proj->{'error'};
    delete $proj->{'error'};
    $proj = $oproj if $proj->{'proto'} && $oproj && !$oproj->{'proto'};
    delete $proj->{'config'};
    $proj->{'config'} = $c if defined $c;
    if ($error) {
      $proj->{'error'} = $error;
      addretryevent({'type' => 'project', 'project' => $projid}) if $error =~ /interconnect error:/;
    }
    $remoteprojs{$projid} = $proj;
  }
}

# -> BSUtil
sub identical {
  my ($d1, $d2, $except) = @_;

  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}, $except);
    }    
  } elsif ($r eq 'ARRAY') {
    return 0 unless @$d1 == @$d2;
    for (my $i = 0; $i < @$d1; $i++) {
      return 0 unless identical($d1->[$i], $d2->[$i], $except);
    }    
  } else {
    return 0;
  }
  return 1;
}

# RPC bottom half of update_project_meta
sub update_project_meta_resume {
  my ($handle, $error, $projpacksin) = @_;

  my $projid = $handle->{'_projid'};
  if ($error || !update_project_meta_check($projid, $projpacksin)) {
    if ($error) {
      chomp $error;
      warn("$error\n");
    }
    # update meta failed, do it the hard way...
    my $async = {'_dolink' => $handle->{'_dolink'}, '_changetype' => $handle->{'_changetype'}, '_changelevel' => $handle->{'_changelevel'}};
    get_projpacks($handle->{'_ctx'}, $async, $projid);
  } elsif ($projpacks->{$projid}) {
    # update meta worked! Now get those packages as well
    my $packids = $handle->{'_packids'};
    if ($packids && @$packids) {
      # fetch those as well
      my $async = {'_dolink' => 1, '_changetype' => 'high', '_changelevel' => 1};
      get_projpacks($handle->{'_ctx'}, $async, $projid, @$packids);
    } else {
      xrpc_setchanged($handle, $projid);
    }
  } else {
    # project is gone!
    delete $handle->{'_packids'};
    get_projpacks_resume($handle, $error, $projpacksin);
  }
}

# just update the meta information, do not touch package data unless
# the project was deleted
sub update_project_meta {
  my ($ctx, $doasync, $projid) = @_;
  print "updating meta for project '$projid' from $BSConfig::srcserver\n";

  my $projpacksin;
  my $param = {
    'uri' => "$BSConfig::srcserver/getprojpack",
  };
  if ($doasync) {
    $param->{'async'} = { %$doasync, '_resume' => \&update_project_meta_resume, '_projid' => $projid };
  }
  eval {
    # withsrcmd5 is needed for the patterns md5sum
    $projpacksin = xrpc($ctx, $projid, $param, $BSXML::projpack, "project=$projid", 'nopackages', 'withrepos', 'withconfig', 'withsrcmd5', "arch=$myarch");
  };
  if ($@ || !$projpacksin) {
    print $@ if $@;
    return undef;
  }
  return $projpacksin if $projpacksin && $param->{'async'};
  return update_project_meta_check($projid, $projpacksin);
}

sub update_project_meta_check {
  my ($projid, $projpacksin) = @_;
  return 0 unless $projpacksin;
  my $proj = $projpacksin->{'project'}->[0];
  if (!$proj) {
    # project is gone!
    delete $projpacks->{$projid};
    return 1;
  }
  return undef unless $proj->{'name'} eq $projid;
  delete $proj->{'name'};
  delete $proj->{'package'};
  my $oldproj = $projpacks->{$projid};
  $proj->{'package'} = $oldproj->{'package'} if $oldproj->{'package'};
  # check if the project meta has critical change
  return 0 unless identical($proj->{'build'}, $oldproj->{'build'});
  return 0 unless identical($proj->{'link'}, $oldproj->{'link'});
  # XXX: could be more clever here
  return 0 unless identical($proj->{'repository'}, $oldproj->{'repository'});

  # check macro definitions and build type for all repositories
  for my $repoid (map {$_->{'name'}} @{$proj->{'repository'} || []}) {
    my @mprefix = ("%define _project $projid", "%define _repository $repoid");
    my $cold = Build::read_config($myarch, @mprefix, split("\n", $oldproj->{'config'} || ''));
    my $cnew = Build::read_config($myarch, @mprefix, split("\n", $proj->{'config'} || ''));
    return 0 unless identical($cold->{'macros'}, $cnew->{'macros'});
    return 0 unless identical($cold->{'type'}, $cnew->{'type'});
  }
  $projpacks->{$projid} = $proj;
  return 1;
}

# used to remember old data before calling update_projpacks so that
# postprocess_needed_check has something to compare against.
sub clone_projpacks_part {
  my ($projid, $packids) = @_;
  
  my $oldprojdata = { %{$projpacks->{$projid} || {}} };
  delete $oldprojdata->{'package'};
  $oldprojdata = Storable::dclone($oldprojdata);
  my $oldpackdata = {};
  my $packs = ($projpacks->{$projid} || {})->{'package'} || {};
  for my $packid (@$packids) {
    $oldpackdata->{$packid} = $packs->{$packid} ? Storable::dclone($packs->{$packid}) : undef;
  }
  $oldprojdata->{'package'} = $oldpackdata;
  return $oldprojdata;
}

sub postprocess_needed_check {
  my ($projid, $oldprojdata) = @_;

  return 1 unless $oldprojdata && $oldprojdata->{'package'};		# sanity
  # if we just had a srcmd5 change in some packages there's no need to postprocess
  if (!identical($projpacks->{$projid}, $oldprojdata, {'package' => 1})) {
    return 1;
  }
  my $packs = ($projpacks->{$projid} || {})->{'package'} || {};
  my %except = map {$_ => 1} qw{rev srcmd5 versrel verifymd5 revtime dep prereq file name error build publish useforbuild};
  my $oldpackdata = $oldprojdata->{'package'};
  for my $packid (keys %$oldpackdata) {
    if (!identical($oldpackdata->{$packid}, $packs->{$packid}, \%except)) {
      return 1;
    }
  }
  return 0;
}


#
# post-process projpack information
#  calculate package link information
#  calculate ordered prp list
#  calculate remote info
# 
sub get_projpacks_postprocess {
  %watchremote = ();
  %watchremoteprojs = ();

  #print Dumper($projpacks);
  calc_projpacks_linked();	# modifies watchremote/watchremoteprojs
  calc_prps();			# modifies watchremote/watchremoteprojs

  updateremoteprojs();
  %watchremoteprojs = ();
}

#
# addwatchremote:  register for a possibly remote resource
#
# input:  $type: type of resource (project/package/repository)
#         $projid: local name of the project
#         $watch: extra data to match
#
sub addwatchremote {
  my ($type, $projid, $watch) = @_;

  return undef if $projpacks->{$projid} && !$projpacks->{$projid}->{'remoteurl'};
  my $proj = remoteprojid($projid);
  $watchremoteprojs{$projid} = $proj;
  return undef unless $proj;
  $watchremote{$proj->{'remoteurl'}}->{"$type/$proj->{'remoteproject'}$watch"} = $projid;
  return $proj;
}

sub addretryevent {
  my ($ev) = @_;
  for my $oev (@retryevents) {
    next if $ev->{'type'} ne $oev->{'type'} || $ev->{'project'} ne $oev->{'project'};
    if ($ev->{'type'} eq 'repository' || $ev->{'type'} eq 'recheck') {
      next if $ev->{'repository'} ne $oev->{'repository'};
    } elsif ($ev->{'type'} eq 'package') {
      next if ($ev->{'package'} || '') ne ($oev->{'package'} || '');
    }
    return;
  }
  $ev->{'retry'} = time() + 60;
  push @retryevents, $ev;
}

#
# calc_projpacks_linked  - generate projpacks_linked helper array
#
# input:  $projpacks (global)
# output: @projpacks_linked (global)
#
sub calc_projpacks_linked {
  @projpacks_linked = ();
  for my $projid (sort keys %$projpacks) {
    my ($mypackid, $pack);
    while (($mypackid, $pack) = each %{$projpacks->{$projid}->{'package'} || {}}) {
      next unless $pack->{'linked'};
      my @li = @{$pack->{'linked'}};
      for my $li (@li) {
	$li = { %$li };		# clone so that we don't change projpack
	addwatchremote('package', $li->{'project'}, "/$li->{'package'}");
	$li->{'myproject'} = $projid;
	$li->{'mypackage'} = $mypackid;
      }
      push @projpacks_linked, @li;
    }
    if ($projpacks->{$projid}->{'link'}) {
      my @li = expandprojlink($projid);
      for my $li (@li) {
	addwatchremote('package', $li->{'project'}, '');	# watch all packages
	$li->{'package'} = ':*';
	$li->{'myproject'} = $projid;
      }
      push @projpacks_linked, @li;
    }
  }
  #print Dumper(\@projpacks_linked);
}

#
# find_linked_sources - find which projects/packages link to the specified project/packages
#
# output: hash ref project -> package list
#
sub find_linked_sources {
  my ($projid, $packids) = @_;
  my %linked;
  if ($packids) {
    for my $packid (unify(@$packids)) {
      for my $linfo (grep {$_->{'project'} eq $projid && ($_->{'package'} eq $packid || $_->{'package'} eq ':*')} @projpacks_linked) {
	push @{$linked{$linfo->{'myproject'}}}, defined($linfo->{'mypackage'}) ? $linfo->{'mypackage'} : $packid;
      }
    }
  } else {
    for my $linfo (grep {$_->{'project'} eq $projid} @projpacks_linked) {
      next unless exists $linfo->{'mypackage'};
      push @{$linked{$linfo->{'myproject'}}}, $linfo->{'mypackage'};
    }
  }
  return \%linked;
}

#
# expandsearchpath  - recursively expand the last component
#                     of a repository's path
#
# input:  $projid     - the project the repository belongs to
#         $repository - the repository data
# output: expanded path array
#
sub expandsearchpath {
  my ($projid, $repository) = @_;
  my %done;
  my @ret;
  my @path = @{$repository->{'path'} || []};
  # our own repository is not included in the path,
  # so put it infront of everything
  unshift @path, {'project' => $projid, 'repository' => $repository->{'name'}};
  while (@path) {
    my $t = shift @path;
    my $prp = "$t->{'project'}/$t->{'repository'}";
    push @ret, $t unless $done{$prp};
    $done{$prp} = 1;
    addwatchremote('repository', $t->{'project'}, "/$t->{'repository'}/$myarch") unless $t->{'repository'} eq '_unavailable';
    if (!@path) {
      last if $done{"/$prp"};
      my ($pid, $rid) = ($t->{'project'}, $t->{'repository'});
      my $proj = addwatchremote('project', $pid, '');
      if ($proj) {
	$proj = fetchremoteproj($proj, $pid);
      } else {
	$proj = $projpacks->{$pid};
      }
      next unless $proj;
      $done{"/$prp"} = 1;	# mark expanded
      my @repo = grep {$_->{'name'} eq $rid} @{$proj->{'repository'} || []};
      push @path, @{$repo[0]->{'path'}} if @repo && $repo[0]->{'path'};
    }
  }
  return @ret;
}

sub expandprojlink {
  my ($projid) = @_;

  my @ret;
  my $proj = $projpacks->{$projid};
  my @todo = map {$_->{'project'}} @{$proj->{'link'} || []};
  my %seen = ($projid => 1);
  while (@todo) {
    my $lprojid = shift @todo;
    next if $seen{$lprojid};
    push @ret, {'project' => $lprojid};
    $seen{$lprojid} = 1;
    my $lproj = addwatchremote('project', $lprojid, '');
    if ($lproj) {
      $lproj = fetchremoteproj($lproj, $lprojid);
    } else {
      $lproj = $projpacks->{$lprojid};
    }
    unshift @todo, map {$_->{'project'}} @{$lproj->{'link'} || []};
  }
  return @ret;
}

#
# calc_prps
#
# find all prps we have to schedule, expand search path for every prp,
# set up inter-prp dependency graph, sort prps using this graph.
#
# input:  $projpacks     (global)
# output: @prps          (global)
#         %prpsearchpath (global)
#         %prpdeps       (global)
#         %prpnoleaf     (global)
#

sub calc_prps {
  print "calculating project dependencies...\n";
  # calculate prpdeps dependency hash
  @prps = ();
  %prpsearchpath = ();
  %prpdeps = ();
  %prpnoleaf = ();
  for my $projid (sort keys %$projpacks) {
    my $repos = $projpacks->{$projid}->{'repository'} || [];
    my @aggs = grep {$_->{'aggregatelist'}} values(%{$projpacks->{$projid}->{'package'} || {}});
    my @kiwiinfos = grep {$_->{'path'}} map {@{$_->{'info'} || []}} values(%{$projpacks->{$projid}->{'package'} || {}});
    for my $repo (@$repos) {
      next unless grep {$_ eq $myarch} @{$repo->{'arch'} || []};
      my $repoid = $repo->{'name'};
      my $prp = "$projid/$repoid";
      push @prps, $prp;
      my @searchpath = expandsearchpath($projid, $repo);
      # map searchpath to internal prp representation
      my @sp = map {"$_->{'project'}/$_->{'repository'}"} @searchpath;
      $prpsearchpath{$prp} = \@sp;
      $prpdeps{"$projid/$repo->{'name'}"} = \@sp;

      # Find extra dependencies due to aggregate/kiwi description files
      my @xsp;
      if (@aggs) {
	# push source repositories used in this aggregate onto xsp, obey target mapping
	for my $agg (map {@{$_->{'aggregatelist'}->{'aggregate'} || []}} @aggs) {
	  my $aprojid = $agg->{'project'};
	  my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$agg->{'repository'} || []}; 
          if (@arepoids) {
	    # got some mappings for our target, use source as repoid
            push @xsp, map {"$aprojid/$_->{'source'}"} grep {exists($_->{'source'})} @arepoids;
          } else {
	    # no repository mapping, just use own repoid
	    push @xsp, "$aprojid/$repoid";
          }
	}
      }
      if (@kiwiinfos) {
        # push repositories used in all kiwi files
	push @xsp, map {"$_->{'project'}/$_->{'repository'}"} map {@{$_->{'path'}}} grep {$_->{'repository'} eq $repoid} @kiwiinfos;
      }

      if (@xsp) {
        # found some repos, join extra deps with project deps
        for my $xsp (@xsp) {
	  next if $xsp eq $prp;
          my ($mprojid, $mrepoid) = split('/', $xsp, 2);
          # we just watch the repository as it costs too much to
          # watch every single package
          addwatchremote('repository', $mprojid, "/$mrepoid/$myarch");
        }
        my %xsp = map {$_ => 1} (@sp, @xsp);
	delete $xsp{$prp};
        $prpdeps{$prp} = [ sort keys %xsp ];
      }
      # set noleaf info
      for (@{$prpdeps{$prp}}) {
        $prpnoleaf{$_} = 1 if $_ ne $prp;
      }
    }
  }

  # do the real sorting
  print "sorting projects and repositories...\n";
  @prps = sortpacks(\%prpdeps, undef, undef, @prps);
}

####################################################################

sub updateremoteprojs {
  for my $projid (keys %remoteprojs) {
    my $r = $watchremoteprojs{$projid};
    if (!$r) {
      delete $remoteprojs{$projid};
      next;
    }
    my $or = $remoteprojs{$projid};
    next if $or && $or->{'remoteurl'} eq $r->{'remoteurl'} && $or->{'remoteproject'} eq $r->{'remoteproject'};
    delete $remoteprojs{$projid};
  }
  for my $projid (sort keys %watchremoteprojs) {
    fetchremoteproj($watchremoteprojs{$projid}, $projid);
  }
}

sub remoteprojid {
  my ($projid) = @_;
  my $rsuf = '';
  my $origprojid = $projid;

  my $proj = $projpacks->{$projid};
  if ($proj) {
    return undef unless $proj->{'remoteurl'};
    return undef unless $proj->{'remoteproject'};
    return {
      'name' => $projid,
      'root' => $projid,
      'remoteroot' => $proj->{'remoteproject'},
      'remoteurl' => $proj->{'remoteurl'},
      'remoteproject' => $proj->{'remoteproject'},
    };
  }
  while ($projid =~ /^(.*)(:.*?)$/) {
    $projid = $1;
    $rsuf = "$2$rsuf";
    $proj = $projpacks->{$projid};
    if ($proj) {
      return undef unless $proj->{'remoteurl'};
      if ($proj->{'remoteproject'}) {
	$rsuf = "$proj->{'remoteproject'}$rsuf";
      } else {
	$rsuf =~ s/^://;
      }
      return {
        'name' => $origprojid,
        'root' => $projid,
        'remoteroot' => $proj->{'remoteproject'},
        'remoteurl' => $proj->{'remoteurl'},
        'remoteproject' => $rsuf,
      };
    }
  }
  return undef;
}

sub maptoremote {
  my ($proj, $projid) = @_;
  return "$proj->{'root'}:$projid" unless $proj->{'remoteroot'};
  return $proj->{'root'} if $projid eq $proj->{'remoteroot'};
  return '_unavailable' if $projid !~ /^\Q$proj->{'remoteroot'}\E:(.*)$/;
  return "$proj->{'root'}:$1";
}

sub fetchremoteproj {
  my ($proj, $projid) = @_;
  return undef unless $proj && $proj->{'remoteurl'} && $proj->{'remoteproject'};
  $projid ||= $proj->{'name'};
  return $remoteprojs{$projid} if exists $remoteprojs{$projid};
  print "fetching remote project data for $projid\n";
  my $rproj;
  my $param = {
    'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/_meta",
    'timeout' => 30,
    'proxy' => $proxy,
  };
  eval {
    $rproj = BSRPC::rpc($param, $BSXML::proj);
  };
  if ($@) {
    warn($@);
    my $error = $@;
    $error =~ s/\n$//s;
    $rproj = {'error' => $error};
    addretryevent({'type' => 'project', 'project' => $projid}) if $error !~ /remote error:/;
  }
  return undef unless $rproj;
  for (qw{name root remoteroot remoteurl remoteproject}) {
    $rproj->{$_} = $proj->{$_};
  }
  # map remote project names to local names
  for my $repo (@{$rproj->{'repository'} || []}) {
    for my $pathel (@{$repo->{'path'} || []}) {
      $pathel->{'project'} = maptoremote($proj, $pathel->{'project'});
    }    
  }
  for my $link (@{$rproj->{'link'} || []}) {
    $link->{'project'} = maptoremote($proj, $link->{'project'});
  }
  $remoteprojs{$projid} = $rproj;
  return $rproj;
}

sub fetchremoteconfig {
  my ($projid) = @_;

  my $proj = $remoteprojs{$projid};
  return undef if !$proj || $proj->{'error'};
  return $proj->{'config'} if exists $proj->{'config'};
  print "    fetching remote project config for $projid\n";
  my $c;
  my $param = {
    'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/_config",
    'timeout' => 30,
    'proxy' => $proxy,
  };
  eval {
    $c = BSRPC::rpc($param);
  };
  if ($@) {
    warn($@);
    $proj->{'error'} = $@;
    $proj->{'error'} =~ s/\n$//s;
    addretryevent({'type' => 'project', 'project' => $projid}) if $proj->{'error'} !~ /remote error:/;
    return undef;
  }
  $proj->{'config'} = $c;
  return $c;
}

### remote repository handling

sub addrepo_remote_unpackcpio {
  my ($pool, $prp, $arch, $cpio, $error) = @_;

  my $repodata;
  if ($arch eq $myarch) {
    $repodatas{$prp} ||= {};
    $repodata = $repodatas{$prp};
  } else {
    $repodatas_alien{"$prp/$arch"} ||= {};
    $repodata = $repodatas_alien{"$prp/$arch"};
  }
  my $cachemd5 = Digest::MD5::md5_hex("$prp/$arch");
  substr($cachemd5, 2, 0, '/');

  if ($error) {
    chomp $error;
    warn("$error\n");
    if ($error !~ /remote error:/) {
      my ($projid, $repoid) = split('/', $prp, 2);
      addretryevent({'type' => 'repository', 'project' => $projid, 'repository' => $repoid, 'arch' => $arch});
      if (-s "$remotecache/$cachemd5.solv") {
	# try last solv file
	my $r;
	eval {$r = $pool->repofromfile($prp, "$remotecache/$cachemd5.solv");};
	if ($r) {
	  $repodata->{'lastscan'} = time();
	  $repodata->{'solvfile'} = "$remotecache/$cachemd5.solv";
	  return $r;
	}
      }
    }
    $repodata->{'lastscan'} = time();
    $repodata->{'error'} = $error;
    return undef;
  }

  my %cpio = map {$_->{'name'} => $_->{'data'}} @{$cpio || []};
  my $repostate = $cpio{'repositorystate'};
  $repostate = XMLin($BSXML::repositorystate, $repostate) if $repostate;
  if ($arch eq $myarch) {
    delete $prpnotready{$prp};
    if ($repostate && $repostate->{'blocked'}) {
      $prpnotready{$prp} = { map {$_ => 1} @{$repostate->{'blocked'}} };
    }
  }
  my $r;
  my $solv;
  if (exists $cpio{'repositorysolv'} && $BSConfig::usesolvstate) {
    eval {$r = $pool->repofromstr($prp, $cpio{'repositorysolv'}); };
    warn($@) if $@;
  } elsif (exists $cpio{'repositorycache'}) {
    my $cache;
    eval { $cache = Storable::thaw(substr($cpio{'repositorycache'}, 4)); };
    delete $cpio{'repositorycache'};	# free mem
    warn($@) if $@;
    return undef unless $cache;
    # free some unused entries to save mem
    for (values %$cache) {
      delete $_->{'path'};
      delete $_->{'id'};
    }
    $r = $pool->repofromdata($prp, $cache);
  } else {
    # return empty repo
    $r = $pool->repofrombins($prp, '');
    $repodata->{'solv'} = $r->tostr();	# small enough to keep it incore
  }
  return undef unless $r;
  # write solv file
  mkdir_p("$remotecache/".substr($cachemd5, 0, 2));
  writesolv("$remotecache/$cachemd5.solv.new$$", "$remotecache/$cachemd5.solv", $r);
  $repodata->{'lastscan'} = time();
  $repodata->{'solvfile'} = "$remotecache/$cachemd5.solv";
  return $r;
}

sub addrepo_remote_resume {
  my ($handle, $error, $cpio) = @_;
  my $pool = BSSolv::pool->new();
  my $r = addrepo_remote_unpackcpio($pool, $handle->{'_prp'}, $handle->{'_arch'}, $cpio, $error);
  xrpc_setchanged($handle) unless !$r && $error && $error !~ /remote error:/;
}

sub addrepo_remote {
  my ($ctx, $pool, $prp, $arch, $remoteproj) = @_;

  my ($projid, $repoid) = split('/', $prp, 2);
  return undef if !$remoteproj || $remoteproj->{'error'};

  my $cachemd5 = Digest::MD5::md5_hex("$prp/$arch");
  substr($cachemd5, 2, 0, '/');

  print "    fetching remote repository state for $prp\n";
  my $param = {
    'uri' => "$remoteproj->{'remoteurl'}/build/$remoteproj->{'remoteproject'}/$repoid/$arch/_repository",
    'timeout' => 200,
    'receiver' => \&BSHTTP::cpio_receiver,
    'proxy' => $proxy,
  };
  if ($asyncmode) {
    $param->{'async'} = { '_resume' => \&addrepo_remote_resume, '_prp' => $prp, '_arch' => $arch };
  }
  my $cpio;
  eval {
    die('unsupported view\n') unless $BSConfig::usesolvstate;
    $cpio = xrpc($ctx, "repository/$prp/$arch", $param, undef, 'view=solvstate');
  };
  if ($@ && $@ =~ /unsupported view/) {
    eval {
      $cpio = xrpc($ctx, "repository/$prp/$arch", $param, undef, 'view=cache');
    };
  }
  if ($@) {
    return addrepo_remote_unpackcpio($pool, $prp, $arch, $cpio, $@);
  }
  return 0 if $param->{'async'} && $cpio;	# hack: false but not undef
  return addrepo_remote_unpackcpio($pool, $prp, $arch, $cpio);
}


# add repo belonging to a different architecture
sub addrepo_alien {
  my ($ctx, $pool, $prp, $arch) = @_;

  $repodatas_alien{"$prp/$arch"}->{'dontwrite'} = 1;
  my $oldrepodata = $repodatas{$prp};
  my $oldprpnotready = $prpnotready{$prp};
  delete $prpnotready{$prp};
  $repodatas{$prp} = $repodatas_alien{"$prp/$arch"};
  my $savemyarch = $myarch;
  $myarch = $arch;
  my $r = addrepo($ctx, $pool, $prp);
  $myarch = $savemyarch;
  $repodatas_alien{"$prp/$arch"} = $repodatas{$prp};
  delete $repodatas{$prp};
  $repodatas{$prp} = $oldrepodata if $oldrepodata;
  delete $prpnotready{$prp};
  $prpnotready{$prp} = $oldprpnotready if $oldprpnotready;
  return $r;
}

### remote project binary state handling 

sub convertpackagebinarylist {
  my ($prpa, $packagebinarylist, $error, $packstatususer) = @_;

  if ($error) {
    chomp $error;
    warn("$error\n");
    $error ||= 'internal error';
    $remotegbininfos{$prpa} = { 'lastfetch' => time(), 'error' => $error };
    return (undef, undef);
  }
  my $gbininfo = {};
  my $rpackstatus = {};
  for my $binaryversionlist (@{$packagebinarylist->{'binaryversionlist'} || []}) {
    my %bins;
    for my $binary (@{$binaryversionlist->{'binary'} || []}) {
      my $filename = $binary->{'name'};
      # XXX: should not rely on the filename here!
      if ($filename =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/) {
        $bins{$filename} = {'filename' => $filename, 'name' => $1, 'arch' => $2};
      } elsif ($filename =~ /^([^\/]+)_[^\/]*_([^\/]*)\.deb$/) {
        $bins{$filename} = {'filename' => $filename, 'name' => $1, 'arch' => $2};
      } elsif ($filename =~ /^([^\/]+)-[^-]+-[^-]+-([a-zA-Z][^\/\.\-]*)\.pkg\.tar\..z$/) {
        $bins{$filename} = {'filename' => $filename, 'name' => $1, 'arch' => $2};
      } else {
        $bins{$filename} = {'filename' => $filename};
      }
      $bins{$filename}->{'hdrmd5'} = $binary->{'hdrmd5'} if $binary->{'hdrmd5'};
      $bins{$filename}->{'leadsigmd5'} = $binary->{'leadsigmd5'} if $binary->{'leadsigmd5'};
    }
    my $pkg = $binaryversionlist->{'package'};
    $gbininfo->{$pkg} = \%bins;
    $rpackstatus->{$pkg} = $binaryversionlist->{'code'} if $binaryversionlist->{'code'};
  }
  my $cachemd5 = Digest::MD5::md5_hex($prpa);
  substr($cachemd5, 2, 0, '/');
  mkdir_p("$remotecache/".substr($cachemd5, 0, 2));
  BSUtil::store("$remotecache/$cachemd5.bininfo.new$$", "$remotecache/$cachemd5.bininfo", $gbininfo);

  $remotegbininfos{$prpa} = { 'lastfetch' => time() };

  if ($packstatususer) {
    $rpackstatus->{'/users'} = [];
    $rpackstatus->{'/users'} = [ @{$remotepackstatus{$prpa}->{'/users'} || []} ] if $remotepackstatus{$prpa};
    push @{$rpackstatus->{'/users'}}, $packstatususer unless grep {$_ eq $packstatususer} @{$rpackstatus->{'/users'}};
    push @{$remotepackstatus_cleanup{$packstatususer}}, $prpa;
    $remotepackstatus{$prpa} = $rpackstatus;
  }

  return ($gbininfo, $rpackstatus);
}

sub cleanup_remotepackstatus {
  my ($prp) = @_;

  return unless $remotepackstatus_cleanup{$prp};
  print "    cleaning up remote packstatus\n";
  for my $prpa (@{$remotepackstatus_cleanup{$prp}}) {
    my $rpackstatus = $remotepackstatus{$prpa};
    my @users = grep {$_ ne $prp} @{$rpackstatus->{'/users'} || []};
    $rpackstatus->{'/users'} = \@users;
    print "      - $prpa: ".@users." users\n";
    delete $remotepackstatus{$prpa} unless @users;
  }
  delete $remotepackstatus_cleanup{$prp};
}

sub read_gbininfo_remote_resume {
  my ($handle, $error, $packagebinarylist) = @_;
  convertpackagebinarylist($handle->{'_prpa'}, $packagebinarylist, $error, $handle->{'_ctx'}->{'prp'});
  xrpc_setchanged($handle);
}

sub read_gbininfo_remote {
  my ($ctx, $prpa, $remoteproj, $packstatus) = @_;

  return undef unless $remoteproj;
  return undef if $remoteproj->{'error'};

  my $cachemd5 = Digest::MD5::md5_hex($prpa);
  substr($cachemd5, 2, 0, '/');

  my $now = time();

  # first check error case
  if ($remotegbininfos{$prpa} && $remotegbininfos{$prpa}->{'error'} && ($remotegbininfos{$prpa}->{'lastfetch'} || 0) > $now - 3600) {
    return undef;
  }

  # check if we can use the cache
  my $rpackstatus;
  if ($packstatus && $remotepackstatus{$prpa} && $asyncmode) {
    my $prp = $ctx->{'prp'};
    $rpackstatus = $remotepackstatus{$prpa} if grep {$_ eq $prp} @{$remotepackstatus{$prpa}->{'/users'} || []};
  }
  if ((!$packstatus || $rpackstatus) && $remotegbininfos{$prpa} && ($remotegbininfos{$prpa}->{'lastfetch'} || 0) > $now - 3600) {
    if (-s "$remotecache/$cachemd5.bininfo") {
      my $gbininfo = BSUtil::retrieve("$remotecache/$cachemd5.bininfo", 1);
      if ($gbininfo) {
	if ($packstatus) {
	  for my $pkg (keys %$gbininfo) {
	    $packstatus->{$pkg} = $rpackstatus->{$pkg} if $rpackstatus->{$pkg};
	  }
	}
        return $gbininfo;
      }
    }
  }

  print "    fetching remote project binary state for $prpa\n";
  my ($projid, $repoid, $arch) = split('/', $prpa, 3);
  my $param = {
    'uri' => "$remoteproj->{'remoteurl'}/build/$remoteproj->{'remoteproject'}/$repoid/$arch",
    'timeout' => 200,
    'proxy' => $proxy,
  };
  if ($asyncmode) {
    $param->{'async'} = { '_resume' => \&read_gbininfo_remote_resume, '_prpa' => $prpa };
  }
  my $packagebinarylist;
  eval {
    $packagebinarylist = xrpc($ctx, "bininfo/$prpa", $param, $BSXML::packagebinaryversionlist, "view=binaryversionscode");
  };
  if ($@) {
    warn($@);
    my $error = $@;
    $error =~ s/\n$//s;
    ($projid, $repoid) = split('/', $ctx->{'prp'}, 2);
    addretryevent({'type' => 'recheck', 'project' => $projid, 'repository' => $repoid}) if $error !~ /remote error:/;
    return undef;
  }
  return 0 if $packagebinarylist && $param->{'async'};
  my $gbininfo;
  ($gbininfo, $rpackstatus) = convertpackagebinarylist($prpa, $packagebinarylist);
  if ($packstatus && $rpackstatus) {
    $packstatus->{$_} = $rpackstatus->{$_} for keys %$rpackstatus;
    delete $packstatus->{'/users'};
  }
  return $gbininfo;
}


#
# patch the packstatus entry of package $packid so that it reflects the finished state
# and does not revert back to scheduled
#
sub patchpackstatus {
  my ($prp, $packid, $code) = @_;

  $code ||= 'unknown';
  BSUtil::appendstr("$reporoot/$prp/$myarch/:packstatus.finished", "$code $packid\n");
  # touch mtime to make watchers see a change
  utime(time, time, "$reporoot/$prp/$myarch/:packstatus");
}

#
# jobfinished - called when a build job is finished
#
# - move built packages into :full tree
# - set changed flag
#
# input: $job       - job identification
#        $js        - job status information (BSXML::jobstatus)
#        $changed   - reference to changed hash, mark prp if
#                     we changed the repository
#        $fullcache - store data for delayed writing of :full.solv
#
sub jobfinished {
  my ($ectx, $job, $js) = @_;

  my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
  my $jobdatadir = "$myjobsdir/$job:dir";
  if (!$info || ! -d $jobdatadir) {
    print "  - $job is bad\n";
    return;
  }
  if ($info->{'file'} eq '_aggregate') {
    aggregatefinished($ectx, $job, $js);
    return ;
  }
  if ($info->{'file'} eq '_delta') {
    deltafinished($ectx, $job, $js);
    return ;
  }
  my $fullcache = $ectx->{'fullcache'};
  my $changed = $ectx->{'changed_med'};

  my $projid = $info->{'project'};
  my $repoid = $info->{'repository'};
  my $packid = $info->{'package'};
  my $prp = "$projid/$repoid";

  sync_fullcache($fullcache) if $fullcache && $fullcache->{'prp'} && $fullcache->{'prp'} ne $prp;	# hey!
  
  my $now = time(); # ensure that we use the same time in all logs
  if ($info->{'arch'} ne $myarch) {
    print "  - $job has bad arch\n";
    return;
  }
  if (!$projpacks->{$projid}) {
    print "  - $job belongs to an unknown project\n";
    return;
  }
  my $pdata = ($projpacks->{$projid}->{'package'} || {})->{$packid};
  if (!$pdata) {
    print "  - $job belongs to an unknown package, discard\n";
    return;
  }
  my $statusdir = "$reporoot/$prp/$myarch/$packid";
  my $status = readxml("$statusdir/status", $BSXML::buildstatus, 1);
  if ($status && (!$status->{'job'} || $status->{'job'} ne $job)) {
    print "  - $job is outdated\n";
    return;
  }
  $status ||= {'readytime' => $info->{'readytime'} || $info->{'starttime'}};
  # calculate exponential weighted average
  my $myjobtime = time() - $status->{'readytime'};
  my $weight = 0.1; 
  $buildavg = ($weight * $myjobtime) + ((1 - $weight) * $buildavg);
  
  delete $status->{'job'};	# no longer building

  delete $status->{'arch'};	# obsolete
  delete $status->{'uri'};	# obsolete

  my $code = $js->{'result'};
  $code = 'failed' unless $code eq 'succeeded' || $code eq 'unchanged';

  my @all = ls($jobdatadir);
  my %all = map {$_ => 1} @all;
  @all = map {"$jobdatadir/$_"} @all;

  my $gdst = "$reporoot/$prp/$myarch";
  my $dst = "$gdst/$packid";
  mkdir_p($dst);
  mkdir_p("$gdst/:meta");
  mkdir_p("$gdst/:logfiles.fail");
  mkdir_p("$gdst/:logfiles.success");
  unlink("$reporoot/$prp/$myarch/:repodone");
  if (!$all{'meta'}) {
    if ($code eq 'succeeded') {
      print "  - $job claims success but there is no meta\n";
      return;
    }
    # severe failure, create src change fake...
    my $verifymd5 = $info->{'verifymd5'} || $info->{'srcmd5'};
    writestr("$jobdatadir/meta", undef, "$verifymd5  $packid\nfake to detect source changes...  fake\n");
    push @all, "$jobdatadir/meta";
    $all{'meta'} = 1;
  }

  # update packstatus so that it doesn't fall back to scheduled
  patchpackstatus($prp, $packid, $code);

  my $meta = $all{'meta'} ? "$jobdatadir/meta" : undef;
  if ($code eq 'unchanged') {
    print "  - $job: build result is unchanged\n";
    if ( -e "$gdst/:logfiles.success/$packid" ){
      # make sure to use the last succeeded logfile matching to these binaries
      link("$gdst/:logfiles.success/$packid", "$dst/logfile.dup");
      rename("$dst/logfile.dup", "$dst/logfile");
      unlink("$dst/logfile.dup");
    }
    if (open(F, '+>>', "$dst/logfile")) {
      # Add a comment to logfile from last real build
      print F "\nRetried build at ".localtime(time())." returned same result, skipped";
      close(F);
    }
    unlink("$gdst/:logfiles.fail/$packid");
    rename($meta, "$gdst/:meta/$packid") if $meta;
    unlink($_) for @all;
    rmdir($jobdatadir);
    addjobhist($prp, $info, $status, $js, 'unchanged');
    $status->{'status'} = 'succeeded';
    writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
    $changed->{$prp} ||= 1;	# package is no longer blocking
    return;
  }
  if ($code eq 'failed') {
    print "  - $job: build failed\n";
    link("$jobdatadir/logfile", "$jobdatadir/logfile.dup");
    rename("$jobdatadir/logfile", "$dst/logfile");
    rename("$jobdatadir/logfile.dup", "$gdst/:logfiles.fail/$packid");
    rename($meta, "$gdst/:meta/$packid") if $meta;
    unlink($_) for @all;
    rmdir($jobdatadir);
    $status->{'status'} = 'failed';
    addjobhist($prp, $info, $status, $js, 'failed');
    writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
    $changed->{$prp} ||= 1;	# package is no longer blocking
    return;
  }
  print "  - $prp: $packid built: ".(@all). " files\n";
  mkdir_p("$gdst/:logfiles.success");
  mkdir_p("$gdst/:logfiles.fail");

  unlink("$jobdatadir/.preinstallimage");
  BSUtil::touch("$jobdatadir/.preinstallimage") if $info->{'file'} eq '_preinstallimage';
  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($prp, $packid, $dst, $jobdatadir, $meta, $useforbuildenabled, $prpsearchpath{$prp}, $fullcache);
  $changed->{$prp} = 2 if $useforbuildenabled;
  delete $repounchanged{$prp} if $useforbuildenabled;
  $repounchanged{$prp} = 2 if $repounchanged{$prp};
  $changed->{$prp} ||= 1;

  # save meta file
  rename($meta, "$gdst/:meta/$packid") if $meta;

  # write new status
  $status->{'status'} = 'succeeded';
  addjobhist($prp, $info, $status, $js, 'succeeded');
  writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);

  # write history file
  my $h = {'versrel' => $info->{'versrel'}, 'bcnt' => $info->{'bcnt'}, 'time' => $now, 'srcmd5' => $info->{'srcmd5'}, 'rev' => $info->{'rev'}, 'reason' => $info->{'reason'}};
  BSFileDB::fdb_add("$reporoot/$prp/$myarch/$packid/history", $historylay, $h);

  # update relsync file (use relsync.merge if relsync is too big)
  if (((-s "$reporoot/$prp/$myarch/:relsync") || 0) < 8192 && ! -e "$reporoot/$prp/$myarch/:relsync.merge") {
    my $relsync = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync", 1) || {};
    $relsync->{$packid} = "$info->{'versrel'}.$info->{'bcnt'}";
    BSUtil::store("$reporoot/$prp/$myarch/.:relsync", "$reporoot/$prp/$myarch/:relsync", $relsync);
  } else {
    my $relsync = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync.merge", 1) || {};
    $relsync->{$packid} = "$info->{'versrel'}.$info->{'bcnt'}";
    BSUtil::store("$reporoot/$prp/$myarch/.:relsync.merge", "$reporoot/$prp/$myarch/:relsync.merge", $relsync);
  }
  
  # save logfile
  link("$jobdatadir/logfile", "$jobdatadir/logfile.dup");
  rename("$jobdatadir/logfile", "$dst/logfile");
  rename("$jobdatadir/logfile.dup", "$gdst/:logfiles.success/$packid");
  unlink("$gdst/:logfiles.fail/$packid");
  unlink($_) for @all;
  rmdir($jobdatadir);
}

sub aggregatefinished {
  my ($ectx, $job, $js) = @_;

  my $changed = $ectx->{'changed_med'};
  my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
  my $jobdatadir = "$myjobsdir/$job:dir";
  if (!$info || ! -d $jobdatadir) {
    print "  - $job is bad\n";
    return;
  }
  my $projid = $info->{'project'};
  my $repoid = $info->{'repository'};
  my $packid = $info->{'package'};
  if ($info->{'arch'} ne $myarch) {
    print "  - $job has bad arch\n";
    return;
  }
  if (!$projpacks->{$projid}) {
    print "  - $job belongs to an unknown project\n";
    return;
  }
  my $pdata = ($projpacks->{$projid}->{'package'} || {})->{$packid};
  if (!$pdata) {
    print "  - $job belongs to an unknown package, discard\n";
    return;
  }
  my $prp = "$projid/$repoid";
  my $gdst = "$reporoot/$prp/$myarch";
  my $dst = "$gdst/$packid";
  mkdir_p($dst);
  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($prp, $packid, $dst, $jobdatadir, undef, $useforbuildenabled, $prpsearchpath{$prp});
  $changed->{$prp} = 2 if $useforbuildenabled;
  delete $repounchanged{$prp} if $useforbuildenabled;
  $repounchanged{$prp} = 2 if $repounchanged{$prp};
  $changed->{$prp} ||= 1;
  unlink("$reporoot/$prp/$myarch/:repodone");
  unlink("$gdst/:logfiles.fail/$packid");
  unlink("$gdst/:logfiles.success/$packid");
  unlink("$dst/logfile");
  unlink("$dst/status");
  mkdir_p("$gdst/:meta");
  rename("$jobdatadir/meta", "$gdst/:meta/$packid") || die("rename $jobdatadir/meta $gdst/:meta/$packid: $!\n");
  patchpackstatus($prp, $packid, 'succeeded');
}

sub deltafinished {
  my ($ectx, $job, $js) = @_;

  my $changed = $ectx->{'changed_med'};
  my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
  my $jobdatadir = "$myjobsdir/$job:dir";
  if (!$info || ! -d $jobdatadir) {
    print "  - $job is bad\n";
    return;
  }
  my $projid = $info->{'project'};
  my $repoid = $info->{'repository'};
  my $packid = $info->{'package'};
  if ($info->{'arch'} ne $myarch) {
    print "  - $job has bad arch\n";
    return;
  }
  if (!$projpacks->{$projid}) {
    print "  - $job belongs to an unknown project\n";
    return;
  }
  my $prp = "$projid/$repoid";
  my $gdst = "$reporoot/$prp/$myarch";
  my $dst = "$gdst/$packid";
  mkdir_p($dst);
  my $code = $js->{'result'} || 'failed';
  my $status = {'readytime' => $info->{'readytime'} || $info->{'starttime'}};
  addjobhist($prp, $info, $status, $js, $code);
  if ($code ne 'succeeded') {
    print "  - $job: build failed\n";
    unlink("$dst/logfile");
    rename("$jobdatadir/logfile", "$dst/logfile");
    unlink("$reporoot/$prp/$myarch/:repodone");
    return;
  }
  my @all = sort(ls($jobdatadir));
  print "  - $prp: $packid built: ".(@all). " files\n";
  for my $f (@all) {
    next unless $f =~ /^(.*)\.(drpm|out|dseq)$/s;
    my $deltaid = $1;
    if ($2 ne 'dseq') {
      rename("$jobdatadir/$f", "$dst/$deltaid");
    } else {
      rename("$jobdatadir/$f", "$dst/$deltaid.dseq");
    }
  }
  $changed->{$prp} ||= 1;
  unlink("$reporoot/$prp/$myarch/:repodone");
  unlink("$dst/logfile");
  rename("$jobdatadir/logfile", "$dst/logfile");
}

sub uploadbuildevent {
  my ($ectx, $job, $js) = @_;

  my $changed = $ectx->{'changed_med'};
  my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
  my $jobdatadir = "$myjobsdir/$job:dir";
  if (!$info || ! -d $jobdatadir) {
    print "  - $job is bad\n";
    return;
  }
  my $projid = $info->{'project'};
  my $repoid = $info->{'repository'};
  my $packid = $info->{'package'};
  if ($info->{'arch'} ne $myarch) {
    print "  - $job has bad arch\n";
    return;
  }
  if (!$projpacks->{$projid}) {
    print "  - $job belongs to an unknown project\n";
    return;
  }
  my $pdata = ($projpacks->{$projid}->{'package'} || {})->{$packid};
  if (!$pdata) {
    print "  - $job belongs to an unknown package, discard\n";
    return;
  }
  my $prp = "$projid/$repoid";
  my $gdst = "$reporoot/$prp/$myarch";
  my $dst = "$gdst/$packid";
  mkdir_p($dst);
  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($prp, $packid, $dst, $jobdatadir, undef, $useforbuildenabled, $prpsearchpath{$prp});
  $changed->{$prp} = 2 if $useforbuildenabled;
  delete $repounchanged{$prp} if $useforbuildenabled;
  $repounchanged{$prp} = 2 if $repounchanged{$prp};
  $changed->{$prp} ||= 1;
  unlink("$reporoot/$prp/$myarch/:repodone");
}

sub importevent {
  my ($ectx, $job, $js) = @_;

  my $changed = $ectx->{'changed_med'};
  my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
  my $jobdatadir = "$myjobsdir/$job:dir";
  if (!$info || ! -d $jobdatadir) {
    print "  - $job is bad\n";
    return;
  }
  my $projid = $info->{'project'};
  my $repoid = $info->{'repository'};
  my $packid = $info->{'package'};
  my $prp = "$projid/$repoid";
  my @all = ls($jobdatadir);
  my %all = map {$_ => 1} @all;
  my $meta = $all{'meta'} ? "$jobdatadir/meta" : undef;
  @all = map {"$jobdatadir/$_"} @all;
  my $pdata = (($projpacks->{$projid} || {})->{'package'} || {})->{$packid};
  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled) if $projpacks->{$projid};
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($prp, $packid, undef, $jobdatadir, $meta, $useforbuildenabled, $prpsearchpath{$prp});
  $changed->{$prp} = 2 if $useforbuildenabled;
  delete $repounchanged{$prp} if $useforbuildenabled;
  unlink($_) for @all;
  rmdir($jobdatadir);
}

##########################################################################
##########################################################################
##
##

sub metacheck {
  my ($ctx, $packid, $packtype, $new_meta, $data) = @_;
  
  my $prp = $ctx->{'prp'};
  my @meta = split("\n", (readstr("$reporoot/$prp/$myarch/:meta/$packid", 1) || ''));
  if (!@meta) {
    print "      - $packid ($packtype)\n";
    print "        start build\n";
    return ('scheduled', [ @$data, {'explain' => 'new build'} ]);
  }
  if ($meta[0] ne $new_meta->[0]) {
    print "      - $packid ($packtype)\n";
    print "        src change, start build\n";
    return ('scheduled', [ @$data, {'explain' => 'source change', 'oldsource' => substr($meta[0], 0, 32)} ]);
  }
  if (@meta == 2 && $meta[1] =~ /^fake/) {
    my @s = stat("$reporoot/$prp/$myarch/:meta/$packid");
    if (!@s || $s[9] + 14400 > time()) {
      print "      - $packid ($packtype)\n";
      print "        buildsystem setup failure\n";
      return ('failed')
    }
    print "      - $packid ($packtype)\n";
    print "        retrying bad build\n";
    return ('scheduled', [ @$data, { 'explain' => 'retrying bad build' } ]);
  }
  if (join('\n', @meta) eq join('\n', @$new_meta)) {
    if (($packtype eq 'kiwi-image' || $packtype eq 'kiwi-product') && $ctx->{'relsynctrigger'}->{$packid}) {
      print "      - $packid ($packtype)\n";
      print "        rebuild counter sync\n";
      return ('scheduled', [ @$data, {'explain' => 'rebuild counter sync'} ]);
    }
    #print "      - $packid ($packtype)\n";
    #print "        nothing changed\n";
    return ('done');
  }
  my $repo = $ctx->{'repo'};
  if (($packtype eq 'kiwi-image' || $packtype eq 'kiwi-product') && $repo->{'rebuild'} && $repo->{'rebuild'} eq 'local') {
    #print "      - $packid ($packtype)\n";
    #print "        nothing changed\n";
    return ('done');
  }
  my @diff = diffsortedmd5(\@meta, $new_meta);
  print "      - $packid ($packtype)\n";
  print "        $_\n" for @diff;
  print "        meta change, start build\n";
  return ('scheduled', [ @$data, {'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff)} ]);
}


##########################################################################
##########################################################################
##
##  kiwi-image package type handling
##
sub checkkiwiimage {
  my ($ctx, $packid, $pdata, $info) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $prp = $ctx->{'prp'};
  my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
  my $repo = $ctx->{'repo'};

  # get config from path
  my $bconf = getconfig($myarch, \@aprps);
  if (!$bconf) {
    print "      - $packid (kiwi-image)\n";
    print "        no config\n";
    return ('broken', 'no config');
  }

  my $pool = BSSolv::pool->new();
  $pool->settype('deb') if $bconf->{'binarytype'} eq 'deb';

  for my $aprp (@aprps) {
    if (!checkprpaccess($aprp, $prp)) {
      print "      - $packid (kiwi-image)\n";
      print "        repository $aprp is unavailable";
      return ('broken', "repository $aprp is unavailable");
    }
    my $r = addrepo($ctx, $pool, $aprp);
    if (!$r) {
      my $error = "repository '$aprp' is unavailable";
      $error .= " (delayed)" if defined $r;
      print "      - $packid (kiwi-image)\n";
      print "        $error\n";
      return ('delayed', $error) if defined $r;
      return ('broken', $error);
    }
  }
  $pool->createwhatprovides();
  my $bconfignore = $bconf->{'ignore'};
  my $bconfignoreh = $bconf->{'ignoreh'};
  delete $bconf->{'ignore'};
  delete $bconf->{'ignoreh'};
  my @deps = @{$info->{'dep'} || []};
  my $xp = BSSolv::expander->new($pool, $bconf);
  my $ownexpand = sub {
    $_[0] = $xp; 
    goto &BSSolv::expander::expand;
  };   
  no warnings 'redefine';
  local *Build::expand = $ownexpand;
  use warnings 'redefine';
  my ($eok, @edeps) = Build::get_deps($bconf, [], @deps);
  if (!$eok) {
    print "      - $packid (kiwi-image)\n";
    print "        unresolvables:\n";
    print "            $_\n" for @edeps;
    return ('unresolvable', join(', ', @edeps));
  }
  $bconf->{'ignore'} = $bconfignore if $bconfignore;
  $bconf->{'ignoreh'} = $bconfignoreh if $bconfignoreh;

  my @new_meta;
  push @new_meta, ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})."  $packid";
  for (@{$info->{'extrasource'} || []}) {
    push @new_meta, "$_->{'srcmd5'}  $_->{'project'}/$_->{'package'}";
  }

  my $notready = $ctx->{'notready'};
  my $prpnotready = $ctx->{'prpnotready'};
  my @blocked;
  for my $arepo ($pool->repos()) {
    my $aprp = $arepo->name();
    if (!$repo->{'block'} || $repo->{'block'} ne 'never') {
      my $nr = ($prp eq $aprp ? $notready : $prpnotready->{$aprp}) || {};
      my @b = grep {$nr->{$_}} @edeps;
      if (@b) {
	@b = map {"$aprp/$_"} @b if $prp ne $aprp;
	push @blocked, @b;
      }
      next if @blocked;
    }
    my %names = $arepo->pkgnames();
    for my $dep (sort(@edeps)) {
      my $p = $names{$dep};
      next unless $p;
      push @new_meta, $pool->pkg2pkgid($p)."  $aprp/$dep";
    }
  }
  if (@blocked) {
    print "      - $packid (kiwi-image)\n";
    print "        blocked (@blocked)\n";
    return ('blocked', join(', ', @blocked));
  }
  return metacheck($ctx, $packid, 'kiwi-image', \@new_meta, [ $bconf, \@edeps ]);
}

sub rebuildkiwiimage {
  my ($ctx, $packid, $pdata, $info, $data) = @_;
  my $bconf = $data->[0];
  my $edeps = $data->[1];
  my $reason = $data->[2];

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $repo = $ctx->{'repo'};

  my ($job, $joberror);
  if (!@{$repo->{'path'} || []}) {
    # repo has no path, use kiwi repositories also for kiwi system setup
    my $prp = "$projid/$repoid";
    my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
    # setup pool again for kiwi system expansion
    my $pool = BSSolv::pool->new();
    $pool->settype('deb') if $bconf->{'binarytype'} eq 'deb';
    for my $aprp (@aprps) {
      if (!checkprpaccess($aprp, $prp)) {
	print "      - $packid (kiwi-image)\n";
	print "        repository $aprp is unavailable";
	return ('broken', "repository $aprp is unavailable");
      }
      my $r = addrepo($ctx, $pool, $aprp);
      if (!$r) {
	my $error = "repository '$aprp' is unavailable";
	$error .= " (delayed)" if defined $r;
        print "      - $packid (kiwi-image)\n";
        print "        $error\n";
        return ('delayed', $error) if defined $r;
        return ('broken', $error);
      }
    }
    $pool->createwhatprovides();
    my $xp = BSSolv::expander->new($pool, $bconf);
    my $ownexpand = sub {
      $_[0] = $xp; 
      goto &BSSolv::expander::expand;
    };   
    no warnings 'redefine';
    local *Build::expand = $ownexpand;
    use warnings 'redefine';
    ($job, $joberror) = set_building($ctx->{'project'}, $ctx->{'repository'}, $packid, $pdata, $info, $bconf, [], $edeps, undef, $reason, $ctx->{'relsyncmax'}, 0);
  } else {
    # repo has a configured path, expand kiwi system with it
    my $prp = "$projid/$repoid";
    return ('broken', 'no config') unless $bconf;	# should not happen
    ($job, $joberror) = set_building($ctx->{'project'}, $ctx->{'repository'}, $packid, $pdata, $info, $ctx->{'conf'}, [], $edeps, $ctx->{'prpsearchpath'} || [], $reason, $ctx->{'relsyncmax'}, 0);
  }
  if ($job) {
    return ('scheduled', $job);
  } else {
    return ('broken', $joberror);
  }
}

##########################################################################
##########################################################################
##
##  kiwi-product package type handling
##
my %bininfo_oldok_cache;

sub read_bininfo_oldok {
  my ($dir) = @_;
  my @s = stat("$dir/.bininfo");
  if (@s) {
    my $bininfo = BSUtil::retrieve("$dir/.bininfo", 1);
    if ($bininfo) {
      $bininfo->{'.bininfo'} = {'id' => "$s[9]/$s[7]/$s[1]"};
      return $bininfo;
    }
    # check the old format cache
    $bininfo = $bininfo_oldok_cache{$dir};
    return $bininfo if $bininfo && $bininfo->{'.bininfo'}->{'id'} eq "$s[9]/$s[7]/$s[1]";
    local *F;
    if (open(F, '<', "$dir/.bininfo")) {
      $bininfo = {};
      while (<F>) {
	chomp;
        if (length($_) <= 34 || substr($_, 32, 2) ne '  ') {
	  # seems to be a corrupt file
	  undef $bininfo;
	  last;
	}
	my $file = substr($_, 34);
	next unless $file =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
	$bininfo->{$file} = {'filename' => $file, 'hdrmd5' => substr($_, 0, 32), 'name' => $1, 'arch' => $2};
      }
      close(F);
      if ($bininfo) {
        $bininfo->{'.bininfo'} = {'id' => "$s[9]/$s[7]/$s[1]"};
        $bininfo_oldok_cache{$dir} = $bininfo;
        return $bininfo;
      }
    }
  }
  my $bininfo = {};
  for my $file (ls($dir)) {
    next unless $file =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
    $bininfo->{$file} = {'filename' => $file, 'name' => $1, 'arch' => $2};
  }
  return $bininfo;
}

sub checkkiwiproduct {
  my ($ctx, $packid, $pdata, $info) = @_;

  # hmm, should get the arch from the kiwi info
  # but how can we map it to the buildarchs?
  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $repo = $ctx->{'repo'};
  my $prp = "$projid/$repoid";

  # calculate all involved architectures
  my %imagearch = map {$_ => 1} @{$info->{'imagearch'} || []};
  return ('broken', 'no architectures for packages') unless grep {$imagearch{$_}} @{$repo->{'arch'} || []};
  $imagearch{'local'} = 1 if $BSConfig::localarch;
  my @archs = grep {$imagearch{$_}} @{$repo->{'arch'} || []};
  
  if (!grep {$_ eq $myarch} @archs) {
    print "      - $packid (kiwi-product)\n";
    print "        not mine\n";
    return ('excluded');
  }

  my @deps = @{$info->{'dep'} || []};	# expanded?
  my %deps = map {$_ => 1} @deps;
  delete $deps{''};

  my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
  my @bprps = @{$ctx->{'prpsearchpath'}};
  my $bconf = $ctx->{'conf'};

  if (!@{$repo->{'path'} || []}) {
    # have no configured path, use repos from kiwi file instead
    @bprps = @aprps;
    $bconf = getconfig($myarch, \@bprps);
    if (!$bconf) {
      print "      - $packid (kiwi-product)\n";
      print "        no config\n";
      return ('broken', 'no config');
    }
  }

  my @blocked;
  my @rpms;
  my %rpms_meta;
  my %rpms_hdrmd5;

#print "prps: @aprps\n";
#print "archs: @archs\n";
#print "deps: @deps\n";
  if ($archs[0] eq $myarch) {
    # calculate packages needed for building
    my @kdeps = @{$bconf->{'substitute'}->{'kiwi-setup:product'} || []};
    @kdeps = ('kiwi') unless @kdeps;	# default
    push @kdeps, grep {/^kiwi-.*:/} @{$info->{'dep'} || []};
    my $pool = BSSolv::pool->new();
    $pool->settype('deb') if $bconf->{'binarytype'} eq 'deb';

    for my $aprp (@bprps) {
      if (!checkprpaccess($aprp, $prp)) {
	print "      - $packid (kiwi-product)\n";
	print "        repository $aprp is unavailable";
	return ('broken', "repository $aprp is unavailable");
      }
      my $arch = $myarch eq 'local' && $BSConfig::localarch ? $BSConfig::localarch : $myarch;
      my $r = $arch eq $myarch ? addrepo($ctx, $pool, $aprp) : addrepo_alien($ctx, $pool, $aprp, $arch);
      if (!$r) {
	my $error = "repository '$aprp' is unavailable";
	$error .= " (delayed)" if defined $r;
	print "      - $packid (kiwi-product)\n";
	print "        $error\n";
	return ('delayed', $error) if defined $r;
	return ('broken', $error);
      }
    }
    $pool->createwhatprovides();
    my $xp = BSSolv::expander->new($pool, $bconf);
    my $ownexpand = sub {
      $_[0] = $xp; 
      goto &BSSolv::expander::expand;
    };   
    no warnings 'redefine';
    local *Build::expand = $ownexpand;
    use warnings 'redefine';
    my $eok;
    ($eok, @kdeps) = Build::get_build($bconf, [], @kdeps);
    if (!$eok) {
      print "      - $packid (kiwi-product)\n";
      print "        unresolvables:\n";
      print "          $_\n" for @kdeps;
      return ('unresolvable', join(', ', @kdeps));
    }
    my %dep2pkg;
    for my $p ($pool->consideredpackages()) {
      $dep2pkg{$pool->pkg2name($p)} = $p;
    }
    # check access
    for my $aprp (@aprps) {
      if (!checkprpaccess($aprp, $prp)) {
	print "      - $packid (kiwi-product)\n";
	print "        repository $aprp is unavailable";
	return ('broken', "repository $aprp is unavailable");
      }
    }
    # check if we are blocked
    if ($myarch eq 'local' && $BSConfig::localarch) {
      my %used;
      for my $bin (@kdeps) {
        my $p = $dep2pkg{$bin};
        my $aprp = $pool->pkg2reponame($p);
        my $pname = $pool->pkg2srcname($p);
        push @{$used{$aprp}}, $pname;
      }
      for my $aprp (@aprps) {
        my %pnames = map {$_ => 1} @{$used{$aprp}};
	next unless %pnames;
	# FIXME: does not work for remote repos
	my $ps = BSUtil::retrieve("$reporoot/$aprp/$BSConfig::localarch/:packstatus", 1);
	if (!$ps) {
	  $ps = (readxml("$reporoot/$aprp/$BSConfig::localarch/:packstatus", $BSXML::packstatuslist, 1) || {})->{'packstatus'} || [];
	  $ps = { 'packstatus' => { map {$_->{'name'} => $_->{'status'}} @$ps } };
	}
        $ps = ($ps || {})->{'packstatus'} || {};
	# FIXME: this assumes packid == pname
	push @blocked, grep {$ps->{$_} && ($ps->{$_} eq 'scheduled' || $ps->{$_} eq 'blocked' || $ps->{$_} eq 'finished')} sort keys %pnames;
      }
      if (@blocked) {
	if (! -e "$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$BSConfig::localarch") {
          mkdir_p("$reporoot/$projid/$repoid/$myarch/$packid");
          BSUtil::touch("$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$BSConfig::localarch");
	}
      } else {
	unlink("$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$BSConfig::localarch");
      }
    } else {
      my $notready = $ctx->{'notready'};
      my $prpnotready = $ctx->{'prpnotready'};
      for my $bin (@kdeps) {
        my $p = $dep2pkg{$bin};
        my $aprp = $pool->pkg2reponame($p);
        my $pname = $pool->pkg2srcname($p);
        my $nr = ($prp eq $aprp ? $notready : $prpnotready->{$aprp}) || {};
        push @blocked, $bin if $nr->{$pname};
      }
    }
    if (@blocked) {
      print "      - $packid (kiwi-product)\n";
      print "        blocked (@blocked)\n";
      return ('blocked', join(', ', @blocked));
    }
    push @rpms, @kdeps;
  }

  my $allpacks = $deps{'*'} ? 1 : 0;

  my $maxblocked = 20;
  my %blockedarch;
  for my $aprp (@aprps) {
    my %known;
    my ($aprojid, $arepoid) = split('/', $aprp, 2);
    my $pdatas = ($projpacks->{$aprojid} || {})->{'package'} || {};
    my @apackids = sort keys %$pdatas;
    for my $apackid (@apackids) {
      my $info = (grep {$_->{'repository'} eq $arepoid} @{$pdatas->{$apackid}->{'info'} || []})[0];
      $known{$apackid} = $info->{'name'} if $info && $info->{'name'};
    }
    for my $arch ($archs[0] eq $myarch ? @archs : $myarch) {
      my $ps = BSUtil::retrieve("$reporoot/$aprp/$arch/:packstatus", 1);
      if (!$ps) {
	$ps = (readxml("$reporoot/$aprp/$arch/:packstatus", $BSXML::packstatuslist, 1) || {})->{'packstatus'} || [];
	$ps = { 'packstatus' => { map {$_->{'name'} => $_->{'status'}} @$ps } };
      }
      $ps = ($ps || {})->{'packstatus'} || {};

      my $gbininfo;
      if ($remoteprojs{$aprojid}) {
	if ($arch eq 'local' && $BSConfig::localarch) {
	  $gbininfo = {};
	} else {
	  $gbininfo = read_gbininfo_remote($ctx, "$aprp/$arch", $remoteprojs{$aprojid}, $ps);
	}
	if (!$gbininfo) {
	  my $error = "project binary state of $aprp is unavailable";
	  $error .= " (delayed)" if defined $gbininfo;
	  print "      - $packid (kiwi-product)\n";
	  print "        $error\n";
	  return ('delayed', $error) if defined $gbininfo;
	  return ('broken', $error);
	}
	@apackids = unify(@apackids, sort keys %$gbininfo);
      } else {
        $gbininfo = read_gbininfo("$reporoot/$aprp/$arch", $arch eq $myarch ? 0 : 1);
      }
      if (!$gbininfo && -d "$eventdir/$arch") {
        print "    requesting :repoinfo for $aprp/$arch\n";
        # tell other scheduler that it is missing
	my $ev = {
	  'type' => 'unblocked',
	  'project' => $aprojid,
	  'repository' => $arepoid,
	};
	sendevent($ev, $arch, "unblocked::${projid}::${repoid}");
      }

      for my $apackid (@apackids) {
        if (($allpacks && !$deps{"-$apackid"} && !$deps{'-'.($known{$apackid} || '')}) || $deps{$apackid} || $deps{$known{$apackid} || ''}) {
	  # hey, we probably need this package! wait till it's finished
	  my $code = $ps->{$apackid} || 'unknown';
	  if ($code eq 'scheduled' || $code eq 'blocked' || $code eq 'finished') {
	    push @blocked, "$aprp/$arch/$apackid";
	    $blockedarch{$arch} = 1;
	    last if @blocked > $maxblocked;
	    next;
	  }
        }

        # hmm, we don't know if we really need it. check bininfo.
	my $bininfo;
	if ($gbininfo) {
	  $bininfo = $gbininfo->{$apackid} || {};
	} else {
	  $bininfo = read_bininfo_oldok("$reporoot/$aprp/$arch/$apackid");
	}
	my @got;
	my $needit;
	for my $b (values %$bininfo) {
	  next unless $b->{'filename'} && $b->{'filename'} =~ /\.rpm$/;
	  $needit = 1 if $deps{$b->{'name'}} || ($allpacks && !$deps{"-$b->{'name'}"});
	  push @got, "$aprp/$arch/$apackid/$b->{'filename'}";
	  $rpms_hdrmd5{$got[-1]} = $b->{'hdrmd5'} if $b->{'hdrmd5'};
	  $rpms_meta{$got[-1]} = "$aprp/$arch/$apackid/$b->{'name'}.$b->{'arch'}";
        }
	next unless $needit;
	# ok we need it. check if the package is built.
	my $code = $ps->{$apackid} || 'unknown';
	if ($code eq 'scheduled' || $code eq 'blocked' || $code eq 'finished') {
	  push @blocked, "$aprp/$arch/$apackid";
	  $blockedarch{$arch} = 1;
	  last if @blocked > $maxblocked;
	  next;
        }
	push @rpms, @got;
      }
      last if @blocked > $maxblocked;
    }
    last if @blocked > $maxblocked;
  }
  if ($archs[0] eq $myarch) {
    for my $arch (grep {$_ ne $myarch} @archs) {
      if ($blockedarch{$arch}) {
	next if -e "$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$arch";
        mkdir_p("$reporoot/$projid/$repoid/$myarch/$packid");
        BSUtil::touch("$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$arch");
      } else {
	unlink("$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$arch");
      }
    }
  }
  if (@blocked) {
    push @blocked, '...' if @blocked > $maxblocked;
    print "      - $packid (kiwi-product)\n";
    print "        blocked (@blocked)\n";
    return ('blocked', join(', ', @blocked));
  }

  if ($archs[0] ne $myarch) {
    # looks good from our side. tell master arch
    # to check it
    if (-e "$reporoot/$projid/$repoid/$archs[0]/$packid/.waiting_for_$myarch") {
      unlink("$reporoot/$projid/$repoid/$archs[0]/$packid/.waiting_for_$myarch");
      my $ev = {
        'type' => 'unblocked',
        'project' => $projid,
        'repository' => $repoid,
      };
      sendevent($ev, $archs[0], "unblocked::${projid}::${repoid}");
      print "      - $packid (kiwi-product)\n";
      print "        unblocked\n";
    }
    return ('excluded');
  }

  # now create meta info
  my @new_meta;
  push @new_meta, ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})."  $packid";
  push @new_meta, map {"$_->{'srcmd5'}  $_->{'project'}/$_->{'package'}"} @{$info->{'extrasource'} || []};
  for my $rpm (sort {$rpms_meta{$a} cmp $rpms_meta{$b} || $a cmp $b} grep {$rpms_meta{$_}} @rpms) {
    my $id = $rpms_hdrmd5{$rpm};
    eval { $id ||= Build::queryhdrmd5("$reporoot/$rpm"); };
    $id ||= "deaddeaddeaddeaddeaddeaddeaddead";
    push @new_meta, "$id  $rpms_meta{$rpm}";
  }
  return metacheck($ctx, $packid, 'kiwi-product', \@new_meta, [ $bconf, \@rpms ]);
}

sub rebuildkiwiproduct {
  my ($ctx, $packid, $pdata, $info, $data) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $repo = $ctx->{'repo'};
  my $relsyncmax = $ctx->{'relsyncmax'};

  my $bconf = $data->[0];
  my $rpms = $data->[1];
  my $reason = $data->[2];
  my $prp = "$projid/$repoid";
  my $srcmd5 = $pdata->{'srcmd5'};
  my $job = jobname($prp, $packid);
  return ('scheduled', "$job-$srcmd5") if -s "$myjobsdir/$job-$srcmd5";
  my @otherjobs = grep {/^\Q$job\E-[0-9a-f]{32}$/} ls($myjobsdir);
  $job = "$job-$srcmd5";

  # kill those ancient other jobs
  for my $otherjob (@otherjobs) {
    print "        killing old job $otherjob\n";
    killjob($otherjob);
  }

  my $now = time(); # ensure that we use the same time in all logs

  my $syspath;
  if (@{$repo->{'path'} || []}) {
    # images repo has a configured path, use it to set up the kiwi system
    $syspath = [];
    for (@{$prpsearchpath{$prp}}) {
      my @pr = split('/', $_, 2);
      if ($remoteprojs{$pr[0]}) {
        push @$syspath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
      } else {
        push @$syspath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
      }
    }
  }
  my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
  my $searchpath = [];
  for (@aprps) {
    my @pr = split('/', $_, 2);
    if ($remoteprojs{$pr[0]}) {
      push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
    } else {
      push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
    }
  }

  my @bdeps;
  my @pdeps = Build::get_preinstalls($bconf);
  my @vmdeps = Build::get_vminstalls($bconf);
  my %runscripts = map {$_ => 1} Build::get_runscripts($bconf);
  my %pdeps = map {$_ => 1} @pdeps;
  my %vmdeps = map {$_ => 1} @vmdeps;
  for my $rpm (unify(@pdeps, @vmdeps, @{$rpms || []})) {
    my @b = split('/', $rpm);
    if (@b == 1) {
      push @bdeps, { 'name' => $rpm, 'notmeta' => 1, };
      $bdeps[-1]->{'preinstall'} = 1 if $pdeps{$rpm};
      $bdeps[-1]->{'vminstall'} = 1 if $vmdeps{$rpm};
      $bdeps[-1]->{'repoarch'} = $BSConfig::localarch if $myarch eq 'local' && $BSConfig::localarch;
      next;
    }
    next unless @b == 5;
    next unless $b[4] =~ /^(.+)-([^-]+)-([^-]+)\.([a-zA-Z][^\.\-]*)\.rpm$/;
    push @bdeps, {
      'name' => $1,
      'version' => $2,
      'release' => $3,
      'arch' => $4,
      'project' => $b[0],
      'repository' => $b[1],
      'repoarch' => $b[2],
      'package' => $b[3],
    };
  }
  if ($info->{'extrasource'}) {
    push @bdeps, map {{
      'name' => $_->{'file'}, 'version' => '', 'repoarch' => 'src',
      'project' => $_->{'project'}, 'package' => $_->{'package'}, 'srcmd5' => $_->{'srcmd5'},
    }} @{$info->{'extrasource'}};
  }

  # find the last build count we used for this version/release
  mkdir_p("$reporoot/$prp/$myarch/$packid");
  my $h = BSFileDB::fdb_getmatch("$reporoot/$prp/$myarch/$packid/history", $historylay, 'versrel', defined($pdata->{'versrel'}) ? $pdata->{'versrel'} : '', 1);
  $h = {'bcnt' => 0} unless $h;

  # max with sync data
  my $tag = $pdata->{'bcntsynctag'} || $packid;
  if ($relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
    if ($h->{'bcnt'} + 1 < $relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
      $h->{'bcnt'} = $relsyncmax->{"$tag/$pdata->{'versrel'}"} - 1;
    }
  }

  my $binfo = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => $packid,
    'srcserver' => $BSConfig::srcserver,
    'reposerver' => $BSConfig::reposerver,
    'job' => $job,
    'arch' => $myarch,
    'srcmd5' => $srcmd5,
    'verifymd5' => $pdata->{'verifymd5'} || $srcmd5,
    'rev' => $pdata->{'rev'},
    'file' => $info->{'file'},
    'versrel' => $pdata->{'versrel'},
    'bcnt' => $h->{'bcnt'} + 1,
    'bdep' => \@bdeps,
    'path' => $searchpath,
    'reason' => $reason->{'explain'},
    'readytime' => $now,
  };
  $binfo->{'syspath'} = $syspath if $syspath;
  $binfo->{'hostarch'} = $bconf->{'hostarch'} if $bconf->{'hostarch'};
  $binfo->{'revtime'} = $pdata->{'revtime'} if $pdata->{'revtime'};
  $binfo->{'imagetype'} = $info->{'imagetype'} if $info->{'imagetype'};
  mkdir_p("$reporoot/$prp/$myarch/$packid");
  writexml("$reporoot/$prp/$myarch/$packid/.status", "$reporoot/$prp/$myarch/$packid/status", { 'status' => 'scheduled', 'readytime' => $now, 'job' => $job}, $BSXML::buildstatus);
  $reason->{'time'} = $now;
  writexml("$reporoot/$prp/$myarch/$packid/.reason", "$reporoot/$prp/$myarch/$packid/reason", $reason, $BSXML::buildreason);
  writexml("$myjobsdir/.$job", "$myjobsdir/$job", $binfo, $BSXML::buildinfo);
  $ourjobs{$job} = 1;
  return ('scheduled', $job);
}

##########################################################################
##########################################################################
##
##  patchinfo package type handling
##
sub checkpatchinfo {
  my ($ctx, $packid, $pdata, $info) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $repo = $ctx->{'repo'};
  my @archs = @{$repo->{'arch'}};
  return ('broken', 'missing archs') unless @archs;	# can't happen
  my $patchinfo = $pdata->{'patchinfo'};

  if (@{$patchinfo->{'releasetarget'} || []}) {
    my $ok;
    for my $rt (@{$patchinfo->{'releasetarget'}}) {
      $ok = grep {$rt->{'project'} eq $_->{'project'} && (!defined($rt->{'repository'}) || $rt->{'repository'} eq $_->{'repository'})} @{$repo->{'releasetarget'} || []};
      last if $ok;
    }
    return ('excluded') unless $ok;
  }

  return ('broken', "patchinfo is stopped: ".$patchinfo->{'stopped'}) if $patchinfo->{'stopped'};
  return ('broken', 'patchinfo lacks category') unless $patchinfo->{'category'};

  my $ptype = 'local';
  $ptype = 'binary' if ($projpacks->{$projid}->{'kind'} || '') eq 'maintenance_incident';
  
  my $broken;
  # find packages
  my @packages;
  if ($patchinfo->{'package'}) {
    @packages = @{$patchinfo->{'package'}};
    my $pdatas = ($projpacks->{$projid} || {})->{'package'} || {};
    my @missing;
    for my $apackid (@packages) {
      if (!$pdatas->{$apackid}) {
	push @missing, $_;
      }
    }
    $broken = 'missing packages: '.join(', ', @missing) if @missing;
  } else {
    my $pdatas = ($projpacks->{$projid} || {})->{'package'} || {};
    @packages = grep {!$pdatas->{$_}->{'aggregatelist'} && !$pdatas->{$_}->{'patchinfo'}} sort keys %$pdatas;
  }
  if (!@packages && !$broken) {
    $broken = 'no packages found';
  }

  if ($archs[0] ne $myarch) {
    # XXX wipe just in case! remove when we do that elsewhere...
    if (-d "$reporoot/$projid/$repoid/$myarch/$packid") {
      # (patchinfo packages will not be in :full)
      unlink("$reporoot/$projid/$repoid/$myarch/:meta/$packid");
      unlink("$reporoot/$projid/$repoid/$myarch/:logfiles.fail/$packid");
      unlink("$reporoot/$projid/$repoid/$myarch/:logfiles.success/$packid");
      unlink("$reporoot/$projid/$repoid/$myarch/:logfiles.success/$packid");
      BSUtil::cleandir("$reporoot/$projid/$repoid/$myarch/$packid");
      rmdir("$reporoot/$projid/$repoid/$myarch/$packid");
    }
    # check if we go from blocked to unblocked
    my $blocked;
    my $packstatus = $ctx->{'packstatus'};
    for my $apackid (@packages) {
      my $code = $packstatus->{$apackid} || '';
      if ($code eq 'excluded') {
	next;
      }
      if ($code ne 'done' && $code ne 'disabled') {
	$blocked = 1;
        last;
      }
      if (-e "$reporoot/$projid/$repoid/$myarch/:logfiles.fail/$apackid") {
	$blocked = 1;
        last;
      }
      if (! -e "$reporoot/$projid/$repoid/$myarch/:logfiles.success/$apackid") {
	next if $code eq 'disabled';
	$blocked = 1;
        last;
      }
    }
    if (!$blocked) {
      if (-e "$reporoot/$projid/$repoid/$archs[0]/$packid/.waiting_for_$myarch") {
        unlink("$reporoot/$projid/$repoid/$archs[0]/$packid/.waiting_for_$myarch");
        my $ev = {
          'type' => 'unblocked',
          'project' => $projid,
          'repository' => $repoid,
        };
        sendevent($ev, $archs[0], "unblocked::${projid}::${repoid}");
        print "      - $packid (patchinfo)\n";
        print "        unblocked\n";
      }
    }
    return ('excluded');
  }

  return ('broken', $broken) if $broken;

  my @new_meta;
  push @new_meta, ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})."  $packid";

  if ($ptype eq 'local') {
    # only rebuild if patchinfo source changes
    my @meta;
    if (open(F, '<', "$reporoot/$projid/$repoid/$myarch/:meta/$packid")) {
      @meta = <F>;
      close F;
      chomp @meta;
    }
    if (@meta == 1 && $meta[0] eq $new_meta[0]) {
      print "      - $packid (patchinfo)\n";
      print "        nothing changed\n";
      return ('done');
    }
  }

  # collect em
  my $apackstatus;
  my @blocked;
  my @tocopy;
  my %metas;
  for my $arch (@archs) {
    if ($arch eq $myarch) {
      $apackstatus = $ctx->{'packstatus'};
    } else {
      my $ps = BSUtil::retrieve("$reporoot/$projid/$repoid/$arch/:packstatus", 1);
      if (!$ps) {
	$ps = (readxml("$reporoot/$projid/$repoid/$arch/:packstatus", $BSXML::packstatuslist, 1) || {})->{'packstatus'} || [];
	$ps = { 'packstatus' => { map {$_->{'name'} => $_->{'status'}} @$ps } } if $ps;
      }
      $apackstatus = ($ps || {})->{'packstatus'} || {};
    }
    my $blockedarch;
    for my $apackid (@packages) {
      my $code = $apackstatus->{$apackid} || '';
      if ($code eq 'excluded') {
	next;
      }
      if ($code ne 'done' && $code ne 'disabled') {
	$blockedarch = 1;
	push @blocked, "$arch/$apackid";
        next;
      }
      if (-e "$reporoot/$projid/$repoid/$arch/:logfiles.fail/$apackid") {
	push @blocked, "$arch/$apackid";
	$blockedarch = 1;
      }
      if (! -e "$reporoot/$projid/$repoid/$arch/:logfiles.success/$apackid") {
	next if $code eq 'disabled';
	push @blocked, "$arch/$apackid";
	$blockedarch = 1;
      }
      if ($ptype eq 'binary') {
	# like aggregates
	my $d = "$reporoot/$projid/$repoid/$arch/$apackid";
	my @d = grep {/\.rpm/} ls($d);
	my $m = '';
	for my $b (sort @d) {
	  my @s = stat("$d/$b");
	  $m .= "$b\0$s[9]/$s[7]/$s[1]\0" if @s;
	}
	$metas{"$arch/$apackid"} = Digest::MD5::md5_hex($m);
      } elsif ($ptype eq 'direct' || $ptype eq 'transitive') {
	my ($ameta) = split("\n", readstr("$reporoot/$projid/$repoid/$arch/:meta/$apackid", 1) || '', 2);
	if (!$ameta) {
	  push @blocked, "$arch/$apackid";
	  $blockedarch = 1;
	} else {
	  if ($metas{$apackid} && $metas{$apackid} ne $ameta) {
	    push @blocked, "meta/$apackid";
	    $blockedarch = 1;
	  } else {
	    $metas{$apackid} = $ameta;
	  }
	}
      }
      push @tocopy, "$arch/$apackid";
    }
    if ($blockedarch && $arch ne $myarch) {
      mkdir_p("$reporoot/$projid/$repoid/$myarch/$packid");
      BSUtil::touch("$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$arch") unless -e "$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$arch";
    } else {
      unlink("$reporoot/$projid/$repoid/$myarch/$packid/.waiting_for_$arch");
    }
  }

  if (@blocked) {
    print "      - $packid (patchinfo)\n";
    print "        blocked (@blocked)\n";
    return ('blocked', join(', ', @blocked));
  }

  return ('broken', 'no binaries found') unless @tocopy;

  for (sort(keys %metas)) {
    push @new_meta, "$metas{$_}  $_";
  }

  # compare with stored meta
  my @meta;
  if (open(F, '<', "$reporoot/$projid/$repoid/$myarch/:meta/$packid")) {
    @meta = <F>;
    close F;
    chomp @meta;
  }
  if (@meta == @new_meta && join("\n", @meta) eq join("\n", @new_meta)) {
    print "      - $packid (patchinfo)\n";
    print "        nothing changed\n";
    return ('done');
  }

  # now collect...
  return ('scheduled', [ \@tocopy, \%metas, $ptype]);
}

sub rebuildpatchinfo {
  my ($ctx, $packid, $pdata, $info, $data) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my @tocopy = @{$data->[0]};
  my $ckmetas = $data->[1];
  my $ptype = $data->[2];
  
  print "      - $packid (patchinfo)\n";
  print "        rebuilding\n";
  my $now = time();
  my $prp = "$projid/$repoid";
  my $job = jobname($prp, $packid);
  return ('scheduled', $job) if -s "$myjobsdir/$job";

  my $patchinfo = $pdata->{'patchinfo'};
  my $jobdatadir = "$myjobsdir/$job:dir";
  unlink "$jobdatadir/$_" for ls($jobdatadir);
  mkdir_p($jobdatadir);
  my $jobrepo = {};
  my $error;
  my %donebins;
  my @upackages;
  my $broken;
  my %metas;
  my $bininfo = {};
  my $updateinfodata;
  my %updateinfodata_tocopy;
  my %binaryfilter = map {$_ => 1} @{$patchinfo->{'binary'} || []};
  my %filtered;
  
  if (-s "$reporoot/$prp/$myarch/$packid/.updateinfodata") {
    $updateinfodata = BSUtil::retrieve("$reporoot/$prp/$myarch/$packid/.updateinfodata");
    %updateinfodata_tocopy = map {$_ => 1} @{$updateinfodata->{'packages'} || []};
  }

  for my $tocopy (@tocopy) {
    my ($arch, $apackid) = split('/', $tocopy, 2);
    my @bins;
    my $meta;
    my $from;
    my $mpackid;

    if ($ptype eq 'local') {
      # always reuse old packages
    } elsif ($ptype eq 'binary') {
      $mpackid = "$arch/$apackid";
    } elsif ($ptype eq 'direct' || $ptype eq 'transitive') {
      $mpackid = $apackid;
    } else {
      $broken = "illegal ptype";
      last;
    }
    if ($updateinfodata->{'filtered'} && $updateinfodata->{'filtered'}->{$tocopy}) {
      # we previously filtered packages, check if this is still true
      if (grep {!%binaryfilter || $binaryfilter{$_}} keys %{$updateinfodata->{'filtered'}->{$tocopy}}) {
	# can't reuse old packages, as the filter changed
	delete $updateinfodata_tocopy{$tocopy};
      }
    }
    if ($updateinfodata_tocopy{$tocopy} && (!defined($mpackid) || ($updateinfodata->{'metas'}->{$mpackid} || '') eq $ckmetas->{$mpackid})) {
      print "        reusing old packages for '$tocopy'\n";
      $from = "$reporoot/$projid/$repoid/$myarch/$packid";
      @bins = grep {$updateinfodata->{'binaryorigins'}->{$_} eq $tocopy} keys(%{$updateinfodata->{'binaryorigins'}});
    } else {
      $from = "$reporoot/$projid/$repoid/$tocopy";
      @bins = grep {/\.rpm$/} ls ($from);
    }
    if (defined($mpackid)) {
      my $meta = $ckmetas->{$mpackid};
      if (!$meta) {
	$broken = "$tocopy has no meta";
	last;
      }
      $metas{$mpackid} ||= $meta;
      if ($metas{$mpackid} ne $meta) {
	$broken = "$mpackid has different sources";
	last;
      }
    }
    my $m = '';
    for my $bin (sort @bins) {
      if ($donebins{$bin}) {
        if ($ptype eq 'binary') {
	  my @s = stat("$from/$bin");
	  $m .= "$bin\0$s[9]/$s[7]/$s[1]\0" if @s;
        }
        next;
      }
      if (!link("$from/$bin", "$jobdatadir/$bin")) {
        my $error = "link $from/$bin $jobdatadir/$bin: $!\n";
        return ('broken', $error);
      }
      my @s = stat("$jobdatadir/$bin");
      return ('broken', "$jobdatadir/$bin: stat failed") unless @s;
      if ($ptype eq 'binary') {
        # be extra careful with em, recalculate meta
	$m .= "$bin\0$s[9]/$s[7]/$s[1]\0" if @s;
      }
      my $d;
      eval {
        $d = Build::query("$jobdatadir/$bin", 'evra' => 1, 'unstrippedsource' => 1);
	BSVerify::verify_nevraquery($d);
	my $leadsigmd5 = '';
	die("$jobdatadir/$bin: no hdrmd5\n") unless Build::queryhdrmd5("$jobdatadir/$bin", \$leadsigmd5);
	$d->{'leadsigmd5'} = $leadsigmd5 if $leadsigmd5;
      };
      if (@$ || !$d) {
        return ('broken', "$bin: bad rpm");
      }
      if (%binaryfilter && !$binaryfilter{$d->{'name'}}) {
	$filtered{$tocopy} ||= {};
	$filtered{$tocopy}->{$d->{'name'}} = 1;
        unlink("$jobdatadir/$bin");
        next;
      }
      $donebins{$bin} = $tocopy;
      $bininfo->{$bin} = {'name' => $d->{'name'}, 'arch' => $d->{'arch'}, 'hdrmd5' => $d->{'hdrmd5'}, 'filename' => $bin, 'id' => "$s[9]/$s[7]/$s[1]"};
      $bininfo->{$bin}->{'leadsigmd5'} = $d->{'leadsigmd5'} if $d->{'leadsigmd5'};
      $bininfo->{$bin}->{'md5sum'} = $d->{'md5sum'} if $d->{'md5sum'};
      my $upd = {
	'name' => $d->{'name'},
	'version' => $d->{'version'},
	'release' => $d->{'release'},
	'epoch' => $d->{'epoch'} || 0,
	'arch' => $d->{'arch'},
	'filename' => $bin,
      };
      $upd->{'src'} = $d->{'source'} if $d->{'source'};
      $upd->{'reboot_suggested'} = 'True' if exists $patchinfo->{'reboot_needed'};
      $upd->{'relogin_suggested'} = 'True' if exists $patchinfo->{'relogin_needed'};
      $upd->{'restart_suggested'} = 'True' if exists $patchinfo->{'zypp_restart_needed'};
      push @upackages, $upd;
    }
    $metas{$mpackid} = Digest::MD5::md5_hex($m) if $ptype eq 'binary';
  }

  $broken ||= 'no binaries found' unless @upackages;

  my $update = {};
  $update->{'status'} = 'stable';
  $update->{'from'} = $patchinfo->{'packager'} if $patchinfo->{'packager'};
  # quick hack, to be replaced with something sane
  if ($BSConfig::updateinfo_fromoverwrite) {
    for (sort keys %$BSConfig::updateinfo_fromoverwrite) {
      $update->{'from'} = $BSConfig::updateinfo_fromoverwrite->{$_} if $projid =~ /$_/;
    }
  }
  $update->{'version'} = $patchinfo->{'version'} || '1';	# bodhi inserts its own version...
  $update->{'id'} = $patchinfo->{'incident'};
  if (!$update->{'id'}) {
    $update->{'id'} = $projid;
    $update->{'id'} =~ s/:/_/g;
  }
  $update->{'type'} = $patchinfo->{'category'};
  $update->{'title'} = $patchinfo->{'summary'};
  $update->{'severity'} = $patchinfo->{'rating'} if defined $patchinfo->{'rating'};
  $update->{'description'} = $patchinfo->{'description'};
  # FIXME: do not guess the release element!
  $update->{'release'} = $repoid eq 'standard' ? $projid : $repoid;
  $update->{'release'} =~ s/_standard$//;
  $update->{'release'} =~ s/[_:]+/ /g;
  $update->{'issued'} = { 'date' => $now };

  # fetch defined issue trackers from src server. FIXME: cache this
  my @references;
  my $issue_trackers;
  my $param = {
    'uri' => "$BSConfig::srcserver/issue_trackers",
    'timeout' => 30,
    'proxy' => $proxy,
  };
  eval {
    $issue_trackers = BSRPC::rpc($param, $BSXML::issue_trackers);
  };
  warn($@) if $@;
  if ($issue_trackers) {
    for my $b (@{$patchinfo->{'issue'} || []}) {
      my $it = (grep {$_->{'name'} eq $b->{'tracker'}} @{$issue_trackers->{'issue-tracker'} || []})[0];
      if ($it && $b->{'id'}) {
        my $url = $it->{'show-url'};
        $url =~ s/@@@/$b->{'id'}/g;
        my $title = $b->{'_content'};
        $title = $url unless defined($title) && $title ne '';
        push @references, {'href' => $url, 'id' => $b->{'id'}, 'title' => $title, 'type' => $it->{'kind'}};
      }
    }
  }
  $update->{'references'} = { 'reference' => \@references };
  # XXX: set name and short
  my $col = {
    'package' => \@upackages,
  };
  $update->{'pkglist'} = {'collection' => [ $col ] };
  writexml("$jobdatadir/updateinfo.xml", undef, {'update' => [$update]}, $BSXML::updateinfo);
  writestr("$jobdatadir/logfile", undef, "update built succeeded ".localtime($now)."\n");
  $updateinfodata = {
    'packages' => \@tocopy,
    'metas' => \%metas,
    'binaryorigins' => \%donebins,
  };
  $updateinfodata->{'filtered'} = \%filtered if %filtered;
  BSUtil::store("$jobdatadir/.updateinfodata", undef, $updateinfodata);
  if ($broken) {
    BSUtil::cleandir($jobdatadir);
    writestr("$jobdatadir/logfile", undef, "update built failed".localtime($now)."\n\n$broken\n");
  }
  my @new_meta = ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})."  $packid";
  for my $apackid (sort(keys %metas)) {
    push @new_meta, "$metas{$apackid}  $apackid";
  }
  writestr("$jobdatadir/meta", undef, join("\n", @new_meta)."\n");
  # XXX write reason

  # now commit it...
  my $gdst = "$reporoot/$prp/$myarch";
  my $dst = "$gdst/$packid";
  mkdir_p($dst);
  mkdir_p("$gdst/:meta");
  mkdir_p("$gdst/:logfiles.fail");
  mkdir_p("$gdst/:logfiles.success");
  unlink("$reporoot/$prp/$myarch/:repodone");
  link("$jobdatadir/logfile", "$jobdatadir/logfile.dup");
  if ($broken) {
    rename("$jobdatadir/logfile.dup", "$gdst/:logfiles.fail/$packid");
  } else {
    rename("$jobdatadir/logfile.dup", "$gdst/:logfiles.success/$packid");
    unlink("$gdst/:logfiles.fail/$packid");
  }
  BSUtil::cleandir($dst);
  rename("$jobdatadir/meta", "$gdst/:meta/$packid");
  for my $f (ls($jobdatadir)) {
    rename("$jobdatadir/$f", "$dst/$f") || die("rename $jobdatadir/$f $dst/$f: $!\n");
  }
  if (!checkaccess('sourceaccess', $projid, $packid, $repoid)) {
    BSUtil::touch("$dst/.nosourceaccess");
    $bininfo->{'.nosourceaccess'} = {};
  }
  BSUtil::cleandir($jobdatadir);
  rmdir($jobdatadir);
  BSUtil::store("$dst/.bininfo.new", "$dst/.bininfo", $bininfo);
  return ('done');
}

##########################################################################
##########################################################################
##
##  aggregate package type handling
##

#
# checkaggregate  - calculate package status of an aggregate package
#
# input:  $ctx         - our context
#         $packid      - aggregate package
#         $pdata       - package data information
# output: new package status
#         package status details (new meta in 'scheduled' case)
#
# globals used: $projpacks
#
sub checkaggregate {
  my ($ctx, $packid, $pdata, $info) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  # clone it as we may patch the 'packages' array below
  my $aggregates = Storable::dclone($pdata->{'aggregatelist'}->{'aggregate'} || []);
  my @broken;
  my @blocked;
  my $prpfinished = $ctx->{'prpfinished'};
  my $delayed;
  for my $aggregate (@$aggregates) {
    my $aprojid = $aggregate->{'project'};
    my $proj = $remoteprojs{$aprojid} || $projpacks->{$aprojid};
    if (!$proj) {
      push @broken, $aprojid;
      next;
    }
    if ($proj->{'error'}) {
      # XXX: hmm, there's already a project retryevent on $aprojid
      addretryevent({'type' => 'package', 'project' => $projid, 'package' => $packid}) if $proj->{'error'} !~ /remote error:/;
      $delayed = 1 if $proj->{'error'} !~ /remote error:/;
      push @broken, $aprojid;
      next;
    }
    if (!checkprpaccess($aprojid, $projid)) {
      push @broken, $aprojid;
      next;
    }
    my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$aggregate->{'repository'} || []};
    if (@arepoids) {
      @arepoids = map {$_->{'source'}} grep {exists($_->{'source'})} @arepoids;
    } else {
      @arepoids = ($repoid);
    }
    my @apackids = @{$aggregate->{'package'} || []};
    my $abinfilter;
    $abinfilter = { map {$_ => 1} @{$aggregate->{'binary'}} } if $aggregate->{'binary'};
    for my $arepoid (@arepoids) {
      my $aprp = "$aprojid/$arepoid";
      my $arepo = (grep {$_->{'name'} eq $arepoid} @{$proj->{'repository'} || []})[0];
      if (!$arepo || !grep {$_ eq $myarch} @{$arepo->{'arch'} || []}) {
	push @broken, $aprp;
	next;
      }
      next if !$remoteprojs{$aprojid} && $prpfinished->{$aprp} && $aggregate->{'package'};	# no need to check blocked state
      # notready/prpnotready is indexed with source binary names, so we cannot use it here...
      my $ps = BSUtil::retrieve("$reporoot/$aprp/$myarch/:packstatus", 1);
      if (!$ps) {
	$ps = (readxml("$reporoot/$aprp/$myarch/:packstatus", $BSXML::packstatuslist, 1) || {})->{'packstatus'} || [];
	$ps = { 'packstatus' => { map {$_->{'name'} => $_->{'status'}} @$ps } };
      }
      $ps = ($ps || {})->{'packstatus'} || {};

      if (!$aggregate->{'package'}) {
        # calculate apackids using the gbininfo file
        my $gbininfo;
        if ($remoteprojs{$aprojid}) {
          $gbininfo = read_gbininfo_remote($ctx, "$aprp/$myarch", $remoteprojs{$aprojid}, $ps);
        } else {
          $gbininfo = read_gbininfo("$reporoot/$aprp/$myarch");
        }
	if (!$gbininfo) {
	  $delayed = 1 if defined $gbininfo;
	  push @broken, $aprp;
	  next;
	}
	for my $apackid (keys %$gbininfo) {
	  my $bininfo = $gbininfo->{$apackid};
	  if ($abinfilter) {
	    next unless grep {defined($_->{'name'}) && $abinfilter->{$_->{'name'}}} values %$bininfo;
	  }
	  push @apackids, $apackid;
	}
	@apackids = unify(sort(@apackids));
      }
      for my $apackid (@apackids) {
	my $code = $ps->{$apackid} || 'unknown';
	if ($code eq 'scheduled' || $code eq 'blocked' || $code eq 'finished') {
	  next if $aprojid eq $projid && $arepoid eq $repoid && $apackid eq $packid;
	  push @blocked, "$aprp/$apackid";
	}
      }
    }
    # patch in calculated package list
    $aggregate->{'package'} ||= \@apackids;
  }
  if (@broken) {
    print "      - $packid (aggregate)\n";
    print "        broken (@broken)\n";
    print "        (delayed)\n" if $delayed;
    return ('broken', 'missing repositories: '.join(', ', @broken));
  }
  if (@blocked) {
    print "      - $packid (aggregate)\n";
    print "        blocked (@blocked)\n";
    return ('blocked', join(', ', @blocked));
  }
  my @new_meta;
  my $error;
  for my $aggregate (@$aggregates) {
    my $aprojid = $aggregate->{'project'};
    my @apackids = @{$aggregate->{'package'} || []};
    my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$aggregate->{'repository'} || []};
    if (@arepoids) {
      @arepoids = map {$_->{'source'}} grep {exists($_->{'source'})} @arepoids;
    } else {
      @arepoids = ($repoid);
    }
    for my $arepoid (@arepoids) {
      for my $apackid (@apackids) {
	my $m = '';
        if ($remoteprojs{$aprojid}) {
	  print "    fetching remote binary data for $aprojid/$arepoid/$myarch/$apackid\n";
	  my $param = {
	    'uri' => "$remoteprojs{$aprojid}->{'remoteurl'}/build/$remoteprojs{$aprojid}->{'remoteproject'}/$arepoid/$myarch/$apackid",
	    'timeout' => 20,
	    'proxy' => $proxy,
	  };
	  my $binarylist;
	  eval {
	    $binarylist = BSRPC::rpc($param, $BSXML::binarylist);
	  };
	  if ($@) {
	    warn($@);
	    $error = $@;
	    $error =~ s/\n$//s;
	    addretryevent({'type' => 'repository', 'project' => $aprojid, 'repository' => $arepoid, 'arch' => $myarch}) if $error !~ /remote error:/;
	    last;
	  }
	  for my $binary (@{$binarylist->{'binary'} || []}) {
	    $m .= "$binary->{'filename'}\0$binary->{'mtime'}/$binary->{'size'}/0\0";
	  }
	} else {
	  next if $aprojid eq $projid && $arepoid eq $repoid && $apackid eq $packid;
	  my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
	  my @d = grep {$_ eq 'updateinfo.xml' || /\.(?:$binsufsre)$/} ls($d);
	  for my $b (sort @d) {
	    my @s = stat("$d/$b");
	    next unless @s;
	    $m .= "$b\0$s[9]/$s[7]/$s[1]\0";
	  }
	}
	$m = Digest::MD5::md5_hex($m)."  $aprojid/$arepoid/$myarch/$apackid";
	push @new_meta, $m;
      }
      last if $error;
    }
    last if $error;
  }
  if ($error) {
    # leave old rpms
    print "      - $packid (aggregate)\n";
    print "        $error\n";
    return ('done');
  }
  my @meta;
  if (open(F, '<', "$reporoot/$projid/$repoid/$myarch/:meta/$packid")) {
    @meta = <F>;
    close F;
    chomp @meta;
  }
  if (join('\n', @meta) eq join('\n', @new_meta)) {
    print "      - $packid (aggregate)\n";
    print "        nothing changed\n";
    return ('done');
  }
  my @diff = diffsortedmd5(\@meta, \@new_meta);
  print "      - $packid (aggregate)\n";
  print "        $_\n" for @diff;
  my $new_meta = join('', map {"$_\n"} @new_meta);
  return ('scheduled', [ $new_meta, $aggregates ]);
}

#
# rebuildaggregate  - copy packages from other projects to rebuild an
#                     aggregate
#
# input:  $projid    - our project
#         $repoid    - our repository
#         $packid    - aggregate package
#         $pdata     - package data information
#         $info      - file and dependency information
#         $new_meta  - the new meta file data
# output: new package status
#         package status details
#
# globals used: $projpacks
#
sub rebuildaggregate {
  my ($ctx, $packid, $pdata, $info, $data) = @_;

  my $new_meta = $data->[0];
  my $aggregates = $data->[1];
  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $prp = "$projid/$repoid";
  my $job = jobname($prp, $packid);
  return ('scheduled', $job) if -s "$myjobsdir/$job";
  my $jobdatadir = "$myjobsdir/$job:dir";
  unlink "$jobdatadir/$_" for ls($jobdatadir);
  mkdir_p($jobdatadir);
  my $jobrepo = {};
  my %jobbins;
  my $error;
  for my $aggregate (@$aggregates) {
    my $aprojid = $aggregate->{'project'};
    my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$aggregate->{'repository'} || []};
    if (@arepoids) {
      @arepoids = map {$_->{'source'}} grep {exists($_->{'source'})} @arepoids;
    } else {
      @arepoids = ($repoid);
    }
    my @apackids = @{$aggregate->{'package'} || []};
    my $abinfilter;
    $abinfilter = { map {$_ => 1} @{$aggregate->{'binary'}} } if $aggregate->{'binary'};
    for my $arepoid (reverse @arepoids) {
      for my $apackid (@apackids) {
        my @d;
	my $cpio;
        my $nosource = exists($aggregate->{'nosources'}) ? 1 : 0;
        my $updateinfo;
        if ($remoteprojs{$aprojid}) {
	  my $param = {
	    'uri' => "$remoteprojs{$aprojid}->{'remoteurl'}/build/$remoteprojs{$aprojid}->{'remoteproject'}/$arepoid/$myarch/$apackid",
	    'receiver' => \&BSHTTP::cpio_receiver,
	    'directory' => $jobdatadir,
	    'map' => "upload:",
	    'timeout' => 300,
	    'proxy' => $proxy,
	  };
	  my $done;
	  if ($nosource) {
	    eval {
	      $cpio = BSRPC::rpc($param, undef, "view=cpio", "nosource=1");
	    };
	    $done = 1 if !$@ || $@ !~ /nosource/;
	  }
	  eval {
	    $cpio = BSRPC::rpc($param, undef, "view=cpio");
	  } unless $done;
	  if ($@) {
	    warn($@);
	    $error = $@;
	    $error =~ s/\n$//s;
	    addretryevent({'type' => 'repository', 'project' => $aprojid, 'repository' => $arepoid, 'arch' => $myarch}) if $error !~ /remote error:/;
	    last;
	  }
	  for my $bin (@{$cpio || []}) {
	    $updateinfo = "$jobdatadir/$bin->{'name'}" if $bin->{'name'} eq 'upload:updateinfo.xml';
	    push @d, "$jobdatadir/$bin->{'name'}";
	  }
        } else {
	  next if $aprojid eq $projid && $arepoid eq $repoid && $apackid eq $packid;
	  my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
	  $updateinfo = "$d/updateinfo.xml" if -f "$d/updateinfo.xml";
	  @d = grep {/\.(?:$binsufsre)$/} ls($d);
          @d = map {"$d/$_"} sort(@d);
          $nosource = 1 if -e "$d/.nosourceaccess";
	}
	my $ajobrepo = findbins_dir(\@d);
	my $copysources;
	for my $abin (sort keys %$ajobrepo) {
	  my $r = $ajobrepo->{$abin};
	  next unless $r->{'source'};
	  next if $abinfilter && !$abinfilter->{$r->{'name'}};
	  # FIXME: How is debian handling debug packages ?
	  next if $nosource && ($r->{'name'} =~ /-debug(:?info|source)?$/);
	  my $basename = $abin;
	  $basename =~ s/.*\///;
	  $basename =~ s/^upload:// if $cpio;
	  next if $jobbins{$basename};	# first one wins
	  $jobbins{$basename} = 1;
	  BSUtil::cp($abin, "$jobdatadir/$basename");
	  $jobrepo->{"$jobdatadir/$basename"} = $r;
	  $copysources = 1 unless $nosource;
	}
	if ($updateinfo && !($abinfilter && !$abinfilter->{'updateinfo.xml'})) {
	  BSUtil::cp($updateinfo, "$jobdatadir/updateinfo.xml");
	}
	if ($copysources) {
	  for my $abin (sort keys %$ajobrepo) {
	    my $r = $ajobrepo->{$abin};
	    next if $r->{'source'};
	    my $basename = $abin;
	    $basename =~ s/.*\///;
	    $basename =~ s/^upload:// if $cpio;
	    BSUtil::cp($abin, "$jobdatadir/$basename");
	    $jobrepo->{"$jobdatadir/$basename"} = $r;
	  }
	}
        for my $bin (@{$cpio || []}) {
	  unlink("$jobdatadir/$bin->{'name'}");
	}
      }
      last if $error;
    }
    last if $error;
  }
  if ($error) {
    print "        $error\n";
    BSUtil::cleandir($jobdatadir);
    rmdir($jobdatadir);
    return ('failed', $error);
  }
  writestr("$jobdatadir/meta", undef, $new_meta);

  local *F;
  my $jobstatus = {
    'code' => 'finished',
    'result' => 'succeeded',
  };
  if (!BSUtil::lockcreatexml(\*F, "$myjobsdir/.$job", "$myjobsdir/$job:status", $jobstatus, $BSXML::jobstatus)) {
    die("job lock failed\n");
  }
  my $buildinfo = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => $packid,
    'arch' => $myarch,
    'job' => $job,
    'file' => '_aggregate',
  };
  writexml("$myjobsdir/.$job", "$myjobsdir/$job", $buildinfo, $BSXML::buildinfo);
  close(F);
  print "        scheduled\n";

  my $ev = {'type' => 'built', 'arch' => $myarch, 'job' => $job};
  if ($sign && grep {/\.(?:rpm|pkg\.tar\.gz|pkg\.tar.xz)$/} keys %$jobrepo) {
    sendevent($ev, 'signer', "finished:$myarch:$job");
  } else {
    sendevent($ev, $myarch, "finished:$job");
  }
  $ourjobs{$job} = 1;
  return ('scheduled', $job);
}


##########################################################################
##########################################################################
##
##  preinstallimage type handling
##

sub checkpreinstallimage {
  my ($ctx, $packid, $pdata, $info) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};

  # check if we're blocked
  my $edeps = $ctx->{'edeps'}->{$packid} || [];
  my $notready = $ctx->{'notready'};
  my $dep2src = $ctx->{'dep2src'};
  my $dep2pkg = $ctx->{'dep2pkg'};
  my @blocked = grep {$notready->{$dep2src->{$_}}} @$edeps;
  return ('blocked', join(', ', @blocked)) if @blocked;

  # expand like in set_building, so that we have all used packages
  # in the meta file
  my $bconf = $ctx->{'conf'};
  my ($eok, @bdeps) = Build::get_build($bconf, [], @{$info->{'dep'} || []});
  if (!$eok) {
    print "      - $packid (preinstallimage)\n";
    print "        unresolvables:\n";
    print "          $_\n" for @bdeps;
    return ('unresolvable', join(', ', @bdeps));
  }
  my @pdeps = Build::get_preinstalls($bconf);
  my @vmdeps = Build::get_vminstalls($bconf);
  my @cbpdeps = Build::get_cbpreinstalls($bconf);
  my @cbdeps = Build::get_cbinstalls($bconf);
  @bdeps = unify(@pdeps, @vmdeps, @bdeps, @cbpdeps, @cbdeps);

  # create meta
  my $pool = $ctx->{'pool'};
  my @new_meta;
  for my $dep (@bdeps) {
    my $p = $dep2pkg->{$dep};
    if (!$p) {
      print "      - $packid (preinstallimage)\n";
      print "        unresolvables:\n          $dep\n";
      return ('unresolvable', $dep);
    }
    push @new_meta, $pool->pkg2pkgid($p)."  $dep";
  }
  @new_meta = BSSolv::gen_meta([], @new_meta);

  unshift @new_meta, ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})."  $packid";
  return metacheck($ctx, $packid, 'preinstallimage', \@new_meta, [ \@bdeps ]);
}

sub rebuildpreinstallimage {
  my ($ctx, $packid, $pdata, $info, $data) = @_;
  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $bdeps = $data->[0];
  my $reason = $data->[1];
  my ($job, $joberror) = set_building($projid, $repoid, $packid, $pdata, $info, $ctx->{'conf'}, [], $bdeps, $ctx->{'prpsearchpath'}, $reason, $ctx->{'relsyncmax'}, 0);
  return ('broken', $joberror) unless $job;
  return ('scheduled', $job);
}


##########################################################################
##########################################################################
##
##  standard package type handling
##

sub checkpackage {
  my ($ctx, $packid, $pdata, $info, $packtype) = @_;
  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $repo = $ctx->{'repo'};
  my $prp = $ctx->{'prp'};
  my $notready = $ctx->{'notready'};
  my $dep2src = $ctx->{'dep2src'};
  my $edeps = $ctx->{'edeps'}->{$packid} || [];
  my $depislocal = $ctx->{'depislocal'};

  # check for localdep repos
  if (exists($pdata->{'originproject'})) {
    if ($repo->{'linkedbuild'} && $repo->{'linkedbuild'} eq 'localdep') {
      if (!grep {$depislocal->{$_}} @$edeps) {
	return ('excluded', 'project link, only depends on non-local packages');
      }
    }
  }

  # calculate if we're blocked
  my @blocked = grep {$notready->{$dep2src->{$_}}} @$edeps;
  @blocked = () if $repo->{'block'} && $repo->{'block'} eq 'never';
  if ($ctx->{'cychash'}->{$packid}) {
    # package belongs to a cycle, prune blocked list
    my $cycpass = $ctx->{'cycpass'}->{$packid} || 0;
    if (@blocked && $cycpass == 2) {
      # cycpass == 2 means that packages of this cycle are building
      # because of source changes
      print "      - $packid ($packtype)\n";
      print "        blocked by cycle builds\n";
      return ('blocked', join(', ', @blocked));
    }
    my %cycs = map {$_ => 1} @{$ctx->{'cychash'}->{$packid}};
    # prune building cycle packages from blocked
    my $building = $ctx->{'building'};
    @blocked = grep {!$cycs{$_} || !$building->{$_}} @blocked;
  }
  if (@blocked) {
    # print "      - $packid ($packtype)\n";
    # print "        blocked\n";
    return ('blocked', join(', ', @blocked));
  }

  my $reason;
  my @meta_s = stat("$reporoot/$prp/$myarch/:meta/$packid");
  # 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, '<', "$reporoot/$prp/$myarch/:meta/$packid")) {
      @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';
      $mylastcheck .= "$meta_s[9]/$meta_s[7]/$meta_s[1]";
      $lastcheck->{$packid} = $mylastcheck;
    } else {
      delete $lastcheck->{$packid};
      undef $mylastcheck;
    }
  }
  if (!$mylastcheck) {
    print "      - $packid ($packtype)\n";
    print "        start build\n";
    return ('scheduled', [ { 'explain' => 'new build' } ]);
  } elsif (substr($mylastcheck, 0, 32) ne ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})) {
    print "      - $packid ($packtype)\n";
    print "        src change, start build\n";
    return ('scheduled', [ { 'explain' => 'source change', 'oldsource' => substr($mylastcheck, 0, 32) } ]);
  } elsif (substr($mylastcheck, 32, 32) eq 'fakefakefakefakefakefakefakefake') {
    my @s = stat("$reporoot/$prp/$myarch/:meta/$packid");
    if (!@s || $s[9] + 14400 > time()) {
      print "      - $packid ($packtype)\n";
      print "        buildsystem setup failure\n";
      return ('failed')
    }
    print "      - $packid ($packtype)\n";
    print "        retrying bad build\n";
    return ('scheduled', [ { 'explain' => 'retrying bad build' } ]);
  } else {
    if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'local') {
      # rebuild on src changes only
      goto relsynccheck;
    }
    # more work, check if dep rpm changed
    if ($ctx->{'incycle'}) {
      # print "      - $packid ($packtype)\n";
      # print "        in cycle, no source change...\n";
      return ('done');
    }
    my $check = substr($mylastcheck, 32, 32);
    my $pool = $ctx->{'pool'};
    my $dep2pkg = $ctx->{'dep2pkg'};
    $check .= $repo->{'rebuild'} || 'transitive';
    $check .= $pool->pkg2pkgid($dep2pkg->{$_}) for sort @$edeps;
    $check = Digest::MD5::md5_hex($check);
    if ($check eq substr($mylastcheck, 64, 32)) {
      # print "      - $packid ($packtype)\n";
      # print "        nothing changed\n";
      goto relsynccheck;
    }
    substr($mylastcheck, 64, 32) = $check;
    # even more work, generate new meta, check if it changed
    my @new_meta;
    my $dep2meta = $repodatas{$prp}->{'meta'};
    $repodatas{$prp}->{'meta'} = $dep2meta = {} unless $dep2meta;
    for my $bpack (@$edeps) {
      my $pkg = $dep2pkg->{$bpack};
      my $path = $pool->pkg2fullpath($pkg, $myarch);
      if ($depislocal->{$bpack} && $path) {
	if (!exists $dep2meta->{$bpack}) {
	  my @m;
	  # the next line works for deb and rpm
	  my $mf = substr("$reporoot/$path", 0, -4);
	  #print "        reading meta for $path\n";
	  if (! -e "$mf.meta") {
	    # the generic version
	    $mf = "$reporoot/$path";
	    $mf =~ s/\.(?:$binsufsre)$//;
	  }
	  if (open(F, '<', "$mf.meta") || open(F, '<', "$mf-MD5SUMS.meta")) {
	    @m = <F>;
	    close F;
	    chomp @m;
	    s/  /  $bpack\// for @m;
	    $m[0] =~ s/  .*/  $bpack/ if @m;
	  }
	  @m = ($pool->pkg2pkgid($pkg)."  $bpack") unless @m;
	  $dep2meta->{$bpack} = join("\n", @m);
	  push @new_meta, @m;
	} else {
	  push @new_meta, split("\n", $dep2meta->{$bpack});
	}
      } else {
	my $pkgid = $pool->pkg2pkgid($pkg);
	push @new_meta, "$pkgid  $bpack";
      }
    }
    @new_meta = BSSolv::gen_meta($ctx->{'subpacks'}->{$info->{'name'}} || [], @new_meta);
    unshift @new_meta, ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})."  $packid";
    if (Digest::MD5::md5_hex(join("\n", @new_meta)) eq substr($mylastcheck, 32, 32)) {
      # print "      - $packid ($packtype)\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, '<', "$reporoot/$prp/$myarch/:meta/$packid")) {
      @meta = <F>;
      close F;
      chomp @meta;
    }
    if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'direct') {
      @meta = grep {!/\//} @meta;
      @new_meta = grep {!/\//} @new_meta;
    }
    if (@meta == @new_meta && join('\n', @meta) eq join('\n', @new_meta)) {
      # print "      - $packid ($packtype)\n";
      # print "        nothing changed (looked harder)\n";
      $ctx->{'nharder'}++;
      if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'direct') {
	$lastcheck->{$packid} = $mylastcheck;
      } else {
	# should not happen, delete lastcheck cache
	delete $lastcheck->{$packid};
      }
      goto relsynccheck;
    }
    my @diff = diffsortedmd5(\@meta, \@new_meta);
    print "      - $packid ($packtype)\n";
    print "        $_\n" for @diff;
    print "        meta change, start build\n";
    return ('scheduled', [ { 'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff) } ] );
  }
relsynccheck:
  if ($ctx->{'relsynctrigger'}->{$packid}) {
    print "      - $packid ($packtype)\n";
    print "        rebuild counter sync, start build\n";
    return ('scheduled', [ { 'explain' => 'rebuild counter sync' } ] );
  }
  return ('done');
}

sub rebuildpackage {
  my ($ctx, $packid, $pdata, $info, $data) = @_;

  my $projid = $ctx->{'project'};
  my $repoid = $ctx->{'repository'};
  my $needed = $ctx->{'rebuildpackage_needed'};
  if (!$needed) {
    $needed = $ctx->{'rebuildpackage_needed'} = {};
    my $edeps = $ctx->{'edeps'};
    my $dep2src = $ctx->{'dep2src'};
    for my $p (keys %$edeps) {
      $needed->{$_}++ for map { $dep2src->{$_} || $_ } @{$edeps->{$p}};
    }
  }
  my ($job, $joberror) = set_building($projid, $repoid, $packid, $pdata, $info, $ctx->{'conf'},
     $ctx->{'subpacks'}->{$info->{'name'}} || [], $ctx->{'edeps'}->{$packid} || [],
     $ctx->{'prpsearchpath'}, $data->[0], $ctx->{'relsyncmax'}, $needed->{$packid} || 0);
  return ('broken', $joberror) unless $job;	# could not start job
  return ('scheduled', $job);
}

##########################################################################
##########################################################################

sub select_read {
  my ($timeout, @watchers) = @_;
  my @retrywatchers = grep {$_->{'retry'}} @watchers;
  if (@retrywatchers) {
    my $now = time();
    for (splice @retrywatchers) {
      if ($_->{'retry'} <= $now) {
        push @retrywatchers, $_;
	next;
      }
      $timeout = $_->{'retry'} - $now if !defined($timeout) || $_->{'retry'} - $now < $timeout;
    }
    return @retrywatchers if @retrywatchers;
    @watchers = grep {!$_->{'retry'}} @watchers;
  }
  @watchers = grep {exists $_->{'socket'}} @watchers;
  while(1) {
    my $rin = '';
    for (@watchers) {
      vec($rin, fileno($_->{'socket'}), 1) = 1;
    }
    my $nfound = select($rin, undef, undef, $timeout);
    if (!defined($nfound) || $nfound == -1) {
      next if $! == POSIX::EINTR;
      die("select: $!\n");
    }
    return () if !$nfound && defined($timeout);
    die("select: $!\n") unless $nfound;
    @watchers = grep {vec($rin, fileno($_->{'socket'}), 1)} @watchers;
    die unless @watchers;
    return @watchers;
  }
}

sub changed2lookat {
  my ($changed_low, $changed_med, $changed_high, $lookat_high, $lookat_med, $lookat_next) = @_;

  push @$lookat_high, grep {$changed_high->{$_}} sort keys %$changed_med;
  push @$lookat_med, grep {!$changed_high->{$_}} sort keys %$changed_med;
  @$lookat_high = unify(@$lookat_high);
  @$lookat_med = unify(@$lookat_med);
  my %lookat_high = map {$_ => 1} @$lookat_high;
  @$lookat_med = grep {!$lookat_high{$_}} @$lookat_med;
  for my $prp (@prps) {
    if (!$changed_low->{$prp} && !$changed_med->{$prp}) {
      next unless grep {$changed_med->{$_}} @{$prpdeps{$prp}};
    }
    $lookat_next->{$prp} = 1;
  }
  %$changed_low = ();
  %$changed_med = ();
  %$changed_high = ();
}

sub updaterelsyncmax {
  my ($prp, $arch, $new, $cleanup) = @_;
  local *F;
  BSUtil::lockopen(\*F, '+>>', "$reporoot/$prp/$arch/:relsync.max");
  my $relsyncmax;
  if (-s "$reporoot/$prp/$arch/:relsync.max") {
    $relsyncmax = BSUtil::retrieve("$reporoot/$prp/$arch/:relsync.max", 2);
  }
  $relsyncmax ||= {};
  my $changed;
  for my $tag (keys %$new) {
    next if defined($relsyncmax->{$tag}) && $relsyncmax->{$tag} >= $new->{$tag};   
    $relsyncmax->{$tag} = $new->{$tag};
    $changed = 1;
  }
  if ($cleanup) {
    for (grep {!$new->{$_}} keys %$relsyncmax) {
      delete $relsyncmax->{$_};
      $changed = 1;
    }
  }
  BSUtil::store("$reporoot/$prp/$arch/.:relsync.max", "$reporoot/$prp/$arch/:relsync.max", $relsyncmax) if $changed;
  close(F);
  return $changed;
}


sub setupremotewatcher {
  my ($remoteurl, $watchremote, $start) = @_;
  if ($start) {
    print "setting up watcher for $remoteurl, start=$start\n";
  } else {
    print "setting up watcher for $remoteurl\n";
  }
  # collaps filter list, watch complete project if more than 3 packages are watched
  my @filter;
  my %filterpackage;
  for (sort keys %$watchremote) {
    if (substr($_, 0, 8) eq 'package/') {
      my @s = split('/', $_);
      if (!defined($s[2])) {
	unshift @{$filterpackage{$s[1]}}, undef;
      } else {
	push @{$filterpackage{$s[1]}}, $_;
      }
    } else {
      push @filter, $_;
    }
  }
  for (sort keys %filterpackage) {
    if (!defined($filterpackage{$_}->[0]) || @{$filterpackage{$_}} > 3) {
      push @filter, "package/$_";
    } else {
      push @filter, @{$filterpackage{$_}};
    }
  }
  my $param = {
    'uri' => "$remoteurl/lastevents",
    'async' => 1,
    'request' => 'POST',
    'headers' => [ 'Content-Type: application/x-www-form-urlencoded' ],
    'proxy' => $proxy,
  };
  my @args;
  push @args, "obsname=$BSConfig::obsname/$myarch" if $BSConfig::obsname;
  push @args, map {"filter=$_"} @filter;
  push @args, "start=$start" if $start;
  my $ret;
  eval {
    $ret = BSRPC::rpc($param, $BSXML::events, @args);
  };
  if ($@) {
    warn($@);
    print "retrying in 60 seconds\n";
    $ret = {'retry' => time() + 60};
  }
  $ret->{'remoteurl'} = $remoteurl;
  return $ret;
}

sub getremoteevents {
  my ($watcher, $watchremote, $starthash) = @_;

  my $remoteurl = $watcher->{'remoteurl'};
  my $start = $starthash->{$remoteurl};
  print "response from watcher for $remoteurl\n";
  my $ret;
  eval {
    $ret = BSRPC::rpc($watcher);
  };
  if ($@) {
    warn $@;
    close($watcher->{'socket'}) if defined $watcher->{'socket'};
    delete $watcher->{'socket'};
    $watcher->{'retry'} = time() + 60;
    print "retrying in 60 seconds\n";
    return ();
  }
  my @remoteevents;
  if ($ret->{'sync'} && $ret->{'sync'} eq 'lost') {
    # ok to lose sync on call with no start (actually not, FIXME)
    if ($start) {
      print "lost sync with server, was at $start\n";
      print "next: $ret->{'next'}\n" if $ret->{'next'};
      # synthesize all events we watch
      for my $watch (sort keys %$watchremote) {
	my $projid = $watchremote->{$watch};
	next unless defined $projid;
	my @s = split('/', $watch);
	if ($s[0] eq 'project') {
	  push @remoteevents, {'type' => 'project', 'project' => $projid};
	} elsif ($s[0] eq 'package') {
	  push @remoteevents, {'type' => 'package', 'project' => $projid, 'package' => $s[2]};
	} elsif ($s[0] eq 'repository' || $s[0] eq 'repoinfo') {
	  push @remoteevents, {'type' => $s[0], 'project' => $projid, 'repository' => $s[2], 'arch' => $s[3]};
	}
      }
    }
  }
  for my $ev (@{$ret->{'event'} || []}) {
    next unless $ev->{'project'};
    my $watch;
    if ($ev->{'type'} eq 'project') {
      $watch = "project/$ev->{'project'}";
    } elsif ($ev->{'type'} eq 'package') {
      $watch = "package/$ev->{'project'}/$ev->{'package'}";
      $watch = "package/$ev->{'project'}" unless defined $watchremote->{$watch};
    } elsif ($ev->{'type'} eq 'repository' || $ev->{'type'} eq 'repoinfo') {
      $watch = "$ev->{'type'}/$ev->{'project'}/$ev->{'repository'}/$myarch";
    } else {
      next;
    }
    my $projid = $watchremote->{$watch};
    next unless defined $projid;
    push @remoteevents, {%$ev, 'project' => $projid};
  }
  $starthash->{$remoteurl} = $ret->{'next'} if $ret->{'next'};
  return @remoteevents;
}

sub no_expander {
  return 1, splice(@_, 2);
}

my %handlers = (
  'spec' => {
    'expand' => \&Build::get_deps,
    'check' => \&checkpackage,
    'rebuild' => \&rebuildpackage,
  },
  'dsc' => {
    'expand' => \&Build::get_deps,
    'check' => \&checkpackage,
    'rebuild' => \&rebuildpackage,
  },
  'arch' => {
    'expand' => \&Build::get_deps,
    'check' => \&checkpackage,
    'rebuild' => \&rebuildpackage,
  },
  'kiwi-product' => {
    'expand' => \&no_expander,
    'check' => \&checkkiwiproduct,
    'rebuild' => \&rebuildkiwiproduct,
  },
  'kiwi-image' => {
    'expand' => \&no_expander,
    'check' => \&checkkiwiimage,
    'rebuild' => \&rebuildkiwiimage,
  },
  'patchinfo' => {
    'expand' => \&no_expander,
    'check' => \&checkpatchinfo,
    'rebuild' => \&rebuildpatchinfo,
  },
  'aggregate' => {
    'expand' => \&no_expander,
    'check' => \&checkaggregate,
    'rebuild' => \&rebuildaggregate,
  },
  'preinstallimage' => {
    'expand' => \&Build::get_deps,
    'check' => \&checkpreinstallimage,
    'rebuild' => \&rebuildpreinstallimage,
  },
);

my $unknownchecker = {
  'expand' => \&no_expander,
  'check' => sub {return ('broken', 'unknown package type')},
};


##########################################################################
##########################################################################
##
## Event handlers
##

sub event_built {
  my ($ectx, $ev) = @_;

  my $job = $ev->{'job'};
  local *F;
  my $js = BSUtil::lockopenxml(\*F, '<', "$myjobsdir/$job:status", $BSXML::jobstatus, 1);
  if (!$js) {
    print "  - $job is gone\n";
    close F;
    return;
  }
  if ($js->{'code'} ne 'finished') {
    print "  - $job is not finished: $js->{'code'}\n";
    close F;
    return;
  }
  if ($ev->{'type'} eq 'built') {
    jobfinished($ectx, $job, $js);
  } elsif ($ev->{'type'} eq 'uploadbuild') {
    uploadbuildevent($ectx, $job, $js);
  } elsif ($ev->{'type'} eq 'import') {
    importevent($ectx, $job, $js);
  }
  if (-d "$myjobsdir/$job:dir") {
    unlink("$myjobsdir/$job:dir/$_") for ls("$myjobsdir/$job:dir");
    rmdir("$myjobsdir/$job:dir");
  }
  unlink("$myjobsdir/$job");
  unlink("$myjobsdir/$job:status");
  close F;
  delete $ourjobs{$job};
}

sub event_uploadbuild_delay {
  my ($ectx, $ev) = @_;
  # have to be extra careful with those. if the package is in
  # (delayed)fetchprojpacks, delay event processing until we
  # updated the projpack data.
  my $fetchprojpacks = $ectx->{'fetchprojpacks'};
  my $fetchprojpacks_nodelay = $ectx->{'fetchprojpacks_nodelay'};
  my $delayedfetchprojpacks = $ectx->{'delayedfetchprojpacks'};

  return 0 unless $ev->{'job'};
  my $info = readxml("$myjobsdir/$ev->{'job'}", $BSXML::buildinfo, 1) || {};
  my $projid = $info->{'project'};
  my $packid = $info->{'package'};
  return 0 unless defined($projid) && defined($packid);
  return 1 if $iswaiting{$projid};	# delay if getprojpack in progress
  if (grep {!defined($_) || $_ eq $packid} (@{$fetchprojpacks->{$projid} || []}, @{$delayedfetchprojpacks->{$projid} || []})) {
    push @{$fetchprojpacks->{$projid}}, $packid;
    # remove package from delayedfetchprojpacks to prevent looping
    $delayedfetchprojpacks->{$projid} = [ grep {$_ ne $packid} @{$delayedfetchprojpacks->{$projid} || []} ];
    delete $delayedfetchprojpacks->{$projid} unless @{$delayedfetchprojpacks->{$projid}};
    $fetchprojpacks_nodelay->{$projid} = 1;
    return 1;
  }
  return 0;
}

sub event_package {
  my ($ectx, $ev) = @_;

  my $fetchprojpacks = $ectx->{'fetchprojpacks'};
  my $deepcheck = $ectx->{'deepcheck'};
  my $projid = $ev->{'project'};
  return unless defined $projid;
  my $packid = $ev->{'package'};
  push @{$fetchprojpacks->{$projid}}, $packid;
  $deepcheck->{$projid} = 1 if !defined $packid;
}

sub event_project {
  my ($ectx, $ev) = @_;

  my $fetchprojpacks = $ectx->{'fetchprojpacks'};
  my $lowprioproject = $ectx->{'lowprioproject'};
  my $projid = $ev->{'project'};
  return unless defined $projid;
  push @{$fetchprojpacks->{$projid}}, undef;
  $lowprioproject->{$projid} = 1 if $ev->{'type'} eq 'lowprioproject';
}

sub event_repository {
  my ($ectx, $ev) = @_;

  my $changed_med = $ectx->{'changed_med'};
  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  my $prp = "$projid/$repoid";
  $changed_med->{$prp} = 2;
  if ($ev->{'type'} eq 'repository') {
    delete $repodatas{$prp};
    delete $repounchanged{$prp};
  } elsif ($ev->{'type'} eq 'repoinfo') {
    $repounchanged{$prp} = 2 if $repounchanged{$prp};
  }
}

sub event_check {
  my ($ectx, $ev) = @_;

  my $changed_high = $ectx->{'changed_high'};
  my $changed_dirty = $ectx->{'changed_dirty'};
  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  my %admincheck;
  if (!defined($projid)) {
    my $changed_low = $ectx->{'changed_low'};
    for my $prp (@prps) {
      $changed_low->{$prp} ||= 1;
    }
    return;
  }
  if (defined($repoid)) {
    my $prp = "$projid/$repoid";
    $changed_high->{$prp} ||= 1;
    $changed_dirty->{$prp} = 1;
    $admincheck{$prp} = 1 if $ev->{'type'} eq 'admincheck';
  } else {
    for my $prp (@prps) {
      if ((split('/', $prp, 2))[0] eq $projid) {
        $changed_high->{$prp} ||= 1;
        $changed_dirty->{$prp} = 1;
	$admincheck{$prp} = 1 if $ev->{'type'} eq 'admincheck';
      }
    }
    $changed_high->{$projid} ||= 1;
  }
  if (%admincheck) {
    my $lookat_high = $ectx->{'lookat_high'};
    my $nextmed = $ectx->{'nextmed'};
    @$lookat_high = grep {!$admincheck{$_}} @$lookat_high;
    unshift @$lookat_high, sort keys %admincheck;
    delete $nextmed->{$_} for keys %admincheck;
  }
}

sub event_check_med {
  my ($ectx, $ev) = @_;

  my $changed_med = $ectx->{'changed_med'};
  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  return unless defined($projid) && defined($repoid);
  my $prp = "$projid/$repoid";
  print "$prp is $ev->{'type'}\n";
  $changed_med->{$prp} ||= 1;
}

sub event_scanrepo {
  my ($ectx, $ev) = @_;

  my $changed_high = $ectx->{'changed_high'};
  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  if (!defined($projid) && !defined($repoid)) {
    print "flushing all repository data\n";
    %repodatas = ();
    return;
  }
  if (defined($projid) && defined($repoid)) {
    my $prp = "$projid/$repoid";
    print "reading packages of repository $projid/$repoid\n";
    delete $repodatas{$prp};
    my $pool = BSSolv::pool->new();
    addrepo({'gctx' => $ectx, 'prp' => $prp}, $pool, $prp);
    undef $pool;
    $changed_high->{$prp} = 2;
    delete $repounchanged{$prp};
  }
}

sub event_scanprjbinaries {
  my ($ectx, $ev) = @_;
  my $changed_high = $ectx->{'changed_high'};
  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  my $packid = $ev->{'package'};
  if (defined($projid) && defined($repoid)) {
    my $prp = "$projid/$repoid";
    delete $remotegbininfos{"$prp/$myarch"};
    return if $remoteprojs{$projid};
    if (defined($packid)) {
      unlink("$prp/$myarch/$packid/.bininfo");
    } else {
      for my $packid (grep {!/^[:\.]/} ls("$prp/$myarch")) {
        next if $packid eq '_deltas';
        next unless -d "$prp/$myarch/$packid";
        unlink("$prp/$myarch/$packid/.bininfo");
      }
    }
    unlink("$reporoot/$prp/$myarch/:bininfo");
    unlink("$reporoot/$prp/$myarch/:bininfo.merge");
    print "reading project binary state of repository $projid/$repoid\n";
    read_gbininfo("$reporoot/$prp/$myarch");
    $changed_high->{$prp} = 1;
  }
}

sub event_dumprepo {
  my ($ectx, $ev) = @_;

  my $prp = "$ev->{'project'}/$ev->{'repository'}";
  my $repodata = $repodatas{$prp} || {};
  local *F;
  open(F, '>', "/tmp/repodump");
  print F "# repodump for $prp\n\n";
  print F Dumper($repodata);
  close F;
}

sub event_wipenotyet {
  my ($ectx, $ev) = @_;

  my $prp = "$ev->{'project'}/$ev->{'repository'}";
  my $nextmed = $ectx->{'nextmed'} || {};
  delete $nextmed->{$prp};
}

sub event_wipe {
  my ($ectx, $ev) = @_;

  my $changed_high = $ectx->{'changed_high'};
  my $changed_dirty = $ectx->{'changed_dirty'};

  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  my $packid = $ev->{'package'};
  return unless defined($projid) && defined($repoid) && defined($packid);
  my $prp = "$projid/$repoid";
  my $gdst = "$reporoot/$prp/$myarch";
  print "wiping $prp $packid\n";
  return unless -d "$gdst/$packid";
  # delete repository done flag
  unlink("$gdst/:repodone");
  # delete full entries
  my $pdata = (($projpacks->{$projid} || {})->{'package'} || {})->{$packid};
  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled) if $projpacks->{$projid};
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($prp, $packid, "$gdst/$packid" , undef, undef, $useforbuildenabled, $prpsearchpath{$prp});
  delete $repounchanged{$prp};
  # delete other files
  unlink("$gdst/:logfiles.success/$packid");
  unlink("$gdst/:logfiles.fail/$packid");
  unlink("$gdst/:meta/$packid");
  for my $f (ls("$gdst/$packid")) {
    next if $f eq 'history';
    if (-d "$gdst/$packid/$f") {
      BSUtil::cleandir("$gdst/$packid/$f");
      rmdir("$gdst/$packid/$f");
    } else {
      unlink("$gdst/$packid/$f");
    }
  }
  rmdir("$gdst/$packid");	# in case there is no history
  for $prp (@prps) {
    if ((split('/', $prp, 2))[0] eq $projid) {
      $changed_high->{$prp} = 2;
      $changed_dirty->{$prp} = 1;
    }
  }
  $changed_high->{$projid} = 2;
}

sub event_useforbuild {
  my ($ectx, $ev) = @_;

  my $changed_high = $ectx->{'changed_high'};
  my $changed_dirty = $ectx->{'changed_dirty'};

  my $projid = $ev->{'project'};
  my $repoid = $ev->{'repository'};
  return unless defined($projid) && defined($repoid);
  my $prp = "$projid/$repoid";
  my $packs = $projpacks->{$projid}->{'package'} || {};
  my @packs;
  if ($ev->{'package'}) {
    @packs = ($ev->{'package'});
  } else {
    @packs = sort keys %$packs;
  }
  for my $packid (@packs) {
    my $gdst = "$reporoot/$prp/$myarch";
    next unless -d "$gdst/$packid";
    my $useforbuildenabled = 1;
    my $pdata = $packs->{$packid};
    $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
    $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
    next unless $useforbuildenabled;
    my $meta = "$gdst/:meta/$packid";
    undef $meta unless -s $meta;
    update_dst_full($prp, $packid, "$gdst/$packid", "$gdst/$packid", $meta, $useforbuildenabled, $prpsearchpath{$prp});
  }
  for $prp (@prps) {
    if ((split('/', $prp, 2))[0] eq $projid) {
      if ((split('/', $prp, 2))[0] eq $projid) {
	$changed_high->{$prp} = 2 if (split('/', $prp, 2))[0] eq $projid;
	$changed_dirty->{$prp} = 1;
      }
    }
  }
  $changed_high->{$projid} = 2;
}

sub event_exit {
  my ($ectx, $ev) = @_;

  print "exiting...\n" if $ev->{'type'} eq 'exit';
  print "exiting (with complete info)...\n" if $ev->{'type'} eq 'exitcomplete';
  print "restarting...\n" if $ev->{'type'} eq 'restart';
  print "dumping scheduler state...\n" if $ev->{'type'} eq 'dumpstate';
  print "dumping emergency state...\n" if $ev->{'type'} eq 'emergencydump';
  my $lookat_next = $ectx->{'lookat_next'} || {};
  my @new_lookat = @{$ectx->{'lookat_low'} || []};
  push @new_lookat, grep {$lookat_next->{$_}} @prps;
  # here comes our scheduler state
  my $schedstate = {};
  if ($ev->{'type'} eq 'exitcomplete' || $ev->{'type'} eq 'restart' || $ev->{'type'} eq 'emergencydump') {
    $schedstate->{'projpacks'} = $projpacks;
  }
  $schedstate->{'prps'} = \@prps;
  $schedstate->{'changed_low'} = $ectx->{'changed_low'};
  $schedstate->{'changed_med'} = $ectx->{'changed_med'};
  $schedstate->{'changed_high'} = $ectx->{'changed_high'};
  $schedstate->{'lookat'} = \@new_lookat;
  $schedstate->{'lookat_oob'} = $ectx->{'lookat_med'};
  $schedstate->{'lookat_oobhigh'} = $ectx->{'lookat_high'};
  $schedstate->{'prpfinished'} = \%prpfinished;
  $schedstate->{'globalnotready'} = \%prpnotready;
  $schedstate->{'fetchprojpacks'} = $ectx->{'fetchprojpacks'} if %{$ectx->{'fetchprojpacks'} || {}};
  $schedstate->{'delayedfetchprojpacks'} = $ectx->{'delayedfetchprojpacks'};
  $schedstate->{'watchremote_start'} = \%watchremote_start;
  unlink("$rundir/bs_sched.$myarch.state");
  my $statefile = "$rundir/bs_sched.$myarch.state";
  $statefile = "$rundir/bs_sched.$myarch.dead" if $ev->{'type'} eq 'emergencydump';
  BSUtil::store("$statefile.new", $statefile, $schedstate);
  if ($ev->{'type'} eq 'exit' || $ev->{'type'} eq 'exitcomplete') {
    print "bye.\n";
    exit(0);
  }
  if ($ev->{'type'} eq 'restart') {
    exec $0, $myarch;
    warn("$0: $!\n");
  }
}

my %event_handlers = (
  'built' => \&event_built,
  'uploadbuild' => \&event_built,
  'import' => \&event_built,
  'srcevent' => \&event_package,
  'package' => \&event_package,
  'project' => \&event_project,
  'projevent' => \&event_project,
  'lowprioproject' => \&event_project,
  'repository' => \&event_repository,
  'repoinfo' => \&event_repository,
  'rebuild' => \&event_check,
  'recheck' => \&event_check,
  'admincheck' => \&event_check,
  'unblocked' => \&event_check_med,
  'relsync' => \&event_check_med,
  'scanrepo' => \&event_scanrepo,
  'scanprjbinaries' => \&event_scanprjbinaries,
  'dumprepo' => \&event_dumprepo,
  'wipenotyet' => \&event_wipenotyet,
  'wipe' => \&event_wipe,
  'exit' => \&event_exit,
  'exitcomplete' => \&event_exit,
  'restart' => \&event_exit,
  'dumpstate' => \&event_exit,
  'useforbuild' => \&event_useforbuild,
);


#
# event loop postprocessing: fetch all changed projpack entries
#

sub do_fetchprojpacks {
  my ($ectx, $doasync) = @_;

  my $fetchprojpacks = $ectx->{'fetchprojpacks'};
  my $fetchprojpacks_nodelay = $ectx->{'fetchprojpacks_nodelay'};
  my $delayedfetchprojpacks = $ectx->{'delayedfetchprojpacks'};

  my $deepcheck = $ectx->{'deepcheck'} || {};
  my $lowprioproject = $ectx->{'lowprioproject'} || {};

  my $changed_low = $ectx->{'changed_low'};
  my $changed_med = $ectx->{'changed_med'};
  my $changed_high = $ectx->{'changed_high'};
  my $changed_dirty = $ectx->{'changed_dirty'};

  return unless %$fetchprojpacks;

  #pass0: delay them if possible
  for my $projid (sort keys %$fetchprojpacks) {
    next if $fetchprojpacks_nodelay->{$projid};
    next if grep {!defined($_)} @{$fetchprojpacks->{$projid}};
    # only source updates, delay them
    my $foundit;
    for my $prp (@prps) {
      if ((split('/', $prp, 2))[0] eq $projid) {
	$changed_high->{$prp} ||= 1;
	$changed_dirty->{$prp} = 1;
	$foundit = 1;
      }
    }
    next unless $foundit;	# can't delay it, we never look at the project
    push @{$delayedfetchprojpacks->{$projid}}, @{$fetchprojpacks->{$projid}};
    my $linked = find_linked_sources($projid, $fetchprojpacks->{$projid});
    if (%$linked) {
      for my $lprojid (keys %$linked) {
	push @{$delayedfetchprojpacks->{$lprojid}}, @{$linked->{$lprojid}};
      }
      for my $lprp (@prps) {
	if ($linked->{(split('/', $lprp, 2))[0]}) {
	  $changed_med->{$lprp} ||= 1;
	  $changed_dirty->{$lprp} = 1; 
	}
      }
    }
    delete $fetchprojpacks->{$projid};
  }
  return unless %$fetchprojpacks;

  # create context
  my $ctx = {'gctx' => $ectx};

  # pass1: fetch all projpacks
  for my $projid (sort keys %$fetchprojpacks) {
    my $fetchedall;
    if (grep {!defined($_)} @{$fetchprojpacks->{$projid}}) {
      # project change, this can be
      # a change in _meta
      # a change in _config
      # a change in _pattern
      # deletion of a project
      if ($doasync) {
        my %packids = map {$_ => 1} grep {defined($_)} @{$fetchprojpacks->{$projid}};
        my $async = { '_changetype' => 'high', '_changelevel' => 2 };
	$async->{'_dolink'} = 1 if %packids || $deepcheck->{$projid};
        $async->{'_changetype'} = 'low' if $lowprioproject->{$projid} && !$deepcheck->{$projid};
	if ($projpacks->{$projid} && !$deepcheck->{$projid}) {
	  $async->{'_packids'} = [ sort keys %packids ];
	  update_project_meta($ctx, $async, $projid);
	} else {
	  get_projpacks($ctx, $async, $projid);
	}
	delete $fetchprojpacks->{$projid};	# backgrounded
	next;
      }
      if ($projpacks->{$projid} && !$deepcheck->{$projid}) {
	if (!update_project_meta($ctx, 0, $projid)) {
	  # update meta failed, do it the hard way...
	  get_projpacks($ctx, undef, $projid);
	  $fetchedall = 1;
	}
      } else {
	get_projpacks($ctx, undef, $projid);
	$fetchedall = 1;
      }
    }
    if (!$fetchedall) {
      # single package (source) changes
      my %packids = map {$_ => 1} grep {defined($_)} @{$fetchprojpacks->{$projid}};
      # remove em from the delay queue
      if ($delayedfetchprojpacks->{$projid} && %packids) {
	$delayedfetchprojpacks->{$projid} = [ grep {!$packids{$_}} @{$delayedfetchprojpacks->{$projid} || []} ];
	delete $delayedfetchprojpacks->{$projid} unless @{$delayedfetchprojpacks->{$projid}};
      }
      if ($doasync && %packids) {
        my $async = { '_dolink' => 1, '_changetype' => 'high', '_changelevel' => 1 };
	get_projpacks($ctx, $async, $projid, sort keys %packids);
	delete $fetchprojpacks->{$projid};	# backgrounded
	next;
      }
      get_projpacks($ctx, undef, $projid, sort keys %packids) if %packids;
    } else {
      delete $delayedfetchprojpacks->{$projid};
    }
  }

  return unless %$fetchprojpacks;	# still something on the list?

  get_projpacks_postprocess();

  # pass2: postprocess, set changed_high, calculate link info
  my %fetchlinkedprojpacks;
  my %fetchlinkedprojpacks_srcchange;
  for my $projid (sort keys %$fetchprojpacks) {
    my $changed = $lowprioproject->{$projid} && $projpacks->{$projid} && !$deepcheck->{$projid} ? $changed_low : $changed_high;
    if (grep {!defined($_)} @{$fetchprojpacks->{$projid}}) {
      for my $prp (@prps) {
	if ((split('/', $prp, 2))[0] eq $projid) {
	  $changed->{$prp} = 2;
	  $changed_dirty->{$prp} = 1;
	}
      }
      $changed_high->{$projid} = 2;	# $changed only works for prps
      # more work if the project was deleted
      # (if it's just a config change we really do not care about source links)
      if (!$projpacks->{$projid} || $deepcheck->{$projid}) {
	my $linked = find_linked_sources($projid, undef);
	push @{$fetchlinkedprojpacks{$_}}, @{$linked->{$_}} for keys %$linked;
      }
    } else {
      for my $prp (@prps) {
	if ((split('/', $prp, 2))[0] eq $projid) {
	  $changed_high->{$prp} ||= 1;
	  $changed_dirty->{$prp} = 1;
	}
      }
      $changed_high->{$projid} ||= 1;
    }
    my @packids = grep {defined($_)} @{$fetchprojpacks->{$projid}};
    my $linked = find_linked_sources($projid, \@packids);
    for my $lprojid (keys %$linked) {
      push @{$fetchlinkedprojpacks{$lprojid}}, @{$linked->{$lprojid}};
      $fetchlinkedprojpacks_srcchange{$lprojid} = 1;	# mark as source changes
    }
  }

  # pass3: update link information
  if (%fetchlinkedprojpacks) {
    my $projpackchanged;
    for my $projid (sort keys %fetchlinkedprojpacks) {
      my %packids = map {$_ => 1} @{$fetchlinkedprojpacks{$projid}};
      if ($doasync) {
	my $async = { '_changelevel' => 1, '_changetype' => 'low' };
	$async->{'_changetype'} = 'med' if $fetchlinkedprojpacks_srcchange{$projid};
	get_projpacks($ctx, $async, $projid, sort keys %packids);
	next;
      }
      get_projpacks($ctx, undef, $projid, sort keys %packids);
      $projpackchanged = 1;
      # we assign source changed through links med prio,
      # everything else is low prio
      if ($fetchlinkedprojpacks_srcchange{$projid}) {
	for my $prp (@prps) {
	  if ((split('/', $prp, 2))[0] eq $projid) {
	    $changed_med->{$prp} ||= 1;
	    $changed_dirty->{$prp} = 1;
	  }
	}
	$changed_med->{$projid} ||= 1;
      } else {
	for my $prp (@prps) {
	  if ((split('/', $prp, 2))[0] eq $projid) {
	    $changed_low->{$prp} ||= 1;
	    $changed_dirty->{$prp} = 1;
	  }
	}
	$changed_low->{$projid} ||= 1;
      }
    }
    get_projpacks_postprocess() if $projpackchanged;	# just in case...
  }

  %$fetchprojpacks = ();
}


##########################################################################
##########################################################################
##
## Scheduler startup code
##

$| = 1;
$SIG{'PIPE'} = 'IGNORE';
if ($testmode && ($testmode eq 'exit' || $testmode eq 'restart')) {
  if (!(-e "$rundir/bs_sched.$myarch.lock") || BSUtil::lockcheck('>>', "$rundir/bs_sched.$myarch.lock")) {
    die("scheduler is not running for $myarch.\n") if $testmode eq 'restart';
    print("scheduler is not running for $myarch.\n");
    exit(0);
  }
  if ($testmode eq 'restart') {
    print "restarting scheduler for $myarch...\n";
  } else {
    print "shutting down scheduler for $myarch...\n";
  }
  my $ev = {
    'type' => $testmode eq 'restart' ? 'restart' : 'exitcomplete',
  };
  my $evname = "$ev->{'type'}::";
  sendevent($ev, $myarch, $evname);
  BSUtil::waituntilgone("$myeventdir/$evname");
  if ($testmode eq 'exit') {
    # scheduler saw the event, wait until the process is gone
    local *F;
    BSUtil::lockopen(\*F, '>>', "$rundir/bs_sched.$myarch.lock", 1);
    close F;
  }
  exit(0);
}
print "starting build service scheduler\n";

# get lock
mkdir_p($rundir);
if (!$testprojid) {
  open(RUNLOCK, '>>', "$rundir/bs_sched.$myarch.lock") || die("$rundir/bs_sched.$myarch.lock: $!\n");
  flock(RUNLOCK, LOCK_EX | LOCK_NB) || die("scheduler is already running for $myarch!\n");
  utime undef, undef, "$rundir/bs_sched.$myarch.lock";
}

# setup event mechanism
for my $d ($eventdir, $myeventdir, $jobsdir, $myjobsdir, $infodir) {
  next if -d $d;
  mkdir($d) || die("$d: $!\n");
}
if (!-p "$myeventdir/.ping") {
  POSIX::mkfifo("$myeventdir/.ping", 0666) || die("$myeventdir/.ping: $!");
  chmod(0666, "$myeventdir/.ping");
}

sysopen(PING, "$myeventdir/.ping", POSIX::O_RDWR) || die("$myeventdir/.ping: $!");
#fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);


# changed: 1: something "local" changed, :full unchanged,
#          2: the :full repo is changed
# set all projects and prps to :full repo changed
my %changed_low;
my %changed_med;
my %changed_high;
my %changed_dirty;
my %lastcheck;
my %delayedfetchprojpacks;

my %lookat_next;	# not so important, next series
my @lookat_low;         # not so important
my @lookat_med;         # do those first (out of band), triggered through direct build results
my @lookat_high;        # do those really first so that our users are happy, triggered through user interaction


# read old state if present
if (!$testprojid && -s "$rundir/bs_sched.$myarch.state") {
  print "reading old state...\n";
  my $schedstate = BSUtil::retrieve("$rundir/bs_sched.$myarch.state", 2);
  unlink("$rundir/bs_sched.$myarch.state");
  if ($schedstate) {
    # just for testing...
    print "  - $_\n" for sort keys %$schedstate;
    if ($schedstate->{'projpacks'}) {
      $projpacks = $schedstate->{'projpacks'};
    } else {
      # get project and package information from src server
      get_projpacks(undef, undef);	# XXX: async
    }
    get_projpacks_postprocess();

    my %oldprps = map {$_ => 1} @{$schedstate->{'prps'} || []};
    my @newprps = grep {!$oldprps{$_}} @prps;

    # update lookat arrays
    @lookat_low = @{$schedstate->{'lookat'} || []};
    @lookat_med = @{$schedstate->{'lookat_oob'} || []};
    @lookat_high = @{$schedstate->{'lookat_oobhigh'} || []};

    # update changed hash
    %changed_low = ();
    %changed_med = ();
    %changed_high = ();
    for my $prp (@newprps) {
      $changed_med{$prp} = 2;
      $changed_med{(split('/', $prp, 2))[0]} = 2;
    }

    my $oldchanged_low = $schedstate->{'changed_low'} || {};
    my $oldchanged_med = $schedstate->{'changed_med'} || {};
    my $oldchanged_high = $schedstate->{'changed_high'} || {};
    for my $projid (keys %$projpacks) {
      $changed_low{$projid} = $oldchanged_low->{$projid} if exists $oldchanged_low->{$projid};
      $changed_med{$projid} = $oldchanged_med->{$projid} if exists $oldchanged_med->{$projid};
      $changed_high{$projid} = $oldchanged_high->{$projid} if exists $oldchanged_high->{$projid};
    }
    for my $prp (@prps) {
      $changed_low{$prp} = $oldchanged_low->{$prp} if exists $oldchanged_low->{$prp};
      $changed_med{$prp} = $oldchanged_med->{$prp} if exists $oldchanged_med->{$prp};
      $changed_high{$prp} = $oldchanged_high->{$prp} if exists $oldchanged_high->{$prp};
    }

    ## update repodata hash
    #my $oldrepodata = $schedstate->{'repodata'} || {};
    #for my $prp (@prps) {
    #  $repodata{$prp} = $oldrepodata->{$prp} if exists $oldrepodata->{$prp};
    #}

    # update prpfinished hash
    my $oldprpfinished = $schedstate->{'prpfinished'} || {};
    for my $prp (@prps) {
      $prpfinished{$prp} = $oldprpfinished->{$prp} if exists $oldprpfinished->{$prp};
    }

    # update prpnotready hash
    my $oldprpnotready = $schedstate->{'globalnotready'} || {};
    for my $prp (@prps) {
      $prpnotready{$prp} = $oldprpnotready->{$prp} if %{$oldprpnotready->{$prp} || {}};
    }

    # update delayedfetchprojpacks hash
    my $olddelayedfetchprojpacks = $schedstate->{'delayedfetchprojpacks'} || {};
    for my $projid (keys %$projpacks) {
      $delayedfetchprojpacks{$projid} = $olddelayedfetchprojpacks->{$projid} if $olddelayedfetchprojpacks->{$projid};
    }

    # use old start values
    if ($schedstate->{'watchremote_start'}) {
      %watchremote_start = %{$schedstate->{'watchremote_start'}};
    }

    # start project data fetch for delayed startup projects
    for my $projid (sort keys %$projpacks) {
      my $packs = $projpacks->{$projid}->{'package'} || {};
      for my $packid (sort keys %$packs) {
        $delayedfetchprojpacks{$projid} = [ '/all' ] if ($packs->{$packid}->{'error'} || '') eq 'delayed startup';
      }
    }

    if ($schedstate->{'fetchprojpacks'} && $schedstate->{'projpacks'}) {
      my $ectx = {
	'fetchprojpacks' => $schedstate->{'fetchprojpacks'},
	'fetchprojpacks_nodelay' => { map {$_ => 1} keys %{$schedstate->{'fetchprojpacks'}} },
	'delayedfetchprojpacks' => \%delayedfetchprojpacks,
        'changed_low' => \%changed_low,
        'changed_med' => \%changed_med,
        'changed_high' => \%changed_high,
        'changed_dirty' => \%changed_dirty,
      };
      do_fetchprojpacks($ectx, $asyncmode) if %{$ectx->{'fetchprojpacks'}};
    }
  }
}

if (!$projpacks && $startupmode) {
  if ($startupmode == 1) {
    print "cold start, scanning all non-remote projects\n";
  } else {
    print "cold start, initializing all projects\n";
  }
  my $param = {
    'uri' => "$BSConfig::srcserver/getprojpack",
  };
  my @args = ('withrepos', 'withconfig', "arch=$myarch", 'withremotemap=1', 'noremote=1');
  push @args, 'withsrcmd5', 'withdeps' if $startupmode == 1;
  my $projpacksin;
  while (1) {
    eval {
      $projpacksin = BSRPC::rpc($param, $BSXML::projpack, @args);
    };
    last unless $@ || !$projpacksin;
    print $@ if $@;
    print "retrying in 60 seconds...\n";
    sleep(60);
  }
  update_projpacks($projpacksin);
  get_projpacks_postprocess();
  for my $projid (sort keys %$projpacks) {
    my $packs = $projpacks->{$projid}->{'package'} || {};
    next unless %$packs;
    if ($startupmode == 1) {
      my @delayed;
      my $ok;
      for my $packid (sort keys %$packs) {
	my $pdata = $packs->{$packid};
	if ($pdata->{'error'}) {
	  if ($pdata->{'error'} =~ /noremote option/) {
	    $pdata->{'error'} = 'delayed startup';
	    push @delayed, $packid;
	  } else {
	    $ok++;
	  }
	} else {
	  if (grep {$_->{'error'} && $_->{'error'} =~ /noremote option/} @{$pdata->{'info'} || []}) {
	    $pdata->{'error'} = 'delayed startup';
	    push @delayed, $packid;
	  } else {
	    $ok++;
	  }
	}
      }
      if (!$ok) {
        $delayedfetchprojpacks{$projid} = [ '/all' ];	# hack
      } else {
        $delayedfetchprojpacks{$projid} = [ @delayed ];
      }
    } else {
      $delayedfetchprojpacks{$projid} = [ '/all' ];	# hack
      for my $packid (sort keys %$packs) {
        $packs->{$packid}->{'error'} = 'delayed startup';
      }
    }
  }
  @lookat_low = sort keys %$projpacks;
  push @lookat_low, @prps;
}

if (!$projpacks) {
  # get project and package information from src server
  print "cold start, scanning all projects\n";
  get_projpacks(undef, undef);
  get_projpacks(undef, undef, 'opensuse_org') if $testprojid;
  get_projpacks_postprocess();
  # look at everything
  @lookat_low = sort keys %$projpacks;
  push @lookat_low, @prps;
}

unlink("$rundir/bs_sched.$myarch.dead");	# alive and kicking

#XXX
#@lookat_low = sort keys %$projpacks;
#push @lookat_low, @prps;

my %remotewatchers;
my %nextmed;

my %prpchecktimes;
my %prplastcheck;
my %prpunfinished;

if (@lookat_low) {
  %lookat_next = map {$_ => 1} @lookat_low;
  @lookat_low = ();
}

my $slept = 0;
my $notlow = 0;
my $notmed = 0;
my $schedulerstart = time();
my $gotevent = 1;
$gotevent = 0 if $testprojid;

my $lastschedinfo = 0;
my $initialstartup = 1;

# create global context
my $gctx = {
  'changed_low' => \%changed_low,
  'changed_med' => \%changed_med,
  'changed_high' => \%changed_high,
  'changed_dirty' => \%changed_dirty,

  'lookat_low' => \@lookat_low,
  'lookat_med' => \@lookat_med,
  'lookat_high' => \@lookat_high,
  'lookat_next' => \%lookat_next,

  'delayedfetchprojpacks' => \%delayedfetchprojpacks,

  'nextmed' => \%nextmed,
  'iswaiting' => \%iswaiting,
};

##
## Here comes the big loop...
##

eval {

  while(1) {
NEXTPRP:
    if (%changed_low || %changed_med || %changed_high) {
      changed2lookat(\%changed_low, \%changed_med, \%changed_high, \@lookat_high, \@lookat_med, \%lookat_next);
      next;
    }

    # delete no longer needed or outdated remotewatchers
    for my $remoteurl (sort keys %remotewatchers) {
      my $watcher = $remotewatchers{$remoteurl};
      if (!$watchremote{$remoteurl} || join("\0", sort keys %{$watchremote{$remoteurl}}) ne $watcher->{'watchlist'}) {
	close $watcher->{'socket'} if defined $watcher->{'socket'};
	delete $remotewatchers{$remoteurl};
	next;
      }
    }

    # create watchers
    for my $remoteurl (sort keys %watchremote) {
      if (!$remotewatchers{$remoteurl}) {
	my $watcher = setupremotewatcher($remoteurl, $watchremote{$remoteurl}, $watchremote_start{$remoteurl});
	$watcher->{'watchlist'} = join("\0", sort keys %{$watchremote{$remoteurl}});
	$remotewatchers{$remoteurl} = $watcher;
      }
    }

    my $dummy;
    my @remoteevents;
    my $pingsock = {
      'socket' => \*PING,
      'remoteurl' => 'ping',
    };
    if (@retryevents) {
      my $now = time();
      @remoteevents = grep {$_->{'retry'} <= $now} @retryevents;
      if (@remoteevents) {
	@retryevents = grep {$_->{'retry'} > $now} @retryevents;
	delete $_->{'retry'} for @remoteevents;
	$gotevent = 1;
	print "retrying ".@remoteevents." events\n";
      }
    }
    if ($testprojid) {
      print "ignoring events due to test mode\n";
    } elsif (%remotewatchers || %iswaiting) {
      my @readok = select_read(0, $pingsock, values %remotewatchers, values %iswaiting);
      $gotevent = 1 if @readok;
      for my $watcher (@readok) {
	my $remoteurl = $watcher->{'remoteurl'};
        if (!defined($remoteurl)) {
	  xrpc_resume($watcher);
	  next;
	}
	next if $remoteurl eq 'ping';
	if ($watcher->{'retry'}) {
	  print "retrying watcher for $remoteurl\n";
	  delete $remotewatchers{$remoteurl};
	  next;
	}
	push @remoteevents, getremoteevents($watcher, $watchremote{$remoteurl}, \%watchremote_start);
	delete $remotewatchers{$remoteurl} unless $watcher->{'retry'};
      }
    } else {
      fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
      if ((sysread(PING, $dummy, 1, 0) || 0) > 0) {
	$gotevent = 1;
      }
      fcntl(PING,F_SETFL,0);
    }

    if ($gotevent) {
      die if $testprojid;
      $gotevent = 0;
      # drain ping pipe
      fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
      1 while (sysread(PING, $dummy, 1024, 0) || 0) > 0;
      fcntl(PING,F_SETFL,0);

      my @events = @remoteevents;
      # check eventdir for new events
      for my $evfilename (sort(ls($myeventdir))) {
	next if $evfilename =~ /^\./;
	my $ev;
	if ($evfilename =~ /^finished:(.*)/) {
	  $ev = {'type' => 'built', 'job' => $1};
	} else {
	  $ev = readxml("$myeventdir/$evfilename", $BSXML::event, 1);
	  if (!$ev) {
	    print "$evfilename: bad xml\n";
	    unlink("$myeventdir/$evfilename");
	    next;
	  }
	}
	$ev->{'evfilename'} = $evfilename;
	push @events, $ev;
      }

      # create local clone of global context with some extra state used
      # in the event processing loop
      my $ectx = {
	%$gctx,
	'fetchprojpacks' => {},
	'fetchprojpacks_nodelay' => {},
	'deepcheck' => {},
	'lowprioproject' => {},
	'fullcache' => undef,
      };

      if (@events > 1) {
	# sort events a bit, exit events go first ;-)
	# uploadbuild events must go last
	my %evprio = ('exit' => -1, 'exitcomplete' => -1, 'restart' => -1, 'uploadbuild' => 1);
	@events = sort {($evprio{$a->{'type'}} || 0) <=> ($evprio{$b->{'type'}} || 0) || 
			$a->{'type'} cmp $b->{'type'} ||
			($a->{'project'} || '') cmp ($b->{'project'} || '') ||
			($a->{'job'} || '') cmp ($b->{'job'} || '')
		       } @events;
      }

      eval {
	# the event processing loop
	while (@events) {
	  my $ev = shift @events;
	  $ev->{'type'} ||= 'unknown';

	  # have to be extra careful with uploadbuild events. if the package is in
	  # (delayed)fetchprojpacks, delay event processing until we updated the projpack data.
	  if ($ev->{'type'} eq 'uploadbuild' && event_uploadbuild_delay($ectx, $ev)) {
	    $gotevent = 1;
	    next;
	  }

	  unlink("$myeventdir/$ev->{'evfilename'}") if $ev->{'evfilename'};
	  delete $ev->{'evfilename'};

	  if ($ev->{'type'} ne 'built' && $ectx->{'fullcache'}) {
	    sync_fullcache($ectx->{'fullcache'});
	    $ectx->{'fullcache'} = undef;
	  }
	  if ($ev->{'type'} eq 'built' && !$ectx->{'fullcache'}) {
	    # turn on fullcache if the next event is also of type built
	    $ectx->{'fullcache'} = {} if $events[0] && $events[0]->{'type'} eq 'built';
	  }

          # ignore exit events on initial startup, they are most likely left overs...
          if ($initialstartup && ($ev->{'type'} eq 'exit' || $ev->{'type'} eq 'exitcomplete')) {
	    print "WARNING: there was an exit event, but we ignore it directly after starting the scheduler.";
            next;
          }

	  my $evhandler = $event_handlers{$ev->{'type'}};
	  if ($evhandler) {
	    $evhandler->($ectx, $ev);
	    $notlow = $notmed = 0 if $ev->{'type'} eq 'admincheck';	# HACK
	  } else {
	    print "unknown event type '$ev->{'type'}'\n";
	  }
	}

	sync_fullcache($ectx->{'fullcache'}) if $ectx->{'fullcache'} && $ectx->{'fullcache'}->{'prp'};

	do_fetchprojpacks($ectx, $asyncmode) if %{$ectx->{'fetchprojpacks'}};
      };
      if ($@) {
        warn($@);
        event_exit($ectx, {'type' => 'emergencydump'});
        exit(1);
      }

      # add all changed_high entries to changed_med to make things simpler
      for (keys %changed_high) {
	next if $changed_med{$_} && $changed_med{$_} == 2;
	$changed_med{$_} = $changed_high{$_};
      }
      next;
    }
    # done with first time event processing
    $initialstartup = undef;

    # mark all indirect affected repos dirty
    for my $prp (keys %changed_dirty) {
      next if ! -d "$reporoot/$prp/$myarch";
      next if   -e "$reporoot/$prp/$myarch/:schedulerstate.dirty";
      BSUtil::touch("$reporoot/$prp/$myarch/:schedulerstate.dirty");
    }
    %changed_dirty = ();

    my @ltim = localtime(time);
    my $msgtm = sprintf "%04d-%02d-%02d %02d:%02d:%02d:", $ltim[5] + 1900, $ltim[4] + 1, @ltim[3,2,1,0];

    sub check_queue {
      my ($lookat, $nextmed) = @_;
      my $prp = shift @$lookat;

      if ($nextmed && $nextmed->{$prp}) {
	my $now = time();
	my @notyet;
	while ($nextmed->{$prp} && $now < $nextmed->{$prp}) {
	  print "  not yet $prp\n";
	  push @notyet, $prp;
	  $prp = shift @$lookat;
	  last unless defined $prp;
	}
	unshift @$lookat, @notyet;
      }
      return $prp;
    }

    # if lookat_low array is empty, start new series with lookat_next
    if (!@lookat_low && %lookat_next) {
      @lookat_low = grep {$lookat_next{$_}} @prps;
      %lookat_next = ();
    }

    my $prp;
    my $lookattype;
    while (1) {
      $lookattype = 'low',  last if @lookat_low && $notlow > 10 && defined($prp = check_queue(\@lookat_low));
      $notlow = 0 if $notlow > 10;	# don't try so often
      $lookattype = 'med',  last if @lookat_med && $notmed > 2  && defined($prp = check_queue(\@lookat_med,  \%nextmed));
      $notmed = 0 if $notmed > 2;	# don't try so often
      $lookattype = 'high', last if @lookat_high                && defined($prp = check_queue(\@lookat_high, \%nextmed));
      $lookattype = 'med',  last if @lookat_med                 && defined($prp = check_queue(\@lookat_med,  \%nextmed));
      $lookattype = 'low',  last if @lookat_low                 && defined($prp = check_queue(\@lookat_low));
      $lookattype = 'high', last if @lookat_high                && defined($prp = check_queue(\@lookat_high));
      $lookattype = 'med',  last if @lookat_med                 && defined($prp = check_queue(\@lookat_med));
      last;
    }

    # postpone if we got source change RPCs running
    if (defined($prp)) {
      my ($projid) = split('/', $prp, 2);
      if ($iswaiting{$projid}) {
	my $rhandle = $iswaiting{$projid};
	push @{$rhandle->{'_wakeup'}}, [ $prp, {'changetype' => $lookattype} ];
	next;
      }
    }

    if (%iswaiting) {
      print "running async RPC requests:\n";
      for my $server (sort keys %iswaiting_server) {
	next unless %{$iswaiting_server{$server} || {}};
	print "  - $server: ".scalar(keys %{$iswaiting_server{$server}})." running, ".@{$iswaiting_serverload{$server} || []}."/".@{$iswaiting_serverload_low{$server} || []}." waiting\n";
      }
    }

    if (!defined($prp)) {
      if ($testmode && !%iswaiting) {
	print "Test mode, all sources and events processed, exiting...\n";
	exit(0);
      }
      my @ltim = localtime(time);
      my $msgtm = sprintf "%04d-%02d-%02d %02d:%02d:%02d:", $ltim[5] + 1900, $ltim[4] + 1, @ltim[3,2,1,0];
      print "$msgtm waiting for an event...\n";
      exit 0 if $testprojid;
      my $timeout;
      my $sleepstart = time();
      if (%remotewatchers || %iswaiting) {
	select_read($timeout, $pingsock, @retryevents, values %remotewatchers, values %iswaiting);
	$slept += time() - $sleepstart;
	next;
      } else {
	sysread(PING, $dummy, 1, 0);
	$gotevent = 1;
      }
      $slept += time() - $sleepstart;
      next;
    }

    $notmed++;
    $notlow++;
    if ($lookattype eq 'low') {
      @lookat_high = grep {$_ ne $prp} @lookat_high;
      @lookat_med = grep {$_ ne $prp} @lookat_med;
      $notlow = 0;
    } elsif ($lookattype eq 'med') {
      @lookat_high = grep {$_ ne $prp} @lookat_high;
      $notmed = 0;
    } else {
      @lookat_med = grep {$_ ne $prp} @lookat_med;
    }
    print "$msgtm looking at $lookattype prio $prp";
    print " (".@lookat_high."/".@lookat_med."/".@lookat_low."/".(keys %lookat_next)."/".@prps.")\n";
    delete $nextmed{$prp};

    my ($projid, $repoid) = split('/', $prp, 2);
    next if $testprojid && $projid ne $testprojid;

    if (!defined($repoid)) {
      # project maintenance, check for deleted repositories
      my %repoids;
      for my $repo (@{($projpacks->{$projid} || {})->{'repository'} || []}) {
	$repoids{$repo->{'name'}} = 1 if grep {$_ eq $myarch} @{$repo->{'arch'} || []};
      }
      for my $repoid (ls("$reporoot/$projid")) {
	next if $repoid eq ':all';	# XXX
	next if $repoids{$repoid};
	my $prp = "$projid/$repoid";
	next if -l "$reporoot/$prp";	# XXX
	next unless -d "$reporoot/$prp/$myarch";
	# we no longer build this repoid
	print "  - deleting repository $prp\n";
	delete $prpfinished{$prp};
	delete $prpnotready{$prp};
	delete $prpunfinished{$prp};
	delete $prpchecktimes{$prp};
	delete $repodatas{$prp};
	delete $lastcheck{$prp};
	for my $dir (ls("$reporoot/$prp/$myarch")) {
	  # need lock for deleting publish area
	  next if $dir eq ':repo' || $dir eq ':repoinfo';
	  if (-d "$reporoot/$prp/$myarch/$dir") {
	    BSUtil::cleandir("$reporoot/$prp/$myarch/$dir");
	    rmdir("$reporoot/$prp/$myarch/$dir") || die("$reporoot/$prp/$myarch/$dir: $!\n");
	  } else {
	    unlink("$reporoot/$prp/$myarch/$dir") || die("$reporoot/$prp/$myarch/$dir: $!\n");
	  }
	}
	$changed_med{$prp} = 2;
	sendrepochangeevent($prp);
	killbuilding($prp);
	prpfinished($prp);
	# now that :repo is gone we can remove the directory
	while (!rmdir("$reporoot/$prp/$myarch")) {
	  die("$reporoot/$prp/$myarch: $!\n") unless -e "$reporoot/$prp/$myarch/:schedulerstate.dirty";
	  print "rep server created dirty file $reporoot/$prp/$myarch/:schedulerstate.dirty, retry ...\n";
	  unlink("$reporoot/$prp/$myarch/:schedulerstate.dirty");
	}
	# XXX this should be rewritten if :repoinfo lives somewhere else
	my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
	if (!$repo) {
	  # this repo doesn't exist any longer!
	  my $others;
	  for (ls("$reporoot/$prp")) {
	    next unless -d $_;
	    $others = 1;
	  }
	  if (!$others) {
	    unlink("$reporoot/$prp/:repoinfo");
	    unlink("$reporoot/$prp/.finishedlock");
	    rmdir("$reporoot/$prp");
	  }
	}
      }
      rmdir("$reporoot/$projid");		# in case this was the last repo
      next;
    }

    # do delayed projpack fetches
    while ($delayedfetchprojpacks{$projid}) {
      my %packids = map {$_ => 1} @{$delayedfetchprojpacks{$projid}};
      delete $delayedfetchprojpacks{$projid};
      my $ctx = { 'prp' => $prp, 'gctx' => $gctx, 'changetype' => $lookattype };
      if ($packids{'/all'}) {
	if ($asyncmode) {
	  get_projpacks($ctx, {}, $projid);
          goto NEXTPRP;
	}
	get_projpacks($ctx, undef, $projid);
	get_projpacks_postprocess();
	%packids = ();
      }
      if (%packids) {
	if ($asyncmode) {
	  get_projpacks($ctx, {}, $projid, sort keys %packids);
          goto NEXTPRP;
	}
        my @packids = sort keys %packids;
	my $oldprojdata = clone_projpacks_part($projid, \@packids);
	get_projpacks($ctx, undef, $projid, @packids);
	get_projpacks_postprocess() if postprocess_needed_check($projid, $oldprojdata);
      }
    }

    if (!$prpsearchpath{$prp}) {
      next if $remoteprojs{$projid};
      print "  - $prp: no longer exists\n";
      next;
    }

    # merge bininfo
    if (-e "$reporoot/$prp/$myarch/:bininfo.merge" || ! -e "$reporoot/$prp/$myarch/:bininfo") {
      read_gbininfo("$reporoot/$prp/$myarch");
      $repounchanged{$prp} = 2 if $repounchanged{$prp};
    }

    # merge relsync
    if (-e "$reporoot/$prp/$myarch/:relsync.merge") {
      print "    merging relsync data\n";
      my $relsync_merge = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync.merge", 2);
      if ($relsync_merge) {
	my $relsync;
	$relsync = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync", 2) if -e "$reporoot/$prp/$myarch/:relsync";
	$relsync = { %{$relsync || {}}, %$relsync_merge };
	BSUtil::store("$reporoot/$prp/$myarch/.:relsync", "$reporoot/$prp/$myarch/:relsync", $relsync);
      }
      unlink("$reporoot/$prp/$myarch/:relsync.merge");
    }

    # merge metacache
    if (-e "$reporoot/$prp/$myarch/:full.metacache.merge") {
      print "    merging metacache data\n";
      my $metacache_merge = BSUtil::retrieve("$reporoot/$prp/$myarch/:full.metacache.merge", 2);
      if ($metacache_merge) {
	my $metacache;
	$metacache = BSUtil::retrieve("$reporoot/$prp/$myarch/:full.metacache", 2) if -e "$reporoot/$prp/$myarch/:full.metacache";
	$metacache = { %{$metacache || {}}, %$metacache_merge };
	delete $metacache->{$_} for grep {!defined($metacache_merge->{$_})} keys %$metacache_merge;
	if (%$metacache) {
	  BSUtil::store("$reporoot/$prp/$myarch/.:full.metacache", "$reporoot/$prp/$myarch/:full.metacache", $metacache);
	} else {
	  unlink("$reporoot/$prp/$myarch/:full.metacache");
	}
      }
      unlink("$reporoot/$prp/$myarch/:full.metacache.merge");
    }

    my $bconf = getconfig($myarch, $prpsearchpath{$prp});
    if (!$bconf) {
      # see if it is caused by a remote error
      my $error;
      for my $pprp (@{$prpsearchpath{$prp} || []}) {
	my ($pprojid, $prepoid) = split('/', $pprp, 2);
	$error = $remoteprojs{$pprojid}->{'error'} if $remoteprojs{$pprojid} && $remoteprojs{$pprojid}->{'error'};
	if ($error) {
	  if ($error =~ /interconnect error:/) {
	    addretryevent({'type' => 'project', 'project' => $pprojid});
	  }
	  print "  - $prp: $pprojid: $error\n";
	  last;
	}
      }
      next if $error;
      my $lastprojid = (split('/', $prpsearchpath{$prp}->[-1]))[0];
      print "  - $prp: no config ($lastprojid)\n";
      set_repo_state($prp, 'broken', "no config ($lastprojid)");
      $prpfinished{$prp} = 1;
      next;
    }

    my $prptype = $bconf->{'type'};
    if (!$prptype || $prptype eq 'UNDEFINED') {
      my $lastprojid = (split('/', $prpsearchpath{$prp}->[-1]))[0];
      print "  - $prp: bad config ($lastprojid)\n";
      set_repo_state($prp, 'broken', "bad config ($lastprojid)");
      $prpfinished{$prp} = 1;
      next;
    }
    if ($bconf->{'hostarch'} && !$BSCando::knownarch{$bconf->{'hostarch'}}) {
      print "  - $prp: bad hostarch ($bconf->{'hostarch'})\n";
      set_repo_state($prp, 'broken', "bad hostarch ($bconf->{'hostarch'})");
      $prpfinished{$prp} = 1;
      next;
    }
    my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
    if (!$repo) {
      print " - $prp: no repo?\n";
      set_repo_state($prp, 'broken', 'no repo');
      $prpfinished{$prp} = 1;
      next;
    }

    print "  - $prp\n";

    if (!$lastcheck{$prp}) {
      my $oldlastcheck = BSUtil::retrieve("$reporoot/$prp/$myarch/:lastcheck", 1) || {};
      my $packs = $projpacks->{$projid}->{'package'} || {};
      for (keys %$oldlastcheck) {
	# delete old cruft
	delete $oldlastcheck->{$_} unless $packs->{$_};
      }
      $lastcheck{$prp} = $oldlastcheck;
    }
    my $ctx = { 'project' => $projid, 'repository' => $repoid, 'prp' => $prp,
		'repo' => $repo, 'gctx' => $gctx, 'changetype' => $lookattype,
		'prpsearchpath' => $prpsearchpath{$prp} || [], 'conf' => $bconf,
		'prpfinished' => \%prpfinished, 'prpnotready' => \%prpnotready,
		'lastcheck' => $lastcheck{$prp} };

    if ($repo->{'status'} && $repo->{'status'} eq 'disabled') {
      print "      disabled\n";
      set_repo_state($prp, 'disabled');
      $prpfinished{$prp} = 1;
      next;
    }

    mkdir_p("$reporoot/$prp/$myarch");
    set_repo_state($prp, 'scheduling');

    my $packs = $projpacks->{$projid}->{'package'} || {};
    my @packs = sort keys %$packs;

    # XXX: setup packid2info hash?

    # Step 2a: check if packages got deleted/excluded
    for my $packid (grep {!/^[:\.]/} ls("$reporoot/$prp/$myarch")) {
      if (!$packs->{$packid}) {
	next if $packid eq '_deltas';
	next if $projpacks->{$projid}->{'missingpackages'};
	print "      - $packid: is obsolete\n";
      } else {
	my $pdata = $packs->{$packid};
	if (($pdata->{'error'} || '') eq 'excluded') {
	  print "      - $packid: is excluded\n";
	} else {
	  my %info = map {$_->{'repository'} => $_} @{$pdata->{'info'} || []};
	  my $info = $info{$repoid};
	  next unless $info && ($info->{'error'} || '') eq 'excluded';
	  print "      - $packid: is excluded\n";
	}
      }
      delete $lastcheck{$prp}->{$packid};
      my $gdst = "$reporoot/$prp/$myarch";
      # delete full entries
      my $useforbuildenabled = 1;
      $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
      # hmm, need to exclude patchinfos here. cheating.
      $useforbuildenabled = 0 if -s "$gdst/$packid/.updateinfodata";
      update_dst_full($prp, $packid, "$gdst/$packid" , undef, undef, $useforbuildenabled, $prpsearchpath{$prp});
      $changed_med{$prp} = 2;
      sendrepochangeevent($prp);
      # delete other files
      unlink("$gdst/:logfiles.success/$packid");
      unlink("$gdst/:logfiles.fail/$packid");
      unlink("$gdst/:meta/$packid");
      BSUtil::cleandir("$gdst/$packid");
      rmdir("$gdst/$packid");
      killbuilding($prp, $packid);
      unlink("$reporoot/$prp/$myarch/:repodone");
    }


    # Step 2b: set up pool and repositories
    my $pool = BSSolv::pool->new();
    $pool->settype('deb') if $bconf->{'binarytype'} eq 'deb';
    $ctx->{'pool'} = $pool;

    my %building;
    my %dep2src;
    my %dep2pkg;
    my %depislocal;	# used in meta calculation
    my $error;
    my %unfinished;	# is blocked or needs rebuild
    my %notready;		# unfinished and will modify :full

    my $delayed;
    for my $rprp (@{$prpsearchpath{$prp}}) {
      if (!checkprpaccess($rprp, $prp)) {
	$error = "repository '$rprp' is unavailable";
	last;
      }
      my $r = addrepo($ctx, $pool, $rprp);
      if (!$r) {
	$delayed = 1 if defined $r;
	$error = "repository '$rprp' is unavailable";
	last;
      }
    }
    if ($error) {
      print "    $error\n";
      $ctx->{'havedelayed'} = 1 if $delayed;
      set_repo_state($prp, 'broken', $error) unless $delayed;
      next;
    }
    
    $pool->createwhatprovides();
    for my $p ($pool->consideredpackages()) {
      my $rprp = $pool->pkg2reponame($p);
      my $n = $pool->pkg2name($p);
      $dep2pkg{$n} = $p;
      $dep2src{$n} = $pool->pkg2srcname($p) || $n;
      if ($rprp eq $prp) {
	$depislocal{$n} = 1;
      } else {
	$notready{$n} = 2 if $prpnotready{$rprp} && $prpnotready{$rprp}->{$n};
      }
    }
    $ctx->{'building'} = \%building;
    $ctx->{'notready'} = \%notready;
    $ctx->{'dep2pkg'} = \%dep2pkg;
    $ctx->{'dep2src'} = \%dep2src;
    $ctx->{'depislocal'} = \%depislocal;

    if ($repo->{'block'} && $repo->{'block'} eq 'local') {
      for (keys %notready) {
	delete $notready{$_} if $notready{$_} == 2;
      }
    }

    my $xp = BSSolv::expander->new($pool, $bconf);
    my $ownexpand = sub {
      $_[0] = $xp;
      goto &BSSolv::expander::expand;
    };
    no warnings 'redefine';
    local *Build::expand = $ownexpand;
    use warnings 'redefine';

    my $prpchecktime = time();

    # Step 2c: expand all dependencies, put them in %pdeps hash
    my %subpacks;
    push @{$subpacks{$dep2src{$_}}}, $_ for keys %dep2src;
    print "    expanding dependencies\n";
    my %experrors;
    $ctx->{'subpacks'} = \%subpacks;

    my %pdeps;
    my %pkg2src;
    my %pkgdisabled;
    my %havepatchinfos;
    my %pkg2packtype;
    for my $packid (@packs) {
      my $pdata = $packs->{$packid};

      if ($pdata->{'error'} && $pdata->{'error'} eq 'excluded') {
	$pdeps{$packid} = [];
	next;
      }

      my $info = (grep {$_->{'repository'} eq $repoid} @{$pdata->{'info'} || []})[0];

      # calculate package type
      my $packtype = 'unknown';
      if ($pdata->{'aggregatelist'}) {
	$packtype = 'aggregate';
      } elsif ($pdata->{'patchinfo'}) {
	$packtype = 'patchinfo';
      } elsif ($info && $info->{'file'}) {
	if ($info->{'file'} eq 'PKGBUILD') {
	  $packtype = 'arch';
	} elsif ($info->{'file'} eq '_preinstallimage') {
	  $packtype = 'preinstallimage';
	} elsif ($info->{'file'} =~ /\.(spec|dsc|kiwi)$/) {
	  $packtype = $1;
	  if ($packtype eq 'kiwi') {
	    # check if it is kiwi-product
	    if ($info->{'imagetype'} && $info->{'imagetype'}->[0] eq 'product') {
	      $packtype = 'kiwi-product';
	    } else {
	      $packtype = 'kiwi-image';
	    }
	  }
	}
      }
      $pkg2packtype{$packid} = $packtype;
      $havepatchinfos{$packid} = 1 if $packtype eq 'patchinfo';

      if (!$info || !defined($info->{'file'}) || !defined($info->{'name'})) {
	if ($pdata->{'error'} && $pdata->{'error'} eq 'disabled') {
	  $pkgdisabled{$packid} = 1;
	}
	if ($info && $info->{'error'} && $info->{'error'} eq 'disabled') {
	  $pkgdisabled{$packid} = 1;
	}
	$pdeps{$packid} = [];
	next;
      }
      if ($info->{'error'} && $info->{'error'} eq 'excluded') {
	$pdeps{$packid} = [];
	next;
      }
      if (exists($pdata->{'originproject'})) {
	# this is a package from a project link
	if (!$repo->{'linkedbuild'} || ($repo->{'linkedbuild'} ne 'localdep' && $repo->{'linkedbuild'} ne 'all')) {
	  $pdeps{$packid} = [];
	  next;
	}
      }
      $pkg2src{$packid} = $info->{'name'};

      my @deps = @{$info->{'dep'} || []};
      my ($eok, @edeps) = ($handlers{$packtype} || $unknownchecker)->{'expand'}->($bconf, $subpacks{$info->{'name'}}, @deps);
      if (! $eok) {
	$experrors{$packid} = join(', ', @edeps) || '?';
	@edeps = @deps;
      }
      $pdeps{$packid} = \@edeps;
    }
    $ctx->{'edeps'} = \%pdeps;

    # sort packages by pdeps
    print "    sorting ".@packs." packages\n";
    my @cycles;
    @packs = sortpacks(\%pdeps, \%dep2src, \@cycles, @packs);
    if (%havepatchinfos) {
      # bring patchinfos to back
      my @packs_patchinfos = grep {$havepatchinfos{$_}} @packs;
      @packs = grep {!$havepatchinfos{$_}} @packs;
      push @packs, @packs_patchinfos;
    }

    # write dependency information
    if (%pkgdisabled) {
      # leave info of disabled packages untouched
      my $olddepends = BSUtil::retrieve("$reporoot/$prp/$myarch/:depends", 1);
      if ($olddepends) {
	for (keys %pkgdisabled) {
	  $pdeps{$_} = $olddepends->{'pkgdeps'}->{$_} if $olddepends->{'pkgdeps'}->{$_};
	  $pkg2src{$_} = $olddepends->{'pkg2src'}->{$_} if $olddepends->{'pkg2src'}->{$_};
	}
      }
    }
    my %prunedsubpacks;
    for (values %pkg2src) {
      $prunedsubpacks{$_} = $subpacks{$_} if $subpacks{$_};
    }
    BSUtil::store("$reporoot/$prp/$myarch/.:depends", "$reporoot/$prp/$myarch/:depends", {
      'pkgdeps' => \%pdeps,
      'subpacks' => \%prunedsubpacks,
      'pkg2src' => \%pkg2src,
      'cycles' => \@cycles,
    });
    %prunedsubpacks = ();
    # remove old entries again
    for (keys %pkgdisabled) {
      $pdeps{$_} = [];
      delete $pkg2src{$_};
    }

    # now build cychash mapping packages to all other cycle members
    my %cychash;
    if (@cycles) {
      for my $cyc (@cycles) {
	my %nc = map {$_ => 1} @$cyc;
	for my $p (@$cyc) {
	  next unless $cychash{$p};
	  $nc{$_} = 1 for @{$cychash{$p}};
	}
	my $c = [ sort keys %nc ];
	$cychash{$_} = $c for @$c;
      }
    }

    my $projbuildenabled = 1;
    $projbuildenabled = enabled($repoid, $projpacks->{$projid}->{'build'}, 1) if $projpacks->{$projid}->{'build'};
    my $projlocked = 0;
    $projlocked = enabled($repoid, $projpacks->{$projid}->{'lock'}, 0) if $projpacks->{$projid}->{'lock'};

    # fetch relsync data
    my $relsyncmax;
    my %relsynctrigger;
    if (-s "$reporoot/$prp/$myarch/:relsync.max") {
      $relsyncmax = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync.max", 2);
      if ($relsyncmax && -s "$reporoot/$prp/$myarch/:relsync") {
	my $relsync = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync", 2);
	for my $packid (@packs) {
	  my $tag = $packs->{$packid}->{'bcntsynctag'} || $packid;
	  next unless $relsync->{$packid};
	  next unless $relsync->{$packid} =~ /(.*)\.(\d+)$/;
	  next unless defined($relsyncmax->{"$tag/$1"}) && $2 < $relsyncmax->{"$tag/$1"};
	  $relsynctrigger{$packid} = 1;
	}
      }
      if (%relsynctrigger) {
	# filter failed packages
	for (ls("$reporoot/$prp/$myarch/:logfiles.fail")) {
	  delete $relsynctrigger{$_};
	}
      }
    }
    $ctx->{'relsynctrigger'} = \%relsynctrigger;
    $ctx->{'relsyncmax'} = $relsyncmax;

    # Step 2d: check status of all packages
    my %packstatus;
    my $oldpackstatus;
    my %packerror;
    my @cpacks = @packs;
    my %cycpass;
    my $needed;
    $ctx->{'packstatus'} = \%packstatus;
    $ctx->{'cychash'} = \%cychash;
    $ctx->{'cycpass'} = \%cycpass;
    $ctx->{'nharder'} = 0;

    my $prjuseforbuildenabled = 1;
    $prjuseforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $prjuseforbuildenabled);

    # copy old data over if we have missing packages
    if ($projpacks->{$projid}->{'missingpackages'}) {
      addretryevent({'type' => 'package', 'project' => $projid});
      $oldpackstatus = BSUtil::retrieve("$reporoot/$prp/$myarch/:packstatus", 1) || {};
      $oldpackstatus->{'packstatus'} ||= {};
      $oldpackstatus->{'packerror'} ||= {};
      for my $packid (keys %{$oldpackstatus->{'packstatus'}}) {
	next if $packs->{$packid};
	$packstatus{$packid} = $oldpackstatus->{'packstatus'}->{$packid};
	$packerror{$packid} = $oldpackstatus->{'packerror'}->{$packid} if $oldpackstatus->{'packerror'}->{$packid}; 
      }
    }

    while (@cpacks) {
      my $packid = shift @cpacks;
      my $incycle = 0;
      if ($cychash{$packid}) {
	next if $packstatus{$packid} && $packstatus{$packid} ne 'done'; # already decided in phase 1
	# cycle package, we look at a cycle two times:
	# 1) just trigger package builds caused by source changes
	# 2) normal package build triggering
	# cychash contains all packages of this cycle

	# calculate phase 1 packages
	my @cnext = grep {!$cycpass{$_}} @{$cychash{$packid}};
	if (@cnext) {
	  # still phase1 packages left, do them first
	  unshift @cpacks, $packid;
	  $packid = shift @cnext;
	  $cycpass{$packid} = 1;	# now doinig phase 1
	  $incycle = 1;
	  if (@cnext == 1) {
	    # just one package left in cycle, enter phase 2
	    if (grep {$building{$_}} @{$cychash{$packid}}) {
	      # we are building packages because of source changes,
	      # set cycpass to 2 so that we don't start other builds
	      $cycpass{$_} = 2 for @{$cychash{$packid}};
	    }
	  }
	}
      }
      $ctx->{'incycle'} = $incycle;

      # product definitions are never building themself
      if ($packid eq '_product') {
	$packstatus{$packid} = 'excluded';
	next;
      }

      my $pdata = $packs->{$packid};
      if ($pdata->{'error'}) {
	if ($pdata->{'error'} eq 'disabled' || $pdata->{'error'} eq 'excluded') {
	  $packstatus{$packid} = $pdata->{'error'};
	  next;
	}
	print "      - $packid ($pdata->{'error'})\n";
	if ($pdata->{'error'} =~ /download in progress/) {
	  $packstatus{$packid} = 'blocked';
	  $packerror{$packid} = $pdata->{'error'};
	  next;
	}
	if ($pdata->{'error'} =~ /source update running/ || $pdata->{'error'} =~ /service in progress/) {
	  $packstatus{$packid} = 'blocked';
	  $packerror{$packid} = $pdata->{'error'};
	  next;
	}
	if ($pdata->{'error'} eq 'delayed startup' || $pdata->{'error'} =~ /interconnect error:/) {
	  addretryevent({'type' => 'package', 'project' => $projid, 'package' => $packid});
	  $ctx->{'havedelayed'} = 1;
	  $packstatus{$packid} = 'blocked';
	  $packerror{$packid} = $pdata->{'error'};
	  next;
	}
	$packstatus{$packid} = 'broken';
	$packerror{$packid} = $pdata->{'error'};
	next;
      }

      if (exists($pdata->{'originproject'})) {
	# this is a package from a project link
	if (!$repo->{'linkedbuild'} || ($repo->{'linkedbuild'} ne 'localdep' && $repo->{'linkedbuild'} ne 'all')) {
	  $packstatus{$packid} = 'excluded';
	  $packerror{$packid} = 'project link';
	  next;
	}
      }

      if ($pdata->{'build'}) {
	if (!enabled($repoid, $pdata->{'build'}, $projbuildenabled)) {
	  $packstatus{$packid} = 'disabled';
	  next;
	}
      } else {
	if (!$projbuildenabled) {
	  $packstatus{$packid} = 'disabled';
	  next;
	}
      }

      if ($pdata->{'lock'}) {
	if (enabled($repoid, $pdata->{'lock'}, $projlocked)) {
	  $packstatus{$packid} = 'locked';
	  next;
	}
      } else {
	if ($projlocked) {
	  $packstatus{$packid} = 'locked';
	  next;
	}
      }

      # select correct info again
      my $info = (grep {$_->{'repository'} eq $repoid} @{$pdata->{'info'} || []})[0] || {};

      # name of src package, needed for block detection
      my $pname = $info->{'name'} || $packid;

      if ($info->{'error'}) {
	if ($info->{'error'} eq 'disabled' || $info->{'error'} eq 'excluded') {
	  $packstatus{$packid} = $info->{'error'};
	  next;
	}
	print "      - $packid ($info->{'error'})\n";
	$packstatus{$packid} = 'broken';
	$packerror{$packid} = $info->{'error'};
	next;
      }

      # calculate package build type
      my $packtype = $pkg2packtype{$packid};
      if (!$packtype || $packtype eq 'unknown') {
	print "      - $packid (no spec/dsc/kiwi file)\n";
	$packstatus{$packid} = 'broken';
	$packerror{$packid} = 'no spec/dsc/kiwi file';
	next;
      }
      my $handler = $handlers{$packtype};
      if (!$handler) {
	print "      - $packid (no handler for type $packtype)\n";
	$packstatus{$packid} = 'broken';
	$packerror{$packid} = "no handler for type $packtype";
	next;
      }
      #print "      - $packid ($packtype)\n";

      if (!$incycle) {
	# hmm, this might be a bad idea...
	my $job = jobname($prp, $packid)."-$pdata->{'srcmd5'}";
	if (-s "$myjobsdir/$job") {
	  # print "      - $packid ($packtype)\n";
	  # print "        already scheduled\n";
	  add_crossmarker($bconf, $job) if $bconf->{'hostarch'};
	  my $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $prjuseforbuildenabled);
	  $building{$packid} = $job;
	  $notready{$pname} = 1 if $useforbuildenabled;
	  $unfinished{$pname} = 1;
	  $packstatus{$packid} = 'scheduled';
	  next;
	}
      }

      # now print expandsion errors
      if ($experrors{$packid}) {
	print "      - $packid ($packtype)\n";
	print "        unresolvables:\n";
	print "            $experrors{$packid}\n";
	$packstatus{$packid} = 'unresolvable';
	$packerror{$packid} = $experrors{$packid};
	next;
      }

      # dispatch to handlers
      my ($astatus, $aerror) = $handler->{'check'}->($ctx, $packid, $pdata, $info, $packtype);
      if ($astatus eq 'scheduled') {
	# aerror contains rebuild data in this case
	($astatus, $aerror) = $handler->{'rebuild'}->($ctx, $packid, $pdata, $info, $aerror);
	if ($astatus eq 'scheduled') {
	  $building{$packid} = $aerror || 'job'; # aerror contains jobid in this case
	  undef $aerror;
	} elsif ($astatus eq 'broken' && $aerror =~ /^unresolvable: (.*)/) {
	  ($astatus, $aerror) = ('unresolvable', $1);
	} elsif ($astatus eq 'delayed') {
          $ctx->{'havedelayed'} = 1;
	  ($astatus, $aerror) = ('blocked', defined($aerror) ? "delayed: $aerror" : 'delayed');
	}
	unlink("$reporoot/$prp/$myarch/:repodone");
      } elsif ($astatus eq 'delayed') {
        $ctx->{'havedelayed'} = 1;
	if (!$oldpackstatus) {
	  $oldpackstatus = BSUtil::retrieve("$reporoot/$prp/$myarch/:packstatus", 1) || {};
	  $oldpackstatus->{'packstatus'} ||= {};
	  $oldpackstatus->{'packerror'} ||= {};
	}
	$astatus = $oldpackstatus->{'packstatus'}->{$packid};
	$aerror = $oldpackstatus->{'packerror'}->{$packid};
	($astatus, $aerror) = ('blocked', 'delayed') unless $astatus;
	$unfinished{$pname} = 1;
      }
      $packstatus{$packid} = $astatus;
      $packerror{$packid} = $aerror if defined $aerror;
      if ($astatus eq 'blocked' || $astatus eq 'scheduled') {
	my $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $prjuseforbuildenabled);
	$notready{$pname} = 1 if $useforbuildenabled;
	$unfinished{$pname} = 1;
      }
    }

    # delete global entries from notready
    for (keys %notready) {
      delete $notready{$_} if $notready{$_} == 2;
    }
    # put local notready into prpnotready if not a leaf
    if (%notready && $prpnoleaf{$prp}) {
      $prpnotready{$prp} = \%notready;
    } else {
      delete $prpnotready{$prp};
    }

    # write blocked data into a file so that remote servers can fetch it
    # we don't put it into :packstatus to make retrival fast
    if (%notready) {
      my @blocked = sort keys %notready;
      writexml("$reporoot/$prp/$myarch/.:repostate", "$reporoot/$prp/$myarch/:repostate", {'blocked' => \@blocked}, $BSXML::repositorystate);
    } else {
      unlink("$reporoot/$prp/$myarch/:repostate");
    }

    # building jobs may have changed back to excluded, blocked or disabled, remove the jobs
    my $prpjobs = jobname($prp, '');
    for my $job (grep {/^\Q$prpjobs\E/} sort keys %ourjobs) {
      if ($job =~ /^\Q$prpjobs\E(.*)-[0-9a-f]{32}$/) {
	my $status = $packstatus{$1} || '';
	next if $status eq 'scheduled';
	if (! -e "$myjobsdir/$job") {
	  delete $ourjobs{$job};
	  next;
	}
	if ($status eq 'disabled' || $status eq 'excluded' || $status eq 'locked') {
	  print "        killing old job $job, now in disabled/excluded/locked state\n";
	  killjob($job);
	} elsif ($status eq 'blocked' || $status eq 'unresolvable' || $status eq 'broken') {
	  # blocked jobs get removed, if they are currently not building. building jobs
	  # stay since they may become valid again
	  killscheduled($job);
	}
      }
    }

    # notify remote build services of repository changes or block state
    # changes
    # we alse send it if we finish a prp to give linked aggregates a
    # chance to work
    if (!$repounchanged{$prp} || (!%unfinished && !$prpfinished{$prp})) {
      sendrepochangeevent($prp);
      $repounchanged{$prp} = 1;
    } elsif ($repounchanged{$prp} == 2) {
      sendrepochangeevent($prp, 'repoinfo');
      $repounchanged{$prp} = 1;
    }

    # free memory
    Build::forgetdeps($bconf);

    # write package status for this project
    BSUtil::store("$reporoot/$prp/$myarch/.:packstatus", "$reporoot/$prp/$myarch/:packstatus", {
      'packstatus' => \%packstatus,
      'packerror' => \%packerror,
    });
    unlink("$reporoot/$prp/$myarch/:packstatus.finished");

    $prpchecktime = time() - $prpchecktime;

    # write some stats
    for my $status (sort keys %{{map {$_ => 1} values %packstatus}}) {
      print "    $status: ".scalar(grep {$_ eq $status} values %packstatus)."\n";
    }
    print "    looked harder: $ctx->{'nharder'}\n" if $ctx->{'nharder'};
    print "    building: ".scalar(keys %building).", notready: ".scalar(keys %notready).", unfinished: ".scalar(keys %unfinished)."\n";
    print "    took $prpchecktime seconds to check the packages\n";

    my $schedulerstate;
    my $schedulerdetails;
    if (keys %building) {
      $schedulerstate = 'building';
    } elsif ($ctx->{'havedelayed'} || keys %unfinished) {
      $schedulerstate = 'blocked';
    } else {
      $schedulerstate = 'finished';
    }

    # we always publish kiwi...
    if ((!%unfinished && !$ctx->{'havedelayed'}) || $prptype eq 'kiwi') {
      my $locked = 0;
      $locked = enabled($repoid, $projpacks->{$projid}->{'lock'}, $locked) if $projpacks->{$projid}->{'lock'};
      my $pubenabled = enabled($repoid, $projpacks->{$projid}->{'publish'}, 1);
      my %pubenabled;
      for my $packid (@packs) {
	my $pdata = $packs->{$packid};
        next if defined($pdata->{'lock'}) && enabled($repoid, $pdata->{'lock'}, $locked);
        next if !defined($pdata->{'lock'}) && $locked;
	if ($pdata->{'publish'}) {
	  $pubenabled{$packid} = enabled($repoid, $pdata->{'publish'}, $pubenabled);
	} else {
	  $pubenabled{$packid} = $pubenabled;
	}
      }
      my $repodonestate = $projpacks->{$projid}->{'patternmd5'} || '';
      for my $packid (@packs) {
	$repodonestate .= "\0$packid" if $pubenabled{$packid};
      }
      $repodonestate .= "\0$_" for sort keys %unfinished;
      $repodonestate = Digest::MD5::md5_hex($repodonestate);
      if (@packs && !grep {$_} values %pubenabled) {
	# all packages have publish disabled hint
	$repodonestate = "disabled:$repodonestate";
      }
      if (-e "$reporoot/$prp/$myarch/:repodone") {
	my $oldrepodone = readstr("$reporoot/$prp/$myarch/:repodone", 1) || '';
	unlink("$reporoot/$prp/$myarch/:repodone") if $oldrepodone ne $repodonestate;
      }
      my $publisherror;
      if ($locked) {
	print "    publishing is locked\n";
      } elsif (! -e "$reporoot/$prp/$myarch/:repodone") {
	if (($repodonestate !~ /^disabled/) || -d "$reporoot/$prp/$myarch/:repo") {
	  mkdir_p("$reporoot/$prp/$myarch");
	  $publisherror = prpfinished($prp, \@packs, \%pubenabled, $bconf, $prpsearchpath{$prp});
	} else {
	  print "    publishing is disabled\n";
	}
	writestr("$reporoot/$prp/$myarch/:repodone", undef, $repodonestate) unless $publisherror || %unfinished;
	$schedulerdetails = $publisherror if $publisherror;
      }
      if (!%unfinished && !$publisherror) {
	$prpfinished{$prp} = 1;
	# write out lastcheck cache and delete it
	if ($lastcheck{$prp} && %{$lastcheck{$prp}}) {
	  BSUtil::store("$reporoot/$prp/$myarch/.:lastcheck", "$reporoot/$prp/$myarch/:lastcheck", $lastcheck{$prp}) if $lastcheck{$prp};
	} else {
	  unlink("$reporoot/$prp/$myarch/:lastcheck");
	}
	delete $lastcheck{$prp};
	# delete pkg meta cache
	delete $repodatas{$prp}->{'meta'} if $repodatas{$prp};
	if (!$prpnoleaf{$prp}) {
	  # only free repo data if all projects we depend on are finished, too.
	  # (we always have to do the expansion if something changes)
	  if (! grep {!$prpfinished{$_}} @{$prpdeps{$prp}}) {
	    print "    leaf prp, freeing data\n";
	    delete $repodatas{$prp};
	  }
	}
      }
    } else {
      delete $prpfinished{$prp};
      unlink("$reporoot/$prp/$myarch/:repodone");
    }

    set_repo_state($prp, $schedulerstate, $schedulerdetails);

    if (%unfinished) {
      $prpunfinished{$prp} = scalar(keys %unfinished);
    } else {
      delete $prpunfinished{$prp};
    }
    $prpchecktimes{$prp} = $prpchecktime;

    # send relsync file if something has been changed
    my @relsync1 = stat("$reporoot/$prp/$myarch/:relsync");
    my @relsync2 = stat("$reporoot/$prp/$myarch/:relsync.sent");
    if (@relsync1 && (!@relsync2 || "$relsync1[9]/$relsync1[7]/$relsync1[1]" ne "$relsync2[9]/$relsync2[7]/$relsync2[1]")) {
      print "    updating relsync information\n";
      my $relsync = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync") || {};
      my $relsyncmax = {};
      for my $packid (sort keys %$relsync) {
	next unless $relsync->{$packid} =~ /^(.*)\.([^-]*)$/;
	my $tag = ($packs->{$packid} || {})->{'bcntsynctag'} || $packid;
	next if defined($relsyncmax->{"$tag/$1"}) && $relsyncmax->{"$tag/$1"} >= $2;
	$relsyncmax->{"$tag/$1"} = $2;
      }
      updaterelsyncmax($prp, $myarch, $relsyncmax, %unfinished ? 0 : 1);
      my $relsyncdata = "pst0".Storable::nfreeze($relsyncmax);
      # sent new data!
      my $param = {
	'uri' => "$BSConfig::srcserver/relsync",
	'request' => 'POST',
	'data' => $relsyncdata,
      };
      eval {
	BSRPC::rpc($param, undef, "project=$projid", "repository=$repoid", "arch=$myarch");
      };
      if (!$@) {
	unlink("$reporoot/$prp/$myarch/:relsync$$");
	link("$reporoot/$prp/$myarch/:relsync", "$reporoot/$prp/$myarch/:relsync$$");
	rename("$reporoot/$prp/$myarch/:relsync$$", "$reporoot/$prp/$myarch/:relsync.sent");
      } else {
	warn($@);
      }
    }

    cleanup_remotepackstatus($prp) if $remotepackstatus_cleanup{$prp} && !$ctx->{'havedelayed'};

    my $now = time();
    if ($prpchecktime) {
      $nextmed{$prp} = $now + 10 * $prpchecktime;
    } else {
      delete $nextmed{$prp};
    }
    $prplastcheck{$prp} = $now;

    if ($now - $lastschedinfo > 60) {
      # update scheduler stats
      my $sinfo = {'arch' => $myarch, 'started' => $schedulerstart, 'time' => $now, 'slept' => $slept};
      $sinfo->{'projects'} = keys %$projpacks;
      $sinfo->{'repositories'} = @prps;
      my $unfinishedsum = 0;
      $unfinishedsum += $_ for values %prpunfinished;
      $sinfo->{'notready'} = $unfinishedsum;
      $sinfo->{'queue'} = {};
      $sinfo->{'queue'}->{'high'} = @lookat_high;
      $sinfo->{'queue'}->{'med'} = @lookat_med;
      $sinfo->{'queue'}->{'low'} = @lookat_low;
      $sinfo->{'queue'}->{'next'} = keys %lookat_next;
      my $sum = 0;
      my $sum2 = 0;
      my $n = keys %prpchecktimes;
      for my $prp (sort keys %prpchecktimes) {
	my $t = $prpchecktimes{$prp};
	$sum += $t;
	$sum2 += $t * $t;
      }
      $sinfo->{'avg'} = $sum / $n;
      $sinfo->{'variance'} = sqrt(abs(($sum2 - $sum * $sum / $n) / $n));
      for my $prp (splice(@{[sort {$prpchecktimes{$b} <=> $prpchecktimes{$a}} keys %prpchecktimes]}, 0, 10)) {
	my ($projid, $repoid) = split('/', $prp, 2);
	my $worst = {'project' => $projid, 'repository' => $repoid};
	$worst->{'packages'} = keys %{($projpacks->{$projid} || {})->{'package'} || {}};
	$worst->{'time'} = $prpchecktimes{$prp};
	push @{$sinfo->{'worst'}}, $worst;
      }
      $sinfo->{'buildavg'} = $buildavg;
      writexml("$infodir/.schedulerinfo.$myarch", "$infodir/schedulerinfo.$myarch", $sinfo, $BSXML::schedulerinfo);
      $lastschedinfo = $now;
    }
  }

};

if ($@) {
  warn($@);
  event_exit($gctx, {'type' => 'emergencydump'});
  exit(1);
}

exit(0);
