- backport watchremote fix
[opensuse:build-service.git] / src / backend / bs_sched
1 #!/usr/bin/perl -w
2 #
3 # Copyright (c) 2006, 2007 Michael Schroeder, Novell Inc.
4 # Copyright (c) 2008 Adrian Schroeter, Novell Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License version 2 as
8 # published by the Free Software Foundation.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program (see the file COPYING); if not, write to the
17 # Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 #
20 ################################################################
21 #
22 # The Scheduler. One big chunk of code for now.
23 #
24
25 BEGIN {
26   my ($wd) = $0 =~ m-(.*)/- ;
27   $wd ||= '.';
28   unshift @INC,  "$wd/build";
29   unshift @INC,  "$wd";
30 }
31
32 use Digest::MD5 ();
33 use Data::Dumper;
34 use Storable ();
35 use XML::Structured ':bytes';
36 use POSIX;
37 use Fcntl qw(:DEFAULT :flock);
38
39 use BSConfig;
40 use BSRPC ':https';
41 use BSUtil;
42 use BSFileDB;
43 use BSXML;
44 use BSDBIndex;
45 use BSBuild;
46 use BSVerify;
47 use Build;
48 use BSDB;
49 use Meta;
50
51 use strict;
52
53 my $user = $BSConfig::bsuser;
54 my $group = $BSConfig::bsgroup;
55
56 !defined($user) || defined($user = (getpwnam($user))[2]) || die("unknown user\n");
57 !defined($group) || defined($group = (getgrnam($group))[2]) || die("unknown group\n");
58 if (defined $group) {
59   ($(, $)) = ($group, $group);
60   die "setgid: $!\n" if ($) != $group);
61 }
62 if (defined $user) {
63   ($<, $>) = ($user, $user); 
64   die "setuid: $!\n" if ($> != $user); 
65 }
66
67
68
69 my $reporoot = "$BSConfig::bsdir/build";
70 my $jobsdir = "$BSConfig::bsdir/jobs";
71 my $eventdir = "$BSConfig::bsdir/events";
72 my $extrepodir = "$BSConfig::bsdir/repos";
73 my $extrepodir_sync = "$BSConfig::bsdir/repos_sync";
74 my $extrepodb = "$BSConfig::bsdir/db/published";
75 my $uploaddir = "$BSConfig::bsdir/upload";
76 my $rundir = $BSConfig::rundir || "$BSConfig::bsdir/run";
77 my $infodir = "$BSConfig::bsdir/info";
78
79 my $myarch = $ARGV[0] || 'i586';
80
81 my $myjobsdir = "$jobsdir/$myarch";
82 my $myeventdir = "$eventdir/$myarch";
83
84 my $historylay = [qw{versrel bcnt srcmd5 rev time}];
85
86 my %remoteprojs;        # remote project cache
87
88 # Create directory on first start
89 mkdir_p($infodir) || die ("Failed to create ".$infodir);
90
91 sub unify {
92   my %h = map {$_ => 1} @_;
93   return grep(delete($h{$_}), @_);
94 }
95
96 sub sendevent {
97   my ($ev, $arch, $evname) = @_;
98
99   mkdir_p("$eventdir/$arch");
100   writexml("$eventdir/$arch/.$evname$$", "$eventdir/$arch/$evname", $ev, $BSXML::event);
101   local *F;
102   if (sysopen(F, "$eventdir/$arch/.ping", POSIX::O_WRONLY|POSIX::O_NONBLOCK)) {
103     syswrite(F, 'x');
104     close(F);
105   }
106 }
107
108 #
109 # input: depsp  -> hash of arrays
110 #        mapp   -> hash of strings
111 #        basep  -> hash of bools
112 #        buildp -> hash of bools
113 #
114 # XXX: put this in BSSort.pm or somehow use sort
115 #      from Build.pm
116
117 sub sortpacks {
118   my ($depsp, $mapp, $basep, $buildp, $cychp, @packs) = @_;
119
120   return @packs if @packs < 2;
121
122   my %deps;
123   my %rdeps;
124   my %needed;
125
126   # map and unify dependencies, create rdeps and needed
127   my %known = map {$_ => 1} @packs;
128   die("sortpacks: input not unique\n") if @packs != keys(%known);
129   for my $p (@packs) {
130     if ($basep && $basep->{$p}) {
131       $deps{$p} = [];
132       $needed{$p} = 0;
133       next;
134     }
135     my @fdeps = @{$depsp->{$p} || []};
136     @fdeps = map {$mapp->{$_} || $_} @fdeps if $mapp;
137     @fdeps = grep {$known{$_}} @fdeps;
138     my %fdeps = ($p => 1);      # no self reference
139     @fdeps = grep {!$fdeps{$_}++} @fdeps;
140     $deps{$p} = \@fdeps;
141     $needed{$p} = @fdeps;
142     push @{$rdeps{$_}}, $p for @fdeps;
143   }
144   undef %known;         # free memory
145
146   @packs = sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @packs;
147   if ($buildp) {        # bring running to front
148     my @packsr = grep {$buildp->{$_}} @packs;
149     @packs = grep {!$buildp->{$_}} @packs;
150     unshift @packs, @packsr;
151   }
152   my @good;
153   my @res;
154   if ($basep) {
155     @good = grep {$basep->{$_}} @packs;
156     if (@good) {
157       @packs = grep {!$basep->{$_}} @packs;
158       push @res, @good;
159       for my $p (@good) {
160         $needed{$_}-- for @{$rdeps{$p} || []};
161       }
162     }
163   }
164   # the big sort loop
165   while (@packs) {
166     @good = grep {$needed{$_} == 0} @packs;
167     if (@good) {
168       @packs = grep {$needed{$_}} @packs;
169       push @res, @good;
170       for my $p (@good) {
171         $needed{$_}-- for @{$rdeps{$p}};
172       }
173       next;
174     }
175     die unless @packs > 1;
176     # uh oh, cycle alert. find and remove all cycles.
177     my %notdone = map {$_ => 1} @packs;
178     $notdone{$_} = 0 for @res;  # already did those
179     my @todo = @packs;
180     while (@todo) {
181       my $v = shift @todo;
182       if (ref($v)) {
183         $notdone{$$v} = 0;      # finished this one
184         next;   
185       }
186       my $s = $notdone{$v};
187       next unless $s;
188       my @e = grep {$notdone{$_}} @{$deps{$v}};
189       if (!@e) {
190         $notdone{$v} = 0;       # all deps done, mark as finished
191         next;
192       }
193       if ($s == 1) {
194         $notdone{$v} = 2;       # now under investigation
195         unshift @todo, @e, \$v;
196         next;
197       }
198       # reached visited package, found a cycle!
199       my @cyc = ();
200       my $cycv = $v;
201       # go back till $v is reached again
202       while(1) {
203         die unless @todo;
204         $v = shift @todo;
205         next unless ref($v);
206         $v = $$v;
207         $notdone{$v} = 1 if $notdone{$v} == 2;
208         unshift @cyc, $v;
209         last if $v eq $cycv;
210       }
211       unshift @todo, $cycv;
212       print "cycle: ".join(' -> ', @cyc)."\n";
213       if ($cychp) {
214         my %nc = map {$_ => 1} @cyc;
215         for my $p (@cyc) {
216           next unless $cychp->{$p};
217           $nc{$_} = 1 for @{$cychp->{$p}};
218         }
219         my $c = [ sort keys %nc ];
220         $cychp->{$_} = $c for @$c;
221       }
222       my $breakv;
223       if ($buildp) {
224         my @b = grep {$buildp->{$_}} @cyc;
225         $breakv = $b[0] if @b;
226       }
227       if (!defined($breakv)) {
228         my @b = @cyc;
229         @b = sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @b;
230         $breakv = $b[0];
231       }
232       push @cyc, $cyc[0];
233       shift @cyc while $cyc[0] ne $breakv;
234       $v = $cyc[1];
235       print "  breaking with $breakv -> $v\n";
236       $deps{$breakv} = [ grep {$_ ne $v} @{$deps{$breakv}} ];
237       $rdeps{$v} = [ grep {$_ ne $breakv} @{$rdeps{$v}} ];
238       $needed{$breakv}--;
239     }
240   }
241   return @res;
242 }
243
244 sub sortedmd5toreason {
245   my @res;
246   for my $line (@_) {
247     my $tag = substr($line, 0, 1); # just the first char
248     $tag = 'md5sum' if $tag eq '!';
249     $tag = 'added' if $tag eq '+';
250     $tag = 'removed' if $tag eq '-';
251     push @res, { 'change' => $tag, 'key' => substr($line, 1) };
252   }
253   return \@res;
254 }
255
256 sub diffsortedmd5 {
257   my $md5off = shift;
258   my $fromp = shift;
259   my $top = shift;
260
261   my @ret = ();
262   my @from = map {[$_, substr($_, 0, $md5off).substr($_, $md5off+($md5off ? 33 : 34))]} @$fromp;
263   my @to   = map {[$_, substr($_, 0, $md5off).substr($_, $md5off+($md5off ? 33 : 34))]} @$top;
264   @from = sort {$a->[1] cmp $b->[1] || $a->[0] cmp $b->[0]} @from;
265   @to   = sort {$a->[1] cmp $b->[1] || $a->[0] cmp $b->[0]} @to;
266
267   for my $f (@from) {
268     if (@to && $f->[1] eq $to[0]->[1]) {
269       push @ret, "!$f->[1]" if $f->[0] ne $to[0]->[0];
270       shift @to;
271       next;   
272     }
273     if (!@to || $f->[1] lt $to[0]->[1]) {
274       push @ret, "-$f->[1]";
275       next;   
276     }
277     while (@to && $f->[1] gt $to[0]->[1]) {
278       push @ret, "+$to[0]->[1]";
279       shift @to;
280     }
281     redo;   
282   }
283   push @ret, "+$_->[1]" for @to;
284   return @ret;
285 }
286
287 sub findbins_dir {
288   my ($dir) = @_;
289   my @bins;
290   if (ref($dir)) {
291     @bins = grep {/\.(?:rpm|deb|iso)$/} @$dir;
292   } else {
293     @bins = ls($dir);
294     @bins = map {"$dir/$_"} grep {/\.(?:rpm|deb|iso|raw|raw\.install)$/} sort @bins;
295   }
296   my $repobins = {};
297   for my $bin (@bins) {
298     my @s = stat($bin);
299     next unless @s;
300     my $id = "$s[9]/$s[7]/$s[1]";
301     my $data = Build::query($bin, 'evra' => 1); # need arch
302     next unless $data;
303     BSVerify::verify_nevraquery($data);
304     $data->{'id'} = $id;
305     delete $data->{'epoch'};
306     delete $data->{'version'};
307     delete $data->{'release'};
308     $repobins->{$bin} = $data;
309   }
310   return $repobins;
311 }
312
313 my $projpacks;          # global project/package data
314
315 sub findbins {
316   my ($prp) = @_;
317   local *D;
318   my $dir = "$reporoot/$prp/$myarch/:full";
319   my $repobins = {};
320   my $cnt = 0;
321
322   my $cache;
323   if (-e "$dir.cache") {
324     eval { $cache = Storable::retrieve("$dir.cache"); };
325     warn($@) if $@;
326     undef $cache unless ref($cache) eq 'HASH';
327     if ($cache) {
328       delete $cache->{'/url'};
329       if ($cache->{'/external'}) {
330         delete $cache->{'/external'};
331         for (values %$cache) {
332           $_->{'path'} = "$prp/$myarch/:full/$_->{'path'}";
333         }
334         return $cache;
335       }
336       my $byid = {};
337       for (keys %$cache) {
338         my $v = $cache->{$_};
339         $v->{'name'} = $_ unless exists $v->{'name'};
340         $byid->{$v->{'id'}} = $v;
341       }
342       $cache = $byid;
343     }
344   }
345   if (!opendir(D, $dir)) {
346     return findbins_remote($prp);
347   }
348   my @prp = split("/", $prp);
349   my $projid = $prp[0];
350   my @dl = grep { $_->{'arch'} eq $myarch } @{$projpacks->{$projid}->{'download'}} if exists $projpacks->{$projid}->{'download'};
351   if (@dl && $BSConfig::enable_dod) {
352     # XXX: should be allow more than one metafile for a specific arch (e.g. for addons etc.)?
353     eval {
354       $repobins = Meta::parse("$dir/$dl[0]->{'metafile'}", "$dl[0]->{'mtype'}", { 'arch' => [ $myarch ] });
355     };
356     warn("cannot read metadata ($dir/$dl[0]->{'metafile'}): $@") if $@;
357     # fix up path entries
358     for (values %$repobins) {
359       $_->{'rpath'} = $_->{'path'};
360       $_->{'path'} =~ s/.*\///;
361     }
362     $repobins->{'/url'} = $dl[0]->{'baseurl'};
363     $cnt = keys %$repobins;
364   }
365
366   my @bins = grep {/\.(?:rpm|deb)$/} readdir(D);
367   closedir D;
368   if (!@bins && -s "$dir.subdirs") {
369     for my $subdir (split(' ', readstr("$dir.subdirs"))) {
370       push @bins, map {"$subdir/$_"} grep {/\.(?:rpm|deb)$/} ls("$dir/$subdir");
371     }
372   }
373   my ($hits, $misses) = (0, 0);
374   for my $bin (sort @bins) {
375     my @s = stat("$dir/$bin");
376     next unless @s;
377     my $id = "$s[9]/$s[7]/$s[1]";
378     my $data;
379     $data = $cache->{$id} if $cache;
380     if ($data) {
381       $hits++;
382     } else {
383       $misses++;
384       $data = Build::query("$dir/$bin");
385       next unless $data;
386       $data->{'id'} = $id;
387     }
388     $data->{'path'} = $bin;     # no dir for now!
389     $repobins->{$data->{'name'}} = $data;
390     $cnt++;
391   }
392   if (!$cnt) {
393     print "    packages found: none\n";
394   } else {
395     print "    packages found: $cnt (hits: $hits, misses: $misses)\n";
396   }
397   if (Storable::nstore($repobins, "$dir.cache.new")) {
398     rename("$dir.cache.new", "$dir.cache") || die("rename $dir.cache.new $dir.cache: $!\n");
399   }
400   delete $repobins->{'/url'};
401   # add dir to make real path
402   for (values %$repobins) {
403     $_->{'path'} = "$prp/$myarch/:full/$_->{'path'}";
404   }
405   return $repobins;
406 }
407
408 sub enabled {
409   my ($repoid, $disen, $default) = @_;
410   return BSUtil::enabled($repoid, $disen, $default, $myarch);
411 }
412
413
414
415 # this is basically getconfig from the source server
416 # we do not need any macros, just the config
417 sub getconfig {
418   my ($arch, $path) = @_;
419   my $config = '';
420   for my $prp (reverse @$path) {
421     my ($p, $r) = split('/', $prp, 2);
422     my $c;
423     if ($remoteprojs{$p}) {
424       $c = fetchremoteconfig($p); 
425       return undef unless defined $c;
426     } elsif ($projpacks->{$p}) {
427       $c = $projpacks->{$p}->{'config'};
428     }
429     next unless defined $c;
430     $config .= "\n### from $p\n";
431     $config .= "%define _repository $r\n";
432     $c = defined($1) ? $1 : '' if $c =~ /^(.*\n)?\s*macros:[^\n]*\n/si;
433     $config .= $c;
434   }
435   # it's an error if we have no config at all
436   return undef unless $config ne '';
437   # now we got the combined config, parse it
438   my @c = split("\n", $config);
439   my $c = Build::read_config($arch, \@c);
440   $c->{'repotype'} = [ 'rpm-md' ] unless @{$c->{'repotype'}};
441   return $c;
442 }
443
444
445 #######################################################################
446 #######################################################################
447 ##
448 ## Job management functions
449 ##
450
451 #
452 # killjob - kill a single build job
453 #
454 # input: $job - job identificator
455 #
456 sub killjob {
457   my ($job) = @_;
458
459   local *F;
460   if (! -e "$myjobsdir/$job:status") {
461     # create locked status
462     my $js = {'code' => 'deleting'};
463     if (BSUtil::lockcreatexml(\*F, "$myjobsdir/.sched.$$", "$myjobsdir/$job:status", $js, $BSXML::jobstatus)) {
464       print "        (job was not building)\n";
465       unlink("$myjobsdir/$job");
466       unlink("$myjobsdir/$job:status");
467       close F;
468       return;
469     }
470     # lock failed, dispatcher was faster!
471     die("$myjobsdir/$job:status: $!") unless -e "$myjobsdir/$job:status";
472   }
473   my $js = BSUtil::lockopenxml(\*F, '<', "$myjobsdir/$job:status", $BSXML::jobstatus, 1);
474   if (!$js) {
475     # can't happen actually
476     print "        (job was not building)\n";
477     unlink("$myjobsdir/$job");
478     return;
479   }
480   if ($js->{'code'} eq 'building') {
481     print "        (job was building on $js->{'workerid'})\n";
482     my $req = {
483       'uri' => "$js->{'uri'}/discard",
484       'timeout' => 60,
485     };
486     eval {
487       BSRPC::rpc($req, undef, "jobid=$js->{'jobid'}");
488     };
489     warn("kill $job: $@") if $@;
490   }
491   if (-d "$myjobsdir/$job:dir") {
492     unlink("$myjobsdir/$job:dir/$_") for ls("$myjobsdir/$job:dir");
493     rmdir("$myjobsdir/$job:dir");
494   }
495   unlink("$myjobsdir/$job");
496   unlink("$myjobsdir/$job:status");
497   close(F);
498 }
499
500 #
501 # jobname - create first part job job identifcation
502 #
503 # input:  $prp    - prp the job belongs to
504 #         $packid - package we are building
505 # output: first part of job identification
506 #
507 # append srcmd5 for full identification
508 #
509 sub jobname {
510   my ($prp, $packid) = @_;
511   my $job = "$prp/$packid";
512   $job =~ s/\//::/g;
513   return $job;
514 }
515
516 #
517 # killbuilding - kill build jobs 
518 #
519 # - used if a project/package got deleted to kill all running
520 #   jobs
521
522 # input: $prp    - prp we are working on
523 #        $packid - just kill the builds of the package
524 #           
525 sub killbuilding {
526   my ($prp, $packid) = @_;
527   my @jobs;
528   if (defined $packid) {
529     my $f = jobname($prp, $packid);
530     @jobs = grep {$_ eq $f || /^\Q$f\E-[0-9a-f]{32}$/} ls($myjobsdir);
531   } else {
532     my $f = jobname($prp, '');
533     @jobs = grep {/^\Q$f\E/} ls($myjobsdir);
534     @jobs = grep {!/(?::dir|:status)$/} @jobs;
535   }
536   for my $job (@jobs) {
537     print "        killing obsolete job $job\n";
538     killjob($job);
539   }
540 }
541
542 #
543 # set_building  - create a new build job
544 #
545 # input:  $projid        - project this package belongs to
546 #         $repoid        - repository we are building for
547 #         $packid        - package to be built
548 #         $pdata         - package data
549 #         $info          - file and dependency information
550 #         $bconf         - project configuration
551 #         $subpacks      - all subpackages of this package we know of
552 #         $edeps         - expanded build dependencies
553 #         $prpsearchpath - build repository search path
554 #         $reason        - what triggered the build
555 #         $relsyncmax    - bcnt sync data
556 #
557 # output: $job           - the job identifier
558 #         $error         - in case we could not start the job
559 #
560 # check if this job is already building, if yes, do nothing.
561 # otherwise calculate and expand build dependencies, kill all
562 # other jobs of the same prp/package, write status and job info.
563 # not that hard, was it?
564 #
565 sub set_building {
566   my ($projid, $repoid, $packid, $pdata, $info, $bconf, $subpacks, $edeps, $prpsearchpath, $reason, $relsyncmax) = @_;
567
568   my $prp = "$projid/$repoid";
569   my $srcmd5 = $pdata->{'srcmd5'};
570   my $f = jobname($prp, $packid);
571   return "$f-$srcmd5" if -s "$myjobsdir/$f-$srcmd5";
572   return $f if -s "$myjobsdir/$f";
573   my @otherjobs = grep {/^\Q$f\E-[0-9a-f]{32}$/} ls($myjobsdir);
574   $f = "$f-$srcmd5";
575
576   # a new one. expand usedforbuild. write info file.
577   my $searchpath = [];
578   for (@$prpsearchpath) {
579     my @pr = split('/', $_, 2);
580     if ($remoteprojs{$pr[0]}) {
581       push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
582     } else {
583       push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
584     }
585   }
586
587   # Get image types defined in KIWI config file
588   my @imagetypes = @{$info->{'imagetype'} || []};
589
590   # calculate packages needed for building
591   my $prptype = $bconf->{'type'};
592   $info->{'file'} =~ /\.(spec|dsc|kiwi)$/;
593   my $packtype = $1 || 'spec';
594
595   my @bdeps = ( @{$info->{'dep'} || []}, @{$info->{'prereq'} || []} );
596
597   if ($packtype eq 'kiwi') {
598     # packages used for build environment
599     @bdeps = ('kiwi', 'createrepo', 'tar');
600     push @bdeps, grep {/^kiwi-/} @{$info->{'dep'} || []};
601   }
602
603   my $eok;
604   ($eok, @bdeps) = Build::get_build($bconf, $subpacks, @bdeps);
605   if (!$eok) {
606     print "        expansion errors:\n";
607     print "          $_\n" for @bdeps;
608     return (undef, "expansion error: ".join(', ', @bdeps));
609   }
610
611   # find the last build count we used for this version/release
612   mkdir_p("$reporoot/$prp/$myarch/$packid");
613   my $h;
614   if (-e "$reporoot/$prp/$myarch/$packid/history") {
615     $h = BSFileDB::fdb_getmatch("$reporoot/$prp/$myarch/$packid/history", $historylay, 'versrel', $pdata->{'versrel'}, 1);
616   }
617   $h = {'bcnt' => 0} unless $h;
618
619   # max with sync data
620   my $tag = $pdata->{'bcntsynctag'} || $packid;
621   if ($relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
622     if ($h->{'bcnt'} + 1 < $relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
623       $h->{'bcnt'} = $relsyncmax->{"$tag/$pdata->{'versrel'}"} - 1;
624     }
625   }
626
627   # kill those ancient other jobs
628   for my $otherjob (@otherjobs) {
629     print "        killing old job $otherjob\n";
630     killjob($otherjob);
631   }
632
633   # jay! ready for building, write status and job info
634   writexml("$reporoot/$prp/$myarch/$packid/.status", "$reporoot/$prp/$myarch/$packid/status", { 'status' => 'scheduled', 'readytime' => time(), 'job' => $f}, $BSXML::buildstatus);
635   # And store reason and time
636   $reason->{'time'} = time();
637   writexml("$reporoot/$prp/$myarch/$packid/.reason", "$reporoot/$prp/$myarch/$packid/reason", $reason, $BSXML::buildreason);
638
639   my @pdeps = Build::get_preinstalls($bconf);
640   my @vmdeps = Build::get_vminstalls($bconf);
641   my %runscripts = map {$_ => 1} Build::get_runscripts($bconf);
642   my %bdeps = map {$_ => 1} @bdeps;
643   my %pdeps = map {$_ => 1} @pdeps;
644   my %vmdeps = map {$_ => 1} @vmdeps;
645   my %edeps = map {$_ => 1} @$edeps;
646   @bdeps = unify(@pdeps, @vmdeps, @$edeps, @bdeps);
647   for (@bdeps) {
648     $_ = {'name' => $_};
649     $_->{'preinstall'} = 1 if $pdeps{$_->{'name'}};
650     $_->{'vminstall'} = 1 if $vmdeps{$_->{'name'}};
651     $_->{'runscripts'} = 1 if $runscripts{$_->{'name'}};
652     $_->{'notmeta'} = 1 unless $edeps{$_->{'name'}};
653     $_->{'noinstall'} = 1 if $packtype eq 'kiwi' && $edeps{$_->{'name'}} && !($bdeps{$_->{'name'}} || $vmdeps{$_->{'name'}} || $pdeps{$_->{'name'}});
654   }
655   if ($info->{'extrasource'}) {
656     push @bdeps, map {{
657       'name' => $_->{'file'}, 'version' => '', 'repoarch' => 'src',
658       'project' => $_->{'project'}, 'package' => $_->{'package'}, 'srcmd5' => $_->{'srcmd5'},
659     }} @{$info->{'extrasource'}};
660   }
661
662   my $vmd5 = $pdata->{'verifymd5'} || $pdata->{'srcmd5'};
663   my $binfo = {
664     'project' => $projid,
665     'repository' => $repoid,
666     'package' => $packid,
667     'reposerver' => $BSConfig::reposerver,
668     'job' => $f,
669     'arch' => $myarch,
670     'reason' => $reason->{'explain'},
671     'srcmd5' => $pdata->{'srcmd5'},
672     'verifymd5' => $vmd5,
673     'rev' => $pdata->{'rev'},
674     'file' => $info->{'file'},
675     'versrel' => $pdata->{'versrel'},
676     'bcnt' => $h->{'bcnt'} + 1,
677     'subpack' => ($subpacks || []),
678     'imagetype' => \@imagetypes,
679     'bdep' => \@bdeps,
680     'path' => $searchpath,
681   };
682   my $release = $pdata->{'versrel'};
683   $release = '0' unless defined $release;
684   $release =~ s/.*-//;
685   my $bcnt = $h->{'bcnt'} + 1;
686   if (defined($bconf->{'release'})) {
687     $binfo->{'release'} = $bconf->{'release'};
688     $binfo->{'release'} =~ s/\<CI_CNT\>/$release/g;
689     $binfo->{'release'} =~ s/\<B_CNT\>/$bcnt/g;
690   }
691   my $debuginfo = $bconf->{'debuginfo'};
692   $debuginfo = enabled($repoid, $projpacks->{$projid}->{'debuginfo'}, $debuginfo);
693   $debuginfo = enabled($repoid, $pdata->{'debuginfo'}, $debuginfo);
694   $binfo->{'debuginfo'} = 1 if $debuginfo;
695
696   writexml("$myjobsdir/$f:new", "$myjobsdir/$f", $binfo, $BSXML::buildinfo);
697   # all done. the dispatcher will now pick up the job and send it
698   # to a worker.
699   return $f;
700 }
701
702
703 #######################################################################
704 #######################################################################
705 ##
706 ## Repository management functions
707 ##
708
709 #
710 # sendpublishevent - send a publish event to the publisher
711 #
712 # input: $prp - prp to be published
713 #
714 sub sendpublishevent {
715   my ($prp) = @_;
716
717   my ($projid, $repoid) = split('/', $prp, 2);
718   my $ev = {
719     'type' => 'publish',
720     'project' => $projid,
721     'repository' => $repoid,
722   };
723   sendevent($ev, 'publish', "${projid}::$repoid");
724 }
725
726 sub sendrepochangeevent {
727   my ($prp) = @_;
728
729   my ($projid, $repoid) = split('/', $prp, 2);
730   my $ev = {
731     'type' => 'repository',
732     'project' => $projid,
733     'repository' => $repoid,
734     'arch' => $myarch,
735   };
736   sendevent($ev, 'repository', "${projid}::${repoid}::${myarch}");
737 }
738
739 #
740 # prpfinished  - publish a prp
741 #
742 # updates :repo and sends an event to the publisher
743 #
744 # input:  $prp        - the finished prp
745 #         $packs      - packages in project
746 #
747 # prpfinished  - publish a prp
748 #
749 # updates :repo and sends an event to the publisher
750 #
751 # input:  $prp        - the finished prp
752 #         $packs      - packages in project
753 #                       undef -> arch no longer builds this repository
754 #         $pubenabled - only publish those packages
755 #                       undef -> publish all packages
756 #         $bconf      - the config for this prp
757 #
758
759 #my $default_publishfilter = [
760 #  '-debuginfo-.*\.rpm$',
761 #  '-debugsource-.*\.rpm$',
762 #];
763
764 my $default_publishfilter;
765
766 sub prpfinished {
767   my ($prp, $packs, $pubenabled, $bconf) = @_;
768
769   print "    prp $prp is finished...\n";
770
771   local *F;
772   open(F, '>', "$reporoot/$prp/.finishedlock") || die("$reporoot/$prp/.finishedlock: $!\n");
773   if (!flock(F, LOCK_EX | LOCK_NB)) {
774     print "    waiting for lock...\n";
775     flock(F, LOCK_EX) || die("flock: $!\n");
776     print "    got the lock...\n";
777   }
778   if (!$packs) {
779     # delete all in :repo
780     my $r = "$reporoot/$prp/$myarch/:repo";
781     unlink("${r}info");
782     if (-d $r) {
783       BSUtil::cleandir($r);
784       rmdir($r) || die("rmdir $r: $!\n");
785     } else {
786       print "    nothing to delete...\n";
787       close(F);
788       return;
789     }
790     # release lock
791     close(F);
792     sendpublishevent($prp);
793     return;
794   }
795
796   my $rdir = "$reporoot/$prp/$myarch/:repo";
797
798   my $rinfo = {};
799   if (@$packs && $pubenabled && grep {!$_} values(%$pubenabled)) {
800     $rinfo = Storable::retrieve("${rdir}info") if -s "${rdir}info";
801   }
802   $rinfo->{'binaryorigins'} ||= {};
803
804   # link all packages into :repo
805   my %origin;
806   my $changed;
807   my $filter;
808   $filter = $bconf->{'publishfilter'} if $bconf;
809   undef $filter if $filter && !@$filter;
810   $filter ||= $default_publishfilter;
811
812   for my $packid (@$packs) {
813     if ($pubenabled && !$pubenabled->{$packid}) {
814       # publishing of this package is disabled
815       print "        $packid: publishing disabled\n";
816       my @all = grep {$rinfo->{'binaryorigins'}->{$_} eq $packid} keys %{$rinfo->{'binaryorigins'}};
817       for my $bin (@all) {
818         next if exists $origin{$bin};   # first one wins
819         $origin{$bin} = $packid;
820       }
821       next;
822     }
823     my $pdir = "$reporoot/$prp/$myarch/$packid";
824     my @all = sort(ls($pdir));
825     @all = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne '.bininfo' && $_ ne 'reason'} @all;
826     for my $bin (@all) {
827       next if exists $origin{$bin};     # first one wins
828       $origin{$bin} = $packid;
829       if ($filter) {
830         my $bad;
831         for (@$filter) {
832           next unless $bin =~ /$_/;
833           $bad = 1;
834           last;
835         }
836         next if $bad;
837       }
838       my @sr = lstat("$rdir/$bin");
839       if (@sr) {
840         my $risdir = -d _ ? 1 : 0;
841         my @s = lstat("$pdir/$bin");
842         my $pisdir = -d _ ? 1 : 0;
843         next unless @s;
844         next if "$s[9]/$s[7]/$s[1]" eq "$sr[9]/$sr[7]/$sr[1]";
845         if ($risdir && $pisdir) {
846           my $rinfo = BSUtil::treeinfo("$rdir/$bin");
847           my $pinfo = BSUtil::treeinfo("$pdir/$bin");
848           next if join(',', @$rinfo) eq join(',', @$pinfo);
849         }
850         print "      ! :repo/$bin ($packid)\n";
851         if ($risdir) {
852           BSUtil::cleandir("$rdir/$bin");
853           rmdir("$rdir/$bin");
854         } else {
855           unlink("$rdir/$bin");
856         }
857       } else {
858         print "      + :repo/$bin ($packid)\n";
859         mkdir_p($rdir) unless -d $rdir;
860       }
861       if (! -l "$pdir/$bin" && -d _) {
862         BSUtil::linktree("$pdir/$bin", "$rdir/$bin");
863       } else {
864         link("$pdir/$bin", "$rdir/$bin") || die("link $pdir/$bin $rdir/$bin: $!\n");
865       }
866       $changed = 1;
867     }
868   }
869   for my $bin (sort(ls($rdir))) {
870     next if exists $origin{$bin};
871     print "      - :repo/$bin\n";
872     if (! -l "$rdir/$bin" && -d _) {
873       BSUtil::cleandir("$rdir/$bin");
874       rmdir("$rdir/$bin") || die("rmdir $rdir/$bin: $!\n");
875     } else {
876       unlink("$rdir/$bin") || die("unlink $rdir/$bin: $!\n");
877     }
878     $changed = 1;
879   }
880
881   # write new rpminfo
882   $rinfo = {'binaryorigins' => \%origin};
883   Storable::nstore($rinfo, "${rdir}info");
884
885   # release lock and ping publisher
886   close(F);
887   sendpublishevent($prp);
888 }
889
890 my $exportcnt = 0;
891
892 sub createexportjob {
893   my ($prp, $arch, $jobrepo, $dst, $oldrepo, $meta, @exports) = @_;
894
895   # create unique id
896   my $job = "import-".Digest::MD5::md5_hex("$exportcnt.$$.$myarch.".time());
897   $exportcnt++;
898
899   local *F;
900   my $jobstatus = {
901     'code' => 'finished',
902   };
903   if (!BSUtil::lockcreatexml(\*F, "$jobsdir/$arch/.$job", "$jobsdir/$arch/$job:status", $jobstatus, $BSXML::jobstatus)) {
904     print "job lock failed!\n";
905     return;
906   }
907
908   my ($projid, $repoid) = split('/', $prp, 2);
909   my $info = {
910     'project' => $projid,
911     'repository' => $repoid,
912     'package' => ':import',
913     'arch' => $arch,
914     'job' => $job,
915   };
916   mkdir_p("$jobsdir/$arch") unless -d "$jobsdir/$arch";
917   writexml("$jobsdir/$arch/.$job", "$jobsdir/$arch/$job", $info, $BSXML::buildinfo);
918   my $dir = "$jobsdir/$arch/$job:dir";
919   mkdir_p($dir);
920   if ($meta) {
921     link($meta, "$meta.dup");
922     rename("$meta.dup", "$dir/meta");
923     unlink("$meta.dup");
924   }
925   my %seen;
926   while (@exports) {
927     my ($rp, $r) = splice(@exports, 0, 2);
928     next unless $r->{'source'};
929     link("$dst/$rp", "$dir/$rp") || warn("link $dst/$rp $dir/$rp: $!\n");
930     $seen{$r->{'id'}} = 1;
931   }
932   my @replaced;
933   for my $rp (sort keys %$oldrepo) {
934     my $r = $oldrepo->{$rp};
935     next unless $r->{'source'}; # no src rpms in full tree
936     next if $seen{$r->{'id'}};
937     my $suf = $rp;
938     $suf =~ s/.*\.//;
939     push @replaced, {'name' => "$r->{'name'}.$suf", 'id' => $r->{'id'}};
940   }
941   if (@replaced) {
942     writexml("$dir/replaced.xml", undef, {'name' => 'replaced', 'entry' => \@replaced}, $BSXML::dir);
943   }
944   close F;
945   my $ev = {
946     'type' => 'import',
947     'job' => $job,
948   };
949   sendevent($ev, $arch, "import.$job");
950 }
951
952
953 my %default_exportfilters = (
954   'i586' => {
955     '\.x86_64\.rpm$'   => [ 'x86_64' ],
956     '\.ia64\.rpm$'     => [ 'ia64' ],
957     '-debuginfo-.*\.rpm$' => [],
958     '-debugsource-.*\.rpm$' => [],
959   },
960   'x86_64' => {
961     '-debuginfo-.*\.rpm$' => [],
962     '-debugsource-.*\.rpm$' => [],
963   },
964   'ppc' => {
965     '\.ppc64\.rpm$'   => [ 'ppc64' ],
966     '-debuginfo-.*\.rpm$' => [],
967     '-debugsource-.*\.rpm$' => [],
968   },
969   'ppc64' => {
970     '\.ppc\.rpm$'   => [ 'ppc' ],
971     '-debuginfo-.*\.rpm$' => [],
972     '-debugsource-.*\.rpm$' => [],
973   },
974 );
975
976 #
977 # moves binary packages from jobrepo to dst and updates full repository
978 #
979
980 sub update_dst_full {
981   my ($repodata, $prp, $dst, $jobdir, $meta, $useforbuildenabled, $prpsearchpath) = @_;
982
983   my $jobrepo;
984   my @jobfiles;
985   if (defined($jobdir)) {
986     @jobfiles = sort(ls($jobdir));
987     @jobfiles = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne 'reason'} @jobfiles;
988     $jobrepo = findbins_dir([ map {"$jobdir/$_"} grep {/\.(?:rpm|deb)$/} @jobfiles ]);
989   } else {
990     $jobrepo = {};
991   }
992
993   ##################################################################
994   # part 1: move files into package directory ($dst)
995
996   my $gdst = "$reporoot/$prp/$myarch";
997
998   my $oldrepo;
999   my $isimport;
1000
1001   if ($dst && $dst eq $jobdir) {
1002     # a "refresh" operation, nothing to do here
1003     $oldrepo = $jobrepo;
1004   } elsif ($dst) {
1005     # get old state
1006     my @oldfiles = sort(ls($dst));
1007     @oldfiles = grep {$_ ne 'history' && $_ ne 'logfile' && $_ ne 'meta' && $_ ne 'status' && $_ ne 'reason'} @oldfiles;
1008     $oldrepo = findbins_dir([ map {"$dst/$_"} grep {/\.(?:rpm|deb)$/} @oldfiles ]);
1009
1010     # move files over
1011     my %new;
1012     for my $f (@jobfiles) {
1013       if (! -l "$dst/$f" && -d _) {
1014         BSUtil::cleandir("$dst/$f");
1015         rmdir("$dst/$f");
1016       }
1017       rename("$jobdir/$f", "$dst/$f") || die("rename $jobdir/$f $dst/$f: $!\n");
1018       $new{$f} = 1;
1019     }
1020     for my $f (grep {!$new{$_}} @oldfiles) {
1021       if (! -l "$dst/$f" && -d _) {
1022         BSUtil::cleandir("$dst/$f");
1023         rmdir("$dst/$f");
1024       } else {
1025         unlink("$dst/$f") ;
1026       }
1027     }
1028   } else {
1029     # dst = undef is true for importevents
1030     $isimport = 1;
1031     my $replaced = (readxml("$jobdir/replaced.xml", $BSXML::dir, 1) || {})->{'entry'};
1032     $oldrepo = {};
1033     for (@{$replaced || []}) {
1034       my $rp = $_->{'name'};
1035       $_->{'name'} =~ s/\.[^\.]*$//;
1036       $_->{'source'} = 1;
1037       $oldrepo->{$rp} = $_;
1038     }
1039     $dst = $jobdir;     # get em from the jobdir
1040   }
1041
1042   if (!$isimport) {
1043     # write .bininfo file
1044     my $bininfo = '';
1045     for my $rp (sort keys %$jobrepo) {
1046       my $nn = $rp;
1047       $nn =~ s/.*\///;
1048       $bininfo .= "$jobrepo->{$rp}->{'hdrmd5'}  $nn\n";
1049     }
1050     writestr("$dst/.bininfo.new", "$dst/.bininfo", $bininfo);
1051   }
1052
1053   ##################################################################
1054   # part 2: link needed binaries into :full tree
1055
1056   my $filter;
1057   # argh, this slows us down a bit
1058   my $bconf;
1059   $bconf = getconfig($myarch, $prpsearchpath) if $prpsearchpath;
1060   $filter = $bconf->{'exportfilter'} if $bconf;
1061   undef $filter if $filter && !%$filter;
1062   $filter ||= $default_exportfilters{$myarch};
1063
1064   # link new ones into full, delete old ones no longer in use
1065   my %exports;
1066
1067   my %new;
1068   for my $rp (sort keys %$jobrepo) {
1069     my $nn = $rp;
1070     $nn =~ s/.*\///;
1071     $new{$nn} = $jobrepo->{$rp};
1072   }
1073
1074   # find destination for all new binaries
1075   my @movetofull;
1076   for my $rp (sort keys %new) {
1077     my $r = $new{$rp};
1078     next unless $r->{'source'}; # no src in full tree
1079
1080     if ($filter) {
1081       my $skip;
1082       for (reverse sort keys %$filter) {
1083         if ($rp =~ /$_/) {
1084           $skip = $filter->{$_};
1085           last;
1086         }
1087       }
1088       if ($skip) {
1089         my $myself;
1090         for my $exportarch (@$skip) {
1091           if ($exportarch eq '.' || $exportarch eq $myarch) {
1092             $myself = 1;
1093             next;
1094           }
1095           next if $isimport;    # no re-exports
1096           push @{$exports{$exportarch}}, $rp, $r;
1097         }
1098         next unless $myself;
1099       }
1100     }
1101     push @movetofull, $rp;
1102   }
1103   if ($filter && !$isimport) {
1104     # need also to check old entries
1105     for my $rp (sort keys %$oldrepo) {
1106       my $r = $oldrepo->{$rp};
1107       next unless $r->{'source'};       # no src rpms in full tree
1108       my $rn = $rp;
1109       $rn =~ s/.*\///;
1110       my $skip;
1111       for (sort keys %$filter) {
1112         if ($rn =~ /$_/) {
1113           $skip = $filter->{$_};
1114           last;
1115         }
1116       }
1117       if ($skip) {
1118         for my $exportarch (@$skip) {
1119           $exports{$exportarch} ||= [] if $exportarch ne '.' && $exportarch ne $myarch;
1120         }
1121       }
1122     }
1123   }
1124
1125   if ($filter && !$isimport) {
1126     # we always export, the other schedulers are free to reject the job
1127     # if move to full is also disabled for them
1128     for my $exportarch (sort keys %exports) {
1129       # check if this prp supports the arch
1130       my ($projid, $repoid) = split('/', $prp, 2);
1131       next unless $projpacks->{$projid};
1132       my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
1133       if ($repo && grep {$_ eq $exportarch} @{$repo->{'arch'} || []}) {
1134         print "    sending filtered packages to $exportarch\n";
1135         createexportjob($prp, $exportarch, $jobrepo, $dst, $oldrepo, $meta, @{$exports{$exportarch}});
1136       }
1137     }
1138   }
1139
1140   if (!$useforbuildenabled) {
1141     print "    move to :full is disabled\n";
1142     return;
1143   }
1144
1145   # move em over into :full
1146   mkdir_p("$gdst/:full") if @movetofull && ! -d "$gdst/:full";
1147   my %fnew;
1148   for my $rp (@movetofull) {
1149     my $r = $new{$rp};
1150     my $suf = $rp;
1151     $suf =~ s/.*\.//;
1152     my $n = $r->{'name'};
1153     print "      + :full/$n.$suf ($rp)\n";
1154     # link gives an error if the dest exists, so we dup
1155     # and rename instead.
1156     # when the dest is the same file, rename doesn't do
1157     # anything, so we need the unlink after the rename
1158     unlink("$dst/$rp.dup");
1159     link("$dst/$rp", "$dst/$rp.dup");
1160     rename("$dst/$rp.dup", "$gdst/:full/$n.$suf") || die("rename $dst/$rp.dup $gdst/:full/$n.$suf: $!\n");
1161     unlink("$dst/$rp.dup");
1162     if ($suf eq 'rpm') {
1163       unlink("$gdst/:full/$n.deb");
1164     } else {
1165       unlink("$gdst/:full/$n.rpm");
1166     }
1167     if ($meta) {
1168       link($meta, "$meta.dup");
1169       rename("$meta.dup", "$gdst/:full/$n.meta");
1170       unlink("$meta.dup");
1171     } else {
1172       unlink("$gdst/:full/$n.meta");
1173     }
1174
1175     $fnew{$n} = 1;
1176     delete $r->{'arch'};        # not in repodata
1177     $r->{'path'} = "$prp/$myarch/:full/$n.$suf";
1178     $repodata->{$n} = $r;
1179   }
1180
1181   # delete obsolete full entries
1182   for my $rp (sort keys %$oldrepo) {
1183     my $r = $oldrepo->{$rp};
1184     next unless $r->{'source'}; # no src rpms in full tree
1185     my $suf = $rp;
1186     $suf =~ s/.*\.//;
1187     my $n = $r->{'name'};
1188     next if $fnew{$n};          # got new version, already deleted old
1189
1190     my @s = stat("$gdst/:full/$n" . ($rp =~ /\.rpm$/ ? '.rpm' : '.deb'));
1191
1192     # don't delete package if not ours
1193     next unless @s && $r->{'id'} eq "$s[9]/$s[7]/$s[1]";
1194     # package no longer built, kill full entry
1195     print "      - :full/$n.$suf\n";
1196     unlink("$gdst/:full/$n.rpm");
1197     unlink("$gdst/:full/$n.deb");
1198     unlink("$gdst/:full/$n.iso");
1199     unlink("$gdst/:full/$n.meta");
1200     unlink("$gdst/:full/$n-MD5SUMS.meta");
1201     delete $repodata->{$n};
1202   }
1203
1204   # update :full cache file
1205   for my $pack (values %$repodata) {
1206     delete $pack->{'meta'};
1207     $pack->{'path'} =~ s/.*\///;
1208   }
1209   mkdir_p($gdst) if ! -d "$gdst/:full";
1210   if (Storable::nstore($repodata, "$gdst/:full.cache.new")) {
1211     rename("$gdst/:full.cache.new", "$gdst/:full.cache") || die("rename $gdst/:full.cache.new $gdst/:full.cache: $!\n");
1212   }
1213   for my $pack (values %$repodata) {
1214     $pack->{'path'} = "$prp/$myarch/:full/$pack->{'path'}";
1215   }
1216 }
1217
1218 sub addjobhist {
1219   my ($prp, $info, $status, $js) = @_;
1220   my $jobhist = {};
1221   $jobhist->{$_} = $status->{$_} for qw{readytime};
1222   $jobhist->{'code'} = $status->{'status'};
1223   $jobhist->{$_} = $js->{$_} for qw{starttime endtime uri workerid hostarch};
1224   $jobhist->{$_} = $info->{$_} for qw{package rev srcmd5 versrel bcnt reason};
1225   mkdir_p("$reporoot/$prp/$myarch");
1226   BSFileDB::fdb_add("$reporoot/$prp/$myarch/:jobhistory", $BSXML::jobhistlay, $jobhist);
1227 }
1228
1229
1230 ####################################################################
1231 ####################################################################
1232 ##
1233 ##  project/package data collection functions
1234 ##
1235
1236 my @prps;               # all prps(project-repositories-sorted) we have to schedule, sorted
1237 my %prpsearchpath;      # maps prp -> [ prp, prp, ...]
1238                         # build packages with the packages of the prps
1239 my %prpdeps;            # searchpath plus aggregate deps plus kiwi deps
1240                         # maps prp -> [ prp, prp ... ]
1241                         # used for sorting
1242 my %prpnoleaf;          # is this prp referenced by another prp?
1243 my @projpacks_linked;   # data of all linked sources
1244
1245 my %watchremote;
1246 my %watchremote_start;
1247
1248 my %repounchanged;
1249 my %globalnotready;
1250
1251 my %watchremoteprojs;   # tmp, only set in addwatchremote
1252
1253 my @retryevents;
1254
1255
1256 #
1257 # get_projpacks:  get/update project/package information
1258 #
1259 # input:  $projid: update just this project
1260 #         $packid: update just this package
1261 # output: $projpacks (global)
1262 #
1263 # calls calc_prps and calc_projpacks_linked for post-processing
1264 #
1265
1266 sub get_projpacks {
1267   my ($projid, @packids) = @_;
1268
1269   undef $projid unless $projpacks;
1270   @packids = () unless defined $projid;
1271   @packids = grep {defined $_} @packids;
1272
1273   if (!@packids) {
1274     if (defined($projid)) {
1275       delete $remoteprojs{$projid};
1276     } else {
1277       %remoteprojs = ();
1278     }
1279   }
1280
1281   my @args;
1282   if (@packids) {
1283     print "getting data for project '$projid' package '".join("', '", @packids)."' from $BSConfig::srcserver\n";
1284     push @args, "project=$projid";
1285     for my $packid (@packids) {
1286       delete $projpacks->{$projid}->{'package'}->{$packid} if $projpacks->{$projid} && $projpacks->{$projid}->{'package'};
1287       push @args, "package=$packid";
1288     }
1289   } elsif (defined($projid)) {
1290     print "getting data for project '$projid' from $BSConfig::srcserver\n";
1291     push @args, "project=$projid";
1292     delete $projpacks->{$projid};
1293   } else {
1294     print "getting data for all projects from $BSConfig::srcserver\n";
1295     $projpacks = {};
1296   }
1297   my $projpacksin;
1298   while (1) {
1299     eval {
1300       $projpacksin = BSRPC::rpc("$BSConfig::srcserver/getprojpack", $BSXML::projpack, 'withsrcmd5', 'withdeps', 'withrepos', 'withconfig', "arch=$myarch", @args);
1301     };
1302     if ($@ || !$projpacksin) {
1303       print $@ if $@;
1304       if (@args) {
1305         print "retrying...\n";
1306         get_projpacks();
1307         return;
1308       }
1309       printf("could not get project/package information, sleeping 1 minute\n");
1310       sleep(60);
1311       print "retrying...\n";
1312       next;
1313     }
1314     last;
1315   }
1316   for my $proj (@{$projpacksin->{'project'} || []}) {
1317     if (@packids) {
1318       die("bad projpack answer\n") unless $proj->{'name'} eq $projid;
1319       if ($projpacks->{$projid}) {
1320         # use all packages/configs from old projpacks
1321         my $opackage = $projpacks->{$projid}->{'package'} || {};
1322         for (keys %$opackage) {
1323           $opackage->{$_}->{'name'} = $_;
1324           push @{$proj->{'package'}}, $opackage->{$_};
1325         }
1326       }
1327     }
1328     $projpacks->{$proj->{'name'}} = $proj;
1329     delete $proj->{'name'};
1330     my $packages = {};
1331     for my $pack (@{$proj->{'package'} || []}) {
1332       $packages->{$pack->{'name'}} = $pack;
1333       delete $pack->{'name'};
1334     }
1335     if (%$packages) {
1336       $proj->{'package'} = $packages;
1337     } else {
1338       delete $proj->{'package'};
1339     }
1340   }
1341
1342   %watchremote = ();
1343   %watchremoteprojs = ();
1344
1345   #print Dumper($projpacks);
1346   calc_projpacks_linked();
1347   calc_prps();
1348
1349   updateremoteprojs();
1350   %watchremoteprojs = ();
1351 }
1352
1353 #
1354 # addwatchremote:  register for a remote resource
1355 #
1356 # input:  $type: type of resource (project/repository/source)
1357 #         $projid: update just this project
1358 #         $watch: extra data to match
1359 #
1360 sub addwatchremote {
1361   my ($type, $projid, $watch) = @_;
1362
1363   return undef if $projpacks->{$projid} && !$projpacks->{$projid}->{'remoteurl'};
1364   my $proj = remoteprojid($projid);
1365   $watchremoteprojs{$projid} = $proj;
1366   return undef unless $proj;
1367   $watchremote{$proj->{'remoteurl'}}->{"$type/$proj->{'remoteproject'}$watch"} = $projid;
1368   return $proj;
1369 }
1370
1371 sub addretryevent {
1372   my ($ev) = @_;
1373   for my $oev (@retryevents) {
1374     next if $ev->{'type'} ne $oev->{'type'} || $ev->{'project'} ne $oev->{'project'};
1375     if ($ev->{'type'} eq 'repository') {
1376       next if $ev->{'repository'} ne $oev->{'repository'};
1377     } elsif ($ev->{'type'} eq 'package') {
1378       next if $ev->{'package'} ne $oev->{'package'};
1379     }
1380     return;
1381   }
1382   $ev->{'retry'} = time() + 60;
1383   push @retryevents, $ev;
1384 }
1385
1386 #
1387 # calc_projpacks_linked  - generate projpacks_linked helper array
1388 #
1389 # input:  $projpacks (global)
1390 # output: @projpacks_linked (global)
1391 #
1392 sub calc_projpacks_linked {
1393   @projpacks_linked = ();
1394   for my $projid (sort keys %$projpacks) {
1395     my ($mypackid, $pack);
1396     while (($mypackid, $pack) = each %{$projpacks->{$projid}->{'package'} || {}}) {
1397       next unless $pack->{'linked'};
1398       for my $li (@{$pack->{'linked'}}) {
1399         addwatchremote('package', $li->{'project'}, "/$li->{'package'}");
1400         $li->{'myproject'} = $projid;
1401         $li->{'mypackage'} = $mypackid;
1402       }
1403       push @projpacks_linked, @{$pack->{'linked'}};
1404     }
1405   }
1406   #print Dumper(\@projpacks_linked);
1407 }
1408
1409 #
1410 # expandsearchpath  - recursively expand the last component
1411 #                     of a repository's path
1412 #
1413 # input:  $projid     - the project the repository belongs to
1414 #         $repository - the repository data
1415 # output: expanded path array
1416 #
1417 sub expandsearchpath {
1418   my ($projid, $repository) = @_;
1419   my %done;
1420   my @ret;
1421   my @path = @{$repository->{'path'} || []};
1422   for my $pathel (@path) {
1423     addwatchremote('repository', $pathel->{'project'}, "/$pathel->{'repository'}/$myarch");
1424   }
1425   # our own repository is not included in the path,
1426   # so put it infront of everything
1427   unshift @path, {'project' => $projid, 'repository' => $repository->{'name'}};
1428   while (@path) {
1429     my $t = shift @path;
1430     my $prp = "$t->{'project'}/$t->{'repository'}";
1431     push @ret, $t unless $done{$prp};
1432     $done{$prp} = 1;
1433     if (!@path) {
1434       last if $done{"/$prp"};
1435       my ($pid, $tid) = ($t->{'project'}, $t->{'repository'});
1436       my $proj = addwatchremote('project', $pid, '');
1437       if ($proj) {
1438         # check/invalidate cache?
1439         $proj = fetchremoteproj($proj, $pid);
1440         # clone it as we modify the repopath
1441         $proj = Storable::dclone($proj);
1442         my @repo = grep {$_->{'name'} eq $tid} @{$proj->{'repository'} || []};
1443         if (@repo && $repo[0]->{'path'}) {
1444           addwatchremote('repository', $pid, "/$tid/$myarch");
1445           for my $pathel (@{$repo[0]->{'path'}}) {
1446             # map projects to remote
1447             my $remoteprojid = $pathel->{'project'};
1448             $pathel->{'project'} = maptoremote($proj, $remoteprojid);
1449             addwatchremote('repository', $pathel->{'project'}, "/$pathel->{'repository'}/$myarch") if $pathel->{'project'} ne '_unavailable';
1450           }
1451         }
1452       } else {
1453         $proj = $projpacks->{$pid};
1454       }
1455       next unless $proj;
1456       $done{"/$prp"} = 1;       # mark expanded
1457       my @repo = grep {$_->{'name'} eq $tid} @{$proj->{'repository'} || []};
1458       push @path, @{$repo[0]->{'path'}} if @repo && $repo[0]->{'path'};
1459     }
1460   }
1461   return @ret;
1462 }
1463
1464 #
1465 # calc_prps
1466 #
1467 # find all prps we have to schedule, expand search path for every prp,
1468 # set up inter-prp dependency graph, sort prps using this graph.
1469 #
1470 # input:  $projpacks     (global)
1471 # output: @prps          (global)
1472 #         %prpsearchpath (global)
1473 #         %prpdeps       (global)
1474 #         %prpnoleaf     (global)
1475 #
1476
1477 sub calc_prps {
1478   print "calculating project dependencies...\n";
1479   # calculate prpdeps dependency hash
1480   @prps = ();
1481   %prpsearchpath = ();
1482   %prpdeps = ();
1483   %prpnoleaf = ();
1484   for my $projid (sort keys %$projpacks) {
1485     my $repos = $projpacks->{$projid}->{'repository'} || [];
1486     my @aggs = grep {$_->{'aggregatelist'}} values(%{$projpacks->{$projid}->{'package'} || {}});
1487     my @kiwiinfos = grep {$_->{'path'}} map {@{$_->{'info'} || []}} values(%{$projpacks->{$projid}->{'package'} || {}});
1488     for my $repo (@$repos) {
1489       next unless grep {$_ eq $myarch} @{$repo->{'arch'} || []};
1490       my $repoid = $repo->{'name'};
1491       my $prp = "$projid/$repoid";
1492       push @prps, $prp;
1493       my @searchpath = expandsearchpath($projid, $repo);
1494       # map searchpath to internal prp representation
1495       my @sp = map {"$_->{'project'}/$_->{'repository'}"} @searchpath;
1496       $prpsearchpath{$prp} = \@sp;
1497       $prpdeps{"$projid/$repo->{'name'}"} = \@sp;
1498
1499       # Find extra dependencies due to aggregate/kiwi description files
1500       my @xsp;
1501       if (@aggs) {
1502         # push source repositories used in this aggregate onto xsp, obey target mapping
1503         for my $agg (map {@{$_->{'aggregatelist'}->{'aggregate'} || []}} @aggs) {
1504           my $aprojid = $agg->{'project'};
1505           my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$agg->{'repository'} || []}; 
1506           if (@arepoids) {
1507             # got some mappings for our target, use source as repoid
1508             push @xsp, map {"$aprojid/$_->{'source'}"} grep {exists($_->{'source'})} @arepoids;
1509           } else {
1510             # no repository mapping, just use own repoid
1511             push @xsp, "$aprojid/$repoid";
1512           }
1513         }
1514       }
1515       if (@kiwiinfos) {
1516         # push repositories used in all kiwi files
1517         push @xsp, map {"$_->{'project'}/$_->{'repository'}"} map {@{$_->{'path'}}} grep {$_->{'name'} eq $repoid} @kiwiinfos;
1518       }
1519
1520       if (@xsp) {
1521         # found some repos, join extra deps with project deps
1522         for my $xsp (@xsp) {
1523           next if $xsp eq $prp;
1524           my ($mprojid, $mrepoid) = split('/', $xsp, 2);
1525           # we just watch the repository as it costs too much to
1526           # watch every single package
1527           addwatchremote('repository', $mprojid, "/$mrepoid/$myarch");
1528         }
1529         my %xsp = map {$_ => 1} (@sp, @xsp);
1530         delete $xsp{$prp};
1531         $prpdeps{$prp} = [ sort keys %xsp ];
1532       }
1533       # set noleaf info
1534       for (@{$prpdeps{$prp}}) {
1535         $prpnoleaf{$_} = 1 if $_ ne $prp;
1536       }
1537     }
1538   }
1539
1540   # do the real sorting
1541   print "sorting projects and repositories...\n";
1542   @prps = sortpacks(\%prpdeps, undef, undef, undef, undef, @prps);
1543 }
1544
1545 ####################################################################
1546
1547 sub updateremoteprojs {
1548   for my $projid (keys %remoteprojs) {
1549     my $r = $watchremoteprojs{$projid};
1550     if (!$r) {
1551       delete $remoteprojs{$projid};
1552       next;
1553     }
1554     my $or = $remoteprojs{$projid};
1555     next if $or && $or->{'remoteurl'} eq $r->{'remoteurl'} && $or->{'remoteproject'} eq $r->{'remoteproject'};
1556     delete $remoteprojs{$projid};
1557   }
1558   for my $projid (sort keys %watchremoteprojs) {
1559     fetchremoteproj($watchremoteprojs{$projid}, $projid);
1560   }
1561 }
1562
1563 sub remoteprojid {
1564   my ($projid) = @_;
1565   my $rsuf = '';
1566   my $origprojid = $projid;
1567
1568   my $proj = $projpacks->{$projid};
1569   if ($proj) {
1570     return undef unless $proj->{'remoteurl'};
1571     return undef unless $proj->{'remoteproject'};
1572     return {
1573       'name' => $projid,
1574       'root' => $projid,
1575       'remoteroot' => $proj->{'remoteproject'},
1576       'remoteurl' => $proj->{'remoteurl'},
1577       'remoteproject' => $proj->{'remoteproject'},
1578     };
1579   }
1580   while ($projid =~ /^(.*)(:.*?)$/) {
1581     $projid = $1;
1582     $rsuf = "$2$rsuf";
1583     $proj = $projpacks->{$projid};
1584     if ($proj) {
1585       return undef unless $proj->{'remoteurl'};
1586       if ($proj->{'remoteproject'}) {
1587         $rsuf = "$proj->{'remoteproject'}$rsuf";
1588       } else {
1589         $rsuf =~ s/^://;
1590       }
1591       return {
1592         'name' => $origprojid,
1593         'root' => $projid,
1594         'remoteroot' => $proj->{'remoteproject'},
1595         'remoteurl' => $proj->{'remoteurl'},
1596         'remoteproject' => $rsuf,
1597       };
1598     }
1599   }
1600   return undef;
1601 }
1602
1603 sub maptoremote {
1604   my ($proj, $projid) = @_;
1605   return "$proj->{'root'}:$projid" unless $proj->{'remoteroot'};
1606   return $proj->{'root'} if $projid eq $proj->{'remoteroot'};
1607   return '_unavailable' if $projid !~ /^\Q$proj->{'remoteroot'}\E:(.*)$/;
1608   return "$proj->{'root'}:$1";
1609 }
1610
1611 sub fetchremoteproj {
1612   my ($proj, $projid) = @_;
1613   return undef unless $proj && $proj->{'remoteurl'} && $proj->{'remoteproject'};
1614   $projid ||= $proj->{'name'};
1615   return $remoteprojs{$projid} if exists $remoteprojs{$projid};
1616   print "fetching remote project data for $projid\n";
1617   my $rproj;
1618   my $param = {
1619     'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/_meta",
1620     'timeout' => 30,
1621   };
1622   eval {
1623     $rproj = BSRPC::rpc($param, $BSXML::proj);
1624   };
1625   if ($@) {
1626     warn($@);
1627     my $error = $@;
1628     $error =~ s/\n$//s;
1629     $rproj = {'error' => $error};
1630     addretryevent({'type' => 'project', 'project' => $projid}) if $error !~ /^remote error:/;
1631   }
1632   return undef unless $rproj;
1633   for (qw{name root remoteroot remoteurl remoteproject}) {
1634     $rproj->{$_} = $proj->{$_};
1635   }
1636   $remoteprojs{$projid} = $rproj;
1637   return $rproj;
1638 }
1639
1640 sub fetchremoteconfig {
1641   my ($projid) = @_;
1642
1643   my $proj = $remoteprojs{$projid};
1644   return undef if !$proj || $proj->{'error'};
1645   return $proj->{'config'} if exists $proj->{'config'};
1646   print "fetching remote project config for $projid\n";
1647   my $c;
1648   my $param = {
1649     'uri' => "$proj->{'remoteurl'}/source/$proj->{'remoteproject'}/_config",
1650     'timeout' => 30,
1651   };
1652   eval {
1653     $c = BSRPC::rpc($param);
1654   };
1655   if ($@) {
1656     warn($@);
1657     $proj->{'error'} = $@;
1658     $proj->{'error'} =~ s/\n$//s;
1659     addretryevent({'type' => 'project', 'project' => $projid}) if $proj->{'error'} !~ /^remote error:/;
1660     return undef;
1661   }
1662   $proj->{'config'} = $c;
1663   return $c;
1664 }
1665
1666 sub findbins_remote {
1667   my ($prp) = @_;
1668   my ($projid, $repoid) = split('/', $prp, 2);
1669   my $proj = $remoteprojs{$projid};
1670   return {} if !$proj || $proj->{'error'};
1671   print "fetching remote repository state for $prp\n";
1672   my $param = {
1673     'uri' => "$proj->{'remoteurl'}/build/$proj->{'remoteproject'}/$repoid/$myarch/_repository",
1674     'timeout' => 200,
1675     'receiver' => \&BSHTTP::cpio_receiver,
1676   };
1677   my $cpio;
1678   eval {
1679     $cpio = BSRPC::rpc($param, undef, "view=cache");
1680   };
1681   if ($@) {
1682     warn($@);
1683     my $error = $@;
1684     $error =~ s/\n$//s;
1685     addretryevent({'type' => 'repository', 'project' => $projid, 'repository' => $repoid, 'arch' => $myarch}) if $error !~ /^remote error:/;
1686     return undef;
1687   }
1688   my %cpio = map {$_->{'name'} => $_->{'data'}} @{$cpio || []};
1689   my $repostate = $cpio{'repositorystate'};
1690   $repostate = XMLin($BSXML::repositorystate, $repostate) if $repostate;
1691   delete $globalnotready{$prp};
1692   if ($repostate && $repostate->{'blocked'}) {
1693     $globalnotready{$prp} = { map {$_ => 1} @{$repostate->{'blocked'}}};
1694   }
1695   my $cachedata = $cpio{'repositorycache'};
1696   return {} unless $cachedata;
1697   my $cache;
1698   eval { $cache = Storable::thaw(substr($cachedata, 4)); };
1699   undef $cachedata;
1700   warn($@) if $@;
1701   return undef unless $cache;
1702   for (values %$cache) {
1703     delete $_->{'path'};
1704     delete $_->{'id'};
1705   }
1706   return $cache;
1707 }
1708
1709 #
1710 # jobfinished - called when a build job is finished
1711 #
1712 # - move built packages into :full tree
1713 #   (updates corresponding $repodata entry)
1714 # - set changed flag
1715 #
1716 # input: $job       - job identification
1717 #        $js        - job status information (BSXML::jobstatus)
1718 #        $repodatas - reference of global repodata hash
1719 #        $changed   - reference to changed hash, mark prp if
1720 #                     we changed the repository
1721 #        $pdata     - package data
1722 #
1723 sub jobfinished {
1724   my ($job, $js, $repodatas, $changed) = @_;
1725
1726   my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
1727   my $jobdatadir = "$myjobsdir/$job:dir";
1728   if (!$info || ! -d $jobdatadir) {
1729     print "  - $job is bad\n";
1730     return;
1731   }
1732   my $projid = $info->{'project'};
1733   my $repoid = $info->{'repository'};
1734   my $packid = $info->{'package'};
1735   my $prp = "$projid/$repoid";
1736   my $mytime = time(); # ensure that we use the same time in all logs
1737   if ($info->{'arch'} ne $myarch) {
1738     print "  - $job has bad arch\n";
1739     return;
1740   }
1741   if (!$projpacks->{$projid}) {
1742     print "  - $job belongs to an unknown project\n";
1743     return;
1744   }
1745   my $pdata = ($projpacks->{$projid}->{'package'} || {})->{$packid};
1746   if (!$pdata) {
1747     print "  - $job belongs to an unknown package, discard\n";
1748     return;
1749   }
1750   my $statusdir = "$reporoot/$prp/$myarch/$packid";
1751   if (! -d $statusdir) {
1752     print "  - $job belongs to obsolete package\n";
1753     return;
1754   }
1755   my $status = readxml("$statusdir/status", $BSXML::buildstatus, 1);
1756   if (!$status) {
1757     print "  - $job has no status\n";
1758     return;
1759   }
1760   if (!$status->{'job'} || $status->{'job'} ne $job) {
1761     print "  - $job is outdated\n";
1762     return;
1763   }
1764   delete $status->{'job'};      # no longer building
1765
1766   delete $status->{'arch'};     # obsolete
1767   delete $status->{'uri'};      # obsolete
1768
1769   my $code = $js->{'result'};
1770   $code = 'failed' unless $code eq 'succeeded' || $code eq 'unchanged';
1771
1772   my @all = ls($jobdatadir);
1773   my %all = map {$_ => 1} @all;
1774   @all = map {"$jobdatadir/$_"} @all;
1775
1776   my $gdst = "$reporoot/$prp/$myarch";
1777   my $dst = "$gdst/$packid";
1778   mkdir_p($dst);
1779   mkdir_p("$gdst/:meta");
1780   mkdir_p("$gdst/:logfiles.fail");
1781   mkdir_p("$gdst/:logfiles.success");
1782   unlink("$reporoot/$prp/$myarch/:repodone");
1783   if (!$all{'meta'}) {
1784     if ($code eq 'succeeded') {
1785       print "  - $job claims success but there is no meta\n";
1786       return;
1787     }
1788     # severe failure, create src change fake...
1789     writestr("$jobdatadir/meta", undef, "$info->{'srcmd5'}  $packid\nfake to detect source changes...  fake\n");
1790     push @all, "$jobdatadir/meta";
1791     $all{'meta'} = 1;
1792   }
1793
1794   # update packstatus so that it doesn't fall back to scheduled
1795   my $ps = readxml("$reporoot/$prp/$myarch/:packstatus", $BSXML::packstatuslist, 1);
1796   if ($ps) {
1797     for (@{$ps->{'packstatus'} || []}) {
1798       next unless $_->{'name'} eq $packid;
1799       $_->{'status'} = 'finished';
1800       $_->{'error'} = $code;
1801     }
1802     writexml("$reporoot/$prp/$myarch/.:packstatus", "$reporoot/$prp/$myarch/:packstatus", $ps, $BSXML::packstatuslist);
1803   }
1804
1805   my $meta = $all{'meta'} ? "$jobdatadir/meta" : undef;
1806   if ($code eq 'unchanged') {
1807     print "  - $job: build results is unchanged\n";
1808     if ( -e "$gdst/:logfiles.success/$packid" ){
1809       # make sure to use the last succeeded logfile matching to these binaries
1810       link("$gdst/:logfiles.success/$packid", "$dst/logfile.dup");
1811       rename("$dst/logfile.dup", "$dst/logfile");
1812       unlink("$dst/logfile.dup");
1813     };
1814     if (open(F, '+>>', "$dst/logfile")) {
1815       # Add a comment to logfile from last real build
1816       print F "\nSkipped built at ".localtime(time());
1817       close(F);
1818     };
1819     unlink("$gdst/:logfiles.fail/$packid");
1820     rename($meta, "$gdst/:meta/$packid") if $meta;
1821     unlink($_) for @all;
1822     rmdir($jobdatadir);
1823     $status->{'status'} = 'unchanged'; # log as "unchanged" in history
1824     addjobhist($prp, $info, $status, $js);
1825     $status->{'status'} = 'succeeded'; # but show as "succeeded" in package status, since old build was successfull
1826     writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
1827     $changed->{$prp} ||= 1;     # package is no longer blocking
1828     return;
1829   }
1830   if ($code eq 'failed') {
1831     print "  - $job: build failed\n";
1832     link("$jobdatadir/logfile", "$jobdatadir/logfile.dup");
1833     rename("$jobdatadir/logfile", "$dst/logfile");
1834     rename("$jobdatadir/logfile.dup", "$gdst/:logfiles.fail/$packid");
1835     rename($meta, "$gdst/:meta/$packid") if $meta;
1836     unlink($_) for @all;
1837     rmdir($jobdatadir);
1838     $status->{'status'} = 'failed';
1839     addjobhist($prp, $info, $status, $js);
1840     writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
1841     $changed->{$prp} ||= 1;     # package is no longer blocking
1842     return;
1843   }
1844   print "  - $prp: $packid built: ".(@all). " files\n";
1845   mkdir_p("$gdst/:logfiles.success");
1846   mkdir_p("$gdst/:logfiles.fail");
1847
1848   $repodatas->{$prp} ||= findbins($prp);
1849   my $repodata = $repodatas->{$prp};
1850
1851   my $useforbuildenabled = 1;
1852   $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
1853   $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
1854   update_dst_full($repodata, $prp, $dst, $jobdatadir, $meta, $useforbuildenabled, $prpsearchpath{$prp});
1855   $changed->{$prp} = 2 if $useforbuildenabled;
1856   delete $repounchanged{$prp} if $useforbuildenabled;
1857   $changed->{$prp} ||= 1;
1858
1859   # save meta file
1860   rename($meta, "$gdst/:meta/$packid") if $meta;
1861
1862   # write new status
1863   $status->{'status'} = 'succeeded';
1864   addjobhist($prp, $info, $status, $js);
1865   writexml("$statusdir/.status", "$statusdir/status", $status, $BSXML::buildstatus);
1866
1867   # write history file
1868   my $h = {'versrel' => $info->{'versrel'}, 'bcnt' => $info->{'bcnt'}, 'time' => $mytime, 'srcmd5' => $info->{'srcmd5'}, 'rev' => $info->{'rev'}, 'reason' => $info->{'reason'}};
1869   BSFileDB::fdb_add("$reporoot/$prp/$myarch/$packid/history", $historylay, $h);
1870
1871   # update relsync file
1872   my $relsync;
1873   eval { $relsync = Storable::retrieve("$reporoot/$prp/$myarch/:relsync"); };
1874   $relsync ||= {};
1875   $relsync->{$packid} = "$info->{'versrel'}.$info->{'bcnt'}";
1876   Storable::nstore($relsync, "$reporoot/$prp/$myarch/:relsync.new");
1877   rename("$reporoot/$prp/$myarch/:relsync.new", "$reporoot/$prp/$myarch/:relsync") || die("rename $reporoot/$prp/$myarch/:relsync.new $reporoot/$prp/$myarch/:relsync: $!\n");
1878   
1879   # save logfile
1880   link("$jobdatadir/logfile", "$jobdatadir/logfile.dup");
1881   rename("$jobdatadir/logfile", "$dst/logfile");
1882   rename("$jobdatadir/logfile.dup", "$gdst/:logfiles.success/$packid");
1883   unlink("$gdst/:logfiles.fail/$packid");
1884   unlink($_) for @all;
1885   rmdir($jobdatadir);
1886 }
1887
1888 sub importevent {
1889   my ($job, $js, $repodatas, $changed) = @_;
1890
1891   my $info = readxml("$myjobsdir/$job", $BSXML::buildinfo, 1);
1892   my $jobdatadir = "$myjobsdir/$job:dir";
1893   if (!$info || ! -d $jobdatadir) {
1894     print "  - $job is bad\n";
1895     return;
1896   }
1897   my $projid = $info->{'project'};
1898   my $repoid = $info->{'repository'};
1899   my $packid = $info->{'package'};
1900   my $prp = "$projid/$repoid";
1901   my @all = ls($jobdatadir);
1902   my %all = map {$_ => 1} @all;
1903   my $meta = $all{'meta'} ? "$jobdatadir/meta" : undef;
1904   @all = map {"$jobdatadir/$_"} @all;
1905   $repodatas->{$prp} ||= findbins($prp);
1906   my $repodata = $repodatas->{$prp};
1907   my $useforbuildenabled = 1;
1908   update_dst_full($repodata, $prp, undef, $jobdatadir, $meta, $useforbuildenabled, $prpsearchpath{$prp});
1909   $changed->{$prp} = 2 if $useforbuildenabled;
1910   unlink($_) for @all;
1911   rmdir($jobdatadir);
1912 }
1913
1914 ##########################################################################
1915 ##########################################################################
1916 ##
1917 ##  kiwi-image package type handling
1918 ##
1919 sub checkkiwiimage {
1920   my ($projid, $repoid, $packid, $pdata, $info, $repodatas, $notready, $relsynctrigger) = @_;
1921
1922   my $prp = "$projid/$repoid";
1923   my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
1924
1925   my @repos;
1926   for my $aprp (@aprps) {
1927     $repodatas->{$aprp} ||= findbins($aprp);
1928     my $repodata = $repodatas->{$aprp}; 
1929     if (!$repodata) {
1930       print "      - $packid (kiwi-image)\n";
1931       print "        repository $aprp is unavailable";
1932       return ('broken', "repository $aprp is unavailable");
1933     }
1934     push @repos, $repodata;
1935   }
1936   # get config from path
1937   my $bconf = getconfig($myarch, \@aprps);
1938   if (!$bconf) {
1939     print "      - $packid (kiwi-image)\n";
1940     print "        no config\n";
1941     return ('broken', 'no config');
1942   }
1943
1944   my $bconfignore = $bconf->{'ignore'};
1945   my $bconfignoreh = $bconf->{'ignoreh'};
1946   delete $bconf->{'ignore'};
1947   delete $bconf->{'ignoreh'};
1948   Build::readdeps($bconf, undef, reverse @repos);
1949   # expand deps for that path
1950   my @deps = @{$info->{'dep'} || []};
1951   my ($eok, @edeps) = Build::get_deps($bconf, [], @deps);
1952   if (!$eok) {
1953     print "      - $packid (kiwi-image)\n";
1954     print "        expansion errors:\n";
1955     print "            $_\n" for @edeps;
1956     return ('expansion error', join(', ', @edeps));
1957   }
1958   $bconf->{'ignore'} = $bconfignore if $bconfignore;
1959   $bconf->{'ignoreh'} = $bconfignoreh if $bconfignoreh;
1960
1961   my @new_meta;
1962   push @new_meta, "$pdata->{'srcmd5'}  $packid";
1963   for (@{$info->{'extrasource'} || []}) {
1964     push @new_meta, "$_->{'srcmd5'}  $_->{'project'}/$_->{'package'}";
1965   }
1966   my @blocked;
1967   for my $aprp (@aprps) {
1968     $repodatas->{$aprp} ||= findbins($aprp);
1969     my $repodata = $repodatas->{$aprp};
1970     my $gn = ($prp eq $aprp ? $notready : $globalnotready{$aprp}) || {};
1971     my @b = grep {$gn->{$_}} @edeps;
1972     if (@b) {
1973       @b = map {"$aprp/$_"} @b if $prp ne $aprp;
1974       push @blocked, @b;
1975     }
1976     next if @blocked;
1977     for my $dep (sort(@edeps)) {
1978       my $r = $repodata->{$dep};
1979       next unless $r;
1980       push @new_meta, "$r->{'hdrmd5'}  $aprp/$dep";
1981     }
1982   }
1983   if (@blocked) {
1984     print "      - $packid (kiwi-image)\n";
1985     print "        blocked (@blocked)\n";
1986     return ('blocked', join(', ', @blocked));
1987   }
1988   my @meta = split("\n", (readstr("$reporoot/$prp/$myarch/:meta/$packid", 1) || ''));
1989   if (!@meta || !$meta[0]) {
1990     print "      - $packid (kiwi-image)\n";
1991     print "        start build\n";
1992     return ('scheduled', [ $bconf, \@edeps, {'explain' => 'new build'} ]);
1993   }
1994   if ($meta[0] ne $new_meta[0]) {
1995     print "      - $packid (kiwi-image)\n";
1996     print "        src change, start build\n";
1997     return ('scheduled', [ $bconf, \@edeps, {'explain' => 'source change', 'oldsource' => substr($meta[0], 0, 32)} ]);
1998   }
1999   if (join('\n', @meta) eq join('\n', @new_meta)) {
2000     if ($relsynctrigger) {
2001       print "      - $packid (kiwi-image)\n";
2002       print "        rebuild counter sync\n";
2003       return ('scheduled', [ $bconf, \@edeps, {'explain' => 'rebuild counter sync'} ]);
2004     }
2005     print "      - $packid (kiwi-image)\n";
2006     print "        nothing changed\n";
2007     return ('done');
2008   }
2009   my @diff = diffsortedmd5(0, \@meta, \@new_meta);
2010   print "      - $packid (kiwi-image)\n";
2011   print "        $_\n" for @diff;
2012   print "        meta change, start build\n";
2013   return ('scheduled', [ $bconf, \@edeps, {'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff)} ]);
2014 }
2015
2016 sub rebuildkiwiimage {
2017   my ($projid, $repoid, $packid, $pdata, $info, $data, $relsyncmax) = @_;
2018   my $bconf = $data->[0];
2019   my $edeps = $data->[1];
2020   my $reason = $data->[2];
2021   my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
2022   my ($job, $joberror) = set_building($projid, $repoid, $packid, $pdata, $info, $bconf, [], $edeps, \@aprps, $reason, $relsyncmax);
2023   if ($job) {
2024     return ('scheduled', $job);
2025   } else {
2026     return ('broken', $joberror);
2027   }
2028 }
2029
2030 ##########################################################################
2031 ##########################################################################
2032 ##
2033 ##  kiwi-product package type handling
2034 ##
2035 my %bininfo_cache;
2036
2037 sub checkkiwiproduct {
2038   my ($projid, $repoid, $packid, $pdata, $info, $relsynctrigger) = @_;
2039
2040   # hmm, should get the arch from the kiwi info
2041   # but how can we map it to the buildarchs?
2042   my $repo = (grep {$_->{'name'} eq $repoid} @{$projpacks->{$projid}->{'repository'} || []})[0];
2043   return ('broken', 'missing repo') unless $repo;       # can't happen
2044
2045   # calculate all involved architectures
2046   my @archs;
2047   my $savemyarch = $myarch;
2048   for my $arch (@{$repo->{'arch'} || []}) {
2049     my $enabled = 1;
2050     $myarch = $arch;
2051     $enabled = enabled($repoid, $projpacks->{$projid}->{'build'}, 1);
2052     $enabled = enabled($repoid, $pdata->{'build'}, $enabled);
2053     push @archs, $arch if $enabled;
2054   }
2055   $myarch = $savemyarch;
2056   
2057   if (!grep {$_ eq $myarch} @archs) {
2058     print "      - $packid (kiwi-product)\n";
2059     print "        not mine\n";
2060     return ('excluded');
2061   }
2062
2063   my @deps = @{$info->{'dep'} || []};   # expanded?
2064   my %deps = map {$_ => 1} @deps;
2065   delete $deps{''};
2066   my @aprps = map {"$_->{'project'}/$_->{'repository'}"} @{$info->{'path'} || []};
2067
2068   my @blocked;
2069   my @rpms;
2070   my %rpms_meta;
2071   my %rpms_hdrmd5;
2072
2073 #print "packids: @packids\n";
2074 #print "prps: @aprps\n";
2075 #print "archs: @archs\n";
2076 #print "deps: @deps\n";
2077   my $allpacks = $deps{'*'} ? 1 : 0;
2078
2079   my $maxblocked = 20;
2080   for my $aprp (@aprps) {
2081     my %known;
2082     my ($aprojid, $arepoid) = split('/', $aprp, 2);
2083     my $pdatas = $projpacks->{$aprojid}->{'package'} || {};
2084     my @apackids = sort keys %$pdatas;
2085     for my $apackid (@apackids) {
2086       my $info = (grep {$_->{'repository'} eq $arepoid} @{$pdatas->{$apackid}->{'info'} || []})[0];
2087       $known{$apackid} = $info->{'name'} if $info && $info->{'name'};
2088     }
2089     for my $arch ($archs[0] eq $myarch ? @archs : $myarch) {
2090       my $ps = (readxml("$reporoot/$aprp/$arch/:packstatus", $BSXML::packstatuslist, 1) || {})->{'packstatus'} || [];
2091       for my $apackid (@apackids) {
2092         if ($allpacks || $deps{$apackid} || $deps{$known{$apackid} || ''}) {
2093           # hey, we probably need this package! wait till it's finished
2094           my @notyet = grep {$_->{'name'} eq $apackid && ($_->{'status'} eq 'scheduled' || $_->{'status'} eq 'blocked' || $_->{'status'} eq 'finished')} @$ps;
2095           if (@notyet) {
2096             push @blocked, "$aprp/$arch/$apackid";
2097             last if @blocked > $maxblocked;
2098             next;
2099           }
2100         }
2101         # hmm, we don't know if we really need it. scan content.
2102         my @got;
2103         my $needit;
2104         $needit = 1 if $allpacks;
2105         my $bis;
2106         my @bininfo_s = stat("$reporoot/$aprp/$arch/$apackid/.bininfo");
2107         if (-s _) {
2108           $bis = $bininfo_cache{"$aprp/$arch/$apackid/.bininfo"};
2109           if (!defined($bis->[0]) || $bis->[0] ne "$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]") {
2110             local *F;
2111             undef $bis;
2112             if (open(F, '<', "$reporoot/$aprp/$arch/$apackid/.bininfo")) {
2113               @bininfo_s = stat(F);
2114               die unless @bininfo_s;
2115               my $bisc = '';
2116               1 while sysread(F, $bisc, 8192, length($bisc));
2117               close F;
2118               $bis = ["$bininfo_s[9]/$bininfo_s[7]/$bininfo_s[1]", $bisc];
2119               $bininfo_cache{"$aprp/$arch/$apackid/.bininfo"} = $bis;
2120             }
2121           }
2122         }
2123         if ($bis) {
2124           for my $bi (split("\n", $bis->[1])) {
2125             my $b = substr($bi, 34);
2126             next unless $b =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
2127             $needit = 1 if $deps{$1};
2128             push @got, "$aprp/$arch/$apackid/$b";
2129             $rpms_hdrmd5{$got[-1]} = substr($bi, 0, 32);
2130             $rpms_meta{$got[-1]} = "$aprp/$arch/$apackid/$1.$2";
2131           }
2132         } else {
2133           for my $b (ls("$reporoot/$aprp/$arch/$apackid")) {
2134             next unless $b =~ /^(.+)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
2135             $needit = 1 if $deps{$1};
2136             push @got, "$aprp/$arch/$apackid/$b";
2137             $rpms_meta{$got[-1]} = "$aprp/$arch/$apackid/$1.$2";
2138           }
2139         }
2140         next unless $needit;
2141         # ok we need it. check if they are built.
2142         my @notyet = grep {$_->{'name'} eq $apackid && ($_->{'status'} eq 'scheduled' || $_->{'status'} eq 'blocked' || $_->{'status'} eq 'finished')} @$ps;
2143         if (@notyet) {
2144           push @blocked, "$aprp/$arch/$apackid";
2145           last if @blocked > $maxblocked;
2146           next;
2147         }
2148         push @rpms, @got;
2149       }
2150       last if @blocked > $maxblocked;
2151     }
2152     last if @blocked > $maxblocked;
2153   }
2154   if (@blocked) {
2155     push @blocked, '...' if @blocked > $maxblocked;
2156     print "      - $packid (kiwi-product)\n";
2157     print "        blocked (@blocked)\n";
2158     return ('blocked', join(', ', @blocked));
2159   }
2160
2161   if ($archs[0] ne $myarch) {
2162     # looks good from our side. tell master arch
2163     # to check it
2164     my $ev = {
2165       'type' => 'unblocked',
2166       'project' => $projid,
2167       'repository' => $repoid,
2168     };
2169     my $evname = "unblocked::${projid}::${repoid}";
2170     sendevent($ev, $archs[0], "unblocked::${projid}::${repoid}");
2171     print "      - $packid (kiwi-product)\n";
2172     print "        unblocked\n";
2173     return ('excluded');
2174   }
2175
2176   # now create meta info
2177   my @new_meta;
2178   push @new_meta, "$pdata->{'srcmd5'}  $packid";
2179   push @new_meta, map {"$_->{'srcmd5'}  $_->{'project'}/$_->{'package'}"} @{$info->{'extrasource'} || []};
2180   for my $rpm (sort {$rpms_meta{$a} cmp $rpms_meta{$b}} @rpms) {
2181     my $id = $rpms_hdrmd5{$rpm};
2182     eval {
2183       $id ||= Build::queryhdrmd5("$reporoot/$rpm");
2184     };
2185     $id = "deaddeaddeaddeaddeaddeaddeaddead" unless $id;
2186     push @new_meta, "$id  $rpms_meta{$rpm}";
2187   }
2188   my @meta;
2189   if (open(F, '<', "$reporoot/$projid/$repoid/$myarch/:meta/$packid")) {
2190     @meta = <F>;
2191     close F;
2192     chomp @meta;
2193   }
2194   if (join('\n', @meta) eq join('\n', @new_meta)) {
2195     if ($relsynctrigger) {
2196       print "      - $packid (kiwi-product)\n";
2197       print "        rebuild counter sync\n";
2198       return ('scheduled', [ \@rpms, {'explain' => 'rebuild counter sync'} ]);
2199     }
2200     print "      - $packid (kiwi-product)\n";
2201     print "        nothing changed\n";
2202     return ('done');
2203   }
2204   my @diff = diffsortedmd5(0, \@meta, \@new_meta);
2205   print "      - $packid (kiwi-product)\n";
2206   print "        $_\n" for @diff;
2207   return ('scheduled', [ \@rpms, {'explain' => 'meta change', 'packagechange' => sortedmd5toreason(@diff)} ]);
2208 }
2209
2210 sub rebuildkiwiproduct {
2211   my ($projid, $repoid, $packid, $pdata, $info, $prpsearchpath, $data, $relsyncmax) = @_;
2212
2213   my $rpms = $data->[0];
2214   my $reason = $data->[1];
2215   my $prp = "$projid/$repoid";
2216   my $srcmd5 = $pdata->{'srcmd5'};
2217   my $f = jobname($prp, $packid);
2218   my $mytime = time(); # ensure that we use the same time in all logs
2219   return ('scheduled', "$f-$srcmd5") if -s "$myjobsdir/$f-$srcmd5";
2220   my @otherjobs = grep {/^\Q$f\E-[0-9a-f]{32}$/} ls($myjobsdir);
2221   $f = "$f-$srcmd5";
2222
2223   # kill those ancient other jobs
2224   for my $otherjob (@otherjobs) {
2225     print "        killing old job $otherjob\n";
2226     killjob($otherjob);
2227   }
2228
2229   my $searchpath = [];
2230   for (@$prpsearchpath) {
2231     my @pr = split('/', $_, 2);
2232     if ($remoteprojs{$pr[0]}) {
2233       push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::srcserver};
2234     } else {
2235       push @$searchpath, {'project' => $pr[0], 'repository' => $pr[1], 'server' => $BSConfig::reposerver};
2236     }    
2237   }
2238
2239   my @bdeps;
2240   for my $rpm (@{$rpms || []}) {
2241     my @b = split('/', $rpm);
2242     next unless @b == 5;
2243     next unless $b[4] =~ /^(.+)-([^-]+)-([^-]+)\.([a-zA-Z][^\.\-]*)\.rpm$/;
2244     push @bdeps, {
2245       'name' => $1,
2246       'version' => $2,
2247       'release' => $3,
2248       'arch' => $4,
2249       'project' => $b[0],
2250       'repository' => $b[1],
2251       'repoarch' => $b[2],
2252       'package' => $b[3],
2253     };
2254   }
2255   if ($info->{'extrasource'}) {
2256     push @bdeps, map {{
2257       'name' => $_->{'file'}, 'version' => '', 'repoarch' => 'src',
2258       'project' => $_->{'project'}, 'package' => $_->{'package'}, 'srcmd5' => $_->{'srcmd5'},
2259     }} @{$info->{'extrasource'}};
2260   }
2261
2262   # find the last build count we used for this version/release
2263   mkdir_p("$reporoot/$prp/$myarch/$packid");
2264   my $h = BSFileDB::fdb_getmatch("$reporoot/$prp/$myarch/$packid/history", $historylay, 'versrel', defined($pdata->{'versrel'}) ? $pdata->{'versrel'} : '', 1);
2265   $h = {'bcnt' => 0} unless $h;
2266
2267   # max with sync data
2268   my $tag = $pdata->{'bcntsynctag'} || $packid;
2269   if ($relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
2270     if ($h->{'bcnt'} + 1 < $relsyncmax->{"$tag/$pdata->{'versrel'}"}) {
2271       $h->{'bcnt'} = $relsyncmax->{"$tag/$pdata->{'versrel'}"} - 1;
2272     }
2273   }
2274
2275   my $binfo = {
2276     'project' => $projid,
2277     'repository' => $repoid,
2278     'package' => $packid,
2279     'reposerver' => $BSConfig::reposerver,
2280     'job' => $f,
2281     'arch' => $myarch,
2282     'srcmd5' => $srcmd5,
2283     'verifymd5' => $pdata->{'verifymd5'} || $srcmd5,
2284     'rev' => $pdata->{'rev'},
2285     'file' => $info->{'file'},
2286     'versrel' => $pdata->{'versrel'},
2287     'bcnt' => $h->{'bcnt'} + 1,
2288     'bdep' => \@bdeps,
2289     'path' => [{'project' => $projid, 'repository' => $repoid, 'server' => $BSConfig::reposerver}],
2290     'reason' => $reason->{'explain'},
2291   };
2292   mkdir_p("$reporoot/$prp/$myarch/$packid");
2293   writexml("$reporoot/$prp/$myarch/$packid/.status", "$reporoot/$prp/$myarch/$packid/status", { 'status' => 'scheduled', 'readytime' => $mytime, 'job' => $f}, $BSXML::buildstatus);
2294   $reason->{'time'} = $mytime;
2295   writexml("$reporoot/$prp/$myarch/$packid/.reason", "$reporoot/$prp/$myarch/$packid/reason", $reason, $BSXML::buildreason);
2296   writexml("$myjobsdir/$f:new", "$myjobsdir/$f", $binfo, $BSXML::buildinfo);
2297   return ('scheduled', $f);
2298 }
2299
2300 ##########################################################################
2301 ##########################################################################
2302 ##
2303 ##  aggregate package type handling
2304 ##
2305
2306 #
2307 # checkaggregate  - calculate package status of an aggregate package
2308 #
2309 # input:  $projid      - our project
2310 #         $repoid      - our repository
2311 #         $packid      - aggregate package
2312 #         $pdata       - package data information
2313 #         $prpfinished - reference to project finished marker hash
2314 # output: new package status
2315 #         package status details (new meta in 'scheduled' case)
2316 #
2317 # globals used: $projpacks
2318 #
2319 sub checkaggregate {
2320   my ($projid, $repoid, $packid, $pdata, $prpfinished) = @_;
2321
2322   my @aggregates = @{$pdata->{'aggregatelist'}->{'aggregate'} || []};
2323   my @broken;
2324   my @blocked;
2325   for my $aggregate (@aggregates) {
2326     my $aprojid = $aggregate->{'project'};
2327     my $proj = $remoteprojs{$aprojid} || $projpacks->{$aprojid};
2328     if (!$proj) {
2329       push @broken, $aprojid;
2330       next;
2331     }
2332     if ($remoteprojs{$aprojid} && !$aggregate->{'package'}) {
2333       # remote aggregates need packages, otherwise they are too
2334       # expensive
2335       push @broken, $aprojid;
2336       next;
2337     }
2338     my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$aggregate->{'repository'} || []};
2339     if (@arepoids) {
2340       @arepoids = map {$_->{'source'}} grep {exists($_->{'source'})} @arepoids;
2341     } else {
2342       @arepoids = ($repoid);
2343     }
2344     for my $arepoid (@arepoids) {
2345       my $arepo = (grep {$_->{'name'} eq $arepoid} @{$proj->{'repository'} || []})[0];
2346       if (!$arepo || !grep {$_ eq $myarch} @{$arepo->{'arch'} || []}) {
2347         push @broken, "$aprojid/$arepoid";
2348         next;
2349       }
2350       push @blocked, "$aprojid/$arepoid" unless $remoteprojs{$aprojid} || $prpfinished->{"$aprojid/$arepoid"};
2351     }
2352   }
2353   if (@broken) {
2354     print "      - $packid (aggregate)\n";
2355     print "        broken (@broken)\n";
2356     return ('broken', 'missing repositories: '.join(', ', @broken));
2357   }
2358   if (@blocked) {
2359     print "      - $packid (aggregate)\n";
2360     print "        blocked (@blocked)\n";
2361     return ('blocked', join(', ', @blocked));
2362   }
2363   my @new_meta = ();
2364   my $error;
2365   for my $aggregate (@aggregates) {
2366     my $aprojid = $aggregate->{'project'};
2367     my @apackids;
2368     if ($aggregate->{'package'}) {
2369       @apackids = @{$aggregate->{'package'}};
2370     } else {
2371       @apackids = sort keys(%{$projpacks->{$aprojid}->{'package'} || {}});
2372     }
2373     my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$aggregate->{'repository'} || []};
2374     if (@arepoids) {
2375       @arepoids = map {$_->{'source'}} grep {exists($_->{'source'})} @arepoids;
2376     } else {
2377       @arepoids = ($repoid);
2378     }
2379     for my $arepoid (@arepoids) {
2380       for my $apackid (@apackids) {
2381         my $m = '';
2382         if ($remoteprojs{$aprojid}) {
2383           print "fetching remote binary data for $aprojid/$arepoid/$myarch/$apackid\n";
2384           my $param = {
2385             'uri' => "$remoteprojs{$aprojid}->{'remoteurl'}/build/$remoteprojs{$aprojid}->{'remoteproject'}/$arepoid/$myarch/$apackid",
2386             'timeout' => 20,
2387           };
2388           my $binarylist;
2389           eval {
2390             $binarylist = BSRPC::rpc($param, $BSXML::binarylist);
2391           };
2392           if ($@) {
2393             warn($@);
2394             $error = $@;
2395             $error =~ s/\n$//s;
2396             addretryevent({'type' => 'repository', 'project' => $aprojid, 'repository' => $arepoid, 'arch' => $myarch}) if $error !~ /^remote error:/;
2397             last;
2398           }
2399           for my $binary (@{$binarylist->{'binary'} || []}) {
2400             $m .= "$binary->{'filename'}\0$binary->{'mtime'}/$binary->{'size'}/0\0";
2401           }
2402         } else {
2403           my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
2404           my @d = grep {/\.(?:rpm|deb)$/} ls($d);
2405           for my $b (sort @d) {
2406             my @s = stat("$d/$b");
2407             next unless @s;
2408             $m .= "$b\0$s[9]/$s[7]/$s[1]\0";
2409           }
2410         }
2411         $m = Digest::MD5::md5_hex($m)."  $aprojid/$arepoid/$myarch/$apackid";
2412         push @new_meta, $m;
2413       }
2414       last if $error;
2415     }
2416     last if $error;
2417   }
2418   if ($error) {
2419     # leave old rpms
2420     print "      - $packid (aggregate)\n";
2421     print "        $error\n";
2422     return ('done');
2423   }
2424   my @meta;
2425   if (open(F, '<', "$reporoot/$projid/$repoid/$myarch/:meta/$packid")) {
2426     @meta = <F>;
2427     close F;
2428     chomp @meta;
2429   }
2430   if (join('\n', @meta) eq join('\n', @new_meta)) {
2431     print "      - $packid (aggregate)\n";
2432     print "        nothing changed\n";
2433     return ('done');
2434   }
2435   my @diff = diffsortedmd5(0, \@meta, \@new_meta);
2436   print "      - $packid (aggregate)\n";
2437   print "        $_\n" for @diff;
2438   my $new_meta = join('', map {"$_\n"} @new_meta);
2439   return ('scheduled', $new_meta);
2440 }
2441
2442 ## -> repserver
2443 sub resign {
2444   my ($projid, @bins) = @_;
2445   return unless $BSConfig::sign;
2446   my @signargs;
2447   push @signargs, '--project', $projid if $BSConfig::sign_project;
2448   my $signkey;
2449   while(1) {
2450     eval {
2451       $signkey = BSRPC::rpc("$BSConfig::srcserver/getsignkey", undef, "project=$projid");
2452     };
2453     if ($@) {
2454       warn($@);
2455       sleep(10);
2456       next;
2457     }
2458     last;
2459   }
2460   if ($signkey) {
2461     mkdir_p("$uploaddir");
2462     writestr("$uploaddir/sched.$$", undef, $signkey);
2463     push @signargs, '-P', "$uploaddir/sched.$$";
2464   }
2465   for my $bin (@bins) {
2466     next unless $bin =~ /.rpm$/;
2467     system('rpm', '--delsign', $bin) && warn("delsign failed\n");
2468     system($BSConfig::sign, @signargs, '-r', $bin) && warn("sign failed\n");
2469   }
2470   unlink("$uploaddir/sched.$$") if $signkey;
2471 }
2472
2473 #
2474 # rebuildaggregate  - copy packages from other projects to rebuild an
2475 #                     aggregate
2476 #
2477 # input:  $projid    - our project
2478 #         $repoid    - our repository
2479 #         $packid    - aggregate package
2480 #         $pdata     - package data information
2481 #         $repodatas - reference of global repodata hash
2482 #         $changed   - reference to changed hash, mark prp if
2483 #                      we changed the repository
2484 #         $new_meta  - the new meta file data
2485 # output: new package status
2486 #         package status details
2487 #
2488 # globals used: $projpacks
2489 #
2490 sub rebuildaggregate {
2491   my ($projid, $repoid, $packid, $pdata, $repodatas, $changed, $new_meta) = @_;
2492
2493   my $prp = "$projid/$repoid";
2494   my @aggregates = @{$pdata->{'aggregatelist'}->{'aggregate'} || []};
2495   my $job = jobname($prp, $packid);
2496   my $jobdatadir = "$myjobsdir/$job:dir";
2497   unlink "$jobdatadir/$_" for ls($jobdatadir);
2498   mkdir_p($jobdatadir);
2499   my $jobrepo = {};
2500   my %jobbins;
2501   my $error;
2502   for my $aggregate (@aggregates) {
2503     my $aprojid = $aggregate->{'project'};
2504     my @arepoids = grep {!exists($_->{'target'}) || $_->{'target'} eq $repoid} @{$aggregate->{'repository'} || []};
2505     if (@arepoids) {
2506       @arepoids = map {$_->{'source'}} grep {exists($_->{'source'})} @arepoids;
2507     } else {
2508       @arepoids = ($repoid);
2509     }
2510     my @apackids;
2511     if ($aggregate->{'package'}) {
2512       @apackids = @{$aggregate->{'package'}};
2513     } else {
2514       @apackids = sort keys(%{$projpacks->{$aprojid}->{'package'} || {}});
2515     }
2516     my $abinfilter;
2517     $abinfilter = { map {$_ => 1} @{$aggregate->{'binary'}} } if $aggregate->{'binary'};
2518     for my $arepoid (reverse @arepoids) {
2519       for my $apackid (@apackids) {
2520         my @d;
2521         my $cpio;
2522         if ($remoteprojs{$aprojid}) {
2523           my $param = {
2524             'uri' => "$remoteprojs{$aprojid}->{'remoteurl'}/build/$remoteprojs{$aprojid}->{'remoteproject'}/$arepoid/$myarch/$apackid",
2525             'receiver' => \&BSHTTP::cpio_receiver,
2526             'directory' => $jobdatadir,
2527             'map' => "upload:",
2528             'timeout' => 300,
2529           };
2530           eval {
2531             $cpio = BSRPC::rpc($param, undef, "view=cpio");
2532           };
2533           if ($@) {
2534             warn($@);
2535             $error = $@;
2536             $error =~ s/\n$//s;
2537             addretryevent({'type' => 'repository', 'project' => $aprojid, 'repository' => $arepoid, 'arch' => $myarch}) if $error !~ /^remote error:/;
2538             last;
2539           }
2540           for my $bin (@{$cpio || []}) {
2541             push @d, "$jobdatadir/$bin->{'name'}";
2542           }
2543         } else {
2544           my $d = "$reporoot/$aprojid/$arepoid/$myarch/$apackid";
2545           @d = grep {/\.(?:rpm|deb)$/} ls($d);
2546           @d = map {"$d/$_"} sort(@d);
2547         }
2548         my $ajobrepo = findbins_dir(\@d);
2549         my $copysources = 0;
2550         for my $abin (sort keys %$ajobrepo) {
2551           my $r = $ajobrepo->{$abin};
2552           next unless $r->{'source'};
2553           next if $abinfilter && !$abinfilter->{$r->{'name'}};
2554           next if $jobbins{$r->{'name'}};
2555           $jobbins{$r->{'name'}} = 1;
2556           my $basename = $abin;
2557           $basename =~ s/.*\///;
2558           $basename =~ s/^upload:// if $cpio;
2559           BSUtil::cp($abin, "$jobdatadir/$basename");
2560           $jobrepo->{"$jobdatadir/$basename"} = $r;
2561           $copysources = 1;
2562         }
2563         if ($copysources) {
2564           for my $abin (sort keys %$ajobrepo) {
2565             my $r = $ajobrepo->{$abin};
2566             next if $r->{'source'};
2567             my $basename = $abin;
2568             $basename =~ s/.*\///;
2569             $basename =~ s/^upload:// if $cpio;
2570             BSUtil::cp($abin, "$jobdatadir/$basename");
2571             $jobrepo->{"$jobdatadir/$basename"} = $r;
2572           }
2573         }
2574         for my $bin (@{$cpio || []}) {
2575           unlink("$jobdatadir/$bin->{'name'}");
2576         }
2577       }
2578       last if $error;
2579     }
2580     last if $error;
2581   }
2582   if ($error) {
2583     print "        $error\n";
2584     return ('failed', $error);
2585   }
2586   writestr("$jobdatadir/meta", undef, $new_meta);
2587
2588   resign($projid, sort keys %$jobrepo) if $BSConfig::sign;
2589
2590   # now commit job into build area (and full tree if enabled)
2591   my $gdst = "$reporoot/$prp/$myarch";
2592   my $dst = "$gdst/$packid";
2593   mkdir_p($dst);
2594   $repodatas->{$prp} ||= findbins($prp);
2595   my $repodata = $repodatas->{$prp};
2596   my $useforbuildenabled = 1;
2597   $useforbuildenabled = enabled($repoid, $projpacks->{$projid}->{'useforbuild'}, $useforbuildenabled);
2598   $useforbuildenabled = enabled($repoid, $pdata->{'useforbuild'}, $useforbuildenabled);
2599   update_dst_full($repodata, $prp, $dst, $jobdatadir, undef, $useforbuildenabled, $prpsearchpath{$prp});
2600   $changed->{$prp} = 2 if $useforbuildenabled;
2601   delete $repounchanged{$prp} if $useforbuildenabled;
2602   $changed->{$prp} ||= 1;
2603   unlink("$gdst/:logfiles.fail/$packid");
2604   unlink("$gdst/:logfiles.success/$packid");
2605   unlink("$dst/logfile");
2606   unlink("$dst/status");
2607   mkdir_p("$gdst/:meta");
2608   rename("$jobdatadir/meta", "$gdst/:meta/$packid") || die("rename $jobdatadir/meta $gdst/:meta/$packid: $!\n");
2609
2610   # commit done, clean up
2611   unlink "$jobdatadir/$_" for ls($jobdatadir);
2612   rmdir($jobdatadir);
2613
2614   print "        rebuilt\n";
2615   unlink("$reporoot/$prp/$myarch/:repodone");
2616   return ('succeeded');
2617 }
2618
2619 sub select_read {
2620   my ($timeout, @watchers) = @_;
2621   my @retrywatchers = grep {$_->{'retry'}} @watchers;
2622   if (@retrywatchers) {
2623     my $now = time();
2624     for (splice @retrywatchers) {
2625       if ($_->{'retry'} <= $now) {
2626         push @retrywatchers, $_;
2627         next;
2628       }
2629       $timeout = $_->{'retry'} - $now if !defined($timeout) || $_->{'retry'} - $now < $timeout;
2630     }
2631     return @retrywatchers if @retrywatchers;
2632     @watchers = grep {!$_->{'retry'}} @watchers;
2633   }
2634   while(1) {
2635     my $rin = '';
2636     for (@watchers) {
2637       vec($rin, fileno($_->{'socket'}), 1) = 1;
2638     }
2639     my $nfound = select($rin, undef, undef, $timeout);
2640     if (!defined($nfound) || $nfound == -1) {
2641       next if $! == POSIX::EINTR;
2642       die("select: $!\n");
2643     }
2644     return () if !$nfound && defined($timeout);
2645     die("select: $!\n") unless $nfound;
2646     @watchers = grep {vec($rin, fileno($_->{'socket'}), 1)} @watchers;
2647     die unless @watchers;
2648     return @watchers;
2649   }
2650 }
2651
2652 sub changed2lookat {
2653   my ($changed, $changed_high, $lookat_oobhigh, $lookat_oob, $lookat_next) = @_;
2654
2655   push @$lookat_oobhigh, grep {$changed_high->{$_}} sort keys %$changed;
2656   push @$lookat_oob, grep {!$changed_high->{$_}} sort keys %$changed;
2657   @$lookat_oobhigh = unify(@$lookat_oobhigh);
2658   @$lookat_oob = unify(@$lookat_oob);
2659   my %lookat_oobhigh = map {$_ => 1} @$lookat_oobhigh;
2660   @$lookat_oob = grep {!$lookat_oobhigh{$_}} @$lookat_oob;
2661   for my $prp (@prps) {
2662     if (!$changed->{$prp}) {
2663       # FIXME: aggregates do not use the full tree!
2664       next unless grep {$changed->{$_} && $changed->{$_} != 1} @{$prpdeps{$prp}};
2665     }
2666     $lookat_next->{$prp} = 1;
2667   }
2668   %$changed = ();
2669   %$changed_high = ();
2670 }
2671
2672 sub updaterelsyncmax {
2673   my ($prp, $arch, $new, $cleanup) = @_;
2674   local *F;
2675   BSUtil::lockopen(\*F, '+>>', "$reporoot/$prp/$arch/:relsync.max");
2676   my $relsyncmax;
2677   if (-s "$reporoot/$prp/$arch/:relsync.max") {
2678     eval { $relsyncmax = Storable::retrieve("$reporoot/$prp/$arch/:relsync.max"); };
2679     warn($@) if $@;
2680   }
2681   $relsyncmax ||= {};
2682   my $changed;
2683   for my $tag (keys %$new) {
2684     next if defined($relsyncmax->{$tag}) && $relsyncmax->{$tag} >= $new->{$tag};   
2685     $relsyncmax->{$tag} = $new->{$tag};
2686     $changed = 1;
2687   }
2688   if ($cleanup) {
2689     for (grep {!$new->{$_}} keys %$relsyncmax) {
2690       delete $relsyncmax->{$_};
2691       $changed = 1;
2692     }
2693   }
2694   if ($changed) {
2695     Storable::nstore($relsyncmax, "$reporoot/$prp/$arch/:relsync.max.new");
2696     rename("$reporoot/$prp/$arch/:relsync.max.new", "$reporoot/$prp/$arch/:relsync.max");
2697   }
2698   close(F);
2699   return $changed;
2700 }
2701
2702 ##########################################################################
2703 ##########################################################################
2704 ##
2705 ## Here comes the big loop
2706 ##
2707
2708 $| = 1;
2709 print "starting build service scheduler\n";
2710
2711 # get lock
2712 mkdir_p($rundir);
2713 open(RUNLOCK, '>>', "$rundir/bs_sched.$myarch.lock") || die("$rundir/bs_sched.$myarch.lock: $!\n");
2714 flock(RUNLOCK, LOCK_EX | LOCK_NB) || die("scheduler is already running for $myarch!\n");
2715 utime undef, undef, "$rundir/bs_sched.$myarch.lock";
2716
2717 # setup event mechanism
2718 for my $d ($eventdir, $myeventdir, $jobsdir, $myjobsdir, $infodir) {
2719   next if -d $d;
2720   mkdir($d) || die("$d: $!\n");
2721 }
2722 if (!-p "$myeventdir/.ping") {
2723   POSIX::mkfifo("$myeventdir/.ping", 0666) || die("$myeventdir/.ping: $!");
2724   chmod(0666, "$myeventdir/.ping");
2725 }
2726
2727 sysopen(PING, "$myeventdir/.ping", POSIX::O_RDWR) || die("$myeventdir/.ping: $!");
2728 #fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
2729
2730
2731 # changed: 1: something "local" changed, :full unchanged,
2732 #          2: the :full repo is changed
2733 # set all projects and prps to :full repo changed
2734 my %repodata;
2735 my %changed;
2736 my %changed_high;
2737 my %prpfinished;
2738 my %lastcheck;
2739
2740 my @lookat;             # not so important
2741 my %lookat_next;        # not so important, next series
2742 my @lookat_oob;         # do those first (out of band)
2743 my @lookat_oobhigh;     # do those really first so that our users are happy
2744
2745
2746 # read old state if present
2747 if (-s "$rundir/bs_sched.$myarch.state") {
2748   print "reading old state...\n";
2749   my $schedstate;
2750   eval {
2751     $schedstate = Storable::retrieve("$rundir/bs_sched.$myarch.state");
2752   };
2753   if ($@) {
2754     print "$@";
2755     undef $schedstate;
2756   }
2757   unlink("$rundir/bs_sched.$myarch.state");
2758   if ($schedstate) {
2759     # just for testing...
2760     print "  - $_\n" for sort keys %$schedstate;
2761     if ($schedstate->{'projpacks'}) {
2762       $projpacks = $schedstate->{'projpacks'};
2763       calc_projpacks_linked();
2764       calc_prps();
2765     } else {
2766       # get project and package information from src server
2767       get_projpacks();
2768     }
2769
2770     my %oldprps = map {$_ => 1} @{$schedstate->{'prps'} || []};
2771     my @newprps = grep {!$oldprps{$_}} @prps;
2772
2773     # update lookat arrays
2774     @lookat = @{$schedstate->{'lookat'} || []};
2775     @lookat_oob = @{$schedstate->{'lookat_oob'} || []};
2776     @lookat_oobhigh = @{$schedstate->{'lookat_oobhigh'} || []};
2777
2778     # update changed hash
2779     %changed = ();
2780     %changed_high = ();
2781     for my $prp (@newprps) {
2782       $changed{$prp} = 2;
2783       $changed{(split('/', $prp, 2))[0]} = 2;
2784     }
2785
2786     my $oldchanged = $schedstate->{'changed'} || {};
2787     my $oldchanged_high = $schedstate->{'changed_high'} || {};
2788     for my $projid (keys %$projpacks) {
2789       $changed{$projid} = $oldchanged->{$projid} if exists $oldchanged->{$projid};
2790       $changed_high{$projid} = $oldchanged_high->{$projid} if exists $oldchanged_high->{$projid};
2791     }
2792     for my $prp (@prps) {
2793       $changed{$prp} = $oldchanged->{$prp} if exists $oldchanged->{$prp};
2794       $changed_high{$prp} = $oldchanged_high->{$prp} if exists $oldchanged_high->{$prp};
2795     }
2796
2797     ## update repodata hash
2798     #my $oldrepodata = $schedstate->{'repodata'} || {};
2799     #for my $prp (@prps) {
2800     #  $repodata{$prp} = $oldrepodata->{$prp} if exists $oldrepodata->{$prp};
2801     #}
2802
2803     # update prpfinished hash
2804     my $oldprpfinished = $schedstate->{'prpfinished'} || {};
2805     for my $prp (@prps) {
2806       $prpfinished{$prp} = $oldprpfinished->{$prp} if exists $oldprpfinished->{$prp};
2807     }
2808
2809     # update globalnotready hash
2810     my $oldglobalnotready = $schedstate->{'globalnotready'} || {};
2811     for my $prp (@prps) {
2812       $globalnotready{$prp} = $oldglobalnotready->{$prp} if exists $oldglobalnotready->{$prp};
2813     }
2814   }
2815 }
2816
2817 if (!$projpacks) {
2818   # get project and package information from src server
2819   print "cold start, scanning all projects\n";
2820   get_projpacks();
2821   # look at everything
2822   @lookat = sort keys %$projpacks;
2823   push @lookat, @prps;
2824 }
2825
2826 my %remotewatchers;
2827 my %nextmed;
2828
2829 my %prpchecktimes;
2830 my %notreadies;
2831
2832 if (@lookat) {
2833   %lookat_next = map {$_ => 1} @lookat;
2834   @lookat = ();
2835 }
2836
2837 my $slept = 0;
2838 my $schedulerstart = time();
2839
2840 while(1) {
2841   if (%changed) {
2842     changed2lookat(\%changed, \%changed_high, \@lookat_oobhigh, \@lookat_oob, \%lookat_next);
2843     next;
2844   }
2845
2846   for my $remoteurl (sort keys %remotewatchers) {
2847     my $watcher = $remotewatchers{$remoteurl};
2848     if (!$watchremote{$remoteurl}) {
2849       close $watcher->{'socket'};
2850       delete $remotewatchers{$remoteurl};
2851       next;
2852     }
2853     my $watchlist = join("\0", sort keys %{$watchremote{$remoteurl}});
2854     if ($watchlist ne $watcher->{'watchlist'}) {
2855       close $watcher->{'socket'};
2856       delete $remotewatchers{$remoteurl};
2857       next;
2858     }
2859   }
2860
2861   for my $remoteurl (sort keys %watchremote) {
2862     if ($remotewatchers{$remoteurl}) {
2863       next;
2864     }
2865     if ($watchremote_start{$remoteurl}) {
2866       print "setting up watcher for $remoteurl, start=$watchremote_start{$remoteurl}\n";
2867     } else {
2868       print "setting up watcher for $remoteurl\n";
2869     }
2870     my $watchlist = join("\0", sort keys %{$watchremote{$remoteurl}});
2871     my $param = {
2872       'uri' => "$remoteurl/lastevents",
2873       'async' => 1,
2874     };
2875     my @args = map {"filter=$_"} sort keys %{$watchremote{$remoteurl}};
2876     push @args, "start=$watchremote_start{$remoteurl}" if $watchremote_start{$remoteurl};
2877     my $ret;
2878     eval {
2879       $ret = BSRPC::rpc($param, $BSXML::events, @args);
2880     };
2881     if ($@) {
2882       warn($@);
2883       print "retrying in 60 seconds\n";
2884       $ret = {'retry' => time() + 60};
2885     }
2886     $ret->{'watchlist'} = $watchlist;
2887     $ret->{'remoteurl'} = $remoteurl;
2888     $remotewatchers{$remoteurl} = $ret;
2889   }
2890
2891   my $dummy;
2892   my $gotevent;
2893   my @remoteevents;
2894   my $pingsock = {
2895     'socket' => \*PING,
2896     'remoteurl' => 'ping',
2897   };
2898   if (@retryevents) {
2899     my $now = time();
2900     @remoteevents = grep {$_->{'retry'} <= $now} @retryevents;
2901     if (@remoteevents) {
2902       @retryevents = grep {$_->{'retry'} > $now} @retryevents;
2903       delete $_->{'retry'} for @remoteevents;
2904       $gotevent = 1;
2905       print "retrying ".@remoteevents." events\n";
2906     }
2907   }