#!/usr/bin/env ruby
# -*- ruby -*-
# vim: set sts=2 sw=2 ts=8 et:
#
# Copyright (c) 2000-2004 Akinori MUSHA
# Copyright (c) 2005,2006 KOMATSU Shinichiro
# Copyright (c) 2006-2008 Sergey Matveychuk <sem@FreeBSD.org>
# Copyright (c) 2009-2012 Stanislav Sedov <stas@FreeBSD.org>
# Copyright (c) 2012 Bryan Drewery <bdrewery@FreeBSD.org>
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#

MYNAME = File.basename($0)

require "optparse"
require "pkgtools"

REASON_COMMENT = {
  :badcpp => "bad C++ code",
  :bison => "bison error",
  :categories => "invalid category",
  :cc => "compiler error",
  :checksum => "checksum mismatch",
  :chown => "chown error",
  :configure => "configure error",
  :coredump => "coredump",
  :dependobj => "depend object",
#  :dependpkg => "depend package",
#  :diskfull => "disk full",
  :display => "X DISPLAY error",
  :distinfo => "distinfo incorrect",
  :elf => "ELF",
#  :extra => 'extra files',
#  :fetch_timeout => "fetch timeout",
  :fetch => "fetch error",
  :gcc_bug => "gcc bug",
  :header => "missing header",
  :install => "install error",
  :interrupt => "interrupted by user",
  :ld => "linker error",
  :libdepends => "dependent libraries",
  :malloc_h => "reference to malloc.h",
  :manpage => "manpage error",
  :motif => "Motif error",
  :motiflib => "Motif libraries error",
  :newgcc => "new compiler error",
#  :nfs => "NFS error",
  :patch => "patch error",
  :perm => "permission denied",
  :perl => "perl missing",
  :perl5 => "Perl5 error (h2ph)",
  :plist => "package error",
#  :runaway => "runaway process",
  :segfault => "segmentation fault",
  :soundcard_h => "reference to soundcard.h",
  :stdio => "stdio compatibility",
  :struct => "struct changes",
  :texinfo => "texinfo error",
  :union => "union wait error",
  :unknown => "unknown build error",
  :usexlib => "X libraries missing",
#  :wrkdir => "WRKDIR error",
  :values_h => "reference to values.h",
  :vulnerabilities => "security vulnerabilities",
  :xfree4man => "X manpage error",
}

class OriginMissingError < StandardError
  def message
    "missing origin"
  end
end
class PortDirError < StandardError
  def message
    "port directory error"
  end
end
class PortDeletedError < StandardError
  def message
    "port deleted"
  end
end
class ConfigError < StandardError
  def message
    "make config failed"
  end
end
class MakefileBrokenError < StandardError
  def message
    "Makefile broken"
  end
end
class IgnoreMarkError < StandardError
  def message
    "marked as IGNORE"
  end
end
class InvalidPkgNameError < StandardError
  def message
    "invalid package name"
  end
end
class BackupError < StandardError
  def message
    "backup error"
  end
end
class UninstallError < StandardError
#  def message
#    "uninstall error"
#  end
end
class FetchError < StandardError
  def message
    "fetch error"
  end
end
class BuildError < StandardError
#  def message
#    "build error"
#  end
end
class InstallError < StandardError
#  def message
#    "install error"
#  end
end
class PkgNotFoundError < StandardError
  def message
    "package not found"
  end
end
class RecursiveDependencyError < StandardError
  def message
    "recursive dependency"
  end
end

begin
  $initial_pwd = Dir.pwd

  if $initial_pwd.empty?
    raise Errno::ENOENT, 'No such file or directory'
  end
rescue => e
  raise e if e.class == PkgDB::NeedsPkgNGSupport
  # XXX: the .sub(/ - .*/, '') part should be removed later
  STDERR.puts "Cannot locate current working directory: #{e.message.sub(/ - .*/, '')}"
  exit 1
end

COLUMNSIZE = 24
NEXTLINE = "\n%*s" % [5 + COLUMNSIZE, '']

def init_global
  $afterinstall = ''
  $all = false
  $backup_packages = false
  $batch_mode = false
  $beforebuild = ''
  $clean = true
  $cleanup = true
  $config = false
  $force_config = false
  $current_pkgname = ''
  $depends = Hash.new
  $distclean = 0
  $emit_summaries = false
  $exclude_packages = []
  $fetch_only = false
  $fetch_recursive = false
  $force = false
  $keep_going = false
  $ignore_moved = false
  $interactive = false
  $logfilename_format = nil
  $make_args = ""
  $make_env = []
  $new = MYNAME == 'portinstall'
  $noexecute = false
  $noconfig = false
  $origin = nil
  $package = false
  $pkg_cache = {}
  $pkgdb_update = false
  $quiet = false
  $recursive = false
  $resultsfile = nil
  $sanity_check = true
  $tempdir = ""
  $uninstall_extra_flags = 'P'
  $upward_recursive = false
  $use_packages = false
  $use_packages_only = false
  $without_env_upgrade = false
  $yestoall = false
end

def main(argv)
  usage = <<-"EOF"
usage: #{MYNAME} [-habcCDDefFiklnOpPPqrRsuvwWy] [-A command] [-B command]
        [-L format] [-S command] [-x pkgname_glob]
        [[-o origin] [-m make_args] [-M make_env] pkgname_glob ...]
  EOF

  banner = <<-"EOF"
#{MYNAME} #{Version} (#{PkgTools::DATE})

