# $Idaemons: /home/cvs/pkgtools/lib/pkgtools.rb,v 1.70 2002/04/28 19:52:50 knu Exp $

PREFIX = "/usr/local"

begin
  require 'fnmatch' unless File.respond_to?(:fnmatch?)
rescue LoadError
  if ENV['RUBY_STATIC']
    raise "fnmatch module not found"
  else
    exec '/usr/bin/env', 'RUBY_STATIC=1', "#{PREFIX}/bin/ruby_s", $0, *ARGV
  end
end

require "pkg"
require "ports"
require "pkgmisc"

require "tempfile"

module PkgConfig
end

def load_config
  file = ENV['PKGTOOLS_CONF'] || File.join(PREFIX, 'etc/pkgtools.conf')

  File.exist?(file) or return false

  begin
    load file
  rescue Exception => e
    STDERR.puts "** Error occured reading #{file}:",
      e.message.gsub(/^/, "\t")
    exit 1
  end

  init_pkgtools_global

  val = config_value(:SANITY_CHECK)
  val.nil? or $sanity_check = val

  $make_args_table = nil
  $beforebuild_table = nil
  $afterinstall_table = nil

  $hold_pkgs = nil

  $portsdb.ignore_categories = config_value(:IGNORE_CATEGORIES) || []
  $portsdb.extra_categories = config_value(:EXTRA_CATEGORIES) || []

  if a = config_value(:PKG_SITES)
    $pkg_sites.concat(a)
  else
    $pkg_sites << PkgConfig.pkg_site_mirror()
  end

  true
end

def config_value(name)
  PkgConfig.const_defined?(name) ? PkgConfig.const_get(name) : nil
end

def config_make_args(origin)
  if $make_args_table.nil?
    $make_args_table = {}

    if h = config_value(:MAKE_ARGS)
      h.each do |pattern, args|
	$portsdb.glob(pattern) do |portinfo|
	  o = portinfo.origin

	  if val = $make_args_table[o]
	    $make_args_table[o] = val + ' ' + args
	  else
	    $make_args_table[o] = args
	  end
	end
      end
    end
  end

  $make_args_table[origin]
end

def config_beforebuild(origin)
  if $beforebuild_table.nil?
    $beforebuild_table = {}

    if h = config_value(:BEFOREBUILD)
      h.each do |pattern, command|
	command = command.strip

	next if command.empty?

	$portsdb.glob(pattern) do |portinfo|
	  o = portinfo.origin

	  if $beforebuild_table.key?(o)
	    $beforebuild_table[o] << command
	  else
	    $beforebuild_table[o] = [command]
	  end
	end
      end
    end
  end

  $beforebuild_table[origin]
end

def config_afterinstall(origin)
  if $afterinstall_table.nil?
    $afterinstall_table = {}

    if h = config_value(:AFTERINSTALL)
      h.each do |pattern, command|
	command = command.strip

	next if command.empty?

	$portsdb.glob(pattern) do |portinfo|
	  o = portinfo.origin

	  if $afterinstall_table.key?(o)
	    $afterinstall_table[o] << command
	  else
	    $afterinstall_table[o] = [command]
	  end
	end
      end
    end
  end

  $afterinstall_table[origin]
end

def config_held?(p)
  if $hold_pkgs.nil?
    $hold_pkgs = []

    if a = config_value(:HOLD_PKGS)
      a.each do |pattern|
	$portsdb.glob(pattern) do |portinfo|
	  $hold_pkgs << portinfo.origin
	end

	if pkgnames = $pkgdb.deorigin_glob(pattern)
	  pkgnames.each do |pkgname|
	    $hold_pkgs << pkgname
	  end
	end

	$hold_pkgs.concat($pkgdb.glob(pattern, false))
      end

      $hold_pkgs.sort!
    end
  end

  case p
  when PortInfo
    return $hold_pkgs.qinclude?(p.origin)
  when PkgInfo
    return (o = p.origin and $hold_pkgs.qinclude?(o)) ||
      $hold_pkgs.qinclude?(p.fullname)
  end

  $hold_pkgs.qinclude?(p)
end

