#!/usr/local/bin/ruby
# -*- ruby -*-
#
# Copyright (c) 2000-2002 Akinori MUSHA
#
# 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.
#

RCS_ID = %q$Idaemons: /home/cvs/pkgtools/bin/portupgrade,v 1.179 2002/04/05 12:06:49 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require "optparse"
require "pkgtools"

REQUIRED_BY = PkgDB::PKGDB_FILES[:required_by]

REASON_COMMENT = {
  :aout => "a.out error",
  :bison => "bison error",
  :categories => "invalid category",
  :cc => "compiler error",
  :checksum => "checksum mismatch",
  :configure => "configure error",
#  :dependency => "dependency error",
#  :diskfull => "disk full",
  :display => "X DISPLAY error",
  :distinfo => "distinfo incorrect",
#  :extra => 'extra files',
#  :fetch_timeout => "fetch timeout",
  :fetch => "fetch error",
  :header => "missing header",
  :install => "install error",
  :interrupt => "interrupted by user",
  :ld => "linker error",
  :libdepends => "dependent libraries",
  :manpage => "manpage error",
  :motif => "Motif error",
  :motiflib => "Motif libraries error",
  :newgcc => "new compiler error",
#  :nfs => "NFS error",
  :patch => "patch error",
  :perm => "permission denied",
  :perl5 => "Perl5 error (h2ph)",
  :plist => "package error",
#  :runaway => "runaway process",
  :texinfo => "texinfo error",
  :unknown => "unknown build error",
  :usexlib => "X libraries missing",
#  :wrkdir => "WRKDIR error",
  :xfree4man => "X manpage error",
}

class OriginMissingError < StandardError
  def message
    "missing origin"
  end
end
class PortDirError < StandardError
  def message
    "port directory error"
  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 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 CommandFailedError < StandardError
#  def message
#    "command failed"
#  end
end

begin
  $initial_pwd = Dir.pwd

  if $initial_pwd.empty?
    raise Errno::ENOENT, 'No such file or directory'
  end
rescue => e
  # 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
  $beforebuild = ''
  $clean = true			# now cleaned by default
  $cleanup = true		# not cleaned up by default
  $distclean = false
  $exclude_packages = []
  $fetch_only = false
  $fetch_recursive = false
  $force = false
  $go_on = false
  $interactive = false
  $logfile_prefix = nil
  $make_args = ""
  $make_env = []
  $new = MYNAME == 'portinstall'
  $noexecute = false
  $noconfig = false
  $origin = nil
  $package = false
  $pkg_cache = {}
  $pkgdb_update = false
  $recursive = false
  $resultsfile = nil
  $sanity_check = true
  $uninstall_extra_flags = 'P'
  $upward_recursive = false
  $use_packages = false
  $use_packages_only = false
  $yestoall = false
end

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

  banner = <<-"EOF"
#{MYNAME} rev.#{RCS_REVISION}

