From 472d4182c17efd6faa9e373fe7d0f14790eeed9e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 3 Feb 2013 10:15:46 -0800 Subject: [PATCH] Basic logic behind prune action is good. --- plugins/commands/plugin/action.rb | 2 + plugins/commands/plugin/action/prune_gems.rb | 131 +++++++++++++++++++ plugins/commands/plugin/gem_helper.rb | 12 +- 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 plugins/commands/plugin/action/prune_gems.rb diff --git a/plugins/commands/plugin/action.rb b/plugins/commands/plugin/action.rb index 6a0fee7f3..2c6522e29 100644 --- a/plugins/commands/plugin/action.rb +++ b/plugins/commands/plugin/action.rb @@ -10,6 +10,7 @@ module VagrantPlugins Vagrant::Action::Builder.new.tap do |b| b.use BundlerCheck b.use InstallGem + b.use PruneGems end end @@ -26,6 +27,7 @@ module VagrantPlugins autoload :BundlerCheck, action_root.join("bundler_check") autoload :InstallGem, action_root.join("install_gem") autoload :ListPlugins, action_root.join("list_plugins") + autoload :PruneGems, action_root.join("prune_gems") end end end diff --git a/plugins/commands/plugin/action/prune_gems.rb b/plugins/commands/plugin/action/prune_gems.rb new file mode 100644 index 000000000..e06511464 --- /dev/null +++ b/plugins/commands/plugin/action/prune_gems.rb @@ -0,0 +1,131 @@ +require "rubygems" +require "set" + +require "log4r" + +module VagrantPlugins + module CommandPlugin + module Action + # This class prunes any unnecessary gems from the Vagrant-managed + # gem folder. This keeps the gem folder to the absolute minimum set + # of required gems and doesn't let it blow up out of control. + # + # A high-level description of how this works: + # + # 1. Get the list of installed plugins. Vagrant maintains this + # list on its own. + # 2. Get the list of installed RubyGems. + # 3. Find the latest version of each RubyGem that matches an installed + # plugin. These are our root RubyGems that must be installed. + # 4. Go through each root and mark all dependencies recursively as + # necessary. + # 5. Set subtraction between all gems and necessary gems yields a + # list of gems that aren't needed. Uninstall them. + # + class PruneGems + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::prune") + end + + def call(env) + @logger.info("Pruning gems...") + + # Get the list of installed plugins according to the state file + installed = Set.new(env[:plugin_state_file].installed_plugins) + + # Get the actual specifications of installed gems + all_specs = env[:gem_helper].with_environment do + result = [] + Gem::Specification.find_all { |s| result << s } + result + end + + # The list of specs to prune initially starts out as all of them + all_specs = Set.new(all_specs) + + # Go through each spec and find the latest version of the installed + # gems, since we want to keep those. + installed_specs = {} + + @logger.debug("Collecting installed plugin gems...") + all_specs.each do |spec| + # If this isn't a spec that we claim is installed, skip it + next if !installed.include?(spec.name) + + # If it is already in the specs, then we need to make sure we + # have the latest version. + if installed_specs.has_key?(spec.name) + if installed_specs[spec.name].version > spec.version + next + end + end + + @logger.debug(" -- #{spec.name} (#{spec.version})") + installed_specs[spec.name] = spec + end + + # Recursive dependency checker to keep all dependencies and remove + # all non-crucial gems from the prune list. + good_specs = Set.new + to_check = installed_specs.values + + while true + # If we're out of gems to check then we break out + break if to_check.empty? + + # Get a random (first) element to check + spec = to_check.shift + + # If we already checked this, then do the next one + next if good_specs.include?(spec) + + # Find all the dependencies and add the latest compliant gem + # to the `to_check` list. + if spec.dependencies.length > 0 + @logger.debug("Finding dependencies for '#{spec.name}' to mark as good...") + spec.dependencies.each do |dep| + # Ignore non-runtime dependencies + next if dep.type != :runtime + @logger.debug("Searching for: '#{dep.name}'") + + latest_matching = nil + + all_specs.each do |prune_spec| + if dep =~ prune_spec + # If we have a matching one already and this one isn't newer + # then we ditch it. + next if latest_matching && + prune_spec.version <= latest_matching.version + + latest_matching = prune_spec + end + end + + if latest_matching.nil? + @logger.error("Missing dependency for '#{spec.name}': #{dep.name}") + next + end + + @logger.debug("Latest matching dep: '#{latest_matching.name}' (#{latest_matching.version})") + to_check << latest_matching + end + end + + # Add ito the list of checked things so we don't accidentally + # re-check it + good_specs.add(spec) + end + + # Figure out the gems we need to prune + prune_specs = all_specs - good_specs + @logger.info("Gems to prune: #{prune_specs.inspect}") + + # TODO: Prune + + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/plugin/gem_helper.rb b/plugins/commands/plugin/gem_helper.rb index ff1eec780..d91be4442 100644 --- a/plugins/commands/plugin/gem_helper.rb +++ b/plugins/commands/plugin/gem_helper.rb @@ -49,16 +49,22 @@ module VagrantPlugins # path. def with_environment old_gem_home = ENV["GEM_HOME"] + old_gem_path = ENV["GEM_PATH"] ENV["GEM_HOME"] = @gem_home - @logger.debug("Set GEM_HOME to: #{ENV["GEM_HOME"]}") + ENV["GEM_PATH"] = @gem_home + @logger.debug("Set GEM_* to: #{ENV["GEM_HOME"]}") # Clear paths so that it reads the new GEM_HOME setting - Gem.clear_paths + Gem.paths = ENV return yield ensure - # Restore the old GEM_HOME + # Restore the old GEM_* settings ENV["GEM_HOME"] = old_gem_home + ENV["GEM_PATH"] = old_gem_path + + # Reset everything + Gem.paths = ENV end end end