File Coverage

blib/lib/PkgForge/BuildCommand/Builder/RPM.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package PkgForge::BuildCommand::Builder::RPM; # -*-perl-*-
2 2     2   21542 use strict;
  2         5  
  2         69  
3 2     2   10 use warnings;
  2         4  
  2         126  
4              
5             # $Id: Queue.pm.in 13577 2010-08-26 08:34:57Z squinney@INF.ED.AC.UK $
6             # $Source:$
7             # $Revision: 13577 $
8             # $HeadURL: https://svn.lcfg.org/svn/source/trunk/PkgForge/lib/PkgForge/Queue.pm.in $
9             # $Date: 2010-08-26 09:34:57 +0100 (Thu, 26 Aug 2010) $
10              
11             our $VERSION = '1.1.10';
12              
13 2     2   1732 use English qw(-no_match_vars);
  2         6635  
  2         11  
14 2     2   2948 use File::Copy ();
  2         7533  
  2         42  
15 2     2   2262 use File::Find::Rule ();
  2         18135  
  2         44  
16 2     2   15 use File::Path ();
  2         5  
  2         27  
17 2     2   10 use File::Spec ();
  2         4  
  2         25  
18 2     2   2572 use File::Temp ();
  2         42583  
  2         50  
19 2     2   2846 use IPC::Run ();
  2         96443  
  2         47  
20 2     2   171207 use PkgForge::Source::SRPM ();
  0            
  0            
