#!/usr/bin/ruby

require 'solv'
require 'rubygems'
require 'inifile'
require 'tempfile'

class Repo_generic
  def initialize(name, type, attribs = {})
    @name = name
    @type = type
    @attribs = attribs.dup
    @incomplete = false
  end

  def enabled?
    return @attribs['enabled'].to_i != 0
  end

  def autorefresh?
    return @attribs['autorefresh'].to_i != 0
  end

  def calc_cookie_fp(f)
    chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
    chksum.add_fp(f)
    return chksum.raw
  end

  def calc_cookie_file(filename)
    chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
    chksum.add("1.1")
    chksum.add_stat(filename)
    return chksum.raw
  end

  def cachepath(ext = nil)
    path = @name.sub(/^\./, '_')
    path += ext ? "_#{ext}.solvx" : '.solv'
    return '/var/cache/solv/' + path.gsub(/\//, '_')
  end

  def load(pool)
    @handle = pool.add_repo(@name)
    @handle.appdata = self
    @handle.priority = 99 - @attribs['priority'].to_i if @attribs['priority']
    dorefresh = autorefresh?
    if dorefresh
      begin
        s = File.stat(cachepath)
        dorefresh = false if s && Time.now - s.mtime < @attribs['metadata_expire'].to_i
      rescue SystemCallError
      end
    end
    @cookie = nil
    if !dorefresh && usecachedrepo(nil)
      puts "repo: '#{@name}' cached"
      return true
    end
    return load_if_changed()
  end

  def load_ext(repodata)
    return false
  end

  def load_if_changed
    return false
  end

  def download(file, uncompress, chksum, markincomplete = false)
    url = @attribs['baseurl']
    if !url
      puts "%{@name}: no baseurl"
      return nil
    end
    url = url.sub(/\/$/, '') + "/#{file}"
    f =  Tempfile.new('rbsolv')
    f.unlink
    st = system('curl', '-f', '-s', '-L', '-o', "/dev/fd/" + f.fileno.to_s, '--', url)
    return nil if f.stat.size == 0 && (st || !chksum)
    if !st
	puts "#{file}: download error #{$? >> 8}"
        @incomplete = true if markincomplete
        return nil
    end
    if chksum
      fchksum = Solv::Chksum.new(chksum.type)
      fchksum.add_fd(f.fileno)
      if !fchksum.matches(chksum)
	puts "#{file}: checksum error"
        @incomplete = true if markincomplete
        return nil
      end
    end
    if uncompress
      return Solv::xfopen_dup(file, f.fileno)
    else
      return Solv::xfopen_dup('', f.fileno)
    end
  end

  def download_location(location, chksum)
    f = download(location, false, chksum)
    abort("\n#{@name}: #{location} not found in repository\n") unless f
    return f
  end

  def usecachedrepo(ext, mark = false)
    cookie = ext ? @extcookie : @cookie
    begin
      repopath = cachepath(ext)
      f = File.new(repopath, "r")
      f.sysseek(-32, IO::SEEK_END)
      fcookie = f.sysread(32)
      return false if fcookie.length != 32
      return false if cookie && fcookie != cookie
      if !ext && @type != 'system'
        f.sysseek(-32 * 2, IO::SEEK_END)
        fextcookie = f.sysread(32)
        return false if fextcookie.length != 32
      end
      f.sysseek(0, IO::SEEK_SET)
      f = Solv::xfopen_dup('', f.fileno)
      flags = ext ? Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES : 0
      flags |= Solv::Repo::REPO_LOCALPOOL if ext && ext != 'DL'
      if ! @handle.add_solv(f, flags)
        Solv::xfclose(f)
        return false
      end
      Solv::xfclose(f)
      @cookie = fcookie unless ext
      @extcookie = fextcookie if !ext && @type != 'system'
      now = Time.now
      begin
        File::utime(now, now, repopath) if mark
      rescue SystemCallError
      end
      return true
    rescue SystemCallError
      return false
    end
    return true
  end

  def genextcookie(f)
    chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
    chksum.add(@cookie)
    if f
      s = f.stat()
      chksum.add(s.dev.to_s)
      chksum.add(s.ino.to_s)
      chksum.add(s.size.to_s)
      chksum.add(s.mtime.to_s)
    end
    @extcookie = chksum.raw()
    @extcookie[0] = 1 if @extcookie[0] == 0
  end

  def writecachedrepo(ext, info = nil)
    begin
      Dir::mkdir("/var/cache/solv", 0755) unless FileTest.directory?("/var/cache/solv")
      f =  Tempfile.new('.newsolv-', '/var/cache/solv')
      f.chmod(0444)
      sf = Solv::xfopen_dup('', f.fileno)
      if !info
	@handle.write(sf)
      elsif ext
	info.write(sf)
      else
	@handle.write_first_repodata(sf)
      end
      Solv::xfclose(sf)
      f.sysseek(0, IO::SEEK_END)
      if @type != 'system' && !ext
        genextcookie(f) unless @extcookie
        f.syswrite(@extcookie)
      end
      f.syswrite(ext ? @extcookie : @cookie)
      f.close(false)
      if @handle.iscontiguous?
	sf = Solv::xfopen(f.path)
	if sf
	  if !ext
	    @handle.empty()
	    abort("internal error, cannot reload solv file") unless @handle.add_solv(sf, Solv::Repo::SOLV_ADD_NO_STUBS)
	  else
	    info.extend_to_repo()
	    info.add_solv(sf, Solv::Repo::REPO_EXTEND_SOLVABLES)
	  end
	  Solv::xfclose(sf)
	end
      end
      File.rename(f.path, cachepath(ext))
      f.unlink
      return true
    rescue SystemCallError
      return false
    end
  end

  def updateaddedprovides(addedprovides)
    return if @incomplete
    return unless @handle && !@handle.isempty?
    repodata = @handle.first_repodata()
    return unless repodata
    oldaddedprovides = repodata.lookup_idarray(Solv::SOLVID_META, Solv::REPOSITORY_ADDEDFILEPROVIDES)
    return if (oldaddedprovides | addedprovides) == oldaddedprovides
    for id in addedprovides
      repodata.add_idarray(Solv::SOLVID_META, Solv::REPOSITORY_ADDEDFILEPROVIDES, id)
    end
    repodata.internalize()
    writecachedrepo(nil, repodata)
  end
end

class Repo_rpmmd < Repo_generic

  def find(what)
    di = @handle.Dataiterator(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE, what, Solv::Dataiterator::SEARCH_STRING)
    di.prepend_keyname(Solv::REPOSITORY_REPOMD)
    for d in di
      d.setpos_parent()
      filename = d.pool.lookup_str(Solv::SOLVID_POS, Solv::REPOSITORY_REPOMD_LOCATION)
      next unless filename
      checksum = d.pool.lookup_checksum(Solv::SOLVID_POS, Solv::REPOSITORY_REPOMD_CHECKSUM)
      if !checksum
	puts "no #{filename} checksum!"
	return nil, nil
      end
      return filename, checksum
    end
    return nil, nil
  end

  def load_if_changed
    print "rpmmd repo '#{@name}: "
    f = download("repodata/repomd.xml", false, nil, nil)
    if !f
      puts "no repomd.xml file, skipped"
      @handle.free(true)
      @handle = nil
      return false
    end
    @cookie = calc_cookie_fp(f)
    if usecachedrepo(nil, true)
      puts "cached"
      Solv.xfclose(f)
      return true
    end
    @handle.add_repomdxml(f, 0)
    Solv::xfclose(f)
    puts "fetching"
    filename, filechksum = find('primary')
    if filename
      f = download(filename, true, filechksum, true)
      if f
        @handle.add_rpmmd(f, nil, 0)
        Solv::xfclose(f)
      end
      return false if @incomplete
    end
    filename, filechksum = find('updateinfo')
    if filename
      f = download(filename, true, filechksum, true)
      if f
        @handle.add_updateinfoxml(f, 0)
        Solv::xfclose(f)
      end
    end
    add_exts()
    writecachedrepo(nil) unless @incomplete
    @handle.create_stubs()
    return true
  end

  def add_ext(repodata, what, ext)
    filename, filechksum = find(what)
    filename, filechksum = find('prestodelta') if !filename && what == 'deltainfo'
    return unless filename
    h = repodata.new_handle()
    repodata.set_poolstr(h, Solv::REPOSITORY_REPOMD_TYPE, what)
    repodata.set_str(h, Solv::REPOSITORY_REPOMD_LOCATION, filename)
    repodata.set_checksum(h, Solv::REPOSITORY_REPOMD_CHECKSUM, filechksum)
    if ext == 'DL'
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOSITORY_DELTAINFO)
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_FLEXARRAY)
    elsif ext == 'FL'
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_FILELIST)
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRSTRARRAY)
    end
    repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h)
  end

  def add_exts
    repodata = @handle.add_repodata(0)
    add_ext(repodata, 'deltainfo', 'DL')
    add_ext(repodata, 'filelists', 'FL')
    repodata.internalize()
  end

  def load_ext(repodata)
    repomdtype = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE)
    if repomdtype == 'filelists'
      ext = 'FL'
    elsif repomdtype == 'deltainfo'
      ext = 'DL'
    else
      return false
    end
    print "[#{@name}:#{ext}: "
    STDOUT.flush
    if usecachedrepo(ext)
      puts "cached]\n"
      return true
    end
    puts "fetching]\n"
    filename = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_LOCATION)
    filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_CHECKSUM)
    f = download(filename, true, filechksum)
    return false unless f
    if ext == 'FL'
      @handle.add_rpmmd(f, 'FL', Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES)
    elsif ext == 'DL'
      @handle.add_deltainfoxml(f, Solv::Repo::REPO_USE_LOADING)
    end
    Solv::xfclose(f)
    writecachedrepo(ext, repodata)
    return true
  end

