#!/usr/bin/perl
#
use strict;
use warnings;
use File::Spec::Functions;

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC,  "$wd/build";
  unshift @INC,  "$wd";
  $ENV{VIRTUAL_ENV} = "/" if ! defined $ENV{VIRTUAL_ENV};
  unshift @INC,  canonpath("$ENV{VIRTUAL_ENV}/usr/lib/build");
}

use YAML qw(LoadFile);
use threads;
use threads::shared;
use File::Find ();
use Term::ANSIColor qw(:constants);
use File::Path;
use File::Basename;
use URI;
use POSIX ":sys_wait_h";
use File::Glob ':glob';
use User::pwent qw(getpw);
# Global vars


# Flag to inform all threads that application is terminating
my $TERM:shared=0;

# Prevents double detach attempts
my $DETACHING:shared;

# Flag to inform main thread update pkgdeps
my $dirty:shared=0;

# Set the variable $File::Find::dont_use_nlink if you're using AFS,
# since AFS cheats.

# for the convenience of &wanted calls, including -eval statements:
use vars qw/*name *dir *prune/;
*name   = *File::Find::name;
*dir    = *File::Find::dir;
*prune  = *File::Find::prune;

my ($zuid, $zgid);
if (getlogin()) {
     ($zuid, $zgid) = (getpwnam(getlogin()))[2,3];
} else {
     ($zuid, $zgid) = (getpwuid($<))[2,3];
}


use Cwd qw(cwd abs_path);
use Getopt::Long;
use Pod::Usage;
use File::Temp qw(tempfile);
use Build;
use Build::Rpm;
use Data::Dumper;
use File::Basename;

# "sudo -v" period
use constant SUDOV_PERIOD => 300;

my @threads;
my @exclude = ();
my @repos= ();
my $arch = "i586";
my $path = "";
my $style = "git";
my $clean = 0;
my $binarylist = "";
my $buildall = 0;
my $commit = "";
my $includeall = 0;
my $upstream_branch = "";
my $upstream_tag = "";
my $squash_patches_until = "";
my $packaging_dir = "packaging";
my $dist = "tizen";
my $dryrun = 0;
my $help = 0;
my $keepgoing = 0;

my $virtualenv = "$ENV{'VIRTUAL_ENV'}";
my $build_root = $ENV{TIZEN_BUILD_ROOT};
$build_root = expand_filename($build_root);
my $localrepo = "$build_root/local/repos";
my $order_dir = "$build_root/local/order";


my $cache_dir = "$build_root/local/cache";
my $groupfile="$build_root/meta/group.xml";
my $build_dir = canonpath("$virtualenv/usr/lib/build");
$ENV{'BUILD_DIR'} = $build_dir; # must change env variable in main thread
my $config_filename = "$build_root/meta/local.yaml";
my $dist_configs = "$build_root/meta/dist";
my $exclude_from_file = "$build_root/meta/exclude";
my $cleanonce = 0;
my $debug = 0;
my $incremental = 0;
my $run_configure = 0;
my $overwrite = 0;
my $MAX_THREADS = 1;
my $extra_packs = "";
my $ccache = 0;

my @tofind = ();
my %to_build = ();
my %repo = ();
my %pkgdeps = ();
my %pkgddeps = (); # direct dependency dict
my %visit    = ();
my @running :shared = ();
my @done :shared = ();
my @skipped = ();
my @original_specs = ();

my @cleaned : shared = ();
my %errors :shared;
my %expansion_errors = ();
my @export_errors;
my %tmp_expansion_errors = ();
my $packages_built :shared  = 0;
my %workers = ();


GetOptions (
    "repository=s" => \@repos,
    "arch=s" => \$arch,
    "dist=s" => \$dist,
    "configdir=s" => \$dist_configs,
    "clean" => \$clean,
    "clean-once" => \$cleanonce,
    "exclude=s" => \@exclude,
    "exclude-from-file=s" => \$exclude_from_file,
    "build-all" => \$buildall,
    "commit=s" => \$commit,
    "include-all" => \$includeall,
    "upstream-branch=s" => \$upstream_branch,
    "upstream-tag=s" => \$upstream_tag,
    "squash-patches-until=s" => \$squash_patches_until,
    "packaging-dir=s" => \$packaging_dir,
    "binary=s" => \$binarylist,
    "style=s" => \$style,
    "path=s" => \$path,
    "dryrun" => \$dryrun,
    "help|?" => \$help,
    "keepgoing" => \$keepgoing,
    "overwrite" => \$overwrite,
    "debug" => \$debug,
    "incremental" => \$incremental,
    "no-configure" => \$run_configure,
    "threads=s" => \$MAX_THREADS,
    "extra-packs=s" => \$extra_packs,
    "ccache" => \$ccache,
    );

if ( $help ) {
    print "
Depanneur is a package build tool based on the obs-build script.

Available options:

    --arch <Architecture>
      Build for the specified architecture.

    --dist <Distribution>
      Build for the specified distribution.

    --path <path to sources>
      Path to git repo tree, default is packages/ sub-directory
      in the developer environment.

    --clean
      clean the build environment before building a package.

    --clean-once
      clean the build environment only once when you start
      building multiple packages, after that use existing
      environment for all packages.

    --threads  [number of threads]
      Build packages in parallel. This will start up to the
      specified number of build jobs when there are more
      than 1 job in the queue.

    --overwrite
      Overwrite existing binaries.

    --keepgoing
      If a package build fails, do not abort and continue
      building other packages in the queue.

    --incremental
      Build a package from the local git tree directly.
      This option does not produce packages now, it is very
      helpful when debugging build failures and helps with
      speeding up development.
      This option options mounts the local tree in the build
      environment and builds using sources in the git tree,
      if the build fails, changes can be done directly to the
      source and build can continue from where it stopped.

    --no-configure
      This option disables running configure scripts and auto-
      generation of auto-tools to make incremental build possible
      It requires the configure scripts in the spec to be refereneced
      using the %configure, %reconfigre and %autogen macros.

    --debug
      Debug output.

";
    exit(0);
}

sub debug {
    my $msg = shift;
    $msg =~ s#://[^@]*@#://#g;
    print MAGENTA, "debug: ", RESET, "$msg\n" if $debug == 1;
}

sub info {
    my $msg = shift;
    print GREEN, "info: ", RESET, "$msg\n";
}

sub warning {
    my $msg = shift;
    print YELLOW, "warning: ", RESET, "$msg\n";
}

sub error {
    my $msg = shift;
    print RED, "error: ", RESET, "$msg\n";
    exit 1;
}

sub my_system {
    my $cmd = shift;
    debug("my_system: $cmd");
    my $ret;
    defined(my $pid=fork) or die "Can not fork: $!\n";
    unless ($pid) {
        exec ($cmd);
        exit -1;
    }
    waitpid ($pid,0);
    $ret = WIFEXITED($?);
    $ret = $?;
    return $ret;
}

sub expand_filename {
    my $path = shift;
    my $home_dir = sub { my $p = getpw($_[0]) or die "$_[0] is not a valid username\n";
                         return $p->dir();
                       };
    $path =~ s{^~(?=/|$)}{ $ENV{HOME} ? "$ENV{HOME}" : $home_dir->( $< ) }e
          or $path =~ s{^~(.+?)(?=/|$)}{ $home_dir->( $1 ) }e;
    return $path;
}

if ($incremental == 1 && $style ne 'git') {
    error("incremental build only support git style packages");
}
if ($style ne 'git' && $style ne 'obs') {
    error("style should be 'git' or 'obs'");
}

my @package_repos = ();
my $Config;
if (-e $config_filename) {
    $Config = LoadFile($config_filename);
    if (!$Config) {
        error("Error while parsing $config_filename");
    }
}

if (@repos) {
    @package_repos = @repos;
} else {
    if ($Config){
        foreach my $r (@{$Config->{Repositories}}) {
            my $uri = URI->new($r->{Url});
            if ( $r->{Password} && $r->{Username} ) {
                $uri->userinfo($r->{Username} . ":" . $r->{Password});
            }
            if ($uri->scheme ne "file") {
                push(@package_repos, $uri);
            }
        }
    }
}

my $pkg_path = "$build_root/local/sources/$dist";
my $cache_path = "$build_root/local/sources/$dist/cache";
my $scratch_dir = "$build_root/local/scratch.$arch";


sub mkdir_p {
    my $path = shift;
    my $err_msg;
    # attempt a 'mkdir -p' on the provided path and catch any errors returned
    my $mkdir_out = File::Path::make_path( $path, { error => \my $err } );
    # catch and return the error if there was one
    if (@$err) {
        for my $diag (@$err) {
            my ( $file, $message ) = %$diag;
            $err_msg .= $message;
        }
        print STDERR "$err_msg";
    }
}

if ( $exclude_from_file ne "" && -e $exclude_from_file ) {
    debug("using $exclude_from_file for package exclusion");
    open my $file, '<', $exclude_from_file  or die $!;
    @exclude = <$file>;
    chomp(@exclude);
    close($file);
}


mkdir_p($order_dir);
mkdir_p "$localrepo/$dist/$arch/logs/success";
mkdir_p "$localrepo/$dist/$arch/logs/fail";
mkdir_p($cache_path);

my @packs;
my $package_path = "";
# FIXME

my @arm_archs = ( "armv7el" , "armv7l", "noarch");
my @ix86_archs = ("i386", "i586", "i686", "noarch");
my @archs = ();
if ( $arch eq "i586" ) {
    @archs = @ix86_archs;
} else {
    @archs = @arm_archs;
}
my $archpath;
foreach my $ap (@archs) {
    $archpath .= $ap.":";
}
my $config = Build::read_config_dist($dist, $archpath, $dist_configs);

if ( -d "$packaging_dir" && -d ".git" ) {
    $package_path = cwd();
} else {
    if ( $path eq "" ) {
        $package_path = "$build_root/packages";
    } else {
        $package_path = abs_path($path);
    }
}
if ($binarylist ne "") {
    $buildall = 1;
}

sub git_wanted {
    fill_packs_from_git($name) if /^($packaging_dir)\z/s && -d $_;
}

sub obs_wanted {
    /^.*\.spec\z/s && fill_packs_from_obs($name);
}

sub fill_packs_from_obs {
    my $name = shift;
    # exclude spec file that in .osc subdirs
    $name =~ m/\.osc/ || push(@packs, $name);
}


sub fill_packs_from_git {
    my $name = shift;
    my $base = dirname($name);
    my $prj = basename($base);
    if ( ! -e "$base/.git" ) {
        debug("$base is not a git checkout");
        return;
    }
    if ( (grep $_ eq $prj, @exclude) ) {
        return;
    }
    debug("working on $base");
    my $pattern = "$name/*.spec";
    push(@original_specs, glob($pattern));
}

sub gbs_export {
    my ($base, $spec) = @_;
    my @args = ();
    my $cmd;
    push @args, "gbs";
    push @args, "--debug" if ($debug);
    push @args, "export";
    push @args, "$base";
    push @args, "-o $pkg_path";
    push @args, "--spec $spec";
    if ($includeall == 1) {
        push @args, "--include-all";
    } elsif ($commit ne "") {
        push @args, "--commit=$commit";
    }
    if (! $upstream_branch eq "") {
        push @args, "--upstream-branch=$upstream_branch";
    }
    if (! $upstream_tag eq "") {
        push @args, "--upstream-tag=$upstream_tag";
    }
    if (! $squash_patches_until eq "") {
        push @args, "--squash-patches-until=$squash_patches_until";
    }
    if (! $packaging_dir eq "") {
        push @args, "--packaging-dir=$packaging_dir";
    }
    $cmd = join(" ", @args);
    return my_system($cmd);
}

sub read_cache {
    my ($cache_key) = @_;
    my $cache_fname = "$cache_path/$cache_key";

    my $cache = '';
    if (-e $cache_fname) {
        open(my $rev, '<', $cache_fname) ||
            die "read reversion cache($cache_fname) failed: $!";
        $cache = readline($rev);
        close($rev);
        chomp $cache;
    }
    return $cache;
}

sub write_cache {
    my ($cache_key, $cache_val, $base, $spec) = @_;
    my $cache_fname = "$cache_path/$cache_key";

    if (gbs_export($base, $spec) != 0) {
        push(@export_errors, $cache_key);
        return;
    }

    my $pattern = "$localrepo/$dist/src/SRPMS/$cache_key.*.rpm";
    my @binaries = glob $pattern;
    if (@binaries != 0) {
        # Remove old source rpm packages to build again, or depanneur
        # will skip packages with src.rpm exists
        my_system("rm -f $pattern");
    }

    open(my $rev1, "+>", $cache_fname) ||
        die "write reversion cache($cache_fname) failed: $!";
    print $rev1 $cache_val . "\n";
    close($rev1);
    1;
}

sub clean_cache {
    my ($cache_key) = @_;
    my $cache_fname = "$cache_path/$cache_key";

    unlink $cache_fname;
}

sub query_git_commit_rev {
    my ($base, $commit_id) = @_;

    open(my $git, '-|', "git --git-dir $base/.git rev-parse $commit_id") ||
        die "query git commit reversion($commit_id) failed: $!";
    my $rev = readline($git);
    close($git);
    chomp $rev;
    return $rev;
}

sub prepare_git {
    my $config = shift;
    my $spec = shift;

    my $packaging = dirname($spec);
    my $base = dirname($packaging);
    my $prj = basename($base);

    my $pack = Build::Rpm::parse($config, $spec);
    if (! exists $pack->{name} || ! exists $pack->{version} || ! exists $pack->{release}) {
        debug("failed to parse spec file: $spec, name,version,release fields must be present");
        return;
    }
    my $pkg_name = $pack->{name};
    my $pkg_version = $pack->{version};
    my $pkg_release = $pack->{release};
    my $cache_key = "$pkg_name-$pkg_version-$pkg_release";
    my $spec_file = basename($spec);
    my $skip = 0;
    my $current_rev = '';

    if (! -e "$base/.git") {
        warning("not a git repo: $base/.git!!");
        return;
    } else {
        my $commit_id;
        if ($commit eq "") {
            $commit_id = "HEAD";
        }else{
            $commit_id = $commit;
        }

        $current_rev = query_git_commit_rev($base, $commit_id);

        my $cached_rev = read_cache($cache_key);
        $skip = ($cached_rev eq $current_rev);
    }

    if (!$skip || $includeall == 1) {
        # Set cache_rev as 'include-all' if --include-all specified
        my $val = ($includeall == 1) ? "include-all" : $current_rev;
        info("start export source from: $base ...");
        unless (write_cache($cache_key, $val, $base, $spec)) {
            clean_cache($cache_key);
            debug("$pkg_name was not exported correctly");
            return;
        }
    }
    push(@packs, {
        filename => "$pkg_path/$cache_key/$spec_file",
        project_base_path => $base,
    });
}

sub parse_packs {
    my ($config, @packs) = @_;
    my %packs = ();
    foreach my $spec_ref (@packs) {
        my $spec;
        my $base;
        if (ref($spec_ref) eq "HASH") {
            # project_base_path set in sub prepare_git()
            $spec = $spec_ref->{filename};
            $base = $spec_ref->{project_base_path};
        } else {
            $spec = $spec_ref;
        }
        my $pack = Build::Rpm::parse($config, $spec);
        if ( ( $pack->{'exclarch'} ) &&  ( ! grep $_ eq $archs[0], @{$pack->{'exclarch'}} ) ) {
            debug("arch not compatible");
            next;
        }
        my $name = $pack->{name};
        my $version = $pack->{version};
        my $release = $pack->{release};
        my @buildrequires = $pack->{deps};
        my @subpacks = $pack->{subpacks};
        if ( (grep $_ eq $name, @exclude) ) {
            next;
        }
        $packs{$name} = {
            name => $name,
            version => $version,
            release => $release,
            deps => @buildrequires,
            subpacks => @subpacks,
            filename => $spec,
        };
        if ($base) {
            $packs{$name}{project_base_path} = $base;
        }
    }
    return %packs;
}

sub refresh_repo {
    my $rpmdeps = "$order_dir/.repo.cache";
    my (%fn, %prov, %req);

    my %packs;
    my %ids;

    my %packs_arch;
    my %packs_done;
    open(my $fh, '<', $rpmdeps) || die("$rpmdeps: $!\n");
    # WARNING: the following code assumes that the 'I' tag comes last
    my ($pkgF, $pkgP, $pkgR);
    while(<$fh>) {
      chomp;
      if (/^F:(.*?)-\d+\/\d+\/\d+: (.*)$/) {
        $pkgF = $2;
        next if $fn{$1};
        $fn{$1} = $2;
        my $pack = $1;
        $pack =~ /^(.*)\.([^\.]+)$/ or die;
        push @{$packs_arch{$2}}, $1;
      } elsif (/^P:(.*?)-\d+\/\d+\/\d+: (.*)$/) {
        $pkgP = $2;
        next if $prov{$1};
        $prov{$1} = $2;
      } elsif (/^R:(.*?)-\d+\/\d+\/\d+: (.*)$/) {
        $pkgR = $2;
        next if $req{$1};
        $req{$1} = $2;
      } elsif (/^I:(.*?)-\d+\/\d+\/\d+: (.*)$/) {
        if ($ids{$1} && $packs_done{$1} && defined($pkgF) && defined($pkgP) && defined($pkgR)) {
          my $i = $1;
          my $oldid = $ids{$1};
          my $newid = $2;
          if (Build::Rpm::verscmp($oldid, $newid) < 0) {
            $ids{$i}  = $newid;
            $fn{$i}   = $pkgF;
            $prov{$i} = $pkgP;
            $req{$i}  = $pkgR;
          }
        } else {
          next if $ids{$1};
          $ids{$1} = $2;
        }
        undef $pkgF;
        undef $pkgP;
        undef $pkgR;
      } elsif ($_ eq 'D:') {
        %packs_done = %ids;
      }
    }
    close $fh;

    for my $arch (@archs) {
      $packs{$_} ||= "$_.$arch" for @{$packs_arch{$arch} || []};
    }

    my $dofileprovides = %{$config->{'fileprovides'}};

    for my $pack (keys %packs) {
      my $r = {};
      my (@s, $s, @pr, @re);
      @s = split(' ', $prov{$packs{$pack}} || '');
      while (@s) {
        $s = shift @s;
        next if !$dofileprovides && $s =~ /^\//;
        if ($s =~ /^rpmlib\(/) {
          splice(@s, 0, 2);
          next;
        }
        push @pr, $s;
        splice(@s, 0, 2) if @s && $s[0] =~ /^[<=>]/;
      }
      @s = split(' ', $req{$packs{$pack}} || '');
      while (@s) {
        $s = shift @s;
        next if !$dofileprovides && $s =~ /^\//;
        if ($s =~ /^rpmlib\(/) {
          splice(@s, 0, 2);
          next;
        }
        push @re, $s;
        splice(@s, 0, 2) if @s && $s[0] =~ /^[<=>]/;
      }
      $r->{'provides'} = \@pr;
      $r->{'requires'} = \@re;
      $repo{$pack} = $r;
    }

}

sub expand_deps {
    my $spec = shift;
    my ($packname, $packvers, $subpacks, @packdeps);
    $subpacks = [];

    if ($spec) {
      my $d;
      if ($spec =~ /\.kiwi$/) {
        # just set up kiwi root for now
        $d = {
          'deps' => [ 'kiwi', 'zypper', 'createrepo', 'squashfs' ],
          'subpacks' => [],
        };
      } else {
        $d = Build::parse($config, $spec);
      }
      $packname = $d->{'name'};
      $packvers = $d->{'version'};
      $subpacks = $d->{'subpacks'};
      @packdeps = @{$d->{'deps'} || []};
    }

    Build::readdeps($config, undef, \%repo);

    #######################################################################
    my @extradeps = ();
    my @bdeps = Build::get_build($config, $subpacks, @packdeps, @extradeps);

    return @bdeps;
}

# get direct dependencies of specified package
sub get_deps {
    my $spec = shift;
    my @bdeps;
    my @packdeps;
    my $d = Build::parse($config, $spec);

    @packdeps = @{$d->{'deps'} || []};
    foreach my $pack (@packdeps) {
        my $pkg;
        my $found = 0;
        foreach my $pkg (keys %repo) {
            my @prov = @{$repo{$pkg}->{'provides'}};
            if (grep $_ eq $pack, @prov ){
                push (@bdeps, $pkg);
                last;
            }
        }
    }
    return @bdeps;
}

sub createrepo
{
    my $arch = shift;
    my $dist = shift;
    mkdir_p "$localrepo/$dist/src/SRPMS";
    my_system ("cd $localrepo/$dist/src && rm -rf repodata && createrepo --changelog-limit=0 -q . > /dev/null 2>&1 ") == 0 or die "createrepo failed: $?\n";
    mkdir_p "$localrepo/$dist/$arch/RPMS";
    my_system("touch $localrepo/$dist/$arch/RPMS");

    my $groups = "";
    if ( -e $groupfile ) {
        $groups = " --groupfile=$groupfile ";
    }

    my_system ("cd $localrepo/$dist/$arch && rm -rf repodata && createrepo $groups --changelog-limit=0 -q --exclude 'logs/*rpm' . > /dev/null 2>&1 ") == 0
        or die "createrepo failed: $?\n";
}

sub find_idle {
    my $idle = -1;
    foreach my $w (sort keys %workers) {
        my $tid = $workers{$w}->{tid};
        my $state = $workers{$w}->{state};
        if (! defined(threads->object($tid))) {
            set_idle($w);
            $idle = $w;
            last;
        }
    }
    foreach my $w (sort keys %workers) {
        if ( $workers{$w}->{state} eq 'idle' ) {
            $idle = $w;
            last;
        }
    }
    return $idle;
}

sub set_busy {
    my $worker = shift;
    my $thread = shift;
    $workers{$worker} = { 'state' => 'busy', 'tid' => $thread };
}

sub set_idle {
    my $worker = shift;
    $workers{$worker} = { 'state' => 'idle' , 'tid' => undef};
}

sub source_of {
    my ($sub, %packs) = @_;
    foreach my $x (keys %packs) {
        my @sp = @{$packs{$x}->{subpacks}};
        if (grep $_ eq $sub, @sp ) {
            return $x;
        }
    }
    return;
}

sub update_pkgdeps
{
    %tmp_expansion_errors = ();
    foreach my $name (keys %to_build) {
        next if (defined $pkgdeps{$name});
        if(! (grep $_ eq $name, @skipped)) {
            my $fn = $to_build{$name}->{filename};
            debug("Checking dependencies for $name");
            my @bdeps = expand_deps($fn);
            if (!shift @bdeps ) {
                debug("expansion error");
                debug("  $_") for @bdeps;
                $tmp_expansion_errors{$name} = [@bdeps];
                next;
            }
            my @deps;
            foreach my $depp (@bdeps) {
                my $so = source_of($depp, %to_build);
                if (defined($so) && $name ne $so
                    && (! grep($_ eq $so, @skipped))
                    && (! grep($_ eq $so, @deps))) {
                    push (@deps, $so);
                }
            }
            $pkgdeps{$name} = [@deps];
        }
    }
}

sub update_pkgddeps {
    foreach my $name (keys %to_build) {
        # Skip expansion error packages
        next if (exists $tmp_expansion_errors{$name});
        if(! (grep $_ eq $name, @skipped)) {
            my $fn = $to_build{$name}->{filename};
            my @bdeps = get_deps($fn);
            my @deps;
            foreach my $depp (@bdeps) {
                my $so = source_of($depp, %to_build);
                if (defined($so) && $name ne $so
                    && (! grep($_ eq $so, @skipped))
                    && (! grep($_ eq $so, @deps))
                    && (! exists $tmp_expansion_errors{$so})) {
                    push (@deps, $so);
                }
            }
            $pkgddeps{$name} = [@deps]
        }
    }
}

sub find_circle {
    my (@stack) = @_;
    my $curpkg = $stack[$#stack];

    return 0 if (exists $tmp_expansion_errors{$curpkg});

    my @deps = @{$pkgddeps{$curpkg}};
    my $dep;

    foreach my $dep (@deps) {
        if ($visit{$dep} == 1 && ! (grep $_ eq $dep, @stack)){
            next;
        }
        $visit{$dep} = 1;
        if (grep $_ eq $dep, @stack){
            my @circle = ();
            push @circle, $dep;
            while (@stack) {
                my $cur = pop @stack;
                unshift @circle, $cur;
                last if ($cur eq $dep);
            }
            warning ("circle found: " . join("->", @circle));
            return 1;
        } else {
            push (@stack, $dep);
            return 1 if (find_circle(@stack) == 1);
            pop @stack;
        }
    }

    return 0;
}

sub check_circle {
    my $pkg;
    my $reset_visit = sub {
        for my $pkg (keys %pkgddeps) {
            # Skip expansion error packages
            next if (exists $tmp_expansion_errors{$pkg});
            $visit{$pkg} = 0;
        }
    };
    for $pkg (keys %pkgddeps) {
        my @visit_stack;
        &$reset_visit();
        push (@visit_stack, $pkg);
        $visit{$pkg} = 1;
        if (find_circle(@visit_stack) == 1) {
            return 1;
        }
    }

    return 0;
}

sub worker_thread {
    my ($name, $thread) = @_;

    my $status;
    eval {
        $status = build_package($name, $thread);
    };
    if ($@) {
        warning("$@");
        $status = -1;
    }

    {
        lock($DETACHING);
        threads->detach() if ! threads->is_detached();
        @running = grep { $_ ne "$name"} @running;
        push(@done, $name);
        if ($status == 0) {
            $dirty = 1;
        } else {
            my $version = $to_build{$name}->{version};
            my $release = $to_build{$name}->{release};
            if (-f "$localrepo/$dist/$arch/logs/fail/$name-$version-$release/log") {
                $errors{"$name-$dist-$arch"} = "$localrepo/$dist/$arch/logs/fail/$name-$version-$release/log"
            } else {
                $errors{"$name-$dist-$arch"} = "";
            }
        }
    }

    debug("*** build $name exit with status($status), is dirty:$dirty, (worker: $thread) ***");
    return $status;
}

sub build_package {
    my ($name, $thread) = @_;
    use vars qw(@package_repos);

    my $version = $to_build{$name}->{version};
    my $release = $to_build{$name}->{release};
    my $spec_name = basename($to_build{$name}->{filename});
    my $pkg_path = "$build_root/local/sources/$dist/$name-$version-$release";
    my $srpm_filename = "";
    if ( $style eq "git" ) {
        $srpm_filename = "$pkg_path/$spec_name";
    } else {
        $srpm_filename = $to_build{$name}->{filename};
    }

    my @args = ();
    my @args_inc = ();
    if ($TERM == 1) {
        return -1;
    }
    push @args, "sudo -E $virtualenv/usr/bin/build";
    if ($arch ne "i586" ) {
        push @args, "--use-system-qemu";
    }
    push @args, "--uid $zuid:$zgid";
    push @args, "--jobs 4";
    push @args, "--cachedir $cache_dir";
    push @args, "--dist $dist";
    push @args, "--configdir $dist_configs";
    push @args, "--arch $archpath";
    push @args, "$srpm_filename";
    push @args, "--ccache" if ($ccache);
    if (! $extra_packs eq "") {
        my $packs = join(' ', split(',', $extra_packs));
        push @args, "--extra-packs=\"$packs\"";
    }

    # Rebuild the package.
    info("*** building $name-$version-$release $arch $dist (worker: $thread) ***");

    if ( -d "$localrepo/$dist/$arch/RPMS" ) {
        push @args, "--repository $localrepo/$dist/$arch/RPMS";
    }
    foreach my $r (@package_repos) {
        push @args, "--repository $r";
    }

    if ( ($clean || $cleanonce ) && ( ! grep $_ == $thread, @cleaned) &&
          $incremental == 0)  {
       push @args, "--clean";
       if ($cleanonce) {
            push(@cleaned, $thread);
       }
    }
    my $scratch = "$scratch_dir.$thread";

    my $redirect = "";
    if ($MAX_THREADS > 1 ) {
        $redirect = "> /dev/null 2>&1";
    }
    @args_inc = @args;
    my $cmd = "";
    if ($incremental == 1) {
        info("doing incremental build");
        my $buildcmd = "";
        if ( -d "$scratch_dir.incremental/home/abuild/rpmbuild/BUILD/$name-$version" ) {
            $scratch = "$scratch_dir.incremental";
        } elsif ( ! -d "$scratch/home/abuild/rpmbuild/BUILD/$name-$version" ){
            debug("Build directory exists");
            $scratch = "$scratch_dir.incremental";
            push @args, "--stage=\"-bp\"";
            push @args, "--root $scratch";
            push @args, "--clean";
            push @args, $redirect;
            $cmd = join(" ", @args);
            return -1 if (my_system($cmd) != 0);
        } else {
            info("build directory does not exist");
        }
        my $project_base_path = $to_build{$name}->{project_base_path};
        my $mount = "sudo mount -o bind $project_base_path $scratch/home/abuild/rpmbuild/BUILD/$name-$version";
        my_system($mount);
        if ($run_configure == 1 ) {
            push @args_inc, "--define '%configure echo'";
            push @args_inc, "--define '%reconfigure echo'";
            push @args_inc, "--define '%autogen echo'";
        }
        push @args_inc, "--stage=\"-bp\"";
        push @args_inc, "--root $scratch";
        push @args_inc, "--no-topdir-cleanup";
        push @args_inc, "--no-init";
        push @args_inc, "--short-circuit --stage=\"-bc\"";
        push @args_inc, $redirect;
        $cmd = join(" ", @args_inc);
        my_system ($cmd);

        $mount = "sudo umount $scratch/home/abuild/rpmbuild/BUILD/$name-$version";
        my_system($mount);
        info("finished incremental building $name");
        info("building log can be found here: $scratch/.build.log");
        $packages_built = 1;
        return 0;
    }

    push @args, "--root $scratch";
    push @args, "--clean" if (-e "$scratch/not-ready");
    push @args, $redirect;

    $cmd = join(" ", @args);
    debug($cmd);
    if (my_system ($cmd) == 0 ) {
        if (bsd_glob "$scratch/home/abuild/rpmbuild/SRPMS/*.rpm") {
            my_system ("cp $scratch/home/abuild/rpmbuild/SRPMS/*.rpm $localrepo/$dist/src/SRPMS");
        }
        if (bsd_glob "$scratch/home/abuild/rpmbuild/RPMS/*/*.rpm") {
            my_system ("cp $scratch/home/abuild/rpmbuild/RPMS/*/*.rpm $localrepo/$dist/$arch/RPMS");
        }
        mkdir_p "$localrepo/$dist/$arch/logs/success/$name-$version-$release";
        if (-e "$scratch/.build.log") {
            my_system ("cp $scratch/.build.log $localrepo/$dist/$arch/logs/success/$name-$version-$release/log");
            my_system ("sudo rm -f $scratch/.build.log ");
        }
        # Detach and terminate
        {
            lock($DETACHING);
            my_system("$build_dir/createrpmdeps $localrepo/$dist/$arch/RPMS > $order_dir/.repo.cache.local ");
            my_system("echo D: >> $order_dir/.repo.cache.local");
            # Merge local repo catch and remote repo cache
            my_system("cat $order_dir/.repo.cache.local $order_dir/.repo.cache.remote >$order_dir/.repo.cache");
        }
        info("finished building $name");
        $packages_built = 1;
        return 0;
    } else {
        mkdir_p "$localrepo/$dist/$arch/logs/fail/$name-$version-$release";
        if ( -f "$scratch/.build.log" ) {
            my_system ("cp $scratch/.build.log $localrepo/$dist/$arch/logs/fail/$name-$version-$release/log");
            my_system ("sudo rm -f $scratch/.build.log");
            warning("build failed, Leaving the logs in $localrepo/$dist/$arch/logs/fail/$name-$version-$release/log");
        }
        return 1;
    }

}