#{usage}
  EOF

  dry_parse = true
  results = []

  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") {
      |$all|
      $recursive = false
      $upward_recursive = false
    }

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

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

    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") {
      |$beforebuild|
      $beforebuild.strip!
    }

    opts.def_option("-c", "--clean", "Do \"make clean\" before each build [default]") {
      |$clean|
    }

    opts.def_option("-C", "--cleanup", "Do \"make clean\" after each installation [default]") {
      |$cleanup|
    }

    opts.def_option("-D", "--distclean", "Do \"make distclean\" before each fetch or build") {
      |$distclean|
    }

    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") {
      |$force|
    }

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

    opts.def_option("-g", "--go-on", "Force the upgrade of a port even if some of the#{NEXTLINE}requisite ports have failed to upgrade") {
      |$go_on|
    }

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

    opts.def_option("-l", "--log-results=FILE", "Save the results as a file named <FILE>#{NEXTLINE}(default: do not save)") {
      |resultsfile|
      $resultsfile = File.expand_path(resultsfile)
    }

    opts.def_option("-L", "--log-prefix=PREFIX", "Save each port's build & install log as a file#{NEXTLINE}named <PREFIX><category>::<portname>#{NEXTLINE}(default: do not save)") {
      |logfile_prefix|
      $logfile_prefix = File.expand_path(logfile_prefix)
    }

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

    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") {
      |$noexecute|
      $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'})") {
      |$new|
    }

    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
    }

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

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

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

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

    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") {
      |$sudo|
    }

    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 = ''
    }

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

    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("-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") {
      |$yestoall|
      $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_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 = []
    package_tasks = []
    dep_hash = {}
    task_options = Hash.new({})

    result_proc = proc {
      if $pkgdb_update
	if $sudo
	  $pkgdb.close_db()

	  system! PkgDB::CMD[:pkgdb], '-u'
	else
	  $pkgdb.update_db()
	end
      end

      ret = show_results(results, $fetch_only ? 'fetched' : 'installed or upgraded')
      save_results(results, $resultsfile) if $resultsfile
      ret
    }

    $interrupt_proc = result_proc

    begin
      init_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
	print opts, "\n"
	warning_message "No package name is given."
	return 0
      end

      all = '*'
      argv << all

      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

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

	      list |= $pkgdb.recurse(pkgname, $recursive, $upward_recursive, $sanity_check)
	    end
	  rescue => e
	    STDERR.puts e.message
	    exit 1
	  end

	  if list.empty?
	    throw :pkg
	  end

	  list -= $exclude_packages

	  if list.empty?
	    warning_message "All the packages matching '#{arg}' were excluded."
	    throw :pkg
	  end

	  list.each do |i|
	    task_options[i] = {
	      :make_args => $make_args
	    }

	    if i == first
	      task_options[i][:origin] = $origin
	    end
	  end

	  upgrade_tasks |= list

	  $origin = nil

	  found = true
	}

	next if found

	unless $new
	  warning_message "No such package '#{arg}' is installed."
	  next
	end

	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).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 installed package nor such port called '#{arg}' is found."
	    next
	  end
	when 1
	  unique = true
	else
	  progress_message "#{ports.size} ports match the given pattern '#{arg}':"
	  ports.each { |origin| puts "\t#{origin}" }
	end

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

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

	  make_args = get_make_args(origin)

	  install_tasks << origin
	  task_options[origin] = {
	    :make_args => make_args
	  }

	  if $upward_recursive
	    $portsdb.all_depends_list!(origin, shelljoin(*$make_env), make_args).each do |o|
	      make_args = get_make_args(o)

	      if pkgnames = $pkgdb.deorigin(o)
		pkgnames.each do |p|
		  upgrade_tasks << p
		  task_options[p] = {
		    :make_args => make_args,
		    :origin => o,
		    :dependency => origin
		  } unless task_options.include?(p)
		end
	      else
		# XXX: needs to be aware of :extract, :patch, :configure, etc. before enabling this.
		# install_tasks << o
		# task_options[o] = {
		#   :make_args => make_args
		#   :origin => o,
		#   :dependency => origin
		# } unless task_options.include?(o)
	      end
	    end
	  end
	end
      end

      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

    upgrade_tasks -= $exclude_packages

    upgrade_tasks.each do |pkgname|
      pkg = PkgInfo.new(pkgname)

      $origin, $make_args, dependency = task_options[pkgname].indices(:origin, :make_args, :dependency)
      origin = $origin || pkg.origin

      if origin
	$make_args = task_options[pkgname][:make_args] = get_make_args(origin)

	skip = false

	if result = results.assoc(origin)
	  progress_message "Skipping '#{origin}' (#{pkgname}) which has already " + get_result_phrase(result[1], true)

	  skip = true
	elsif !$go_on
	  deps = pkg.pkgdep || []

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

	    result = results.assoc(o)

	    if result && is_result_failed(result[1])
	      progress_message "Skipping '#{origin}' (#{pkgname}) because '#{o}' (#{dep}) failed"
	      skip = true
	      break
	    end
	  end
	end

	if skip
	  results << [origin, nil, pkgname]	# skipped
	  next
	end
      end

      stty_sane

      upgraded = false

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

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

	results << [origin, result, pkgname]	# done or ignored
      rescue IgnoreMarkError => e
	results << [origin, false, pkgname]	# ignored
      rescue => e
	results << [origin, e, pkgname]		# error
      end

      if !upgraded && package_tasks.include?(pkgname)
	progress_message "Packaging '#{pkgname}' as dependency"

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

	system!('env', "PKGREPOSITORY=#{$packages_dir}", PkgDB::CMD[:pkg_tarup], pkgname)
      end
    end

    install_tasks.each do |origin|
      skip = false

      if result = results.assoc(origin)
	progress_message "Skipping '#{origin}' which has already " + get_result_phrase(result[1], true)

	skip = true
      else
	unless $go_on
	  make_args = get_make_args(origin)

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

	    if result && is_result_failed(result[1])
	      progress_message "Skipping '#{origin}' because '#{o}' failed"
	      skip = true
	      break	# not next
	    end
	  end
	end
      end

      if skip
	results << [origin, nil]	# skipped
	next
      end

      stty_sane

      $make_args = task_options[origin][:make_args]

      begin
	if install_new_port(origin, false)	# confirmed in advance
	  results << [origin, true]	# done
	else
	  results << [origin, nil]	# skipped
	end
      rescue IgnoreMarkError => e
	results << [origin, false]	# ignored
      rescue => e
	results << [origin, e]		# error
      end
    end

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