#{usage}
  EOF

  dry_parse = true
  $results = PkgResultSet.new

  OptionParser.new(banner, COLUMNSIZE) do |opts|
    opts.def_option("-h", "--help",
		    "Show this message") {
      print opts
      exit 0
    }

    opts.def_option("-a", "--all",
		    "Do with all the installed packages") { |v|
      $all = v
      $recursive = false
      $upward_recursive = false
    }

    opts.def_option("-A", "--afterinstall=CMD",
		    "Run the command after each installation") { |v|
      $afterinstall = v
      $afterinstall.strip!
    }

    opts.def_option("-b", "--backup-packages",
		    "Keep backup packages of the old versions") { |v|
      $backup_packages = v
    }

    opts.def_option("--batch",
		    "Run an upgrading process in a batch mode" << NEXTLINE <<
    		    "(with BATCH=yes)") { |v|
      $batch_mode = v
    }

    opts.def_option("-B", "--beforebuild=CMD",
		    "Run the command before each build; If the command" << NEXTLINE <<
		    "exits in failure, then the port will be skipped") { |v|
      $beforebuild = v
      $beforebuild.strip!
    }

    opts.def_option("-c", "--config",
		    "Run \"make config-conditional\" before everything for all tasks") { |v|
      $config = v
    }

    opts.def_option("-C", "--force-config",
		    "Run \"make config\" before everything for all tasks") { |v|
      $force_config = v
      $config = true
    }

    opts.def_option("-D", "--distclean",
		    "Delete failed distfiles and retry if checksum fails" << NEXTLINE <<
		    "Specified twice, do \"make distclean\" before each" << NEXTLINE <<
		    "fetch or build") {
      $distclean += 1
    }

    opts.def_option("-e", "--emit-summaries",
		    "Emit summary info after each port processing" << NEXTLINE ) { |v|
      $emit_summaries = v
    }

    opts.def_option("-f", "--force",
		    "Force the upgrade of a port even if it is to be a" << NEXTLINE <<
		    "downgrade or just a reinstall, or the port is held") { |v|
      $force = v
    }

    opts.def_option("-F", "--fetch-only",
		    "Only fetch distfiles or packages (if -P is given);" << NEXTLINE <<
		    "Do not build or install anything") { |v|
      $fetch_only = v
    }

    opts.def_option("--ignore-moved",
		    "Ignore MOVED file") { |v|
      $ignore_moved = v
    }

    opts.def_option("-i", "--interactive",
		    "Turn on interactive mode") { |v|
      $interactive = v
      $verbose = true
    }

    opts.def_option("-k", "--keep-going",
		    "Force the upgrade of a port even if some of the" << NEXTLINE <<
		    "requisite ports have failed to upgrade") { |v|
      $keep_going = v
    }

    opts.def_option("-l", "--results-file=FILE",
		    "Specify a file name to save the results to" << NEXTLINE <<
		    "(default: do not save results)") { |resultsfile|
      $resultsfile = File.expand_path(resultsfile)
    }

    opts.def_option("-L", "--log-file=FORMAT",
		    "Specify a printf(3) style format to determine the" << NEXTLINE <<
		    "log file name for each port; '%s::%s' is appended" << NEXTLINE <<
		    "if it does not contain a %; category and portname" << NEXTLINE <<
		    "are given as arguments (default: do not save logs)") { |fmt|
      fmt.include?(?%) or fmt << '%s::%s'

      $logfilename_format = File.expand_path(fmt)
    }

    opts.def_option("-m", "--make-args=ARGS",
		    "Specify arguments to append to each make(1)" << NEXTLINE <<
		    "command line") { |v|
      $make_args = v
    } 

    opts.def_option("-M", "--make-env=ARGS",
		    "Specify arguments to prepend to each make(1)" << NEXTLINE <<
		    "command line") { |make_env|
      $make_env = shellwords(make_env) unless make_env.empty?

      if $make_env[0].include?('=')
	$make_env.unshift('env')
      end
    }

    opts.def_option("-n", "--noexecute",
		    "Do not upgrade any ports, but just show what would" << NEXTLINE <<
		    "be done") { |v|
      $noexecute = v
      $verbose = true
      $interactive = true
      $yestoall = false
    }

    opts.def_option("-N", "--new",
		    "Install a new one when a specified package is" << NEXTLINE <<
		    "not installed, after upgrading all the dependent" << NEXTLINE <<
		    "packages (default: #{MYNAME == 'portinstall' ? 'on' : 'off'})") { |v|
      $new = v
    }

    opts.def_option("-o", "--origin=ORIGIN",
		    "Specify a port to upgrade the following pkg with") { |origin|
      $origin = $portsdb.strip(origin) || origin
    }

    opts.def_option("-O", "--omit-check",
		    "Omit sanity checks for dependencies.") {
      $sanity_check = false
      $uninstall_extra_flags << 'O'
    }

    opts.def_option("-p", "--package",
		    "Build package when each port is installed") { |v|
      $package = v
    }

    opts.def_option("-P", "--use-packages",
		    "Use packages instead of ports whenever available;" << NEXTLINE <<
		    "Specified twice, --use-packages-only is implied") {
      if $use_packages and !dry_parse
	$use_packages_only = true
      else
	$use_packages = true
      end
    }

    opts.def_option("--use-packages-only",
		    "Or -PP; Use no ports but packages only") { |v|
      $use_packages_only = v
      $use_packages = true
    }

    opts.def_option("-q", "--quiet",
		    "Be quiet when -N option specified and the package already installed") { |v|
      $quiet = v
    }

    opts.def_option("--noconfig",
		    "Do not read pkgtools.conf") { |v|
      $noconfig = v
    }

    opts.def_option("-r", "--recursive",
		    "Do with all those depending on the given packages" << NEXTLINE <<
		    "as well") {
      $recursive = true unless $all
    }

    opts.def_option("-R", "--upward-recursive",
		    "Do with all those required by the given packages" << NEXTLINE <<
		    "as well / Fetch recursively if -F is specified") {
      $upward_recursive = true unless $all
      $fetch_recursive = true
    }

    opts.def_option("-s", "--sudo",
		    "Run commands under sudo(8) where needed") { |v|
      $sudo = v
    }

    opts.def_option("-S", "--sudo-command=CMD",
		    "Specify an alternative to sudo(8)" << NEXTLINE <<
		    "e.g. 'su root -c \"%s\"' (default: sudo)") { |sudo_command|
      $sudo_args = shellwords(sudo_command)
    }

    opts.def_option("-u", "--uninstall-shlibs",
		    "Do not preserve old shared libraries") {
      $uninstall_extra_flags.sub!(/P/, '');
    }

    opts.def_option("-v", "--verbose",
		    "Be verbose") { |v|
      $verbose = v
    }

    opts.def_option("-w", "--noclean",
		    "Do not \"make clean\" before each build") { |noclean|
      $clean = false
    }

    opts.def_option("-W", "--nocleanup",
		    "Do not \"make clean\" after each installation") { |nocleanup|
      $cleanup = false
    }

    opts.def_option("--without-env-upgrade",
      		    "Do not set UPGRADE_* environment variables") { |v|
      $without_env_upgrade = v
    }

    opts.def_option("-x", "--exclude=GLOB",
		    "Exclude packages matching the specified glob" << NEXTLINE <<
		    "pattern") { |arg|
      begin
	pattern = parse_pattern(arg)
      rescue RegexpError => e
	warning_message e.message.capitalize
	break
      end

      $exclude_packages |= $pkgdb.glob(pattern, false) unless dry_parse
    }

    opts.def_option("-y", "--yes",
		    "Answer yes to all the questions") { |v|
      $yestoall = v
      $verbose = true
      $noexecute = false
    }

    opts.def_tail_option '
pkgname_glob is one of these: a full pkgname, a pkgname w/o version,
a shell glob pattern in which you can use wildcards *, ?, and [..],
an extended regular expression preceded by a colon (:), or a date range
specification preceded by either < or >.  See pkg_glob(1) for details.
The package list is automatically sorted in dependency order.

