lib/plugin_manager.rb in plugin_manager-1.1 vs lib/plugin_manager.rb in plugin_manager-1.2

- old
+ new

@@ -4,79 +4,203 @@ require 'plugin_manager/plugin' require 'plugin_manager/plugin_definition' require 'plugin_manager/definition_builder' class PluginManager - attr_reader :unreadable_definitions, :plugins_with_errors, :loaded_plugins, :unloaded_plugins + attr_reader :unreadable_definitions, :plugins_with_errors, :loaded_plugins, :unloaded_plugins, :output - def initialize - @unloaded_plugins = [] - @loaded_plugins = [] + class << self + attr_accessor :current + end + + def initialize(output = STDOUT) + @plugins = [] + @unloaded_plugins = [] + @loaded_plugins = [] @unreadable_definitions = [] - @plugins_with_errors = [] + @plugins_with_errors = [] + @output = output end + def on_load(&block) + @load_observer = block + end + def plugins - @unloaded_plugins + @loaded_plugins + @plugins end + + def plugin_objects + @loaded_plugins.map {|definition| definition.object} + end + + def objects_implementing(method_name) + plugin_objects.select {|obj| obj.respond_to?(method_name) } + end + class Dependency + attr_reader :required_name + attr_reader :required_version + + def initialize(plugin_manager, required_name, required_version) + @plugin_manager = plugin_manager + @required_name = required_name + @required_version = required_version + end + + def satisfied? + if loaded_plugin = @plugin_manager.loaded_plugins.detect {|pl| pl.name == required_name } + PluginManager.compare_version(required_version, loaded_plugin.version) + end + end + + def inspect + "dep(#{required_name} #{required_version})" + end + end + + def add_gem_plugin_source + all_gem_names = Gem::SourceIndex.from_installed_gems.map {|n, _| n} + redcar_plugin_gem_names = all_gem_names.select {|n| n =~ /^redcar-/} + + definition_files = redcar_plugin_gem_names.map do |gem_name| + [gem_name, ENV["GEM_HOME"] + "/gems/" + gem_name + "/plugin.rb"] + end + + definition_files = definition_files.select do |name, definition_file| + File.exist?(definition_file) + end + + if definition_files.any? + gem_names = definition_files.map {|n, _| n } + @output.puts "[PluginManager] found gem plugins #{gem_names.inspect}" if ENV["PLUGIN_DEBUG"] + end + + + add_definition_files(definition_files.map {|_, f| f}) + end + def add_plugin_source(directory) definition_files = Dir[File.join(File.expand_path(directory), "*", "plugin.rb")] definition_files.reject! {|f| plugins.any? {|pl| pl.definition_file == File.expand_path(f) } } - new_definitions = - definition_files.map do |file| - begin - definition = instance_eval(File.read(file)) - definition.definition_file = File.expand_path(file) - definition - rescue Object => e - puts "Unreadable plugin definition: #{file}" - puts " " + e.message - puts e.backtrace.map {|l| " " + l } - @unreadable_definitions << file - nil + + add_definition_files(definition_files) + end + + def add_definition_files(definition_files) + definition_files.each do |file| + begin + PluginManager.current = self + definition = instance_eval(File.read(file)) + PluginManager.current = nil + definition.definition_file = File.expand_path(file) + if already_with_that_name = @plugins.detect {|pl| pl.name == definition.name } + if already_with_that_name.version.to_f < definition.version.to_f + @unloaded_plugins.delete(already_with_that_name) + @plugins.delete(already_with_that_name) + @unloaded_plugins << definition + @plugins << definition + end + else + @unloaded_plugins << definition + @plugins << definition end - end.compact.sort_by {|p| p.name.downcase } - @unloaded_plugins += new_definitions + rescue Object => e + @output.puts "Unreadable plugin definition: #{file}" + @output.puts " " + e.message + @output.puts e.backtrace.map {|l| " " + l } + @unreadable_definitions << file + nil + end + end + + @plugins = @plugins.sort_by {|pl| pl.name } + @unloaded_plugins = @unloaded_plugins.sort_by {|pl| pl.name } end - def load - previous_length = @unloaded_plugins.length + 1 - while previous_length > @unloaded_plugins.length - previous_length = @unloaded_plugins.length - if plugin = next_to_load - begin - puts "[PluginManager] loading #{plugin.name}" if ENV["PLUGIN_DEBUG"] - plugin.load - @loaded_plugins << plugin - rescue Object => e - puts "Error loading plugin: #{plugin.inspect}" - puts " " + e.message - puts e.backtrace.map {|l| " " + l } - @plugins_with_errors << plugin + def load(*plugin_names) + if plugin_names.empty? + return load_maximal + else + target_dependencies = plugin_names.map do |n| + unless result = latest_version_by_name(n) + raise "can't find plugin named #{n}" end - @unloaded_plugins.delete(plugin) + Dependency.new(self, n, ">0") end end + remaining_to_load = expand_dependencies(target_dependencies) + while remaining_to_load.length > 0 + previous_length = remaining_to_load.length + if plugin = next_to_load(remaining_to_load) + load_plugin(plugin) + remaining_to_load = remaining_to_load.reject {|dep| dep.required_name == plugin.name } + else + puts "no plugin to load from #{remaining_to_load.inspect}" + return + end + new_length = remaining_to_load.length + end end - private + def latest_version_by_name(name) + @plugins.select {|pl| pl.name == name }.sort_by {|pl| pl.version }.last + end - def next_to_load - # this ordering ensures we try the most recent version of a plugin first - remaining_plugins = @unloaded_plugins.sort_by {|pl| pl.version }.reverse - - remaining_plugins.detect do |d| - next if @loaded_plugins.map {|pl| pl.name }.include?(d.name) - (d.dependencies||[]).all? do |dep| - req_name, req_ver = *dep - @loaded_plugins.detect do |d1| - d1.name == req_name and - PluginManager.compare_version(req_ver, d1.version) + def load_plugin(plugin) + begin + @output.puts "[PluginManager] loading #{plugin.name}" if ENV["PLUGIN_DEBUG"] + plugin.load + if @load_observer + @load_observer.call(plugin) + end + @loaded_plugins << plugin + rescue Object => e + @output.puts "Error loading plugin: #{plugin.inspect}" + @output.puts " " + e.message + @output.puts e.backtrace.map {|l| " " + l } + @plugins_with_errors << plugin + end + @unloaded_plugins.delete(plugin) + end + + def load_maximal + while ready_plugin = @unloaded_plugins.detect {|pl| pl.dependencies.all? {|dep| dep.satisfied? }} + load_plugin(ready_plugin) + end + @load_observer = nil # After loading all possible plugins, remove the load observer + end + + def expand_dependencies(dependency_array) + previous_length = dependency_array.length + new_dependency_array = dependency_array.map do |dep| + if pl = latest_version_by_name(dep.required_name) + [dep, pl.dependencies] + else + dep + end + end.flatten.uniq + if new_dependency_array.length > previous_length + expand_dependencies(new_dependency_array) + else + new_dependency_array + end + end + + def next_to_load(dependency_array) + hash = Hash.new {|h,k| h[k] = []} + dependency_array.each {|dep| hash[dep.required_name] << dep.required_version} + hash.each do |name, version_requirements| + if candidate_for_loading = unloaded_plugins.detect {|pl| pl.name == name} + all_requirements_met = version_requirements.all? do |version_requirement| + PluginManager.compare_version(version_requirement, candidate_for_loading.version) end + all_candidate_deps_met = candidate_for_loading.dependencies.all? {|dep| dep.satisfied?} + return candidate_for_loading if all_requirements_met and all_candidate_deps_met end end + nil end def self.compare_version(required, got) got = got.gsub(/(\.0)+$/, "") required.split(",").all? do |req| @@ -96,10 +220,10 @@ md[2] == got when "!=" md[2] != got end else - puts "don't recognize version string: #{required.inspect}" + @output.puts "don't recognize version string: #{required.inspect}" end end end end