def get_make_args(origin)
  if args = config_make_args(origin)
    args + ' ' + $make_args
  else
    $make_args
  end
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!

  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!

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

# raises:
#   OriginMissingError, InvalidPkgNameError,
#   InstallError
#   (BuildError - build_port)
#   (PortDirError, MakefileBrokenError, IgnoreMarkError - check_pkgname, find_pkg)
#   (BackupError - 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)
    warning_message "The port '#{origin}' is held by user."

    if $force
      progress_message "Forced by user"
    else
      progress_message "Skipping '#{origin}' (specify -f to force)"
      return false
    end
  elsif config_held?(oldpkgname)
    warning_message "The package '#{oldpkgname}' is held by user."

    if $force
      progress_message "Forced by user"
    else
      progress_message "Skipping '#{oldpkgname}' (specify -f to force)"
      return false
    end
  end

  if origin.nil?
    warning_message "The origin of '#{oldpkgname}' is unknown."
    warning_message "Specify one with -o option, or run 'pkgdb -F' to 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 = newpkgname = nil

  if (oldpkg < portpkg || $force) && $use_packages
    progress_message "Checking the availability of the latest package of '#{origin}'"

    newpkg = find_pkg(origin)

    if !newpkg || newpkg < oldpkg || newpkg < portpkg
      fetch_pkg(origin, logfile) and newpkg = find_pkg(origin)

      if !newpkg || newpkg < portpkg
	warning_message "Could not fetch the latest version '#{portpkg.version}'"
      end

      if newpkg && newpkg < portpkg
	if oldpkg < newpkg && $use_packages_only
	  progress_message "Putting up with the version '#{newpkg.version}'"
	else
	  newpkg = nil
	end
      end
    end

    if $fetch_only
      return newpkg ? true : false
    end

    if newpkg
      have_package = true
    elsif $use_packages_only
      warning_message "The package of '#{origin}' is not found."
      raise PkgNotFoundError
    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 "Package name changed from '#{oldpkg.name}' (#{oldpkg.origin || 'unknown'}) to '#{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 false
  end

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

    if have_package
      msg << " using a package"
    end

    progress_message msg
  end

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

  unless have_package
    build_port(origin, logfile)

    return true if $fetch_only
  end

  old_deps, = update_pkgdep(oldpkgname, newpkgname)

  req_by = oldpkg.required_by

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

  teardown_proc2 = uninstall_pkg(oldpkgname, $uninstall_extra_flags)

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

  new_deps = $pkgdb.pkgdep(newpkgname) || []

  diff = old_deps - new_deps

  if not diff.empty?
    progress_message "Removing the obsoleted dependencies" if $verbose

    diff.each do |pkgname|
      subst_file(/^#{Regexp.quote(newpkgname)}\n/, '',
		 $pkgdb.pkg_required_by(pkgname))
    end
  end

  if req_by && !req_by.empty?
    progress_message "Restoring the dependency info" if $verbose

    t = Tempfile.new(REQUIRED_BY)
    t.puts(*req_by)
    t.close
    File.chmod 0644, t.path
    system! '/bin/cp', t.path, newpkg.pkg_required_by

    if $? != 0
      saved_file = File.join($tmpdir, REQUIRED_BY + '.' + newpkgname)

      warning_message "Copy failed.  Saving as '#{saved_file}'."
      system '/bin/cp', t.path, saved_file
    end
  end

  progress_message "Cleaning out obsolete shared libraries"
  system! '/bin/sh', '-c', "#{PkgDB::CMD[:portsclean]} -L >/dev/null 2>&1"

  true
