#!/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 $bsdir = $BSConfig::bsdir || "/srv/obs";

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

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");
  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 $md5off = shift;
  my $fromp = shift;
  my $top = shift;

  my @ret = ();
  my @from = map {[$_, substr($_, 0, $md5off).substr($_, $md5off+($md5off ? 33 : 34))]} @$fromp;
  my @to   = map {[$_, substr($_, 0, $md5off).substr($_, $md5off+($md5off ? 33 : 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 {/\.(?:rpm|deb|iso)$/} @$dir;
  } else {
    @bins = ls($dir);
    @bins = map {"$dir/$_"} grep {/\.(?:rpm|deb|iso|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

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

  my $now = time();
  if ($repodatas{$prp} && $repodatas{$prp}->{'lastscan'} && $repodatas{$prp}->{'lastscan'} > $now - 8*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 $dir = "$reporoot/$prp/$myarch/:full";
    if (-s "$dir.solv") {
      my $r;
      eval {$r = $pool->repofromfile($prp, "$dir.solv");};
      return $r if $r;
    }
  }
  delete $repodatas{$prp}->{'solv'};
  delete $repodatas{$prp}->{'lastscan'};
  my ($projid, $repoid) = split('/', $prp, 2);
  if ($remoteprojs{$projid}) {
    return addrepo_remote($pool, $prp, $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 {/\.(?:rpm|deb)$/} readdir(D);
    closedir D;
    if (!@bins && -s "$dir.subdirs") {
      for my $subdir (split(' ', readstr("$dir.subdirs"))) {
        push @bins, map {"$subdir/$_"} grep {/\.(?:rpm|deb)$/} 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();
  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";
    $c = defined($1) ? $1 : '' if $c =~ /^(.*\n)?\s*macros:[^\n]*\n/si;
    $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'}};
  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";
      unlink("$myjobsdir/$job");
      unlink("$myjobsdir/$job:status");
      close F;
      delete $ourjobs{$job};
      return;
    }
    # lock failed, dispatcher was faster!
    die("$myjobsdir/$job:status: $!\n") unless -e "$myjobsdir/$job:status";
  }
  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)) {
    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;
  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';

  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 = ( @{$info->{'dep'} || []}, @{$info->{'prereq'} || []} );

  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->{"$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'};
  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 @roles, @{$pdata->{'person'} || []}, @{$pdata->{'group'} || []};
  }
  push @roles, @{$oproj->{'person'} || []}, @{$oproj->{'group'} || []};
  while ($oprojid =~ /^(.+):/) {
    $oprojid = $1;
    $oproj = $projpacks->{$oprojid} || {};
    push @roles, @{$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;
    }    
    if (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. 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) = @_;

  my ($projid, $repoid) = split('/', $prp, 2);
  my $ev = {
    '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.new", "$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");
    }
  }

  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;
      my @binstat = stat("$pdir/$bin");
      next unless @binstat;

      # 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. this does not work for
	# projects that have publishing disabled, like "openSUSE:*"
	# as a workaround, we also look at $bsdir/deltarepo/$aprp
	if (!$oldbins{"$aprp/$binarch"}) {
	  my $aextrep = $aprp;
	  $aextrep =~ s/:/:\//g;
	  $aextrep = "$extrepodir/$aextrep";
	  $aextrep = "$bsdir/deltarepo/$aprp" if ! -d $aextrep && -d "$bsdir/deltarepo/$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;
	if (@cand > 1) {
	  # sort version/release  FIXME: epoch? use file mtime instead?
	  @cand = sort { Build::Rpm::verscmp($b, $a) } @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 (-e "$ddir/$deltaid" && (-e "$ddir/$deltaid.dseq" || ! -s "$ddir/$deltaid")) {
	    # make sure we don't already have this one
	    if (!grep {$_->[1] eq $obin} @{$havedelta{"$packid/$bin"} || []}) {
	      push @{$havedelta{"$packid/$bin"}}, [ $deltaid, $obin ];
	    }
	  } else {
	    $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;

  # 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
      if (!$rinfo) {
	$rinfo = {};
	$rinfo = BSUtil::retrieve("${rdir}info") if -s "${rdir}info";
	$rinfo->{'binaryorigins'} ||= {};
      }
      print "        $packid: publishing disabled\n";
      my @all = grep {$rinfo->{'binaryorigins'}->{$_} eq $packid} keys %{$rinfo->{'binaryorigins'}};
      for my $bin (@all) {
        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;
    my $nosourceaccess = grep {$_ eq '.nosourceaccess'} @all;
    @all = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne '.bininfo' && $_ ne 'reason' && $_ ne '.nosourceaccess'} @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 $rinfo = BSUtil::treeinfo("$rdir/$rbin");
	  my $pinfo = BSUtil::treeinfo("$pdir/$bin");
	  next if join(',', @$rinfo) eq join(',', @$pinfo);
	}
        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;
    }
  }
  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',
  };
  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 = $rp;
    $suf =~ s/.*\.//;
    push @replaced, {'name' => "$r->{'name'}.$suf", 'id' => $r->{'id'}};
  }
  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;
}

#
# 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);

  # 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;
  if (defined($jobdir)) {
    @jobfiles = sort(ls($jobdir));
    @jobfiles = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne 'reason' && $_ ne '.bininfo'} @jobfiles;
    my $cache;
    if (-e "$jobdir/.bininfo") {
      $cache = BSUtil::retrieve("$jobdir/.bininfo", 1);
      unlink("$jobdir/.bininfo");
    }
    $jobrepo = findbins_dir([ map {"$jobdir/$_"} grep {/\.(?:rpm|deb)$/ && !/\.delta\.rpm$/} @jobfiles ], $cache);
  } else {
    $jobrepo = {};
  }

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

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

  my $oldrepo;
  my $isimport;

  if ($dst && $jobdir && $dst eq $jobdir) {
    # a "refresh" operation, nothing to do here
    $oldrepo = $jobrepo;
  } 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 {/\.(?:rpm|deb)$/ && !/\.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;
    }
    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.
    BSUtil::touch("$dst/.nosourceaccess") unless checkaccess('sourceaccess', $projid, $packid, $repoid);
  } 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
  }

  if (!$isimport) {
    # write .bininfo file
    my $bininfo = '';
    for my $rp (sort keys %$jobrepo) {
      my $nn = $rp;
      $nn =~ s/.*\///;
      $bininfo .= "$jobrepo->{$rp}->{'hdrmd5'}  $nn\n";
    }
    writestr("$dst/.bininfo.new", "$dst/.bininfo", $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;
  if ($fullcache && $fullcache->{'old'}) {
    $pool = $fullcache->{'pool'};
    $satrepo = $fullcache->{'satrepo'};
    %old = %{$fullcache->{'old'}};
  } else {
    $pool = BSSolv::pool->new();
    eval { $satrepo = $pool->repofromfile($prp, "$gdst/:full.solv"); };
    %old = $satrepo->getpathid() if $satrepo;
  }

  # 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;
  for my $rp (@movetofull) {
    my $r = $new{$rp};
    my $suf = $rp;
    $suf =~ s/.*\.//;
    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]";
    if ($suf eq 'rpm') {
      unlink("$gdst/:full/$n.deb");
      delete $old{"$n.deb"};
    } else {
      unlink("$gdst/:full/$n.rpm");
      delete $old{"$n.rpm"};
    }
    if ($meta) {
      if ($BSConfig::maxmetahardlink && ++$linkedmeta >= $BSConfig::maxmetahardlink) {
	# workaround for btrfs hardlink limitation. sigh.
	writestr("$meta.dup", $meta, readstr($meta));
	$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");
    } else {
      unlink("$gdst/:full/$n.meta");
    }
    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 = $rp;
    $suf =~ s/.*\.//;
    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";
    unlink("$gdst/:full/$n.rpm");
    unlink("$gdst/:full/$n.deb");
    unlink("$gdst/:full/$n.iso");
    unlink("$gdst/:full/$n.meta");
    unlink("$gdst/:full/$n-MD5SUMS.meta");
    delete $old{"$n.rpm"};
    delete $old{"$n.deb"};
    delete $dep2meta->{$n} if $dep2meta;
  }
  
  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;
  } 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);
  }
  delete $repodatas{$prp}->{'solv'};
}

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'};
  %$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 %prpnotready;	# maps prp => { packid => 1, ... }