# MAIN
info("start building packages from: " . $package_path . " ($style)");

if ($style eq 'git') {
    my @specs = @ARGV;
    if ($buildall || @specs == 0) {
        File::Find::find({wanted => \&git_wanted}, $package_path );
        if (@original_specs > 1 && ! $commit eq ""){
            error("--commit option can't be specified with multiple packages");
        }
        if (@original_specs == 0) {
            error("No source package found at $package_path");
        }
        push @specs, @original_specs;
    }
    info("prepare sources...");
    foreach my $sp (@specs) {
        prepare_git($config, $sp);
    }
} else {
    @packs = @ARGV;
    if ($buildall || @packs == 0) {
        File::Find::find({wanted => \&obs_wanted}, $package_path );
    }
}
error("no spec files to build.\n") if (@packs == 0);

info("retrieving repo metadata...");
my $repos_setup = 1;
my_system("> $order_dir/.repo.cache.local");
if (-d "$localrepo/$dist/$arch/RPMS") {
    my_system("$build_dir/createrpmdeps $localrepo/$dist/$arch/RPMS >> $order_dir/.repo.cache.local");
    my_system("echo D: >> $order_dir/.repo.cache.local");
}
my_system("> $order_dir/.repo.cache.remote");
foreach my $repo (@package_repos) {
    my $cmd = "";
    if ($repo =~ /^\// && ! -e "$repo/repodata/repomd.xml") {
        $cmd = "$build_dir/createrpmdeps $repo >> $order_dir/.repo.cache.remote ";
    } else {
        $cmd = "$build_dir/createrepomddeps --cachedir=$cache_dir $repo >> $order_dir/.repo.cache.remote ";
    }
    debug($cmd);
    if ( my_system($cmd) == 0 ) {
        my_system("echo D: >> $order_dir/.repo.cache.remote");
    } else {
        $repos_setup = 0;
    }
}
# Merge local repo cache and remote repo cache
my_system("cat $order_dir/.repo.cache.local $order_dir/.repo.cache.remote >$order_dir/.repo.cache");