rescue CommandFailedError => e
  warning_message "Command failed: " + e.message
  progress_message "Skipping '#{origin}'"
  return false
ensure
  if $logfile_prefix && logfile &&
      File.exist?(logfile) && !File.zero?(logfile)
    file = $logfile_prefix + origin.gsub('/', '::')

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

    system('/bin/mv', '-f', logfile, file)
  end
end

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

  if command = get_beforebuild_command(origin)
    progress_message "Executing a command before building '#{origin}': " + command

    unless $noexecute
      Dir.chdir(portdir) {
	script(logfile, '/bin/sh', '-c', command)
      }

      if $? != 0
	raise CommandFailedError, format("exit code %d: %s", $? >> 8, command)
      end
    end
  end

  get_pkgname(origin)
end

# raises:
#   PortDirError, MakefileBrokenError, IgnoreMarkError
def get_pkgname(origin)
  portdir = $portsdb.portdir(origin)

  if not File.directory?(portdir)
    warning_message "The port directory for '#{origin}' does not exist."
    raise PortDirError
  end

  cmdargs = $make_env.dup << 'make'

  cmdargs.concat(shellwords($make_args))

  output = `cd #{portdir} && #{shelljoin(*cmdargs)} -V PKGNAME -V IGNORE 2>&1`.to_a

  if output.size != 2
    warning_message "Makefile of '#{origin}' is possibly broken:"
    output.each { |line| STDERR.print "\t" + line }
    raise MakefileBrokenError
  end

  ignore = output[1].chomp

  if not ignore.empty?
    warning_message "'#{origin}' is marked as IGNORE:"
    STDERR.puts "\t" + ignore
    raise IgnoreMarkError
  end

  output[0].chomp
end

