#!/usr/bin/perl ##- Nanar ##- ##- This program is free software; you can redistribute it and/or modify ##- it under the terms of the GNU General Public License as published by ##- the Free Software Foundation; either version 2, or (at your option) ##- any later version. ##- ##- This program is distributed in the hope that it will be useful, ##- but WITHOUT ANY WARRANTY; without even the implied warranty of ##- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ##- GNU General Public License for more details. ##- ##- You should have received a copy of the GNU General Public License ##- along with this program; if not, write to the Free Software ##- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # $Id$ use strict; use warnings; use Getopt::Long; use POSIX (qw/nice O_WRONLY O_CREAT O_APPEND/); my $ipcstat; if (($ARGV[0] || "") eq '--trap') { shift @ARGV; my $trap = shift(@ARGV) or die "No log file specified\n"; $0 = "hrpmreb [trapping]"; open (my $t, ">>", $trap) or exit(1); printf $t "\n\nRunning: %s\n\n", join(" ", @ARGV); my $cmd = join(" ", @ARGV); open(my $htrap, '-|', "$cmd 2>&1") or do { print $t "Can't run cmd $cmd: $!\n"; exit(1); }; { $| = 1; my $oldfh = select($t); $| =1; select($oldfh); } while(<$htrap>) { print $_; print $t $_; } close($t); exit(!close($htrap)); } if (($ARGV[0] || '') eq '--stat') { require IPC::ShareLite; foreach my $k (1000 .. 1020) { $ipcstat = new IPC::ShareLite ( -key => $k, -create => 'no', -destroy => 'no', ) or last; $ipcstat->lock(); my @ipcs = map { pack("h*", $_) } split(/ /, $ipcstat->fetch()); printf "Building: %s for %ds\n", shift(@ipcs), time - shift(@ipcs); $ipcstat->unlock(); show_stat(@ipcs); } exit(0); } =head1 NAME hrpmreb =head1 DESCRIPTION A very powerful rpm rebuilder using perl-RPM4 =head1 SYNOPSYS hrpmreb -m macros rpm.src.rpm =head1 OPTIONS =head2 -m|--macros macrosfile Read a rpm macros file and add it to rpm configuration. To read several macros files, use -m macro1 -m macro2 ... =head2 -D|--define "macro value" Add a new macro to rpm configuration: example: -D "_sourcedir /tmp" This option can be used several time. =head2 -b|--batch Rebuild rpms found in L<%bindir> instead of the one given on command line =head2 -v|--verbose LEVEL Set rpm verbosity level to LEVEL. LEVEL can be an integer value (0 to 8) or a string value like "ERR", "DEBUG" or "INFO". =head2 --noupload Don't upload rpm after build. This option has the same effect than defining L<%upload> to 0. =head2 --nochkbin Don't check if binary already exist This option has the same effect that defining L<%checkbinary> to 0. =head2 --noinstdep Don't install dependancies needed to build the rpm This option has the same effect than defining L<%installdep> to 0. =head2 --keepalllog Don't delete log if build was successful. This option has the same effect than defining L<%keepalllog> to 1. =head2 --nobuild Skip build (and upload) stage, usefull for testing =head2 --nosort Do not sort srpms by builddate. This option has the same effect than defining L<%sortbybuilddate> to 0. =head2 --livestat Enable livestat functionnalities, see L<%livestat>, L<--stat>. =head2 --stat Give the statistics of current hrpmreb running on the current computer if they has been started with livestat functionnality. =cut my (@define, @with, @without); GetOptions( 'm|macros=s' => \my @macros, 'D|define=s' => \@define, 'b|batch' => \my $batch, 'v|verbose=s' => \my $verbose, 'nobuild' => \my $nobuild, 'nosort' => sub { push @define, "sortbybuilddate 0"; }, 'noupload' => sub { push @define, "upload 0"; }, 'nochkbin' => sub { push @define, "checkbinary 0"; }, 'noinstdep' => sub { push @define, "installdep 0"; }, 'keepalllog' => sub { push @define, "keepalllog 1"; }, 'trap=s' => sub { die "--trap should be first arg and you don't have to use it...\n" }, 'livestat' => sub { push @define, "livestat 1"; }, 'with=s' => \@with, 'without=s' => \@without, 'nofork' => \my $nofork, 'dump' => \my $dumpconfig, ); push @define, "_with_$_ --with-$_" foreach (@with); push @define, "_without_$_ --without-$_" foreach (@without); =head1 MACROS =head2 %myself The program itself ($0) =head2 %checkbinary If set, check if binary does not exists in L<%bindir> =head2 %srcdir A list of path separated by ':' where src.rpm should be found =head2 %bindir A list of path separated by ':' where binary rpm should be found =head2 %installdep If set, try to install dependancies (see L<%installrpmcmd>) =head2 %installdepcmd The command to run to install dependancies (urpmi ...) =head2 %upload If set, run the L<%uploadcmd> command =head2 %uploadcmd The upload command to run to upload binary rpms =head2 %logdir Where logfile should be put =head2 %keepalllog Don't delete log even on build success =head2 %nice Renice the build process =head2 %createrpmsdir If set, it will try to create rpms build directory =head2 %sortbybuilddate If define, sort srpms by builddate before build =head2 %livestat if set, hrpmreb use IPC to store his status, then you'll be able to use hrpmreb --stat to immediatelly get status of the current build. =cut require RPM4; RPM4::setverbosity($verbose || "INFO"); my $hlog = undef; RPM4::setlogcallback( sub { my %arg = @_; loging("%s", $arg{msg}); } ); sub loging { my ($fmt, @var) = @_; printf STDERR $fmt, @var; if ($hlog) { printf $hlog $fmt, @var; } } config_macros(); if (RPM4::expandnumeric("%livestat")) { require IPC::ShareLite; foreach my $k (1000 .. 1020) { $ipcstat = new IPC::ShareLite ( -key => $k, -create => 'yes', -destroy => 'no', -mode => 0644, ) and last; } $ipcstat or die "no IPC availlable\n"; } my %status = ( srpmdone => 0, srpmfailure => 0, srpmtotal => 0, steptime => time, currentsrpms => 'Preparing build', currentstep => '', ); my $passphrase = RPM4::expand('%{?gpgpass}'); my @srcdir = split(':', RPM4::expand('%{?srcdir}')); print RPM4::expand("Source dir: %{?srcdir}%{?!srcdir:(none)}\n") if ($batch); !@srcdir && $batch and die "No src dir, please define \%srcdir\n"; my @bindir = split(':', RPM4::expand('%{?bindir}')); if (RPM4::expandnumeric('%checkbinary')) { if (@bindir) { print RPM4::expand("Using binary dir: %{?bindir}%{?!bindir:(none)}\n"); } else { die "No bin dir, please define \%bindir or unset \%checkbinary\n"; } } if ($dumpconfig) { RPM4::dumprc(\*STDOUT); exit 0; } -d RPM4::expand("%{?logdir}%{!?logdir:.}") or mkdir RPM4::expand("%{?logdir}%{!?logdir:.}") or die "Can't create logdir ". RPM4::expand("%{?logdir}%{!?logdir:.}") . " $!\n"; if (RPM4::expandnumeric('%createrpmsdir')) { foreach my $macro (qw(%_topdir %_sourcedir %_builddir %_srpmdir %_rpmdir %_rpmdir/noarch %_rpmdir/%_target_cpu %_specdir %_tmppath)) { -d RPM4::expand($macro) or mkdir RPM4::expand($macro) or die "Can't create dir " . RPM4::expand($macro) . "($macro): $!"; } } set_ipc_status(currentstep => "searching srpms"); my @srpmstobuild; my @specstobuild; if($batch) { @srpmstobuild = map { glob("$_/*.src.rpm") } @srcdir; } foreach my $arg (@ARGV) { -f $arg or do { push(@srpmstobuild, map { glob("$_/$arg") } @srcdir); next; }; if ($arg =~ /\.src\.rpm$/) { push @srpmstobuild, $arg; } else { push @specstobuild, $arg; }; } $status{srpmtotal} = scalar(@srpmstobuild) + scalar(@specstobuild); if (! $status{srpmtotal}) { die sprintf("Nothing to do, %s\n", $batch ? "check \%srcdir value" : "give src.rpm too rebuild or use -b"); } if (RPM4::expandnumeric("%sortbybuilddate")) { loging("Sorting srpms by buildate\n"); set_ipc_status(currentstep => "sorting srpms"); @srpmstobuild = sort_srpms(@srpmstobuild); } # readjusting $status{srpmtotal} = scalar(@srpmstobuild) + scalar(@specstobuild); foreach my $specfile (@specstobuild) { my $pid = fork(); if ($pid) { parent_wait_child($pid); $? and $status{srpmfailure}++; show_stat($status{srpmtotal}, ++$status{srpmdone}, $status{srpmfailure}); config_macros(); } else { my $oldcmd = RPM4::expand("%___build_cmd"); RPM4::add_macro("___build_cmd %myself --trap %_tmppath/%name-%version-%release.build.log $oldcmd"); exit(build_specfile($specfile)); } } foreach my $srpm (@srpmstobuild) { my $pid = fork(); if ($pid) { parent_wait_child($pid); $? and $status{srpmfailure}++; show_stat($status{srpmtotal}, ++$status{srpmdone}, $status{srpmfailure}); config_macros(); } else { my $oldcmd = RPM4::expand("%___build_cmd"); RPM4::add_macro("___build_cmd %myself --trap %_tmppath/%name-%version-%release.build.log $oldcmd"); exit(build_srcrpm($srpm)); } } sub parent_wait_child { my ($pid) = @_; my $subdie = sub { kill 15, $pid; waitpid $pid, 0; exit(1); }; local $SIG{'TERM'} = $subdie; local $SIG{'INT'} = $subdie; waitpid $pid, 0; } sub show_advance { my ($end, $total, $done) = @_; printf(" %5d / %5d [%-50s] %3d %%%s", $done, $total, '#' x ($total ? ($done * 50 / $total) : 0), ($total ? ($done * 100 / $total) : 0), $end); } sub show_stat { my ($total, $done, $failure) = @_; print "\nDone:\n"; show_advance("\n", $total, $done); print "Failed:\n"; show_advance("\n\n", $done, $failure); } sub set_ipc_status { my (%val) = @_; foreach (keys %val) { $status{$_} = $val{$_}; /currentsrpms/ and do { $status{steptime} = time; $status{currentstep} = ''; }; } $ipcstat or return; $ipcstat->lock(); $ipcstat->store(sprintf( "%s %s %s %s %s", unpack("h*", "$status{currentsrpms} ($status{currentstep})"), unpack("h*", $status{steptime}), unpack("h*", $status{srpmtotal}), unpack("h*", $status{srpmdone}), unpack("h*", $status{srpmfailure}) )); $ipcstat->unlock(); } sub sort_srpms { my %s; my $db = RPM4::newdb(); $db->vsflags([ qw(NOSIGNATURES NOPAYLOAD NODIGESTS) ]); my $done = 0; my @specs; foreach my $src (@_) { my $h = $db->rpm2header($src) or next; $s{$src} = $h->tag("BUILDTIME") || 0; show_advance("\r", scalar(@_), ++$done); } print "\n"; return sort { $s{$a} <=> $s{$b} } keys %s; } sub config_macros { RPM4::resetmacros(); RPM4::add_macro("logfileformat %name.log"); RPM4::readconfig(); RPM4::loadmacrosfile($_) foreach (@macros); RPM4::add_macro($_) foreach(@define); RPM4::add_macro("myself $0"); } sub checkbinary { my ($spec) = @_; RPM4::expandnumeric('%checkbinary') or return 1; my $missing = 0; my @bin = $spec->binrpm(); foreach my $r (@bin) { $r =~ s!^.*/!!; my @rfake; if (my ($rp) = $r =~ /^(.*)(?:amd64|x86_64)\.rpm/) { push @rfake, $rp."x86_64.rpm", $rp."amd64.rpm"; } else { push @rfake, $r; } my $ok = 0; foreach my $rp (@rfake) { if (grep { -f "$_/$rp" } @bindir) { loging("$rp found\n"); $ok = 1; last; } } if (!$ok) { loging("$r not found (need build)\n"); $missing++; } } return 0 if ($missing == 0); return RPM4::expandnumeric('%checkbinary') <= ($missing == scalar(@bin) ? 2 : 1); } sub build_srcrpm { my ($srpm) = @_; my ($specf, $cookies) = RPM4::installsrpm($srpm) or return 1; my $rc = 0; my $spec; if ($spec = RPM4::specnew($specf, $passphrase, "/", $cookies, 0, 0)) { open($hlog, ">", RPM4::expand('%{?logdir:%{logdir}/}%{logfileformat}')); $0 = RPM4::expand("hrpmreb [%name-%version-%release]"); set_ipc_status(currentsrpms => RPM4::expand("%name-%version-%release")); $SIG{'TERM'} = sub { $spec && $spec->build(["RMSOURCE", "RMSPEC", "RMBUILD"]); $spec && $spec->build(["CLEAN"]); }; if (checkbinary($spec)) { $rc = build_spec($spec, $srpm); upload_build($spec, 0) unless($rc); } else { loging("Found binary, skipping build\n"); } if ($rc || RPM4::expandnumeric('%keepalllog')) { if (open(my $buildlog, "<", RPM4::expand("%_tmppath/%name-%version-%release.build.log"))) { while (<$buildlog>) { print $hlog $_; } close($buildlog); } } else { unlink(RPM4::expand("\%{?logdir:%{logdir}/}%{logfileformat}")); } close($hlog); $hlog = undef; } else { $rc = 1; } $spec ||= RPM4::specnew($specf, $passphrase, "/", $cookies, 1, 1); if ($spec) { set_ipc_status(currentstep => "cleaning"); loging("cleaning sources, spec\n"); $spec && $spec->build(["RMSOURCE", "RMSPEC", "RMBUILD"]); } unlink(RPM4::expand("%_tmppath/%name-%version-%release.build.log")); return $rc; } sub build_specfile { my ($specf) = @_; my $rc = 0; my $spec; if ($spec = RPM4::specnew($specf, $passphrase, "/", undef, 0, 0)) { open($hlog, ">", RPM4::expand('%{?logdir:%{logdir}/}%{logfileformat}')); $0 = RPM4::expand("hrpmreb [%name-%version-%release]"); set_ipc_status(currentsrpms => RPM4::expand("%name-%version-%release")); $SIG{'TERM'} = sub { $spec && $spec->build(["RMBUILD"]); }; $spec->build(["PACKAGESOURCE"]); my $srpm = $spec->srcrpm(); if (checkbinary($spec)) { $rc = build_spec($spec, $srpm); upload_build($spec, 1) unless($rc); } else { loging("Found binary, skipping build\n"); } if ($rc || RPM4::expandnumeric('%keepalllog')) { if (open(my $buildlog, "<", RPM4::expand("%_tmppath/%name-%version-%release.build.log"))) { while (<$buildlog>) { print $hlog $_; } close($buildlog); } } else { unlink(RPM4::expand("\%{?logdir:%{logdir}/}%{logfileformat}")); } close($hlog); $hlog = undef; } else { $rc = 1; } $spec ||= RPM4::specnew($specf, $passphrase, "/", undef, 1, 1); if ($spec) { set_ipc_status(currentstep => "cleaning"); loging("cleaning sources, spec\n"); $spec && $spec->build(["RMBUILD"]); } unlink(RPM4::expand("%_tmppath/%name-%version-%release.build.log")); return $rc; } sub build_spec { my ($spec, $srpm) = @_; my ($rc, $starttime, $chkdeptime, $installdeptime, $uploadtime, $buildtime, $endtime) = (0); $starttime = time; if (RPM4::expandnumeric('%installdep') && $srpm) { set_ipc_status(currentstep => "installing dep"); runmacro("%installdepcmd", $srpm); $installdeptime = time; } nice(RPM4::expand("%{?nice}")) if (RPM4::expandnumeric("%{?nice:1}")); set_ipc_status(currentstep => "checking dep"); my $pbs; { my $db = RPM4::newdb(); my $sh = $spec->srcheader(); $db->transadd($sh, "", 0); $db->transcheck; $pbs = $db->transpbs(); } $chkdeptime = time; if ($pbs) { $pbs->init; loging("\nMissing dependancies:\n"); while($pbs->hasnext) { loging($pbs->problem() . "\n"); } $rc = 1; } elsif(! $nobuild) { set_ipc_status(currentstep => "compiling"); $rc = $spec->build([ qw/PREP BUILD INSTALL CHECK FILECHECK PACKAGEBINARY/ ]); $spec->build([ qw/CLEAN/ ]); $buildtime = time; } $endtime = time; loging("\nBuild time in sec:\n"); loging("%20s %5s\n", 'installdepcmd: ', defined($installdeptime) ? $installdeptime - $starttime : "N/A"); loging("%20s %5s\n", 'check dep: ', $chkdeptime - ($installdeptime || $starttime)); loging("%20s %5s\n", 'build: ', defined($buildtime) ? $buildtime - $chkdeptime : "N/A"); loging("%20s %5s\n", 'Total: ', $endtime - $starttime); loging("Build exit code: $rc\n"); return $rc; } sub upload_build { my ($spec, $uploadsrc) = @_; my @buildrpms = $spec->binrpm; unshift @buildrpms, $spec->srcrpm if($uploadsrc); my $noexist = 0; foreach (@buildrpms) { if(-f $_) { loging("%s has been built\n", $_); } else { loging("%s has not been built\n", $_); $noexist++; } } $noexist and return 1; if (RPM4::expandnumeric('%upload')) { set_ipc_status(currentstep => "uploading"); !runmacro("%uploadcmd", @buildrpms) and unlink @buildrpms; } return 0; } sub runmacro { my ($macro, @args) = @_; my $cmd = RPM4::expand($macro. " " . join(' ', @args)); $cmd =~ /^\Q$macro/ and return 1; loging("Executing(%%%s) %s\n", $macro, $cmd); system($cmd) } __END__ =head1 AUTHOR Olivier Thauvin =cut