#!/usr/bin/env 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/portsclean,v 1.8 2001/08/11 10:09:57 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require "optparse"
require "pkgtools"
require "find"

$interactive = false
$noexecute = false

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

def main(argv)
  usage = <<-"EOF"
usage: #{MYNAME} [-hCDin]
  EOF

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

#{usage}
  EOF

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

    opts.on("-C", "--workclean", "Clean up working directories") {
      |$workclean|
    }

    opts.on("-D", "--distclean", "Clean up unreferenced distfiles") {
      |$distclean|
    }

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

    opts.on("-n", "--noexecute", "Do not actually delete files") {
      |$noexecute|
      if $noexecute
	$interactive = false
      end
    }

    begin
      opts.parse!(argv)

      if !$workclean && !$distclean
	print opts
	exit 0
      end

      workclean if $workclean
      distclean if $distclean
    rescue OptionParser::ParseError => e
      STDERR.puts "#{MYNAME}: #{e}", usage
      exit 64
    end
  end

  return 0
end

def workclean
  perm = '770'
  grp = 'wheel'

  unless File.exists?(File.join($ports_dir, 'Mk/bsd.port.mk'))
    STDERR.puts "#{MYNAME}: PORTSDIR (#{$ports_dir}) does not seem to be a ports directory."
    return false
  end

  wrkdirprefix = `cd #{$ports_dir} && make -V WRKDIRPREFIX`.strip.tr_s('/', '/').chomp('/')

  if wrkdirprefix.empty?
    puts "Cleaning #{$ports_dir}/*/*/work..."

    # Ruby's glob yields the given block everytime a new matching
    # entry is found, so it doesn't overflow, and we don't have to
    # wait until everything is found.

    Dir.chdir $ports_dir
    Dir.glob(File.join($ports_dir, "*/*/work")) do |dir|
      delete_dir dir
    end

    puts "done."
    return true
  end

  wrkdir = wrkdirprefix + $ports_dir

  puts "Cleaning #{wrkdir}..."

  if File.directory?(wrkdir)
    delete_dir wrkdir
  end

  # Dig group writable directories in advance so all wheel users can
  # build ports without the root privilege.

  system('/bin/mkdir', '-p', wrkdir)
  system('/usr/bin/chgrp', '-f', grp, wrkdir)
  system('/bin/chmod', '-f', perm, wrkdir)

  $portsdb.categories.each do |category|
    subdir = File.join(wrkdir, category)

    system('/bin/mkdir', '-p', subdir)
    system('/usr/bin/chgrp', '-f', grp, subdir)
    system('/bin/chmod', '-f', perm, subdir)
  end

  puts "done."

  true
end

def distclean
  puts "Detecting unreferenced distfiles..."

  dist_dir = `make -f #{$ports_dir}/Mk/bsd.port.mk -V DISTDIR 2>&1`.chomp

  ports = installed_ports
  hash = categorize_ports(ports)
  categories = sorted_categories(hash)
  distfiles = scan_distfiles(dist_dir)

  catch (:done) {
    throw :done if distfiles.empty?

    ports.each do |origin|
      check_distinfo(origin).each do |file|
	distfiles.delete file
      end

      throw :done if distfiles.empty?
    end

    categories.each do |category|
      installed = hash[category]

      $portsdb.each_origin(category) do |origin|
	next if installed.include? origin

	check_distinfo(origin).each do |file|
	  distfiles.delete file
	end

	throw :done if distfiles.empty?
      end
    end
  }

  if distfiles.empty?
    puts 'no unreferenced distfiles found.'
    exit 0
  end

  distfiles.each do |file|
    file = File.join(dist_dir, file)

    delete_file file
  end
end

def installed_ports
  return `#{PkgDB::CMD[:pkg_info]} -aqo 2>/dev/null`.split
end

def categorize_ports(ports)
  hash = Hash.new([])

  ports.each do |origin|
    category, = origin.split('/')

    if hash.key?(category)
      hash[category] << origin
    else
      hash[category] = [origin]
    end
  end

  hash
end

def sorted_categories(hash)
  $portsdb.categories.sort { |a,b|
    hash[b].size <=> hash[a].size
  }
end

def scan_distfiles(dist_dir)
  distfiles = []

  Dir.chdir(dist_dir)
  Find.find('.') do |f|
    next if f == '.'
    next unless File.file? f

    distfiles << f[2..-1]	# chop `./' off
  end

  distfiles.sort!
end

def parse_distinfo(file)
  distfiles = []

  open(file) do |f|
    f.each do |line|
      if /^MD5 \((.*)\) = / =~ line
	distfiles << $1
      end
    end

    return distfiles
  end rescue []
end
  
def check_distinfo(origin)
  distfiles = []

  Dir.glob(File.join($ports_dir, origin, 'distinfo*')) do |file|
    files = parse_distinfo(file)

    distfiles.push(*files) unless files.empty?
  end

  distfiles
end

def delete_file(file)
  puts 'Deleting ' + file

  return if $noexecute

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

  File.unlink(file)
rescue => e
  STDERR.puts e
end

def delete_dir(dir)
  puts 'Deleting ' + dir

  return if $noexecute

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

  system('/bin/rm', '-rf', dir)
end

def signal_handler(sig)
  puts "\nInterrupted."
  $interrupt_proc.call if $interrupt_proc
  exit
end

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

  exit main(ARGV)
end

exit 0
