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