#!/usr/bin/env ruby
# -*- ruby -*-
#
# Copyright (c) 2000-2001 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/portupgrade,v 1.96 2001/08/27 08:01:29 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 PortNotFoundError < StandardError
  def message
    "port not found"
  end
end
class MakefileBrokenError < StandardError
  def message
    "Makefile broken"
  end
end
class InvalidPkgNameError < StandardError
  def message
    "invalid package name"
  end
end
class BackupError < StandardError
  def message
    "backup 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

$initial_pwd = Dir.pwd

$all = false
$force = false
$go_on = false
$clean = true		# now cleaned by default
$cleanup = true		# not cleaned up by default
$distclean = false
$regex = false
$fetch_only = false
$uninstall = true	# now uninstalled by default
$uninstall_extra_flags = 'P'
$recursive = false
$upward_recursive = false
$interactive = false
$resultsfile = nil
$logfile_prefix = nil
$afterinstall_command = nil
$beforebuild_command = nil
$noexecute = false
$new = MYNAME == 'portinstall'
$install_target = 'reinstall'
$pkg_hash = {}
$use_packages = false
$use_packages_only = false
$exclude_packages = []
$origin = nil
$make_args = ""

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

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

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

#{usage}
  EOF

  OptionParser.new(banner, COLUMNSIZE) do |opts|
    opts.def_option("-h", "--help", "Show this message") {
      print opts
      return 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_command|
    }

    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_command|
    }

    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("-E", "--regex", "Use extended regular expression for pkg_glob") {
      |$regex|
    }

    opts.def_option("-f", "--force", "Force upgrade (even if equal or older)") {
      |$force|
    }

    opts.def_option("-F", "--fetch", "Only fetch distfiles, do not build anything") {
      |$fetch_only|
    }

    opts.def_option("-g", "--go-on", "Go on with dependent ports even if it fails to#{NEXTLINE}update a depended port") {
      |$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 pass to make(1)") {
      |$make_args|
    }

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

    opts.def_option("-N", "--new", "Install a new one when a specified package is#{NEXTLINE}not installed (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("-p", "--package", "Build package when each port is installed") {
      $install_target = 'reinstall package'
    }

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

    opts.def_option("--use-packages-only", "Use no ports but packages only") {
      |$use_packages_only|
      scan_packages()
    }

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

    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") {
      if $uninstall
	$uninstall_extra_flags = ''
      else
	$uninstall = true
      end
    }

    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 = $regex ? Regexp.new(arg) : arg
      rescue RegexpError => e
	warning_message e.message.capitalize
	break
      end

      $exclude_packages |= $pkgdb.glob(pattern, 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 the wildcards *, ?, and [..],
or an extended regular expression if -E/--regex is specified.
The package list is automatically sorted in dependency order.

Environment Variables [default]:
    PKG_TMPDIR               temporary directory for backup etc. [$TMPDIR]
    (Note: This must have enough free space when upgrading a big package)
    TMPDIR                   temporary directory [/var/tmp]
    PORTSDIR                 ports directory [/usr/ports]
    PKGREPOSITORY            packages directory [$PORTSDIR/packages]
    PKGREPOSITORYSUBDIR      packages subdirectory [All]
    PKG_DBDIR                packages DB directory [/var/db/pkg]
    PORTS_INDEX              ports index file [$PORTSDIR/INDEX]
    PORTS_DBDIR              ports db directory [$PORTSDIR]
    PORTUPGRADE              default options (e.g. -v) [none]'

    tasks = []
    task_options = {}
    results = []
    donts = {}

    result_proc = proc {
      show_results(results, $fetch_only ? 'fetched for' : 'installed or upgraded')
      save_results(results, $resultsfile) if $resultsfile
    }

    $interrupt_proc = result_proc

    begin
      opts.order!(argv)

      envopt = ENV[MYNAME.upcase]
      if envopt
	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, true) || $portsdb.strip(arg, true)

	  if pattern.nil?
	    pattern = arg

	    if $regex && pattern[-1] == ?/
	      pattern[-1] = ?$
	    end

	    if pattern.include?('/')
	      # `|| arg' just in case
	      pattern = $portsdb.strip(pattern) || $pkgdb.strip(pattern) || arg
	    end
	  end

	  if $regex
	    begin
	      pattern = Regexp.new(pattern)
	    rescue RegexpError => e
	      warning_message e.message.capitalize
	      next
	    end
	  end
	end

	list = []

	found = false

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

	    list |= $pkgdb.recurse(pkgname, $recursive, $upward_recursive)
	  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

	  tasks |= list

	  $origin = nil

	  found = true
	}

	next if found

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

	ports = $portsdb.glob(pattern)

	case ports.size
	when 0

	  # XXX: fake install_new_port() if the specified port does
	  # not have an entry in the INDEX but the port actually
	  # exists.

	  if %r{^[^/]+/[^/]+$} =~ arg && File.exists?(File.join($ports_dir, arg, 'Makefile'))
	    port = Object.new
	    port.instance_eval %{
	      def origin
		#{arg.inspect}
	      end
	    }

	    ports << port
	  else
	    warning_message "No such installed package nor such port called '#{arg}' is found."
	    next
	  end
	when 1
	  confirm = $interactive
	else
	  progress_message "#{ports.size} ports match the given pattern '#{arg}'."
	  confirm = true
	end

	ports.each do |portinfo|
	  stty_sane

	  origin = portinfo.origin

	  result = results.assoc(origin)

	  skip = false

	  if result
	    case result[1]
	    when nil
	      done = "been done"
	    when false
	      done = "been skipped"
	    else
	      done = "failed"
	    end

	    progress_message "Skipping '#{origin}' which has already #{done}"

	    skip = true
	  elsif !$go_on
	    portinfo.all_depends.each do |dep|
	      o = $portsdb.origin_of(dep)	# perhaps nil

	      result = results.assoc(o)

	      if result && result[1]
		progress_message "Skipping '#{origin}' because '#{o}' failed"
		skip = true
		break	# cannot next here
	      end
	    end
	  end

	  if skip
	    results << [origin, false]
	    next
	  end

	  begin
	    if install_new_port(portinfo, confirm)
	      results << [origin, nil]	# done
	    else
	      results << [origin, false]	# skipped
	    end
	  rescue => e
	    results << [origin, e]		# error
	  end
	end
      end

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

    tasks -= $exclude_packages

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

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

      if origin
	result = results.assoc(origin)

	skip = false

	if result
	  case result[1]
	  when nil
	    done = "been done"
	  when false
	    done = "been skipped"
	  else
	    done = "failed"
	  end

	  progress_message "Skipping '#{origin}' (#{pkgname}) which has already #{done}"

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

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

	    result = results.assoc(o)

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

	if skip
	  results << [origin, false, pkgname]
	  next
	end
      end

      stty_sane

      begin
        if upgrade_pkg(pkg, origin)
	  results << [origin, nil, pkgname]	# done
	else
	  results << [origin, false, pkgname]	# skipped
	end
      rescue => e
	results << [origin, e, pkgname]		# error
      end
    end

    return result_proc.call
  end
