1 package Fedora
::Rebuild
;
4 use version
0.77; our $VERSION = version
->declare("v0.4.1");
7 use Moose
::Util
::TypeConstraints
;
10 use Fedora
::Rebuild
::Set
::Package
;
11 use Fedora
::Rebuild
::Package
;
12 use Fedora
::Rebuild
::Scheduler
;
15 has
'all' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
16 has
'done' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
17 has
'failed' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
18 has
'workdir' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
20 # "f14", "f15" etc. Use "rawhide" for latest one.
21 has
'dist' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
23 # "dist-f14", "dist-f15" etc. Use "dist-rawhide" for latest one.
24 has
'target' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
25 has
'message' => (is
=> 'ro', isa
=> 'Str', required
=> 1);
26 # Reference to function with three arguments (BuildRequire name, relation
27 # flag, version) returning false the BuildRequire should be considered, true
28 # otherwise. If attribute is undef or missing, no filtering will be performed
29 # (i.e. the same effect as sub {1}).
30 has
'buildrequiresfilter' => (is
=> 'ro', isa
=> 'CodeRef', required
=> 0);
31 # Pass true for committing and building locally only. Default is false to
33 has
'local' => (is
=> 'ro', isa
=> 'Bool', required
=> 0);
34 # Run rebuild in given number of threads. Default is 1.
35 has
'threads' => (is
=> 'ro', isa
=> subtype
('Int' => where
{$_>0} =>
36 message
{"Attribute threads must be positive integer (was $_)"}),
38 # Load binary provides of already done packages or buildrequires in given
39 # number of threads. This is to lower local I/O load. Default is 1.
40 has
'loadthreads' => (is
=> 'ro', isa
=> subtype
('Int' => where
{$_>0} =>
41 message
{"Attribute loadthreads must be positive integer (was $_)"}),
43 # Maximal count of immediately sequential failures to accept and continue
44 # rebuild process. If the limit exceeds, rebuild process will terminate. This
45 # is good to catch pathologic cases when something is obviously wrong and
46 # should be fixed before rebuilding (e.g. Koji is down, or you have not SSH
47 # key loaded into SSH agent). Use non-positive number to disable this check.
48 # Default value is 0 (i.e. not to check).
49 has
'failurelimit' => (is
=> 'ro', isa
=> 'Int', required
=> 0, default => 0);
51 has
'remaining_packages' => (is
=> 'ro', isa
=> 'Fedora::Rebuild::Set::Package',
52 lazy_build
=> 1, init_arg
=> undef);
53 has
'done_packages' => (is
=> 'ro', isa
=> 'Fedora::Rebuild::Set::Package',
54 lazy_build
=> 1, init_arg
=> undef);
55 has
'failed_packages' => (is
=> 'ro', isa
=> 'Fedora::Rebuild::Set::Package',
56 lazy_build
=> 1, init_arg
=> undef);
57 has
'subsequent_failures' => (is
=> 'rw', isa
=> 'Int', default => 0,
59 has
'last_failed' => (is
=> 'rw', isa
=> 'Bool', default => 0,
63 # Creates set of packages not yet rebuilt.
64 sub _build_done_packages
{
66 my $packages = Fedora
::Rebuild
::Set
::Package
->new();
69 if (not -e
$self->done) {
70 print "No packages have been rebuilt yet.\n";
74 open($file, '<', $self->done) or
75 croak
"Could not open `" . $self->done .
76 "' for loading list of already rebuilt packages: $!";
77 print "Loading list of already rebuilt package names...\n";
78 while (local $_ = <$file>) {
80 if (m/^\s*$/) { next; }
82 my $package = Fedora
::Rebuild
::Package
->new(name
=> $_,
83 workdir
=> $self->workdir, dist
=> $self->dist,
84 target
=> $self->target, message
=> $self->message);
85 $packages->insert($package);
88 croak
"Could not read list of rebuilt package names from file `" .
89 $self->done . "': $!";
92 print "Number of done packages: " . $packages->size() . "\n";
99 my ($self, $package) = @_;
100 my $file = IO
::Handle
->new();
102 open($file, '>>', $self->done) or
103 croak
"Could not open `" . $self->done . "' for appending: $!";
104 printf $file $package->name . "\n" or
105 croak
"Could not write data into `" . $self->done . "': $!";
106 $file->flush && $file->sync && close($file) or
107 croak
"Could not flush and close `" . $self->done . "': $!";
109 $self->done_packages->insert($package);
110 $self->last_failed(0);
111 $self->subsequent_failures(0);
114 # Creates set of packages not yet rebuilt not already failed.
115 sub _build_remaining_packages
{
117 my $packages = Fedora
::Rebuild
::Set
::Package
->new();
120 open($file, '<', $self->all) or
121 croak
"Could not open " . $self->all .
122 " for loading list of package names to rebuild: $!";
123 print "Loading list of all package names to rebuild...\n";
125 while (local $_ = <$file>) {
127 if (m/^\s*$/) { next; }
128 if ($packages->contains($_)) { next; }
129 if (! $self->done_packages->contains($_) &&
130 ! $self->failed_packages->contains($_)) {
131 $packages->insert(Fedora
::Rebuild
::Package
->new(name
=> $_,
132 workdir
=> $self->workdir, dist
=> $self->dist,
133 target
=> $self->target, message
=> $self->message));
137 croak
"Could not read list of package names to rebuild from file `" .
138 $self->all . "' :$!";
142 print "Number of all packages: " .
143 ($packages->size + $self->done_packages->size
144 + $self->failed_packages->size) . "\n";
145 print "Number of not yet rebuilt packages: " . $packages->size() . "\n";
150 # Set of packages whose rebuild failed.
151 sub _build_failed_packages
{
153 my $packages = Fedora
::Rebuild
::Set
::Package
->new();
156 if (not -e
$self->failed) {
157 print "No packages have failed yet.\n";
161 open($file, '<', $self->failed) or
162 croak
"Could not open `" . $self->failed .
163 "' for loading list of already failed packages: $!";
164 print "Loading list of already failed package names...\n";
165 while (local $_ = <$file>) {
167 if (m/^\s*$/) { next; }
169 my $package = Fedora
::Rebuild
::Package
->new(name
=> $_,
170 workdir
=> $self->workdir, dist
=> $self->dist,
171 target
=> $self->target, message
=> $self->message);
172 $packages->insert($package);
175 croak
"Could not read list of failed package names from file `" .
176 $self->failed . "': $!";
179 print "Number of failed packages: " . $packages->size() . "\n";
184 # Record package names into log of failed packages
186 my ($self, $package) = @_;
187 my $file = IO
::Handle
->new();
190 open($file, '>>', $self->failed) or
191 croak
"Could not open `" . $self->failed . "' for appending: $!";
192 printf $file $package->name . "\n" or
193 croak
"Could not write data into `" . $self->failed . "': $!";
194 $file->flush && $file->sync && close($file) or
195 croak
"Could not flush and close `" . $self->failed . "': $!";
197 # Move to list of failed
198 $self->failed_packages->insert($package);
200 # Check for very failures
201 if ($self->last_failed) {
202 $self->subsequent_failures($self->subsequent_failures + 1);
204 $self->last_failed(1);
205 $self->subsequent_failures(1);
207 if ($self->failurelimit > 0 &&
208 $self->subsequent_failures > $self->failurelimit) {
209 croak
"More then " . $self->failurelimit .
210 " package(s) failed subsequently which is more then set " .
211 "threshold. Aborting now.\n";
215 # Load binary requires and provides of each done package.
216 # Return true in case of success or croaks in case of failure.
217 sub load_binarydependencies
{
219 my $scheduler = Fedora
::Rebuild
::Scheduler
->new($self->loadthreads);
221 my @packages = $self->done_packages->packages;
224 print "Loading binary dependenices of already rebuilt packages...\n";
226 foreach my $package (@packages) {
227 my $job = $scheduler->schedule($package->can('get_binarydependencies'),
228 $package, $self->local);
229 if (! defined $job) { next; }
230 $jobs{$job} = $package;
231 my %finished = $scheduler->finish(++$i < @packages);
233 while (my ($job, $status) = each %finished) {
234 my $package = $jobs{$job};
236 print "Could not load binary dependencies for already built " .
237 "package `" . $package->name . "'.\n";
238 print "Waiting for finishing scheduled jobs...\n";
239 $scheduler->finish(1);
240 print "All jobs loading binary dependcies have finished.\n";
241 croak
"Could not load binary dependencies\n";
246 print "Binary dependencies of already rebuilt packages loaded " .
251 # Decides a package is installable in build root now.
252 # Package that has been rebuild may be uinstallable bacause it can require
253 # dependencies that have not buil built alreade (i.e. the Requres does not
254 # need to match BuildRequire).
255 # This is hard problem as it requires recursive evaluation of each package
256 # satisifying run-time dependency of previous package.
257 # Return 0 for false, 1 or true, undef for error while deciding.
259 my ($self, $package) = @_;
264 if (!defined $package->get_buildrequires()) {
268 # Each requirement of $package must be satisfied by at least one already
270 for my $rname (keys %{$package->requires}) {
271 for my $require (@
{${$package->requires}{$rname}}) {
272 my $rflag = $$require[0];
273 my $rversion = $$require[1];
274 my $is_satisfied = 0;
276 if (defined $self->buildrequiresfilter and
277 &{$self->buildrequiresfilter}($rname, $rflag, $rversion)) {
278 # Avoid user-defined BuildRequires from dependency eva;uation
282 for my $rebuilt_package ($self->done_packages->packages) {
283 if (Fedora
::Rebuild
::RPM
::is_satisfied
($rname, $rflag,
284 $rversion, $rebuilt_package->provides)) {
290 if (!$is_satisfied) {
291 print qq{Package
`} . $package->name . qq{' cannot be rebuilt }
292 . qq{now because `}. $rname . qq{ } .
293 Fedora
::Rebuild
::RPM
::flag_as_string
($rflag) .
294 qq{ } . $rversion . qq{' could not been satisfied.} . "\n";
300 print "BuildRequires for `" . $package->name . "' fulfilled
.\n";
305 # Decides a package is rebuildable now.
306 # Return 0 for false, 1 or true, undef for error while deciding.
308 my ($self, $package) = @_;
310 if (!defined $package->get_buildrequires()) {
314 # Each requirement of $package must be satisfied by at least one already
316 for my $rname (keys %{$package->requires}) {
317 for my $require (@{${$package->requires}{$rname}}) {
318 my $rflag = $$require[0];
319 my $rversion = $$require[1];
320 my $is_satisfied = 0;
322 if (defined $self->buildrequiresfilter and
323 &{$self->buildrequiresfilter}($rname, $rflag, $rversion)) {
324 # Avoid user-defined BuildRequires from dependency eva;uation
328 for my $rebuilt_package ($self->done_packages->packages) {
329 if (Fedora::Rebuild::RPM::is_satisfied($rname, $rflag,
330 $rversion, $rebuilt_package->provides)) {
336 if (!$is_satisfied) {
337 print qq{Package `} . $package->name . qq{' cannot be rebuilt }
338 . qq{now because `}. $rname . qq{ } .
339 Fedora::Rebuild::RPM::flag_as_string($rflag) .
340 qq{ } . $rversion . qq{' could not been satisfied.} . "\n";
346 print "BuildRequires
for `" . $package->name . "' fulfilled.\n";
351 # Return F:R:Set:Packages than can be rebuilt now
352 sub select_rebuildable {
354 my $scheduler = Fedora::Rebuild::Scheduler->new($self->loadthreads);
357 print "Selecting rebuildable packages...\n";
359 my $rebuildables = Fedora::Rebuild::Set::Package->new;
360 foreach my $package ($self->remaining_packages->packages) {
361 my $job = $scheduler->schedule($self->can('is_rebuildable'), $self,
363 if (! defined $job) { next; }
364 $jobs{$job} = $package;
365 %finished = (%finished, ($scheduler->finished()));
367 %finished = (%finished, ($scheduler->finish()));
369 for my $job (keys %finished) {
370 my $job_status = $finished{$job};
371 my $package = $jobs{$job};
374 $rebuildables->insert($package);
375 } elsif (!defined $job_status) {
376 # Could not decide whether the $package is rebuildable. This is
377 # fatal for the package. Move it to failed packages.
378 $self->remaining_packages->delete($package);
379 $self->mark_failed($package);
383 print "Packages selected to rebuild (" . $rebuildables->size .
384 "): " . $rebuildables->string . "\n";
385 return $rebuildables;
389 # Rebuild all remaining packages
392 print "remaining_packages: " . $self->remaining_packages->string . "\n";
393 print "done_packages: " . $self->done_packages->string . "\n";
394 print "Rebuild mode: " . (($self->local)?"local":"public") . "\n";
396 $self->load_binarydependencies;
398 while ($self->remaining_packages->size > 0) {
399 my $rebuildable_packages = $self->select_rebuildable;
400 if ($rebuildable_packages->size == 0) {
401 printf "No more packages can be rebuilt!\n";
405 my $scheduler = Fedora::Rebuild::Scheduler->new($self->threads);
409 my @packages = $rebuildable_packages->packages;
412 foreach my $package (@packages) {
413 $self->remaining_packages->delete($package);
415 my $job = $scheduler->schedule($package->can('rebuild'), $package,
417 if (! defined $job) { next; }
418 $jobs{$job} = $package;
419 my %finished = $scheduler->finish(++$i < @packages);
421 while (my ($job, $status) = each %finished) {
422 my $package = $jobs{$job};
424 # TODO: Push Provides into global list of available
426 # XXX: Noting here, fall through to rotation waiting,
427 # rebuilt package list tracked in $rebuildable_packages.
429 $rebuildable_packages->delete($package);
430 $self->mark_failed($package);
436 # Wait for Koji rotation
437 # This is separeted from rebuilding process to solve trade-off between
438 # high threads number (consumes lot of memory and CPU time when
439 # starting) and the rebuild itself is much faster Koji rotation period
440 # (lot of time lost to wait for rotation).
442 @packages = $rebuildable_packages->packages;
445 foreach my $package (@packages) {
446 my $job = $scheduler->schedule($package->can('waitforbuildroot'),
447 $package, $self->local);
448 if (! defined $job) { next; }
449 $jobs{$job} = $package;
450 my %finished = $scheduler->finish(++$i < @packages);
452 while (my ($job, $status) = each %finished) {
453 my $package = $jobs{$job};
455 $self->mark_done($package);
457 $self->mark_failed($package);
463 if ($self->failed_packages->size > 0) {
464 print "Rebuild of following packages failed (" .
465 $self->failed_packages->size . "): " .
466 $self->failed_packages->string . "\n";
469 print "All packages have been rebuilt.\n";
480 Fedora::Rebuild - Rebuilds Fedora packages from scratch
484 Main goal is to rebuild perl modules packages for Fedora. The rebuild is
485 driven from bottom to top, i.e. from perl interpreter to modules depending on
486 intermediate modules. This way, it's possible to upgrade perl interpreter to
487 incompatible version and to rebuild all modules against the new interpreter.
489 Procedure is following: We have a set of source package to rebuild. We
490 distill all build-time dependencies for each source package. We ignore
491 non-perl dependencies as we focus on Perl only.
493 We select all source packages that do not depend on anything and push them
494 into Koji to rebuild. Obviously, the only fulfilling package is Perl interpret
497 Once the first subset of packages is rebuilt, we gather their binary packages
498 and distill their binary provides. These freshly available provides are put
499 into new set of available provides. At the same time we remove the rebuilt
500 subset from original set of all source packages.
502 Then we wait for build root rotation in Koji to get rebuilt binary packages
503 available in build root.
505 (Of course we could get provides of new binary packages from koji repository
506 after rotation and we can get provides through repoquery instead of
507 inspecting binary packages manually.)
509 If package rebuild fails, the package is removed from future consideration.
511 Then we return to start of this procedure to select other subset of source
512 packages whose build-time dependecies can be satisfied from set of binary
513 provides we have obtained till now.
515 This loop cycles until set of source packages is empty.
517 =head1 SYNCHRONIZATION
519 Because mass rebuild is long term issue and lot of intermediate failures can
520 emerge, one must be able to resume failed rebuild process.
522 Safe resume is assured by proper logging. Once a package has been rebuilt, its
523 name is logged into list of done packages. Restarting rebuild program with the
524 same arguments loads done packages, remove them from set of remaining
525 packages, and populates list of available provides. In additon, packages whose
526 rebuild failed are removed from set of remaining packages.
530 Petr Písař <ppisar@redhat.com>
534 Copyright (C) 2011 Petr Pisar <ppisar@redhat.com>
536 This program is free software: you can redistribute it and/or modify
537 it under the terms of the GNU General Public License as published by
538 the Free Software Foundation, either version 3 of the License, or
539 (at your option) any later version.
541 This program is distributed in the hope that it will be useful,
542 but WITHOUT ANY WARRANTY; without even the implied warranty of
543 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
544 GNU General Public License for more details.
546 You should have received a copy of the GNU General Public License
547 along with this program. If not, see <http://www.gnu.org/licenses/>.