################################################################
#
# Copyright (c) 1995-2014 SUSE Linux Products GmbH
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################

package Build;

use strict;
use Digest::MD5;
use Build::Rpm;
use POSIX qw(strftime);
#use Data::Dumper;

our $expand_dbg;

our $do_rpm;
our $do_deb;
our $do_kiwi;
our $do_arch;
our $do_collax;
our $do_livebuild;
our $do_snapcraft;
our $do_appimage;
our $do_docker;
our $do_fissile;

sub import {
  for (@_) {
    $do_rpm = 1 if $_ eq ':rpm';
    $do_deb = 1 if $_ eq ':deb';
    $do_kiwi = 1 if $_ eq ':kiwi';
    $do_arch = 1 if $_ eq ':arch';
    $do_collax = 1 if $_ eq ':collax';
    $do_livebuild = 1 if $_ eq ':livebuild';
    $do_snapcraft = 1 if $_ eq ':snapcraft';
    $do_appimage = 1 if $_ eq ':appimage';
    $do_docker = 1 if $_ eq ':docker';
    $do_fissile = 1 if $_ eq ':fissile';
  }
  $do_rpm = $do_deb = $do_kiwi = $do_arch = $do_collax = $do_livebuild = $do_snapcraft = $do_appimage = $do_docker = $do_fissile = 1 if !$do_rpm && !$do_deb && !$do_kiwi && !$do_arch && !$do_collax && !$do_livebuild && !$do_snapcraft && !$do_appimage && !$do_docker && !$do_fissile;

  if ($do_deb) {
    require Build::Deb;
  }
  if ($do_kiwi) {
    require Build::Kiwi;
  }
  if ($do_arch) {
    require Build::Arch;
  }
  if ($do_collax) {
    require Build::Collax;
  }
  if ($do_livebuild) {
    require Build::LiveBuild;
  }
  if ($do_snapcraft) {
    require Build::Snapcraft;
  }
  if ($do_appimage) {
    require Build::Appimage;
  }
  if ($do_docker) {
    require Build::Docker;
  }
  if ($do_fissile) {
    require Build::Fissile;
  }
}

package Build::Features;
our $preinstallimage = 1;	# on sale now
package Build;

# this is synced with rpm 4.13.0. The additional architectures of arm behind the spaces are
# from MeeGo project. They don't exist elsewhere, but don't conflict either luckily
my $std_macros = q{
%define nil
%define ix86    i386 i486 i586 i686 pentium3 pentium4 athlon geode
%define arm     armv3l armv4b armv4l armv4tl armv5b armv5l armv5teb armv5tel armv5tejl armv6l armv6hl armv7l armv7hl armv7hnl       armv5el armv5eb armv6el armv6eb armv7el armv7eb armv7nhl armv8el
%define arml    armv3l armv4l armv5l armv5tel armv6l armv6hl armv7l armv7hl armv7hnl
%define armb    armv4b armv5b armv5teb
%define mips32  mips mipsel mipsr6 mipsr6el
%define mips64  mips64 mips64el mips64r6 mips64r6el
%define mipseb  mips mipsr6 mips64 mips64r6
%define mipsel  mipsel mipsr6el mips64el mips64r6el
%define mips    %{mips32} %{mips64}
%define sparc   sparc sparcv8 sparcv9 sparcv9v sparc64 sparc64v
%define alpha   alpha alphaev56 alphaev6 alphaev67
%define power64 ppc64 ppc64p7 ppc64le
};
my $extra_macros = '';

sub unify {
  my %h = map {$_ => 1} @_;
  return grep(delete($h{$_}), @_);
}

sub define($)
{
  my $def = shift;
  $extra_macros .= '%define '.$def."\n";
}

sub init_helper_hashes {
  my ($config) = @_;

  $config->{'preferh'} = { map {$_ => 1} @{$config->{'prefer'}} };

  my %ignore;
  for (@{$config->{'ignore'}}) {
    if (!/:/) {
      $ignore{$_} = 1;
      next;
    }
    my @s = split(/[,:]/, $_);
    my $s = shift @s;
    $ignore{"$s:$_"} = 1 for @s;
  }
  $config->{'ignoreh'} = \%ignore;

  my %conflicts;
  for (@{$config->{'conflict'}}) {
    my @s = split(/[,:]/, $_);
    my $s = shift @s;
    push @{$conflicts{$s}}, @s;
    push @{$conflicts{$_}}, $s for @s;
  }
  for (keys %conflicts) {
    $conflicts{$_} = [ unify(@{$conflicts{$_}}) ]
  }
  $config->{'conflicth'} = \%conflicts;
}

