#!/usr/bin/perl -w
#
# Copyright (c) 2006, 2007 Michael Schroeder, 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 Build;
use BSDB;

use strict;

my $user = $BSConfig::bsuser;
my $group = $BSConfig::bsgroup;

!defined($user) || defined($user = (getpwnam($user))[2]) || die("unknown user\n");
!defined($group) || defined($group = (getgrnam($group))[2]) || die("unknown group\n");
if (defined $group) {
  ($(, $)) = ($group, $group);
  die "setgid: $!\n" if ($) != $group);
}
if (defined $user) {
  ($<, $>) = ($user, $user); 
  die "setuid: $!\n" if ($> != $user); 
}



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

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

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

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

my %aliendeps = (
  'spec' => 'rpm',	# should be rpmbuild?
  'dsc' => 'dpkg',
  'kiwi' => 'kiwi',
);

my %remoteprojs;	# remote project cache

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

#
# input: depsp  -> hash of arrays
#        mapp   -> hash of strings
#        basep  -> hash of bools
#        buildp -> hash of bools
#
# XXX: put this in BSSort.pm or somehow use sort
#      from Build.pm
# 
sub sortpacks {
  my ($depsp, $mapp, $basep, $buildp, $cychp, @packs) = @_;

  return @packs if @packs < 2;

  my %deps;
  my %rdeps;
  my %needed;

  # map and unify dependencies, create rdeps and needed
  my %known = map {$_ => 1} @packs;
  die("sortpacks: input not unique\n") if @packs != keys(%known);
  for my $p (@packs) {
    if ($basep && $basep->{$p}) {
      $deps{$p} = [];
      $needed{$p} = 0;
      next;
    }
    my @fdeps = @{$depsp->{$p} || []};
    @fdeps = map {$mapp->{$_} || $_} @fdeps if $mapp;
    @fdeps = grep {$known{$_}} @fdeps;
    my %fdeps = ($p => 1);	# no self reference
    @fdeps = grep {!$fdeps{$_}++} @fdeps;
    $deps{$p} = \@fdeps;
    $needed{$p} = @fdeps;
    push @{$rdeps{$_}}, $p for @fdeps;
  }
  undef %known;		# free memory

  @packs = sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @packs;
  if ($buildp) {	# bring running to front
    my @packsr = grep {$buildp->{$_}} @packs;
    @packs = grep {!$buildp->{$_}} @packs;
    unshift @packs, @packsr;
  }
  my @good;
  my @res;
  if ($basep) {
    @good = grep {$basep->{$_}} @packs;
    if (@good) {
      @packs = grep {!$basep->{$_}} @packs;
      push @res, @good;
      for my $p (@good) {
	$needed{$_}-- for @{$rdeps{$p} || []};
      }
    }
  }
  # the big sort loop
  while (@packs) {
    @good = grep {$needed{$_} == 0} @packs;
    if (@good) {
      @packs = grep {$needed{$_}} @packs;
      push @res, @good;
      for my $p (@good) {
	$needed{$_}-- for @{$rdeps{$p}};
      }
      next;
    }
    die unless @packs > 1;
    # uh oh, cycle alert. find and remove all cycles.
    my %notdone = map {$_ => 1} @packs;
    $notdone{$_} = 0 for @res;	# already did those
    my @todo = @packs;
    while (@todo) {
      my $v = shift @todo;
      if (ref($v)) {
	$notdone{$$v} = 0;	# finished this one
	next;   
      }
      my $s = $notdone{$v};
      next unless $s;
      my @e = grep {$notdone{$_}} @{$deps{$v}};
      if (!@e) {
	$notdone{$v} = 0;	# all deps done, mark as finished
	next;
      }
      if ($s == 1) {
	$notdone{$v} = 2;	# now under investigation
	unshift @todo, @e, \$v;
	next;
      }
      # reached visited package, found a cycle!
      my @cyc = ();
      my $cycv = $v;
      # go back till $v is reached again
      while(1) {
	die unless @todo;
	$v = shift @todo;
	next unless ref($v);
	$v = $$v;
	$notdone{$v} = 1 if $notdone{$v} == 2;
	unshift @cyc, $v;
	last if $v eq $cycv;
      }
      unshift @todo, $cycv;
      print "cycle: ".join(' -> ', @cyc)."\n";
      if ($cychp) {
	my %nc = map {$_ => 1} @cyc;
	for my $p (@cyc) {
	  next unless $cychp->{$p};
	  $nc{$_} = 1 for @{$cychp->{$p}};
	}
	my $c = [ sort keys %nc ];
	$cychp->{$_} = $c for @$c;
      }
      my $breakv;
      if ($buildp) {
	my @b = grep {$buildp->{$_}} @cyc;
	$breakv = $b[0] if @b;
      }
      if (!defined($breakv)) {
	my @b = @cyc;
	@b = sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @b;
	$breakv = $b[0];
      }
      push @cyc, $cyc[0];
      shift @cyc while $cyc[0] ne $breakv;
      $v = $cyc[1];
      print "  breaking with $breakv -> $v\n";
      $deps{$breakv} = [ grep {$_ ne $v} @{$deps{$breakv}} ];
      $rdeps{$v} = [ grep {$_ ne $breakv} @{$rdeps{$v}} ];
      $needed{$breakv}--;
    }
  }
  return @res;
}

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) = @_;
  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 = Build::query($bin, 'evra' => 1);	# need arch
    next unless $data;
    $data->{'id'} = $id;
    delete $data->{'epoch'};
    delete $data->{'version'};
    delete $data->{'release'};
    $repobins->{$bin} = $data;
  }
  return $repobins;
}

sub findbins {
  my ($prp) = @_;
  local *D;
  my $dir = "$reporoot/$prp/$myarch/:full";
  my $repobins = {};
  my $cnt = 0;

  my $cache;
  if (-e "$dir.cache") {
    eval { $cache = Storable::retrieve("$dir.cache"); };
    warn($@) if $@;
    undef $cache unless ref($cache) eq 'HASH';
    if ($cache) {
      my $byid = {};
      for (keys %$cache) {
	my $v = $cache->{$_};
	$v->{'name'} = $_ unless exists $v->{'name'};
	$byid->{$v->{'id'}} = $v;
      }
      $cache = $byid;
    }
  }
  if (!opendir(D, $dir)) {
    return findbins_remote($prp);
  }
  my @bins = grep {/\.(?:rpm|deb|iso|raw|raw.install)$/} readdir(D);
  closedir D;
  if (!@bins && -s "$dir.subdirs") {
    for my $subdir (split(' ', readstr("$dir.subdirs"))) {
      push @bins, map {"$subdir/$_"} grep {/\.(?:rpm|deb|iso)$/} ls("$dir/$subdir");
    }
  }
  my ($hits, $misses) = (0, 0);
  for my $bin (sort @bins) {
    my @s = stat("$dir/$bin");
    next unless @s;
    my $id = "$s[9]/$s[7]/$s[1]";
    my $data;
    $data = $cache->{$id} if $cache;
    if ($data) {
      $hits++;
    } else {
      $misses++;
      $data = Build::query("$dir/$bin");
      next unless $data;
      $data->{'id'} = $id;
    }
    $data->{'path'} = $bin;	# no dir for now!
    $repobins->{$data->{'name'}} = $data;
    $cnt++;
  }
  if (!$cnt) {
    print "    packages found: none\n";
  } else {
    print "    packages found: $cnt (hits: $hits, misses: $misses)\n";
  }
  if (Storable::nstore($repobins, "$dir.cache.new")) {
    rename("$dir.cache.new", "$dir.cache") || die("rename $dir.cache.new $dir.cache: $!\n");
  }
  # add dir to make real path
  for (values %$repobins) {
    $_->{'path'} = "$prp/$myarch/:full/$_->{'path'}";
  }
  return $repobins;
}

