vagrant/plugins/commands/plugin/action/prune_gems.rb

154 lines
5.4 KiB
Ruby

require "rubygems"
require "rubygems/user_interaction"
require "rubygems/uninstaller"
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.debug("Gems to prune: #{prune_specs.inspect}")
@logger.info("Pruning #{prune_specs.length} gems.")
if prune_specs.length > 0
env[:gem_helper].with_environment do
# Due to a bug in rubygems 2.0, we need to load the
# specifications before removing any. This achieves that.
Gem::Specification.to_a
prune_specs.each do |prune_spec|
uninstaller = Gem::Uninstaller.new(prune_spec.name, {
:all => true,
:executables => true,
:force => true,
:ignore => true,
:version => prune_spec.version.version
})
@logger.info("Uninstalling: #{prune_spec.name} (#{prune_spec.version})")
uninstaller.uninstall
end
end
end
@app.call(env)
end
end
end
end
end