Environment Variables [default]:
    PACKAGES         packages directory [$PORTSDIR/packages]
    PKGTOOLS_CONF    configuration file [$PREFIX/etc/pkgtools.conf]
    PKG_BACKUP_DIR   directory for keeping old packages [$PKG_PATH]
    PKG_DBDIR        packages DB directory [/var/db/pkg]
    PKG_PATH         packages search path [$PACKAGES/All]
    PKG_TMPDIR       temporary directory for backup etc. [$TMPDIR]
    (Note: This must have enough free space when upgrading a big package)
    PORTSDIR         ports directory [/usr/ports]
    PORTS_DBDIR      ports db directory [$PORTSDIR]
    PORTS_INDEX      ports index file [$PORTSDIR/INDEX]
    PORTUPGRADE      default options (e.g. -v) [none]
    TMPDIR           temporary directory [/var/tmp]'

    upgrade_tasks = []
    install_tasks = []
    install_tasks_orphans = {}
    $package_tasks = []
    $dep_hash = {}
    $task_options = Hash.new({})

    result_proc = proc {
      if $pkgdb_update
	$pkgdb.close_db

	$pkgdb.autofix(true)
      end

      ret = $results.show($fetch_only ? 'fetched' : 'installed or upgraded')
      $results.save($resultsfile) if $resultsfile
      ret
    }

    $interrupt_proc = result_proc

    begin
      init_global
      init_pkgtools_global

      rest = opts.order(*argv)

      unless $noconfig
        init_global
	load_config
      else
	argv = rest
      end

      dry_parse = false

      opts.order!(argv)

      if envopt = config_value(:PORTUPGRADE_ARGS)
	progress_message "Reading default options: " + envopt if $verbose

	opts.parse(*shellwords(envopt))
      end

      if argv.empty? && !$all
	if o = guess_missing_origin
	  argv << o
	else
	  print opts, "\n"
	  warning_message "No package names given."
	  return 0
	end
      end

      # Change to a good directory to avoid errors if the user
      # is running in a WRKDIR
      Dir.chdir '/tmp'

      all = '*'
      argv << all

      timer_start("Session")

      opts.order(*argv) do |arg|
	first = nil

	if arg.equal? all
	  next unless $all

	  pattern = arg
	else
	  pattern = $pkgdb.strip(arg) || arg

	  begin
	    pattern = parse_pattern(pattern)
	  rescue RegexpError => e
	    warning_message e.message.capitalize
	    next
	  end
	end

	list = []

	found = false

	unless $new
	  catch(:pkg) {
	    begin
	      $pkgdb.glob(pattern, false).each do |pkgname|
		first ||= pkgname

		list |= $pkgdb.recurse(pkgname, $recursive, false, $sanity_check)
	      end
	    rescue => e
              raise e if e.class == PkgDB::NeedsPkgNGSupport
	      STDERR.puts e.message
	      exit 1
	    end

	    list -= ['']
	    list.each do |i|
	      if i == first
		$task_options[i][:origin] = $origin
	      end
	    end

	    upgrade_tasks |= list

	    found = true

	    # Check packages for updates and gather dependecies
	    depends = []
	    not_need_upgrade = []
	    upgrade_tasks.each do |task|
	      pkg = PkgInfo.new(task)
	      if task == first && $origin
		origin = $origin
	      else
		origin = $pkgdb.origin(task)
                # Check if this origin has been MOVED and automatically replace the origin
                if !$ignore_moved and \
                  !config_ignore_moved?(pkg) and \
                  (moved = $portsdb.moved.trace(pkg.origin)) and \
                  moved.last.to
                  origin = moved.last.to
                end
	      end
	      if !origin.nil?
		begin
		  name = get_pkgname(origin)
		rescue IgnoreMarkError => e
		  $results << PkgResult.new(origin, :ignored, e.message)
		  not_need_upgrade << task
		  next
		rescue PortDeletedError => e
		  $results << PkgResult.new(origin, :ignored, e.message)
		  not_need_upgrade << task
		  next
		rescue PortDirError => e
		  $results << PkgResult.new(origin, :ignored, e.message)
		  not_need_upgrade << task
		  next
		end
		if name == ''
		  warning_message "There are errors in a meta info for #{task}"
		  warning_message "Run 'pkgdb -F' to interactively fix them."
		  exit 1
		end
		if $upward_recursive || $config
		  dep = []
		  get_all_depends(origin).each do |d|
		    newdep = $pkgdb.deorigin(d)
		    unless newdep.nil? then
                      if newdep.length > 1
                        warning_message "Duplicated origin - #{d}: #{newdep.join(" ")}"
                        warning_message "Run 'pkgdb -F' to interactively fix them."
                        exit 1
                      end
		      dep << newdep.join
		    end
		  end
		  depends |= dep if $upward_recursive
		end
		name =~ /^(.+)-([^-]+)$/
		newversion = PkgVersion.new($2)
		if newversion <= pkg.version && !$force
		  not_need_upgrade << task
		  next
		end
		# XXX: See #30 and #62
		#install_tasks |= get_notinstalled_depends(origin)
	      end
	    end
	    upgrade_tasks -= not_need_upgrade

	    # Check dependencies for updates
	    depends -= ['']
	    if $upward_recursive
	      STDERR.print '[Exclude up-to-date packages '
	      not_need_upgrade = []
	      depends.each do |task|
		STDERR.print '.'
		next if task.nil? or task.empty?
		begin
		  pkg = PkgInfo.new(task)
	        rescue ArgumentError => e
		  $results << PkgResult.new(task, :ignored, e.message)
		  not_need_upgrade << task
		  next
		end
		origin = $pkgdb.origin(task)
		if !origin.nil?
		  begin
		    name = get_pkgname(origin)
		  rescue IgnoreMarkError => e
		    $results << PkgResult.new(origin, :ignored, e.message)
		    not_need_upgrade << task
		    next
		  rescue PortDeletedError => e
		    $results << PkgResult.new(origin, :ignored, e.message)
		    not_need_upgrade << task
		    next
		  rescue PortDirError => e
		    $results << PkgResult.new(origin, :ignored, e.message)
		    not_need_upgrade << task
		    next
		  end
		  if name == ''
		    warning_message "There are errors in a meta info for #{task}"
		    warning_message "Run 'pkgdb -F' to interactively fix them."
		    exit 1
		  end
		  name =~ /^(.+)-([^-]+)$/
		  newversion = PkgVersion.new($2)
		  if newversion <= pkg.version && !$force
		    not_need_upgrade << task
		    next
		  end
		end
	      end
	      STDERR.puts ' done]'
	      depends -= not_need_upgrade
	    end
	    upgrade_tasks += depends

	    if upgrade_tasks.empty?
	      throw :pkg
	    end

	    upgrade_tasks -= $exclude_packages

	    if upgrade_tasks.empty?
	      warning_message "No matching packages left after exclusion: #{arg}"
	      throw :pkg
	    end

	  }
	  unless found
	    warning_message "No such installed package: #{arg}"
	  end

	else		# User wants install, not upgdate

	  pattern = $portsdb.strip(arg) || arg	# allow pkgname_glob

	  begin
	    pattern = parse_pattern(pattern)
	  rescue RegexpError => e
	    warning_message e.message.capitalize
	    next
	  end

	  stty_sane

	  ports = $portsdb.glob(pattern).each.map { |i| i.origin }

	  unique = false

	  case ports.size
	  when 0
	    if $portsdb.exist?(arg)
	      # The specified port does not have an entry in the INDEX but
	      # the port directory actually exists.

	      unique = true

	      ports << arg
	    else
	      warning_message "No such package or port: #{arg}"
	      next
	    end
	  when 1
	    unique = true
	  else
	    progress_message "Found #{ports.size} ports matching '#{arg}':"
	    ports.each { |origin| puts "\t#{origin}" }
	  end

	  ports.each do |origin|
	    if pkgnames = $pkgdb.deorigin(origin)
	      if !$quiet
		warning_message "Found already installed package(s) of '#{origin}': " + pkgnames.join(' ')
	      end
	      next
	    else
	      interactive = $interactive || !unique
	      yes_by_default = true
	    end

	    if $noexecute
	      puts "Install '#{origin}'? [no]" if interactive
	    elsif $yestoall
	      puts "Install '#{origin}'? [yes]" if interactive
	    elsif interactive
	      prompt_yesno("Install '#{origin}'?", yes_by_default) or next	# "
	    end

	    install_tasks << origin
            install_tasks_orphans[origin] = false
	    # Track all not installed
            # XXX: See #30 and #62
	    #get_notinstalled_depends(origin).each do |dep|
	    #  install_tasks << dep
            #  install_tasks_orphans[dep] = true
	    #end
	  end
	end
      end	# Arguments parsing done

      if $package && !$fetch_only
	t = $pkgdb.tsort(upgrade_tasks)
	h = t.dump
	upgrade_tasks.each do |k|
	  $dep_hash[k] = h[k] & upgrade_tasks
	end
	upgrade_tasks = t.tsort! & upgrade_tasks
      else
	$pkgdb.sort_build!(upgrade_tasks)
      end

      $portsdb.sort!(install_tasks)
    rescue OptionParser::ParseError => e
      STDERR.puts "#{MYNAME}: #{e}", usage
      exit 64
    end

    ntasks = upgrade_tasks.size + install_tasks.size
    ctask = 0

    $current_pkgname = ''
    install_tasks.each do |origin| 
      $ignore_conflicts = false
      ctask += 1
      setproctitle('[%d/%d] %s', ctask, ntasks, origin)
      do_install(origin, install_tasks_orphans[origin])
      progress_message "** Install tasks #{install_tasks.length}: #{$results.summary}" if $emit_summaries || $verbose
    end

    upgrade_tasks.each do |pkgname|
      $ignore_conflicts = false
      ctask += 1
      setproctitle('[%d/%d] %s', ctask, ntasks, pkgname)
      $current_pkgname = pkgname
      do_upgrade(pkgname)
      progress_message "** Upgrade tasks #{upgrade_tasks.length}: #{$results.summary}" if $emit_summaries || $verbose
    end

    return result_proc.call
  end
ensure
  stty_sane unless $results.empty?

  timer_end("Session")
end

# Returns:
#      Set: all recursive depends list
def get_all_depends(origin, parents_list = nil)
  if $use_packages_only
    depends_vars = %w{LIB_DEPENDS RUN_DEPENDS}
  else
  depends_vars = %w{FETCH_DEPENDS EXTRACT_DEPENDS PATCH_DEPENDS
		      BUILD_DEPENDS LIB_DEPENDS RUN_DEPENDS}
  end

  unless $depends.has_key?(origin)
    depends = Set.new

    portdir = $portsdb.portdir(origin)
    return [] if not File.directory?(portdir)

    begin
      run_make_config(portdir, origin) if $config and !$use_packages_only
    rescue => e
      raise e if e.class == PkgDB::NeedsPkgNGSupport
      puts "#{e.message}. Ignored."
    end

    first = false
    if parents_list.nil?
      parents_list = Set.new
      STDERR.print "[Gathering depends for #{origin} "
      first = true
    elsif $verbose
      STDERR.print "(#{origin})"
    end
    raise RecursiveDependencyError if parents_list.include?(origin)
    parents_list.add(origin)

    make_env = get_make_env(origin)
    cmdargs = make_env << 'make'

    cmdargs.concat(get_make_args(origin))

    `cd #{portdir} && #{shelljoin(*cmdargs)} -V #{depends_vars.join(' -V ')}`.each_line do |line|
	line.split(/\s+/).each do |dep|
	  dep.sub!(/.*?:/,'')
	  if dep.rindex(':') != nil
	    (dep, target) = dep.split(':')
	    next if target != "install"
	  end
	  depends.add($portsdb.strip(dep)) if !dep.empty?
	end
    end

    STDERR.print '.'*depends.length if depends.length > 0

    children_deps = Set.new
    depends.each do |dep|
      children_deps.merge(get_all_depends(dep, parents_list)) if !dep.nil?
    end
    if !children_deps.nil?
      depends.merge(children_deps)
    end

    STDERR.puts ' done]' if first
    $depends[origin] = depends.to_a.compact
  else
    $depends[origin]
  end
end

def get_notinstalled_depends(origin)
  notinstalled = Array.new

  get_all_depends(origin).each do |dep|
    next if alt_dep('', dep)
    if !$pkgdb.deorigin(dep)
      notinstalled.push(dep)
    end
  end
  
  notinstalled
end

# raises:
#   ConfigError
def run_make_config(portdir, origin)
  make_args = get_make_args(origin)
  make_env = get_make_env(origin)

  cmdargs = make_env << 'make'

  cmdargs.concat(make_args)

  setproctitle('[make config] %s', origin)

  unless $verbose
    cmdargs << 'ECHO_MSG=/usr/bin/true'
  end

  if $force_config
    cmdargs << 'config'
  else
    cmdargs << 'config-conditional'
  end

  if ! system "cd #{portdir} && #{shelljoin(*cmdargs)}"
    warning_message "make config failed for #{origin}"
    raise ConfigError
  end
