#!/bin/ruby # generate dependency graph among packages require 'net/http' require 'tsort' require 'set' # this requires color-tools ruby gem require 'rubygems' require 'color' SITE="http://pigalle.sfbay:10000/" # represents a graph of packages rooted at the list of packages class PackageSet include TSort # support strongly connected components attr_accessor :packages,:clusters,:roots,:max_cluster_size,:max_package_size,:name2version # build a package set from a list of package names def initialize(pkgNames) # load package list in the repository pkgs = IO.popen("pkgrecv -s #{SITE} -n").readlines pkgs = pkgs.map{|it| it[5..-1] } # cut 'pkg:/' # package short name to their version @name2version={} pkgs.each { |it| pair = it.split('@') @name2version[pair[0]] = pair[1] } # build up @packages @packages = {} @roots = pkgNames.map{|name| get(name)} # build up @clusters @clusters = self.strongly_connected_components().map{|pkgs| Cluster.new(self,pkgs) } # find the biggest cluster size @max_cluster_size = @clusters.map{|c| c.size }.max @max_package_size = @packages.map{|p| p.size }.max end def get(name) p = @packages[name] if p==nil then p = Package.new(self,name) end return p end # generate GraphViz definition def digraph() puts "digraph G {" @packages.values.each do |f| f.dependencies.each {|t| puts "\"#{f}\" -> \"#{t}\"" } end puts "}" end # generate GraphViz definition of clusters. # In the graph, only include the given clusters and no others def cluster_digraph(clusters=@clusters) within = Set.new(clusters) puts "digraph G {" clusters.each do |f| f.dependencies.select{|d| within.include? d }.each {|t| puts "\"#{f}\" -> \"#{t}\"" } # puts "\"#{f}\" [\"#{f.color.html}\"]" # puts "\"#{f}\" [color=red]" end puts "}" end def tsort_each_node(&block) @packages.each_value(&block) end def tsort_each_child(node,&block) node.dependencies.each(&block) end end # represents a package class Package attr_accessor :parent, :name, :dependencies, :reverse_dependencies, :cluster, :size, :metadata def initialize(set,name) set.packages[name] = self @parent = set @name = name @reverse_dependencies = [] # load manifest v = @parent.name2version[name] contents = Net::HTTP.get(URI.parse("#{SITE}manifest/0/#{name}@#{v}")).split("\n") # figure out dependencies @dependencies = contents.select {|it| it=~/^depend .+/ }.map {|it| fmri = it.split(" ")[1] fmri = fmri[5..-1] # chop off 'fmri=' name = fmri.split("@")[0] p = @parent.get(name) p.reverse_dependencies << self # set up reverse dependency link p } # compute size sizes = contents.select {|it| it=~/^file .+/ }.map do |l| m=l.match(/pkg.size=(\d+)/) m==nil ? 0 : m[1].to_i end @size = sizes.inject(0) {|sum,i| sum+i } # build metadata @metadata={} contents.select {|it| it=~/^set / }.map do |l| m = l.match(/name=(.+) value=(.+)/) if m!=nil then @metadata[m[1]]= (m[2]=~Regexp.new("^\".*\"$") ? m[2][1..-2] : m[2]) end end end # transitive dependencies def transitive_dependencies if @transitive_dependencies==nil then r = Set.new() q = [self]; while !q.empty? do p = q.pop() if !r.include? p then r.add(p) p.dependencies.each{|d| q.push(d); } end end @transitive_dependencies = r.to_a end return @transitive_dependencies end # reverse transitive dependencies def reverse_transitive_dependencies if @reverse_transitive_dependencies==nil then r = Set.new() q = [self]; while !q.empty? do p = q.pop() if !r.include? p then r.add(p) p.reverse_dependencies.each{|d| q.push(d); } end end @reverse_transitive_dependencies = r.to_a end return @reverse_transitive_dependencies end def description @metadata['description'] end def classification c = @metadata['info.classification'] return c==nil ? c : c[(c.index(":")+1)..-1] end # is this classified as a core package? def core? classification()=="System/Core" end # is this classified as a driver? def driver? classification()=~ /^Drivers/ end def to_s if @name =~ /^SUNW/ then return @name[4..-1] else return @name end end def color return Color::HSL.from_fraction(0.666*@size/@parent.max_package_size,1,0.5) end end # represents a package cluster, # which is a strongly connected component of packages class Cluster attr_accessor :packages,:size,:parent def initialize(parent,pkgs) @parent = parent @packages = pkgs @packages.each{|p| p.cluster=self } @size = pkgs.inject(0) {|sum,p| sum+p.size } end # compute a list of other clusters that we depend on def dependencies if @dependencies==nil then r = Set.new @packages.each {|p| p.dependencies.each {|q| r << q.cluster } } r.delete(self) @dependencies = r.to_a end return @dependencies end def reverse_dependencies if @reverse_dependencies==nil then r = Set.new @packages.each {|p| p.reverse_dependencies.each {|q| r << q.cluster } } r.delete(self) @reverse_dependencies = r.to_a end return @reverse_dependencies end def transitive_dependencies if @transitive_dependencies==nil then r = @packages[0].transitive_dependencies.map {|p| p.cluster} r = Set.new(r) r.delete(self) @transitive_dependencies = r.to_a end return @transitive_dependencies end def reverse_transitive_dependencies if @reverse_transitive_dependencies==nil then r = @packages[0].reverse_transitive_dependencies.map {|p| p.cluster} r = Set.new(r) r.delete(self) @reverse_transitive_dependencies = r.to_a end return @reverse_transitive_dependencies end def to_s if @packages.length==1 then return @packages[0].to_s else return "{#{@packages.join(',')}}" end end def color r = 1.0*@size/@parent.max_cluster_size r = ((r*2.0).to_i)/16.0 return Color::HSL.from_fraction(0.666*(1-r),1,0.5) end end # list up all core modules def listAllCore(ps) listPackages(ps, Regexp.new("System/Core") ) end def listAllDrivers(ps) listPackages(ps, /^Drivers/ ) end def listPackages(ps,regexp) ps.packages.values.each do |p| if p.classification() =~ regexp then puts "" end end end