ensure
  stty_sane
end

# raises:
#   OriginMissingError, InvalidPkgNameError,
#   InstallError
#   (BuildError - build_port)
#   (PortNotFoundError, MakefileBrokenError - check_pkgname)
#   (BackupError - uninstall_pkg)
def upgrade_pkg(oldpkg, origin = nil)
  logfile = nil
  f = Tempfile.new(MYNAME)
  f.close

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

  if origin.nil?
    warning_message "The origin of '#{oldpkgname}' is unknown."
    warning_message "Specify one with -o option, or run 'pkgdepfix -o' to fix it."
    raise OriginMissingError
  end

  logfile = f.path

  if $use_packages && $pkg_hash.include?(origin)
    have_package = true

    newpkg = $pkg_hash[origin]
    newpkgname = newpkg.fullname
  else
    if $use_packages_only
      warning_message "The package for '#{origin}' does not exist."
      progress_message "Skipping '#{origin}'"
      return false
    end

    have_package = false

    newpkgname = check_pkgname(origin, logfile)

    if newpkgname.nil?
      warning_message "Command failed: " + $beforebuild_command
      progress_message "Skipping '#{origin}'"
      return false
    end

    newpkg = nil

    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
    warning_message "No need to upgrade '#{oldpkgname}' (>= #{newpkgname})."
    return false
  end

  if $verbose || $interactive
    case service
    when :upgrade
      progress_message "Upgrading '#{oldpkgname}' to '#{newpkgname}' (#{origin})"
    when :downgrade
      progress_message "Downgrading '#{oldpkgname}' to '#{newpkgname}' (#{origin})"
    when :reinstall
      progress_message "Reinstalling '#{oldpkgname}' (#{origin})"
    end
  end

  if $interactive && !prompt_yesno('OK?', true)
    return false
  end

  return true if $noexecute

  unless have_package
    build_port(origin, logfile)

    return true if $fetch_only
  end

  update_pkgdep(oldpkgname, newpkgname)

  req_by = oldpkg.required_by

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

  if $uninstall
    teardown_proc2 = uninstall_pkg(oldpkgname, $uninstall_extra_flags)
  else
    teardown_proc2 = nil
  end

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

  if not $uninstall || oldpkgname == newpkgname
    progress_message "Removing the old package info" if $verbose

    system! '/bin/rm', '-rf', oldpkg.pkgdir
  end

  if req_by
    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

  true