sub enabled {
  my ($repoid, $disen, $default) = @_;
  return $default unless $disen;
  if (($default || !defined($default)) && $disen->{'disable'}) {
    for (@{$disen->{'disable'}}) {
      next if exists($_->{'arch'}) && $_->{'arch'} ne $myarch;
      next if exists($_->{'repository'}) && $_->{'repository'} ne $repoid;
      $default = 0;
      last;
    }
  }
  if (!$default && $disen->{'enable'}) {
    for (@{$disen->{'enable'}}) {
      next if exists($_->{'arch'}) && $_->{'arch'} ne $myarch;
      next if exists($_->{'repository'}) && $_->{'repository'} ne $repoid;
      $default = 1;
      last;
    }
  }
  return $default;
}

#
# this is basically getconfig from the source server
# we do not need any macros, just the config
#
# XXX: this is wrong, the scheduler may have no direct access to
# the config
#
sub getconfig {
  my ($arch, $path) = @_;
  my $config = '';
  for my $prp (reverse @$path) {
    my ($p, $r) = split('/', $prp, 2);
    my $c;
    if (-s "$projectsdir/$p.conf") {
      $c = readstr("$projectsdir/$p.conf", 1);
    } elsif ($remoteprojs{$p}) {
      $c = fetchremoteconfig($p); 
      return undef unless defined $c;
    }
    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
##

my $projpacks;		# global project/package data

#
# 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;
      return;
    }
    # lock failed, dispatcher was faster!
    die("$myjobsdir/$job:status: $!") 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");
    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);
}

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

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

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

  # a new one. expand usedforbuild. write info file.
  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};
    }
  }

  # Get image types defined in KIWI config file
  my @imagetypes = @{$info->{'imagetype'} || []};

  # calculate packages needed for building
  my $prptype = $bconf->{'type'};
  $info->{'file'} =~ /\.(spec|dsc|kiwi)$/;
  my $packtype = $1 || 'spec';

  my @bdeps = @{$info->{'dep'} || []};
  push @bdeps, $aliendeps{$packtype} if $packtype ne $prptype;
  my $eok;
  ($eok, @bdeps) = Build::get_build($bconf, $subpacks, @bdeps);
  if (!$eok) {
    print "        expansion errors:\n";
    print "          $_\n" for @bdeps;
    return (undef, "expansion error: ".join(', ', @bdeps));
  }

  # 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', $pdata->{'versrel'}, 1);
  $h = {'bcnt' => 0} unless $h;

  # 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
  writexml("$reporoot/$prp/$myarch/$packid/.status", "$reporoot/$prp/$myarch/$packid/status", { 'status' => 'scheduled', 'readytime' => time(), 'job' => $f}, $BSXML::buildstatus);

  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;
  my %edeps = map {$_ => 1} @$edeps;
  @bdeps = (@pdeps, @vmdeps, @$edeps, @bdeps);
  my %ddeps;
  for (splice(@bdeps)) {
    next if $ddeps{$_};
    push @bdeps, $_;
    $ddeps{$_} = 1;
  }
  for (@bdeps) {
    $_ = {'name' => $_};
    $_->{'preinstall'} = 1 if $pdeps{$_->{'name'}};
    $_->{'vminstall'} = 1 if $vmdeps{$_->{'name'}};
    $_->{'runscripts'} = 1 if $runscripts{$_->{'name'}};
    $_->{'notmeta'} = 1 unless $edeps{$_->{'name'}};
  }
  
  my $vmd5 = $pdata->{'verifymd5'} || $pdata->{'srcmd5'};
  my $binfo = {
    'project' => $projid,
    'repository' => $repoid,
    'package' => $packid,
    'job' => $f,
    'arch' => $myarch,
    'srcmd5' => $pdata->{'srcmd5'},
    'verifymd5' => $vmd5,
    'rev' => $pdata->{'rev'},
    'file' => $info->{'file'},
    'versrel' => $pdata->{'versrel'},
    'bcnt' => $h->{'bcnt'} + 1,
    'subpack' => ($subpacks || []),
    'imagetype' => \@imagetypes,
    'bdep' => \@bdeps,
    'path' => $searchpath,
  };
  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/$f:new", "$myjobsdir/$f", $binfo, $BSXML::buildinfo);
  # all done. the dispatcher will now pick up the job and send it
  # to a worker.
  return $f;
}


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

#
# 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 $evname = "${projid}::$repoid";
  my $ev = {
    'type' => 'publish',
    'project' => $projid,
    'repository' => $repoid,
  };
  mkdir_p("$eventdir/publish");
  writexml("$eventdir/publish/.$evname$$", "$eventdir/publish/$evname", $ev, $BSXML::event);
  local *F;
  if (sysopen(F, "$eventdir/publish/.ping", POSIX::O_WRONLY|POSIX::O_NONBLOCK)) {
    syswrite(F, 'x');
    close(F);
  }
}

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

  my ($projid, $repoid) = split('/', $prp, 2);
  my $evname = "${projid}::${repoid}::${myarch}";
  my $ev = {
    'type' => 'repository',
    'project' => $projid,
    'repository' => $repoid,
    'arch' => $myarch,
  };
  mkdir_p("$eventdir/repository");
  writexml("$eventdir/repository/.$evname$$", "$eventdir/repository/$evname", $ev, $BSXML::event);
}

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

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

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

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

  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) {
      unlink("$r/$_") for ls($r);
      rmdir($r) || die("rmdir $r: $!\n");
    } else {
      print "    nothing to delete...\n";
      close(F);
      return;
    }
    # release lock
    close(F);
    sendpublishevent($prp);
    return;
  }

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

  my $rinfo = {};
  if (@$packs && $pubenabled && grep {!$_} values(%$pubenabled)) {
    $rinfo = Storable::retrieve("${rdir}info") if -s "${rdir}info";
  }
  $rinfo->{'binaryorigins'} ||= {};

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

  for my $packid (@$packs) {
    if ($pubenabled && !$pubenabled->{$packid}) {
      # publishing of this package is disabled
      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 = grep {/\.(?:deb|rpm|iso)$/} ls($pdir);
    for my $bin (@all) {
      next if exists $origin{$bin};	# first one wins
      $origin{$bin} = $packid;
      if ($filter) {
	my $bad;
	for (@$filter) {
	  next unless $bin =~ /$_/;
	  $bad = 1;
	  last;
	}
	next if $bad;
      }
      my @sr = stat("$rdir/$bin");
      if (@sr) {
        my @s = stat("$pdir/$bin");
        next unless @s;
        next if "$s[9]/$s[7]/$s[1]" eq "$sr[9]/$sr[7]/$sr[1]";
        print "      ! :repo/$bin ($packid)\n";
        unlink("$rdir/$bin");
      } else {
        print "      + :repo/$bin ($packid)\n";
        mkdir_p($rdir) unless -d $rdir;
      }
      link("$pdir/$bin", "$rdir/$bin") || die("link $pdir/$bin $rdir/$bin: $!\n");
      $changed = 1;
    }
  }
  for my $bin (sort(ls($rdir))) {
    next if exists $origin{$bin};
    print "      - :repo/$bin\n";
    unlink("$rdir/$bin") || die("unlink $rdir/$bin: $!\n");
    $changed = 1;
  }

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

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

my $exportcnt = 0;

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

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

  local *F;
  my $jobstatus = {
    'code' => 'finished',
  };
  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);
  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,
  };
  my $evname = "import.$job";
  mkdir_p("$eventdir/$arch");
  writexml("$eventdir/$arch/.$evname", "$eventdir/$arch/$evname", $ev, $BSXML::event);
  if (sysopen(F, "$eventdir/$arch/.ping", POSIX::O_WRONLY|POSIX::O_NONBLOCK)) {
    syswrite(F, 'x');
    close(F);
  }
}


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$' => [],
  },
);

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