if ($repos_setup == 0 ) {
    error("repo cache creation failed...");
}

info("parsing package data...");
my %packs = parse_packs($config, @packs);

if ($binarylist ne "" && -e $binarylist ) {
    open my $file, "<", $binarylist or die $!;
    my @bins = <$file>;
    chomp(@bins);
    close($file);
    my @alldeps = ();
    my @tobuild = ();
    foreach my $b (@bins) {
        next if $b eq "";
        my $found = 0;
        foreach my $name (keys %packs) {
            my @sp = @{$packs{$name}->{subpacks}};
            my $debuginfo = $b;
            $debuginfo =~ s/(.*)-debuginfo/$1/;
            $debuginfo =~ s/(.*)-debugsource/$1/;
            $debuginfo =~ s/(.*)-docs/$1/;
            my $nb;
            if ($b ne $debuginfo) {
                $nb = $debuginfo;
            } else {
                $nb = $b;
            }
            if ( grep $_ eq $nb, @sp ) {
                push(@tobuild, $name);
                $found = 1 ;
                last;
            }
        }
        if (!$found) {
            push(@tofind, $b);
        }
    }

    #print $_ . ", " foreach(sort @tobuild);
    #print "\n";
    #print $_ . ", " foreach(sort @tofind);
    #print "\n";
    foreach my $b (@tobuild) {
        my @bdeps = expand_deps($packs{$b}->{filename});
        if (!shift @bdeps ) {
            debug("expansion error");
            debug("  $_") for @bdeps;
        } else {
            #print $b . ": ";
            #print $_ . ", " foreach(sort @bdeps);
            #print "\n";
            @alldeps = (@bdeps, @alldeps);
        }
    }
    my %hash = map { $_, 1 } @alldeps;
    my @allbins = keys %hash;
    #print "Required dependencies: \n ";
    #print $_ . ", " foreach(sort @allbins);
    #print "\n";
    foreach (@allbins) {
        my $so = source_of($_, %packs);
        if (defined($so)) {
            push(@tobuild, $so);
        }
    }

    %hash = map { $_, 1 } @tobuild;
    @tobuild = keys %hash;
    info ("initial set:");
    foreach my $p (@tobuild) {
        print " $p, ";
    }
    print "\n";
    my @final;
    foreach my $name (@tobuild) {
        my $fn = $packs{$name}->{filename};
        if (exists $packs{$name}{project_base_path}) {
            push(@final, {
                filename => $fn,
                project_base_path => $packs{$name}{project_base_path},
            });
        } else {
            push(@final, $fn);
        }
    }
    %to_build = parse_packs($config, @final);
} elsif ( $binarylist ne "") {
    error("Cant find binary list for image");
} else {
    %to_build = %packs
}

