#!/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 # 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