sub update_dst_full {
  my ($repodata, $prp, $dst, $jobrepo, $meta, $useforbuildenabled, $prpsearchpath) = @_;

  ##################################################################
  # part 1: move binaries in package directory

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

  my $oldrepo;
  my %new;
  my $isimport;

  if ($dst) {
    # get old state
    my @oldrpms = grep {/\.(?:rpm|deb|iso)$/} ls($dst);
    $oldrepo = findbins_dir([ map {"$dst/$_"} @oldrpms ]);

    # move rpms over
    for my $rp (keys %$jobrepo) {
      my $nn = $rp;
      $nn =~ s/.*\///;
      if ($rp ne "$dst/$nn") {
        rename($rp, "$dst/$nn") || die("rename $rp $dst/$nn: $!\n");
      }
      $new{$nn} = $jobrepo->{$rp};
    }
    # delete old rpms in dst
    for my $rp (grep {!$new{$_}} @oldrpms) {
      unlink("$dst/$rp");
    }
  } else {
    # dst = undef is true for importevents
    $isimport = 1;
    my $jobdatadir = $jobrepo->{'.dir'};
    delete $jobrepo->{'.dir'};
    my $replaced = (readxml("$jobdatadir/replaced.xml", $BSXML::dir, 1) || {})->{'entry'};
    $oldrepo = {};
    for (@{$replaced || []}) {
      my $rp = $_->{'name'};
      $_->{'name'} =~ s/\.[^\.]*$//;
      $_->{'source'} = 1;
      $oldrepo->{$rp} = $_;
    }
    for my $rp (keys %$jobrepo) {
      my $nn = $rp;
      $nn =~ s/.*\///;
      $new{$nn} = $jobrepo->{$rp};
    }
    $dst = $jobdatadir;
  }

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

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

  my $filter;
  # argh, this slows us down a bit
  my $bconf;
  $bconf = getconfig($myarch, $prpsearchpath) if $prpsearchpath;
  $filter = $bconf->{'exportfilter'} if $bconf;
  undef $filter if $filter && !%$filter;
  $filter ||= $default_exportfilters{$myarch};

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

  mkdir_p("$gdst/:full");
  for my $rp (sort keys %new) {
    my $r = $new{$rp};
    next unless $r->{'source'};	# no src in full tree

    if ($filter) {
      my $skip;
      for (sort keys %$filter) {
	if ($rp =~ /$_/) {
	  $skip = $filter->{$_};
	  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;
      }
    }

    my $suf = $rp;
    $suf =~ s/.*\.//;
    my $n = $r->{'name'};
    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");
    if ($suf eq 'rpm') {
      unlink("$gdst/:full/$n.deb");
    } else {
      unlink("$gdst/:full/$n.rpm");
    }

    if ($meta) {
      link($meta, "$meta.dup");
      rename("$meta.dup", "$gdst/:full/$n.meta");
      unlink("$meta.dup");
    } else {
      unlink("$gdst/:full/$n.meta");
    }

    $fnew{$n} = 1;
    delete $r->{'arch'};	# not in repodata
    $r->{'path'} = "$prp/$myarch/:full/$n.$suf";
    $repodata->{$n} = $r;
  }
  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 (sort keys %$filter) {
	if ($rn =~ /$_/) {
	  $skip = $filter->{$_};
	  last;
	}
      }
      if ($skip) {
        for my $exportarch (@$skip) {
	  $exports{$exportarch} ||= [] if $exportarch ne '.' && $exportarch ne $myarch;
	}
      }
    }
  }

  for my $exportarch (sort keys %exports) {
    # check if this prp supports the arch
    my ($projid, $repoid) = split('/', $prp, 2);
    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, @{$exports{$exportarch}});
    }
  }

  # delete obsolete full entries
  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" . ($rp =~ /\.rpm$/ ? '.rpm' : '.deb'));

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

  # update :full cache file
  for my $pack (values %$repodata) {
    delete $pack->{'meta'};
    $pack->{'path'} =~ s/.*\///;
  }
  if (Storable::nstore($repodata, "$gdst/:full.cache.new")) {
    rename("$gdst/:full.cache.new", "$gdst/:full.cache") || die("rename $gdst/:full.cache.new $gdst/:full.cache: $!\n");
  }
  for my $pack (values %$repodata) {
    $pack->{'path'} = "$prp/$myarch/:full/$pack->{'path'}";
  }
}

sub addjobhist {
  my ($info, $status, $js) = @_;
  my $jobhist = {};
  $jobhist->{$_} = $status->{$_} for qw{readytime status};
  $jobhist->{$_} = $js->{$_} for qw{starttime endtime uri hostarch};
  $jobhist->{$_} = $info->{$_} for qw{project repository package arch srcmd5};
  BSFileDB::fdb_add("$myinfodir/jobhistory", $BSXML::jobhistlay, $jobhist);
}


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

my @prps;		# all prps 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
			# 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;
my %watchremote_start;

my %repounchanged;
my %globalnotready;

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)
#
# calls calc_prps and calc_projpacks_linked for post-processing
#

sub get_projpacks {
  my ($projid, $packid) = @_;

  if (!$projpacks) {
    undef $projid;
    undef $packid;
  }
  undef $packid unless defined $projid;

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

  my @args;
  if (defined($projid) && defined($packid)) {
    print "getting data for project '$projid' package '$packid' from $BSConfig::srcserver\n";
    push @args, "project=$projid", "package=$packid";
    delete $projpacks->{$projid}->{'package'}->{$packid} if $projpacks->{$projid} && $projpacks->{$projid}->{'package'};
  } 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) {
    eval {
      $projpacksin = BSRPC::rpc("$BSConfig::srcserver/getprojpack", $BSXML::projpack, 'withsrcmd5', 'withdeps', 'withrepos', "arch=$myarch", @args);
    };
    if ($@ || !$projpacksin) {
      print $@ if $@;
      if (@args) {
        print "retrying...\n";
        get_projpacks();
        return;
      }
      printf("could not get project/package information, sleeping 1 minute\n");
      sleep(60);
      print "retrying...\n";
      next;
    }
    last;
  }
  for my $proj (@{$projpacksin->{'project'} || []}) {
    if (defined($packid)) {
      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->{$_};
        }
      }
    }
    $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'};
    }
  }

  %watchremote = ();
  %watchremoteprojs = ();

  #print Dumper($projpacks);
  calc_projpacks_linked();
  calc_prps();

  updateremoteprojs();
  %watchremoteprojs = ();
}