if ($incremental == 1 && scalar(keys %to_build) > 1) {
    error("incremental build only support building one package");
}

# Prepare Workers
for(my $w = 0; $w < $MAX_THREADS; $w++) {
    $workers{$w} = { 'state' => 'idle' , 'tid' => undef };
}

if ( ! -e "$localrepo/$dist/$arch/RPMS" ) {
    info("creating repo...");
    createrepo ($arch, $dist);
}

foreach my $name (keys %to_build) {
    my $fn = $to_build{$name}->{filename};
    my $version = $to_build{$name}->{version};
    my $release = $to_build{$name}->{release};
    my $pattern = "$localrepo/$dist/src/SRPMS/$name-$version-$release.*.rpm";
    my @binaries = glob $pattern;
    if (@binaries != 0 && ! $overwrite) {
        info("skipping $name-$version-$release $arch ");
        push(@skipped, $name);
    } elsif (@binaries != 0 && $overwrite) {
        info("*** overwriting $name-$version-$release $arch ***");
    }
}

# Create & Update package dependency
refresh_repo();
update_pkgdeps();
update_pkgddeps();

if (check_circle() == 1) {
    info("circle found, exit...");
    exit 1;
}

if ($debug) {
    my $pkg;
    info("package dependency:");
    for $pkg (keys %pkgddeps) {
        print "$pkg:";
        my $i;
        for $i (0 .. $#{$pkgddeps{$pkg}}) {
            print "$pkgddeps{$pkg}[$i] ";
        }
        print "\n";
    }
}

