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