The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

##- Nanar <nanardon@zarb.org>
##-
##- 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 <nanardon@zarb.org>

=cut