def init_pkgtools_global
  # initialize pkgdb first - PortsDB uses PkgDB.instance.db_dir.
  $pkgdb = PkgDB.instance.setup
  $pkgdb_dir = $pkgdb.db_dir
  $portsdb = PortsDB.instance.setup
  $ports_dir = $portsdb.ports_dir
  $packages_base = ENV['PACKAGES'] || File.join($ports_dir, 'packages')
  $packages_dir = File.join($packages_base, 'All')
  $tmpdir = ENV['PKG_TMPDIR'] || ENV['TMPDIR'] || '/var/tmp'
  $pkg_path = ENV['PKG_PATH'] || $packages_dir

  $pkg_sites = (ENV['PKG_SITES'] || '').split

  $verbose = false
  $sudo_args = ['sudo']
  $sudo = false
end

def parse_pattern(str, regex = false)
  if str[0] == ?:
    regex = true
    str = str[1..-1]
  end

  if regex
    Regexp.new(str)
  else
    str
  end
end

def stty_sane
  system '/bin/stty', 'sane' if STDIN.tty?
end

def progress_message(message, io = STDOUT)
  io.puts "--->  " + message
end

def information_message(message, io = STDERR)
  io.puts "++ " + message
end

def warning_message(message, io = STDERR)
  io.puts "** " + message
end

def all?(str)
  /^a/i =~ str
end

def yes?(str)
  /^y/i =~ str
end

def no?(str)
  /^n/i =~ str
end

def yesno_str(yes)
  if yes then 'yes' else 'no' end
end
  
def prompt_yesno(message = "OK?", yes_by_default = false)
  print "#{message} [#{yesno_str(yes_by_default)}] "
  STDOUT.flush

  input = (STDIN.gets || '').strip

  if yes_by_default
    !no?(input)
  else
    yes?(input)
  end
end

def prompt_yesnoall(message = "OK?", yes_by_default = false)
  print "#{message} ([y]es/[n]o/[a]ll) [#{yesno_str(yes_by_default)}] "
  STDOUT.flush

  input = (STDIN.gets || '').strip

  if all?(input)
    :all
  elsif yes_by_default
    !no?(input)
  else
    yes?(input)
  end
end

def matchlen(a, b)
  i = 0
  0.upto(a.size) { |i| a[i] != b[i] and break }
  i
end

def input_line(prompt, add_history = nil, completion_proc = nil)
  prev_proc = Readline.completion_proc

  Readline.completion_append_character = nil if Readline.respond_to?(:completion_append_character=)
  Readline.completion_proc = completion_proc if completion_proc.respond_to?(:call)

  Readline.readline(prompt, add_history)
ensure
  Readline.completion_proc = prev_proc if prev_proc.respond_to?(:call)
end

def input_file(prompt, dir, add_history = nil)
  Dir.chdir(dir) {
    return input_line(prompt, add_history, Readline::FILENAME_COMPLETION_PROC)
  }
end

OPTIONS_NONE	= 0x00
OPTIONS_SKIP	= 0x01
OPTIONS_DELETE	= 0x02
OPTIONS_ALL	= 0x04
OPTIONS_HISTORY	= 0x08

def choose_from_options(message = 'Input?', options = nil, flags = OPTIONS_NONE)
  skip		= (flags & OPTIONS_SKIP).nonzero?
  delete	= (flags & OPTIONS_DELETE).nonzero?
  all		= (flags & OPTIONS_ALL).nonzero?
  history	= (flags & OPTIONS_HISTORY).nonzero?

  completion_proc = nil

  unless options.nil?
    case options.size
    when 0
      return :skip
    else
      completion_proc = proc { |head|
	len = head.size
	options.select { |option| head == option[0, len] }
      }
    end
  end

  loop do
    input = input_line(message + ' (? to help): ', history, completion_proc)

    if input.nil?
      print "\n"

      next if not delete

      if all
	ans = prompt_yesnoall("Delete this?", true)
      else
	ans = prompt_yesno("Delete this?", true)
      end

      if ans == :all
	return :delete_all
      elsif ans
	return :delete
      end

      next
    end

    input.strip!

    case input
    when '.'
      return :abort
    when '?'
      print ' [Enter] to skip,' if skip
      print ' [Ctrl]+[D] to delete,' if delete
      print '  [.][Enter] to abort, [Tab] to complete'
      print "\n"
      next
    when ''
      if skip
	if all
	  ans = prompt_yesnoall("Skip this?", true)
	else
	  ans = prompt_yesno("Skip this?", true)
	end

	if ans == :all
	  return :skip_all
	elsif ans
	  return :skip
	end
      end

      next
    else
      if options.include?(input)
	return input
      end

      print "Please choose one of these:\n"

      if options.size <= 20
	puts options.join('  ')
      else
	puts options[0, 20].join('  ') + "  ..."
      end
    end
  end

  # not reached