end

class Repo_susetags < Repo_generic

  def find(what)
    di = @handle.Dataiterator(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME, what, Solv::Dataiterator::SEARCH_STRING)
    di.prepend_keyname(Solv::SUSETAGS_FILE)
    for d in di
      d.setpos_parent()
      checksum = d.pool.lookup_checksum(Solv::SOLVID_POS, Solv::SUSETAGS_FILE_CHECKSUM)
      return what, checksum
    end
    return nil, nil
  end

  def load_if_changed
    print "susetags repo '#{@name}: "
    f = download("content", false, nil, nil)
    if !f
      puts "no content file, skipped"
      @handle.free(true)
      @handle = nil
      return false
    end
    @cookie = calc_cookie_fp(f)
    if usecachedrepo(nil, true)
      puts "cached"
      Solv.xfclose(f)
      return true
    end
    @handle.add_content(f, 0)
    Solv::xfclose(f)
    puts "fetching"
    defvendorid = @handle.lookup_id(Solv::SOLVID_META, Solv::SUSETAGS_DEFAULTVENDOR)
    descrdir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DESCRDIR)
    descrdir = "suse/setup/descr" unless descrdir
    (filename, filechksum) = find('packages.gz')
    (filename, filechksum) = find('packages') unless filename
    if filename
      f = download("#{descrdir}/#{filename}", true, filechksum, true)
      if f
        @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::SUSETAGS_RECORD_SHARES)
        Solv::xfclose(f)
	(filename, filechksum) = find('packages.en.gz')
	(filename, filechksum) = find('packages.en') unless filename
        if filename
          f = download("#{descrdir}/#{filename}", true, filechksum, true)
          if f
            @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::REPO_REUSE_REPODATA|Solv::Repo::REPO_EXTEND_SOLVABLES)
            Solv::xfclose(f)
          end
        end
	@handle.internalize()
      end
    end
    add_exts()
    writecachedrepo(nil) unless @incomplete
    @handle.create_stubs()
    return true
  end

  @@langtags = {
    Solv::SOLVABLE_SUMMARY     => Solv::REPOKEY_TYPE_STR,
    Solv::SOLVABLE_DESCRIPTION => Solv::REPOKEY_TYPE_STR,
    Solv::SOLVABLE_EULA        => Solv::REPOKEY_TYPE_STR,
    Solv::SOLVABLE_MESSAGEINS  => Solv::REPOKEY_TYPE_STR,
    Solv::SOLVABLE_MESSAGEDEL  => Solv::REPOKEY_TYPE_STR,
    Solv::SOLVABLE_CATEGORY    => Solv::REPOKEY_TYPE_ID,
  }

  def add_ext(repodata, what, ext)
    (filename, filechksum) = find(what)
    h = repodata.new_handle()
    repodata.set_str(h, Solv::SUSETAGS_FILE_NAME, filename)
    repodata.set_checksum(h, Solv::SUSETAGS_FILE_CHECKSUM, filechksum)
    if ext == 'DL'
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOSITORY_DELTAINFO)
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_FLEXARRAY)
    elsif ext == 'DU'
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_DISKUSAGE)
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRNUMNUMARRAY)
    elsif ext == 'FL'
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_FILELIST)
      repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRSTRARRAY)
    else
      @@langtags.sort.each do |langid, langtype|
        repodata.add_idarray(h, Solv::REPOSITORY_KEYS, @handle.pool.id2langid(langid, ext, true))
        repodata.add_idarray(h, Solv::REPOSITORY_KEYS, langtype)
      end
    end
    repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h)
  end

  def add_exts
    repodata = @handle.add_repodata(0)
    di = @handle.Dataiterator(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME, nil, 0)
    di.prepend_keyname(Solv::SUSETAGS_FILE)
    for d in di
      filename = d.str
      next unless filename && filename =~ /^packages\.(..)(?:\..*)$/
      next if $1 == 'en' || $1 == 'gz'
      add_ext(repodata, filename, $1)
    end
    repodata.internalize()
  end

  def load_ext(repodata)
    filename = repodata.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME)
    ext = filename[9,2]
    print "[#{@name}:#{ext}: "
    STDOUT.flush
    if usecachedrepo(ext)
      puts "cached]\n"
      return true
    end
    puts "fetching]\n"
    defvendorid = @handle.lookup_id(Solv::SOLVID_META, Solv::SUSETAGS_DEFAULTVENDOR)
    descrdir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DESCRDIR)
    descrdir = "suse/setup/descr" unless descrdir
    filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::SUSETAGS_FILE_CHECKSUM)
    f = download("#{descrdir}/#{filename}", true, filechksum)
    return false unless f
    @handle.add_susetags(f, defvendorid, ext, Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES)
    Solv::xfclose(f)
    writecachedrepo(ext, repodata)
    return true
  end

  def download_location(location, chksum)
    datadir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DATADIR)
    datadir = "suse" unless datadir
    super("#{datadir}/#{location}", chksum)
  end