end

def do_upgrade(pkgname)
  pkg = PkgInfo.new(pkgname)

  origin = $task_options[pkgname][:origin]
  $ignore_conflicts = false

  if !origin || !File.directory?($portsdb.ports_dir() + '/' + origin)
    if !$ignore_moved and \
       !config_ignore_moved?(pkg) and \
       (moved = $portsdb.moved.trace(pkg.origin))
      if origin = moved.last.to
	if pkg.origin != origin
	  warning_message "Package origin of '#{pkg.name}' has been changed: '#{pkg.origin}' -> '#{origin}'"
	end
      else
	warning_message "Package '#{pkg.name}' has been removed from ports tree."
#	$results << PkgResult.new(origin, :ignored, pkgname)
	return
      end
    else
      origin = pkg.origin
    end
  end

  if origin
    if result = $results[origin]
      progress_message "Skipping '#{origin}' (#{pkgname}) because it has already #{result.phrase(true)}"

      $results << PkgResult.new(origin, :skipped, pkgname)
      return
    elsif !$keep_going
      deps = pkg.pkgdep || []

      deps.each do |dep|
	o = $pkgdb.origin(dep)	# perhaps nil

	result = $results[o]

	if result && result.failed?
	  progress_message "Skipping '#{origin}' (#{pkgname}) because a requisite package '#{dep}' (#{o}) failed (specify -k to force)"
	  $results << PkgResult.new(origin, :skipped, pkgname)
	  return
	end
      end
    end

    # Handle replaced/moved origins
    if pkg.origin != origin
      # Ignore conflicts with DISABLE_CONFLICTS
      $ignore_conflicts = true
    end
  end

  stty_sane

  upgraded = false

  use_packages, use_packages_only = $use_packages, $use_packages_only

  if (origin && config_use_packages_only?(origin)) || config_use_packages_only?(pkgname)
    $use_packages = $use_packages_only = true
  elsif (origin && config_use_packages?(origin)) || config_use_packages?(pkgname)
    $use_packages = true
  end

  begin
    result, newpkg = upgrade_pkg(pkg, origin)
    if result == :done
      upgraded = true

      if $package && !$fetch_only
	$dep_hash.each do |key, deps|
	  $package_tasks << key if deps.include?(pkgname)
	end
      end
    end

    result_msg = newpkg ? "#{pkgname} -> #{newpkg.fullname}" : pkgname


    $results << PkgResult.new(origin ? origin : pkg.fullname, result, result_msg)
  rescue IgnoreMarkError => e
    $results << PkgResult.new(origin ? origin : pkg.fullname, :ignored, pkgname)
  rescue => e
    raise e if e.class == PkgDB::NeedsPkgNGSupport
    $results << PkgResult.new(origin ? origin : pkg.fullname, e, pkgname)
  ensure
    $use_packages, $use_packages_only = use_packages, use_packages_only
  end

  if !upgraded && $package_tasks.include?(pkgname)
    $pkgdb.close_db

    progress_message "Fixing up dependencies before creating a package" if $verbose

    $pkgdb.autofix

    progress_message "Packaging '#{pkgname}' as dependency"

    if $noexecute
      puts "OK? [no]" if $interactive
      return
    elsif $yestoall
      puts "OK? [yes]" if $interactive
    elsif $interactive
      prompt_yesno('OK?', true) or return
    end

    if $pkgdb.with_pkgng?
      system!(PkgDB::command(:pkg), 'create', '-o', $packages_dir, '-f', $portsdb.pkg_sufx, pkgname)
    else
      system!(PkgDB::command(:pkg_create), '-vb', pkgname,
              File.join($packages_dir, pkgname + $portsdb.pkg_sufx))
    end
  end
end

def do_install(origin, orphan)
  if origin.nil? then
    warning_message "NIL origin passed to do_install"
    return
  end
  if result = $results[origin]
    progress_message "Skipping '#{origin}' because it has already #{result.phrase(true)}"

    $results << PkgResult.new(origin, :skipped)
    return
  else
    unless $keep_going
      make_args = get_make_args(origin)
      make_env = get_make_env(origin, true)

      $portsdb.all_depends_list!(origin, shelljoin(*make_env), shelljoin(*make_args)).each do |o|
	result = $results[o]

	if result && result.failed?
	  progress_message "Skipping '#{origin}' because a requisite port '#{o}' failed (specify -k to force)"
	  $results << PkgResult.new(origin, :skipped)
	  return
	end
      end
    end
  end

  stty_sane

  use_packages, use_packages_only = $use_packages, $use_packages_only

  if config_use_packages_only?(origin)
    $use_packages = $use_packages_only = true
  elsif config_use_packages?(origin)
    $use_packages = true
  end

  begin
    if install_new_port(origin, orphan, false)	# confirmed in advance
      $results << PkgResult.new(origin, :done)
    else
      $results << PkgResult.new(origin, :skipped)
    end
  rescue IgnoreMarkError => e
    $results << PkgResult.new(origin, :ignored)
  rescue => e
    raise e if e.class == PkgDB::NeedsPkgNGSupport
    $results << PkgResult.new(origin, e)
  ensure
    $use_packages, $use_packages_only = use_packages, use_packages_only
  end
  $pkgdb.autofix
end

def get_make_args(origin, pkgname = nil)
  args = []
  if conf_args = config_make_args(origin, pkgname)
    args = conf_args.split(' ')
    args.concat($make_args.split(' '))
  else
    args = $make_args.split(' ')
  end
  quoted = 0
  n = 0
  is_quoted = false
  while n < args.length
    if /\"/ =~ args[n]
      if is_quoted
        args[quoted] << " " << args[n]
        args[n] = nil
        is_quoted = false
      else
        quoted = n
        is_quoted = true
        n+=1
        next
      end
    end

    if is_quoted
      args[quoted] << " " << args[n]
      args[n] = nil
    end
    n+=1
  end
  args.compact
end

def get_make_env(origin, is_new = false)
  make_env = $make_env.dup
  if make_env.empty?
    make_env << 'env'
  end

  unless is_new or $without_env_upgrade
    make_env.concat(get_upgrade_env_vars($current_pkgname))
  end

  config_env = config_make_env(origin)
  if !config_env.empty?
    make_env.concat(config_env)
  end
  make_env
end

def get_upgrade_env_vars(pkgname)
  env_vars = Array.new

  unless pkgname.empty?
    env_vars << 'UPGRADE_TOOL=portupgrade'
    env_vars << 'UPGRADE_PORT='+pkgname
    pkgname =~ %r{^.*-(.*)$}
    env_vars << 'UPGRADE_PORT_VER='+$1
  end

  env_vars
end

def get_beforebuild_command(origin)
  commands = if $beforebuild.empty? then [] else [$beforebuild] end

  commands[commands.size, 0] = config_beforebuild(origin)	# maybe nil

  commands.uniq!
  commands.each { |cmd| cmd.sub!(/^[;\s]+/, '') if !cmd.nil? }
  commands.reject! { |cmd| cmd.nil? || cmd.empty? }

  if commands.empty?
    nil
  else
    commands.join('; ')
  end
end

def get_afterinstall_command(origin)
  commands = if $afterinstall.empty? then [] else [$afterinstall] end

  commands[0, 0] = config_afterinstall(origin)	# maybe nil

  commands.uniq!
  commands.each { |cmd| cmd.sub!(/^[;\s]+/, '') if !cmd.nil? }
  commands.reject! { |cmd| cmd.nil? || cmd.empty? }

  if commands.empty?
    nil
  else
    commands.join('; ')
  end
end

def process_old_package(pkgfile)
  pkg_backup_dir = ENV['PKG_BACKUP_DIR'] || $pkg_path

  if $backup_packages
    progress_message "Keeping old package in '#{pkg_backup_dir}'" if $verbose
    unless File.directory?(pkg_backup_dir)
      xsystem! '/bin/mkdir', '-p', pkg_backup_dir
    end
    xsystem! '/bin/mv', '-f', pkgfile, pkg_backup_dir
  else
    progress_message "Removing old package'" if $verbose
    xsystem! '/bin/rm', '-f', pkgfile
  end
end