my %watchremoteprojs;	# tmp, only set in addwatchremote

my @retryevents;


#
# get_projpacks:  get/update project/package information
#
# input:  $projid: update just this project
#         $packid: update just this package
# output: $projpacks (global)
#

sub get_projpacks {
  my ($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) {
      delete $projpacks->{$projid}->{'package'}->{$packid} if $projpacks->{$projid} && $projpacks->{$projid}->{'package'};
      push @args, "package=$packid";
    }
  } elsif (defined($projid)) {
    print "getting data for project '$projid' from $BSConfig::srcserver\n";
    push @args, "project=$projid";
    delete $projpacks->{$projid};
  } else {
    print "getting data for all projects from $BSConfig::srcserver\n";
    $projpacks = {};
  }
  my $projpacksin;
  while (1) {
    push @args, 'nopackages' if $testprojid && $projid ne $testprojid;
    for my $tries (4, 3, 2, 1, 0) {
      eval {
        $projpacksin = BSRPC::rpc("$BSConfig::srcserver/getprojpack", $BSXML::projpack, 'withsrcmd5', 'withdeps', 'withrepos', 'withconfig', "arch=$myarch", @args);
      };
      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();
        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;
  }

  # Be sure that this is the right source server for my binary packages
  die("ERROR: source server did not report a repoid") unless $projpacksin->{'repoid'};
  my $buildrepoid;
  eval {
    $buildrepoid = readstr("$reporoot/_repoid");
  };
  unless ($buildrepoid) {
    # set the repoid on first run
    $buildrepoid = $projpacksin->{'repoid'};
    mkdir_p("$reporoot") unless -d "$reporoot";
    eval {
      # it does not matter which bs_sched for which architecture succeeds to write this file
      writestr("$reporoot/._repoid", "$reporoot/_repoid", $projpacksin->{'repoid'});
    };
  }
  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'};

  for my $proj (@{$projpacksin->{'project'} || []}) {
    if (@packids) {
      die("bad projpack answer\n") unless $proj->{'name'} eq $projid;
      if ($projpacks->{$projid}) {
        # 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'};
    }
  }
  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($path->{'project'});
      }
    }
  }
}

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

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

  my $projpacksin;
  eval {
    # withsrcmd5 is needed for the patterns md5sum
    $projpacksin = BSRPC::rpc("$BSConfig::srcserver/getprojpack", $BSXML::projpack, "project=$projid", 'nopackages', 'withrepos', 'withconfig', 'withsrcmd5', "arch=$myarch");
  };
  if ($@ || !$projpacksin) {
    print $@ if $@;
    return undef;
  }
  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
  my $cold = Build::read_config($myarch, split("\n", $oldproj->{'config'} || ''));
  my $cnew = Build::read_config($myarch, 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;
}


#
# 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') {
      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);
}

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

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

  my ($projid, $repoid) = split('/', $prp, 2);
  return undef if !$remoteproj || $remoteproj->{'error'};
  print "    fetching remote repository state for $prp\n";
  my $param = {
    'uri' => "$remoteproj->{'remoteurl'}/build/$remoteproj->{'remoteproject'}/$repoid/$myarch/_repository",
    'timeout' => 200,
    'receiver' => \&BSHTTP::cpio_receiver,
    'proxy' => $proxy,
  };
  my $cpio;
  eval {
    die('unsupported view\n') unless $BSConfig::usesolvstate;
    $cpio = BSRPC::rpc($param, undef, 'view=solvstate');
  };
  if ($@ && $@ =~ /unsupported view/) {
    eval {
      $cpio = BSRPC::rpc($param, undef, 'view=cache');
    };
  }
  if ($@) {
    warn($@);
    my $error = $@;
    $error =~ s/\n$//s;
    addretryevent({'type' => 'repository', 'project' => $projid, 'repository' => $repoid, 'arch' => $myarch}) if $error !~ /remote error:/;
    return undef;
  }
  my %cpio = map {$_->{'name'} => $_->{'data'}} @{$cpio || []};
  my $repostate = $cpio{'repositorystate'};
  $repostate = XMLin($BSXML::repositorystate, $repostate) if $repostate;
  delete $prpnotready{$prp};
  if ($repostate && $repostate->{'blocked'}) {
    $prpnotready{$prp} = { map {$_ => 1} @{$repostate->{'blocked'}} };
  }
  if (exists $cpio{'repositorysolv'} && $BSConfig::usesolvstate) {
    my $r;
    eval {$r = $pool->repofromstr($prp, $cpio{'repositorysolv'}); };
    warn($@) if $@;
    if ($r) {
      $repodatas{$prp}->{'solv'} = $cpio{'repositorysolv'};
      $repodatas{$prp}->{'lastscan'} = time();
    }
    return $r;
  } 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'};
    }
    my $r = $pool->repofromdata($prp, $cache);
    $repodatas{$prp}->{'solv'} = $r->tostr();
    $repodatas{$prp}->{'lastscan'} = time();
    return $r;
  } else {
    # return empty repo
    my $r = $pool->repofrombins($prp, '');
    $repodatas{$prp}->{'solv'} = $r->tostr();
    $repodatas{$prp}->{'lastscan'} = time();
    return $r;
  }
}

#
# 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) = @_;

  my $pschanged;
  my $ps = BSUtil::retrieve("$reporoot/$prp/$myarch/:packstatus", 1);
  if (!$ps) {
    # compatibility: read and convert old xml data
    $ps = readxml("$reporoot/$prp/$myarch/:packstatus", $BSXML::packstatuslist, 1);
    if ($ps) {
      my %packstatus;
      my %packerror;
      for (@{$ps->{'packstatus'} || []}) {
        $packstatus{$_->{'name'}} = $_->{'status'};
        $packerror{$_->{'name'}} = $_->{'error'} if $_->{'error'};
      }
      $ps = {'packstatus' => \%packstatus, 'packerror' => \%packerror};
      $pschanged = 1;
    }
  }
  return unless $ps;
  if (exists($ps->{'packstatus'}->{$packid}) && $ps->{'packstatus'}->{$packid} eq 'scheduled') {
    $ps->{'packstatus'}->{$packid} = 'finished';
    $ps->{'packerror'}->{$packid} = $code;
    $pschanged = 1;
  }
  BSUtil::store("$reporoot/$prp/$myarch/.:packstatus", "$reporoot/$prp/$myarch/:packstatus", $ps) if $pschanged;
}