#
# addwatchremote:  register for a remote resource
#
# input:  $type: type of resource (project/repository/source)
#         $projid: update just this 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'};
      for my $li (@{$pack->{'linked'}}) {
	addwatchremote('project', $li->{'project'}, "/$li->{'package'}");
	$li->{'myproject'} = $projid;
	$li->{'mypackage'} = $mypackid;
      }
      push @projpacks_linked, @{$pack->{'linked'}};
    }
  }
  #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'} || []};
  for my $pathel (@path) {
    addwatchremote('repository', $pathel->{'project'}, "/$pathel->{'repository'}/$myarch");
  }
  # 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;
    if (!@path) {
      last if $done{"/$prp"};
      my ($pid, $tid) = ($t->{'project'}, $t->{'repository'});
      my $proj = addwatchremote('project', $pid, '');
      if ($proj) {
	# check/invalidate cache?
	$proj = fetchremoteproj($proj, $pid);
	# clone it as we modify the repopath
	$proj = Storable::dclone($proj);
        my @repo = grep {$_->{'name'} eq $tid} @{$proj->{'repository'} || []};
        if (@repo && $repo[0]->{'path'}) {
	  addwatchremote('repository', $pid, "/$tid/$myarch");
	  for my $pathel (@{$repo[0]->{'path'}}) {
	    # map projects to remote
	    my $remoteprojid = $pathel->{'project'};
	    $pathel->{'project'} = maptoremote($proj, $remoteprojid);
	    addwatchremote('repository', $pathel->{'project'}, "/$pathel->{'repository'}/$myarch") if $pathel->{'project'} ne '_unavailable';
	  }
	}
      } else {
	$proj = $projpacks->{$pid};
      }
      next unless $proj;
      $done{"/$prp"} = 1;	# mark expanded
      my @repo = grep {$_->{'name'} eq $tid} @{$proj->{'repository'} || []};
      push @path, @{$repo[0]->{'path'}} if @repo && $repo[0]->{'path'};
    }
  }
  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'} || {}});
    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;
      if (@aggs) {
	my @xsp;
	# 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 (@xsp) {
	  # found some repos, join aggregate deps with project deps
	  for my $xsp (@xsp) {
	    my ($aprojid, $arepoid) = split('/', $xsp, 2);
	    # we just watch the repository as it costs too much to
            # watch every single package
	    addwatchremote('repository', $aprojid, "/$arepoid/$myarch");
	  }
	  my %xsp = map {$_ => 1} (@sp, @xsp);
          $prpdeps{$prp} = [ sort keys %xsp ];
	}
      }
      for (@{$prpdeps{$prp}}) {
        $prpnoleaf{$_} = 1 if $_ ne $prp;
      }
    }
  }
  # do the real sorting
  print "sorting projects and repositories...\n";
  @prps = sortpacks(\%prpdeps, undef, undef, 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,
  };
  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->{$_};
  }
  $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,
  };
  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 findbins_remote {
  my ($prp) = @_;
  my ($projid, $repoid) = split('/', $prp, 2);
  my $proj = $remoteprojs{$projid};
  return {} if !$proj || $proj->{'error'};
  print "fetching remote repository state for $prp\n";
  my $param = {
    'uri' => "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid/$myarch/_repository",
    'timeout' => 200,
    'receiver' => \&BSHTTP::cpio_receiver,
  };
  my $cpio;
  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 $globalnotready{$prp};
  if ($repostate && $repostate->{'blocked'}) {
    $globalnotready{$prp} = { map {$_ => 1} @{$repostate->{'blocked'}}};
  }
  my $cachedata = $cpio{'repositorycache'};
  return {} unless $cachedata;
  my $cache;
  eval { $cache = Storable::thaw(substr($cachedata, 4)); };
  undef $cachedata;
  warn($@) if $@;
  return undef unless $cache;
  for (values %$cache) {
    delete $_->{'path'};
    delete $_->{'id'};
  }
  return $cache;
}