# raises:
#   OriginMissingError, InvalidPkgNameError,
#   InstallError
#   (BuildError - build_port)
#   (StandardError - update_pkgdep)
#   (PortDirError, PortDeletedError, MakefileBrokenError, IgnoreMarkError - check_pkgname, find_pkg)
#   (BackupError, UninstallError) - uninstall_pkg)
def upgrade_pkg(oldpkg, origin = nil, interactive = $interactive)
  logfile = nil
  f = Tempfile.new(MYNAME)
  f.close

  oldpkgname = oldpkg.fullname
  origin ||= oldpkg.origin

  if origin && config_held?(origin)
    if $force
      warning_message "Forcing upgrade of a held package: #{origin}"
    else
      progress_message "Skipping '#{origin}' because it is held by user (specify -f to force)"
      return :skipped
    end
  elsif config_held?(oldpkgname)
    if $force
      warning_message "Forcing upgrade of a held package: #{oldpkgname}"
    else
      progress_message "Skipping '#{oldpkgname}' because it is held by user (specify -f to force)"
      return :skipped
    end
  end

  if $use_packages && origin && config_use_ports_only?(origin)
    progress_message "Using the port for '#{origin}' instead of package because of USE_PORTS_ONLY override"
    $use_packages = $use_packages_only = false
  elsif $use_packages && config_use_ports_only?(oldpkgname)
    progress_message "Using the port for '#{oldpkgname}' instead of package because of USE_PORTS_ONLY override"
    $use_packages = $use_packages_only = false
  end

  if origin.nil?
    warning_message "No origin recorded: #{oldpkgname}"
    warning_message "Specify one with -o option, or run 'pkgdb -F' to interactively fix it."
    raise OriginMissingError
  end

  logfile = f.path

  portpkgname = check_pkgname(origin, logfile)	# raises CommandFailedError

  begin
    portpkg = PkgInfo.new(portpkgname)
  rescue ArgumentError => e
    warning_message "Invalid package name: #{origin}: #{e}"
    raise InvalidPkgNameError
  end

  have_package = false
  newpkg = nil

  if (oldpkg.version < portpkg.version || $force) && $use_packages
    newpkg = catch(:newpkg) {
      make_args = get_make_args(origin)

      if !make_args.empty?
        warning_message "Custom MAKE_ARGS or -m option is specified (#{shellwords(make_args.join(','))})"

        unless $use_packages_only
          warning_message "Skipping package"
          throw :newpkg, nil
        end

        warning_message "Trying package anyway, since -PP/--use-packages-only is specified"
      end

      progress_message "Checking for the latest package of '#{origin}'"

      pkg, pkgfile = find_pkg(origin)

      if !pkg || pkg.version < oldpkg.version || pkg.version < portpkg.version
        if $noexecute
          puts "Need to fetch #{origin}: Not fetching with -n, specify -F to do fetch-only"
          throw :newpkg, nil
        end

        if fetch_pkg(origin, logfile)
          pkg, pkgfile = find_pkg(origin)
        end

        if !pkg
          warning_message "Could not find the latest version (#{portpkg.version})"
          throw :newpkg, nil
        end

        progress_message "Located a package version #{pkg.version} (#{pkgfile})"

        throw :newpkg, pkg if $force

        if pkg.version < oldpkg.version
          warning_message "Ignoring the package, which is older than what is installed (#{oldpkg.version})"
          throw :newpkg, nil
        end

        if pkg.version == oldpkg.version
          warning_message "Ignoring the package, which is the same version as is installed (#{oldpkg.version})"
          throw :newpkg, nil
        end

        if pkg.version < portpkg.version
          unless $use_packages_only
            warning_message "Ignoring the package which is not the latest version (#{portpkg.version})"
            throw :newpkg, nil
          end

          progress_message "Using it anyway although it is not the latest version (#{portpkg.version}), since -PP/--use-packages-only is specified"
	end
      end

      pkg
    }

    if $fetch_only
      return newpkg ? [:done, newpkg] : :ignored
    end

    if newpkg or $noexecute
      have_package = true
    elsif $use_packages_only
      warning_message "No package available: #{origin}"
      raise PkgNotFoundError unless $noexecute
    else
      progress_message "Using the port instead of a package"
    end
  end

  if newpkg
    newpkgname = newpkg.fullname
  else
    newpkgname = portpkgname

    begin
      newpkg = PkgInfo.new(newpkgname)
    rescue ArgumentError => e
      warning_message "Invalid package name: #{origin}: #{e}"
      raise InvalidPkgNameError
    end
  end

  cmp = newpkg.version <=> oldpkg.version

  if cmp > 0
    service = :upgrade
  elsif cmp == 0
    service = :reinstall
  else
    service = :downgrade
  end

  if newpkg.name != oldpkg.name
    warning_message "Detected a package name change: #{oldpkg.name} (#{oldpkg.origin || 'unknown'}) -> '#{newpkg.name}' (#{origin})"
  end

  if service != :upgrade && !$force
    if $verbose || oldpkgname != newpkgname
      warning_message "No need to upgrade '#{oldpkgname}' (>= #{newpkgname}). (specify -f to force)"
    end

    return :ignored
  end

  if $fetch_only
    timer_start(time_key = "Fetch for #{origin}")
    progress_message "Fetching the distfile(s) for '#{newpkgname}' (#{origin})"
  else
    case service
    when :upgrade
      time_key = "Upgrade of #{origin}"
      msg = "Upgrading '#{oldpkgname}' to '#{newpkgname}' (#{origin})"
    when :downgrade
      time_key = "Downgrade of #{origin}"
      msg = "Downgrading '#{oldpkgname}' to '#{newpkgname}' (#{origin})"
    when :reinstall
      time_key = "Reinstallation of #{origin}"
      msg = "Reinstalling '#{oldpkgname}' (#{origin})"
    end

    if have_package
      msg << " using a package"
    end

    timer_start(time_key)
    progress_message msg
  end

  if $noexecute
    puts "OK? [no]" if interactive
    return [:done, newpkg]
  elsif $yestoall
    puts "OK? [yes]" if interactive
  elsif interactive
    prompt_yesno('OK?', true) or return :skipped
  end

  unless have_package
    build_port(origin, logfile)

    return [:done, newpkg] if $fetch_only
  end

  update_pkgdep(oldpkgname, newpkgname, origin)

  teardown_proc1 = proc { |behavior|
    if behavior == :restore
      update_pkgdep(newpkgname, oldpkgname, origin)
    end
  }

  # Determine if this port should retain the 'automatic' flag or not from pkgng
  orphan = nil
  if $pkgdb.with_pkgng?
    str = backquote!(PkgDB::command(:pkg), 'query', '%a', origin)
    orphan = str.to_i == 1
  end

  # Save old origin for reverting
  oldorigin = oldpkg.origin.clone

  teardown_proc2 = uninstall_pkg(oldpkgname, orphan, logfile, $uninstall_extra_flags)

  if have_package
    install_pkg(newpkgname, orphan, origin, logfile, false, teardown_proc1, teardown_proc2)
  else
    install_port(origin, orphan, logfile, false, teardown_proc1, teardown_proc2)
  end

  # Change origin in pkgng
  if $pkgdb.with_pkgng? and origin and oldorigin != origin
    str = backquote!(PkgDB::command(:pkg), 'set', '-yo' "#{oldorigin}:#{origin}")
    warning_message "Updating origin in pkg for #{oldorigin} to #{origin}"
  end

  # Close DB before calling portsclean
  $pkgdb.close_db
  sleep 1

  progress_message "Cleaning out obsolete shared libraries"
  system!(PkgDB::command(:portsclean), '-QL')

  [:done, newpkg]
rescue CommandFailedError => e
  warning_message e.message
  progress_message "Skipping '#{origin}'"
  return :skipped
ensure
  if $logfilename_format && logfile &&
      File.exist?(logfile) && !File.zero?(logfile)
    file = $logfilename_format % origin.split('/')

    progress_message "Saving the log as '#{file}'" if $verbose

    begin
      install_data(logfile, file)
    rescue => e
      raise e if e.class == PkgDB::NeedsPkgNGSupport
      warning_message "Failed to save the log file: #{e.message}"
    end
  end

  timer_end(time_key)
end

# raises:
#   PortDirError, PortDeletedError, CommandFailedError
#   (CommandFailedError - xscript)
#   (PortDirError, PortDeletedError, MakefileBrokenError, IgnoreMarkError - get_pkgname)
def check_pkgname(origin, logfile = nil)
  portdir = $portsdb.portdir(origin)

  if command = get_beforebuild_command(origin)
    progress_message "Executing a pre-build command for '#{origin}': " + command

    unless $noexecute
      Dir.chdir(portdir) {
	xscript(logfile, '/bin/sh', '-c', command)	# raises CommandFailedError
      }
    end
  end

  get_pkgname(origin)
end

