#!/usr/bin/ruby -w
# $Id: depends,v 1.5 2003/08/25 08:06:11 hip Exp $
#
# show back/forward package dependencies on a Debian system
# http://www.xs4all.nl/~hipster/lib/ruby/depends
#
# Copyright (C) 2000  Michel van de Ven <hipster@xs4all.nl>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# TODO add sensible getopt handling
# TODO add switch to print all packages on which no-one depends (orphans)
#

# MODIFY: location of the dependency cache
CACHE  = File.expand_path("~/lib/depends.cache")
# ----

STATUS = "/var/lib/dpkg/status"

def usage
  puts "Usage: depends PACKAGE [LEVEL]"
  puts "If package is `ALL', dependencies are printed for all packages."
  puts "Dependencies are printed LEVEL levels deep (default = 1)"
  exit
end

package = (ARGV.shift || usage)
level = (ARGV.shift || 1).to_i
level = 1 if level < 1

class Dependency
  attr_reader :name, :relation, :version

  def initialize txt
    if /(.*) \((.*) (.*)\)/.match(txt)
      @name = $1
      @relation = $2
      @version = $3
    elsif /(.*)/.match(txt)
      @name = $1
      @relation = "="
      @version = "*"
    else
      $stderr.puts "Dependency: could not parse string"
    end
  end
end

class Package
  attr_reader :name, :status, :priority, :section, :size, :maintainer,
    :source, :version, :replaces, :depends, :description, :provides,
    :recommends, :suggests, :conflicts, :essential, :conffiles
  
  def initialize rec
    @depends = []
    @provides = []
    rec.split(/\n/).each{ |field|
      case field
      when /^Package: (.*)/
        @name = $1
      when /^Status: (.*) (.*) (.*)/
        @status = [$1, $2, $3]
      when /^Priority: (.*)/
        @priority = $1
      when /^Section: (.*)/
        # NOTE make this an array? split on possible '/' (e.g. non-free)
        @section = $1 
      when /^Installed-Size: (.*)/
        @size = $1
      when /^Maintainer: (.*)/
        @maintainer = $1
      when /^Source: (.*)/
        @source = $1
      when /^Version: (.*)/
        @version = $1
      when /^Provides: (.*)/
        @provides = $1.split(/,\s*/)
      when /^Recommends: (.*)/
        @recommends = $1
      when /^Suggests: (.*)/
        @suggests = $1
      when /^Conflicts: (.*)/
        @conflicts = $1
      when /^Essential: (.*)/
        @essential = $1
      when /^Conffiles:/
        @conffiles = $1
      when /^Replaces: (.*)/
        @replaces = $1
      when /^Depends: (.*)/
        # split up 'foo (>= 42), bar, foo | bar'
        $1.split(/,\s*/).each{ |dep_raw|
          dep_raw.split(/\s*\|\s*/).each{ |dep|
            @depends << Dependency.new(dep)
          }
        }
      when /^Description: (.*)/
        @description = $1
      else
        # unknown field
      end
    }
  end

  def <=> other
    @name <=> other.name
  end

  def depends_to_s
    (@depends || []).collect{ |dep|
      format("%s %s %s", dep.name, dep.relation, dep.version)
    }.join(", ")
  end
end

class Pool
  def initialize
    @pool = []
    @indent = 0
    npkg = 0
    File.foreach(STATUS, "\n\n"){ |rec|
      pkg = Package.new rec
      @pool << pkg if pkg.status[2] == "installed"
      print npkg += 1, "\r"
    }
    printf "%d packages available, %d installed\n", npkg, @pool.size
  end

  def deps package, level = 1
    pkg_esc = Regexp.new(Regexp.escape(package))
    @pool.sort.each{ |pkg|
      if pkg_esc.match(pkg.name)
        forward_deps pkg.name
        reverse_deps pkg.name, level
        indented { provided_deps pkg, level }
      else
        pkg.provides.each{ |name|
          if pkg_esc.match(name)
            indent "provided by #{pkg.name}:"
            reverse_deps name, level
          end
        }
      end
    }
  end

  def deps_all level = 1
    @pool.sort.each{ |pkg|
      forward_deps pkg.name
      reverse_deps pkg.name, level
      indented { provided_deps pkg, level }
    }
  end

  private

  def forward_deps package
    puts package + " -> " +
      @pool.find{ |pkg| pkg.name == package }.depends_to_s
  end

  def reverse_deps package, level
    return if level == 0
    # select all packages that have `package' in their dependency list
    pkg_esc = Regexp.escape(package)
    deps = @pool.select{ |pkg|
      pkg.depends != nil and pkg.depends.find { |d| d.name == package }
    }
    indent "#{package} <- ("
    indented {
      deps.sort.each{ |pkg|
        if level == 1
          indent pkg.name
        else
          reverse_deps pkg.name, level - 1
        end
      }
    }
    indent ")"
  end

  def provided_deps package, level
    package.provides.each{ |name|
      indent "provided by #{package.name}:"
      reverse_deps name, level
    }
  end

  def indent txt
    print "  " * @indent, txt, "\n"
  end

  def indented
    @indent += 1
    yield
    @indent -= 1
  end
end

class Cache
  attr :pool

  def initialize
    # persist/restore object pool
    if test(?f, CACHE)
      stime = File.stat(STATUS).mtime
      ctime = File.stat(CACHE).mtime
      if ctime < stime
        puts "cache is outdated, refreshing..."
        update_cache
      else
        load_cache
      end
    else
      puts "cache does not exist, creating..."
      update_cache
    end
  end

  private

  def update_cache
    @pool = Pool.new
    File.open(CACHE, "w"){ |f|
      Marshal.dump(@pool, f)
    }
  end

  def load_cache
    File.open(CACHE, "r"){ |f|
      @pool = Marshal.load(f)
    }
  end
end

c = Cache.new
if package == "ALL"
  c.pool.deps_all level                 # apologies to Meyer ;)
else
  c.pool.deps package, level
end