end

class Repo_unknown < Repo_generic
  def load(pool)
    puts "unsupported repo '#{@name}: skipped"
    return false
  end
end

class Repo_system < Repo_generic
  def load(pool)
    @handle = pool.add_repo(@name)
    @handle.appdata = self
    pool.installed = @handle
    print "rpm database: "
    @cookie = calc_cookie_file("/var/lib/rpm/Packages")
    if usecachedrepo(nil)
      puts "cached"
      return true
    end
    puts "reading"
    @handle.add_products("/etc/products.d", Solv::Repo::REPO_NO_INTERNALIZE)
    @handle.add_rpmdb(nil, Solv::Repo::REPO_REUSE_REPODATA)
    writecachedrepo(nil)
    return true
  end
end


def validarch?(pool, arch)
  return false unless arch && arch != ''
  id = pool.str2id(arch, false)
  return id != 0 && pool.isknownarch?(id)
end

def depglob(pool, name, globname, globdep)
  id = pool.str2id(name, false)
  if id != 0
    match = false
    providers = pool.whatprovides(id)
    if globname && providers.find {|s| s.nameid == id }
      return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_NAME, id) ]
    end
    if !providers.empty?
      puts "[using capability match for '#{name}']" if globname && globdep
      return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_PROVIDES, id) ]
    end
  end
  return [] unless name =~ /[\[*?]/
  if globname
    idmatches = {}
    for d in pool.Dataiterator(0, Solv::SOLVABLE_NAME, name, Solv::Dataiterator::SEARCH_GLOB)
      s = d.solvable
      idmatches[s.nameid] = 1 if s.installable?
    end
    if !idmatches.empty?
      return idmatches.keys.sort.collect { |id| pool.Job(Solv::Job::SOLVER_SOLVABLE_NAME, id) }
    end
  end
  if globdep
    idmatches = pool.matchprovidingids(name, Solv::Dataiterator::SEARCH_GLOB)
    if !idmatches.empty?
      puts "[using capability match for '#{name}']"
      return idmatches.sort.collect { |id| pool.Job(Solv::Job::SOLVER_SOLVABLE_PROVIDES, id) }
    end
  end
  return []
end

def limitjobs(pool, jobs, flags, evrstr)
  njobs = []
  evr = pool.str2id(evrstr)
  for j in jobs
    how = j.how
    sel = how & Solv::Job::SOLVER_SELECTMASK
    what = pool.rel2id(j.what, evr, flags)
    if flags == Solv::REL_ARCH
      how |= Solv::Job::SOLVER_SETARCH
    elsif flags == Solv::REL_EQ && sel == Solv::Job::SOLVER_SOLVABLE_NAME
      how |= evrstr.include?(?-) ? Solv::Job::SOLVER_SETEVR : Solv::Job::SOLVER_SETEV
    end
    njobs << pool.Job(how, what)
  end
  return njobs
end

def limitjobs_evrarch(pool, jobs, flags, evrstr)
  if evrstr =~ /^(.+)\.(.+?)$/ && validarch?(pool, $2)
    evrstr = $1
    jobs = limitjobs(pool, jobs, Solv::REL_ARCH, $2)
  end
  return limitjobs(pool, jobs, flags, evrstr)
end

def mkjobs_rel(pool, cmd, name, rel, evr)
  flags = 0
  flags |= Solv::REL_LT if rel.include?(?<)
  flags |= Solv::REL_EQ if rel.include?(?=)
  flags |= Solv::REL_GT if rel.include?(?>)
  jobs = depglob(pool, name, true, true)
  return limitjobs(pool, jobs, flags, evr) unless jobs.empty?
  if (name =~ /^(.+)\.(.+?)$/) && validarch?(pool, $2)
    arch = $2
    jobs = depglob(pool, name, true, true)
    return [] if jobs.empty?
    jobs = limitjobs(pool, jobs, Solv::REL_ARCH, arch)
    return limitjobs(pool, jobs, flags, evr)
  end
  return []
end

def mkjobs_nevra(pool, cmd, arg)
  jobs = depglob(pool, arg, true, true)
  return jobs unless jobs.empty?
  if ((arg =~ /^(.+)\.(.+?)$/) && validarch?(pool, $2))
    arch = $2
    jobs = depglob(pool, $1, true, true)
    return limitjobs(pool, jobs, Solv::REL_ARCH, arch) unless jobs.empty?
  end
  if (arg =~ /^(.+)-(.+?)$/)
    evr = $2
    jobs = depglob(pool, $1, true, false)
    return limitjobs_evrarch(pool, jobs, Solv::REL_EQ, evr) unless jobs.empty?
  end
  if (arg =~ /^(.+)-(.+?-.+?)$/)
    evr = $2
    jobs = depglob(pool, $1, true, false)
    return limitjobs_evrarch(pool, jobs, Solv::REL_EQ, evr) unless jobs.empty?
  end
  return []
end

def mkjobs_filelist(pool, cmd, arg)
  type = Solv::Dataiterator::SEARCH_STRING
  type = Solv::Dataiterator::SEARCH_GLOB if arg =~ /[\[*?]/
  if cmd == 'erase'
    di = pool.installed.Dataiterator(0, Solv::SOLVABLE_FILELIST, arg, type | Solv::Dataiterator::SEARCH_FILES|Solv::Dataiterator::SEARCH_COMPLETE_FILELIST)
  else
    di = pool.Dataiterator(0, Solv::SOLVABLE_FILELIST, arg, type | Solv::Dataiterator::SEARCH_FILES|Solv::Dataiterator::SEARCH_COMPLETE_FILELIST)
  end
  matches = []
  for d in di
    s = d.solvable
    next unless s && s.installable?
    matches.push(s.id)
    di.skip_solvable()
  end
  return [] if matches.empty?
  puts "[using file list match for '#{arg}']"
  if matches.length > 1
    return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_ONE_OF, pool.towhatprovides(matches)) ]
  else
    return [ pool.Job(Solv::Job::SOLVER_SOLVABLE | Solv::Job::SOLVER_NOAUTOSET, matches[0]) ]
  end