# raises:
#   PortDirError, PortDeletedError, MakefileBrokenError, IgnoreMarkError
def get_pkgname(origin)
  # Check if this origin has been deleted
  if !$ignore_moved and \
    !config_ignore_moved?(origin) and \
    (moved = $portsdb.moved.trace(origin))
    unless moved.last.to
      warning_message "Port deleted on #{moved.last.date}: #{origin}"
      STDERR.puts "\t" + moved.last.why
      raise PortDeletedError
    end
  end

  portdir = $portsdb.portdir(origin)

  if not File.directory?(portdir)
    warning_message "Port directory not found: #{origin}"
    raise PortDirError
  end

  make_env = get_make_env(origin)
  cmdargs = make_env << 'make'

  cmdargs.concat(get_make_args(origin))

  output = `cd #{portdir} && #{shelljoin(*cmdargs)} -V PKGNAME -V IGNORE -V NO_IGNORE -V ECHO_MSG`.scan(/.*\n/)

  if output.size != 4
    warning_message "Makefile possibly broken: #{origin}:"
    warning_message "Please report this to the maintainer for #{origin}"
    output.each { |line| STDERR.print "\t" + line }
    raise MakefileBrokenError
  end

  ignore_message = output[1].chomp
  no_ignore = !output[2].chomp.empty?
  echo_cmd = output[3].chomp
  ignore = `#{echo_cmd} "#{ignore_message}"`

  # Some packages ouptut a ':\n ' to try to look nice when using the normal
  # ports build process. That looks really bad here, so we undo that.
  ignore.slice!(0..2) if (ignore.match /^:\n /)

  if not ignore_message.empty?
    warning_message "Port marked as IGNORE: #{origin}:"
    STDERR.puts "\t" + ignore
    raise IgnoreMarkError unless no_ignore

    warning_message "Proceeding anyway since NO_IGNORE is defined"
  end

  output[0].chomp
end

def fetch_pkg(origin, logfile = nil, use_latest_link = false)
  cmdargs = [PkgDB::command(:pkg_fetch)]

  cmdargs << '-f' if $force
  cmdargs << '-R' if $fetch_recursive
  cmdargs << '-v' if $verbose

  newpkgname = check_pkgname(origin, logfile)	# raises CommandFailedError

  cmdargs << newpkgname

  progress_message "Fetching the package(s) for '#{newpkgname}' (#{origin})"

  if $pkgdb.with_pkgng?
    use_latest_link = false
  end

  if not script(logfile, *cmdargs)
    unless $use_packages_only
      return false
    end

    if use_latest_link
      if latest_link = $portsdb.latest_link(origin)
        progress_message "Fetching the latest package(s) for '#{latest_link}' (#{origin})"

        cmdargs[-1] = latest_link + '@'

        script(logfile, *cmdargs) or return false
      else
        warning_message "No latest link for '#{latest_link}' (#{origin}) -- giving up"
      end
    end
  end

  $pkg_cache.delete(origin)

  return true
rescue CommandFailedError => e
  warning_message e.message
  progress_message "Skipping '#{origin}'"
  return false
end

# raises:
#   PkgNotFoundError, InvalidPkgNameError
#   (PortDirError, PortDeletedError, MakefileBrokenError, IgnoreMarkError - check_pkgname)
#   (BuildError - build_port)
#   (InstallError - install_port, install_pkg)
def install_new_port(origin, orphan, interactive = $interactive)
  timer_start(time_key = "Fresh installation of #{origin}")

  logfile = nil
  f = Tempfile.new(MYNAME)
  f.close

  if config_held?(origin)
    if $force
      warning_message "Forcing installation of a held package: #{origin}"
    else
      progress_message "Skipping '#{origin}' because it is held by user (specify -f to force)"
      return false
    end
  end

  logfile = f.path

  portpkgname = check_pkgname(origin, logfile)	# raises CommandFailedError

  begin
    portpkg = PkgInfo.new(portpkgname)
  rescue ArgumentError => e
    warning_message "Invalid package name: #{origin}: #{e}"
    raise InvalidPkgNameError
  end

  have_package = false
  newpkg = newpkgname = nil

  if $use_packages && config_use_ports_only?(origin)
    progress_message "Using the port for '#{origin}' instead of package because of USE_PORTS_ONLY override"
    $use_packages = $use_packages_only = false
  end

  if $use_packages
    progress_message "Checking for the latest package of '#{origin}'"

    newpkg, pkgfile = find_pkg(origin)

    if !newpkg || newpkg < portpkg
      if $noexecute
      end

      unless $noexecute
        if fetch_pkg(origin, logfile, true)
          newpkg, pkgfile = find_pkg(origin)
        end

        if !newpkg
          warning_message "Could not find the latest version (#{portpkg.version})"
        else
          progress_message "Located a package version #{newpkg.version} (#{pkgfile})"

          if newpkg < portpkg
            if $use_packages_only
              progress_message "Using it anyway although it is not the latest version (#{portpkg.version}), since -PP/--use-packages-only is specified"
            else
              warning_message "Ignoring the package which is not the latest version (#{portpkg.version})"
              newpkg = nil
            end
          end
        end
      else
        puts "Need to fetch package for #{origin}: Not fetching with -n, specify -F to do fetch-only"
      end
    end

    if $fetch_only
      return newpkg ? true : false
    end

    if newpkg or $noexecute
      have_package = true
    elsif $use_packages_only
      warning_message "No package available: #{origin}"
      raise PkgNotFoundError unless $noexecute
    else
      progress_message "Using the port instead of a package"
    end
  end

  if newpkg
    newpkgname = newpkg.fullname
  else
    newpkgname ||= portpkgname

    begin
      newpkg = PkgInfo.new(newpkgname)
    rescue ArgumentError => e
      warning_message "Invalid package name: #{origin}: #{e}"
      raise InvalidPkgNameError
    end
  end

  if have_package
    progress_message "Installing '#{newpkgname}' from a package"
  else
    progress_message "Installing '#{newpkgname}' from a port (#{origin})"
  end

  if $noexecute
    puts "OK? [no]" if interactive
    return true
  elsif $yestoall
    puts "OK? [yes]" if interactive
  elsif interactive
    prompt_yesno or return false
  end

  if have_package
    return true if $fetch_only

    install_pkg(newpkgname, orphan, origin, logfile, true)
  else
    build_port(origin, logfile, false, true)

    return true if $fetch_only

    install_port(origin, orphan, logfile, true)
  end
rescue CommandFailedError => e
  warning_message e.message
  progress_message "Skipping '#{origin}'"
  return false
ensure
  if $logfilename_format && logfile &&
      File.exist?(logfile) && !File.zero?(logfile)
    file = $logfilename_format % origin.split('/')

    progress_message "Saving the log as '#{file}'" if $verbose

    begin
      install_data(logfile, file)
    rescue => e
      raise e if e.class == PkgDB::NeedsPkgNGSupport
      warning_message "Failed to save the log file: #{e.message}"
    end
  end

  timer_end(time_key)
end

# raises:
#   BuildError
def build_port(origin, logfile = nil, retried = false, is_new = false)
  timer_start(time_key = "Build of #{origin}") unless retried

  portdir = $portsdb.portdir(origin)

  distclean_mismatched(logfile) if retried

  msg = $fetch_only ? 'Fetching' : 'Building'
  msg << " '#{portdir}'"

  make_env = get_make_env(origin, is_new)
  cmdargs = make_env << 'make'

  cmdargs << 'BATCH=yes' if $batch_mode

  make_args = get_make_args(origin)

  if $ignore_conflicts
    make_args << '-DDISABLE_CONFLICTS'
  end

  unless make_args.empty?
    cmdargs.concat(make_args)

    msg << ' with make flags: ' << shelljoin(*make_args)
  end

  progress_message msg

  cmdargs << 'MASTER_SORT_REGEX=' << 'MASTER_SORT=' if retried

  cmdargs << "FETCH_CMD=#{$fetch_cmd}" if retried && ! $fetch_cmd.nil?

  Dir.chdir(portdir) {
    $fetch_cmd = `make -V FETCH_CMD`.chomp

    if ! STDOUT.tty?
      cmdargs << "FETCH_BEFORE_ARGS=-q"
    end

    if $fetch_only
      cmdargs << '-DBATCH'

      if $distclean >= 2
	script!(logfile, *(cmdargs.dup << 'distclean')) or
	  raise BuildError, 'distclean error'
      end

      if $fetch_recursive
	cmdargs << 'checksum-recursive'
      else
	cmdargs << 'checksum'
      end

      xscript!(logfile, *cmdargs)	# raises CommandFailedError
    else
      if $distclean >= 2
	script(logfile, *(cmdargs.dup << 'distclean')) or
	  raise BuildError, 'distclean error'
      elsif $clean
	script(logfile, *(cmdargs.dup << 'clean')) or
	  raise BuildError, 'clean error'
      end

      if $package
	cmdargs << 'DEPENDS_TARGET=install package'
      end

      if $sudo && Process.euid != 0
	dep_cmdargs = cmdargs.dup << 'fetch-depends' << 'build-depends' << 'lib-depends'

	if not system(shelljoin(*dep_cmdargs) + ' DEPENDS_TARGET="-n nonexistent_target" >/dev/null 2>&1')
	  script!(logfile, *dep_cmdargs) or
	    raise BuildError, 'dependent ports'
	end
      end

      xscript(logfile, *cmdargs)	# raises CommandFailedError
    end
  }

  true