#
# 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 ($job, $js, $changed, $fullcache) = @_;

  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($job, $js, $changed);
    return ;
  }
  if ($info->{'file'} eq '_delta') {
    deltafinished($job, $js, $changed);
    return ;
  }
  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");

  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;
  $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
  my $relsync = BSUtil::retrieve("$reporoot/$prp/$myarch/:relsync", 1) || {};
  $relsync->{$packid} = "$info->{'versrel'}.$info->{'bcnt'}";
  BSUtil::store("$reporoot/$prp/$myarch/:relsync.new", "$reporoot/$prp/$myarch/:relsync", $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 ($job, $js, $changed) = @_;

  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;
  $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 ($job, $js, $changed) = @_;

  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 ($job, $js, $changed) = @_;
  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;
  $changed->{$prp} ||= 1;
  unlink("$reporoot/$prp/$myarch/:repodone");
}

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

  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;
  unlink($_) for @all;
  rmdir($jobdatadir);
}

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

  my $prp = "$projid/$repoid";
  my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
  my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
  return ('broken', 'missing repo') unless $repo;	# can't happen

  # 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->{'type'} eq 'dsc';

  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($pool, $aprp);
    if (!$r) {
      print "      - $packid (kiwi-image)\n";
      print "        repository $aprp is unavailable";
      return ('broken', "repository $aprp is unavailable");
    }
  }
  $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'}";
  }

  if (!$repo->{'block'} || $repo->{'block'} ne 'never') {
    my @blocked;
    for my $repo ($pool->repos()) {
      my $aprp = $repo->name();
      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 = $repo->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));
    }
  }
  my @meta = split("\n", (readstr("$reporoot/$prp/$myarch/:meta/$packid", 1) || ''));
  if (!@meta || !$meta[0]) {
    print "      - $packid (kiwi-image)\n";
    print "        start build\n";
    return ('scheduled', [ $bconf, \@edeps, {'explain' => 'new build'} ]);
  }
  if ($meta[0] ne $new_meta[0]) {
    print "      - $packid (kiwi-image)\n";
    print "        src change, start build\n";
    return ('scheduled', [ $bconf, \@edeps, {'explain' => 'source change', 'oldsource' => substr($meta[0], 0, 32)} ]);
  }
  if (join('\n', @meta) eq join('\n', @new_meta)) {
    if ($relsynctrigger) {
      print "      - $packid (kiwi-image)\n";
      print "        rebuild counter sync\n";
      return ('scheduled', [ $bconf, \@edeps, {'explain' => 'rebuild counter sync'} ]);
    }
    print "      - $packid (kiwi-image)\n";
    print "        nothing changed\n";
    return ('done');
  }
  if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'local') {
    print "      - $packid (kiwi-image)\n";
    print "        nothing changed\n";
    return ('done');
  }
  my @diff = diffsortedmd5(0, \@meta, \@new_meta);
  print "      - $packid (kiwi-image)\n";
  print "        $_\n" for @diff;
  print "        meta change, start build\n";
  return ('scheduled', [ $bconf, \@edeps, {'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff)} ]);
}

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

  my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
  return ('broken', 'missing repo') unless $repo;	# can't happen

  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->{'type'} eq 'dsc';
    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($pool, $aprp);
      if (!$r) {
        print "      - $packid (kiwi-image)\n";
        print "        repository $aprp is unavailable";
        return ('broken', "repository $aprp is unavailable");
      }
    }
    $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($projid, $repoid, $packid, $pdata, $info, $bconf, [], $edeps, undef, $reason, $relsyncmax, 0);
  } else {
    # repo has a configured path, expand kiwi system with it
    my $prp = "$projid/$repoid";
    $bconf = getconfig($myarch, $prpsearchpath{$prp});
    return ('broken', 'no config') unless $bconf;	# should not happen
    ($job, $joberror) = set_building($projid, $repoid, $packid, $pdata, $info, $bconf, [], $edeps, $prpsearchpath{$prp} || [], $reason, $relsyncmax, 0);
  }
  if ($job) {
    return ('scheduled', $job);
  } else {
    return ('broken', $joberror);
  }
}

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