end

def mkjobs(pool, cmd, arg)
  if arg =~ /^\//
    jobs = mkjobs_filelist(pool, cmd, arg)
    return jobs unless jobs.empty?
  end
  if (arg =~ /^(.+?)\s*([<=>]+)\s*(.+?)$/)
    return mkjobs_rel(pool, cmd, $1, $2, $3)
  else
    return mkjobs_nevra(pool, cmd, arg)
  end
end

args = ARGV
cmd = args.shift
cmd = 'list' if cmd == 'li'
cmd = 'install' if cmd == 'in'
cmd = 'erase' if cmd == 'rm'
cmd = 'verify' if cmd == 've'
cmd = 'search' if cmd == 'se'

repos = []
for reposdir in [ '/etc/zypp/repos.d' ] do
  next unless FileTest.directory?(reposdir)
  for reponame in Dir["#{reposdir}/*.repo"].sort do
    cfg = IniFile.new(reponame)
    cfg.each_section do |ali|
      repoattr = { 'alias' => ali, 'enabled' => 0, 'priority' => 99, 'autorefresh' => 1, 'type' => 'rpm-md', 'metadata_expire' => 900}
      repoattr.update(cfg[ali])
      if repoattr['type'] == 'rpm-md'
        repo = Repo_rpmmd.new(ali, 'repomd', repoattr)
      elsif repoattr['type'] == 'yast2'
        repo = Repo_susetags.new(ali, 'susetags', repoattr)
      else
        repo = Repo_unknown.new(ali, 'unknown', repoattr)
      end
      repos.push(repo)
    end
  end