rescue CommandFailedError => e
  reason = guess_reason(logfile)
  comment = REASON_COMMENT[reason]

  if !retried && reason == :checksum && $distclean == 1
    progress_message "Retrying #{origin}"

    $fetch_cmd = nil

    return build_port(origin, logfile, true)
  end

  warning_message e.message
  warning_message "Fix the problem and try again."
  raise BuildError, comment
ensure
  timer_end(time_key) unless retried
end

# raises:
#   CommandFailedError and Errno::*
def distclean_mismatched(logfile)
  progress_message "Deleting mismatched files"

  if File.size(logfile) >= 65536	# 64KB
    obj = "| grep '^=> \\(MD5\\|SHA256\\) Checksum mismatch for ' #{logfile}"
  else
    obj = logfile
  end

  files = []

  open(obj) do |f|
    f.each do |line|
      case line
      when /^=> (MD5|SHA256) Checksum mismatch for (\S+)\.\r?$/
	distfile = File.join($portsdb.dist_dir, $2)

	information_message "Deleting #{distfile}"

	unlink_file(distfile)
      end
    end
  end

  true
end

# raises:
#   InstallError
def install_port(origin, orphan, logfile = nil, is_new = false, *teardown_procs)
  timer_start(time_key = "Installation of #{origin}")

  portdir = $portsdb.portdir(origin)

  msg = 'Installing the new version via the port'

  make_env = get_make_env(origin, is_new)
  cmdargs = make_env << 'make'

  cmdargs << 'BATCH=yes' if $batch_mode

  if $pkgdb.with_pkgng?
    cmdargs << '-DINSTALLS_DEPENDS' if orphan
  end

  make_args = get_make_args(origin)

  unless make_args.empty?
    cmdargs.concat(make_args)

    msg << ' with make flags: ' << shelljoin(*make_args)
  end

  progress_message msg

  if $package
    cmdargs << 'DEPENDS_TARGET=install package'
  end

  if $force
    cmdargs << '-DFORCE_PKG_REGISTER'
  end

  # timestamp hack - let PkgDB detect the update
  $pkgdb.close_db
  sleep 1
  $pkgdb_update = true

  Dir.chdir(portdir) {
    xscript!(logfile, *(cmdargs.dup << 'reinstall'))	# raises CommandFailedError

    if $package
      script!(logfile, *(cmdargs.dup << 'package'))
    end

    if $cleanup
      script!(logfile, *(cmdargs.dup << 'clean'))
    end

    teardown_procs.each { |f|
      f.call(:cleanup) if f
    }

    if command = get_afterinstall_command(origin)
      progress_message "Executing a post-install command for '#{origin}': " + command

      unless $noexecute
	script!(logfile, '/bin/sh', '-c', command)
      end
    end
  }

  true
rescue CommandFailedError => e
  warning_message e.message

  teardown_procs.each { |f|
    f.call(:restore) if f
  }

  warning_message "Fix the installation problem and try again."
  raise InstallError, "install error"
ensure
  timer_end(time_key)
end

# raises:
#   InstallError
def install_pkg(pkgname, orphan, origin, logfile = nil, is_new = false, *teardown_procs)
  newpkg, pkgfile = find_pkg(origin)

  if pkgfile and deporigins = extract_pkgfile_deporigins(pkgfile)
    deporigins.each do |deporigin|
      $pkgdb.deorigin(deporigin) and next

      progress_message "Installing #{deporigin} as dependency required by #{pkgname}"

      do_install(deporigin, true)
    end
  end

  timer_start(time_key = "Installation of #{pkgname}")

  unless $is_new or $without_env_upgrade
    cmdargs = [ '/usr/bin/env' ]
    cmdargs.concat(get_upgrade_env_vars(pkgname))
  else
    cmdargs = []
  end

  if $pkgdb.with_pkgng?
    cmdargs << PkgDB::command(:pkg) << 'add'
    if orphan
      cmdargs << "-A"
    end
    cmdargs << pkgfile
  else
    cmdargs << PkgDB::command(:pkg_add) << '-f' << pkgfile
  end

  progress_message "Installing the new version via the package"

  # timestamp hack - let PkgDB detect the update
  $pkgdb.close_db
  sleep 1
  $pkgdb_update = true

  xscript!(logfile, *cmdargs)	# raises CommandFailedError

  teardown_procs.each { |f|
    f.call(:cleanup) if f
  }

  if command = get_afterinstall_command(origin)
    progress_message "Executing a post-install command for '#{origin}': " + command

    unless $noexecute
      script!(logfile, '/bin/sh', '-c', command)
    end
  end

  true
rescue CommandFailedError => e
  warning_message e.message

  teardown_procs.each { |f|
    f.call(:restore) if f
  }

  warning_message "Fix the package's problem and try again."
  raise InstallError, "pkg_add failed"
ensure
  timer_end(time_key)
end

# raises:
#   BackupError, UninstallError
def uninstall_pkg(pkgname, orphan, logfile = nil, extra_flags = '')
  timer_start(time_key = "Uninstallation of #{pkgname}")

  $pkgdb.close_db

  progress_message "Fixing up dependencies before creating a package" if $verbose

  $pkgdb.autofix

  progress_message "Backing up the old version"

  backup_pkgfile = nil

  if $pkgdb.with_pkgng?
    backup_pkgfile = File.join($tmpdir, pkgname + $portsdb.pkg_sufx)
    backquote!(PkgDB::command(:pkg), 'create', '-o', $tmpdir, '-f', $portsdb.pkg_sufx, pkgname)
  else
    if str = backquote!(PkgDB::command(:pkg_create), '-vb', pkgname,
                        File.join($tmpdir, pkgname + $portsdb.pkg_sufx))
      str.each_line { |line|
        if /^Creating .*tar ball in \'(.*)\'/ =~ line
          backup_pkgfile = $1
          break
        end
      }
    end
  end

  if backup_pkgfile.nil? || !File.file?(backup_pkgfile)
    warning_message "Backup failed."
    raise BackupError
  end

  unless $pkgdb.with_pkgng?
    pkgdir = $pkgdb.pkgdir(pkgname)
    backup_dir = File.join($tmpdir, pkgname + '.bak')

    system!('/bin/cp', '-RPp', pkgdir, backup_dir) or
      raise BackupError
  end

  origin = $pkgdb.origin(pkgname)

  progress_message "Uninstalling the old version"

  # pkg_deinstall will update the pkgdb
  $pkgdb.close_db
  # sleep 1	# pkg_deinstall does the timestamp hack
  $pkgdb_update = false

  unless $without_env_upgrade
    cmd = [ '/usr/bin/env' ]
    cmd.concat(get_upgrade_env_vars(pkgname))
  else
    cmd = []
  end

  cmd << PkgDB::command(:pkg_deinstall) << '-f' + extra_flags << pkgname

  system!(*cmd) or
    raise UninstallError, "uninstall error"

  proc { |behavior|
    case behavior
    when :restore
      progress_message "Restoring the old version"

      if $pkgdb.with_pkgng?
        xsystem! PkgDB::command(:pkg), 'add', backup_pkgfile
        xsystem! PkgDB::command(:pkg), 'set', '-y', '-A', orphan ? "1" : "0", origin
      else
        xsystem! PkgDB::command(:pkg_add), '-f', backup_pkgfile
      end

      if origin and command = get_afterinstall_command(origin)
	progress_message "Executing a post-install command for '#{origin}': " + command
	
	unless $noexecute
	  script!(logfile, '/bin/sh', '-c', command) 
	end
      end

      $pkgdb_update = true

      process_old_package(backup_pkgfile)
    when :cleanup
      progress_message "Removing temporary files and directories" if $verbose

      process_old_package(backup_pkgfile)
      unless $pkgdb.with_pkgng?
        system! '/bin/rm', '-rf', backup_dir
      end
    end
  }
ensure
  timer_end(time_key)
end