# 'canonicalize' dist string as found in rpm dist tags
sub dist_canon($$) {
  my ($rpmdist, $arch) = @_;
  $rpmdist = lc($rpmdist);
  $rpmdist =~ s/-/_/g;
  $rpmdist =~ s/opensuse/suse linux/;
  my $rpmdista;
  if ($rpmdist =~ /\(/) {
    $rpmdista = $rpmdist;
    $rpmdista =~ s/.*\(//;
    $rpmdista =~ s/\).*//;
  } else {
    $rpmdista = $arch;
  }
  $rpmdista =~ s/i[456]86/i386/;
  $rpmdist = '' unless $rpmdista =~ /^(i386|x86_64|ia64|ppc|ppc64|ppc64le|s390|s390x)$/;
  my $dist = 'default';
  if ($rpmdist =~ /unitedlinux 1\.0.*/) {
    $dist = "ul1-$rpmdista";
  } elsif ($rpmdist =~ /suse sles_(\d+)/) {
    $dist = "sles$1-$rpmdista";
  } elsif ($rpmdist =~ /suse linux enterprise (\d+)/) {
    $dist = "sles$1-$rpmdista";
  } elsif ($rpmdist =~ /suse linux (\d+)\.(\d+)\.[4-9]\d/) {
    # alpha version
    $dist = "$1.".($2 + 1)."-$rpmdista";
  } elsif ($rpmdist =~ /suse linux (?:leap )?(\d+\.\d+)/) {
    $dist = "$1-$rpmdista";
  }
  return $dist;
}

sub read_config_dist {
  my ($dist, $archpath, $configdir) = @_;

  my $arch = $archpath;
  $arch = 'noarch' unless defined $arch;
  $arch =~ s/:.*//;
  $arch = 'noarch' if $arch eq '';
  die("Please specify a distribution!\n") unless defined $dist;
  if ($dist !~ /\//) {
    my $saved = $dist;
    $configdir = '.' unless defined $configdir;
    $dist =~ s/-.*//;
    $dist = "sl$dist" if $dist =~ /^\d/;
    $dist = "$configdir/$dist.conf";
    if (! -e $dist) {
      $dist =~ s/-.*//;
      $dist = "sl$dist" if $dist =~ /^\d/;
      $dist = "$configdir/$dist.conf";
    }
    if (! -e $dist) {
      warn "$saved.conf not found, using default.conf\n" unless $saved eq 'default';
      $dist = "$configdir/default.conf";
    }
  }
  die("$dist: $!\n") unless -e $dist;
  my $cf = read_config($arch, $dist);
  die("$dist: parse error\n") unless $cf;
  return $cf;
}

sub read_config {
  my ($arch, $cfile) = @_;
  my @macros = split("\n", $std_macros.$extra_macros);
  push @macros, "%define _target_cpu $arch";
  push @macros, "%define _target_os linux";
  my $config = {'macros' => \@macros, 'arch' => $arch};
  my @config;
  if (ref($cfile)) {
    @config = @$cfile;
  } elsif (defined($cfile)) {
    local *CONF;
    return undef unless open(CONF, '<', $cfile);
    @config = <CONF>;
    close CONF;
    chomp @config;
  }
  # create verbatim macro blobs
  my @newconfig;
  while (@config) {
    push @newconfig, shift @config;
    next unless $newconfig[-1] =~ /^\s*macros:\s*$/si;
    $newconfig[-1] = "macros:\n";
    while (@config) {
      my $l = shift @config;
      last if $l =~ /^\s*:macros\s*$/si;
      $newconfig[-1] .= "$l\n";
    }
  }
  my @spec;
  $config->{'save_expanded'} = 1;
  Build::Rpm::parse($config, \@newconfig, \@spec);
  delete $config->{'save_expanded'};
  $config->{'preinstall'} = [];
  $config->{'vminstall'} = [];
  $config->{'cbpreinstall'} = [];
  $config->{'cbinstall'} = [];
  $config->{'runscripts'} = [];
  $config->{'required'} = [];
  $config->{'support'} = [];
  $config->{'keep'} = [];
  $config->{'prefer'} = [];
  $config->{'ignore'} = [];
  $config->{'conflict'} = [];
  $config->{'substitute'} = {};
  $config->{'substitute_vers'} = {};
  $config->{'optflags'} = {};
  $config->{'order'} = {};
  $config->{'exportfilter'} = {};
  $config->{'publishfilter'} = [];
  $config->{'rawmacros'} = '';
  $config->{'release'} = '<CI_CNT>.<B_CNT>';
  $config->{'repotype'} = [];
  $config->{'patterntype'} = [];
  $config->{'fileprovides'} = {};
  $config->{'constraint'} = [];
  $config->{'expandflags'} = [];
  $config->{'buildflags'} = [];
  $config->{'singleexport'} = '';
  for my $l (@spec) {
    $l = $l->[1] if ref $l;
    next unless defined $l;
    my @l = split(' ', $l);
    next unless @l;
    my $ll = shift @l;
    my $l0 = lc($ll);
    if ($l0 eq 'macros:') {
      $l =~ s/.*?\n//s;
      if ($l =~ /^!\n/s) {
	$config->{'rawmacros'} = substr($l, 2);
      } else {
	$config->{'rawmacros'} .= $l;
      }
      next;
    }
    if ($l0 eq 'preinstall:' || $l0 eq 'vminstall:' || $l0 eq 'required:' || $l0 eq 'support:' || $l0 eq 'keep:' || $l0 eq 'prefer:' || $l0 eq 'ignore:' || $l0 eq 'conflict:' || $l0 eq 'runscripts:' || $l0 eq 'expandflags:' || $l0 eq 'buildflags:') {
      my $t = substr($l0, 0, -1);
      for my $l (@l) {
	if ($l eq '!*') {
	  $config->{$t} = [];
	} elsif ($l =~ /^!/) {
	  $config->{$t} = [ grep {"!$_" ne $l} @{$config->{$t}} ];
	} else {
	  push @{$config->{$t}}, $l;
	}
      }
    } elsif ($l0 eq 'substitute:') {
      next unless @l;
      $ll = shift @l;
      if ($ll eq '!*') {
	$config->{'substitute'} = {};
      } elsif ($ll =~ /^!(.*)$/) {
	delete $config->{'substitute'}->{$1};
      } else {
	$config->{'substitute'}->{$ll} = [ @l ];
      }
    } elsif ($l0 eq 'fileprovides:') {
      next unless @l;
      $ll = shift @l;
      if ($ll eq '!*') {
	$config->{'fileprovides'} = {};
      } elsif ($ll =~ /^!(.*)$/) {
	delete $config->{'fileprovides'}->{$1};
      } else {
	$config->{'fileprovides'}->{$ll} = [ @l ];
      }
    } elsif ($l0 eq 'exportfilter:') {
      next unless @l;
      $ll = shift @l;
      $config->{'exportfilter'}->{$ll} = [ @l ];
    } elsif ($l0 eq 'publishfilter:') {
      $config->{'publishfilter'} = [ @l ];
    } elsif ($l0 eq 'optflags:') {
      next unless @l;
      $ll = shift @l;
      $config->{'optflags'}->{$ll} = join(' ', @l);
    } elsif ($l0 eq 'order:') {
      for my $l (@l) {
	if ($l eq '!*') {
	  $config->{'order'} = {};
	} elsif ($l =~ /^!(.*)$/) {
	  delete $config->{'order'}->{$1};
	} else {
	  $config->{'order'}->{$l} = 1;
	}
      }
    } elsif ($l0 eq 'repotype:') { # type of generated repository data
      $config->{'repotype'} = [ @l ];
    } elsif ($l0 eq 'type:') { # kind of recipe system (spec,dsc,arch,kiwi,...)
      $config->{'type'} = $l[0];
    } elsif ($l0 eq 'buildengine:') { # build engine (build,mock)
      $config->{'buildengine'} = $l[0];
    } elsif ($l0 eq 'binarytype:') { # kind of binary packages (rpm,deb,arch,...)
      $config->{'binarytype'} = $l[0];
    } elsif ($l0 eq 'patterntype:') { # kind of generated patterns in repository
      $config->{'patterntype'} = [ @l ];
    } elsif ($l0 eq 'release:') {
      $config->{'release'} = $l[0];
    } elsif ($l0 eq 'cicntstart:') {
      $config->{'cicntstart'} = $l[0];
    } elsif ($l0 eq 'releaseprg:') {
      $config->{'releaseprg'} = $l[0];
    } elsif ($l0 eq 'releasesuffix:') {
      $config->{'releasesuffix'} = join(' ', @l);
    } elsif ($l0 eq 'changetarget:' || $l0 eq 'target:') {
      $config->{'target'} = join(' ', @l);
      push @macros, "%define _target_cpu ".(split('-', $config->{'target'}))[0] if $config->{'target'};
    } elsif ($l0 eq 'hostarch:') {
      $config->{'hostarch'} = join(' ', @l);
    } elsif ($l0 eq 'constraint:') {
      my $l = join(' ', @l);
      if ($l eq '!*') {
	$config->{'constraint'} = [];
      } else {
	push @{$config->{'constraint'}}, $l;
      }
    } elsif ($l0 eq 'singleexport:') {
      $config->{'singleexport'} = $l[0]; # avoid to export multiple package container in maintenance_release projects
    } elsif ($l0 !~ /^[#%]/) {
      warn("unknown keyword in config: $l0\n");
    }
  }
  for my $l (qw{preinstall vminstall required support keep runscripts repotype patterntype}) {
    $config->{$l} = [ unify(@{$config->{$l}}) ];
  }
  for my $l (keys %{$config->{'substitute'}}) {
    $config->{'substitute_vers'}->{$l} = [ map {/^(.*?)(=)?$/g} unify(@{$config->{'substitute'}->{$l}}) ];
    $config->{'substitute'}->{$l} = [ unify(@{$config->{'substitute'}->{$l}}) ];
    s/=$// for @{$config->{'substitute'}->{$l}};
  }
  init_helper_hashes($config);
  if (!$config->{'type'}) {
    # Fallback to old guessing method if no type (spec, dsc or kiwi) is defined
    if (grep {$_ eq 'rpm'} @{$config->{'preinstall'} || []}) {
      $config->{'type'} = 'spec';
    } elsif (grep {$_ eq 'debianutils'} @{$config->{'preinstall'} || []}) {
      $config->{'type'} = 'dsc';
    } elsif (grep {$_ eq 'pacman'} @{$config->{'preinstall'} || []}) {
      $config->{'type'} = 'arch';
    }
    $config->{'type'} ||= 'UNDEFINED';
  }
  if (!$config->{'binarytype'}) {
    $config->{'binarytype'} = 'rpm' if $config->{'type'} eq 'spec';
    $config->{'binarytype'} = 'deb' if $config->{'type'} eq 'dsc' || $config->{'type'} eq 'collax' || $config->{'type'} eq 'livebuild';
    $config->{'binarytype'} = 'arch' if $config->{'type'} eq 'arch';
    if (grep {$_ eq $config->{'type'}} qw{snapcraft appimage docker fissile kiwi}){
      if (grep {$_ eq 'rpm'} @{$config->{'preinstall'} || []}) {
        $config->{'binarytype'} = 'rpm';
      } elsif (grep {$_ eq 'debianutils'} @{$config->{'preinstall'} || []}) {
        $config->{'binarytype'} = 'deb';
      } elsif (grep {$_ eq 'pacman'} @{$config->{'preinstall'} || []}) {
        $config->{'binarytype'} = 'arch';
      }
    }
    $config->{'binarytype'} ||= 'UNDEFINED';
  }
  # add rawmacros to our macro list
  if ($config->{'rawmacros'} ne '') {
    for my $rm (split("\n", $config->{'rawmacros'})) {
      if (@macros && $macros[-1] =~ /\\$/) {
	if ($rm =~ /\\$/) {
	  push @macros, '...\\';
	} else {
	  push @macros, '...';
	}
      } elsif ($rm !~ /^%/) {
	push @macros, $rm;
      } else {
	push @macros, "%define ".substr($rm, 1);
      }
    }
  }
  for (@{$config->{'expandflags'} || []}) {
    if (/^([^:]+):(.*)$/s) {
      $config->{"expandflags:$1"} = $2;
    } else {
      $config->{"expandflags:$_"} = 1;
    }
  }
  for (@{$config->{'buildflags'} || []}) {
    if (/^([^:]+):(.*)$/s) {
      $config->{"buildflags:$1"} = $2;
    } else {
      $config->{"buildflags:$_"} = 1;
    }
  }
  return $config;
}

sub do_subst {
  my ($config, @deps) = @_;
  my @res;
  my %done;
  my $subst = $config->{'substitute'};
  while (@deps) {
    my $d = shift @deps;
    next if $done{$d};
    my $ds = $d;
    $ds =~ s/\s*[<=>].*$//s;
    if ($subst->{$ds}) {
      unshift @deps, @{$subst->{$ds}};
      push @res, $d if grep {$_ eq $ds} @{$subst->{$ds}};
    } else {
      push @res, $d;
    }
    $done{$d} = 1;
  }
  return @res;
}

sub do_subst_vers {
  my ($config, @deps) = @_;
  my @res;
  my %done;
  my $subst = $config->{'substitute_vers'};
  while (@deps) {
    my ($d, $dv) = splice(@deps, 0, 2);
    next if $done{$d};
    if ($subst->{$d}) {
      unshift @deps, map {defined($_) && $_ eq '=' ? $dv : $_} @{$subst->{$d}};
      push @res, $d, $dv if grep {defined($_) && $_ eq $d} @{$subst->{$d}};
    } else {
      push @res, $d, $dv;
    }
    $done{$d} = 1;
  }
  return @res;
}

my %subst_defaults = (
  # defaults live-build package dependencies base on 4.0~a26 gathered with:
  # grep Check_package -r /usr/lib/live/build
  'build-packages:livebuild' => [
    'apt-utils', 'dctrl-tools', 'debconf', 'dosfstools', 'e2fsprogs', 'grub',
    'librsvg2-bin', 'live-boot', 'live-config', 'mtd-tools', 'parted',
    'squashfs-tools', 'syslinux', 'syslinux-common', 'wget', 'xorriso', 'zsync',
  ],
  'system-packages:livebuild' => [
    'apt-utils', 'cpio', 'dpkg-dev', 'live-build', 'lsb-release', 'tar',
  ],
  'system-packages:mock' => [
    'mock', 'createrepo',
  ],
  'system-packages:debootstrap' => [
    'debootstrap', 'lsb-release',
  ],
  'system-packages:kiwi-image' => [
    'kiwi', 'createrepo', 'tar',
  ],
  'system-packages:kiwi-product' => [
    'kiwi',
  ],
  'system-packages:docker' => [
    'docker',
  ],
  'system-packages:fissile' => [
    'docker', # TODO: Add fissile here as soon as it is packaged
  ],
  'system-packages:deltarpm' => [
    'deltarpm',
  ],
);

# expand the preinstalls/vminstalls
sub expandpreinstalls {
  my ($config) = @_;
  return if !$config->{'expandflags:preinstallexpand'} || $config->{'preinstallisexpanded'};
  my (@pre, @vm);
  if (@{$config->{'preinstall'} || []}) {
    @pre = expand($config, @{$config->{'preinstall'} || []});
    return "preinstalls: $pre[0]" unless shift @pre;
    @pre = sort(@pre);
  }
  if (@{$config->{'vminstall'} || []}) {
    my %pre = map {$_ => 1} @pre;
    my %vmx = map {+"-$_" => 1} @{$config->{'vminstall'} || []};
    my @pren = grep {/^-/ && !$vmx{$_}} @{$config->{'preinstall'} || []};
    @vm = expand($config, @pre, @pren, @{$config->{'vminstall'} || []});
    return "vminstalls: $vm[0]" unless shift @vm;
    @vm = sort(grep {!$pre{$_}} @vm);
  }
  $config->{'preinstall'} = \@pre;
  $config->{'vminstall'} = \@vm;
  #print STDERR "pre: @pre\n";
  #print STDERR "vm: @vm\n";
  $config->{'preinstallisexpanded'} = 1;
  return '';
}

# Delivers all packages which get used for building
sub get_build {
  my ($config, $subpacks, @deps) = @_;

  if ($config->{'expandflags:preinstallexpand'} && !$config->{'preinstallisexpanded'}) {
    my $err = expandpreinstalls($config);
    return (undef, $err) if $err;
  }
  my $buildtype = $config->{'type'} || '';
  if (grep {$_ eq $buildtype} qw{livebuild docker kiwi fissile}) {
    push @deps, @{$config->{'substitute'}->{"build-packages:$buildtype"}
		  || $subst_defaults{"build-packages:$buildtype"} || []};
  }
  my @ndeps = grep {/^-/} @deps;
  my %ndeps = map {$_ => 1} @ndeps;
  my @directdepsend;
  if ($ndeps{'--directdepsend--'}) {
    @directdepsend = @deps;
    for (splice @deps) {
      last if $_ eq '--directdepsend--';
      push @deps, $_;
    }
    @directdepsend = grep {!/^-/} splice(@directdepsend, @deps + 1);
  }
  my @extra = (@{$config->{'required'}}, @{$config->{'support'}});
  if (@{$config->{'keep'} || []}) {
    my %keep = map {$_ => 1} (@deps, @{$config->{'keep'} || []}, @{$config->{'preinstall'}});
    for (@{$subpacks || []}) {
      next if $keep{$_};
      push @ndeps, "-$_";
      $ndeps{"-$_"} = 1;
    }
  } else {
    # new "empty keep" mode, filter subpacks from required/support
    my %subpacks = map {$_ => 1} @{$subpacks || []};
    @extra = grep {!$subpacks{$_}} @extra;
  }
  @deps = grep {!$ndeps{$_}} @deps;
  push @deps, @{$config->{'preinstall'}};
  push @deps, @extra;
  @deps = grep {!$ndeps{"-$_"}} @deps;
  @deps = do_subst($config, @deps);
  @deps = grep {!$ndeps{"-$_"}} @deps;
  if (@directdepsend) {
    @directdepsend = do_subst($config, @directdepsend);
    @directdepsend = grep {!$ndeps{"-$_"}} @directdepsend;
    unshift @directdepsend, '--directdepsend--' if @directdepsend;
  }
  @deps = expand($config, @deps, @ndeps, @directdepsend);
  return @deps;
}

# return the package needed for setting up the build environment.
# an empty result means that the packages from get_build should
# be used instead.
sub get_sysbuild {
  my ($config, $buildtype, $extradeps) = @_;
  my $engine = $config->{'buildengine'} || '';
  $buildtype ||= $config->{'type'} || '';
  my @sysdeps;
  if ($engine eq 'mock' && $buildtype eq 'spec') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:mock'} || []};
    @sysdeps = @{$subst_defaults{'system-packages:mock'} || []} unless @sysdeps;
  } elsif ($engine eq 'debootstrap' && $buildtype eq 'dsc') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:debootstrap'} || []};
    @sysdeps = @{$subst_defaults{'system-packages:debootstrap'} || []} unless @sysdeps;
  } elsif ($buildtype eq 'livebuild') {
    # packages used for build environment setup (build-recipe-livebuild deps)
    @sysdeps = @{$config->{'substitute'}->{'system-packages:livebuild'} || []};
    @sysdeps = @{$subst_defaults{'system-packages:livebuild'} || []} unless @sysdeps;
  } elsif ($buildtype eq 'kiwi-image') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:kiwi-image'} || []};
    @sysdeps = @{$config->{'substitute'}->{'kiwi-setup:image'} || []} unless @sysdeps;
    @sysdeps = @{$subst_defaults{'system-packages:kiwi-image'} || []} unless @sysdeps;
    push @sysdeps, @$extradeps if $extradeps;
  } elsif ($buildtype eq 'kiwi-product') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:kiwi-product'} || []};
    @sysdeps = @{$config->{'substitute'}->{'kiwi-setup:product'} || []} unless @sysdeps;
    @sysdeps = @{$subst_defaults{'system-packages:kiwi-product'} || []} unless @sysdeps;
    push @sysdeps, @$extradeps if $extradeps;
  } elsif ($buildtype eq 'docker') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:docker'} || []} unless @sysdeps;
    @sysdeps = @{$subst_defaults{'system-packages:docker'} || []} unless @sysdeps;
  } elsif ($buildtype eq 'fissile') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:fissile'} || []} unless @sysdeps;
    @sysdeps = @{$subst_defaults{'system-packages:fissile'} || []} unless @sysdeps;
  } elsif ($buildtype eq 'deltarpm') {
    @sysdeps = @{$config->{'substitute'}->{'system-packages:deltarpm'} || []};
    @sysdeps = @{$subst_defaults{'system-packages:deltarpm'} || []} unless @sysdeps;
  }
  return () unless @sysdeps;
  if ($config->{'expandflags:preinstallexpand'} && !$config->{'preinstallisexpanded'}) {
    my $err = expandpreinstalls($config);
    return (undef, $err) if $err;
  }
  my @ndeps = grep {/^-/} @sysdeps;
  my %ndeps = map {$_ => 1} @ndeps;
  @sysdeps = grep {!$ndeps{$_}} @sysdeps;
  push @sysdeps, @{$config->{'preinstall'}}, @{$config->{'required'}};
  push @sysdeps, @{$config->{'support'}} if $buildtype eq 'kiwi-image' || $buildtype eq 'kiwi-product';	# compat to old versions
  @sysdeps = do_subst($config, @sysdeps);
  @sysdeps = grep {!$ndeps{$_}} @sysdeps;
  my $configtmp = $config;
  @sysdeps = expand($configtmp, @sysdeps, @ndeps);
  return @sysdeps unless $sysdeps[0];
  shift @sysdeps;
  @sysdeps = unify(@sysdeps, get_preinstalls($config));
  return (1, @sysdeps);
}