end

pool = Solv::Pool.new()
# require 'sys/uname' ; sysarch = Sys::Uname.machine
sysarch = `uname -p`.strip
pool.setarch(sysarch)

pool.set_loadcallback { |repodata|
  repo = repodata.repo.appdata
  repo ? repo.load_ext(repodata) : false
}

sysrepo = Repo_system.new('@System', 'system')
sysrepo.load(pool)
for repo in repos
  repo.load(pool) if repo.enabled?
end

if cmd == 'search'
  matches = {}
  for di in pool.Dataiterator(0, Solv::SOLVABLE_NAME, args[0], Solv::Dataiterator::SEARCH_SUBSTRING | Solv::Dataiterator::SEARCH_NOCASE)
    matches[di.solvid] = true
  end
  for solvid in matches.keys.sort
    s = pool.solvables[solvid]
    puts "- #{s.str} [#{s.repo.name}]"
  end
  exit
end

addedprovides = pool.addfileprovides_ids()
if !addedprovides.empty?
  sysrepo.updateaddedprovides(addedprovides)
  for repo in repos
    repo.updateaddedprovides(addedprovides)
  end
end
pool.createwhatprovides()

jobs = []
for arg in args
  njobs = mkjobs(pool, cmd, ARGV[0])
  abort("nothing matches '#{arg}'") if njobs.empty?
  jobs += njobs
