lib/plugins/dependencies.rb in rsence-2.0.0.8.pre vs lib/plugins/dependencies.rb in rsence-2.0.0.9.pre

- old
+ new

@@ -5,32 +5,279 @@ # You should have received a copy of the GNU General Public License along # with this software package. If not, contact licensing@riassence.com ## module RSence + + # = Description: + # Generic dependency calculator. Used by PluginManager. + # = Usage: + # This is an almost real world example: + # ## Initialize with pre-satisfied dependencies + # deps = RSence::Dependencies.new( [:foo1,:foo2] ) + # + # ## :client_pkg doesn't depend on anything + # deps.set_deps( :client_pkg, nil ) + # + # ## :system is the category of :client_pkg + # deps.set_deps( :system, :client_pkg ) + # + # ## :index_html depends on :client_pkg + # deps.set_deps( :index_html, :client_pkg ) + # + # ## :system is the category of :index_html + # deps.set_deps( :system, :index_html ) + # + # ## :main depends on :index_html + # deps.set_deps( :main, :index_html ) + # + # ## :system is the category of :main + # deps.set_deps( :system, :main ) + # + # ## :impossible has several dependencies, of which :foo3 can't be satisfied + # deps.set_deps( :impossible, [:foo1, :foo2, :foo3] ) + # + # ## :ticket has no dependencies + # deps.set_deps( :ticket, nil ) + # + # ## :system is the category of :ticket + # deps.set_deps( :system, :ticket ) + # + # ## :welcome depends on the :system category + # deps.set_deps( :welcome, :system ) + # + # ## :first doesn't depend on anything + # deps.set_deps( :first, nil ) + # + # ## Prepending is handled like this: + # deps.set_deps( :client_pkg, :first ) + # deps.set_deps( :ticket, :first ) + # + # ## Calculates the list of dependencies and returns them in an Array + # p deps.list + # + # ## Output of the example above: + # # impossible dependencies: + # # :impossible => [:foo3] + # # [:foo1, :foo2, :first, :ticket, :client_pkg, :index_html, :main, :system, :welcome] class Dependencies - def initialize( deps = [] ) - @deps = deps + + # Don't use Dependencies for external projects yet. It's subject to change + # without deprecation warnings. + # +resolved+ and +categories+ are optional. + def initialize( resolved = [], categories = {} ) + @pre_resolved = resolved.clone @depends_on = { # :name => [ :dep1, :dep2, :dep3, ... ] } - # ...additional initialization? + @dependencies_of = { + #:dep1 => [ :name1, :name2, ... ] + } + @categories = [] + categories.each_key do |cat_name, cat_items| + add_category( cat_name ) + set_deps( cat_name, nil ) + end + @unresolved = [] + recalculate! end + + # Returns true, if +name+ is a pre-resolved dependency. + def pre_resolved?( name ) + @pre_resolved.include?( name ) + end + + # Returns true, if +name+ is not a pre-resolved dependency and not a category. + def loadable?( name ) + return (not pre_resolved?(name) and not category?(name)) + end + + # Returns list of items that are dependencies of +name+ in + # no particular order. + def deps_of( name ) + outp = [] + if @dependencies_of.has_key?(name) + @dependencies_of[name].each do |dep| + outp.push( dep ) + outp += deps_of( dep ) + end + end + outp.uniq! + outp.delete( name ) + return outp + end + alias dependencies_of deps_of + + # Returns list of items that +name+ depends on in no particular order. + def deps_on( name ) + outp = [] + if @depends_on.has_key?(name) + @depends_on[name].each do |dep| + outp.push( dep ) + outp += deps_on( dep ) + end + end + outp.uniq! + outp.delete( name ) + return outp + end + alias depends_on deps_on + + # List of categories + attr_reader :categories + + # Adds the gategory +name+. + def add_category( name ) + @categories.push( name ) unless @categories.include?( name ) + end + + # Deletes the category +name+, but doesn't dissolve its dependencies. + # Essentially turns +name+ into a regular item. + def del_category( name ) + @categories.delete( name ) if @categories.include?( name ) + end + + # Returns true, if +name+ is a category. + def category?( name ) + @categories.include?( name ) + end + + # Set list of dependency names as +deps+ that +name+ depends on def set_deps( name, deps ) + if deps.class == Symbol + deps = [deps] + elsif deps.class == String + deps = [deps.to_sym] + elsif deps.class == NilClass + deps = [] + elsif deps.class != Array + raise "Dependencies.set_deps error: the deps is an unsupported type: #{deps.class}" + end if not @depends_on.has_key?(name) @depends_on[name] = deps else - @depends_on[name].join( deps ) + deps.each do |dep| + @depends_on[name].push( dep ) unless @depends_on[name].include?( dep ) + end end - # ...additional checks? + deps.each do |dep| + if not @dependencies_of.has_key?( dep ) + @dependencies_of[dep] = [name] + elsif not @dependencies_of[dep].include?( name ) + @dependencies_of[dep].push( name ) + end + end + recalculate! end alias set_dependencies set_deps - def deps - # calculate @depends_on and push items with - # resolved dependencies into @deps - # ... - return @deps + alias set_dependency set_deps + + # Returns copy of the @depends_on Array + def clone_depends_on + Marshal.load( Marshal.dump( @depends_on ) ) end - alias deps load_order + + # Returns clone of the @dependencies_of Array + def clone_dependencies_of + Marshal.load( Marshal.dump( @dependencies_of ) ) + end + + # Remove +name+ from dependency system, throws error if it's depended on another item. + def del_item( name ) + d_of = deps_of( name ) + if not d_of.empty? + d_of.each do |dep| + if category?( dep ) + @depends_on[ dep ].delete( name ) + else + throw "Dependencies.del_item error: the following items depend on #{name.inspect} -> #{deps_of(name).inspect}" + end + end + end + unless category?( name ) + deps_on( name ).each do |dep| + @dependencies_of[dep].delete(name) if @dependencies_of.has_key?(dep) + end + @depends_on.delete( name ) + @dependencies_of.delete( name ) + end + recalculate! + end + + # Returns the order of deletion possible without breaking the dependency chain + def del_order( name ) + d_order = [] + d_of = deps_of( name ) + list.reverse.each do |dep| + if d_of.include?( dep ) + d_order.push( dep ) + end + end + d_order.push( name ) + return d_order + end + + # Deletes +name+ and all dependencies of +name+ + def del_deps( name ) + del_order( name ).each do |del_name| + del_item( del_name ) + end + end + + # Returns true, if +name+ is unresolved. The list of unresolved items is + # updated every time self#recalculate is called. + def unresolved?( name ) + @unresolved.include?( name ) + end + + # Returns true, if +name+ is resolved (a valid dependency). Inverse of self#unresolved? + def resolved?( name ) + (not unresolved?( name )) + end + + # Recalculates the list and unresolved items. + def recalculate! + unresolved = [] + resolved = @pre_resolved.clone + same_len = false + depends_on = clone_depends_on + target_len = depends_on.keys.length + resolved.length + until resolved.length == target_len + len = resolved.length + depends_on.each do | name, deps | + if deps.empty? and not resolved.include?(name) + resolved.push( name ) + elsif deps.empty? + next + else + deps.each do |dep| + deps.delete(dep) if resolved.include?(dep) + end + end + end + if len == resolved.length + if same_len + warn "impossible dependencies:" if RSence.args[:debug] + (depends_on.keys - resolved).each do |unsatisfied| + warn " #{unsatisfied.inspect} => #{depends_on[unsatisfied].inspect}" if RSence.args[:debug] + unresolved.push( unsatisfied ) + end + break + else + same_len = true + end + else + same_len = false + end + end + @unresolved = unresolved + @resolved = resolved + end + + # Returns dependencies sorted in load order + def list + @resolved.clone + end + alias load_order list end end