# Signal handling
$SIG{'INT'} = $SIG{'TERM'} = sub {
        print("^C captured\n");
        $TERM=1;
};

$SIG{'ALRM'} = sub {
    if (my_system("sudo -v") != 0) {
        error("failed to request passwd")
    } else {
        alarm(SUDOV_PERIOD);
    }
};

# trigger 'ALRM' immediately
kill 'ALRM', $$;

while (! $TERM) {
    my @order = ();
    my @o = ();

    {
        lock($DETACHING);
        if ($dirty) {
            refresh_repo();
            update_pkgdeps();
            update_pkgddeps();
            if (check_circle() == 1) {
                info("circle found, exit...");
                exit 1;
            }

            $dirty = 0;
        }
        foreach my $name (keys %to_build) {
            if( ! (grep $_ eq $name, @done) &&
                ! (grep $_ eq $name, @skipped) &&
                ! (grep $_ eq $name, @running))
            {
                next if (! exists $pkgdeps{$name});
                my @bdeps = @{$pkgdeps{$name}};
                my $add = 1;
                foreach my $depp (@bdeps) {
                    if ((! grep($_ eq $depp, @skipped)) &&
                        (! exists $expansion_errors{$depp}) &&
                        (! grep($_ eq $depp, @done))) {
                        #debug("not adding $name, since it depends on $depp");
                        $add = 0;
                        last;
                    }
                }
                if ($add == 1 ) {
                    push(@order, $name);
                }
            }
        }
    }

    if (scalar(keys %to_build) == @done + @skipped +
        scalar(keys %expansion_errors) && !$dirty) {
        last;
    }

    # No candidate packges and all thread works are idle, and pkgdeps
    # is updated, in this case, set packages in %tmp_expansion_errors
    # as real expansion_errors, and all packages depend on these packages
    # can not be blocked.
    if (@order == 0 && threads->list() == 0 && $dirty == 0) {
        @expansion_errors{keys %tmp_expansion_errors} = values %tmp_expansion_errors;
    }

    if (@order == 0) {
        # Waiting thread workers done, then re-calculate ready packages
        sleep(1);
        next;
    } else {
        info("next pass:");
        foreach my $o (@order) {
            print $o . "\n";
        }
    }
    if ($dryrun) {
        exit 1
    }

    while (@order && ! $TERM) {
        # Keep max threads running
        my $needed = $MAX_THREADS - threads->list();

        if ($needed == 0) {
            # Waiting for build threads finish
            sleep(1);
            next;
        }

        for (; $needed && ! $TERM; $needed--) {
            my $job = shift(@order);
            last if (! $job);

            my $worker = find_idle();
            my $thr = threads->create(\&worker_thread, $job, $worker);
            my $tid = $thr->tid();
            push (@running, $job);
            set_busy($worker, $tid);
        }
    }

}

# waiting for threads to finish
while ((threads->list() > 0)) {
    sleep(1);
}

if ($packages_built) {
    info("updating local repo");
    createrepo ($arch, $dist);
}

if (%errors || %expansion_errors || @export_errors) {
    my $msg = "*** Error Summary ***\n";

    if (@export_errors) {
        $msg .= "=== the following packages failed to build because export " .
            "source files to build environment failed ===\n";
        $msg .= join("\n", @export_errors) . "\n";
        $msg .= "\n";
    }
    if (%expansion_errors) {
        my $error_pkgs = "";
        foreach my $pkg (keys %expansion_errors) {
            $error_pkgs .= "$pkg:\n  " . join("\n  ", @{$expansion_errors{$pkg}}) . "\n";
        }
        $msg .= "=== the following packages failed to build due to missing " .
            "build dependencies ===\n$error_pkgs\n";
    }
    if (%errors) {
        my $error_pkgs = "";
        foreach my $pkg (keys %errors) {
            $error_pkgs .= "$pkg: $errors{$pkg}\n";
        }
        $msg .= "=== the following packages failed to build due to rpmbuild " .
            "issue ===\n$error_pkgs";
    }
    error($msg);
}

exit 0