#
# jobfinished - called when a build job is finished
#
# - move built packages into :full tree
#   (updates corresponding $repodata entry)
# - set changed flag
#
# input: $job       - job identification
#        $js        - job status information (BSXML::jobstatus)
#        $repodatas - reference of global repodata hash
#        $changed   - reference to changed hash, mark prp if
#                     we changed the repository
#
sub jobfinished {
  my ($job, $js, $repodatas, $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 $prp = $info->{'path'}->[0];
  my $projid = $prp->{'project'};
  my $repoid = $prp->{'repository'};
  my $packid = $info->{'package'};
  $prp = "$projid/$repoid";
  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";
  if (! -d $statusdir) {
    print "  - $job belongs to obsolete package\n";
    return;
  }
  my $status = readxml("$statusdir/status", $BSXML::buildstatus, 1);
  if (!$status) {
    print "  - $job has no status\n";
    return;
  }
  if (!$status->{'job'} || $status->{'job'} ne $job) {
    print "  - $job is outdated\n";
    return;
  }
  delete $status->{'job'};	# no longer building

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

  my @all = ls($jobdatadir);
  my %all = map {$_ => 1} @all;
  @all = map {"$jobdatadir/$_"} @all;
  my $jobrepo = findbins_dir(\@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 (%$jobrepo) {
      print "  - $job claims success but there is no meta\n";
      return;
    }
    # severe failure, create src change fake...
    writestr("$jobdatadir/meta", undef, "$info->{'srcmd5'}  $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
  my $ps = readxml("$reporoot/$prp/$myarch/:packstatus", $BSXML::packstatuslist, 1);
  if ($ps) {
    for (@{$ps->{'packstatus'} || []}) {
      next unless $_->{'name'} eq $packid;
      $_->{'status'} = 'finished';
      $_->{'error'} = %$jobrepo ? 'succeeded' : 'failed';
    }
    writexml("$reporoot/$prp/$myarch/.:packstatus", "$reporoot/$prp/$myarch/:packstatus", $ps, $BSXML::packstatuslist);
  }

  my $meta = $all{'meta'} ? "$jobdatadir/meta" : undef;
  if (!%$jobrepo) {
    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($info, $status, $js);
    writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
    $changed->{$prp} ||= 1;	# package is no longer blocking
    return;
  }
  print "  - $prp: $packid built: ".(keys %$jobrepo). " packages\n";
  mkdir_p("$gdst/:logfiles.success");
  mkdir_p("$gdst/:logfiles.fail");

  $repodatas->{$prp} ||= findbins($prp);
  my $repodata = $repodatas->{$prp};

  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($repodata, $prp, $dst, $jobrepo, $meta, $useforbuildenabled, $prpsearchpath{$prp});
  $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($info, $status, $js);
  writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
  # write history file
  my $h = {'versrel' => $info->{'versrel'}, 'bcnt' => $info->{'bcnt'}, 'time' => time(), 'srcmd5' => $info->{'srcmd5'}, 'rev' => $info->{'rev'}};
  BSFileDB::fdb_add("$reporoot/$prp/$myarch/$packid/history", $historylay, $h);

  # 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 importevent {
  my ($job, $js, $repodatas, $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;
  @all = map {"$jobdatadir/$_"} @all;
  my $jobrepo = findbins_dir(\@all);
  $jobrepo->{'.dir'} = $jobdatadir;
  $repodatas->{$prp} ||= findbins($prp);
  my $repodata = $repodatas->{$prp};
  my $useforbuildenabled = 1;
  update_dst_full($repodata, $prp, undef, $jobrepo, undef, $useforbuildenabled, $prpsearchpath{$prp});
  $changed->{$prp} = 2 if $useforbuildenabled;
  unlink($_) for @all;
  rmdir($jobdatadir);
}

##########################################################################
##########################################################################
##
##  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, $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 ($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);
    }
    for my $arepoid (@arepoids) {
      my $arepo = (grep {$_->{'name'} eq $arepoid} @{$proj->{'repository'} || []})[0];
      if (!$arepo || !grep {$_ eq $myarch} @{$arepo->{'arch'} || []}) {
	push @broken, "$aprojid/$arepoid";
	next;
      }
      push @blocked, "$aprojid/$arepoid" unless $remoteprojs{$aprojid} || $prpfinished->{"$aprojid/$arepoid"};
    }
  }
  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,
	  };
	  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 {/\.(?: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);
}

## -> repserver
sub resign {
  my ($projid, @bins) = @_;
  return unless $BSConfig::sign;
  my $signargs = [];
  my $signkey;
  while(1) {
    eval {
      $signkey = BSRPC::rpc("$BSConfig::srcserver/getsignkey", undef, "project=$projid");
    };
    if ($@) {
      warn($@);
      sleep(10);
      next;
    }
    last;
  }
  if ($signkey) {
    mkdir_p("$uploaddir");
    writestr("$uploaddir/sched.$$", undef, $signkey);
    $signargs = [ '-P', "$uploaddir/sched.$$" ];
  }
  for my $bin (@bins) {
    next unless $bin =~ /.rpm$/;
    system('rpm', '--delsign', $bin) && warn("delsign failed\n");
    system($BSConfig::sign, @$signargs, '-r', $bin) && warn("sign failed\n");
  }
  unlink("$uploaddir/sched.$$") if $signkey;
}

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

  my $prp = "$projid/$repoid";
  my @aggregates = @{$pdata->{'aggregatelist'}->{'aggregate'} || []};
  my $job = jobname($prp, $packid);
  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;
        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,
	  };
	  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 || []}) {
	    push @d, "$jobdatadir/$bin->{'name'}";
	  }
        } else {
	  my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
	  @d = grep {/\.(?:rpm|deb)$/} ls($d);
          @d = map {"$d/$_"} sort(@d);
	}
	my $ajobrepo = findbins_dir(\@d);
	my $copysources = 0;
	for my $abin (sort keys %$ajobrepo) {
	  my $r = $ajobrepo->{$abin};
	  next unless $r->{'source'};
	  next if $abinfilter && !$abinfilter->{$r->{'name'}};
	  next if $jobbins{$r->{'name'}};
	  $jobbins{$r->{'name'}} = 1;
	  my $basename = $abin;
	  $basename =~ s/.*\///;
	  $basename =~ s/^upload:// if $cpio;
	  BSUtil::cp($abin, "$jobdatadir/$basename");
	  $jobrepo->{"$jobdatadir/$basename"} = $r;
	  $copysources = 1;
	}
	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";
    return ('failed', $error);
  }
  writestr("$jobdatadir/meta", undef, $new_meta);

  resign($projid, sort keys %$jobrepo) if $BSConfig::sign;

  # now commit job into build area (and full tree if enabled)
  my $gdst = "$reporoot/$prp/$myarch";
  my $dst = "$gdst/$packid";
  mkdir_p($dst);
  $repodatas->{$prp} ||= findbins($prp);
  my $repodata = $repodatas->{$prp};
  my $useforbuildenabled = 1;
  $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
  $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
  update_dst_full($repodata, $prp, $dst, $jobrepo, undef, $useforbuildenabled, $prpsearchpath{$prp});
  $changed->{$prp} = 2 if $useforbuildenabled;
  delete $repounchanged{$prp} if $useforbuildenabled;
  $changed->{$prp} ||= 1;
  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");

  # commit done, clean up
  unlink "$jobdatadir/$_" for ls($jobdatadir);
  rmdir($jobdatadir);

  print "        rebuilt\n";
  unlink("$reporoot/$prp/$myarch/:repodone");
  return ('succeeded');
}

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, $changed_high, $lookat_oobhigh, $lookat_oob, $lookat_next) = @_;

  push @$lookat_oobhigh, grep {$changed_high->{$_}} sort keys %$changed;
  push @$lookat_oob, grep {!$changed_high->{$_}} sort keys %$changed;
  @$lookat_oobhigh = unify(@$lookat_oobhigh);
  @$lookat_oob = unify(@$lookat_oob);
  my %lookat_oobhigh = map {$_ => 1} @$lookat_oobhigh;
  @$lookat_oob = grep {!$lookat_oobhigh{$_}} @$lookat_oob;
  for my $prp (@prps) {
    if (!$changed->{$prp}) {
      # FIXME: aggregates do not use the full tree!
      next unless grep {$changed->{$_} && $changed->{$_} != 1} @{$prpdeps{$prp}};
    }
    $lookat_next->{$prp} = 1;
  }
  %$changed = ();
  %$changed_high = ();
}

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

$| = 1;
print "starting build service scheduler\n";