# raises:
#   (PortDirError, PortDeletedError, MakefileBrokenError, IgnoreMarkError - get_pkgname)
def find_pkg(origin)
  if $pkg_cache.include?(origin)
    return $pkg_cache[origin]
  end

  pkgname = get_pkgname(origin) or return nil

  name = pkgname.sub(/-[^\-]+$/, '')

  glob_pkgfile = name + '-*.t[bgx]z'
  re_pkgfile = /^#{Regexp.quote(name)}-[^\-]+\.t[bgx]z$/

  if latest_link = $portsdb.latest_link(origin)
    glob_pkgfile = "{#{glob_pkgfile},#{latest_link}.t[bgx]z}"
    re_pkgfile = /(?:#{re_pkgfile.source}|^#{Regexp.quote(latest_link)}\.t[bgx]z$)/
  end

  pkglist = []

  $pkg_path.split(':').each do |dir|
    begin
      Dir.chdir(dir) {
	Dir.glob(glob_pkgfile).grep(re_pkgfile) { |file|
          id_pkgname, id_origin, pkgdep = identify_pkg(file)

          if id_origin == origin
            pkglist << [PkgInfo.new(id_pkgname), File.join(dir, file)]
          end
        }
      }
    rescue => e
      raise e if e.class == PkgDB::NeedsPkgNGSupport
      warning_message e.message
    end
  end

  latest_pkg, pkgfile = *pkglist.max { |(pkg1, file1), (pkg2, file2)|
    pkg1 <=> pkg2
  }

  if latest_pkg
    progress_message "Found a package of '#{origin}': #{pkgfile} (#{latest_pkg.fullname})"
  end

  $pkg_cache[origin] = [latest_pkg, pkgfile]
end

def extract_pkgfile_deporigins(pkgfile)
  dir, file = File.split(pkgfile)

  deporigins = []

  if $pkgdb.with_pkgng?
    pkgdep = backquote!(PkgDB::command(:pkg), 'query', '-F', "#{dir}/#{file}",
                        '%do').split("\n")
  else
    IO.popen("cd #{dir} && #{PkgDB::command(:pkg_info)} -qfo #{file}") do |r|
      r.each do |line|
        case line
        when /^@comment\s+DEPORIGIN:(\S*)/
          deporigins << $1
        end
      end
    end
  end

  return deporigins
rescue => e
  raise e if e.class == PkgDB::NeedsPkgNGSupport
  warning_message e.message
  return nil
end

def guess_reason(logfile)
  if grep_q_file(/\^C/, logfile)
    reason = :interrupt
#  elsif grep_q_file(/list of extra files and directories/, logfile)
#    reason = :mtree
  elsif grep_q_file(/See <URL:http:\/\/www.gnu.org\/software\/gcc\/bugs\.html> for instructions\./, logfile)
    reason = :gcc_bug
  elsif grep_q_file(/Checksum mismatch/, logfile)
    reason = :checksum
  elsif grep_q_file(/perl: Perl is not installed, try .pkg_add -r perl./, logfile)
    reason = :perl
  elsif grep_q_file(/(No checksum recorded for|(Maybe|Either) .* is out of date, or)/, logfile)
    reason = :distinfo
  elsif grep_q_file(/(configure: error:|script.*failed: here are the contents of)/, logfile)
    reason = :configure
  elsif grep_q_file(/(bison:.*(No such file|not found)|multiple definition of \`yy)/, logfile)
    reason = :bison
  elsif grep_q_file(/Couldn't fetch it - please try/, logfile) #'
    reason = :fetch
  elsif grep_q_file(/out of .* hunks .*--saving rejects to/, logfile)
    reason = :patch
  elsif grep_q_file(/Error: category .* not in list of valid categories/, logfile)
    reason = :categories
  elsif grep_q_file(/make: don.t know how to make .*\.man. Stop/, logfile)
    reason = :xfree4man
  elsif grep_q_file(/Xm\/Xm\.h: No such file/, logfile)
    reason = :motif
  elsif grep_q_file(/undefined reference to \`Xp/, logfile)
    reason = :motiflib
#  elsif grep_q_file(/read-only file system/, logfile)
#    reason = :wrkdir
  elsif grep_q_file(/makeinfo: .* use --force/, logfile)
    reason = :texinfo
  elsif grep_q_file(/means that you did not run the h2ph script/, logfile)
    reason = :perl5
  elsif grep_q_file(/Error: shared library ".*" does not exist/, logfile)
    reason = :libdepends
  elsif grep_q_file(/(crt0|c\+\+rt0)\.o: No such file/, logfile)
    reason = :elf
  elsif grep_q_file(/machine\/soundcard\.h: No such file or directory/, logfile)
    reason = :soundcard_h
  elsif grep_q_file(/values\.h: No such file or directory/, logfile)
    reason = :values_h
  elsif grep_q_file(/.*\.h: No such file/, logfile)
    if grep_q_file(/(X11\/.*|Xosdefs)\.h: No such file/, logfile)
      if $pkgdb.glob('XFree86-*').empty?
	reason = :usexlib
      else
	reason = :header
      end
    else
      reason = :header
    end
#  elsif grep_q_file(/pnohang: killing make checksum/, logfile)
#    reason = :fetch_timeout
#  elsif grep_q_file(/pnohang: killing make package/, logfile)
#    reason = :runaway
#  elsif grep_q_file(/cd: can't cd to/, logfile) #'
#    reason = :nfs
#  elsif grep_q_file(/pkg_add: (can't find enough temporary space|projected size of .* exceeds available free space)/, logfile) #'
#    reason = :diskfull
  elsif grep_q_file(/(parse error|too (many|few) arguments to|argument.*doesn.*prototype|incompatible type for argument|conflicting types for|undeclared \(first use (in |)this function\)|incorrect number of parameters|has incomplete type and cannot be initialized)/, logfile)
    reason = :cc
  elsif grep_q_file(/(ANSI C.. forbids|is a contravariance violation|changed for new ANSI .for. scoping|[0-9]: passing .* changes signedness|discards qualifiers|lacks a cast|redeclared as different kind of symbol|invalid type .* for default argument to|wrong type argument to unary exclamation mark|duplicate explicit instantiation of|incompatible types in assignment|assuming . on overloaded member function|call of overloaded .* is ambiguous|declaration of C function .* conflicts with|initialization of non-const reference type|using typedef-name .* after|[0-9]: implicit declaration of function|[0-9]: size of array .* is too large|fixed or forbidden register .* for class)/, logfile)
    reason = :newgcc
  elsif grep_q_file(/(syntax error before|ISO C\+\+ forbids|friend declaration|no matching function for call to|.main. must return .int.|invalid conversion from|cannot be used as a macro name as it is an operator in C\+\+|is not a member of type|after previous specification in|no class template named|because worst conversion for the former|better than worst conversion|no match for.*operator|no match for call to|undeclared in namespace|is used as a type, but is not)/, logfile)
    reason = :badcpp
  elsif grep_q_file(/(\/usr\/libexec\/elf\/ld: cannot find|undefined reference to|cannot open -l.*: No such file)/, logfile)
    reason = :ld
  elsif grep_q_file(/install: .*: No such file/, logfile)
    reason = :install
  elsif grep_q_file(/chown:.*invalid argument/, logfile)
    reason = :chown
  elsif grep_q_file(/\/usr\/.*\/man\/.*: No such file or directory/, logfile)
    reason = :manpage
  elsif grep_q_file(/tar: can't add file|pkg_create: make_dist: tar command failed with code/, logfile) #'
    reason = :plist
  elsif grep_q_file(/Can't open display/, logfile) #'
    reason = :display
  elsif grep_q_file(/ is already installed - perhaps an older version/, logfile)
    reason = :dependobj
#  elsif grep_q_file(/error in dependency .*, exiting/, logfile)
#    reason = :dependpkg
  elsif grep_q_file(/\#error "<malloc\.h> has been replaced by <stdlib\.h>"/, logfile)
    reason = :malloc_h
  elsif grep_q_file(/core dumped/, logfile)
    reason = :coredump
  elsif grep_q_file(/Segmentation fault/, logfile)
    reason = :segfault
  elsif grep_q_file(/storage size of.*isn't known/, logfile)
    reason = :wait
  elsif grep_q_file(/initializer element is not constant/, logfile)
    reason = :stdio
  elsif grep_q_file(/structure has no member named/, logfile)
    reason = :struct
  elsif grep_q_file(/Permission denied/, logfile)
    reason = :perm
  elsif grep_q_file(/has known vulnerabilities/, logfile)
    reason = :vulnerabilities
  else
    reason = :unknown
  end

  reason
end

def guess_missing_origin
  require 'pathname'

  portsdir = Pathname.new($portsdb.ports_dir).realpath.to_s
  if %r|^#{portsdir}/([^/]+/[^/]+)| =~ Dir.pwd
    return $1
  else
    return nil
  end
end

class PkgResultSet
  def save(file)
    progress_message "Saving the results to '#{file}'" if $verbose

    f = Tempfile.new(MYNAME)
    write(f, '', true)
    f.close

    Object.send :install_data, f.path, file
  rescue => e
    raise e if e.class == PkgDB::NeedsPkgNGSupport
    warning_message "Failed to save the results: #{e.message}"
  end
end

if $0 == __FILE__
  set_signal_handlers

  exit(main(ARGV) || 1)
end