21             use PkgForge::Utils ();
22             use RPM2;
23              
24             use Readonly;
25             Readonly my $MOCK_BIN => '/usr/bin/mock';
26             Readonly my $MOCK_DIR => '/etc/mock';
27             Readonly my $MOCK_QUERY => '/usr/bin/mock_config_query';
28             Readonly my $RPMBUILD_BIN => '/usr/bin/rpmbuild';
29             Readonly my $RPM_BIN => '/bin/rpm';
30             Readonly my $RPM_API_CHANGE => '4.6';
31              
32             use overload q{""} => sub { shift->stringify };
33              
34             use Moose;
35             use MooseX::Types::Moose qw(Bool Str);
36              
37             with 'PkgForge::BuildCommand::Builder';
38              
39             has '+tools' => (
40             default => sub { [ $MOCK_BIN, $MOCK_QUERY, $RPM_BIN, $RPMBUILD_BIN ] },
41             );
42              
43             has '+architecture' => (
44             required => 1,
45             );
46              
47             has '+accepts' => (
48             default => 'SRPM',
49             );
50              
51             has 'use_mock' => (
52             is => 'ro',
53             isa => Bool,
54             default => 1,
55             documentation => 'Use mock to build packages',
56             );
57              
58             has 'rpm_api_version' => (
59             is => 'ro',
60             isa => Str,
61             default => sub { return RPM2->rpm_api_version },
62             lazy => 1,
63             documentation => 'The RPM API version',
64             );
65              
66             no Moose;
67             __PACKAGE__->meta->make_immutable;
68              
69             sub mock_chroot_name {
70             my ( $self, $bucket ) = @_;
71              
72             my $chroot = join q{-}, $self->platform, $bucket, $self->architecture;
73              
74             return $chroot;
75             }
76              
77             sub mock_query {
78             my ( $self, $chroot, $key ) = @_;
79              
80             my $value = `$MOCK_QUERY -r $chroot $key`;
81             if ( $CHILD_ERROR != 0 || !defined $value ) {
82             die "Failed to query the mock config for $chroot\n";
83             }
84             chomp $value;
85              
86             return $value;
87             }
88              
89             sub mock_clear_resultsdir {
90             my ( $self, $chroot ) = @_;
91              
92             my $resultdir = $self->mock_query( $chroot, 'resultdir' );
93              
94             my @errors;
95             PkgForge::Utils::remove_tree( $resultdir, { error => \@errors,
96             keep_root => 1 } );
97              
98             if ( scalar @errors > 0 ) {
99             die "Could not clear $resultdir: failed on @errors\n";
100             }
101              
102             return;
103             }
104              
105             sub mock_createrepo {
106             my ( $self, $chroot ) = @_;
107              
108             my $createrepo = $self->mock_query( $chroot, 'createrepo_on_rpms' );
109             if ( $createrepo !~ m/true/i ) {
110             return;
111             }
112              
113             my $resultdir = $self->mock_query( $chroot, 'resultdir' );
114             if ( !-d $resultdir ) {
115             my $ok = eval { File::Path::mkpath($resultdir) };
116             if ( !$ok || $EVAL_ERROR ) {
117             die "Failed to create directory $resultdir: $EVAL_ERROR\n";
118             }
119             }
120              
121             my $command = $self->mock_query( $chroot, 'createrepo_command' );
122              
123             my @command = split q{ }, $command;
124             push @command, $resultdir;
125              
126             my $output;
127             my $ok = eval { IPC::Run::run( \@command, \undef, '>&', \$output ) };
128              
129             if ( !$ok || $EVAL_ERROR ) {
130             die "Failed to run createrepo: @command: $output\n";
131             }
132              
133             return;
134             }
135              
136             sub mock_init {
137             my ( $self, $chroot ) = @_;
138              
139             $self->mock_clear_resultsdir($chroot);
140              
141             $self->mock_createrepo($chroot);
142              
143             return 1;
144             }
145              
146             sub mock_run {
147             my ( $self, $job, $buildinfo, $logger ) = @_;
148              
149             my $bucket = $job->bucket;
150             my $chroot = $self->mock_chroot_name($bucket);
151              
152             $logger->debug("Using mock chroot $chroot");
153              
154             my $init_ok = eval { $self->mock_init($chroot) };
155             if ( !$init_ok || $EVAL_ERROR ) {
156             $logger->log_and_die(
157             level => 'critical',
158             message => "mock initialisation failed: $EVAL_ERROR",
159             );
160             } else {
161             $logger->info('mock build environment successfully initialised');
162             }
163              
164             my $timeout = $self->timeout;
165             my @cmd_base = ( $MOCK_BIN,
166             '--root', $chroot,
167             '--rpmbuild_timeout', $timeout );
168              
169             my @todo = $buildinfo->sources_list;
170              
171             my ( @failed, @success );
172             my $failure = 0;
173              
174             while ( !$failure && scalar @todo > 0 ) {
175             @failed = ();
176              
177             for my $pkg (@todo) {
178             my $path = $pkg->fullpath;
179             if ( !-f $path ) {
180             $logger->log_and_die(
181             level => 'critical',
182             message => "Cannot find source package '$path'",
183             );
184             }
185              
186             $logger->info("Attempting to build $path");
187              
188             my $cmd = [ @cmd_base, $path ];
189             $logger->debug("Will run command '@{$cmd}'");
190              
191             my $mock_out;
192             my $ok = eval { IPC::Run::run(
193             $cmd, \undef,
194             '>&', \$mock_out,
195             ) };
196             my $error = $EVAL_ERROR; # ensure it's not eaten by logger
197              
198             $logger->info($mock_out) if $mock_out;
199              
200             if ( $ok && !$error ) {
201             $logger->info("Successfully built $path");
202             push @success, $pkg;
203             } else {
204             $logger->error("Failed to build $path");
205             $logger->error($error) if $error;
206             push @failed, $pkg;
207              
208             if ( $self->error_policy eq 'immediate' ) {
209             $logger->error('Giving up as the policy is to fail immediately');
210             $failure = 1;
211             last;
212             } else {
213             $logger->error('Might retry later');
214             }
215              
216             }
217              
218             }
219              
220             if ( !$failure ) {
221             if ( scalar @failed > 0 ) {
222             # Retry if something has built since the last run.
223              
224             if ( scalar @failed < scalar @todo ) {
225             @todo = @failed;
226             } else {
227             $failure = 1;
228             }
229             } else {
230             @todo = ();
231             }
232             }
233              
234             }
235              
236             # Store all the build information for later package submission and
237             # report generation.
238              
239             $buildinfo->success(\@success);
240             $buildinfo->failures(\@failed);
241             $buildinfo->add_logs($self->mock_find_logs($chroot));
242              
243             $buildinfo->products([$self->mock_find_products($chroot)]);
244              
245             if ($failure) {
246             $logger->error("Failed to build: @failed");
247             return 0;
248             }
249              
250             return 1;
251             }
252              
253             sub mock_find_logs {
254             my ( $self, $chroot ) = @_;
255              
256             my $mock_results = $self->mock_query( $chroot, 'resultdir' );
257              
258             my @files =
259             File::Find::Rule->file()->name('*.log')->maxdepth(1)->in($mock_results);
260              
261             return @files;
262             }
263              
264             sub mock_find_products {
265             my ( $self, $chroot ) = @_;
266              
267             my $mock_results = $self->mock_query( $chroot, 'resultdir' );
268              
269             my @files =
270             File::Find::Rule->file()->name('*.rpm')->maxdepth(1)->in($mock_results);
271              
272             return @files;
273             }
274              
275             # This is one big ugly horrid hack to work around not being able to
276             # directly build from SRPMs on SL5 if they were created with newer
277             # versions of rpmlib.
278              
279             sub fix_sources {
280             my ( $self, $buildinfo, $logger ) = @_;
281              
282             my $api_version = $self->rpm_api_version;
283              
284             # Only have problems if the version of rpmlib on the machine is
285             # less than 4.6 (e.g. RHEL5).
286             if ( RPM2::rpmvercmp( $api_version, $RPM_API_CHANGE ) != -1 ) {
287             return;
288             }
289              
290             # Using a local per-job temporary directory to store the results for
291             # performance reasons.
292              
293             my $resultsdir = File::Spec->catdir( $self->tmpdir, $buildinfo->jobid );
294             if ( !-d $resultsdir ) {
295             my $ok = eval { File::Path::mkpath($resultsdir) };
296             if ( !$ok || $EVAL_ERROR ) {
297             $logger->log_and_die(
298             level => 'error',
299             message => "Failed to create $resultsdir: $EVAL_ERROR",
300             );
301             }
302             }
303              
304             my @updated_sources;
305              
306             for my $source ($buildinfo->sources_list) {
307              
308             # Check if the version of rpm used to create the SRPM is
309             # actually going to be a problem (i.e. is it greater than or equal to 4.6)
310              
311             my $srpm = $source->fullpath;
312             my $pkg = eval { RPM2->open_package($srpm) };
313             if ( !$EVAL_ERROR && defined $pkg &&
314             RPM2::rpmvercmp( $pkg->tag('RPMVERSION'), $RPM_API_CHANGE ) == -1 ) {
315             # Nothing to do
316             push @updated_sources, $source;
317             next;
318             }
319              
320             $logger->info("Rebuilding SRPM '$srpm' as it has been created using an rpmlib version greater than '$api_version'");
321              
322             # We need a separate temporary directory for each source rebuild
323             # so that we can guarantee it is clean when we start. Otherwise it
324             # would be impossible to find the correct specfile and generated
325             # SRPM.
326              
327             my $tempdir = File::Temp->newdir( 'pkgforge-XXXXX',
328             TMPDIR => 1,
329             CLEANUP => 1 );
330              
331             my %dirs;
332             for my $dir (qw/BUILD BUILDROOT RPMS SOURCES SPECS SRPMS/) {
333             $dirs{$dir} = File::Spec->catdir( $tempdir, $dir );
334             my $ok = eval { File::Path::mkpath($dirs{$dir}) };
335             if ( !$ok || $EVAL_ERROR ) {
336             $logger->log_and_die(
337             level => 'error',
338             message => "Failed to create $dirs{$dir}: $EVAL_ERROR",
339             );
340             }
341             }
342              
343             my @defs = ( '--define', "_topdir $tempdir",
344             '--define', "_builddir $dirs{BUILD}",
345             '--define', "_specdir $dirs{SPECS}",
346             '--define', "_sourcedir $dirs{SOURCES}",
347             '--define', "_srcrpmdir $dirs{SRPMS}",
348             '--define', "_rpmdir $dirs{RPMS}",
349             '--define', "_buildrootdir $dirs{BUILDROOT}" );
350              
351             # Install the SRPM so we can get the specfile and sources
352              
353             my $rpm_out;
354             my $rpm_ok = eval {
355             IPC::Run::run(
356             [ $RPM_BIN, @defs, '--nomd5', '--install', $srpm ],
357             \undef, '>&', \$rpm_out )
358             };
359             if ( !$rpm_ok || $EVAL_ERROR ) {
360             $logger->error($EVAL_ERROR) if $EVAL_ERROR;
361             $logger->log_and_die(
362             level => 'error',
363             message => "rpm install failed: $rpm_out",
364             );
365             }
366              
367             my $specfile = ( glob "$dirs{SPECS}/*.spec" )[0];
368              
369             if ( !defined $specfile ) {
370             $logger->log_and_die(
371             level => 'error',
372             message => "Failed to find specfile",
373             );
374             }
375              
376             # Rebuild the SRPM
377              
378             my $rpmbuild_output;
379             my $rpmbuild_ok = eval {
380             IPC::Run::run(
381             [ $RPMBUILD_BIN, '-bs', '--nodeps', @defs, $specfile ],
382             \undef, '>&', \$rpmbuild_output )
383             };
384              
385             if ( !$rpmbuild_ok || $EVAL_ERROR ) {
386             $logger->error($EVAL_ERROR) if $EVAL_ERROR;
387             $logger->log_and_die(
388             level => 'error',
389             message => "rpmbuild failed: $rpmbuild_output",
390             );
391             }
392              
393             my $new_srpm = ( glob "$dirs{SRPMS}/*.src.rpm" )[0];
394              
395             if ( !defined $new_srpm ) {
396             $logger->log_and_die(
397             level => 'error',
398             message => "Failed to find the rebuilt SRPM"
399             );
400             }
401              
402             File::Copy::copy( $new_srpm, $resultsdir ) or
403             $logger->log_and_die(
404             level => 'error',
405             message => "Could not copy $new_srpm to $resultsdir: $OS_ERROR",
406             );
407              
408             my $new_srpm_file = ( File::Spec->splitpath($new_srpm) )[2];
409              
410             my $new_source = eval {
411             PkgForge::Source::SRPM->new( file => $new_srpm_file,
412             basedir => $resultsdir )
413             };
414              
415             if ( !defined $new_source || $EVAL_ERROR ) {
416             $logger->log_and_die(
417             level => 'error',
418             message => "Failed to load $new_srpm_file as PkgForge::Source::SRPM object: $EVAL_ERROR",
419             );
420             }
421              
422             push @updated_sources, $new_source;
423             }
424              
425             $buildinfo->sources(\@updated_sources);
426              
427             return;
428              
429             }
430              
431             sub build {
432             my ( $self, $job, $buildinfo, $buildlog ) = @_;
433              
434             my $logger = $buildlog->logger;
435              
436             $self->fix_sources( $buildinfo, $logger );
437              
438             my $result;
439             if ( $self->use_mock ) {
440             $result = $self->mock_run( $job, $buildinfo, $logger );
441             } else {
442             die "Only mock supported right now\n";
443             }
444              
445             return $result;
446             }
447              
448             1;
449             __END__
450              
451             =head1 NAME
452              
453             PkgForge::BuildCommand::Builder::RPM - A PkgForge class for building RPMs
454              
455             =head1 VERSION
456              
457             This documentation refers to PkgForge::BuildCommand::Builder::RPM version 1.1.10
458              
459             =head1 SYNOPSIS
460              
461             use PkgForge::Job;
462             use PkgForge::BuildCommand::Builder::RPM;
463             use PkgForge::BuildInfo;
464              
465             my $builder = PkgForge::BuildCommand::Builder::RPM->new( platform => 'f13',
466             architecture => 'x86_64' );
467              
468             my $verified = eval { $self->builder->verify_environment };
469             if ( $verified && !$@ ) {
470              
471             my $job = PkgForge::Job->new_from_dir($job_dir);
472              
473             my $buildinfo = PkgForge::BuildInfo->new( jobid => $job->id );
474              
475             $builder->run( $job, $buildinfo );
476              
477             }
478              
479             =head1 DESCRIPTION
480              
481             This is a Package Forge builder class for building RPMs from source
482             using mock.
483              
484             =head1 ATTRIBUTES
485              
486             This inherits most attributes from the L<PkgForge::BuildCommand::Builder> role. This
487             class has the following extra attributes:
488              
489             =over
490              
491             =item use_mock
492              
493             This is a boolean value which controls whether mock should be used to
494             build packages. Currently this is the only supported build tool so the
495             default is true.
496              
497             =item rpm_api_version
498              
499             This is a string which contains the rpmlib version number. This is
500             discovered using the C<rpm_api_version> method in the L<RPM2> Perl
501             module.
502              
503             =back
504              
505             =head1 SUBROUTINES/METHODS
506              
507             This inherits some methods from the L<PkgForge::BuildCommand::Builder> role. The
508             class has the following extra methods:
509              
510             =over
511              
512             =item build( $job, $buildinfo )
513              
514             This is the main method which drives the building of RPMs. It takes a
515             L<PkgForge::Job> object and a L<PkgForge::BuildInfo>
516             object. Currently, only the mock build method is supported so the Job
517             and the BuildInfo objects are passed into the C<mock_run> method.
518              
519             =item verify_environment()
520              
521             This method ensures that the C<mock> and C<pkgsubmit> tools are
522             available. If anything is missing this method will die. This is not
523             called automatically, if you need to run this check you need to do
524             that yourself before calling C<build>.
525              
526             =item mock_run( $job, $buildinfo )
527              
528             This is the main mock build method. It takes a L<PkgForge::Job> object
529             and a L<PkgForge::BuildInfo> object. It will attempt to build each
530             source package in turn. If createrepo is being used then generated
531             packages can be used as build-dependencies for later packages in the
532             job as soon as they are successfully built. If any packages fail to
533             build the job will be considered a failure and this method will log
534             the errors and return 1.
535              
536             =item mock_query( $chroot, $key )
537              
538             This will query the specified configuration option for the specified
539             mock chroot and return the value. It does this using a rather hacky
540             python script, named C<mock_config_query>, which relies on loading the
541             mock python code in a slightly odd way (BE WARNED, this might explode
542             at any moment). This method will die if it cannot find a value for the
543             specified key.
544              
545             =item mock_chroot_name($bucket)
546              
547             This takes the name of the target package bucket and returns the name
548             of the mock chroot based on the builder platform, architecture and the
549             bucket being used for the job. The chroot name will be formed like
550             C<platform-bucket-arch>, e.g. f13-lcfg-i386.
551              
552             =item mock_clear_resultsdir($chroot)
553              
554             This will remove all files and directories in the results directory
555             for the specified chroot. Normally this is called before actually
556             running mock so that it starts with a clean environment. This makes it
557             easy to collect the build products and log files. This method will die
558             if it cannot remove all files and directories.
559              
560             =item mock_createrepo($chroot)
561              
562             If the C<createrepo_on_rpms> mock configuration option is set for the
563             specified chroot then this method will run the command which is
564             specified in the mock C<createrepo_command> configuration option. This
565             method will die if anything fails whilst running createrepo.
566              
567             =item mock_init($chroot)
568              
569             This method will initialise the specified mock chroot. Currently this
570             consists of calling C<mock_clear_resultsdir> and C<mock_createrepo>.
571              
572             =item mock_find_logs($chroot)
573              
574             This finds all the log files (i.e. C<*.log>) in the mock results
575             directory for the chroot and returns the list.
576              
577             =item mock_find_results($chroot)
578              
579             This finds all the packages (i.e. C<*.rpm>) in the mock results
580             directory for the chroot and returns the list.
581              
582             =item fix_sources($buildinfo)
583              
584             This is a big ugly horrid hack to work around the fact that SRPMs
585             created on newer platforms with a recent version of rpmlib (4.6 and
586             newer) cannot be used on older platforms. This is, for example,
587             particularly a problem when needing to build from the same source
588             packages on SL5 and F13.
589              
590             The SRPM is installed and unpacked, using C<rpm>, into a temporary
591             directory using the C<--nomd5> option. The SRPM is then regenerated
592             using C<rpmbuild> and copied back into the results directory for the
593             job. A new L<PkgForge::Source::SRPM> object is created for each
594             package and the sources list for the BuildInfo object is updated.
595              
596             =back
597              
598             =head1 CONFIGURATION AND ENVIRONMENT
599              
600             This module does not directly use any configuration files.
601              
602             You will need to ensure you have mock installed and the chroots
603             correctly configured. The mock chroots are expected to be named like
604             C<platform-bucket-arch> (e.g. there might be a
605             C</etc/mock/f13-lcfg-i386.cfg> file). If you use LCFG to manage your
606             configuration you can use the mock component to do this for you.
607              
608             You will also need the C<pkgsubmit> tool for submitting the built
609             packages. There should be a pkgsubmit configuration for each supported
610             platform/architecture combination, they must be named like
611             C<platform-arch.conf>, e.g. C</etc/pkgsubmit/f13-i386.conf>
612              
613             =head1 DEPENDENCIES
614              
615             This module is powered by L<Moose> and uses L<MooseX::Types>. It also
616             requires L< File::Find::Rule>, L<IPC::Run>, L<RPM2> and L<Readonly>.
617              
618             =head1 SEE ALSO
619              
620             L<PkgForge>, L<PkgForge::Job>, L<PkgForge::BuildCommand::Builder>, L<PkgForge::BuildInfo>
621              
622             =head1 PLATFORMS
623              
624             This is the list of platforms on which we have tested this
625             software. We expect this software to work on any Unix-like platform
626             which is supported by Perl.
627              
628             ScientificLinux5, Fedora13
629              
630             =head1 BUGS AND LIMITATIONS
631              
632             Please report any bugs or problems (or praise!) to bugs@lcfg.org,
633             feedback and patches are also always very welcome.
634              
635             =head1 AUTHOR
636              
637             Stephen Quinney <squinney@inf.ed.ac.uk>
638              
639             =head1 LICENSE AND COPYRIGHT
640              
641             Copyright (C) 2010-2011 University of Edinburgh. All rights reserved.
642              
643             This library is free software; you can redistribute it and/or modify
644             it under the terms of the GPL, version 2 or later.
645              
646             =cut