def fetch_pkg(origin, logfile = nil)
  cmdargs = [PkgDB::CMD[: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 not script(logfile, *cmdargs)
    unless $use_packages_only
      return false
    end

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

      cmdargs[-1] = latest_link + '@'

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

  $pkg_cache.delete(origin)

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

# raises:
#   PkgNotFoundError, InvalidPkgNameError
#   (PortDirError, MakefileBrokenError, IgnoreMarkError - check_pkgname)
#   (BuildError - build_port)
#   (InstallError - install_port, install_pkg)
def install_new_port(origin, interactive = $interactive)
  logfile = nil
  f = Tempfile.new(MYNAME)
  f.close

  if config_held?(origin)
    warning_message "The port '#{origin}' is held by user."

    if $force
      progress_message "Forced by user"
    else
      progress_message "Skipping '#{origin}' (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
    progress_message "Checking the availability of the latest package of '#{origin}'"

    newpkg = find_pkg(origin)

    if !newpkg || newpkg < portpkg
      fetch_pkg(origin, logfile) and newpkg = find_pkg(origin)

      if !newpkg || newpkg < portpkg
	warning_message "Could not fetch the latest version '#{portpkg.version}'"
      end

      if newpkg && newpkg < portpkg
	if $use_packages_only
	  progress_message "Putting up with the version '#{newpkg.version}'"
	else
	  newpkg = nil
	end
      end
    end

    if $fetch_only
      return newpkg ? true : false
    end

    if newpkg
      have_package = true
    elsif $use_packages_only
      warning_message "The package of '#{origin}' is not found."
      raise PkgNotFoundError
    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, origin, logfile)
  else
    build_port(origin, logfile)

    return true if $fetch_only

    install_port(origin, logfile)
  end
rescue CommandFailedError => e
  warning_message "Command failed: " + e.message
  progress_message "Skipping '#{origin}'"
  return false
ensure
  if $logfile_prefix && logfile &&
      File.exist?(logfile) && !File.zero?(logfile)
    file = $logfile_prefix + origin.gsub('/', '::')

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

    system('/bin/mv', '-f', logfile, file)
  end
end

# raises:
#   BuildError
def build_port(origin, logfile = nil)
  portdir = $portsdb.portdir(origin)

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

  cmdargs = $make_env.dup << 'make'

  make_args = shellwords($make_args)

  unless make_args.empty?
    cmdargs.concat(make_args)

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

  progress_message msg

  Dir.chdir(portdir) {
    if $fetch_only
      cmdargs << '-DBATCH' << '-DPACKAGE_BUILDING'

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

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

      ret = script!(logfile, *cmdargs)
    else
      if $distclean
	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=package'
      end

      if $sudo && Process.euid != 0
	dep_cmdargs = cmdargs.dup << 'fetch-depends' << 'build-depends' << 'lib-depends' << 'misc-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

      ret = script(logfile, *cmdargs)
    end

    if not ret
      warning_message "Command failed: " + shelljoin(*cmdargs)
      warning_message "Fix the problem and try again."
      raise BuildError, guess_reason(logfile)
    end
  }

  true
end

# raises:
#   InstallError
def install_port(origin, logfile = nil, *teardown_procs)
  portdir = $portsdb.portdir(origin)

  msg = 'Installing the new version via the port'

  cmdargs = $make_env.dup << 'make'

  make_args = shellwords($make_args)

  unless make_args.empty?
    cmdargs.concat(make_args)

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

  progress_message msg

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

  if $force
    cmdargs << '-DFORCE_PKG_REGISTER'
  end

  $pkgdb_update = true

  Dir.chdir(portdir) {
    if script!(logfile, *(cmdargs.dup << 'reinstall'))
      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 command after installing '#{origin}': " + command

	unless $noexecute
	  script!(logfile, '/bin/sh', '-c', command)
	end
      end
    else
      warning_message "Command failed: " + shelljoin(*cmdargs)

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

      warning_message "Fix the installation problem and try again."
      raise InstallError, "install error"
    end
  }

  true
end

# raises:
#   InstallError
def install_pkg(pkgname, origin, logfile = nil, *teardown_procs)
  cmdargs = [PkgDB::CMD[:pkg_add], '-f', pkgname]

  progress_message "Installing the new version via the package"

  sleep 1	# timestamp hack - let PkgDB detect the update

  $pkgdb_update = true

  if script!(logfile, *cmdargs)
    teardown_procs.each { |f|
      f.call(:cleanup) if f
    }

    if command = get_afterinstall_command(origin)
      progress_message "Executing a command after installing '#{origin}': " + command

      unless $noexecute
	script!(logfile, '/bin/sh', '-c', command)
      end
    end
  else
    warning_message "Command failed: " + shelljoin(*cmdargs)

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

    warning_message "Fix the package's problem and try again."
    raise InstallError, "pkg_add failed"
  end

  true
end

# raises:
#   BackupError
def uninstall_pkg(pkgname, extra_flags = '')
  progress_message "Backing up the old version"

  backup_tgz = nil

  `env PKGREPOSITORY="#{$tmpdir}" #{PkgDB::CMD[:pkg_tarup]} #{pkgname}`.each { |line|
    if /^Creating gzip\'d tar ball in \'(.*)\'/ =~ line
      backup_tgz = $1
      break
    end
  }

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

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

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

  progress_message "Uninstalling the old version"

  # pkg_deinstall will update the pkgdb
  $pkgdb.close_db
  $pkgdb_update = false

  system! PkgDB::CMD[:pkg_deinstall], '-f' + extra_flags, pkgname

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

      system! PkgDB::CMD[:pkg_add], '-f', backup_tgz
      system! '/bin/rm', '-rf', backup_tgz, pkgdir unless $backup_packages
      system! '/bin/mv', '-f', backup_dir, pkgdir

      $pkgdb_update = true
    when :cleanup
      progress_message "Removing the temporary backup files" if $verbose

      files = [backup_dir]
      files << backup_tgz unless $backup_packages

      system! '/bin/rm', '-rf', *files
    end
  }
end