# get lock
mkdir_p($rundir);
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, $myinfodir) {
  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 %repodata;
my %changed;
my %changed_high;
my %prpfinished;
my %lastcheck;

my @lookat;             # not so important
my %lookat_next;	# not so important, next series
my @lookat_oob;         # do those first (out of band)
my @lookat_oobhigh;     # do those really first so that our users are happy


# read old state if present
if (-s "$rundir/bs_sched.$myarch.state") {
  print "reading old state...\n";
  my $schedstate;
  eval {
    $schedstate = Storable::retrieve("$rundir/bs_sched.$myarch.state");
  };
  if ($@) {
    print "$@";
    undef $schedstate;
  }
  unlink("$rundir/bs_sched.$myarch.state");
  if ($schedstate) {
    # just for testing...
    print "  - $_\n" for sort keys %$schedstate;
    if ($schedstate->{'projpacks'}) {
      $projpacks = $schedstate->{'projpacks'};
      calc_projpacks_linked();
      calc_prps();
    } else {
      # get project and package information from src server
      get_projpacks();
    }

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

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

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

    my $oldchanged = $schedstate->{'changed'} || {};
    my $oldchanged_high = $schedstate->{'changed_high'} || {};
    for my $projid (keys %$projpacks) {
      $changed{$projid} = $oldchanged->{$projid} if exists $oldchanged->{$projid};
      $changed_high{$projid} = $oldchanged_high->{$projid} if exists $oldchanged_high->{$projid};
    }
    for my $prp (@prps) {
      $changed{$prp} = $oldchanged->{$prp} if exists $oldchanged->{$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 globalnotready hash
    my $oldglobalnotready = $schedstate->{'globalnotready'} || {};
    for my $prp (@prps) {
      $globalnotready{$prp} = $oldglobalnotready->{$prp} if exists $oldglobalnotready->{$prp};
    }
  }
}

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

my %remotewatchers;
my %nextmed;

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

while(1) {
  if (%changed) {
    changed2lookat(\%changed, \%changed_high, \@lookat_oobhigh, \@lookat_oob, \%lookat_next);
    next;
  }

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

  for my $remoteurl (sort keys %watchremote) {
    if ($remotewatchers{$remoteurl}) {
      next;
    }
    if ($watchremote_start{$remoteurl}) {
      print "setting up watcher for $remoteurl, start=$watchremote_start{$remoteurl}\n";
    } else {
      print "setting up watcher for $remoteurl\n";
    }
    my $watchlist = join("\0", sort keys %{$watchremote{$remoteurl}});
    my $param = {
      'uri' => "$remoteurl/lastevents",
      'async' => 1,
    };
    my @args = map {"filter=$_"} sort keys %{$watchremote{$remoteurl}};
    push @args, "start=$watchremote_start{$remoteurl}" if $watchremote_start{$remoteurl};
    my $ret;
    eval {
      $ret = BSRPC::rpc($param, $BSXML::events, @args);
    };
    if ($@) {
      warn($@);
      print "retrying in 60 seconds\n";
      $ret = {'retry' => time() + 60};
    }
    $ret->{'watchlist'} = $watchlist;
    $ret->{'remoteurl'} = $remoteurl;
    $remotewatchers{$remoteurl} = $ret;
  }

  my $dummy;
  my $gotevent;
  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 (%remotewatchers) {
    my @readok = select_read(0, $pingsock, values %remotewatchers);
    $gotevent = 1 if @readok;
    for my $watcher (@readok) {
      my $remoteurl = $watcher->{'remoteurl'};
      next if $remoteurl eq 'ping';
      if ($watcher->{'retry'}) {
        print "retrying watcher for $remoteurl\n";
	delete $remotewatchers{$remoteurl};
	next;
      }
      print "response from watcher for $remoteurl\n";
      my $ret;
      eval {
        $ret = BSRPC::rpc($watcher);
      };
      if ($@) {
        warn $@;
        close($watcher->{'socket'}) if $watcher->{'socket'};
	delete $watcher->{'socket'};
        $watcher->{'retry'} = time() + 60;
	print "retrying in 60 seconds\n";
	next;
      }
      if ($ret->{'sync'} && $ret->{'sync'} eq 'lost') {
        # ok to lose sync on call with no start (actually not, FIXME)
        if ($watchremote_start{$remoteurl}) {
	  # synthesize all events we watch
	  for my $watch (sort keys %{$watchremote{$remoteurl} || {}}) {
	    my $projid = $watchremote{$remoteurl}->{$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'}";
	} elsif ($ev->{'type'} eq 'repository') {
	  $watch = "repository/$ev->{'project'}/$ev->{'repository'}/$myarch";
	} else {
	  next;
	}
	my $projid = $watchremote{$remoteurl}->{$watch};
	next unless defined $projid;
	push @remoteevents, {%$ev, 'project' => $projid};
      }
      $watchremote_start{$remoteurl} = $ret->{'next'} if $ret->{'next'};
      delete $remotewatchers{$remoteurl};
    }
  } 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 array is empty, start new series with lookat_next
  if (!@lookat && %lookat_next) {
    @lookat = grep {$lookat_next{$_}} @prps;
    %lookat_next = ();
  }

  if (!$gotevent && !@lookat && !@lookat_oob && !@lookat_oobhigh) {
    print "waiting for an event...\n";
    my $timeout;
    if (%remotewatchers) {
      select_read($timeout, $pingsock, @retryevents, values %remotewatchers);
      next;
    } else {
      sysread(PING, $dummy, 1, 0);
      $gotevent = 1;
    }
  }
  if ($gotevent) {
    # drain ping pipe
    fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
    1 while (sysread(PING, $dummy, 1024, 0) || 0) > 0;
    fcntl(PING,F_SETFL,0);

    # check for events
    my @events = ls($myeventdir);
    @events = grep {!/^\./} @events;
    @events = sort @events;
    while (@events || @remoteevents) {
      my $ev;
      if (@events) {
        my $event = shift @events;
        print "event $event \n".@events." left)\n";
        if ($event =~ /^finished:(.*)/) {
	  $ev = {'type' => 'built', 'job' => $1};
        } else {
	  $ev = readxml("$myeventdir/$event", $BSXML::event, 1);
        }
        unlink("$myeventdir/$event");
      } else {
        $ev = shift @remoteevents;
      }
      next unless $ev && $ev->{'type'};

      if ($ev->{'type'} eq 'built' || $ev->{'type'} eq 'import') {
	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') {
	  jobfinished($job, $js, \%repodata, \%changed);
	} else {
	  importevent($job, $js, \%repodata, \%changed);
	}
	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;
	next;
      }

      if ($ev->{'type'} eq 'srcevent' || $ev->{'type'} eq 'package') {
	my $projid = $ev->{'project'};
	my $packid = $ev->{'package'};
	get_projpacks($projid, $packid);
	# need map to create safe copy of array
	for my $linfo (map {$_} grep {$_->{'project'} eq $projid && (!defined($packid) || $_->{'package'} eq $packid)} @projpacks_linked) {
	  my $lprojid = $linfo->{'myproject'};
	  my $lpackid = $linfo->{'mypackage'};
	  get_projpacks($lprojid, $lpackid);
	  for my $prp (@prps) {
	    $changed_high{$prp} ||= 1 if (split('/', $prp, 2))[0] eq $lprojid;
	  }
	  $changed_high{$lprojid} ||= 1;
	}
	for my $prp (@prps) {
	  $changed_high{$prp} ||= 1 if (split('/', $prp, 2))[0] eq $projid;
	}
	$changed_high{$projid} ||= 1;
	next;
      }

      if ($ev->{'type'} eq 'projevent' || $ev->{'type'} eq 'project') {
	my $projid = $ev->{'project'};
	get_projpacks($projid);
	# need map to create safe copy of array
	for my $linfo (map {$_} grep {$_->{'project'} eq $projid} @projpacks_linked) {
	  my $lprojid = $linfo->{'myproject'};
	  my $lpackid = $linfo->{'mypackage'};
	  next if $lprojid eq $projid;
	  for my $prp (@prps) {
	    $changed{$prp} ||= 1 if (split('/', $prp, 2))[0] eq $lprojid;
	  }
	  $changed{$lprojid} ||= 1;
	}
	# we use 2 here as a config change is relevant for all
        # projects building with us
	for my $prp (@prps) {
	  $changed_high{$prp} = 2 if (split('/', $prp, 2))[0] eq $projid;
	}
	$changed_high{$projid} = 2;
	next;
      }

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

      if ($ev->{'type'} eq 'rebuild' || $ev->{'type'} eq 'recheck') {
	my $projid = $ev->{'project'};
	my $packid = $ev->{'package'};
	for my $prp (@prps) {
	  $changed_high{$prp} ||= 1 if (split('/', $prp, 2))[0] eq $projid;
	}
	$changed_high{$projid} ||= 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";
	  %repodata = ();
	  next;
	}
	if (defined($projid) && defined($repoid)) {
	  my $prp = "$projid/$repoid";
	  print "reading packages of repository $projid/$repoid\n";
	  delete $repodata{$prp};
	  $repodata{$prp} = findbins($prp) if -d "$reporoot/$prp/$myarch";
	  $changed_high{$prp} = 2;
          delete $repounchanged{$prp};
	}
	next;
      }

      if ($ev->{'type'} eq 'dumprepo') {
	my $prp = "$ev->{'project'}/$ev->{'repository'}";
	my $repodata = $repodata{$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 '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
	$repodata{$prp} ||= findbins($prp);
	my $repodata = $repodata{$prp};
	update_dst_full($repodata, $prp, "$gdst/$packid" , {}, undef, 1, $prpsearchpath{$prp});
        delete $repounchanged{$prp};
	# delete other files
	unlink("$gdst/:logfiles.success/$packid");
	unlink("$gdst/:logfiles.fail/$packid");
	unlink("$gdst/:meta/$packid");
	unlink("$gdst/$packid/$_") for ls("$gdst/$packid");
	rmdir("$gdst/$packid");
	for $prp (@prps) {
	  $changed_high{$prp} = 2 if (split('/', $prp, 2))[0] eq $projid;
	}
	$changed_high{$projid} = 2;
	next;
      }

      if ($ev->{'type'} eq 'exit' || $ev->{'type'} eq 'dumpstate') {
	print "exiting...\n" if $ev->{'type'} eq 'exit';
	print "dumping scheduler state...\n" if $ev->{'type'} eq 'dumpstate';
	my @new_lookat = @lookat;
        push @new_lookat, grep {$lookat_next{$_}} @prps;
	# here comes our scheduler state
	my $schedstate = {};
	#$schedstate->{'projpacks'} = $projpacks;
	#$schedstate->{'lastcheck'} = \%lastcheck;
	#$schedstate->{'repodata'} = \%repodata;
	$schedstate->{'prps'} = \@prps;
	$schedstate->{'changed'} = \%changed;
	$schedstate->{'changed_high'} = \%changed_high;
	$schedstate->{'lookat'} = \@new_lookat;
	$schedstate->{'lookat_oob'} = \@lookat_oob;
	$schedstate->{'lookat_oobhigh'} = \@lookat_oobhigh;
	$schedstate->{'prpfinished'} = \%prpfinished;
	$schedstate->{'globalnotready'} = \%globalnotready;
	unlink("$rundir/bs_sched.$myarch.state");
	if (!Storable::nstore($schedstate, "$rundir/bs_sched.$myarch.state")) {
	  unlink("$rundir/bs_sched.$myarch.state");
	}
	if ($ev->{'type'} eq 'exit') {
	  print "bye.\n";
	  exit(0);
	}
	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;
	  $repodata{$prp} ||= findbins($prp);
	  my $jobrepo = findbins_dir("$gdst/$packid");
	  my $meta = "$gdst/:meta/$packid";
          undef $meta unless -s $meta;
	  update_dst_full($repodata{$prp}, $prp, "$gdst/$packid", $jobrepo, $meta, $useforbuildenabled, $prpsearchpath{$prp});
	}
	for $prp (@prps) {
	  $changed_high{$prp} = 2 if (split('/', $prp, 2))[0] eq $projid;
	}
	$changed_high{$projid} = 2;
	next;
      }

      print "unknown event type: $ev->{'type'}\n";
    }
    # add all changed_high entries to changed to make things simpler
    for (keys %changed_high) {
      next if $changed{$_} && $changed{$_} == 2;
      $changed{$_} = $changed_high{$_};
    }
    next;
  }

  my $prp;
  if (@lookat_oobhigh) {
    $prp = shift @lookat_oobhigh;
    print "looking at high prio $prp";
  } elsif (@lookat_oob) {
    $prp = shift @lookat_oob;
    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_oob;
	last unless defined $prp;
      }
      unshift @lookat_oob, @notyet;
      $prp = shift @lookat_oob if !defined($prp) && !@lookat;
    }
    print "looking at med prio $prp" if defined $prp;
  }
  if (!defined($prp)) {
    $prp = shift @lookat;
    print "looking at low prio $prp";
  }
  print " (".@lookat_oobhigh."/".@lookat_oob."/".@lookat."/".(keys %lookat_next)."/".@prps.")\n";
  delete $nextmed{$prp};

  my ($projid, $repoid) = split('/', $prp, 2);
  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 $repodata{$prp};
      delete $prpfinished{$prp};
      delete $globalnotready{$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") {
	  unlink("$reporoot/$prp/$myarch/$dir/$_") for ls("$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{$prp} = 2;
      sendrepochangeevent($prp);
      killbuilding($prp);
      prpfinished($prp);
      # now that :repo is gone we can remove the directory
      rmdir("$reporoot/$prp/$myarch") || die("$reporoot/$prp/$myarch: $!\n");
      # XXX this should be rewitten 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;
  }

  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";
    $prpfinished{$prp} = 1;
    next;
  }
  my $prptype = $bconf->{'type'};
  print "  - $prp ($prptype)\n";

  if (grep {$_->{'name'} eq $repoid && $_->{'status'} && $_->{'status'} eq 'disabled'} @{$projpacks->{$projid}->{'repository'}}) {
    print "      disabled\n";
    $prpfinished{$prp} = 1;
    next;
  }

  mkdir_p("$reporoot/$prp/$myarch");

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

  # Step 2a: check if packages got deleted
  for my $packid (grep {!/^:/ && !$packs->{$_}} ls("$reporoot/$prp/$myarch")) {
    print "      - $packid: is obsolete\n";
    delete $lastcheck{"$prp/$packid"};
    my $gdst = "$reporoot/$prp/$myarch";
    # delete full entries
    $repodata{$prp} ||= findbins($prp);
    my $repodata = $repodata{$prp};
    update_dst_full($repodata, $prp, "$gdst/$packid" , {}, undef, 1, $prpsearchpath{$prp});
    $changed{$prp} = 2;
    sendrepochangeevent($prp);
    # delete other files
    unlink("$gdst/:logfiles.success/$packid");
    unlink("$gdst/:logfiles.fail/$packid");
    unlink("$gdst/:meta/$packid");
    unlink("$gdst/$packid/$_") for ls("$gdst/$packid");
    rmdir("$gdst/$packid");
    killbuilding($prp, $packid);
    unlink("$reporoot/$prp/$myarch/:repodone");
  }

  # Step 2b: calculate build repository
  my %building;
  my %notready;
  my %dep2src;
  my %dep2rpm;
  my %depislocal;	# used in meta calculation
  my @repos;
  my $error;

  for my $rprp (@{$prpsearchpath{$prp}}) {
    $repodata{$rprp} ||= findbins($rprp);
    my $rd = $repodata{$rprp};
    if (!$rd) {
      $error = "repository '$rprp' is unavailable";
      last;
    }
    push @repos, $rd;
    my $gn = $globalnotready{$rprp} || {};
    if ($rprp eq $prp) {
      $gn = {};		# ignore last not ready
      $depislocal{$_} = 1 for keys %$rd;
    }
    for (keys %$rd) {
      next if exists $dep2rpm{$_};
      $dep2rpm{$_} = $rd->{$_};
      my $pname = $rd->{$_}->{'source'};
      $dep2src{$_} = $pname;
      $notready{$pname} = 2 if $gn->{$pname};
    }
  }
  if ($error) {
    print "    $error\n";
    next;
  }

  my $prpchecktime = time();

#    for my $rd (reverse @repos) {
#      for (keys %$rd) {
#	$dep2src{$_} = $rd->{$_}->{'source'};
#	$dep2rpm{$_} = $rd->{$_};
#      }
#    }

  # Step 2c: expand all dependencies, put them in %pdeps hash
  my %subpacks;
  push @{$subpacks{$dep2src{$_}}}, $_ for keys %dep2src;
  print "    expanding dependencies\n";
  my %experrors;
  Build::readdeps($bconf, undef, reverse @repos);

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

    # 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'})) {
      $pdeps{$packid} = [];
      next;
    }

    $info->{'file'} =~ /\.(spec|dsc|kiwi)$/;
    my $packtype = $1 || 'spec';

    my @deps = @{$info->{'dep'} || []};
    push @deps, $aliendeps{$packtype} if $packtype ne $prptype;
    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 %cychash;
  @packs = sortpacks(\%pdeps, \%dep2src, undef, undef, \%cychash, @packs);

  # bring expansion errors to back (is this really needed? we do not modify the
  # repodata in the build service)
  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'};

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

  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}};
	  }
	}
      }
    }
    my $pdata = $packs->{$packid};
    if ($pdata->{'error'}) {
      print "      - $packid ($pdata->{'error'})\n";
      if ($pdata->{'error'} eq 'disabled' || $pdata->{'error'} eq 'excluded') {
	$packstatus{$packid} = $pdata->{'error'};
	next;
      }
      $packstatus{$packid} = 'broken';
      $packerror{$packid} = $pdata->{'error'};
      next;
    }

    if ($pdata->{'build'}) {
      if (!enabled($repoid, $pdata->{'build'}, $projbuildenabled)) {
	if ($projbuildenabled) {
	  print "      - $packid (disabled)\n";
	} else {
	  print "      - $packid (disabled on project level, not enabled)\n";
	}
	$packstatus{$packid} = 'disabled';
	next;
      }
    } else {
      if (!$projbuildenabled) {
	print "      - $packid (disabled on project level)\n";
	$packstatus{$packid} = 'disabled';
	next;
      }
    }

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

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

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

    if ($pdata->{'aggregatelist'}) {
      my ($astatus, $aerror) = checkaggregate($projid, $repoid, $packid, $pdata, \%prpfinished);
      if ($astatus eq 'scheduled') {
	# aerror contains new meta file in this case
	($astatus, $aerror) = rebuildaggregate($projid, $repoid, $packid, $pdata, \%repodata, \%changed, $aerror);
	if ($astatus eq 'scheduled') {
	  # aerror contains jobid in this case
	  $notready{$pname} = 1;
	  $building{$packid} = $aerror || 'job';
	  undef $aerror;
	}
	unlink("$reporoot/$prp/$myarch/:repodone");
      }
      $notready{$pname} = 1 if $astatus eq 'blocked';
      $packstatus{$packid} = $astatus;
      $packerror{$packid} = $aerror if defined $aerror;
      next;
    }

    if (!exists $info->{'file'}) {
      print "      - $packid (no spec/dsc/kiwi file)\n";
      $packstatus{$packid} = 'broken';
      $packerror{$packid} = 'no spec/dsc/kiwi file';
      next;
    }
    $info->{'file'} =~ /\.(spec|dsc|kiwi)$/;
    my $packtype = $1 || 'spec';
    #print "      - $packid ($packtype)\n";

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

    my @blocked = grep {$notready{$dep2src{$_}}} @{$pdeps{$packid}};
    if ($cychash{$packid}) {
      # package belongs to a cycle
      next if $packstatus{$packid} && $packstatus{$packid} ne 'done'; # already decided in phase 1
      if ($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";
	$notready{$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";
      $notready{$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";
	$building{$packid} = $job;
	$notready{$pname} = 1;
        # FIXME: this leads to have "finished" jobs being displayed as "scheduled".
	$packstatus{$packid} = 'scheduled';
	next;
      }
    }
    if (open(F, '<', "$reporoot/$prp/$myarch/:meta/$packid")) {
      my @meta = <F>;
      close F;
      chomp @meta;
      if ($meta[0] ne "$pdata->{srcmd5}  $packid") {
	print "      - $packid ($packtype)\n";
	print "        src change, start build\n";
      } elsif (@meta == 2 && $meta[1] =~ /fake/) {
	print "      - $packid ($packtype)\n";
	my @s = stat("$reporoot/$prp/$myarch/:meta/$packid");
	if (!@s || $s[9] + 14400 > time()) {
	  print "        buildsystem setup failure\n";
	  $packstatus{$packid} = 'failed';
	  next;
	} else {
	  print "        retrying bad build\n";
	}
      } else {
	if ($incycle) {
	  # print "      - $packid ($packtype)\n";
	  # print "        in cycle, no source change...\n";
	  $packstatus{$packid} = 'done';
	  next;
	}
	my $check = join('', @meta);
	for my $bpack (sort @{$pdeps{$packid}}) {
	  $check .= $dep2rpm{$bpack}->{'hdrmd5'};
	}
	$check = Digest::MD5::md5_hex($check);
	if ($lastcheck{"$prp/$packid"} && $lastcheck{"$prp/$packid"} eq $check) {
	  # print "      - $packid ($packtype)\n";
	  # print "        nothing changed\n";
	  $packstatus{$packid} = 'done';
	  next;
	}
	my @new_meta = ();
	for my $bpack (@{$pdeps{$packid}}) {
	  my $r = $dep2rpm{$bpack};
	  die unless $r;
          if ($depislocal{$bpack} && $r->{'path'}) {
	    if (!$r->{'meta'}) {
	      my @m;
	      my $mf = substr("$reporoot/$r->{'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 = ("$r->{'hdrmd5'}  $bpack") unless @m;
	      $r->{'meta'} = \@m;
	    }
	    push @new_meta, @{$r->{'meta'}};
          } else {
	    push @new_meta, "$r->{'hdrmd5'}  $bpack";
	  }
	}
	@new_meta = BSBuild::gen_meta($meta[0], $subpacks{$info->{'name'}}, @new_meta);
	if (join('\n', @meta) eq join('\n', @new_meta)) {
	  # print "      - $packid ($packtype)\n";
	  # print "        nothing changed (looked harder)\n";
	  $lastcheck{"$prp/$packid"} = $check;
	  $packstatus{$packid} = 'done';
	  $nharder++;
	  next;
	}
	my @diff = diffsortedmd5(0, \@meta, \@new_meta);
	print "      - $packid ($packtype)\n";
	print "        $_\n" for @diff;
	print "        meta change, start build\n";
      }
    } else {
      print "      - $packid ($packtype)\n";
      print "        start build\n";
    }
    my ($job, $joberror) = set_building($projid, $repoid, $packid, $pdata, $info, $bconf, $subpacks{$info->{'name'}}, $pdeps{$packid}, $prpsearchpath{$prp});
    if (!$job) {
      # could not start job...
      if ($joberror =~ /^expansion error: (.*)$/) {
	$packstatus{$packid} = 'expansion error';
	$packerror{$packid} = $1;
      } else {
	$packstatus{$packid} = 'broken';
	$packerror{$packid} = $joberror;
      }
      next;
    }
    $building{$packid} = $job;
    $notready{$pname} = 1;
    $packstatus{$packid} = 'scheduled';
  }

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

  # write blocked data into a file so that remote servers can
  # fetch it
  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");
  }

  # 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} || (!%notready && !$prpfinished{$prp})) {
    sendrepochangeevent($prp);
    $repounchanged{$prp} = 1;
  }

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

  # write package status for this project
  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);

  $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)."\n";
  print "    took $prpchecktime seconds to check the packages\n";
  if (!%notready) {
    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 = Digest::MD5::md5_hex($repodonestate);
    if (-e "$reporoot/$prp/$myarch/:repodone") {
      my $oldrepodone = readstr("$reporoot/$prp/$myarch/:repodone", 1) || '';
      unlink("$reporoot/$prp/$myarch/:repodone") if $oldrepodone  ne $repodonestate;
    }
    if (! -e "$reporoot/$prp/$myarch/:repodone") {
      if (!@packs || grep {$_} values %pubenabled) {
	mkdir_p("$reporoot/$prp/$myarch");
	prpfinished($prp, \@packs, \%pubenabled, $bconf);
	writestr("$reporoot/$prp/$myarch/:repodone", undef, $repodonestate);
      } else {
	print "    publishing is disabled\n";
      }
    }
    $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 $repodata{$prp};
        delete $lastcheck{"$prp/$_"} for @packs;
      }
    }
  } else {
    delete $prpfinished{$prp};
    unlink("$reporoot/$prp/$myarch/:repodone");
  }
  $nextmed{$prp} = time() + 2 * $prpchecktime;
}