# Delivers all packages which shall have an influence to other package builds (get_build reduced by support packages)
sub get_deps {
  my ($config, $subpacks, @deps) = @_;
  if ($config->{'expandflags:preinstallexpand'} && !$config->{'preinstallisexpanded'}) {
    my $err = expandpreinstalls($config);
    return (undef, $err) if $err;
  }
  my @ndeps = grep {/^-/} @deps;
  my @extra = @{$config->{'required'}};
  if (@{$config->{'keep'} || []}) {
    my %keep = map {$_ => 1} (@deps, @{$config->{'keep'} || []}, @{$config->{'preinstall'}});
    for (@{$subpacks || []}) {
      push @ndeps, "-$_" unless $keep{$_};
    }
  } else {
    # new "empty keep" mode, filter subpacks from required
    my %subpacks = map {$_ => 1} @{$subpacks || []};
    @extra = grep {!$subpacks{$_}} @extra;
  }
  my %ndeps = map {$_ => 1} @ndeps;
  @deps = grep {!$ndeps{$_}} @deps;
  push @deps, @extra;
  @deps = grep {!$ndeps{"-$_"}} @deps;
  @deps = do_subst($config, @deps);
  @deps = grep {!$ndeps{"-$_"}} @deps;
  my %bdeps = map {$_ => 1} (@{$config->{'preinstall'}}, @{$config->{'support'}});
  delete $bdeps{$_} for @deps;
  @deps = expand($config, @deps, @ndeps);
  if (@deps && $deps[0]) {
    my $r = shift @deps;
    @deps = grep {!$bdeps{$_}} @deps;
    unshift @deps, $r;
  }
  return @deps;
}