end

if cmd == 'list' || cmd == 'info'
  abort("no package matched.") if jobs.empty?
    for job in jobs
      for s in job.solvables()
        if cmd == 'info'
	  puts "Name:        #{s.str}"
          puts "Repo:        #{s.repo.name}"
          puts "Summary:     #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}"
          str = s.lookup_str(Solv::SOLVABLE_URL)
          puts "Url:         #{str}" if str
          str = s.lookup_str(Solv::SOLVABLE_LICENSE)
          puts "License:     #{str}" if str
          puts "Description:\n#{s.lookup_str(Solv::SOLVABLE_DESCRIPTION)}"
          puts
        else
	  puts "  - #{s.str} [#{s.repo.name}]"
	  puts "    #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}"
        end
      end
    end
  exit
end

if cmd == 'install' || cmd == 'erase' || cmd == 'up' || cmd == 'dup' || cmd == 'verify'
  if jobs.empty?
    if cmd == 'up' || cmd == 'verify'
      jobs = [ pool.Job(Solv::Job::SOLVER_SOLVABLE_ALL, 0) ]
    elsif cmd != 'dup'
      abort("no package matched.")
    end
  end
  for job in jobs
    if cmd == 'up'
      if job.how == Solv::Job::SOLVER_SOLVABLE_ALL || job.solvables.any? {|s| s.isinstalled?}
        job.how |= Solv::Job::SOLVER_UPDATE
      else
        job.how |= Solv::Job::SOLVER_INSTALL
      end
    elsif cmd == 'install'
      job.how |= Solv::Job::SOLVER_INSTALL
    elsif cmd == 'erase'
      job.how |= Solv::Job::SOLVER_ERASE
    elsif cmd == 'dup'
      job.how |= Solv::Job::SOLVER_DISTUPGRADE
    elsif cmd == 'verify'
      job.how |= Solv::Job::SOLVER_VERIFY
    end
  end

  solver = nil
  #pool.set_debuglevel(1)
  while true
    solver = pool.Solver
    solver.ignorealreadyrecommended = true
    solver.allowuninstall = true if cmd == 'erase'
    if cmd == 'dup' && jobs.empty?
      solver.distupgrade = true
      solver.updatesystem = true
      solver.allowdowngrade = true
      solver.allowvendorchange = true
      solver.allowarchchange = true
      solver.dosplitprovides = true
    elsif cmd == 'up' && jobs.length == 1 && jobs[0].how == (Solv::Job::SOLVER_UPDATE | Solv::Job::SOLVER_SOLVABLE_ALL)
      solver.dosplitprovides = true
    end
    problems = solver.solve(jobs)
    break if problems.empty?
    for problem in problems
      puts "Problem #{problem.id}:"
      puts problem.findproblemrule.info.problemstr
      solutions = problem.solutions
      for solution in solutions
	puts "  Solution #{solution.id}:"
	elements = solution.elements(true)
	for element in elements
	  puts "  - #{element.str}"
	end
	puts
      end
      sol = nil
      while true
        print "Please choose a solution: "
        STDOUT.flush
        sol = STDIN.gets.strip
        break if sol == 's' || sol == 'q'
        break if sol =~ /^\d+$/ && sol.to_i >= 1 && sol.to_i <= solutions.length
      end
      next if sol == 's'
      abort if sol == 'q'
      solution = solutions[sol.to_i - 1]
      for element in solution.elements
        if element.type == Solv::Solver::SOLVER_SOLUTION_JOB
          jobs[element.jobidx] = pool.Job(Solv::Job::SOLVER_NOOP, 0)
	else
          newjob = element.Job()
          jobs.push(newjob) if newjob && !jobs.find {|j| j.how == newjob.how && j.what == newjob.what}
	end
      end
    end
  end
  trans = solver.transaction
  solver = nil
  if trans.isempty?
    puts "Nothing to do."
    exit
  end
  puts "\nTransaction summary:\n"
  for cl in trans.classify()
    if cl.type == Solv::Transaction::SOLVER_TRANSACTION_ERASE
      puts "#{cl.count} erased packages:"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_INSTALL
      puts "#{cl.count} installed packages:"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_REINSTALLED
      puts "#{cl.count} reinstalled packages:"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED
      puts "#{cl.count} downgraded packages:"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_CHANGED
      puts "#{cl.count} changed packages:"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED
      puts "#{cl.count} upgraded packages:"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_VENDORCHANGE
      puts "#{cl.count} vendor changes from '#{pool.id2str(cl.fromid)}' to '#{pool.id2str(cl.toid)}':"
    elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_ARCHCHANGE
      puts "#{cl.count} arch changes from '#{pool.id2str(cl.fromid)}' to '#{pool.id2str(cl.toid)}':"
    else
      next
    end
    for p in cl.solvables
      if cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED || cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED
	puts "  - #{p.str} -> #{trans.othersolvable(p).str}"
      else
	puts "  - #{p.str}"
      end
    end
    puts
  end
  puts "install size change: #{trans.calc_installsizechange()} K\n\n"
  while true:
    print("OK to continue (y/n)? ")
    STDOUT.flush
    yn = STDIN.gets.strip
    break if yn == 'y'
    abort if yn == 'n'
  end
  newpkgs = trans.newpackages()
  newpkgsfp = {}
  if !newpkgs.empty?
    downloadsize = 0
    for p in newpkgs
      downloadsize += p.lookup_num(Solv::SOLVABLE_DOWNLOADSIZE)
    end
    puts "Downloading #{newpkgs.length} packages, #{downloadsize} K"
    for p in newpkgs
      repo = p.repo.appdata
      location, medianr = p.lookup_location()
      next unless location
      chksum = p.lookup_checksum(Solv::SOLVABLE_CHECKSUM)
      f = repo.download_location(location, chksum)
      newpkgsfp[p.id] = f
      print "."
      STDOUT.flush()
    end
    puts
  end
  puts "Committing transaction:"
  puts
  trans.order(0)
  for p in trans.steps
    steptype = trans.steptype(p, Solv::Transaction::SOLVER_TRANSACTION_RPM_ONLY)
    if steptype == Solv::Transaction::SOLVER_TRANSACTION_ERASE
      puts "erase #{p.str}"
      next unless p.lookup_num(Solv::RPM_RPMDBID)
      evr = p.evr.sub(/^[0-9]+:/, '')
      system('rpm', '-e', '--nodeps', '--nodigest', '--nosignature', "#{p.name}-#{evr}.#{p.arch}") || abort("rpm failed: #{$? >> 8}") 
    elsif (steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL || steptype == Solv::Transaction::SOLVER_TRANSACTION_MULTIINSTALL)
      puts "install #{p.str}"
      f = newpkgsfp.delete(p.id)
      next unless f
      mode = steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL ? '-U' : '-i'
      system('rpm', mode, '--force', '--nodeps', '--nodigest', '--nosignature', "/dev/fd/#{Solv::xfileno(f).to_s}") || abort("rpm failed: #{$? >> 8}")
      solv::xfclose(f)
    end
  end
end