end

def sudo(*args)
  if $sudo && Process.euid != 0
    if $sudo_args.grep(/%s/).empty?
      args = $sudo_args + args
    else
      args = $sudo_args.map { |arg|
	format(arg, shelljoin(*args)) rescue arg
      }
    end

    progress_message "[Executing a command as root: " + shelljoin(*args) + "]"
  end

  system(*args)
end
alias system! sudo

def script(file, *args)
  if file
    system('/usr/bin/script', '-qa', file, *args)
  else
    system(*args)
  end
end

def script!(file, *args)
  if file
    system!('/usr/bin/script', '-qa', file, *args)
  else
    system!(*args)
  end
end

def grep_file(re, file)
  system '/usr/bin/egrep', '-q', re.source, file
end

def update_pkgdep(oldpkgname, newpkgname)
  return [], [] if oldpkgname == newpkgname

  progress_message "Updating dependency info" if $verbose

  # gsub's below are unnecessary in reality as pkgname shouldn't contain \'s
  req_by_re_oldpkgname = /^#{Regexp.quote(oldpkgname)}$/
  req_by_sub_newpkgname = newpkgname.gsub(/\\/, "\\\\")

  pkgdep_re_oldpkgname = /^(@pkgdep\s+)#{Regexp.quote(oldpkgname)}$/
  pkgdep_sub_newpkgname = "\\1" + newpkgname.gsub(/\\/, "\\\\")

  pkgdep_pkgnames = []
  req_by_pkgnames = []

  $pkgdb.installed_pkgs.each do |pkgname|
    subst_file(req_by_re_oldpkgname,
	       req_by_sub_newpkgname,
	       $pkgdb.pkg_required_by(pkgname)) and req_by_pkgnames << pkgname

    subst_file(pkgdep_re_oldpkgname,
	       pkgdep_sub_newpkgname,
	       $pkgdb.pkg_contents(pkgname)) and pkgdep_pkgnames << pkgname
  end

  return req_by_pkgnames, pkgdep_pkgnames
end

def subst_file(pattern, subst, file, backup_file = nil)
  File.exist?(file) or return nil

  if File.size(file) >= 65536	# 64KB
    subst_file_ext(pattern, subst, file, backup_file)
  else
    subst_file_int(pattern, subst, file, backup_file)
  end
end

def subst_file_int(pattern, subst, file, backup_file = nil)
  changed_lines = []

  File.open(file) do |r|
    lines = r.collect { |line|
      newline = line.gsub(pattern, subst)
      if newline != line
	changed_lines << [line, newline]
      end
      newline
    }

    r.close

    if changed_lines.empty?
      return changed_lines
    end

    w = Tempfile.new(File.basename(file))
    w.print(*lines)
    w.close
    newfile = w.path

    progress_message "Modifying #{file}" if $verbose

    File.chmod 0644, newfile

    if File.writable? file
      system '/bin/mv', '-f', file, backup_file if backup_file
      system '/bin/cp', '-f', newfile, file
    else
      system! '/bin/mv', '-f', file, backup_file if backup_file
      system! '/bin/cp', '-f', newfile, file
    end
  end

  changed_lines
rescue
  nil
end