ensure
  if $logfile_prefix && logfile &&
      File.exists?(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:
#   PortNotFoundError, MakefileBrokenError
def check_pkgname(origin, logfile = nil)
  portdir = File.join($ports_dir, origin)

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

  if $beforebuild_command
    Dir.chdir_do(portdir) {
      script(logfile, '/bin/sh', '-c', $beforebuild_command)
    }

    if $? != 0
      return nil
    end
  end

  output = `cd #{portdir} && make -V PKGNAME #{$make_args} 2>&1`.to_a

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

  output[0].chomp
end

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

  origin = portinfo.origin

  logfile = f.path

  if $use_packages && $pkg_hash.include?(origin)
    have_package = true

    newpkg = $pkg_hash[origin]
    newpkgname = newpkg.fullname
  else
    if $use_packages_only
      warning_message "The package for '#{origin}' does not exist."
      raise PkgNotFoundError
    end

    have_package = false

    newpkgname = check_pkgname(origin, logfile)

    if newpkgname.nil?
      warning_message "Command failed: " + $beforebuild_command
      progress_message "Skipping '#{origin}'"
      return false
    end

    newpkg = nil

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

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

  if interactive
    if $noexecute
      puts 'OK? [no]'
      return false
    end

    prompt_yesno or return false
  end

  return true if $noexecute

  if have_package
    return true if $fetch_only

    install_pkg(newpkgname, logfile)
  else
    build_port(origin, logfile)

    return true if $fetch_only

    install_port(origin, logfile)
  end
ensure
  if $logfile_prefix && logfile &&
      File.exists?(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 = File.join($ports_dir, origin)

  dep_cmdargs = ['make']
  cmdargs = ['make']

  args = shellwords($make_args.dup)

  if args.empty?
    with_args = ''
  else
    dep_cmdargs.concat(args)
    cmdargs.concat(args)

    with_args = " with make flags: #{$make_args}"
  end

  progress_message "#{$fetch_only ? 'Fetching' : 'Building'} '#{portdir}'#{with_args}" if $verbose

  if $distclean
    cmdargs << 'distclean'
  end

  if $fetch_only
    dep_cmdargs << 'fetch-depends' << '-DBATCH' << '-DPACKAGE_BUILDING'
    cmdargs << 'fetch' << '-DBATCH' << '-DPACKAGE_BUILDING'
  else
    if $clean
      cmdargs << 'clean'
    end

    dep_cmdargs << 'fetch-depends' << 'build-depends' << 'lib-depends' << 'misc-depends'
    cmdargs << 'build'
  end

  Dir.chdir_do(portdir) {
    if not system(shelljoin(*dep_cmdargs) + ' DEPENDS_TARGET="-n nonexistent_target" >/dev/null 2>&1')
      if $clean
	dep_cmdargs[1, 0] = 'clean-depends'
      end

      script!(logfile, *dep_cmdargs)
    end

    if not script(logfile, *cmdargs)
      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 = File.join($ports_dir, origin)

  install_target_a = shellwords($install_target.dup)

  cmdargs = ['make', *install_target_a]

  args = shellwords($make_args.dup)

  if args.empty?
    with_args = ''
  else
    cmdargs.concat(args)

    with_args = " with make flags: #{$make_args}"
  end

  progress_message "Installing the new version via the port#{with_args}" if $verbose

  if $recursive && install_target_a.include?('package')
    cmdargs << 'DEPENDS_TARGET=package'
  end

  if $force
    cmdargs << '-DFORCE_PKG_REGISTER'
  end

  if $cleanup
    cmdargs << 'clean'
  end

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

      if $afterinstall_command
	script(logfile, '/bin/sh', '-c', $afterinstall_command)
      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, logfile = nil, *teardown_procs)
  cmdargs = [PkgDB::CMD[:pkg_add], '-f', File.join($packages_dir, pkgname + '.tgz')]

  progress_message "Installing the new version via the package" if $verbose

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

    if $afterinstall_command
      script(logfile, '/bin/sh', '-c', $afterinstall_command)
    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" if $verbose

  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 = pkgdir + '.bak'

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

  progress_message "Uninstalling the old version" if $verbose

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

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

      system! PkgDB::CMD[:pkg_add], '-f', backup_tgz
      system! '/bin/rm', '-rf', backup_tgz, pkgdir
      system! '/bin/mv', '-f', backup_dir, pkgdir
    when :cleanup
      progress_message "Cleaning up the backup" if $verbose

      system! '/bin/rm', '-rf', backup_dir, backup_tgz
    end
  }
end

def scan_packages
  $pkg_hash.empty? or return

  if not File.directory?(File.join($packages_dir, '.'))
    warning_message "No such directory '#{$packages_dir}' is found."
    exit 1
  end

  progress_message "Scanning the directory: " + $packages_dir

  IO.popen("cd #{$packages_dir} && #{PkgDB::CMD[:pkg_info]} -o *.tgz") do |r|
    pkgname = nil

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

	if pkgname
	  printf "%-26s  %s\n", pkgname, origin if $verbose

	  pkg = PkgInfo.new(pkgname)

	  pkg0 = $pkg_hash[origin]

	  if pkg0.nil? || pkg0 < pkg
	    $pkg_hash[origin] = pkg
	  end

	  pkgname = nil
	end
      end
    end
  end
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