sub get_preinstalls {
  my ($config) = @_;
  if ($config->{'expandflags:preinstallexpand'} && !$config->{'preinstallisexpanded'}) {
    my $err = expandpreinstalls($config);
    return ('expandpreinstalls_error') if $err;
  }
  return @{$config->{'preinstall'}};
}

sub get_vminstalls {
  my ($config) = @_;
  if ($config->{'expandflags:preinstallexpand'} && !$config->{'preinstallisexpanded'}) {
    my $err = expandpreinstalls($config);
    return ('expandpreinstalls_error') if $err;
  }
  return @{$config->{'vminstall'}};
}

sub get_runscripts {
  my ($config) = @_;
  return @{$config->{'runscripts'}};
}

### just for API compability
sub get_cbpreinstalls { return (); }
sub get_cbinstalls { return (); }

###########################################################################

sub readdeps {
  my ($config, $pkginfo, @depfiles) = @_;

  local *F;
  my %requires;
  my %provides;
  my %pkgconflicts;
  my %pkgobsoletes;
  my %recommends;
  my %supplements;
  my $dofileprovides = %{$config->{'fileprovides'} || {}};
  for my $depfile (@depfiles) {
    if (ref($depfile) eq 'HASH') {
      for my $rr (keys %$depfile) {
	$provides{$rr} = $depfile->{$rr}->{'provides'};
	$requires{$rr} = $depfile->{$rr}->{'requires'};
	$pkgconflicts{$rr} = $depfile->{$rr}->{'conflicts'};
	$pkgobsoletes{$rr} = $depfile->{$rr}->{'obsoletes'};
	$recommends{$rr} = $depfile->{$rr}->{'recommends'};
	$supplements{$rr} = $depfile->{$rr}->{'supplements'};
      }
      next;
    }
    # XXX: we don't support different architectures per file
    if (ref($depfile)) {
      *F = $depfile;
    } else {
      open(F, '<', $depfile) || die("$depfile: $!\n");
    }
    while(<F>) {
      my @s = split(' ', $_);
      my $s = shift @s;
      if ($pkginfo && ($s =~ /^I:(.*)\.(.*)-\d+\/\d+\/\d+:$/)) {
	my $pkgid = $1;
	my $arch = $2; 
	my $evr = $s[0];
	$pkginfo->{$pkgid}->{'arch'} = $1 if $s[1] && $s[1] =~ s/-(.*)$//;
	$pkginfo->{$pkgid}->{'buildtime'} = $s[1] if $s[1];
	if ($evr =~ s/^\Q$pkgid-//) {
	  $pkginfo->{$pkgid}->{'epoch'} = $1 if $evr =~ s/^(\d+)://;
	  $pkginfo->{$pkgid}->{'release'} = $1 if $evr =~ s/-([^-]*)$//;
	  $pkginfo->{$pkgid}->{'version'} = $evr;
	}
	next;
      }
      my @ss;
      while (@s) {
	if (!$dofileprovides && $s[0] =~ /^\//) {
	  shift @s;
	  next;
	}
	if ($s[0] =~ /^rpmlib\(/) {
	    splice(@s, 0, 3);
	    next;
	}
	if ($s[0] =~ /^\(/) {
	    push @ss, Build::Rpm::shiftrich(\@s);
	    next;
	}
	push @ss, shift @s;
	while (@s && $s[0] =~ /^[\(<=>|]/) {
	  $ss[-1] .= " $s[0] $s[1]";
	  $ss[-1] =~ s/ \((.*)\)/ $1/;
	  $ss[-1] =~ s/(<|>){2}/$1/;
	  splice(@s, 0, 2);
	}
      }
      my %ss;
      @ss = grep {!$ss{$_}++} @ss;
      if ($s =~ /^(P|R|C|O|r|s):(.*)\.(.*)-\d+\/\d+\/\d+:$/) {
	my $pkgid = $2;
	my $arch = $3;
	if ($1 eq "P") {
	  $provides{$pkgid} = \@ss;
	  if ($pkginfo) {
	    $pkginfo->{$pkgid}->{'name'} = $pkgid;
	    $pkginfo->{$pkgid}->{'arch'} = $arch;
	    $pkginfo->{$pkgid}->{'provides'} = \@ss;
	  }
	}
	if ($1 eq "R") {
	  $requires{$pkgid} = \@ss;
	  $pkginfo->{$pkgid}->{'requires'} = \@ss if $pkginfo;
	  next;
	}
	if ($1 eq "C") {
	  $pkgconflicts{$pkgid} = \@ss;
	  $pkginfo->{$pkgid}->{'conflicts'} = \@ss if $pkginfo;
	  next;
	}
	if ($1 eq "O") {
	  $pkgobsoletes{$pkgid} = \@ss;
	  $pkginfo->{$pkgid}->{'obsoletes'} = \@ss if $pkginfo;
	  next;
	}
	if ($1 eq "r") {
	  $recommends{$pkgid} = \@ss;
	  $pkginfo->{$pkgid}->{'recommends'} = \@ss if $pkginfo;
	  next;
	}
	if ($1 eq "s") {
	  $supplements{$pkgid} = \@ss;
	  $pkginfo->{$pkgid}->{'supplements'} = \@ss if $pkginfo;
	  next;
	}
      }
    }
    close F unless ref($depfile);
  }
  if ($pkginfo) {
    # extract evr from self provides if there is no 'I' line
    for my $pkg (values %$pkginfo) {
      next if defined $pkg->{'version'};
      my $n = $pkg->{'name'};
      next unless defined $n;
      my @sp = grep {/^\Q$n\E\s*=\s*/} @{$pkg->{'provides'} || []};
      next unless @sp;
      my $evr = $sp[-1];
      $evr =~ s/^\Q$n\E\s*=\s*//;
      $pkg->{'epoch'} = $1 if $evr =~ s/^(\d+)://;
      $pkg->{'release'} = $1 if $evr =~ s/-([^-]*)$//;
      $pkg->{'version'} = $evr;
    }
  }
  $config->{'providesh'} = \%provides;
  $config->{'requiresh'} = \%requires;
  $config->{'pkgconflictsh'} = \%pkgconflicts;
  $config->{'pkgobsoletesh'} = \%pkgobsoletes;
  $config->{'recommendsh'} = \%recommends;
  $config->{'supplementsh'} = \%supplements;
  makewhatprovidesh($config);
}

sub getbuildid {
  my ($q) = @_;
  my $evr = $q->{'version'};
  $evr = "$q->{'epoch'}:$evr" if $q->{'epoch'};
  $evr .= "-$q->{'release'}" if defined $q->{'release'};;
  my $buildtime = $q->{'buildtime'} || 0;
  $evr .= " $buildtime";
  $evr .= "-$q->{'arch'}" if defined $q->{'arch'};
  return "$q->{'name'}-$evr";
}

sub writedeps {
  my ($fh, $pkg, $url) = @_;
  $url = '' unless defined $url;
  return unless defined($pkg->{'name'}) && defined($pkg->{'arch'});
  return if $pkg->{'arch'} eq 'src' || $pkg->{'arch'} eq 'nosrc';
  my $id = $pkg->{'id'};
  $id = ($pkg->{'buildtime'} || 0)."/".($pkg->{'filetime'} || 0)."/0" unless $id;
  $id = "$pkg->{'name'}.$pkg->{'arch'}-$id: ";
  print $fh "F:$id$url$pkg->{'location'}\n";
  print $fh "P:$id".join(' ', @{$pkg->{'provides'} || []})."\n";
  print $fh "R:$id".join(' ', @{$pkg->{'requires'}})."\n" if $pkg->{'requires'};
  print $fh "C:$id".join(' ', @{$pkg->{'conflicts'}})."\n" if $pkg->{'conflicts'};
  print $fh "O:$id".join(' ', @{$pkg->{'obsoletes'}})."\n" if $pkg->{'obsoletes'};
  print $fh "r:$id".join(' ', @{$pkg->{'recommends'}})."\n" if $pkg->{'recommends'};
  print $fh "s:$id".join(' ', @{$pkg->{'supplements'}})."\n" if $pkg->{'supplements'};
  print $fh "I:$id".getbuildid($pkg)."\n";
}

sub makewhatprovidesh {
  my ($config) = @_;

  my %whatprovides;
  my $provides = $config->{'providesh'};

  for my $p (keys %$provides) {
    my @pp = @{$provides->{$p}};
    s/[ <=>].*// for @pp;
    push @{$whatprovides{$_}}, $p for unify(@pp);
  }
  for my $p (keys %{$config->{'fileprovides'}}) {
    my @pp = grep {@{$provides->{$_} || []}} @{$config->{'fileprovides'}->{$p}};
    @{$whatprovides{$p}} = unify(@{$whatprovides{$p} || []}, @pp) if @pp;
  }
  $config->{'whatprovidesh'} = \%whatprovides;
}

sub setdeps {
  my ($config, $provides, $whatprovides, $requires) = @_;
  $config->{'providesh'} = $provides;
  $config->{'whatprovidesh'} = $whatprovides;
  $config->{'requiresh'} = $requires;
}

sub forgetdeps {
  my ($config) = @_;
  delete $config->{'providesh'};
  delete $config->{'whatprovidesh'};
  delete $config->{'requiresh'};
  delete $config->{'pkgconflictsh'};
  delete $config->{'pkgobsoletesh'};
  delete $config->{'recommendsh'};
  delete $config->{'supplementsh'};
}

my %addproviders_fm = (
  '>'  => 1,
  '='  => 2,
  '==' => 2,
  '>=' => 3,
  '<'  => 4,
  '<=' => 6,
);

sub addproviders {
  my ($config, $r) = @_;

  my @p;
  my $whatprovides = $config->{'whatprovidesh'};
  $whatprovides->{$r} = \@p;
  my $binarytype = $config->{'binarytype'};
  if ($r =~ /\|/) {
    for my $or (split(/\s*\|\s*/, $r)) {
      push @p, @{$whatprovides->{$or} || addproviders($config, $or)};
    }
    @p = unify(@p) if @p > 1;
    return \@p;
  }
  if ($r !~ /^(.*?)\s*([<=>]{1,2})\s*(.*?)$/) {
    @p = @{$whatprovides->{$r} || addproviders($config, $r)} if $binarytype eq 'deb' && $r =~ s/:any$//;
    return \@p;
  }
  my $rn = $1;
  my $rv = $3;
  my $rf = $addproviders_fm{$2};
  return \@p unless $rf;
  $rn =~ s/:any$// if $binarytype eq 'deb';
  my $provides = $config->{'providesh'};
  my @rp = @{$whatprovides->{$rn} || []};
  for my $rp (@rp) {
    for my $pp (@{$provides->{$rp} || []}) {
      if ($pp eq $rn) {
	# debian: unversioned provides do not match
	next if $binarytype eq 'deb';
	push @p, $rp;
	last;
      }
      next unless $pp =~ /^\Q$rn\E\s*([<=>]{1,2})\s*(.*?)$/;
      my $pv = $2;
      my $pf = $addproviders_fm{$1};
      next unless $pf;
      if ($pf & $rf & 5) {
	push @p, $rp;
	last;
      }
      if ($pv eq $rv) {
	next unless $pf & $rf & 2;
	push @p, $rp;
	last;
      }
      my $rr = $rf == 2 ? $pf : ($rf ^ 5);
      $rr &= 5 unless $pf & 2;
      # verscmp for spec and kiwi types
      my $vv;
      if ($binarytype eq 'deb') {
	$vv = Build::Deb::verscmp($pv, $rv, 1);
      } else {
	$vv = Build::Rpm::verscmp($pv, $rv, 1);
      }
      if ($rr & (1 << ($vv + 1))) {
	push @p, $rp;
	last;
      }
    }
  }
  @p = unify(@p) if @p > 1;
  return \@p;
}

# XXX: should also check the package EVR
sub nevrmatch {
  my ($config, $r, @p) = @_;
  my $rn = $r;
  $rn =~ s/\s*([<=>]{1,2}).*$//;
  return grep {$_ eq $rn} @p;
}

# check if package $q has a conflict against an installed package.
# if yes, add message to @$eq and return true
sub checkconflicts {
  my ($config, $ins, $q, $eq, @r) = @_;
  my $whatprovides = $config->{'whatprovidesh'};
  my $ret = 0;
  for my $r (@r) {
    if ($r =~ /^\(.*\)$/) {
      # note the []: we ignore errors here. they will be reported if the package is chosen.
      my $n = normalizerich($config, $q, $r, 1, []);
      $ret = 1 if check_conddeps_notinst($q, $n, $eq, $ins);
      next;
    }
    my @eq = grep {$ins->{$_}} @{$whatprovides->{$r} || addproviders($config, $r)};
    next unless @eq;
    push @$eq, map {"(provider $q conflicts with $_)"} @eq;
    $ret = 1;
  }
  return $ret;
}

sub checkobsoletes {
  my ($config, $ins, $q, $eq, @r) = @_;
  my $whatprovides = $config->{'whatprovidesh'};
  my $ret = 0;
  for my $r (@r) {
    my @eq = grep {$ins->{$_}} nevrmatch($config, $r, @{$whatprovides->{$r} || addproviders($config, $r)});
    next unless @eq;
    push @$eq, map {"(provider $q obsoletes $_)"} @eq;
    $ret = 1;
  }
  return $ret;
}

sub todo2recommended {
  my ($config, $recommended, $todo) = @_;
  my $whatprovides = $config->{'whatprovidesh'};
  my $pkgrecommends = $config->{'recommendsh'} || {};
  for my $p (splice @$todo) {
    for my $r (@{$pkgrecommends->{$p} || []}) {
      $recommended->{$_} = 1 for @{$whatprovides->{$r} || addproviders($config, $r)}
    }
  }
}

sub cplx_mix {
  my ($q1, $q2, $todnf) = @_;
  my @q;
  for my $qq1 (@$q1) {
    for my $qq2 (@$q2) {
      my @qq = unify(sort(@$qq1, @$qq2));
      my %qq = map {$_ => 1} @qq;
      push @q, \@qq unless grep {$qq{"-$_"}} @qq;
    }
  }
  return $todnf ? 0 : 1 unless @q;
  return (-1, @q);
}

sub cplx_inv {
  my ($f, @q) = @_;
  return 1 - $f if $f == 0 || $f == 1;
  my @iq;
  for my $q (@q) {
    $q = [ map {"-$_"} @$q ];
    s/^--// for @$q;
  }
  return (-1, @q);
}

sub normalize_cplx_rec {
  my ($c, $r, $todnf) = @_;
  if ($r->[0] == 0) {
    my $ri = (split(/[ <=>]/, $r->[1], 2))[0];
    my ($config, $p, $ignore, $xignore) = @$c;
    if (!$todnf) {
      return 1 if $ignore->{$ri} || $xignore->{$ri};
      return 1 if defined($p) && ($ignore->{"$p:$ri"} || $xignore->{"$p:$ri"});
    }
    my $whatprovides = $config->{'whatprovidesh'};
    my @q = @{$whatprovides->{$r->[1]} || addproviders($config, $r->[1])};
    return 0 unless @q;
    if ($todnf) {
      return (-1, map { [ $_ ] } @q);
    } else {
      return (-1, [ @q ]);
    }
  }
  if ($r->[0] == 3 && @$r == 4) {
    # complex if/else case: A IF (B ELSE C) -> (A OR ~B) AND (C OR B)
    my ($n1, @q1) = normalize_cplx_rec($c, [3, $r->[1], $r->[2]], $todnf);
    my ($n2, @q2) = normalize_cplx_rec($c, [2, $r->[2], $r->[3]], $todnf);
    return 0 if $n1 == 0 || $n2 == 0;
    return ($n2, @q2) if $n1 == 1;
    return ($n1, @q1) if $n2 == 1;
    if (!$todnf) {
      return (-1, @q1, @q2);
    } else {
      return cplx_mix(\@q1, \@q2, $todnf);
    }
  }
  if ($r->[0] == 4 && @$r == 4) {
    # complex unless/else case: A UNLESS (B ELSE C) -> (A AND ~B) OR (C AND B)
    my ($n1, @q1) = normalize_cplx_rec($c, [4, $r->[1], $r->[2]], $todnf);
    my ($n2, @q2) = normalize_cplx_rec($c, [1, $r->[2], $r->[3]], $todnf);
    return 1 if $n1 == 1 || $n2 == 1;
    return ($n2, @q2) if $n1 == 0;
    return ($n1, @q1) if $n2 == 0;
    if ($todnf) {
      return (-1, @q1, @q2);
    } else {
      return cplx_mix(\@q1, \@q2, $todnf);
    }
  }
  if ($r->[0] == 1 || $r->[0] == 4) {
    # and / unless
    my $todnf2 = $r->[0] == 4 ? !$todnf : $todnf;
    my ($n1, @q1) = normalize_cplx_rec($c, $r->[1], $todnf);
    my ($n2, @q2) = normalize_cplx_rec($c, $r->[2], $todnf);
    ($n2, @q2) = cplx_inv($n2, @q2) if $r->[0] == 4;
    return 0 if $n1 == 0 || $n2 == 0;
    return ($n2, @q2) if $n1 == 1;
    return ($n1, @q1) if $n2 == 1;
    if (!$todnf) {
      return (-1, @q1, @q2);
    } else {
      return cplx_mix(\@q1, \@q2, $todnf);
    }
  }
  if ($r->[0] == 2 || $r->[0] == 3) {
    # or / if
    my $todnf2 = $r->[0] == 3 ? !$todnf : $todnf;
    my ($n1, @q1) = normalize_cplx_rec($c, $r->[1], $todnf);
    my ($n2, @q2) = normalize_cplx_rec($c, $r->[2], $todnf2);
    ($n2, @q2) = cplx_inv($n2, @q2) if $r->[0] == 3;
    return 1 if $n1 == 1 || $n2 == 1;
    return ($n2, @q2) if $n1 == 0;
    return ($n1, @q1) if $n2 == 0;
    if ($todnf) {
      return (-1, @q1, @q2);
    } else {
      return cplx_mix(\@q1, \@q2, $todnf);
    }
  }
  if ($r->[0] == 6 || $r->[0] == 7) {
    # with / without
    my ($n1, @q1) = normalize_cplx_rec($c, $r->[1], 0);
    my ($n2, @q2) = normalize_cplx_rec($c, $r->[2], 0);
    if ($n2 == 0 && $r->[0] == 7) {
      @q2 = ( [] );
      $n2 = -1;
    }
    return 0 if $n1 != -1 || $n2 != -1;
    return 0 if @q1 != 1 || @q2 != 1;
    @q1 = @{$q1[0]};
    @q2 = @{$q2[0]};
    return 0 if grep {/^-/} @q1;
    return 0 if grep {/^-/} @q2;
    my %q2 = map {$_ => 1} @q2;
    my @q;
    if ($r->[0] == 6) {
      @q = grep {$q2{$_}} @q1;
    } else {
      @q = grep {!$q2{$_}} @q1;
    }
    return 0 unless @q;
    if ($todnf) {
      return (-1, map { [ $_ ] } @q);
    } else {
      return (-1, [ @q ]);
    }
  }
  return 0;
}

sub normalizerich {
  my ($config, $p, $dep, $deptype, $error, $ignore, $xignore) = @_;
  my $r = Build::Rpm::parse_rich_dep($dep);
  if (!$r) {
    if (defined($p)) {
      push @$error, "cannot parse dependency $dep from $p";
    } else {
      push @$error, "cannot parse dependency $dep";
    }
    return [];
  }
  my $c = [$config, $p, $ignore || {}, $xignore || {}];
  my ($n, @q);
  if ($deptype == 0) {
    ($n, @q) = normalize_cplx_rec($c, $r);
    return () if $n == 1;
    if (!$n) {
      if (defined($p)) {
        push @$error, "nothing provides $dep needed by $p";
      } else {
        push @$error, "nothing provides $dep";
      }
      return [];
    }
  } else {
    ($n, @q) = normalize_cplx_rec($c, $r, 1);
    ($n, @q) = cplx_inv($n, @q);
    if (!$n) {
      if (defined($p)) {
        push @$error, "$p conflicts with always true $dep";
      } else {
        push @$error, "conflict with always true $dep";
      }
    }
  }
  for my $q (@q) {
    my @neg = @$q;
    @neg = grep {s/^-//} @neg;
    @neg = grep {$_ ne $p} @neg if defined $p;
    @$q = grep {!/^-/} @$q;
    $q = [$dep, $deptype, \@neg, @$q];
  }
  return \@q;
}

# handle a normalized rich dependency from install of package p
# todo_cond is undef if we are re-checking the cond queue
sub check_conddeps_inst {
  my ($p, $n, $error, $installed, $aconflicts, $todo, $todo_cond) = @_;
  for my $c (@$n) {
    my ($r, $rtype, $cond, @q) = @$c; 
    next unless defined $cond;			# already handled?
    next if grep {$installed->{$_}} @q;         # already fulfilled
    my @cx = grep {!$installed->{$_}} @$cond;   # open conditions
    if (!@cx) {
      $c->[2] = undef;				# mark as handled to avoid dups
      if (@q) {
        push @$todo, $c, $p;
      } elsif (@$cond) {
	if (defined($p)) {
	  push @$error, map {"$p conflicts with $_"} sort(@$cond);
	} else {
	  push @$error, map {"conflicts with $_"} sort(@$cond);
	}
      }    
    } else {
      if (!@q && @cx == 1) { 
	if (defined($p)) {
          $aconflicts->{$cx[0]} = "is in conflict with $p";
	} else {
          $aconflicts->{$cx[0]} = "is in conflict";
	}
      } elsif ($todo_cond) {
        push @{$todo_cond->{$_}}, [ $c, $p ] for @cx;
      }
    }
  }
}

# handle a normalized rich dependency from a not-yet installed package
# (we just check conflicts)
sub check_conddeps_notinst {
  my ($p, $n, $eq, $installed) = @_;
  my $ret = 0;
  for my $c (@$n) {
    my ($r, $rtype, $cond, @q) = @$c; 
    next if @q || !@$cond || grep {!$installed->{$_}} @$cond;
    push @$eq, map {"(provider $p conflicts with $_)"} sort(@$cond);
    $ret = 1;
  }
  return $ret;
}

sub expand {
  my ($config, @p) = @_;

  my $conflicts = $config->{'conflicth'};
  my $pkgconflicts = $config->{'pkgconflictsh'} || {};
  my $pkgobsoletes = $config->{'pkgobsoletesh'} || {};
  my $prefer = $config->{'preferh'};
  my $ignore = $config->{'ignoreh'};
  my $ignoreconflicts = $config->{'expandflags:ignoreconflicts'};
  my $ignoreignore;
  my $userecommendsforchoices = 1;

  my $whatprovides = $config->{'whatprovidesh'};
  my $requires = $config->{'requiresh'};

  my $xignore = { map {substr($_, 1) => 1} grep {/^-/} @p };
  $ignore = {} if $xignore->{'-ignoreignore--'};
  if ($ignoreignore) {
    $xignore = {};
    $ignore = {};
  }
  my @directdepsend;
  if ($xignore->{'-directdepsend--'}) {
    delete $xignore->{'-directdepsend--'};
    @directdepsend = @p;
    for my $p (splice @p) {
      last if $p eq '--directdepsend--';
      push @p, $p;
    }
    @directdepsend = grep {!/^-/} splice(@directdepsend, @p + 1);
  }

  my %p;		# expanded packages
  my @todo;		# dependencies to install
  my @todo_inst;	# packages we decided to install
  my %todo_cond;
  my %recommended;	# recommended by installed packages
  my @rec_todo;		# installed todo
  my @error;
  my %aconflicts;	# packages we are conflicting with

  # handle direct conflicts
  for (grep {/^!/} @p) {
    my $r = /^!!/ ? substr($_, 2) : substr($_, 1);
    if ($r =~ /^\(.*\)$/) {
      my $n = normalizerich($config, undef, $r, 1, \@error);
      my %naconflicts;
      check_conddeps_inst(undef, $n, \@error, \%p, \%naconflicts, \@todo, \%todo_cond);
      push @{$aconflicts{$_}}, $naconflicts{$_} for keys %naconflicts;
      next;
    }
    my @q = @{$whatprovides->{$r} || addproviders($config, $r)};
    @q = nevrmatch($config, $r, @q) if /^!!/;
    push @{$aconflicts{$_}}, "is in conflict" for @q;
  }
  @p = grep {!/^[-!]/} @p;

  # add direct dependency packages. this is different from below,
  # because we add packages even if the dep is already provided and
  # we break ambiguities if the name is an exact match.
  for my $r (splice @p) {
    if ($r =~ /^\(.*\)$/) {
      push @p, $r;	# rich deps are never direct
      next;
    }
    my @q = @{$whatprovides->{$r} || addproviders($config, $r)};
    my $pn = $r;
    $pn =~ s/ .*//;
    @q = grep {$_ eq $pn} @q;
    if (@q != 1) {
      push @p, $r;
      next;
    }
    my $p = $q[0];
    print "added $p because of $r (direct dep)\n" if $expand_dbg;
    push @todo_inst, $p;
  }

  for my $r (@p, @directdepsend) {
    if ($r =~ /^\(.*\)$/) {
      # rich dep. normalize, put on todo.
      my $n = normalizerich($config, undef, $r, 0, \@error);
      my %naconflicts;
      check_conddeps_inst(undef, $n, \@error, \%p, \%naconflicts, \@todo, \%todo_cond);
      push @{$aconflicts{$_}}, $naconflicts{$_} for keys %naconflicts;
    } else {
      push @todo, $r, undef;
    }
  }

  for my $p (@todo_inst) {
    push @error, map {"$p $_"} @{$aconflicts{$p}} if $aconflicts{$p};
  }
  return (undef, @error) if @error;

  while (@todo || @todo_inst) {
    # install a set of chosen packages
    # ($aconficts must not be set for any of them)
    if (@todo_inst) {
      @todo_inst = unify(@todo_inst) if @todo_inst > 1;

      # check aconflicts (just in case)
      for my $p (@todo_inst) {
        push @error, map {"$p $_"} @{$aconflicts{$p}} if $aconflicts{$p};
      }
      return (undef, @error) if @error;

      # check against old cond dependencies. we do this step by step so we don't get dups.
      for my $p (@todo_inst) {
	$p{$p} = 1;
	if ($todo_cond{$p}) {
          for my $c (@{delete $todo_cond{$p}}) {
	    my %naconflicts;
	    check_conddeps_inst($c->[1], [ $c->[0] ], \@error, \%p, \%naconflicts, \@todo);
	    push @{$aconflicts{$_}}, $naconflicts{$_} for keys %naconflicts;
	  }
	}
        delete $aconflicts{$p};		# no longer needed
      }
      return undef, @error if @error;

      # now check our own dependencies
      for my $p (@todo_inst) {
	my %naconflicts;
	my %naobsoletes;
	$naconflicts{$_} = "is in conflict with $p" for @{$conflicts->{$p} || []};
	for my $r (@{$requires->{$p} || []}) {
	  if ($r =~ /^\(.*\)$/) {
	    my $n = normalizerich($config, $p, $r, 0, \@error, $ignore, $xignore);
	    check_conddeps_inst($p, $n, \@error, \%p, \%naconflicts, \@todo, \%todo_cond);
	    next;
	  }
	  my $ri = (split(/[ <=>]/, $r, 2))[0];
	  next if $ignore->{"$p:$ri"} || $xignore->{"$p:$ri"};
	  next if $ignore->{$ri} || $xignore->{$ri};
	  push @todo, ($r, $p);
	}
	if (!$ignoreconflicts) {
	  for my $r (@{$pkgconflicts->{$p}}) {
	    if ($r =~ /^\(.*\)$/) {
	      my $n = normalizerich($config, $p, $r, 1, \@error);
	      check_conddeps_inst($p, $n, \@error, \%p, \%naconflicts, \@todo, \%todo_cond);
	      next;
	    }
	    $naconflicts{$_} = "is in conflict with $p" for @{$whatprovides->{$r} || addproviders($config, $r)};
	  }
	  for my $r (@{$pkgobsoletes->{$p}}) {
	    $naobsoletes{$_} =  "is obsoleted by $p" for nevrmatch($config, $r, @{$whatprovides->{$r} || addproviders($config, $r)});
	  }
	}
	if (%naconflicts) {
	  push @error, map {"$p conflicts with $_"} grep {$_ ne $p && $p{$_}} sort keys %naconflicts;
	  push @{$aconflicts{$_}}, $naconflicts{$_} for keys %naconflicts;
	}
	if (%naobsoletes) {
	  push @error, map {"$p obsoletes $_"} grep {$_ ne $p && $p{$_}} sort keys %naobsoletes;
	  push @{$aconflicts{$_}}, $naobsoletes{$_} for keys %naobsoletes;
	}
	push @rec_todo, $p if $userecommendsforchoices;
      }
      return undef, @error if @error;
      @todo_inst = ();
    }
 
    for my $pass (0, 1, 2, 3, 4, 5) {
      my @todo_next;
      while (@todo) {
	my ($r, $p) = splice(@todo, 0, 2);
	my $rtodo = $r;
	my @q;
	if (ref($r)) {
	  ($r, undef, undef, @q) = @$r;
	} else {
	  @q = @{$whatprovides->{$r} || addproviders($config, $r)};
	}
	next if grep {$p{$_}} @q;
	my $pp = defined($p) ? "$p:" : '';
	my $pn = defined($p) ? " needed by $p" : '';
	if (defined($p) && !$ignoreignore) {
	  next if grep {$ignore->{$_} || $xignore->{$_}} @q;
	  next if grep {$ignore->{"$pp$_"} || $xignore->{"$pp$_"}} @q;
	}

	if (!@q) {
	  next if $r =~ /^\// && defined($p);
	  push @error, "nothing provides $r$pn";
	  next;
	}

	if (@q > 1 && $pass == 0) {
	  push @todo_next, $rtodo, $p;
	  next;
	}

	# pass 0: only one provider
	# pass 1: conflict pruning
        my $nq = @q;
	my @eq;
	for my $q (@q) {
	  push @eq, map {"(provider $q $_)"} @{$aconflicts{$q}} if $aconflicts{$q};
	}
	@q = grep {!$aconflicts{$_}} @q;
	if (!$ignoreconflicts) {
	  for my $q (splice @q) {
	    push @q, $q unless @{$pkgconflicts->{$q} || []} && checkconflicts($config, \%p, $q, \@eq, @{$pkgconflicts->{$q}});
	  }
	  for my $q (splice @q) {
	    push @q, $q unless @{$pkgobsoletes->{$q} || []} && checkobsoletes($config, \%p, $q, \@eq, @{$pkgobsoletes->{$q}});
	  }
	}

	if (!@q) {
	  push @error, "conflict for providers of $r$pn", sort(@eq);
	  next;
	}
        if (@q == 1) {
	  push @todo_inst, $q[0];
	  print "added $q[0] because of $pp$r\n" if $expand_dbg;
          next;
        }

	# pass 2: prune neg prefers and simple pos prefers
        if ($pass < 2) {
	  print "undecided about $pp$r: @q\n" if $expand_dbg;
	  push @todo_next, $rtodo, $p;
	  next;
        }
	if (@q > 1) {
	  my @pq = grep {!$prefer->{"-$_"} && !$prefer->{"-$pp$_"}} @q;
	  @q = @pq if @pq;
	  @pq = grep {$prefer->{$_} || $prefer->{"$pp$_"}} @q;
	  @q = @pq if @pq == 1;
	}
        if (@q == 1) {
	  push @todo_inst, $q[0];
	  print "added $q[0] because of $pp$r\n" if $expand_dbg;
          next;
        }

	# pass 3: prune pos prefers and debian choice deps
        if ($pass < 3) {
	  push @todo_next, $rtodo, $p;
	  next;
        }
	if (@q > 1) {
	  my @pq = grep {$prefer->{$_} || $prefer->{"$pp$_"}} @q;
	  if (@pq > 1) {
	    my %pq = map {$_ => 1} @pq;
	    @q = (grep {$pq{$_}} @{$config->{'prefer'}})[0];
	  } elsif (@pq == 1) {
	    @q = @pq;
	  }
        }
	if (@q > 1 && $r =~ /\|/) {
	  # choice op, implicit prefer of first match...
	  my %pq = map {$_ => 1} @q;
	  for my $rr (split(/\s*\|\s*/, $r)) {
	    next unless $whatprovides->{$rr};
	    my @pq = grep {$pq{$_}} @{$whatprovides->{$rr}};
	      next unless @pq;
	      @q = @pq;
	      last;
	  }
	}
        if (@q == 1) {
	  push @todo_inst, $q[0];
	  print "added $q[0] because of $pp$r\n" if $expand_dbg;
          next;
        }

	# pass 4: prune recommends
        if ($pass < 4) {
	  push @todo_next, $rtodo, $p;
	  next;
        }
	todo2recommended($config, \%recommended, \@rec_todo) if @rec_todo;
	my @pq = grep {$recommended{$_}} @q;
	print "recommended [@pq] among [@q]\n" if $expand_dbg;
	@q = @pq if @pq;
        if (@q == 1) {
	  push @todo_inst, $q[0];
	  print "added $q[0] because of $pp$r\n" if $expand_dbg;
          next;
        }

        # pass 5: record error
        if ($pass < 5) {
	  push @todo_next, $rtodo, $p;
	  next;
        }
	@q = sort(@q);
	if (defined($p)) {
	  push @error, "have choice for $r needed by $p: @q";
	} else {
	  push @error, "have choice for $r: @q";
	}
      }
      @todo = @todo_next;
      last if @todo_inst;
    }
    return undef, @error if @error;
  }

  return 1, (sort keys %p);
}

sub order {
  my ($config, @p) = @_;

  my $requires = $config->{'requiresh'};
  my $recommends = $config->{'recommendsh'};
  my $whatprovides = $config->{'whatprovidesh'};
  my %deps;
  my %rdeps;
  my %needed;
  my %p = map {$_ => 1} @p;
  for my $p (@p) {
    my @r;
    for my $r (@{$requires->{$p} || []}) {
      my @q = @{$whatprovides->{$r} || addproviders($config, $r)};
      push @r, grep {$_ ne $p && $p{$_}} @q;
    }
    if (%{$config->{'order'} || {}}) {
      push @r, grep {$_ ne $p && $config->{'order'}->{"$_:$p"}} @p;
    }
    @r = unify(@r);
    $deps{$p} = \@r;
    $needed{$p} = @r;
    push @{$rdeps{$_}}, $p for @r;
  }
  @p = sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @p;
  my @good;
  my @res;
  # the big sort loop
  while (@p) {
    @good = grep {$needed{$_} == 0} @p;
    if (@good) {
      @p = grep {$needed{$_}} @p;
      push @res, @good;
      for my $p (@good) {
	$needed{$_}-- for @{$rdeps{$p}};
      }
      next;
    }
    # uh oh, cycle alert. find and remove all cycles.
    my %notdone = map {$_ => 1} @p;
    $notdone{$_} = 0 for @res;  # already did those
    my @todo = @p;
    while (@todo) {
      my $v = shift @todo;
      if (ref($v)) {
	$notdone{$$v} = 0;      # finished this one
	next;
      }
      my $s = $notdone{$v};
      next unless $s;
      my @e = grep {$notdone{$_}} @{$deps{$v}};
      if (!@e) {
	$notdone{$v} = 0;       # all deps done, mark as finished
	next;
      }
      if ($s == 1) {
	$notdone{$v} = 2;       # now under investigation
	unshift @todo, @e, \$v;
	next;
      }
      # reached visited package, found a cycle!
      my @cyc = ();
      my $cycv = $v;
      # go back till $v is reached again
      while(1) {
	die unless @todo;
	$v = shift @todo;
	next unless ref($v);
	$v = $$v;
	$notdone{$v} = 1 if $notdone{$v} == 2;
	unshift @cyc, $v;
	last if $v eq $cycv;
      }
      unshift @todo, $cycv;
      print STDERR "cycle: ".join(' -> ', @cyc)."\n";
      my $breakv;
      my @breakv = (@cyc, $cyc[0]);
      while (@breakv > 1) {
	last if $config->{'order'}->{"$breakv[0]:$breakv[1]"};
	shift @breakv;
      }
      if (@breakv > 1) {
	$breakv = $breakv[0];
      } else {
	$breakv = (sort {$needed{$a} <=> $needed{$b} || $a cmp $b} @cyc)[-1];
      }
      push @cyc, $cyc[0];	# make it loop
      shift @cyc while $cyc[0] ne $breakv;
      $v = $cyc[1];
      print STDERR "  breaking dependency $breakv -> $v\n";
      $deps{$breakv} = [ grep {$_ ne $v} @{$deps{$breakv}} ];
      $rdeps{$v} = [ grep {$_ ne $breakv} @{$rdeps{$v}} ];
      $needed{$breakv}--;
    }
  }
  return @res;
}

sub add_all_providers {
  my ($config, @p) = @_;
  my $whatprovides = $config->{'whatprovidesh'};
  my $requires = $config->{'requiresh'};
  my $recommends = $config->{'recommendsh'};
  my %a;
  for my $p (@p) {
    for my $r (@{$requires->{$p} || [$p]}) {
      my $rn = (split(' ', $r, 2))[0];
      $a{$_} = 1 for @{$whatprovides->{$rn} || []};
    }
  }
  push @p, keys %a;
  return unify(@p);
}

###########################################################################

sub recipe2buildtype {
  my ($recipe) = @_;
  return undef unless defined $recipe;
  return $1 if $recipe =~ /\.(spec|dsc|kiwi|livebuild)$/;
  $recipe =~ s/.*\///;
  $recipe =~ s/^_service:.*://;
  return 'arch' if $recipe eq 'PKGBUILD';
  return 'collax' if $recipe eq 'build.collax';
  return 'snapcraft' if $recipe eq 'snapcraft.yaml';
  return 'appimage' if $recipe eq 'appimage.yml';
  return 'docker' if $recipe eq 'Dockerfile';
  return 'fissile' if $recipe eq 'fissile.yml';
  return 'preinstallimage' if $recipe eq '_preinstallimage';
  return 'simpleimage' if $recipe eq 'simpleimage';
  return undef;
}

sub show {
  my ($conffile, $fn, $field, $arch) = @ARGV;
  my $cf = read_config($arch, $conffile);
  die unless $cf;
  my $d = Build::parse($cf, $fn);
  die("$d->{'error'}\n") if $d->{'error'};
  $d->{'sources'} = [ map {ref($d->{$_}) ? @{$d->{$_}} : $d->{$_}} grep {/^source/} sort keys %$d ];
  $d->{'patches'} = [ map {ref($d->{$_}) ? @{$d->{$_}} : $d->{$_}} grep {/^patch/} sort keys %$d ];
  my $x = $d->{$field};
  $x = [ $x ] unless ref $x;
  print "$_\n" for @$x;
}

sub parse_preinstallimage {
  return undef unless $do_rpm;
  my $d = Build::Rpm::parse(@_);
  $d->{'name'} ||= 'preinstallimage';
  return $d;
}

sub parse_simpleimage {
  return undef unless $do_rpm;
  my $d = Build::Rpm::parse(@_);
  $d->{'name'} ||= 'simpleimage';
  if (!defined($d->{'version'})) {
    my @s = stat($_[1]);
    $d->{'version'} = strftime "%Y.%m.%d-%H.%M.%S", gmtime($s[9] || time);
  }
  return $d;
}

sub parse {
  my ($cf, $fn, @args) = @_;
  return Build::Rpm::parse($cf, $fn, @args) if $do_rpm && $fn =~ /\.spec$/;
  return Build::Deb::parse($cf, $fn, @args) if $do_deb && $fn =~ /\.dsc$/;
  return Build::Kiwi::parse($cf, $fn, @args) if $do_kiwi && $fn =~ /config\.xml$/;
  return Build::Kiwi::parse($cf, $fn, @args) if $do_kiwi && $fn =~ /\.kiwi$/;
  return Build::LiveBuild::parse($cf, $fn, @args) if $do_livebuild && $fn =~ /\.livebuild$/;
  my $fnx = $fn;
  $fnx =~ s/.*\///;
  $fnx =~ s/^[0-9a-f]{32,}-//;	# hack for OBS srcrep implementation
  $fnx =~ s/^_service:.*://;
  return parse_simpleimage($cf, $fn, @args) if $fnx eq 'simpleimage';
  return Build::Snapcraft::parse($cf, $fn, @args) if $do_snapcraft && $fnx eq 'snapcraft.yaml';
  return Build::Appimage::parse($cf, $fn, @args) if $do_appimage && $fnx eq 'appimage.yml';
  return Build::Docker::parse($cf, $fn, @args) if $do_docker && $fnx eq 'Dockerfile';
  return Build::Fissile::parse($cf, $fn, @args) if $do_fissile && $fnx eq 'fissile.yml';
  return Build::Arch::parse($cf, $fn, @args) if $do_arch && $fnx eq 'PKGBUILD';
  return Build::Collax::parse($cf, $fn, @args) if $do_collax && $fnx eq 'build.collax';
  return parse_preinstallimage($cf, $fn, @args) if $fnx eq '_preinstallimage';
  return undef;
}

sub parse_typed {
  my ($cf, $fn, $buildtype, @args) = @_;
  $buildtype ||= '';
  return Build::Rpm::parse($cf, $fn, @args) if $do_rpm && $buildtype eq 'spec';
  return Build::Deb::parse($cf, $fn, @args) if $do_deb && $buildtype eq 'dsc';
  return Build::Kiwi::parse($cf, $fn, @args) if $do_kiwi && $buildtype eq 'kiwi';
  return Build::LiveBuild::parse($cf, $fn, @args) if $do_livebuild && $buildtype eq 'livebuild';
  return Build::Snapcraft::parse($cf, $fn, @args) if $do_snapcraft && $buildtype eq 'snapcraft';
  return Build::Appimage::parse($cf, $fn, @args) if $do_appimage && $buildtype eq 'appimage';
  return Build::Docker::parse($cf, $fn, @args) if $do_docker && $buildtype eq 'docker';
  return Build::Fissile::parse($cf, $fn, @args) if $do_fissile && $buildtype eq 'fissile';
  return parse_simpleimage($cf, $fn, @args) if $buildtype eq 'simpleimage';
  return Build::Arch::parse($cf, $fn, @args) if $do_arch && $buildtype eq 'arch';
  return Build::Collax::parse($cf, $fn, @args) if $do_collax && $buildtype eq 'collax';
  return parse_preinstallimage($cf, $fn, @args) if $buildtype eq 'preinstallimage';
  return undef;
}

sub query {
  my ($binname, %opts) = @_;
  my $handle = $binname;
  if (ref($binname) eq 'ARRAY') {
    $handle = $binname->[1];
    $binname = $binname->[0];
  }
  return Build::Rpm::query($handle, %opts) if $do_rpm && $binname =~ /\.d?rpm$/;
  return Build::Deb::query($handle, %opts) if $do_deb && $binname =~ /\.deb$/;
  return Build::Kiwi::queryiso($handle, %opts) if $do_kiwi && $binname =~ /\.iso$/;
  return Build::Arch::query($handle, %opts) if $do_arch && $binname =~ /\.pkg\.tar(?:\.gz|\.xz)?$/;
  return Build::Arch::query($handle, %opts) if $do_arch && $binname =~ /\.arch$/;
  return undef;
}

sub showquery {
  my ($fn, $field) = @ARGV;
  my %opts;
  $opts{'evra'} = 1 if grep {$_ eq $field} qw{epoch version release arch buildid};
  $opts{'weakdeps'} = 1 if grep {$_ eq $field} qw{suggests enhances recommends supplements};
  $opts{'conflicts'} = 1 if grep {$_ eq $field} qw{conflicts obsoletes};
  $opts{'description'} = 1 if grep {$_ eq $field} qw{summary description};
  $opts{'filelist'} = 1 if $field eq 'filelist';
  $opts{'buildtime'} = 1 if grep {$_ eq $field} qw{buildtime buildid};
  my $d = Build::query($fn, %opts);
  die("cannot query $fn\n") unless $d;
  $d->{'buildid'} = getbuildid($d);
  my $x = $d->{$field};
  $x = [] unless defined $x;
  $x = [ $x ] unless ref $x;
  print "$_\n" for @$x;
}

sub queryhdrmd5 {
  my ($binname) = @_;
  return Build::Rpm::queryhdrmd5(@_) if $do_rpm && $binname =~ /\.d?rpm$/;
  return Build::Deb::queryhdrmd5(@_) if $do_deb && $binname =~ /\.deb$/;
  return Build::Kiwi::queryhdrmd5(@_) if $do_kiwi && $binname =~ /\.iso$/;
  return Build::Kiwi::queryhdrmd5(@_) if $do_kiwi && $binname =~ /\.raw$/;
  return Build::Kiwi::queryhdrmd5(@_) if $do_kiwi && $binname =~ /\.raw.install$/;
  return Build::Arch::queryhdrmd5(@_) if $do_arch && $binname =~ /\.pkg\.tar(?:\.gz|\.xz)?$/;
  return Build::Arch::queryhdrmd5(@_) if $do_arch && $binname =~ /\.arch$/;
  return undef;
}

sub queryinstalled {
  my ($binarytype, @args) = @_;
  return Build::Rpm::queryinstalled(@args) if $binarytype eq 'rpm';
  return Build::Deb::queryinstalled(@args) if $binarytype eq 'deb';
  return Build::Arch::queryinstalled(@args) if $binarytype eq 'arch';
  return undef;
}

1;