def subst_file_ext(pattern, subst, file, backup_file = nil)
  pat = pattern.source

  w = Tempfile.new(File.basename(file))
  w.close
  newfile = w.path

  system(format("%s > %s",
		shelljoin('perl',
			  '-pe',
			  format('s/%s/%s/g',
				 pat.gsub(/([\/@%])/, "\\" "\\" "\\1"),
				 subst.gsub(/([\/@%])/, "\\" "\\" "\\1")),
			  file),
		newfile))

  changed_lines = []

  open('| ' + shelljoin('diff', file, newfile)) do |r|
    i = 0
    r.each { |line|
      case line[0]
      when ?<
	changed_lines.push([line[2..-1]])
      when ?>
	changed_lines[i].push(line[2..-1])
	i += 1
      end
    }

    r.close

    if changed_lines.empty?
      return changed_lines
    end

    progress_message "Modifying #{file}" if $verbose

    File.chmod 0644, newfile

    if File.writable? file
      system '/bin/mv', '-f', file, backup_file if backup_file
      system '/bin/cp', '-f', newfile, file
    else
      system! '/bin/mv', '-f', file, backup_file if backup_file
      system! '/bin/cp', '-f', newfile, file
    end
  end

  changed_lines
rescue
  nil
end

def search_paths(command)
  ENV['PATH'].split(':').each do |dir|
    path = File.join(dir, command)
    stat = File.stat(path)
    return path if stat.file? && stat.executable?(path)
  end

  nil
end

def is_result_ok(result)
  result == true || result == false
end

def is_result_failed(result)
  result != true && result != false
end

def get_result_phrase(result, long = false)
  case result
  when true
    "succeeded"
  when false
    long ? "been ignored" : "ignored"
  when nil
    long ? "been skipped" : "skipped"
  else
    "failed"
  end
end

def get_result_sign(result, long = false)
  case result
  when true
    sign = "+"
  when false
    sign = "-"
  when nil
    sign = "*"
  else
    sign = "!"
  end

  if long
    sign + ":" + get_result_phrase(result)
  else
    sign
  end
end

def get_result_message(result)
  case result
  when true, false, nil
    nil
  when Exception
    result.message
  else
    result
  end
end

def show_results(results, done_service = 'done', verbose = $verbose)
  if verbose
    if results.empty?
      warning_message "No package has been #{done_service}."
      return 0
    end

    progress_message "Reporting the results (" +
      [true, false, nil, :error].map { |e| get_result_sign(e, true) }.join(" / ") + ")"
  else
    results.find { |item, e, | is_result_failed(e) } or return 0

    warning_message "The following packages were not #{done_service} (" +
      [nil, :error].map { |e| get_result_sign(e, true) }.join(" / ") + ")"
  end

  write_results(results)
end

def write_results(results, io = STDOUT, prefix = "\t", verbose = $verbose)
  errors = 0

  results.each do |item, e, info|
    next if !verbose && is_result_ok(e)

    if info
      item = "#{item} (#{info})"
    end

    line = prefix.dup << get_result_sign(e) << " " << item

    if message = get_result_message(e)
      line << "\t(" << message << ")"
      errors += 1
    end

    io.puts line
  end

  errors
end

module PkgConfig
  uname = `uname -rm`.chomp

  if m = /^(((\d+)(?:\.\d+)+)-(\w+)(?:\S*)) (\w+)$/.match(uname)
    OS_RELEASE, OS_REVISION, OS_MAJOR, OS_BRANCH, OS_PLATFORM = m[1..-1]
  else
    STDERR.puts "uname(1) could be broken - cannot parse the output: #{uname}"
  end

  def pkg_site_mirror(root = ENV['PACKAGEROOT'] || 'ftp://ftp.FreeBSD.org')
    sprintf('%s/pub/FreeBSD/ports/%s/packages-%s/',
	    root, OS_PLATFORM, OS_RELEASE.downcase)
  end

  def pkg_site_primary()
    pkg_site_mirror('ftp://ftp.FreeBSD.org')
  end

  def pkg_site_builder(latest = false)
    run = latest ? 'latest' : 'full'

    case OS_PLATFORM
    when 'i386'
      sprintf('http://bento.FreeBSD.org/errorlogs/packages-%s-%s/',
	      OS_MAJOR, run)
    when 'alpha'
      sprintf('http://beta.FreeBSD.org/errorlogs/packages-%s-%s/',
	      OS_MAJOR, run)
    else
      raise 'There is no package builder site for the ' + OS_PLATFORM +
	' platform.'
    end
  end

  module_function :pkg_site_mirror, :pkg_site_primary, :pkg_site_builder

  def localbase()
    $portsdb.localbase
  end

  def x11base()
    $portsdb.x11base
  end

  module_function :localbase, :x11base
end

init_pkgtools_global
