[backend] define armvXl architecture but keep armvXel and hl for compatibility
[opensuse:build-service.git] / src / backend / bs_publish
1 #!/usr/bin/perl -w
2 #
3 # Copyright (c) 2006, 2007 Michael Schroeder, Novell Inc.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as
7 # published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program (see the file COPYING); if not, write to the
16 # Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18 #
19 ################################################################
20 #
21 # The Publisher. Create repositories and push them to our mirrors.
22 #
23
24 BEGIN {
25   my ($wd) = $0 =~ m-(.*)/- ;
26   $wd ||= '.';
27   unshift @INC,  "$wd/build";
28   unshift @INC,  "$wd";
29 }
30
31 use Digest;
32 use Digest::MD5 ();
33 use XML::Structured ':bytes';
34 use POSIX;
35 use Fcntl qw(:DEFAULT :flock);
36 use Data::Dumper;
37 use Storable ();
38
39 use BSConfig;
40 use BSRPC;
41 use BSUtil;
42 use BSDBIndex;
43 use Build;
44 use BSDB;
45 use BSXML;
46 use BSNotify;
47
48 use strict;
49
50 BSUtil::drop_privs_to($BSConfig::bsuser, $BSConfig::bsgroup);
51
52 my $reporoot = "$BSConfig::bsdir/build";
53 my $eventdir = "$BSConfig::bsdir/events";
54 my $extrepodir = "$BSConfig::bsdir/repos";
55 my $extrepodir_sync = "$BSConfig::bsdir/repos_sync";
56 my $uploaddir = "$BSConfig::bsdir/upload";
57 my $rundir = $BSConfig::rundir || "$BSConfig::bsdir/run";
58
59 my $extrepodb = "$BSConfig::bsdir/db/published";
60
61 my $myeventdir = "$eventdir/publish";
62
63 sub qsystem {
64   my @args = @_;
65   my $pid;
66   local (*RH, *WH);
67   if ($args[0] eq 'echo') {
68     pipe(RH, WH) || die("pipe: $!\n");
69   }
70   if (!($pid = xfork())) {
71     if ($args[0] eq 'echo') {
72       close WH;
73       open(STDIN, "<&RH");
74       close RH;
75       splice(@args, 0, 2);
76     }
77     open(STDOUT, ">/dev/null");
78     if ($args[0] eq 'chdir') {
79       chdir($args[1]) || die("chdir $args[1]: $!\n");
80       splice(@args, 0, 2);
81     }
82     if ($args[0] eq 'stdout') {
83       open(STDOUT, '>', $args[1]) || die("$args[1]: $!\n");
84       splice(@args, 0, 2);
85     }
86     eval {
87       exec(@args);
88       die("$args[0]: $!\n");
89     };
90     warn($@) if $@;
91     exit 1;
92   }
93   if ($args[0] eq 'echo') {
94     close RH;
95     print WH $args[1];
96     close WH;
97   }
98   waitpid($pid, 0) == $pid || die("waitpid $pid: $!\n");
99   return $?;
100 }
101
102 sub fillpkgdescription {
103   my ($pkg, $extrep, $repoinfo, $name) = @_;
104   my $binaryorigins = $repoinfo->{'binaryorigins'} || {};
105   my $hit;
106   for my $p (sort keys %$binaryorigins) {
107     next if $p =~ /src\.rpm$/;
108     next unless $p =~ /\/\Q$name\E/;
109     my ($pa, $pn) = split('/', $p, 2);
110     if ($pn =~ /^\Q$name\E-([^-]+-[^-]+)\.[^\.]+\.rpm$/) {
111       $hit = $p;
112       last;
113     }
114     if ($pn =~ /^\Q$name\E_([^_]+)_[^_]+\.deb$/) {
115       $hit = $p;
116       last;
117     }
118   }
119   return unless $hit;
120   my $data = Build::query("$extrep/$hit", 'description' => 1);
121   $pkg->{'description'} = str2utf8($data->{'description'});
122   $pkg->{'summary'} = str2utf8($data->{'summary'}) if defined $data->{'summary'};
123 }
124
125
126 ############################################################################################
127
128 #sub db_open {
129 #  my ($name) = @_;
130 #  return undef unless $extrepodb;
131 #  return undef if $name eq 'repoinfo';
132 #  mkdir_p($extrepodb) unless -d $extrepodb;
133 #  return BSDB::opendb($extrepodb, $name);
134 #}
135 #
136 #sub db_updateindex_rel {
137 #  my ($db, $rem, $add) = @_;
138 #  $db->updateindex_rel($rem, $add);
139 #}
140 #
141 #sub db_store {
142 #  my ($db, $k, $v) = @_;
143 #  $db->store($k, $v);
144 #}
145 #
146 #sub db_sync {
147 #}
148
149
150 my @db_sync;
151 my $db_oldsync_read;
152
153 sub db_open {
154   my ($name) = @_;
155
156   return undef unless $extrepodb;
157   if (!$db_oldsync_read) {
158     if (-s "$extrepodb.sync") {
159       my $oldsync = Storable::retrieve("$extrepodb.sync");
160       @db_sync = @{$oldsync || []};
161     }
162     $db_oldsync_read = 1;
163   }
164   return {'name' => $name, 'index' => "$name/"};
165 }
166
167 sub db_updateindex_rel {
168   my ($db, $rem, $add) = @_;
169   push @db_sync, $db->{'name'}, $rem, $add;
170 }
171
172 sub db_store {
173   my ($db, $k, $v) = @_;
174   push @db_sync, $db->{'name'}, $k, $v;
175 }
176
177 sub db_sync {
178   return undef unless $extrepodb;
179   db_open('') unless $db_oldsync_read;
180   return unless @db_sync;
181   my $data = Storable::nfreeze(\@db_sync);
182   my $param = {
183     'uri' => "$BSConfig::srcserver/search/published",
184     'request' => 'POST',
185     'maxredirects' => 3,
186     'timeout' => 60,
187     'headers' => [ 'Content-Type: application/octet-stream' ],
188     'data' => $data,
189   };
190   print "    syncing database\n";
191   eval {
192     BSRPC::rpc($param, undef, 'cmd=updatedb');
193   };
194   if ($@) {
195     warn($@);
196     mkdir_p($1) if $extrepodb =~ /^(.*)\//;
197     Storable::nstore(\@db_sync, "$extrepodb.sync.new");
198     rename("$extrepodb.sync.new", "$extrepodb.sync") || die("rename $extrepodb.sync.new $extrepodb.sync: $!\n");
199   } else {
200     @db_sync = ();
201     unlink("$extrepodb.sync");
202   }
203 }
204
205 ############################################################################################
206
207 sub updatebinaryindex {
208   my ($db, $keyrem, $keyadd) = @_;
209
210   my $index = $db->{'index'};
211   $index =~ s/\/$//;
212   my @add;
213   for my $key (@{$keyadd || []}) {
214     my $n;
215     if ($key =~ /(?:^|\/)([^\/]+)-[^-]+-[^-]+\.[a-zA-Z][^\/\.\-]*\.rpm$/) {
216       $n = $1;
217     } elsif ($key =~ /(?:^|\/)([^\/]+)_([^\/]*)_[^\/]*\.deb$/) {
218       $n = $1;
219     } else {
220       next;
221     }
222     push @add, ["$index/name", $n, $key];
223   }
224   my @rem;
225   for my $key (@{$keyrem || []}) {
226     my $n;
227     if ($key =~ /(?:^|\/)([^\/]+)-[^-]+-[^-]+\.[a-zA-Z][^\/\.\-]*\.rpm$/) {
228       $n = $1;
229     } elsif ($key =~ /(?:^|\/)([^\/]+)_([^\/]*)_[^\/]*\.deb$/) {
230       $n = $1;
231     } else {
232       next;
233     }
234     push @rem, ["$index/name", $n, $key];
235   }
236   db_updateindex_rel($db, \@rem, \@add);
237 }
238
239
240 ##########################################################################
241
242 sub getpatterns {
243   my ($projid) = @_;
244
245   my $dir;
246   eval {
247     $dir = BSRPC::rpc("$BSConfig::srcserver/source/$projid/_pattern", $BSXML::dir);
248   };
249   if ($@) {
250     warn($@);
251     return [];
252   }
253   my @ret;
254   my @args;
255   push @args, "rev=$dir->{'srcmd5'}" if $dir->{'srcmd5'} && $dir->{'srcmd5'} ne 'pattern';
256   for my $entry (@{$dir->{'entry'} || []}) {
257     my $pat;
258     eval {
259       $pat = BSRPC::rpc("$BSConfig::srcserver/source/$projid/_pattern/$entry->{'name'}", undef, @args);
260       # only patterns we can parse, please
261       XMLin($BSXML::pattern, $pat);
262     };
263     if ($@) {
264       warn("   pattern $entry->{'name'}: $@");
265       next;
266     }
267     push @ret, {'name' => $entry->{'name'}, 'md5' => $entry->{'md5'}, 'data' => $pat};
268   }
269   print "    fetched ".@ret." patterns\n";
270   return \@ret;
271 }
272
273 ##########################################################################
274
275 sub addsizechecksum {
276   my ($filename, $d, $sum) = @_;
277
278   local *F;
279   open(F, '<', $filename) || return;
280   $d->{'size'} = -s F;
281   my %known = (
282     'sha' => 'SHA-1',
283     'sha1' => 'SHA-1',
284     'sha256' => 'SHA-256',
285   );
286   if ($known{$sum}) {
287     my $ctx = Digest->new($known{$sum});
288     $ctx->addfile(\*F);
289     $d->{'checksum'} = {'type' => $sum, '_content' => $ctx->hexdigest()};
290   }
291   close F;
292 }
293
294 sub createrepo_rpmmd {
295   my ($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $options, $updateinfos, $deltainfos) = @_;
296
297   my %options = map {$_ => 1} @{$options || []};
298   my $obsname = $BSConfig::obsname || 'build.opensuse.org';
299   my $repotag = "obsrepository://$obsname/$projid/$repoid";
300   my $prp_ext = "$projid/$repoid";
301   $prp_ext =~ s/:/:\//g;
302   print "    running createrepo\n";
303   # cleanup files
304   unlink("$extrep/repodata/repomd.xml.asc");
305   unlink("$extrep/repodata/repomd.xml.key");
306   unlink("$extrep/repodata/latest-feed.xml");
307   unlink("$extrep/repodata/index.html");
308   my @oldrepodata = ls("$extrep/repodata");
309   qsystem('rm', '-rf', "$extrep/repodata/repoview") if -d "$extrep/repodata/repoview";
310   qsystem('rm', '-rf', "$extrep/repodata/.olddata") if -d "$extrep/repodata/.olddata";
311   qsystem('rm', '-f', "$extrep/repodata/patterns*");
312
313   # create generic rpm-md meta data
314   # --update requires a newer createrepo version, tested with version 0.4.10
315   my @createrepoargs;
316   push @createrepoargs, '--changelog-limit', '20';
317   push @createrepoargs, '--repo', $repotag;
318   my @legacyargs;
319   push @legacyargs, '--simple-md-filenames', '--checksum=sha' if $options{'legacy'};
320   my @updateargs;
321   if (-f "$extrep/repodata/repomd.xml") {
322     push @updateargs, '--update';
323   }
324   if (qsystem('createrepo', '-q', '-c', "$extrep/repocache", @updateargs, @createrepoargs, @legacyargs, $extrep)) {
325     print("    createrepo failed: $?\n");
326     if (@updateargs) {
327       print "    re-running without extra options\n";
328       qsystem('createrepo', '-q', '-c', "$extrep/repocache", @createrepoargs, @legacyargs, $extrep) && print("    createrepo failed again: $?\n");
329     }
330   }
331   unlink("$extrep/repodata/$_") for grep {/updateinfo\.xml/} @oldrepodata;
332   if ($updateinfos && @$updateinfos) {
333     print "    adding updateinfo.xml to repodata\n";
334     writexml("$extrep/repodata/updateinfo.xml", undef, {'update' => $updateinfos}, $BSXML::updateinfo);
335     qsystem('modifyrepo', "$extrep/repodata/updateinfo.xml", "$extrep/repodata") && print("    modifyrepo failed: $?\n");
336     unlink("$extrep/repodata/updateinfo.xml");
337   }
338   unlink("$extrep/repodata/$_") for grep {/(?:deltainfo|prestodelta)\.xml/} @oldrepodata;
339   if ($deltainfos && %$deltainfos && ($options{'deltainfo'} || $options{'prestodelta'})) {
340     print "    adding deltainfo.xml to repodata\n" if $options{'deltainfo'};
341     print "    adding prestodelta.xml to repodata\n" if $options{'prestodelta'};
342     # thinks are a bit complex, as we have to merge the deltas, and we also have to add the checksum
343     my %mergeddeltas;
344     for my $d (values(%$deltainfos)) {
345       addsizechecksum("$extrep/$d->{'delta'}->[0]->{'filename'}", $d->{'delta'}->[0], @legacyargs ? 'sha' : 'sha256');
346       my $mkey = "$d->{'arch'}\0$d->{'name'}\0$d->{'epoch'}\0$d->{'version'}\0$d->{'release'}\0";
347       if ($mergeddeltas{$mkey}) {
348         push @{$mergeddeltas{$mkey}->{'delta'}}, $d->{'delta'}->[0];
349       } else {
350         $mergeddeltas{$mkey} = $d;
351       }
352     }
353     # got all, now write
354     my @mergeddeltas = map {$mergeddeltas{$_}} sort keys %mergeddeltas;
355     if ($options{'deltainfo'}) {
356       writexml("$extrep/repodata/deltainfo.xml", undef, {'newpackage' => \@mergeddeltas}, $BSXML::deltainfo);
357       qsystem('modifyrepo', "$extrep/repodata/deltainfo.xml", "$extrep/repodata") && print("    modifyrepo failed: $?\n");
358       unlink("$extrep/repodata/deltainfo.xml");
359     }
360     if ($options{'prestodelta'}) {
361       writexml("$extrep/repodata/prestodelta.xml", undef, {'newpackage' => \@mergeddeltas}, $BSXML::prestodelta);
362       qsystem('modifyrepo', "$extrep/repodata/prestodelta.xml", "$extrep/repodata") && print("    modifyrepo failed: $?\n");
363       unlink("$extrep/repodata/prestodelta.xml");
364     }
365   }
366   if (-d "$extrep/repocache") {
367     my $now = time;
368     for (map { "$extrep/repocache/$_" } ls("$extrep/repocache")) {
369       my @s = stat($_);
370       unlink($_) if @s && $s[9] < $now - 7*86400;
371     }
372   }
373   if (-x "/usr/bin/repoview") {
374     if ($BSConfig::repodownload) {
375       print "    running repoview\n";
376       qsystem('repoview', '-f', "-u$BSConfig::repodownload/$prp_ext", "-t$repoinfo->{'title'}", $extrep) && print("   repoview failed: $?\n");
377     } else {
378       print "    running repoview\n";
379       qsystem('repoview', '-f', "-t$repoinfo->{'title'}", $extrep) && print("   repoview failed: $?\n");
380     }
381   }
382   if ($BSConfig::sign && -e "$extrep/repodata/repomd.xml") {
383     my @signargs;
384     push @signargs, '--project', $projid if $BSConfig::sign_project;
385     push @signargs, @$signargs;
386     qsystem($BSConfig::sign, @signargs, '-d', "$extrep/repodata/repomd.xml") && print("    sign failed: $?\n");
387     writestr("$extrep/repodata/repomd.xml.key", undef, $pubkey) if $pubkey;
388   }
389   if ($BSConfig::repodownload) {
390     local *FILE;
391     open(FILE, '>', "$extrep/$projid.repo$$") || die("$extrep/$projid.repo$$: $!\n");
392     my $projidHeader = $projid;
393     $projidHeader =~ s/:/_/g;
394     print FILE "[$projidHeader]\n";
395     print FILE "name=$repoinfo->{'title'}\n";
396     print FILE "type=rpm-md\n";
397     print FILE "baseurl=$BSConfig::repodownload/$prp_ext/\n";
398     print FILE "gpgcheck=1\n";
399     if (!@$signargs) {
400       print FILE "gpgkey=$BSConfig::gpg_standard_key\n" if ( defined($BSConfig::gpg_standard_key) );
401     } else {
402       print FILE "gpgkey=$BSConfig::repodownload/$prp_ext/repodata/repomd.xml.key\n";
403     }
404     print FILE "enabled=1\n";
405     close(FILE) || die("close: $!\n");
406     rename("$extrep/$projid.repo$$", "$extrep/$projid.repo") || die("rename $extrep/$projid.repo$$ $extrep/$projid.repo: $!\n");
407   }
408 }
409
410 sub deleterepo_rpmmd {
411   my ($extrep, $projid) = @_;
412
413   qsystem('rm', '-rf', "$extrep/repodata") if -d "$extrep/repodata";
414   unlink("$extrep/$projid.repo");
415 }
416
417 sub createrepo_susetags {
418   my ($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo) = @_;
419
420   mkdir_p("$extrep/media.1");
421   mkdir_p("$extrep/descr");
422   my @lt = localtime(time());
423   $lt[4] += 1;
424   $lt[5] += 1900;
425   my $str = sprintf("Open Build Service\n%04d%02d%02d%02d%02d%02d\n1\n", @lt[5,4,3,2,1,0]);
426   writestr("$extrep/media.1/.media", "$extrep/media.1/media", $str);
427   writestr("$extrep/media.1/.directory.yast", "$extrep/media.1/directory.yast", "media\n");
428   $str = <<"EOL";
429 PRODUCT Open Build Service $projid $repoid
430 VERSION 1.0-0
431 LABEL $repoinfo->{'title'}
432 VENDOR Open Build Service
433 ARCH.x86_64 x86_64 i686 i586 i486 i386 noarch
434 ARCH.ppc64 ppc64 ppc noarch
435 ARCH.ppc ppc noarch
436 ARCH.sh4 sh4 noarch
437 ARCH.armv4l arm       armv4l noarch
438 ARCH.armv5l arm armel armv4l armv5l armv5tel noarch
439 ARCH.armv6l arm armel armv4l armv5l armv5tel armv6l armv6vl noarch
440 ARCH.armv7l arm armel armv4l armv5l armv5tel armv6l armv6vl armv7l armv7hl noarch
441 ARCH.i686 i686 i586 i486 i386 noarch
442 ARCH.i586 i586 i486 i386 noarch
443 DEFAULTBASE i586
444 DESCRDIR descr
445 DATADIR .
446 EOL
447   writestr("$extrep/.content", "$extrep/content", $str);
448   print "    running create_package_descr\n";
449   qsystem('chdir', $extrep, 'create_package_descr', '-o', 'descr', '-x', '/dev/null') && print "    create_package_descr failed: $?\n";
450   unlink("$extrep/descr/directory.yast");
451   my @d = map {"$_\n"} sort(ls("$extrep/descr"));
452   writestr("$extrep/descr/.directory.yast", "$extrep/descr/directory.yast", join('', @d));
453 }
454
455 sub deleterepo_susetags {
456   my ($extrep) = @_;
457
458   unlink("$extrep/directory.yast");
459   unlink("$extrep/content");
460   unlink("$extrep/media.1/media");
461   unlink("$extrep/media.1/directory.yast");
462   rmdir("$extrep/media.1");
463   qsystem('rm', '-rf', "$extrep/descr") if -d "$extrep/descr";
464 }
465
466 sub createrepo_debian {
467   my ($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo) = @_;
468
469   unlink("$extrep/Packages");
470   unlink("$extrep/Packages.gz");
471   unlink("$extrep/Release");
472   unlink("$extrep/Release.gpg");
473   unlink("$extrep/Release.key");
474
475
476   print "    running dpkg-scanpackages\n";
477   qsystem('chdir', $extrep, 'stdout', 'Packages.new', 'dpkg-scanpackages', '-m', '.', '/dev/null') && print "    apt-ftparchive failed: $?\n";
478   if (-f "$extrep/Packages.new") {
479     link("$extrep/Packages.new", "$extrep/Packages");
480     qsystem('gzip', '-9', '-f', "$extrep/Packages") && print "    gzip Packages failed: $?\n";
481     unlink("$extrep/Packages");
482     rename("$extrep/Packages.new", "$extrep/Packages");
483   }    
484
485   my $date = POSIX::ctime(time());
486   $date =~ s/\n//m;
487   my $str = <<"EOL";
488 Origin: Open Build Service $projid $repoid
489 Label: $repoinfo->{'title'}
490 Version: 0.00
491 Date: $date
492 Description: Open Build Service $projid $repoid
493 MD5Sum:
494 EOL
495
496   open(OUT,">$extrep/Release") || die("$extrep/Release: $!\n");
497   print OUT $str;
498   close(OUT) || die("close: $!\n");
499
500   open(OUT,">>$extrep/Release") || die("$extrep/Release: $!\n");
501   foreach my $f ( "Release", "Packages", "Packages.gz" ) {
502   
503     open(FILE,"<$extrep/$f") || die;
504     my @all = <FILE>;
505     close(FILE);
506     my $md5  = Digest::MD5::md5_hex(join("",@all));
507     my $size = (stat("$extrep/$f"))[7];
508     print OUT " $md5 $size $f\n";
509   
510   }
511   close(OUT) || die("close: $!\n");
512
513   # re-sign changed Release file
514   if ($BSConfig::sign && -e "$extrep/Release") {
515     my @signargs;
516     push @signargs, '--project', $projid if $BSConfig::sign_project;
517     push @signargs, @$signargs;
518     qsystem($BSConfig::sign, @signargs, '-d', "$extrep/Release") && print("    sign failed: $?\n");
519     rename("$extrep/Release.asc","$extrep/Release.gpg");
520   }
521   if ($BSConfig::sign) {
522     writestr("$extrep/Release.key", undef, $pubkey) if $pubkey;
523   }
524 }
525
526 sub deleterepo_debian {
527   my ($extrep) = @_;
528
529   unlink("$extrep/Packages");
530   unlink("$extrep/Packages.gz");
531   unlink("$extrep/Release");
532   unlink("$extrep/Release.gpg");
533   unlink("$extrep/Release.key");
534 }
535
536
537 ##########################################################################
538
539 sub createpatterns_rpmmd {
540   my ($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $patterns) = @_;
541
542   deletepatterns_rpmmd($extrep);
543   return unless @{$patterns || []};
544
545   # create patterns data structure
546   my @pats;
547   for my $pattern (@$patterns) {
548     push @pats, XMLin($BSXML::pattern, $pattern->{'data'});
549   }
550   print "    adding patterns to repodata\n";
551   my $pats = {'pattern' => \@pats, 'count' => scalar(@pats)};
552   writexml("$extrep/repodata/patterns.xml", undef, $pats, $BSXML::patterns);
553   qsystem('modifyrepo', "$extrep/repodata/patterns.xml", "$extrep/repodata") && print("    modifyrepo failed: $?\n");
554   unlink("$extrep/repodata/patterns.xml");
555
556 #  for my $pattern (@{$patterns || []}) {
557 #    my $pname = "patterns.$pattern->{'name'}";
558 #    $pname =~ s/\.xml$//;
559 #    print "    adding pattern $pattern->{'name'} to repodata\n";
560 #    writestr("$extrep/repodata/$pname.xml", undef, $pattern->{'data'});
561 #    qsystem('modifyrepo', "$extrep/repodata/$pname.xml", "$extrep/repodata") && print("    modifyrepo failed: $?\n");
562 #    unlink("$extrep/repodata/$pname.xml");
563 #  }
564
565   # re-sign changed repomd.xml file
566   if ($BSConfig::sign && -e "$extrep/repodata/repomd.xml") {
567     my @signargs;
568     push @signargs, '--project', $projid if $BSConfig::sign_project;
569     push @signargs, @$signargs;
570     qsystem($BSConfig::sign, @signargs, '-d', "$extrep/repodata/repomd.xml") && print("    sign failed: $?\n");
571   }
572 }
573
574 sub deletepatterns_rpmmd {
575   my ($extrep) = @_;
576   for my $pat (ls("$extrep/repodata")) {
577     next unless $pat =~ /^patterns/;
578     unlink("$extrep/repodata/$pat");
579   }
580 }
581
582 sub createpatterns_comps {
583   my ($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $patterns) = @_;
584
585   deletepatterns_comps($extrep);
586   return unless @{$patterns || []};
587
588   # create comps data structure
589   my @grps;
590   for my $pattern (@$patterns) {
591     my $pat = XMLin($BSXML::pattern, $pattern->{'data'});
592     my $grp = { 'id' => $pattern->{'name'} };
593     for (@{$pat->{'summary'}}) {
594       my $el = { '_content' => $_->{'_content'} };
595       $el->{'xml:lang'} = $_->{lang} if $_->{'lang'};
596       push @{$grp->{'name'}}, $el;
597     }
598     for (@{$pat->{'description'}}) {
599       my $el = { '_content' => $_->{'_content'} };
600       $el->{'xml:lang'} = $_->{'lang'} if $_->{'lang'};
601       push @{$grp->{'description'}}, $el;
602     }
603     for (@{$pat->{'rpm:requires'}->{'rpm:entry'}}) {
604       push @{$grp->{'packagelist'}->{'packagereq'} }, { '_content' => $_->{'name'}, 'type' => 'mandatory' };
605     }
606     for (@{$pat->{'rpm:recommends'}->{'rpm:entry'}}) {
607       push @{$grp->{'packagelist'}->{'packagereq'}},  { '_content' => $_->{'name'}, 'type' => 'default' };
608     }
609     for (@{$pat->{'rpm:suggests'}->{'rpm:entry'}}) {
610       push @{$grp->{'packagelist'}->{'packagereq'}},  { '_content' => $_->{'name'}, 'type' => 'optional' };
611     }
612     push @grps, $grp;
613   }
614   print "    adding comps to repodata\n";
615   my $comps = {'group' => \@grps};
616   writexml("$extrep/repodata/group.xml", undef, $comps, $BSXML::comps);
617   qsystem('modifyrepo', "$extrep/repodata/group.xml", "$extrep/repodata") && print("    modifyrepo failed: $?\n");
618   unlink("$extrep/repodata/group.xml");
619
620   # re-sign changed repomd.xml file
621   if ($BSConfig::sign && -e "$extrep/repodata/repomd.xml") {
622     my @signargs;
623     push @signargs, '--project', $projid if $BSConfig::sign_project;
624     push @signargs, @$signargs;
625     qsystem($BSConfig::sign, @signargs, '-d', "$extrep/repodata/repomd.xml") && print("    sign failed: $?\n");
626   }
627 }
628
629 sub deletepatterns_comps {
630   my ($extrep) = @_;
631   for my $pat (ls("$extrep/repodata")) {
632     next unless $pat =~ /group.xml/;
633     unlink("$extrep/repodata/$pat");
634   }
635 }
636
637
638 sub createpatterns_ymp {
639   my ($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $patterns) = @_;
640
641   deletepatterns_ymp($extrep, $projid, $repoid);
642   return unless @{$patterns || []};
643
644   my $prp_ext = "$projid/$repoid";
645   $prp_ext =~ s/:/:\//g;
646   my $patterndb = db_open('pattern');
647
648   # get title/description data for all involved projects
649   my %nprojpack;
650   my @nprojids = map {$_->{'project'}} @{$repoinfo->{'prpsearchpath'} || []};
651   if (@nprojids) {
652     my @args = map {"project=$_"} @nprojids;
653     my $nprojpack = BSRPC::rpc("$BSConfig::srcserver/getprojpack", $BSXML::projpack, 'nopackages', @args);
654     %nprojpack = map {$_->{'name'} => $_} @{$nprojpack->{'project'} || []};
655   }
656
657   for my $pattern (@$patterns) {
658     my $ympname = $pattern->{'name'};
659     $ympname =~ s/\.xml$//;
660     $ympname .= ".ymp";
661     my $pat = XMLin($BSXML::pattern, $pattern->{'data'});
662     next if !exists $pat->{'uservisible'};
663     print "    writing ymp for pattern $pat->{'name'}\n";
664     my $ymp = {};
665     $ymp->{'xmlns:os'} = 'http://opensuse.org/Standards/One_Click_Install';
666     $ymp->{'xmlns'} = 'http://opensuse.org/Standards/One_Click_Install';
667
668     my $group = {};
669     $group->{'name'} = $pat->{'name'};
670     if ($pat->{'summary'}) {
671       $group->{'summary'} = $pat->{'summary'}->[0]->{'_content'};
672     }    
673     if ($pat->{'description'}) {
674       $group->{'description'} = $pat->{'description'}->[0]->{'_content'};
675     }    
676     my @repos;
677     my @sprp = @{$repoinfo->{'prpsearchpath'} || []};
678     while (@sprp) {
679       my $sprp = shift @sprp;
680       my $sprojid = $sprp->{'project'};
681       my $srepoid = $sprp->{'repository'};
682       my $r = {};
683       $r->{'recommended'} = @sprp || !@repos ? 'true' : 'false';
684       $r->{'name'} = $sprojid;
685       if ($nprojpack{$sprojid}) {
686         $r->{'summary'} = $nprojpack{$sprojid}->{'title'};
687         $r->{'description'} = $nprojpack{$sprojid}->{'description'};
688       }
689       my $sprp_ext = "$sprojid/$srepoid";
690       if ($BSConfig::prp_ext_map && $BSConfig::prp_ext_map->{$sprp_ext}) {
691         $r->{'url'} = $BSConfig::prp_ext_map->{$sprp_ext};
692       } else {
693         $sprp_ext =~ s/:/:\//g;
694         $r->{'url'} = "$BSConfig::repodownload/$sprp_ext/";
695       }
696       push @repos, $r;
697     }
698     $group->{'repositories'} = {'repository' => \@repos };
699     my @software;
700     for my $entry (@{$pat->{'rpm:requires'}->{'rpm:entry'} || []}) {
701       next if $entry->{'kind'} && $entry->{'kind'} ne 'package';
702       push @software, {'name' => $entry->{'name'}, 'summary' => "The $entry->{'name'} package", 'description' => "The $entry->{'name'} package."};
703       fillpkgdescription($software[-1], "$extrepodir/$prp_ext", $repoinfo, $entry->{'name'});
704     }
705     for my $entry (@{$pat->{'rpm:recommends'}->{'rpm:entry'} || []}) {
706       next if $entry->{'kind'} && $entry->{'kind'} ne 'package';
707       push @software, {'name' => $entry->{'name'}, 'summary' => "The $entry->{'name'} package", 'description' => "The $entry->{'name'} package."};
708       fillpkgdescription($software[-1], "$extrepodir/$prp_ext", $repoinfo, $entry->{'name'});
709     }
710     for my $entry (@{$pat->{'rpm:suggests'}->{'rpm:entry'} || []}) {
711       next if $entry->{'kind'} && $entry->{'kind'} ne 'package';
712       push @software, {'recommended' => 'false', 'name' => $entry->{'name'}, 'summary' => "The $entry->{'name'} package", 'description' => "The $entry->{'name'} package."};
713       fillpkgdescription($software[-1], "$extrepodir/$prp_ext", $repoinfo, $entry->{'name'});
714     }
715     $group->{'software'} = { 'item' => \@software };
716     $ymp->{'group'} = [ $group ];
717     
718     writexml("$extrep/.$ympname", "$extrep/$ympname", $ymp, $BSXML::ymp);
719     
720     # write database entry
721     my $ympidx = {'type' => 'ymp'};
722     $ympidx->{'name'} = $pat->{'name'} if defined $pat->{'name'};
723     $ympidx->{'summary'} = $pat->{'summary'}->[0]->{'_content'} if $pat->{'summary'};;
724     $ympidx->{'description'} = $pat->{'description'}->[0]->{'_content'} if $pat->{'description'};
725     $ympidx->{'path'} = $repoinfo->{'prpsearchpath'} if $repoinfo->{'prpsearchpath'};
726     db_store($patterndb, "$prp_ext/$ympname", $ympidx) if $patterndb;
727   }
728 }
729
730 sub deletepatterns_ymp {
731   my ($extrep, $projid, $repoid) = @_;
732
733   my $prp_ext = "$projid/$repoid";
734   $prp_ext =~ s/:/:\//g;
735   my $patterndb = db_open('pattern');
736   for my $ympname (ls($extrep)) {
737     next unless $ympname =~ /\.ymp$/;
738     db_store($patterndb, "$prp_ext/$ympname", undef) if $patterndb;
739     unlink("$extrep/$ympname");
740   }
741 }
742
743 ##########################################################################
744
745 sub deleterepo {
746   my ($projid, $repoid) = @_;
747   print "    deleting repository\n";
748   my $projid_ext = $projid;
749   $projid_ext =~ s/:/:\//g;
750   my $prp = "$projid/$repoid";
751   my $prp_ext = $prp;
752   $prp_ext =~ s/:/:\//g;
753   my $extrep = "$extrepodir/$prp_ext";
754   if (! -d $extrep) {
755     rmdir("$extrepodir/$projid_ext");
756     print "    nothing to delete...\n";
757     unlink("$reporoot/$prp/:repoinfo");
758     rmdir("$reporoot/$prp");
759     return;
760   }
761   # delete all binaries
762   my @db_deleted;
763   for my $arch (ls($extrep)) {
764     next if $arch =~ /^\./;
765     next if $arch eq 'repodata' || $arch eq 'repocache' || $arch eq 'media.1' || $arch eq 'descr';
766     my $r = "$extrep/$arch";
767     next unless -d $r;
768     for my $bin (ls($r)) {
769       my $p = "$arch/$bin";
770       print "      - $p\n";
771       unlink("$r/$bin") || die("unlink $r/$bin: $!\n");
772       push @db_deleted, $p if $p =~ /\.(?:rpm|deb)$/;
773     }
774   }
775   # update repoinfo
776   unlink("$reporoot/$prp/:repoinfo");
777   rmdir("$reporoot/$prp");
778
779   # update published database
780   my $binarydb = db_open('binary');
781   updatebinaryindex($binarydb, [ map {"$prp_ext/$_"} @db_deleted ], []) if $binarydb;
782
783   if ($BSConfig::markfileorigins) {
784     for my $f (sort @db_deleted) {
785       my $req = {
786         'uri' => "$BSConfig::markfileorigins/$prp_ext/$f",
787         'request' => 'HEAD',
788         'maxredirects' => 3,
789         'timeout' => 10,
790         'ignorestatus' => 1,
791       };
792       eval {
793         BSRPC::rpc($req, undef, 'cmd=deleted');
794       };
795       print "      $f: $@" if $@;
796     }
797   }
798   # delete ymps so they get removed from the database
799   deletepatterns_ymp($extrep, $projid, $repoid);
800   # delete everything else
801   qsystem('rm', '-rf', $extrep);
802   if ($BSConfig::stageserver && $BSConfig::stageserver =~ /^rsync:\/\/([^\/]+)\/(.*)$/) {
803     print "    running rsync to $1 at ".localtime(time)."\n";
804     # rsync with a timeout of 1 hour
805     qsystem('echo', "$projid_ext\0", 'rsync', '-ar0', '--delete-after', '--exclude=repocache', '--delete-excluded', '--timeout', '7200', '--files-from=-', $extrepodir, "$1::$2") && die("    rsync failed at ".localtime(time).": $?\n");
806   }
807   # push done trigger sync to other mirrors
808   mkdir_p($extrepodir_sync);
809   writestr("$extrepodir_sync/.$$:$projid", "$extrepodir_sync/$projid", "$projid_ext\0");
810   if ($BSConfig::stageserver_sync && $BSConfig::stageserver_sync =~ /^rsync:\/\/([^\/]+)\/(.*)$/) {
811     print "    running trigger rsync to $1 at ".localtime(time)."\n";
812     # small sync, timout 1 minute
813     qsystem('rsync', '-a', '--timeout', '120', "$extrepodir_sync/$projid", "$1::$2/$projid") && warn("    trigger rsync failed at ".localtime(time).": $?\n");
814   }
815   rmdir("$extrepodir/$projid_ext");
816 }
817
818 sub publish {
819   my ($projid, $repoid) = @_;
820   my $prp = "$projid/$repoid";
821
822   print localtime(time)." publishing $prp\n";
823
824   # get info from source server about this project/repository
825   # we specify "withsrcmd5" so that we get the patternmd5. It still
826   # works with "nopackages".
827   my $projpack = BSRPC::rpc("$BSConfig::srcserver/getprojpack", $BSXML::projpack, 'withrepos', 'expandedrepos', 'withsrcmd5', 'nopackages', "project=$projid", "repository=$repoid");
828   if (!$projpack->{'project'}) {
829     # project is gone
830     deleterepo($projid, $repoid);
831     return;
832   }
833   my $proj = $projpack->{'project'}->[0];
834   die("no such project $projid\n") unless $proj && $proj->{'name'} eq $projid;
835   if (!$proj->{'repository'}) {
836     # repository is gone
837     deleterepo($projid, $repoid);
838     return;
839   }
840   my $repo = $proj->{'repository'}->[0];
841   die("no such repository $repoid\n") unless $repo && $repo->{'name'} eq $repoid;
842   # this is the already expanded path as we used 'expandedrepos' above
843   my $prpsearchpath = $repo->{'path'};
844
845   # we need the config for repotype/patterntype
846   my $config = BSRPC::rpc("$BSConfig::srcserver/getconfig", undef, "project=$projid", "repository=$repoid");
847   $config = Build::read_config('noarch', [ split("\n", $config) ]);
848   $config->{'repotype'} = [ 'rpm-md' ] unless @{$config->{'repotype'} || []};
849
850   # get us the lock
851   local *F;
852   open(F, '>', "$reporoot/$prp/.finishedlock") || die("$reporoot/$prp/.finishedlock: $!\n");
853   if (!flock(F, LOCK_EX | LOCK_NB)) {
854     print "    waiting for lock...\n";
855     flock(F, LOCK_EX) || die("flock: $!\n");
856     print "    got the lock...\n";
857   }
858
859   my $prp_ext = $prp;
860   $prp_ext =~ s/:/:\//g;
861   my $extrep = "$extrepodir/$prp_ext";
862
863   if ($BSConfig::publishredirect && exists($BSConfig::publishredirect->{$prp})) {
864     $extrep = $BSConfig::publishredirect->{$prp};
865     $prp_ext = undef;
866   }
867   
868   # we now know that $reporoot/$prp/*/:repo will not change.
869   # Build repo by mixing all architectures.
870   my @archs = @{$repo->{'arch'} || []};
871   my %bins;
872   my %bins_id;
873   my $binaryorigins = {};
874
875   my @updateinfos;
876   my $updateinfos_state;
877
878   my %deltas;   # XXX remove hack
879   my %deltainfos;
880   my $deltainfos_state;
881
882   for my $arch (@archs) {
883     my $r = "$reporoot/$prp/$arch/:repo";
884     my $repoinfo = {};
885     if (-s "${r}info") {
886       $repoinfo = Storable::retrieve("${r}info") || {};
887     }
888     $repoinfo->{'binaryorigins'} ||= {};
889     for my $rbin (sort(ls($r))) {
890       my $bin = $rbin;
891       if ($bin =~ /:updateinfo.xml$/) {
892         # collect updateinfo data
893         my $updateinfoxml = readstr("$r/$bin", 1) || '';
894         $updateinfos_state .= Digest::MD5::md5_hex($updateinfoxml);
895         my $updateinfo = readxml("$r/$bin", $BSXML::updateinfo, 1) || {};
896         push @updateinfos, @{$updateinfo->{'update'} || []};
897       }
898       if ($bin =~ /^(.*\.rpm)::(.*\.drpm)$/) {
899         # special drpm handling: only take it if we took the corresponding rpm
900         if ($bin =~ /^(.+-[^-]+-[^-]+\.([a-zA-Z][^\/\.\-]*)\.rpm)::(.*\.drpm)$/) {
901           if ($bins{"$2/$1"} eq "$r/$1") {
902             # ok, took it. also take delta
903             $bin = $3;
904             push @{$deltas{"$r/$1"}}, "$r/$rbin";
905           }
906         }
907       }
908       $bin =~ s/^.*?:://;       # strip package name for now
909       #next unless $bin =~ /\.(?:rpm|deb)$/;
910       my $p;
911       if ($bin =~ /^.+-[^-]+-[^-]+\.([a-zA-Z][^\/\.\-]*)\.d?rpm$/) {
912         $p = "$1/$bin";
913       } elsif ($bin =~ /^.+_[^_]+_([^_\.]+)\.deb$/) {
914         $p = "$1/$bin";
915       } elsif ($bin =~ /\.d?rpm$/) {
916         # legacy format
917         my $q = Build::query("$r/$rbin", 'evra' => 1);
918         next unless $q;
919         $p = "$q->{'arch'}/$q->{'name'}-$q->{'version'}-$q->{'release'}.$q->{'arch'}.rpm";
920       } elsif ($bin =~ /\.deb$/) {
921         # legacy format
922         my $q = Build::query("$r/$rbin", 'evra' => 1);
923         $p = "$q->{'arch'}/$q->{'name'}_$q->{'version'}";
924         $p .= "-$q->{'release'}" if defined $q->{'release'};
925         $p .= "_$q->{'arch'}.deb";
926       } else {
927         if ($bin =~ /\.iso(:?\.sha256)?$/) {
928           $p = "iso/$bin";
929         } elsif ($bin =~ /\.raw\.bz2(:?\.sha256)?$/) {
930           $p = "$bin";
931         } elsif ($bin =~ /\.raw(:?\.install)?(:?\.sha256)?$/) {
932           $p = "$bin";
933         } elsif ($bin =~ /\.tar\.(:?gz|bz2)(:?\.sha256)?$/) {
934           $p = "$bin";
935         } elsif ($bin =~ /\.ovf(:?\.sha256)?$/) {
936           $p = "$bin";
937         } elsif ($bin =~ /\.vmdk(:?\.sha256)?$/) {
938           $p = "$bin";
939         } elsif (-d "$r/$rbin") {
940           $p = "repo/$bin";
941         } else {
942           next;
943         }
944       }
945       next unless defined $p;
946       # next if $bins{$p}; # first arch wins
947       my @s = stat("$reporoot/$prp/$arch/:repo/$rbin");
948       next unless @s;
949       if ($bins{$p}) {
950         # keep old file (FIXME: should do this different)
951         my @s2 = stat("$extrep/$p");
952         next if !@s2 || "$s[9]/$s[7]/$s[1]" ne "$s2[9]/$s2[7]/$s2[1]";
953         # argh, kill taken deltas again
954         for my $d (@{$deltas{"$r/$rbin"} || []}) {
955           for my $dp (grep {$bins{$_} eq $d} keys %bins) {
956             delete $bins{$dp};
957             delete $bins_id{$dp};
958             delete $binaryorigins->{$dp};
959             delete $deltainfos{$dp};
960           }
961         }
962       }
963       $bins{$p} = "$r/$rbin";
964       $bins_id{$p} = "$s[9]/$s[7]/$s[1]";
965       $binaryorigins->{$p} = $repoinfo->{'binaryorigins'}->{$rbin} if defined $repoinfo->{'binaryorigins'}->{$rbin};
966       if ($rbin =~ /^(.*)\.drpm$/) {
967         # we took a delta rpm. collect desq if possible
968         my $dseq = "$r/$1.dseq";
969         if (-s $dseq) {
970           my %dseq;
971           for (split("\n", readstr($dseq, 1) || '')) {
972             $dseq{$1} = $2 if /^(.*?): (.*)$/s;
973           }
974           my @needed = qw{Name Epoch Version Release Arch OldName OldEpoch OldVersion OldRelease OldArch Seq};
975           if (!grep {!exists($dseq{$_})} @needed) {
976             # got all required fields. convert to correct data
977             my $dinfo = {'name' => $dseq{'Name'}, 'epoch' => $dseq{'Epoch'} || 0, 'version' => $dseq{'Version'}, 'release' => $dseq{'Release'}, 'arch' => $dseq{'Arch'}};
978             $dinfo->{'delta'} = [ {'oldepoch' => $dseq{'OldEpoch'} || 0, 'oldversion' => $dseq{'OldVersion'}, 'oldrelease' => $dseq{'OldRelease'}, 'filename' => $p, 'sequence' => $dseq{'Seq'}} ];
979             $deltainfos{$p} = $dinfo;
980           }
981         }
982       }
983     }
984   }
985
986   # calculate deltainfos_state
987   if (%deltainfos) {
988     $deltainfos_state = '';
989     for my $p (sort keys %deltainfos) {
990       my @s = stat($bins{$p});
991       my $id = "$s[9]/$s[7]/$s[1]";
992       if ($bins{$p} =~ /^(.*)\.drpm$/) {
993         @s = stat("$1.dseq");
994         $id .= "/$s[9]/$s[7]/$s[1]";
995       }
996       $deltainfos_state .= Digest::MD5::md5_hex($id);
997     }
998   }
999
1000   # now update external repository
1001   my $changed = 0;
1002
1003   my @db_deleted;       # for published db update
1004   my @db_changed;       # for published db update
1005   my @changed;          # All changed files for hooks.
1006
1007   my %bins_done;
1008   for my $arch (sort(ls($extrep))) {
1009     next if $arch =~ /^\./;
1010     next if $arch eq 'repodata' || $arch eq 'repocache' || $arch eq 'media.1' || $arch eq 'descr';
1011     next if $arch =~ /\.repo$/;
1012     my $r = "$extrep/$arch";
1013     if (-f $r) {
1014       $r = $extrep;
1015       my $bin = $arch;
1016       my $p = $arch;
1017       my @s = lstat("$r/$bin");
1018       if (!exists($bins{$p})) {
1019         print "      - $p\n";
1020         unlink("$r/$bin") || die("unlink $r/$bin: $!\n");
1021         push @db_deleted, $p if $p =~ /\.(?:rpm|deb)$/;
1022         $changed = 1;
1023         next;
1024       }
1025       if ("$s[9]/$s[7]/$s[1]" ne $bins_id{$p}) {
1026         unlink("$r/$bin") || die("unlink $r/$bin: $!\n");
1027         link($bins{$p}, "$r/$bin") || die("link $bins{$p} $r/$bin: $!\n");
1028         push @db_changed, $p if $p =~ /\.(?:rpm|deb)$/;
1029         push @changed, $p;
1030         $changed = 1;
1031       }
1032       $bins_done{$p} = 1;
1033       next;
1034     }
1035     next unless -d $r;
1036     for my $bin (sort(ls($r))) {
1037       my $p = "$arch/$bin";
1038       my @s = lstat("$r/$bin");
1039       die("$r/$bin: $!\n") unless @s;
1040       if (!exists($bins{$p})) {
1041         print "      - $p\n";
1042         if (-d _) {
1043           BSUtil::cleandir("$r/$bin");
1044           rmdir("$r/$bin") || die("rmdir $r/$bin: $!\n");
1045         } else {
1046           unlink("$r/$bin") || die("unlink $r/$bin: $!\n");
1047         }
1048         push @db_deleted, $p if $p =~ /\.(?:rpm|deb)$/;
1049         $changed = 1;
1050         next;
1051       }
1052       if ("$s[9]/$s[7]/$s[1]" ne $bins_id{$p}) {
1053         # changed, link over
1054         if (-d _) {
1055           if (! -l $bins{$p} && -d _) {
1056             # both are directories, compare info
1057             # should MIX instead?
1058             my $info1 = BSUtil::treeinfo($bins{$p});
1059             my $info2 = BSUtil::treeinfo("$r/$bin");
1060             if (join(',', @$info1) eq join(',', @$info2)) {
1061               $bins_done{$p} = 1;
1062               next;
1063             }
1064           }
1065           print "      ! $p\n";
1066           BSUtil::cleandir("$r/$bin");
1067           rmdir("$r/$bin") || die("rmdir $r/$bin: $!\n");
1068         } else {
1069           print "      ! $p\n";
1070           unlink("$r/$bin") || die("unlink $r/$bin: $!\n");
1071         }
1072         if (! -l $bins{$p} && -d _) {
1073           BSUtil::linktree($bins{$p}, "$r/$bin");
1074         } else {
1075           link($bins{$p}, "$r/$bin") || die("link $bins{$p} $r/$bin: $!\n");
1076         }
1077         push @db_changed, $p if $p =~ /\.(?:rpm|deb)$/;
1078         push @changed, $p;
1079         $changed = 1;
1080       }
1081       $bins_done{$p} = 1;
1082     }
1083   }
1084   for my $p (sort keys %bins) {
1085     next if $bins_done{$p};
1086     # a new one
1087     my ($arch, $bin) = split('/', $p, 2);
1088     if (!defined($bin)) {
1089       $arch = '.';
1090       $bin = $p;
1091     }
1092     my $r = "$extrep/$arch";
1093     mkdir_p($r) unless -d $r;
1094     print "      + $p\n";
1095     if (! -l $bins{$p} && -d _) {
1096       BSUtil::linktree($bins{$p}, "$r/$bin");
1097     } else {
1098       link($bins{$p}, "$r/$bin") || die("link $bins{$p} $r/$bin: $!\n");
1099     }
1100     push @db_changed, $p if $p =~ /\.(?:rpm|deb)$/;
1101     push @changed, $p;
1102     $changed = 1;
1103   }
1104
1105   close F;     # release repository lock
1106
1107   my $title = $proj->{'title'} || $projid;
1108   $title .= " ($repoid)";
1109   $title =~ s/\n/ /sg;
1110
1111   my $state;
1112   $state = $proj->{'patternmd5'} || '';
1113   $state .= "\0".join(',', @{$config->{'repotype'} || []}) if %bins;
1114   $state .= "\0".($proj->{'title'} || '') if %bins;
1115   $state .= "\0".join(',', @{$config->{'patterntype'} || []}) if $proj->{'patternmd5'};
1116   $state .= "\0".join('/', map {"$_->{'project'}/$_->{'repository'}"} @{$prpsearchpath || []}) if $proj->{'patternmd5'};
1117   $state .= "\0".$updateinfos_state if $updateinfos_state;
1118   $state .= "\0".$deltainfos_state if $deltainfos_state;
1119   $state = Digest::MD5::md5_hex($state) if $state ne '';
1120
1121   # get us the old repoinfo, so we can compare the state
1122   my $repoinfo = {};
1123   if (-s "$reporoot/$prp/:repoinfo") {
1124     $repoinfo = Storable::retrieve("$reporoot/$prp/:repoinfo") || {};
1125   }
1126
1127   if (($repoinfo->{'state'} || '') ne $state) {
1128     $changed = 1;
1129   }
1130   if (!$changed) {
1131     print "    nothing changed\n";
1132     return;
1133   }
1134
1135   mkdir_p($extrep) unless -d $extrep;
1136
1137   # get sign key
1138   my $signargs = [];
1139   my $signkey = BSRPC::rpc("$BSConfig::srcserver/getsignkey", undef, "project=$projid", "withpubkey=1");
1140   my $pubkey;
1141   if ($signkey) {
1142     ($signkey, $pubkey) = split("\n", $signkey, 2);
1143     mkdir_p("$uploaddir");
1144     writestr("$uploaddir/publisher.$$", undef, $signkey);
1145     $signargs = [ '-P', "$uploaddir/publisher.$$" ];
1146   } else {
1147     if ($BSConfig::sign_project && $BSConfig::sign) {
1148       local *S;
1149       open(S, '-|', $BSConfig::sign, '--project', $projid, '-p') || die("$BSConfig::sign: $!\n");;
1150       $pubkey = '';
1151       1 while sysread(S, $pubkey, 4096, length($pubkey));
1152       if (!close(S)) {
1153         print "sign -p failed: $?\n";
1154         $pubkey = undef;
1155       }
1156     } elsif ($BSConfig::keyfile) {
1157       if (-e $BSConfig::keyfile) {
1158         $pubkey = readstr($BSConfig::keyfile);
1159       } else {
1160         print "WARNING: configured sign key $BSConfig::keyfile does not exist\n";
1161       }
1162     }
1163   }
1164
1165   # get all patterns
1166   my $patterns = [];
1167   if ($proj->{'patternmd5'}) {
1168     $patterns = getpatterns($projid);
1169   }
1170
1171   # create and store the new repoinfo
1172   $repoinfo = {
1173     'prpsearchpath' => $prpsearchpath,
1174     'binaryorigins' => $binaryorigins,
1175     'title' => $title,
1176     'state' => $state,
1177   };
1178   $repoinfo->{'arch'} = $repo->{'arch'} if $repo->{'arch'};
1179   my $repoinfodb = db_open('repoinfo');
1180   if ($state ne '') {
1181     Storable::nstore($repoinfo, "$reporoot/$prp/:repoinfo");
1182     db_store($repoinfodb, $prp, $repoinfo) if $repoinfodb;
1183   } else {
1184     unlink("$reporoot/$prp/:repoinfo");
1185     db_store($repoinfodb, $prp, undef) if $repoinfodb;
1186   }
1187
1188   # put in published database
1189   my $binarydb = db_open('binary');
1190   updatebinaryindex($binarydb, [ map {"$prp_ext/$_"} @db_deleted ], [ map {"$prp_ext/$_"} @db_changed ]) if $binarydb && defined($prp_ext);
1191
1192   # mark file origins so we can gather per package statistics
1193   if ($BSConfig::markfileorigins && defined($prp_ext)) {
1194     print "    marking file origins\n";
1195     for my $f (sort @db_changed) {
1196       my $origin = $binaryorigins->{$f};
1197       $origin = "?" unless defined $origin;
1198       my $req = {
1199         'uri' => "$BSConfig::markfileorigins/$prp_ext/$f",
1200         'request' => 'HEAD',
1201         'maxredirects' => 3,
1202         'timeout' => 10,
1203         'ignorestatus' => 1,
1204       };
1205       eval {
1206         BSRPC::rpc($req, undef, 'cmd=setpackage', "package=$origin");
1207       };
1208       print "      $f: $@" if $@;
1209     }
1210     for my $f (sort @db_deleted) {
1211       my $req = {
1212         'uri' => "$BSConfig::markfileorigins/$prp_ext/$f",
1213         'request' => 'HEAD',
1214         'maxredirects' => 3,
1215         'timeout' => 10,
1216         'ignorestatus' => 1,
1217       };
1218       eval {
1219         BSRPC::rpc($req, undef, 'cmd=deleted');
1220       };
1221       print "      $f: $@" if $@;
1222     }
1223   }
1224
1225   # create repositories and patterns
1226   my %repotype;
1227   for (@{$config->{'repotype'} || []}) {
1228     if (/^(.*?):(.*)$/) {
1229       $repotype{$1} = [ split(':', $2) ];
1230     } else {
1231       $repotype{$_} = [];
1232     }
1233   }
1234   my %patterntype;
1235   for (@{$config->{'patterntype'} || []}) {
1236     if (/^(.*?):(.*)$/) {
1237       $patterntype{$1} = [ split(':', $2) ];
1238     } else {
1239       $patterntype{$_} = [];
1240     }
1241   }
1242   if ($repotype{'rpm-md-legacy'}) {
1243     $repotype{'rpm-md'} = $repotype{'rpm-md-legacy'};
1244     unshift @{$repotype{'rpm-md'}}, 'legacy';
1245     delete $repotype{'rpm-md-legacy'};
1246   }
1247   if ($BSConfig::publishprogram && $BSConfig::publishprogram->{$prp}) {
1248     local *PPLOCK;
1249     open(PPLOCK, '>', "$reporoot/$prp/.pplock") || die("$reporoot/$prp/.pplock: $!\n");
1250     flock(PPLOCK, LOCK_EX) || die("flock: $!\n");
1251     if (xfork()) {
1252       close PPLOCK;
1253       return;
1254     }
1255     if (system($BSConfig::publishprogram->{$prp}, $prp, $extrep)) {
1256       die("      $BSConfig::publishprogram{$prp} failed: $?\n");
1257     }
1258     goto publishprog_done;
1259   }
1260
1261   if ($repotype{'rpm-md'}) {
1262     createrepo_rpmmd($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $repotype{'rpm-md'}, \@updateinfos, \%deltainfos);
1263   } else {
1264     deleterepo_rpmmd($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo);
1265   }
1266   if ($repotype{'suse'}) {
1267     createrepo_susetags($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo);
1268   } else {
1269     deleterepo_susetags($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo);
1270   }
1271   if ($repotype{'debian'}) {
1272     createrepo_debian($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo);
1273   } else {
1274     deleterepo_debian($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo);
1275   }
1276
1277   if ($patterntype{'ymp'}) {
1278     createpatterns_ymp($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $patterns);
1279   } else {
1280     deletepatterns_ymp($extrep, $projid, $repoid, $signargs, $pubkey);
1281   }
1282   if ($patterntype{'rpm-md'}) {
1283     createpatterns_rpmmd($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $patterns);
1284   } else {
1285     deletepatterns_rpmmd($extrep, $projid, $repoid, $signargs, $pubkey);
1286   }
1287   if ($patterntype{'comps'}) {
1288     createpatterns_comps($extrep, $projid, $repoid, $signargs, $pubkey, $repoinfo, $patterns);
1289   } else {
1290     deletepatterns_comps($extrep, $projid, $repoid, $signargs, $pubkey);
1291   }
1292
1293
1294 publishprog_done:
1295   unlink("$uploaddir/publisher.$$") if $signkey;
1296
1297   # post process step: create directory listing for poor YaST
1298   if ($repotype{'suse'}) {
1299     unlink("$extrep/directory.yast");
1300     my @d = sort(ls($extrep));
1301     for (@d) {
1302       $_ .= '/' if -d "$extrep/$_";
1303       $_ .= "\n";
1304     }
1305     writestr("$extrep/.directory.yast", "$extrep/directory.yast", join('', @d));
1306   }
1307
1308   # push to stageserver
1309   if ($BSConfig::stageserver && $BSConfig::stageserver =~ /^rsync:\/\/([^\/]+)\/(.*)$/ && defined($prp_ext)) {
1310     print "    running rsync to $1 at ".localtime(time)."\n";
1311     # sync project repos, timeout 1 hour
1312     qsystem('echo', "$prp_ext\0", 'rsync', '-ar0', '--delete-after', '--exclude=repocache', '--timeout', '3600', '--files-from=-', $extrepodir, "$1::$2") && die("    rsync failed: $?\n");
1313   }
1314
1315   # push done trigger to stageserver so that it can send it to the world
1316   if (defined($prp_ext)) {
1317     mkdir_p($extrepodir_sync);
1318     my $projid_ext = $projid;
1319     $projid_ext =~ s/:/:\//g;
1320     writestr("$extrepodir_sync/.$projid", "$extrepodir_sync/$projid", "$projid_ext\0");
1321     if ($BSConfig::stageserver_sync && $BSConfig::stageserver_sync =~ /^rsync:\/\/([^\/]+)\/(.*)$/) {
1322       print "    running trigger rsync to $1 at ".localtime(time)."\n";
1323       # small sync, timeout 1 minute
1324       qsystem('rsync', '-a', '--timeout', '60', "$extrepodir_sync/$projid", "$1::$2/$projid") && warn("    trigger rsync failed: $?\n");
1325     }
1326   }
1327   if ($BSConfig::publishedhook && $BSConfig::publishedhook->{$prp}) {
1328     qsystem($BSConfig::publishedhook->{$prp}, $prp, $extrep, @changed) && warn("    $BSConfig::publishedhook failed: $?");
1329   }
1330
1331   
1332   BSNotify::notify("REPO_PUBLISHED", { project => $projid , 'repo' => $repoid });
1333   # all done. till next time...
1334   if ($BSConfig::publishprogram && $BSConfig::publishprogram->{$prp}) {
1335     exit(0);
1336   }
1337 }
1338
1339
1340 $| = 1;
1341 $SIG{'PIPE'} = 'IGNORE';
1342 BSUtil::restartexit($ARGV[0], 'publisher', "$rundir/bs_publish", "$myeventdir/.ping");
1343 print "starting build service publisher\n";
1344
1345 open(RUNLOCK, '>>', "$rundir/bs_publish.lock") || die("$rundir/bs_publish.lock: $!\n");
1346 flock(RUNLOCK, LOCK_EX | LOCK_NB) || die("publisher is already running!\n");
1347 utime undef, undef, "$rundir/bs_publish.lock";
1348
1349 mkdir_p($myeventdir);
1350 if (!-p "$myeventdir/.ping") {
1351   POSIX::mkfifo("$myeventdir/.ping", 0666) || die("$myeventdir/.ping: $!");
1352   chmod(0666, "$myeventdir/.ping");
1353 }
1354 sysopen(PING, "$myeventdir/.ping", POSIX::O_RDWR) || die("$myeventdir/.ping: $!");
1355
1356 db_sync();
1357
1358 my %publish_retry;
1359
1360 while(1) {
1361   # drain ping pipe
1362   my $dummy;
1363   fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
1364   1 while (sysread(PING, $dummy, 1024, 0) || 0) > 0;
1365   fcntl(PING,F_SETFL,0);
1366
1367   # check for events
1368   my @events = ls($myeventdir);
1369   @events = grep {!/^\./} @events;
1370   for my $event (@events) {
1371     next if $publish_retry{$event};
1372     last if -e "$rundir/bs_publish.exit";
1373     last if -e "$rundir/bs_publish.restart";
1374     my $ev = readxml("$myeventdir/$event", $BSXML::event, 1);
1375     if (!$ev || !$ev->{'type'} || $ev->{'type'} ne 'publish') {
1376       unlink("$myeventdir/$event");
1377       next;
1378     }
1379     if (!defined($ev->{'project'}) || !defined($ev->{'repository'})) {
1380       unlink("$myeventdir/$event");
1381       next;
1382     }
1383     my $prp = "$ev->{'project'}/$ev->{'repository'}";
1384     if ($BSConfig::publishprogram && $BSConfig::publishprogram->{$prp}) {
1385       # check if background publish is still running
1386       local *PPLOCK;
1387       if (open(PPLOCK, '<', "$reporoot/$prp/.pplock")) {
1388         if (flock(PPLOCK, LOCK_EX | LOCK_NB)) {
1389           close PPLOCK;
1390           print "external publish program still running\n";
1391           $publish_retry{$event} = time() + 60;
1392           next;
1393         }
1394         close PPLOCK;
1395       }
1396     }
1397     rename("$myeventdir/$event", "$myeventdir/${event}::inprogress");
1398     BSNotify::notify('REPO_PUBLISH_STATE', { 'project' => $ev->{'project'}, 'repo' => $ev->{'repository'}, 'state' => 'publishing'} );
1399     eval {
1400       publish($ev->{'project'}, $ev->{'repository'});
1401     };
1402     if ($@) {
1403       warn("publish failed: $@");
1404       rename("$myeventdir/${event}::inprogress", "$myeventdir/$event");
1405       $publish_retry{$event} = time() + 60;
1406     } else {
1407       BSNotify::notify('REPO_PUBLISH_STATE', { 'project' => $ev->{'project'}, 'repo' => $ev->{'repository'}, 'state' => 'published'} );
1408       unlink("$myeventdir/${event}::inprogress");
1409     }
1410     db_sync();
1411   }
1412
1413   # check for restart/exit
1414   if (-e "$rundir/bs_publish.exit") {
1415     unlink("$rundir/bs_publish.exit");
1416     print "exiting...\n";
1417     exit(0);
1418   }
1419   if (-e "$rundir/bs_publish.restart") {
1420     unlink("$rundir/bs_publish.restart");
1421     print "restarting...\n";
1422     exec($0);
1423     die("$0: $!\n");
1424   }
1425
1426   if (%publish_retry) {
1427     my $now = time();
1428     for (sort keys %publish_retry) {
1429       delete($publish_retry{$_}) if $publish_retry{$_} < $now;
1430     }
1431     print "sleeping 10 seconds...\n";
1432     sleep(10);
1433   } else {
1434     print "waiting for an event...\n";
1435     sysread(PING, $dummy, 1, 0);
1436   }
1437 }