# raises:
#   (PortDirError, 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_tgz = name + '*.tgz'
  re_tgz = /^#{Regexp.quote(name)}-[^\-]+\.tgz$/

  latest_pkg = nil
  latest_dir = nil

  $pkg_path.split(':').each do |dir|
    begin
      Dir.chdir(dir) {
	files = Dir.glob(glob_tgz).grep(re_tgz)

	IO.popen(shelljoin(PkgDB::CMD[:pkg_info], '-o', *files) + ' 2>/dev/null') do |r|
	  pkgname = nil

	  r.each do |line|
	    case line
	    when /^Information for +(\S+-\S+)\.tgz:/
	      pkgname = $1
	    when /^(\S+\/\S+)$/		# /
	      pkg_origin = $1

	      if pkgname && origin == pkg_origin
		pkg = PkgInfo.new(pkgname)

		if latest_pkg.nil? || latest_pkg < pkg
		  latest_pkg = pkg
		  latest_dir = dir
		end

		pkgname = nil
	      end
	    end
	  end
	end
      }
    rescue => e
      warning_message e.message
    end
  end

  if latest_pkg
    pkgfile = File.join(latest_dir, latest_pkg.fullname + '.tgz')
    progress_message "Found a package of '#{origin}': #{pkgfile}"
  end

  $pkg_cache[origin] = latest_pkg
end

def guess_reason(logfile)
  if grep_file(/\^C/, logfile)
    reason = :interrupt
#  elsif grep_file(/list of extra files and directories/, logfile)
#    reason = :extra
  elsif grep_file(/Checksum mismatch/, logfile)
    reason = :checksum
  elsif grep_file(/(No checksum recorded for|(Maybe|Either) .* is out of date, or)/, logfile)
    reason = :distinfo
  elsif grep_file(/(configure: error:|script.*failed: here are the contents of)/, logfile)
    reason = :configure
  elsif grep_file(/(bison:.*(No such file|not found)|multiple definition of \`yy)/, logfile)
    reason = :bison
  elsif grep_file(/Couldn't fetch it - please try/, logfile) #'
    reason = :fetch
  elsif grep_file(/out of .* hunks .*--saving rejects to/, logfile)
    reason = :patch
  elsif grep_file(/Error: category .* not in list of valid categories/, logfile)
    reason = :categories
  elsif grep_file(/make: don.t know how to make .*\.man. Stop/, logfile)
    reason = :xfree4man
  elsif grep_file(/Xm\/Xm\.h: No such file/, logfile)
    reason = :motif
  elsif grep_file(/undefined reference to \`Xp/, logfile)
    reason = :motiflib
#  elsif grep_file(/read-only file system/, logfile)
#    reason = :wrkdir
  elsif grep_file(/makeinfo: .* use --force/, logfile)
    reason = :texinfo
  elsif grep_file(/means that you did not run the h2ph script/, logfile)
    reason = :perl5
  elsif grep_file(/Error: shared library ".*" does not exist/, logfile)
    reason = :libdepends
  elsif grep_file(/(crt0|c\+\+rt0)\.o: No such file/, logfile)
    reason = :aout
  elsif grep_file(/.*\.h: No such file/, logfile)
    if grep_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_file(/pnohang: killing make checksum/, logfile)
#    reason = :fetch_timeout
#  elsif grep_file(/pnohang: killing make package/, logfile)
#    reason = :runaway
#  elsif grep_file(/cd: can't cd to/, logfile) #'
#    reason = :nfs
#  elsif grep_file(/pkg_add: (can't find enough temporary space|projected size of .* exceeds available free space)/, logfile) #'
#    reason = :diskfull
  elsif grep_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_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_file(/(undefined reference to|cannot open -l.*: No such file)/, logfile)
    reason = :ld
  elsif grep_file(/install: .*: No such file/, logfile)
    reason = :install
  elsif grep_file(/\/usr\/.*\/man\/.*: No such file or directory/, logfile)
    reason = :manpage
  elsif grep_file(/tar: can't add file/, logfile) #'
    reason = :plist
  elsif grep_file(/Can't open display/, logfile) #'
    reason = :display
#  elsif grep_file(/error in dependency .*, exiting/, logfile)
#    reason = :dependency
  elsif grep_file(/Permission denied/, logfile)
    reason = :perm
  else
    reason = :unknown
  end

  REASON_COMMENT[reason]
end

def save_results(results, file)
  progress_message "Saving the results to '#{file}'" if $verbose

  File.open(file, 'w') do |f|
    write_results(results, f, '', true)
  end
rescue => e
  warning_message "Failed to save the results: #{e.message}"
end

def signal_handler(sig)
  puts "\nInterrupted."

  $interrupt_proc.call if $interrupt_proc

  stty_sane

  exit
end

if $0 == __FILE__
  for sig in [2, 3, 15]
    trap(sig) do
      signal_handler(sig)
    end
  end

  exit(main(ARGV) || 1)
end
