debパッケージの依存関係を図示


ubuntuがGにバージョンアップしたので、どかっとアップデートした。ついでにインストールしてあるパッケージの依存関係を把握したくなってdpkg-ruby経由でdotファイル作ってgraphvizで図示してみた。
この画像は真ん中へんを抽出してPNGにしたもの。オリジナルはSVGファイルにしたのでズームできます。こちら(http://sshi.s57.xrea.com/archive/deb_graph_spline.xml)。あたらしーバージョンのoperafirefoxならそのまま見れると思います。ローカルに落として見る場合は拡張子をsvgにしないといけないかも。
graphvizでもレイアウトエンジンがdotだとひどいグラフがでてくるが、fdpにしたら計算時間の増加とひきかえに見れるグラフがでてきた。neatoやfdpは無向グラフじゃないと駄目かと思ってたけど有向グラフでもいけるんだな。あと、依存数の大きいパッケージはでかくなるようにdotファイルに手をくわえてある。
いまは500弱しかパッケージがはいってない(普通はもっとはいってるだろう)けど、これ以上数が増えるとgraphvizの限界を越えるかなあ。まだ大丈夫だろうか。

全体像の図示としてはわりと満足だけど、より詳しくパッケージ間の関係をみようとするとこれではまだわかりにくい。選択したパッケージの依存先や依存元をハイライトする、とかインタラクティブな仕組みがほしいなあ。何でどう作るのがいちばん楽だろうか?

画像の作りかた (2007/10/28 追記)

画像を作るのに使ったRubyスクリプトをはりつけておきます。
ほとんどの仕事はgraphvizがやってるので、graphviz必須。あと、rubyからdpkg経由してパッケージ情報を参照するライブラリ、dpkg-rubyも必須です。aptでいれましょう。

以下のRubyスクリプトを走らせると、パッケージ間の依存関係を保持したdotファイルができるのでそれをgraphvizにくわせてあとは好きに。例えば、make_deb_graph.rbという名前で保存して、

ruby make_deb_graph.rb | dot -Kfdp -Tsvg > 出力ファイル名

とか。ちなみに、Athlon64 3000+なwindows XP上のVMWare上のubuntuで500弱のパッケージのある環境で走らせたら20分くらいかかりました。

ソースみればわかると思うけど、抽出してるのはDependsとPre-Dependsの情報だけ。なので、仮想パッケージを介した関係は抽出できてません。Providesを逆向きに追加すればそれでいいのかな?

以下ソース。

require 'debian'

class Debian::Dep
  def e
    @deps
  end
end

module DebGraph

  module_function

  def make_dep_hash
    dep_hash = {}
    Debian::Dpkg.status.each_package {||deb|
      next unless deb.status == "installed"
      dep_hash[deb.package] = (collect_packages(deb,"depends") + collect_packages(deb,"pre-depends")).uniq
    }
    dep_hash
  end

  def collect_packages(deb,rel)
    (deb.deps(rel).map { |d| d.e.map {|f| f.package}}).flatten
  end

  def make_rev_dep_hash(dep_hash)
    rev_dep_hash = Hash.new {|hash,key| hash[key]=[]}
    dep_hash.each do |deb_name,ar|
      ar.each do |dep_deb_name|
        rev_dep_hash[dep_deb_name] << deb_name
      end
    end
    rev_dep_hash
  end

  def make_dot_file(dep_hash,scale_map)
    id_map = make_id_map(dep_hash)
    ret = []
    ret << "digraph G {"
    ret << 'graph [size="7,10" page="8.5,11" splines=True];'
    ret << "edge[len=5];"
    all_list = dep_hash.keys
    all_list.each {|deb_name|
      point = scale_map[deb_name] || 0
      point = (Math.log(point+2)) * 20
      ret << "#{id_map[deb_name]} [label=\"#{deb_name}\"" +
      "fontsize=\"#{point}\"];"
    }
    dep_hash.each do |parent,c_ar|
      c_ar.each do |c|
        next unless all_list.include?(c)
        ret << "#{id_map[parent]} -> #{id_map[c]}"
      end
    end
    ret << "}"
    ret.join("\n")
  end

  def make_id_map(dep_hash)
    id_map = {}
    all_names  = (dep_hash.keys + dep_hash.values.flatten).uniq
    all_names.each_with_index do |n,i|
      id_map[n] = i
    end
    id_map
  end

  def main
    dep_hash = make_dep_hash
    rev_dep_hash = make_rev_dep_hash(dep_hash)
    scale_map = {}
    rev_dep_hash.each do |name,rev|
      scale_map[name] = rev.size
    end
    puts make_dot_file(dep_hash,scale_map)
  end

end

DebGraph.main if __FILE__ == $0