2014-01-06 17:27:37 +00:00
|
|
|
require "monitor"
|
2014-01-05 22:54:50 +00:00
|
|
|
require "pathname"
|
2014-01-06 07:13:49 +00:00
|
|
|
require "set"
|
2014-01-05 00:15:12 +00:00
|
|
|
require "tempfile"
|
2016-05-27 23:09:38 +00:00
|
|
|
require "fileutils"
|
2014-01-05 00:15:12 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
require "rubygems/package"
|
|
|
|
require "rubygems/uninstaller"
|
2014-01-06 00:16:04 +00:00
|
|
|
|
2014-01-05 00:29:22 +00:00
|
|
|
require_relative "shared_helpers"
|
2014-01-05 00:15:12 +00:00
|
|
|
require_relative "version"
|
2015-11-23 17:39:51 +00:00
|
|
|
require_relative "util/safe_env"
|
2014-01-05 00:15:12 +00:00
|
|
|
|
|
|
|
module Vagrant
|
|
|
|
# This class manages Vagrant's interaction with Bundler. Vagrant uses
|
|
|
|
# Bundler as a way to properly resolve all dependencies of Vagrant and
|
|
|
|
# all Vagrant-installed plugins.
|
|
|
|
class Bundler
|
2016-09-06 01:12:05 +00:00
|
|
|
|
|
|
|
HASHICORP_GEMSTORE = 'https://gems.hashicorp.com'.freeze
|
|
|
|
|
2014-01-05 00:15:12 +00:00
|
|
|
def self.instance
|
|
|
|
@bundler ||= self.new
|
|
|
|
end
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
attr_reader :plugin_gem_path
|
|
|
|
|
2014-01-05 22:54:50 +00:00
|
|
|
def initialize
|
2016-09-06 01:12:05 +00:00
|
|
|
@plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger = Log4r::Logger.new("vagrant::bundler")
|
2014-08-29 15:20:21 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Initializes Bundler and the various gem paths so that we can begin
|
|
|
|
# loading gems. This must only be called once.
|
2016-11-02 17:36:44 +00:00
|
|
|
def init!(plugins, repair=false)
|
2016-09-06 01:12:05 +00:00
|
|
|
# Add HashiCorp RubyGems source
|
|
|
|
Gem.sources << HASHICORP_GEMSTORE
|
|
|
|
|
|
|
|
# Generate dependencies for all registered plugins
|
|
|
|
plugin_deps = plugins.map do |name, info|
|
|
|
|
Gem::Dependency.new(name, info['gem_version'].to_s.empty? ? '> 0' : info['gem_version'])
|
|
|
|
end
|
|
|
|
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Current generated plugin dependency list: #{plugin_deps}")
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Load dependencies into a request set for resolution
|
|
|
|
request_set = Gem::RequestSet.new(*plugin_deps)
|
|
|
|
# Never allow dependencies to be remotely satisfied during init
|
|
|
|
request_set.remote = false
|
|
|
|
|
2016-11-02 17:36:44 +00:00
|
|
|
repair_result = nil
|
|
|
|
begin
|
|
|
|
# Compose set for resolution
|
2016-11-16 21:20:32 +00:00
|
|
|
composed_set = generate_vagrant_set
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Composed local RubyGems set for plugin init resolution: #{composed_set}")
|
2016-11-02 17:36:44 +00:00
|
|
|
# Resolve the request set to ensure proper activation order
|
|
|
|
solution = request_set.resolve(composed_set)
|
|
|
|
rescue Gem::UnsatisfiableDependencyError => failure
|
|
|
|
if repair
|
|
|
|
raise failure if @init_retried
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Resolution failed but attempting to repair. Failure: #{failure}")
|
2016-11-02 17:36:44 +00:00
|
|
|
install(plugins)
|
|
|
|
@init_retried = true
|
|
|
|
retry
|
|
|
|
else
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("#{failure.class}: #{failure}")
|
2016-11-02 17:36:44 +00:00
|
|
|
$stderr.puts "Vagrant failed to properly initialize due to an error while"
|
|
|
|
$stderr.puts "while attempting to load configured plugins. This can be caused"
|
|
|
|
$stderr.puts "by manually tampering with the 'plugins.json' file, or by a"
|
|
|
|
$stderr.puts "recent Vagrant upgrade. To fix this problem, please run:\n\n"
|
|
|
|
$stderr.puts " vagrant plugin repair\n\n"
|
|
|
|
$stderr.puts "The error message is shown below:\n\n"
|
|
|
|
$stderr.puts failure.message
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
end
|
2014-02-12 22:24:17 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Activate the gems
|
2016-11-16 21:20:32 +00:00
|
|
|
activate_solution(solution)
|
2014-02-12 22:24:17 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
full_vagrant_spec_list = Gem::Specification.find_all{true} +
|
|
|
|
solution.map(&:full_spec)
|
|
|
|
|
|
|
|
if(defined?(::Bundler))
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Updating Bundler with full specification list")
|
2016-09-06 01:12:05 +00:00
|
|
|
::Bundler.rubygems.replace_entrypoints(full_vagrant_spec_list)
|
2015-11-23 17:39:51 +00:00
|
|
|
end
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
Gem.post_reset do
|
|
|
|
Gem::Specification.all = full_vagrant_spec_list
|
|
|
|
end
|
2014-01-05 22:54:50 +00:00
|
|
|
end
|
|
|
|
|
2014-08-08 23:47:37 +00:00
|
|
|
# Removes any temporary files created by init
|
|
|
|
def deinit
|
2016-09-06 01:12:05 +00:00
|
|
|
# no-op
|
2014-08-08 23:47:37 +00:00
|
|
|
end
|
|
|
|
|
2014-01-05 22:54:50 +00:00
|
|
|
# Installs the list of plugins.
|
|
|
|
#
|
2014-01-06 06:33:05 +00:00
|
|
|
# @param [Hash] plugins
|
2014-01-05 22:54:50 +00:00
|
|
|
# @return [Array<Gem::Specification>]
|
2014-01-06 18:52:14 +00:00
|
|
|
def install(plugins, local=false)
|
|
|
|
internal_install(plugins, nil, local: local)
|
2014-01-06 06:33:05 +00:00
|
|
|
end
|
2014-01-06 01:04:50 +00:00
|
|
|
|
2014-01-06 17:27:37 +00:00
|
|
|
# Installs a local '*.gem' file so that Bundler can find it.
|
|
|
|
#
|
|
|
|
# @param [String] path Path to a local gem file.
|
|
|
|
# @return [Gem::Specification]
|
2016-11-11 22:50:20 +00:00
|
|
|
def install_local(path, opts={})
|
2016-11-11 22:25:03 +00:00
|
|
|
plugin_source = Gem::Source::SpecificFile.new(path)
|
2016-09-06 01:12:05 +00:00
|
|
|
plugin_info = {
|
2016-11-11 22:25:03 +00:00
|
|
|
plugin_source.spec.name => {
|
2016-11-11 22:50:20 +00:00
|
|
|
"local_source" => plugin_source,
|
|
|
|
"sources" => opts.fetch(:sources, Gem.sources.map(&:to_s))
|
2016-09-06 01:12:05 +00:00
|
|
|
}
|
|
|
|
}
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Installing local plugin - #{plugin_info}")
|
2016-11-11 22:50:20 +00:00
|
|
|
internal_install(plugin_info, {})
|
2016-11-11 22:25:03 +00:00
|
|
|
plugin_source.spec
|
2014-01-06 17:27:37 +00:00
|
|
|
end
|
|
|
|
|
2014-01-06 06:33:05 +00:00
|
|
|
# Update updates the given plugins, or every plugin if none is given.
|
|
|
|
#
|
|
|
|
# @param [Hash] plugins
|
|
|
|
# @param [Array<String>] specific Specific plugin names to update. If
|
|
|
|
# empty or nil, all plugins will be updated.
|
|
|
|
def update(plugins, specific)
|
|
|
|
specific ||= []
|
2016-11-16 21:20:32 +00:00
|
|
|
update = {gems: specific.empty? ? true : specific}
|
2014-01-06 06:33:05 +00:00
|
|
|
internal_install(plugins, update)
|
2014-01-05 22:54:50 +00:00
|
|
|
end
|
|
|
|
|
2014-01-06 00:02:00 +00:00
|
|
|
# Clean removes any unused gems.
|
|
|
|
def clean(plugins)
|
2016-09-06 01:12:05 +00:00
|
|
|
# Generate dependencies for all registered plugins
|
|
|
|
plugin_deps = plugins.map do |name, info|
|
2016-11-16 21:20:32 +00:00
|
|
|
gem_version = info['installed_gem_version']
|
|
|
|
gem_version = info['gem_version'] if gem_version.to_s.empty?
|
|
|
|
gem_version = "> 0" if gem_version.to_s.empty?
|
|
|
|
Gem::Dependency.new(name, gem_version)
|
2014-01-06 01:04:50 +00:00
|
|
|
end
|
2014-01-06 00:02:00 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Load dependencies into a request set for resolution
|
|
|
|
request_set = Gem::RequestSet.new(*plugin_deps)
|
|
|
|
# Never allow dependencies to be remotely satisfied during cleaning
|
|
|
|
request_set.remote = false
|
2014-01-06 17:27:37 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Sets that we can resolve our dependencies from. Note that we only
|
|
|
|
# resolve from the current set as all required deps are activated during
|
|
|
|
# init.
|
2016-11-16 21:20:32 +00:00
|
|
|
current_set = generate_vagrant_set
|
2014-01-06 06:33:05 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Collect all plugin specifications
|
|
|
|
plugin_specs = Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path|
|
|
|
|
Gem::Specification.load(spec_path)
|
2014-01-05 22:54:50 +00:00
|
|
|
end
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Resolve the request set to ensure proper activation order
|
|
|
|
solution = request_set.resolve(current_set)
|
|
|
|
solution_specs = solution.map(&:full_spec)
|
2016-11-16 21:20:32 +00:00
|
|
|
solution_full_names = solution_specs.map(&:full_name)
|
2014-01-06 06:33:05 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Find all specs installed to plugins directory that are not
|
|
|
|
# found within the solution set
|
|
|
|
plugin_specs.delete_if do |spec|
|
2016-11-16 21:20:32 +00:00
|
|
|
solution_full_names.include?(spec.full_name)
|
2014-08-29 15:20:21 +00:00
|
|
|
end
|
|
|
|
|
2016-11-16 21:20:32 +00:00
|
|
|
@logger.debug("Specifications to be removed - #{plugin_specs.map(&:full_name)}")
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Now delete all unused specs
|
|
|
|
plugin_specs.each do |spec|
|
2016-11-16 21:20:32 +00:00
|
|
|
@logger.debug("Uninstalling gem - #{spec.full_name}")
|
2016-09-06 01:12:05 +00:00
|
|
|
Gem::Uninstaller.new(spec.name,
|
|
|
|
version: spec.version,
|
|
|
|
install_dir: plugin_gem_path,
|
|
|
|
ignore: true
|
|
|
|
).uninstall_gem(spec)
|
2014-01-06 01:12:01 +00:00
|
|
|
end
|
2014-01-05 22:54:50 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
solution.find_all do |spec|
|
|
|
|
plugins.keys.include?(spec.name)
|
2014-01-05 22:54:50 +00:00
|
|
|
end
|
2014-01-05 00:15:12 +00:00
|
|
|
end
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# During the duration of the yielded block, Bundler loud output
|
|
|
|
# is enabled.
|
|
|
|
def verbose
|
|
|
|
if block_given?
|
|
|
|
initial_state = @verbose
|
|
|
|
@verbose = true
|
|
|
|
yield
|
|
|
|
@verbose = initial_state
|
|
|
|
else
|
|
|
|
@verbose = true
|
2016-06-20 22:19:21 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
protected
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
def internal_install(plugins, update, **extra)
|
2016-11-11 22:50:20 +00:00
|
|
|
# Only allow defined Gem sources
|
|
|
|
Gem.sources.clear
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-11-16 21:20:32 +00:00
|
|
|
update = {} if !update.is_a?(Hash)
|
|
|
|
skips = []
|
2016-11-11 22:25:03 +00:00
|
|
|
installer_set = Gem::Resolver::InstallerSet.new(:both)
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Generate all required plugin deps
|
|
|
|
plugin_deps = plugins.map do |name, info|
|
2016-11-16 21:20:32 +00:00
|
|
|
if update[:gems] == true || (update[:gems].respond_to?(:include?) && update[:gems].include?(name))
|
2016-09-06 01:12:05 +00:00
|
|
|
gem_version = '> 0'
|
2016-11-16 21:20:32 +00:00
|
|
|
skips << name
|
2016-09-06 01:12:05 +00:00
|
|
|
else
|
|
|
|
gem_version = info['gem_version'].to_s.empty? ? '> 0' : info['gem_version']
|
2014-08-29 15:20:21 +00:00
|
|
|
end
|
2016-11-11 22:25:03 +00:00
|
|
|
if plugin_source = info.delete("local_source")
|
|
|
|
installer_set.add_local(plugin_source.spec.name, plugin_source.spec, plugin_source)
|
|
|
|
end
|
2016-11-11 22:33:26 +00:00
|
|
|
Array(info["sources"]).each do |source|
|
|
|
|
if !Gem.sources.include?(source)
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Adding RubyGems source for plugin install: #{source}")
|
2016-11-11 22:33:26 +00:00
|
|
|
Gem.sources << source
|
|
|
|
end
|
|
|
|
end
|
2016-09-06 01:12:05 +00:00
|
|
|
Gem::Dependency.new(name, gem_version)
|
|
|
|
end
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-11-11 23:08:59 +00:00
|
|
|
@logger.debug("Dependency list for installation: #{plugin_deps}")
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Create the request set for the new plugins
|
|
|
|
request_set = Gem::RequestSet.new(*plugin_deps)
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Generate all existing deps within the "vagrant system"
|
|
|
|
existing_deps = Gem::Specification.find_all{true}.map do |item|
|
|
|
|
Gem::Dependency.new(item.name, item.version)
|
|
|
|
end
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Import constraints into the request set to prevent installing
|
|
|
|
# gems that are incompatible with the core system
|
|
|
|
request_set.import(existing_deps)
|
2014-08-29 15:20:21 +00:00
|
|
|
|
2016-11-16 21:20:32 +00:00
|
|
|
installer_set = Gem::Resolver.compose_sets(installer_set, generate_plugin_set(skips))
|
|
|
|
|
2016-09-06 01:12:05 +00:00
|
|
|
# Generate the required solution set for new plugins
|
2016-11-11 22:25:03 +00:00
|
|
|
solution = request_set.resolve(installer_set)
|
2016-11-16 21:20:32 +00:00
|
|
|
activate_solution(solution)
|
2016-09-06 01:12:05 +00:00
|
|
|
|
|
|
|
# Install all remote gems into plugin path. Set the installer to ignore dependencies
|
|
|
|
# as we know the dependencies are satisfied and it will attempt to validate a gem's
|
|
|
|
# dependencies are satisified by gems in the install directory (which will likely not
|
|
|
|
# be true)
|
|
|
|
result = request_set.install_into(plugin_gem_path.to_s, true, ignore_dependencies: true)
|
2016-11-16 21:20:32 +00:00
|
|
|
result = result.map(&:full_spec)
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
# Generate the composite resolver set totally all of vagrant (builtin + plugin set)
|
|
|
|
def generate_vagrant_set
|
|
|
|
Gem::Resolver.compose_sets(generate_builtin_set, generate_plugin_set)
|
2014-08-29 15:20:21 +00:00
|
|
|
end
|
2016-09-06 01:12:05 +00:00
|
|
|
|
2016-11-16 21:20:32 +00:00
|
|
|
# Generate the builtin resolver set
|
|
|
|
def generate_builtin_set
|
|
|
|
Gem::Resolver::CurrentSet.new
|
|
|
|
end
|
|
|
|
|
|
|
|
# Generate the plugin resolver set. Optionally provide specification names (short or
|
|
|
|
# full) that should be ignored
|
|
|
|
def generate_plugin_set(skip=[])
|
|
|
|
plugin_set = PluginSet.new
|
|
|
|
@logger.debug("Generating new plugin set instance. Skip gems - #{skip}")
|
|
|
|
Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).each do |spec_path|
|
|
|
|
spec = Gem::Specification.load(spec_path)
|
|
|
|
desired_spec_path = File.join(spec.gem_dir, "#{spec.name}.gemspec")
|
|
|
|
# Vendor set requires the spec to be within the gem directory. Some gems will package their
|
|
|
|
# spec file, and that's not what we want to load.
|
|
|
|
if !File.exist?(desired_spec_path) || !FileUtils.cmp(spec.spec_file, desired_spec_path)
|
|
|
|
File.write(desired_spec_path, spec.to_ruby)
|
|
|
|
end
|
|
|
|
next if skip.include?(spec.name) || skip.include?(spec.full_name)
|
|
|
|
plugin_set.add_vendor_gem(spec.name, spec.gem_dir)
|
|
|
|
end
|
|
|
|
plugin_set
|
|
|
|
end
|
|
|
|
|
|
|
|
# Activate a given solution
|
|
|
|
def activate_solution(solution)
|
|
|
|
retried = false
|
|
|
|
begin
|
|
|
|
@logger.debug("Activating solution set: #{solution.map(&:full_name)}")
|
|
|
|
solution.each do |activation_request|
|
|
|
|
unless activation_request.full_spec.activated?
|
|
|
|
@logger.debug("Activating gem #{activation_request.full_spec.full_name}")
|
|
|
|
activation_request.full_spec.activate
|
|
|
|
if(defined?(::Bundler))
|
|
|
|
@logger.debug("Marking gem #{activation_request.full_spec.full_name} loaded within Bundler.")
|
|
|
|
::Bundler.rubygems.mark_loaded activation_request.full_spec
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue Gem::LoadError => e
|
|
|
|
# Depending on the version of Ruby, the ordering of the solution set
|
|
|
|
# will be either 0..n (molinillo) or n..0 (pre-molinillo). Instead of
|
|
|
|
# attempting to determine what's in use, or if it has some how changed
|
|
|
|
# again, just reverse order on failure and attempt again.
|
|
|
|
if retried
|
|
|
|
@logger.error("Failed to load solution set - #{e.class}: #{e}")
|
|
|
|
raise
|
|
|
|
else
|
|
|
|
@logger.debug("Failed to load solution set. Retrying with reverse order.")
|
|
|
|
retried = true
|
|
|
|
solution.reverse!
|
|
|
|
retry
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# This is a custom Gem::Resolver::Set for use with Vagrant plugins. It is
|
|
|
|
# a modified Gem::Resolver::VendorSet that supports multiple versions of
|
|
|
|
# a specific gem
|
|
|
|
class PluginSet < Gem::Resolver::VendorSet
|
|
|
|
##
|
|
|
|
# Adds a specification to the set with the given +name+ which has been
|
|
|
|
# unpacked into the given +directory+.
|
|
|
|
def add_vendor_gem(name, directory)
|
|
|
|
gemspec = File.join(directory, "#{name}.gemspec")
|
|
|
|
spec = Gem::Specification.load(gemspec)
|
|
|
|
if !spec
|
|
|
|
raise Gem::GemNotFoundException,
|
|
|
|
"unable to find #{gemspec} for gem #{name}"
|
|
|
|
end
|
|
|
|
|
|
|
|
spec.full_gem_path = File.expand_path(directory)
|
|
|
|
|
|
|
|
@specs[spec.name] ||= []
|
|
|
|
@specs[spec.name] << spec
|
|
|
|
@directories[spec] = directory
|
|
|
|
|
|
|
|
spec
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Returns an Array of VendorSpecification objects matching the
|
|
|
|
# DependencyRequest +req+.
|
|
|
|
def find_all(req)
|
|
|
|
@specs.values.flatten.select do |spec|
|
|
|
|
req.match?(spec)
|
|
|
|
end.map do |spec|
|
|
|
|
source = Gem::Source::Vendor.new(@directories[spec])
|
|
|
|
Gem::Resolver::VendorSpecification.new(self, spec, source)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Loads a spec with the given +name+. +version+, +platform+ and +source+ are
|
|
|
|
# ignored.
|
|
|
|
def load_spec (name, version, platform, source)
|
|
|
|
version = Gem::Version.new(version) if !version.is_a?(Gem::Version)
|
|
|
|
@specs.fetch(name, []).detect{|s| s.name == name && s.version = version}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Patch for Ruby 2.2 and Bundler to behave properly when uninstalling plugins
|
|
|
|
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
|
|
|
|
if defined?(::Bundler) && !::Bundler::SpecSet.instance_methods.include?(:delete)
|
|
|
|
class Gem::Specification
|
|
|
|
def self.remove_spec(spec)
|
|
|
|
Gem::Specification.reset
|
|
|
|
end
|
|
|
|
end
|
2014-01-05 00:15:12 +00:00
|
|
|
end
|
|
|
|
end
|