sub checkkiwiproduct {
  my ($projid, $repoid, $packid, $pdata, $info, $notready, $relsynctrigger) = @_;

  # hmm, should get the arch from the kiwi info
  # but how can we map it to the buildarchs?
  my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
  return ('broken', 'missing repo') unless $repo;	# can't happen
  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 = @{$repo->{'path'} || []} ? @{$prpsearchpath{$prp} || []} : @aprps;

  # get config from path
  my $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->{'type'} eq 'dsc';

    my $savemyarch = $myarch;
    for my $aprp (@bprps) {
      if (!checkprpaccess($aprp, $prp)) {
	print "      - $packid (kiwi-product)\n";
	print "        repository $aprp is unavailable";
	return ('broken', "repository $aprp is unavailable");
      }
      $myarch = $BSConfig::localarch if $myarch eq 'local' && $BSConfig::localarch;
      $repodatas{$aprp} = {'dontwrite' => 1} if $myarch ne $savemyarch;
      my $r = addrepo($pool, $aprp);
      delete $repodatas{$aprp} if $myarch ne $savemyarch;
      $myarch = $savemyarch;
      if (!$r) {
	print "      - $packid (kiwi-product)\n";
	print "        repository $aprp is unavailable";
	return ('broken', "repository $aprp is unavailable");
      }
    }
    $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 "        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;
	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 } } if $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 {
      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 } } if $ps;
      }
      $ps = ($ps || {})->{'packstatus'} || {};
      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. scan content.
	my @got;
	my $needit;
	my $bis;
	my @bininfo_s = stat("$reporoot/$aprp/$arch/$apackid/.bininfo");
        if (-s _) {
	  $bis = $bininfo_cache{"$aprp/$arch/$apackid/.bininfo"};
	  if (!defined($bis->[0]) || $bis->[0] ne "$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]") {
	    local *F;
	    undef $bis;
	    if (open(F, '<', "$reporoot/$aprp/$arch/$apackid/.bininfo")) {
	      @bininfo_s = stat(F);
	      die unless @bininfo_s;
	      my $bisc = '';
	      1 while sysread(F, $bisc, 8192, length($bisc));
	      close F;
	      $bis = ["$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]", $bisc];
	      $bininfo_cache{"$aprp/$arch/$apackid/.bininfo"} = $bis;
	    }
	  }
	}
	if ($bis) {
	  for my $bi (split("\n", $bis->[1])) {
	    my $b = substr($bi, 34);
	    next unless $b =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
	    $needit = 1 if $deps{$1} || ($allpacks && !$deps{"-$1"});
	    push @got, "$aprp/$arch/$apackid/$b";
	    $rpms_hdrmd5{$got[-1]} = substr($bi, 0, 32);
	    $rpms_meta{$got[-1]} = "$aprp/$arch/$apackid/$1.$2";
	  }
	} else {
          for my $b (ls("$reporoot/$aprp/$arch/$apackid")) {
	    next unless $b =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
	    $needit = 1 if $deps{$1} || ($allpacks && !$deps{"-$1"});
	    push @got, "$aprp/$arch/$apackid/$b";
	    $rpms_meta{$got[-1]} = "$aprp/$arch/$apackid/$1.$2";
          }
        }
	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,
      };
      my $evname = "unblocked::${projid}::${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}";
  }
  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)) {
    if ($relsynctrigger) {
      print "      - $packid (kiwi-product)\n";
      print "        rebuild counter sync\n";
      return ('scheduled', [ $bconf, \@rpms, {'explain' => 'rebuild counter sync'} ]);
    }
    print "      - $packid (kiwi-product)\n";
    print "        nothing changed\n";
    return ('done');
  }
  if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'local') {
    print "      - $packid (kiwi-product)\n";
    print "        nothing changed\n";
    return ('done');
  }
  my @diff = diffsortedmd5(0, \@meta, \@new_meta);
  print "      - $packid (kiwi-product)\n";
  print "        $_\n" for @diff;
  print "        meta change, start build\n";
  return ('scheduled', [ $bconf, \@rpms, {'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff)} ]);
}

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

  my $bconf = $data->[0];
  my $rpms = $data->[1];
  my $reason = $data->[2];

  my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
  return ('broken', 'missing repo') unless $repo;	# can't happen
  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 ($projid, $repoid, $packid, $pdata, $info, $packstatus) = @_;

  my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
  return ('broken', 'missing repo') unless $repo;	# can't happen
  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;
    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,
        };
        my $evname = "unblocked::${projid}::${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 = $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 ($projid, $repoid, $packid, $pdata, $info, $data) = @_;
  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);
      }
      if ($ptype eq 'binary') {
        # be extra careful with em, recalculate meta
	my @s = stat("$jobdatadir/$bin");
	$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);
      };
      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 .= "$d->{'hdrmd5'}  $bin\n";
      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");
  }
  BSUtil::touch("$dst/.nosourceaccess") unless checkaccess('sourceaccess', $projid, $packid, $repoid);
  BSUtil::cleandir($jobdatadir);
  rmdir($jobdatadir);
  writestr("$dst/.bininfo.new", "$dst/.bininfo", $bininfo) if $bininfo;
  return ('done');
}

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

#
# checkaggregate  - calculate package status of an aggregate package
#
# input:  $projid      - our project
#         $repoid      - our repository
#         $packid      - aggregate package
#         $pdata       - package data information
#         $prpfinished - reference to project finished marker hash
# output: new package status
#         package status details (new meta in 'scheduled' case)
#
# globals used: $projpacks
#
sub checkaggregate {
  my ($projid, $repoid, $packid, $pdata, $notready, $prpfinished) = @_;

  my @aggregates = @{$pdata->{'aggregatelist'}->{'aggregate'} || []};
  my @broken;
  my @blocked;
  for my $aggregate (@aggregates) {
    my $aprojid = $aggregate->{'project'};
    my $proj = $remoteprojs{$aprojid} || $projpacks->{$aprojid};
    if (!$proj) {
      push @broken, $aprojid;
      next;
    }
    if (!checkprpaccess($aprojid, $projid)) {
      push @broken, $aprojid;
      next;
    }
    if ($remoteprojs{$aprojid} && !$aggregate->{'package'}) {
      # remote aggregates need packages, otherwise they are too
      # expensive
      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;
    if ($aggregate->{'package'}) {
      @apackids = @{$aggregate->{'package'}};
    } else {
      @apackids = sort keys(%{($projpacks->{$aprojid} || {})->{'package'} || {}});
    }
    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};
      # 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 } } if $ps;
      }
      $ps = ($ps || {})->{'packstatus'} || {};
      for my $apackid (@apackids) {
	my $s = $ps->{$apackid} || '';
        if ($s eq 'scheduled' || $s eq 'blocked' || $s eq 'finished') {
	  next if $aprojid eq $projid && $arepoid eq $repoid && $apackid eq $packid;
	  push @blocked, "$aprp/$apackid";
	}
      }
    }
  }
  if (@broken) {
    print "      - $packid (aggregate)\n";
    print "        broken (@broken)\n";
    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;
    if ($aggregate->{'package'}) {
      @apackids = @{$aggregate->{'package'}};
    } else {
      @apackids = sort keys(%{($projpacks->{$aprojid} || {})->{'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 {
	  my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
	  my @d = grep {$_ eq 'updateinfo.xml' || /\.(?:rpm|deb)$/} 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(0, \@meta, \@new_meta);
  print "      - $packid (aggregate)\n";
  print "        $_\n" for @diff;
  my $new_meta = join('', map {"$_\n"} @new_meta);
  return ('scheduled', $new_meta);
}

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

  my $prp = "$projid/$repoid";
  my @aggregates = @{$pdata->{'aggregatelist'}->{'aggregate'} || []};
  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;
    if ($aggregate->{'package'}) {
      @apackids = @{$aggregate->{'package'}};
    } else {
      @apackids = sort keys(%{($projpacks->{$aprojid} || {})->{'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,
	  };
	  eval {
	    $cpio = BSRPC::rpc($param, undef, "view=cpio");
	  };
	  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 {
	  my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
	  $updateinfo = "$d/updateinfo.xml" if -f "$d/updateinfo.xml";
	  @d = grep {/\.(?:rpm|deb)$/} 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',
  };
  if (!BSUtil::lockcreatexml(\*F, "$myjobsdir/.$job", "$myjobsdir/$job:status", $jobstatus, $BSXML::jobstatus)) {
    die("job lock failed\n");
  }
  my $info = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => $packid,
    'arch' => $myarch,
    'job' => $job,
    'file' => '_aggregate',
  };
  writexml("$myjobsdir/.$job", "$myjobsdir/$job", $info, $BSXML::buildinfo);
  close(F);
  print "        scheduled\n";

  my $ev = {'type' => 'built', 'arch' => $myarch, 'job' => $job};
  if ($sign && grep {/\.rpm$/} keys %$jobrepo) {
    sendevent($ev, 'signer', "finished:$myarch:$job");
  } else {
    sendevent($ev, $myarch, "finished:$job");
  }
  $ourjobs{$job} = 1;
  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;
  }
  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.new", "$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') {
	  push @remoteevents, {'type' => 'repository', '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') {
      $watch = "repository/$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;
}

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

$| = 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 %prpfinished;
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();
    }
    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'}};
    }
  }
}

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

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

while(1) {
  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) {
    my @readok = select_read(0, $pingsock, values %remotewatchers);
    $gotevent = 1 if @readok;
    for my $watcher (@readok) {
      my $remoteurl = $watcher->{'remoteurl'};
      next if $watcher->{'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 lookat_low array is empty, start new series with lookat_next
  if (!@lookat_low && %lookat_next) {
    @lookat_low = grep {$lookat_next{$_}} @prps;
    %lookat_next = ();
  }

  if (!$gotevent && !@lookat_low && !@lookat_med && !@lookat_high) {
    if ($testmode) {
      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) {
      select_read($timeout, $pingsock, @retryevents, values %remotewatchers);
      $slept += time() - $sleepstart;
      next;
    } else {
      sysread(PING, $dummy, 1, 0);
      $gotevent = 1;
    }
    $slept += time() - $sleepstart;
  }

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

    my %fetchprojpacks;
    my %fetchprojpacks_nodelay;
    my %deepcheck;
    my %lowprioproject;

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

    my $fullcache;
    while (@events) {
      my $ev = shift @events;
      $ev->{'type'} ||= 'unknown';

      if ($ev->{'type'} eq 'uploadbuild' && $ev->{'job'}) {
	# have to be extra careful with those. if the package is in
        # (delayed)fetchprojpacks, delay event processing until we
        # updated the projpack data.
        my $info = readxml("$myjobsdir/$ev->{'job'}", $BSXML::buildinfo, 1) || {};
	my $projid = $info->{'project'};
	my $packid = $info->{'package'};
	if (defined($projid) && defined($packid)) {
	  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;
	    $gotevent = 1;
	    next;
	  }
	}
      }

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

      if ($ev->{'type'} ne 'built' && $fullcache) {
	sync_fullcache($fullcache);
	undef $fullcache;
      }

      if ($ev->{'type'} eq 'built' || $ev->{'type'} eq 'import' || $ev->{'type'} eq 'uploadbuild') {
	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;
	  next;
	}
	if ($js->{'code'} ne 'finished') {
	  print "  - $job is not finished: $js->{'code'}\n";
	  close F;
	  next;
	}
        if ($ev->{'type'} eq 'built') {
	  $fullcache = {} if !$fullcache && $events[0] && $events[0]->{'type'} eq 'built';
	  jobfinished($job, $js, \%changed_med, $fullcache);
        } elsif ($ev->{'type'} eq 'uploadbuild') {
	  uploadbuildevent($job, $js, \%changed_med);
	} else {
	  importevent($job, $js, \%changed_med);
	}
	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};
	next;
      }

      if ($ev->{'type'} eq 'srcevent' || $ev->{'type'} eq 'package') {
	my $projid = $ev->{'project'};
	next unless defined $projid;
	my $packid = $ev->{'package'};
	push @{$fetchprojpacks{$projid}}, $packid;
	$deepcheck{$projid} = 1 if !defined $packid;
	next;
      }

      if ($ev->{'type'} eq 'projevent' || $ev->{'type'} eq 'project' || $ev->{'type'} eq 'lowprioproject') {
	my $projid = $ev->{'project'};
	next unless defined $projid;
	push @{$fetchprojpacks{$projid}}, undef;
	$lowprioproject{$projid} = 1 if $ev->{'type'} eq 'lowprioproject';
	next;
      }

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

      if ($ev->{'type'} eq 'rebuild' || $ev->{'type'} eq 'recheck' || $ev->{'type'} eq 'admincheck') {
	my $projid = $ev->{'project'};
	my $packid = $ev->{'package'};
	my $repoid = $ev->{'repository'};
	my %admincheck;
        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) {
	  @lookat_high = grep {!$admincheck{$_}} @lookat_high;
	  unshift @lookat_high, sort keys %admincheck;
	  delete $nextmed{$_} for keys %admincheck;
	  $notlow = $notmed = 0;
	}
	next;
      }

      if ($ev->{'type'} eq 'unblocked' || $ev->{'type'} eq 'relsync') {
	my $projid = $ev->{'project'};
	my $repoid = $ev->{'repository'};
	if (defined($projid) && defined($repoid)) {
	  my $prp = "$projid/$repoid";
	  print "$prp is $ev->{'type'}\n";
	  $changed_med{$prp} ||= 1;
	}
	next;
      }

      if ($ev->{'type'} eq 'scanrepo') {
	my $projid = $ev->{'project'};
	my $repoid = $ev->{'repository'};
	if (!defined($projid) && !defined($repoid)) {
	  print "flushing all repository data\n";
	  next;
	}
	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($pool, $prp);
	  undef $pool;
	  $changed_high{$prp} = 2;
          delete $repounchanged{$prp};
	}
	next;
      }

      if ($ev->{'type'} eq 'dumprepo') {
	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;
	next;
      }

      if ($ev->{'type'} eq 'wipenotyet') {
        my $prp = "$ev->{'project'}/$ev->{'repository'}";
        delete $nextmed{$prp};
        next;
      }

      if ($ev->{'type'} eq 'wipe') {
	my $projid = $ev->{'project'};
	my $repoid = $ev->{'repository'};
	my $packid = $ev->{'package'};
	next unless defined($projid) && defined($repoid) && defined($packid);
	my $prp = "$projid/$repoid";
	my $gdst = "$reporoot/$prp/$myarch";
	print "wiping $prp $packid\n";
	next 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;
	next;
      }

      if ($ev->{'type'} eq 'exit' || $ev->{'type'} eq 'exitcomplete' || $ev->{'type'} eq 'dumpstate' || $ev->{'type'} eq 'restart') {
	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';
	my @new_lookat = @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') {
	  $schedstate->{'projpacks'} = $projpacks;
	}
	$schedstate->{'prps'} = \@prps;
	$schedstate->{'changed_low'} = \%changed_low;
	$schedstate->{'changed_med'} = \%changed_med;
	$schedstate->{'changed_high'} = \%changed_high;
	$schedstate->{'lookat'} = \@new_lookat;
	$schedstate->{'lookat_oob'} = \@lookat_med;
	$schedstate->{'lookat_oobhigh'} = \@lookat_high;
	$schedstate->{'prpfinished'} = \%prpfinished;
	$schedstate->{'globalnotready'} = \%prpnotready;
	$schedstate->{'delayedfetchprojpacks'} = \%delayedfetchprojpacks;
	$schedstate->{'watchremote_start'} = \%watchremote_start;
	unlink("$rundir/bs_sched.$myarch.state");
	BSUtil::store("$rundir/bs_sched.$myarch.state.new", "$rundir/bs_sched.$myarch.state", $schedstate);
	if ($ev->{'type'} eq 'exit' || $ev->{'type'} eq 'exitcomplete') {
	  print "bye.\n";
	  exit(0);
	}
	if ($ev->{'type'} eq 'restart') {
	  exec $0, $myarch;
	  die("$0: $!\n");
	}
	next;
      }

      if ($ev->{'type'} eq 'useforbuild') {
	my $projid = $ev->{'project'};
	my $repoid = $ev->{'repository'};
	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;
	next;
      }

      print "unknown event type: $ev->{'type'}\n";
    }
    sync_fullcache($fullcache) if $fullcache && $fullcache->{'prp'};

    if (%fetchprojpacks) {
      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 %packids = map {$_ => 1} @{$fetchprojpacks{$projid}};
	my %changedproj;
	for my $packid (sort keys %packids) {
	    for my $linfo (grep {$_->{'project'} eq $projid && ($_->{'package'} eq $packid || $_->{'package'} eq ':*')} @projpacks_linked) {
		my $lprojid = $linfo->{'myproject'};
		my $lpackid = defined($linfo->{'mypackage'}) ? $linfo->{'mypackage'} : $packid;
		push @{$delayedfetchprojpacks{$lprojid}}, $lpackid;
		$changedproj{$lprojid} = 1;
	    }
	}
	if (%changedproj) {
	    for my $lprp (@prps) {
		if ($changedproj{(split('/', $lprp, 2))[0]}) {
		    $changed_med{$lprp} ||= 1;
		    $changed_dirty{$lprp} = 1; 
		}
	    }
	}
	delete $fetchprojpacks{$projid};
      }
    }

    if (%fetchprojpacks) {
      # 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 ($projpacks->{$projid} && !$deepcheck{$projid}) {
	    if (!update_project_meta($projid)) {
	      # update meta failed, do it the hard way...
	      get_projpacks($projid);
	      $fetchedall = 1;
	    }
	  } else {
	    get_projpacks($projid);
	    $fetchedall = 1;
	  }
	}
	if (!$fetchedall) {
	  # single package (source) changes
	  my %packids = map {$_ => 1} grep {defined($_)} @{$fetchprojpacks{$projid}};
	  get_projpacks($projid, sort keys %packids) if %packids;
	  # remove em from the delay queue
	  if ($delayedfetchprojpacks{$projid}) {
	    $delayedfetchprojpacks{$projid} = [ grep {!$packids{$_}} @{$delayedfetchprojpacks{$projid} || []} ];
	    delete $delayedfetchprojpacks{$projid} unless @{$delayedfetchprojpacks{$projid}};
	  }
	} else {
	  delete $delayedfetchprojpacks{$projid};
	}
      }
      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}) {
	    for my $linfo (grep {$_->{'project'} eq $projid} @projpacks_linked) {
	      next unless exists $linfo->{'mypackage'};
	      push @{$fetchlinkedprojpacks{$linfo->{'myproject'}}}, $linfo->{'mypackage'};
	    }
	  }
	} 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 = map {$_ => 1} grep {defined($_)} @{$fetchprojpacks{$projid}};
	for my $packid (sort keys %packids) {
	  for my $linfo (grep {$_->{'project'} eq $projid && ($_->{'package'} eq $packid || $_->{'package'} eq ':*')} @projpacks_linked) {
	    push @{$fetchlinkedprojpacks{$linfo->{'myproject'}}}, defined($linfo->{'mypackage'}) ? $linfo->{'mypackage'} : $packid;
	    $fetchlinkedprojpacks_srcchange{$linfo->{'myproject'}} = 1;	# mark as source changes
	  }
	}
      }

      # pass3: update link information
      if (%fetchlinkedprojpacks) {
	for my $projid (sort keys %fetchlinkedprojpacks) {
	  my %packids = map {$_ => 1} @{$fetchlinkedprojpacks{$projid}};
	  get_projpacks($projid, sort keys %packids);
          # 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();	# just in case...
      }
    }

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

  # 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];

  my $prp;
  if (@lookat_low && $notlow > 10) {
    $prp = shift @lookat_low;
    print "$msgtm looking at low prio $prp";
    @lookat_high = grep {$_ ne $prp} @lookat_high;
    @lookat_med = grep {$_ ne $prp} @lookat_med;
    $notlow = 0;
  }
  if (!defined($prp) && @lookat_high && (!@lookat_med || $notmed < 3)) {
    $prp = shift @lookat_high;
    if ($nextmed{$prp}) {
      my $now = time();
      my @notyet;
      while ($nextmed{$prp} && $now < $nextmed{$prp}) {
	print "  not yet $prp\n";
	push @notyet, $prp;
	$prp = shift @lookat_high;
	last unless defined $prp;
      }
      unshift @lookat_high, @notyet;
      $prp = shift @lookat_high if !defined($prp) && !@lookat_med && !@lookat_low;
    }
    print "$msgtm looking at high prio $prp" if defined $prp;
    $notlow++ if defined $prp;
    $notmed++ if defined $prp;
  }
  if (!defined($prp) && @lookat_med) {
    $notmed = 0;
    $prp = shift @lookat_med;
    if ($nextmed{$prp}) {
      my $now = time();
      my @notyet;
      while ($nextmed{$prp} && $now < $nextmed{$prp}) {
	print "  not yet $prp\n";
	push @notyet, $prp;
	$prp = shift @lookat_med;
	last unless defined $prp;
      }
      unshift @lookat_med, @notyet;
      $prp = shift @lookat_med if !defined($prp) && !@lookat_low;
    }
    print "$msgtm looking at med prio $prp" if defined $prp;
    $notlow++ if defined $prp;
  }
  if (!defined($prp)) {
    $prp = shift @lookat_low;
    print "$msgtm looking at low prio $prp";
    $notlow = 0;
  }
  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};

      for my $dir (ls("$reporoot/$prp/$myarch")) {
	delete $lastcheck{"$prp/$dir"};
	# 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};
    if (%packids) {
      # remember old values
      my $packs = ($projpacks->{$projid} || {})->{'package'} || {};
      for my $packid (keys %packids) {
	$packids{$packid} = $packs->{$packid} ? Storable::dclone($packs->{$packid}) : undef;
      }
      my $oldprojdata = { %{$projpacks->{$projid} || {}} };
      delete $oldprojdata->{'package'};
      $oldprojdata = Storable::dclone($oldprojdata);
      get_projpacks($projid, sort keys %packids);
      # if we just had a srcmd5 change, no need to postprocess
      my $need_postprocess;
      if (!identical($projpacks->{$projid}, $oldprojdata, {'package' => 1})) {
        $need_postprocess = 1;
      } else {
        $packs = ($projpacks->{$projid} || {})->{'package'} || {};
        my %except = map {$_ => 1} qw{rev srcmd5 versrel verifymd5 revtime dep prereq file name error build publish useforbuild};
        for my $packid (keys %packids) {
	  if (!identical($packids{$packid}, $packs->{$packid}, \%except)) {
	    $need_postprocess = 1;
	    last;
	  }
	}
      }
      get_projpacks_postprocess() if $need_postprocess;
    }
  }

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

  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) {
        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;
  }
  print "  - $prp\n";

  my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
  next unless $repo;

  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';
      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->{'type'} eq 'dsc';

  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

  for my $rprp (@{$prpsearchpath{$prp}}) {
    if (!checkprpaccess($rprp, $prp)) {
      $error = "repository '$rprp' is unavailable";
      last;
    }
    my $r = addrepo($pool, $rprp);
    if (!$r) {
      $error = "repository '$rprp' is unavailable";
      last;
    }
  }
  if ($error) {
    print "    $error\n";
    set_repo_state($prp, 'broken', $error);
    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);
    if ($rprp eq $prp) {
      $depislocal{$n} = 1;
    } else {
      $notready{$n} = 2 if $prpnotready{$rprp} && $prpnotready{$rprp}->{$n};
    }
  }

  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;

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

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

    # select correct info and set packtype
    my %info = map {$_->{'repository'} => $_} @{$pdata->{'info'} || []};
    my $info = $info{$repoid};
    if (!$info) {
      for ($prptype, 'spec', 'dsc', 'kiwi') {
	last if $info = $info{":$_"};
      }
    }
    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'};

    $info->{'file'} =~ /\.(spec|dsc|kiwi)$/;
    my $packtype = $1 || 'spec';
    my @deps = @{$info->{'dep'} || []};
    if ($packtype eq 'kiwi') {
      # do not expand kiwi dependencies
      $pdeps{$packid} = \@deps;
      next;
    }
    my ($eok, @edeps) = Build::get_deps($bconf, $subpacks{$info->{'name'}}, @deps);
    if (! $eok) {
      @edeps = @deps;
      $experrors{$packid} = 1;
    }
    $pdeps{$packid} = \@edeps;
  }

  # 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'}->{$_};
      }
    }
  }
  BSUtil::store("$reporoot/$prp/$myarch/.:depends", "$reporoot/$prp/$myarch/:depends", {
    'pkgdeps' => \%pdeps,
    'subpacks' => \%subpacks,
    'pkg2src' => \%pkg2src,
    'cycles' => \@cycles,
  });
  # 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;
    }
  }

  # bring unresolvables to back (is this really needed? we do not modify
  # the repositories in the middle of the project check)
  my @packs_experrors = grep {$experrors{$_}} @packs;
  @packs = grep {!$experrors{$_}} @packs;
  push @packs, @packs_experrors;

  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{$_};
      }
    }
  }

  # Step 2d: check status of all packages
  my %packstatus = ();
  my %packerror = ();
  my @cpacks = @packs;
  my %cycpass;
  my $nharder = 0;
  my $needed;

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

  while (@cpacks) {
    my $packid = shift @cpacks;
    my $incycle = 0;
    if ($cychash{$packid}) {
      # 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}};
	  }
	}
      }
    }

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

    my %info = map {$_->{'repository'} => $_} @{$pdata->{'info'} || []};
    my $info = $info{$repoid};
    if (!$info) {
      for ($prptype, 'spec', 'dsc', 'kiwi') {
	last if $info = $info{":$_"};
      }
      $info = {} unless $info;
    }

    # 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;
    if ($pdata->{'aggregatelist'}) {
      $packtype = 'aggregate';
    } elsif ($pdata->{'patchinfo'}) {
      $packtype = 'patchinfo';
    } elsif ($info->{'file'} && $info->{'file'} =~ /\.(spec|dsc|kiwi)$/) {
      $packtype = $1;
    }
    if (!$packtype) {
      print "      - $packid (no spec/dsc/kiwi file)\n";
      $packstatus{$packid} = 'broken';
      $packerror{$packid} = 'no spec/dsc/kiwi file';
      next;
    }
    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';
      }
    }
    #print "      - $packid ($packtype)\n";

    if ($packtype eq 'aggregate') {
      my ($astatus, $aerror) = checkaggregate($projid, $repoid, $packid, $pdata, \%notready, \%prpfinished);
      if ($astatus eq 'scheduled') {
	# aerror contains rebuild data in this case
	($astatus, $aerror) = rebuildaggregate($projid, $repoid, $packid, $pdata, $aerror);
	if ($astatus eq 'scheduled') {
	  $building{$packid} = $aerror || 'job'; # aerror contains jobid in this case
	  undef $aerror;
	}
	unlink("$reporoot/$prp/$myarch/:repodone");
      }
      $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;
      }
      next;
    }

    if ($packtype eq 'patchinfo') {
      my ($astatus, $aerror) = checkpatchinfo($projid, $repoid, $packid, $pdata, $info, \%packstatus);
      if ($astatus eq 'scheduled') {
	# aerror contains rebuild data in this case
	($astatus, $aerror) = rebuildpatchinfo($projid, $repoid, $packid, $pdata, $info, $aerror);
	if ($astatus eq 'scheduled') {
	  $building{$packid} = $aerror || 'job'; # aerror contains jobid in this case
	  undef $aerror;
	}
	unlink("$reporoot/$prp/$myarch/:repodone");
      }
      $packstatus{$packid} = $astatus;
      $packerror{$packid} = $aerror if defined $aerror;
      if ($astatus eq 'blocked' || $astatus eq 'scheduled') {
        $unfinished{$pname} = 1;
      }
      next;
    }

    if ($packtype eq 'kiwi-product') {
      my ($astatus, $aerror) = checkkiwiproduct($projid, $repoid, $packid, $pdata, $info, \%notready, $relsynctrigger{$packid});
      if ($astatus eq 'scheduled') {
	# aerror contains rebuild data in this case
	($astatus, $aerror) = rebuildkiwiproduct($projid, $repoid, $packid, $pdata, $info, $aerror, $relsyncmax);
	if ($astatus eq 'scheduled') {
	  $building{$packid} = $aerror || 'job'; # aerror contains jobid in this case
	  undef $aerror;
	}
	unlink("$reporoot/$prp/$myarch/:repodone");
      }
      $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;
      }
      next;
    }

    if ($packtype eq 'kiwi-image') {
      my ($astatus, $aerror) = checkkiwiimage($projid, $repoid, $packid, $pdata, $info, \%notready, $relsynctrigger{$packid});
      if ($astatus eq 'scheduled') {
	# aerror contains rebuild data in this case
	($astatus, $aerror) = rebuildkiwiimage($projid, $repoid, $packid, $pdata, $info, $aerror, $relsyncmax);
	if ($astatus eq 'scheduled') {
	  $building{$packid} = $aerror || 'job'; # aerror contains jobid in this case
	  undef $aerror;
	}
	unlink("$reporoot/$prp/$myarch/:repodone");
      }
      $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;
      }
      next;
    }

    # packtype is now spec/deb

    if ($experrors{$packid}) {
      # retry expansion of this one
      my @deps = @{$info->{'dep'} || []};
      my ($eok, @edeps) = Build::get_deps($bconf, $subpacks{$info->{'name'}}, @deps);
      if (! $eok) {
	print "      - $packid ($packtype)\n";
	print "        unresolvables:\n";
	print "            $_\n" for @edeps;
	$packstatus{$packid} = 'unresolvable';
	$packerror{$packid} = join(', ', @edeps);
	next;
      }
      delete $experrors{$packid};
      $pdeps{$packid} = \@edeps;
    }

    if (exists($pdata->{'originproject'})) {
      if ($repo->{'linkedbuild'} && $repo->{'linkedbuild'} eq 'localdep') {
        if (!grep {$depislocal{$_}} @{$pdeps{$packid}}) {
	  $packstatus{$packid} = 'excluded';
	  $packerror{$packid} = 'project link, only depends on non-local packages';
	  next;
        }
      }
    }

    # calculate if we're blocked
    my @blocked = grep {$notready{$dep2src{$_}}} @{$pdeps{$packid}};
    @blocked = () if $repo->{'block'} && $repo->{'block'} eq 'never';
    if ($cychash{$packid}) {
      # package belongs to a cycle, prune blocked list
      next if $packstatus{$packid} && $packstatus{$packid} ne 'done'; # already decided in phase 1
      if (@blocked && $cycpass{$packid} && $cycpass{$packid} == 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";
        my $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $prjuseforbuildenabled);
	$notready{$pname} = 1 if $useforbuildenabled;
	$unfinished{$pname} = 1;
	$packstatus{$packid} = 'blocked';
	$packerror{$packid} = join(', ', @blocked);
	next;
      }
      my %cycs = map {$_ => 1} @{$cychash{$packid}};
      # prune building cycle packages from blocked
      @blocked = grep {!$cycs{$_} || !$building{$_}} @blocked;
    }
    if (@blocked) {
      # print "      - $packid ($packtype)\n";
      # print "        blocked\n";
      my $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $prjuseforbuildenabled);
      $notready{$pname} = 1 if $useforbuildenabled;
      $unfinished{$pname} = 1;
      $packstatus{$packid} = 'blocked';
      $packerror{$packid} = join(', ', @blocked);
      next;
    }

    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;
        # FIXME: this leads to have "finished" jobs being displayed as "scheduled".
	$packstatus{$packid} = 'scheduled';
	next;
      }
    }

    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 $mylastcheck = $lastcheck{"$prp/$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]";
      } else {
        delete $lastcheck{"$prp/$packid"};
        undef $mylastcheck;
      }
    }
    if (!$mylastcheck) {
      print "      - $packid ($packtype)\n";
      print "        start build\n";
      $reason = { 'explain' => 'new build' };
    } elsif (substr($mylastcheck, 0, 32) ne ($pdata->{'verifymd5'} || $pdata->{'srcmd5'})) {
      print "      - $packid ($packtype)\n";
      print "        src change, start build\n";
      $reason = { '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";
        $packstatus{$packid} = 'failed';
        next;
      }
      print "      - $packid ($packtype)\n";
      print "        retrying bad build\n";
      $reason = { 'explain' => 'retrying bad build' };
    } else {
      if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'local') {
        # rebuild on src changes only
	$packstatus{$packid} = 'done';
	goto relsynccheck;
      }
      # more work, check if dep rpm changed
      if ($incycle) {
	# print "      - $packid ($packtype)\n";
	# print "        in cycle, no source change...\n";
	$packstatus{$packid} = 'done';
	next;
      }
      my $check = substr($mylastcheck, 32, 32);
      $check .= $repo->{'rebuild'} || 'transitive';
      $check .= $pool->pkg2pkgid($dep2pkg{$_}) for sort @{$pdeps{$packid}};
      $check = Digest::MD5::md5_hex($check);
      if ($check eq substr($mylastcheck, 64, 32)) {
	# print "      - $packid ($packtype)\n";
	# print "        nothing changed\n";
	$packstatus{$packid} = 'done';
	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 (@{$pdeps{$packid}}) {
	my $pkg = $dep2pkg{$bpack};
	my $path = $pool->pkg2fullpath($pkg, $myarch);
	if ($depislocal{$bpack} && $path) {
	  my $meta = $dep2meta->{$bpack};
	  if (!$meta) {
	    my @m;
	    my $mf = substr("$reporoot/$path", 0, -4);
	    #print "        reading meta for $r->{'path'}\n";
	    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;
	    $meta = \@m;
	    $dep2meta->{$bpack} = $meta;
	  }
	  push @new_meta, @$meta;
	} else {
	  my $pkgid = $pool->pkg2pkgid($pkg);
	  push @new_meta, "$pkgid  $bpack";
	}
      }
      @new_meta = BSSolv::gen_meta($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";
	$nharder++;
	$lastcheck{"$prp/$packid"} = $mylastcheck;
	$packstatus{$packid} = 'done';
	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";
	$nharder++;
        if ($repo->{'rebuild'} && $repo->{'rebuild'} eq 'direct') {
	  $lastcheck{"$prp/$packid"} = $mylastcheck;
	} else {
	  # should not happen, delete lastcheck cache
	  delete $lastcheck{"$prp/$packid"};
	}
	$packstatus{$packid} = 'done';
	goto relsynccheck;
      }
      my @diff = diffsortedmd5(0, \@meta, \@new_meta);
      print "      - $packid ($packtype)\n";
      print "        $_\n" for @diff;
      print "        meta change, start build\n";
      $reason = { 'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff) };
    }
relsynccheck:
    if (!$reason) {
      next unless $relsynctrigger{$packid};
      print "      - $packid ($packtype)\n";
      print "        rebuild counter sync, start build\n";
      $reason = { 'explain' => 'rebuild counter sync' };
    }
    if (!$needed) {
      $needed = {};
      for my $p (keys %pdeps) {
        $needed->{$_}++ for map { $dep2src{$_} || $_ } @{$pdeps{$p}};
      }
    }
    my ($job, $joberror) = set_building($projid, $repoid, $packid, $pdata, $info, $bconf, $subpacks{$info->{'name'}}, $pdeps{$packid}, $prpsearchpath{$prp}, $reason, $relsyncmax, $needed->{$packid} || 0);
    if (!$job) {
      # could not start job...
      if ($joberror =~ /^unresolvable: (.*)$/) {
	$packstatus{$packid} = 'unresolvable';
	$packerror{$packid} = $1;
      } else {
	$packstatus{$packid} = 'broken';
	$packerror{$packid} = $joberror;
      }
      next;
    }
    my $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $prjuseforbuildenabled);
    $building{$packid} = $job;
    $notready{$pname} = 1 if $useforbuildenabled;
    $unfinished{$pname} = 1;
    $packstatus{$packid} = 'scheduled';
  }

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

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

  # write package status for this project
  if (0) {
    my @packstatuslist = map {{
      'name' => $_,
      'status' => $packstatus{$_},
      (exists $packerror{$_} ? ('error' => $packerror{$_}) : ())
    }} @packs;
    my $ps = { 'packstatus' => \@packstatuslist, 'project' => $projid, 'repository' => $repoid, 'arch' => $myarch};
    writexml("$reporoot/$prp/$myarch/.:packstatus", "$reporoot/$prp/$myarch/:packstatus", $ps, $BSXML::packstatuslist);
  } else {
    BSUtil::store("$reporoot/$prp/$myarch/.:packstatus", "$reporoot/$prp/$myarch/:packstatus", {
      'packstatus' => \%packstatus,
      'packerror' => \%packerror,
    });
  }

  $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: $nharder\n" if $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 (keys %unfinished) {
    $schedulerstate = 'blocked';
  } else {
    $schedulerstate = 'finished';
  }

  # we always publish kiwi...
  if (!%unfinished || $prptype eq 'kiwi') {
    my $pubenabled = enabled($repoid, $projpacks->{$projid}->{'publish'}, 1);
    my %pubenabled;
    for my $packid (@packs) {
      my $pdata = $packs->{$packid};
      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 (! -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;
      if (!$prpnoleaf{$prp}) {
	# only free 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 $lastcheck{"$prp/$_"} for @packs;
	  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($@);
    }
  }

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