Chris Roberts 5195bee9ea Check if plugin install provides specification
During a plugin install, if the plugin is already installed and
activated, no specification will be returned as there was nothing
new installed. In this situation, look for the requested plugin
within the activated specifications. If it is found, then proceed
since the plugin is installed. If it is not found, return an error.
2020-05-06 15:51:37 -07:00

386 lines
13 KiB
Ruby

require "pathname"
require "set"
require_relative "../bundler"
require_relative "../shared_helpers"
require_relative "state_file"
module Vagrant
module Plugin
# The Manager helps with installing, listing, and initializing plugins.
class Manager
# Returns the path to the [StateFile] for user plugins.
#
# @return [Pathname]
def self.user_plugins_file
Vagrant.user_data_path.join("plugins.json")
end
# Returns the path to the [StateFile] for system plugins.
def self.system_plugins_file
dir = Vagrant.installer_embedded_dir
return nil if !dir
Pathname.new(dir).join("plugins.json")
end
def self.instance
@instance ||= self.new(user_plugins_file)
end
attr_reader :user_file
attr_reader :system_file
attr_reader :local_file
# @param [Pathname] user_file
def initialize(user_file)
@logger = Log4r::Logger.new("vagrant::plugin::manager")
@user_file = StateFile.new(user_file)
system_path = self.class.system_plugins_file
@system_file = nil
@system_file = StateFile.new(system_path) if system_path && system_path.file?
@local_file = nil
@globalized = @localized = false
end
# Enable global plugins
#
# @return [Hash] list of plugins
def globalize!
@globalized = true
@logger.debug("Enabling globalized plugins")
plugins = installed_plugins
bundler_init(plugins, global: user_file.path)
plugins
end
# Enable environment local plugins
#
# @param [Environment] env Vagrant environment
# @return [Hash, nil] list of plugins
def localize!(env)
@localized = true
if env.local_data_path
@logger.debug("Enabling localized plugins")
@local_file = StateFile.new(env.local_data_path.join("plugins.json"))
Vagrant::Bundler.instance.environment_path = env.local_data_path
plugins = local_file.installed_plugins
bundler_init(plugins, local: local_file.path)
plugins
end
end
# @return [Boolean] local and global plugins are loaded
def ready?
@globalized && @localized
end
# Initialize bundler with given plugins
#
# @param [Hash] plugins List of plugins
# @return [nil]
def bundler_init(plugins, **opts)
if !Vagrant.plugins_init?
@logger.warn("Plugin initialization is disabled")
return nil
end
@logger.info("Plugins:")
plugins.each do |plugin_name, plugin_info|
installed_version = plugin_info["installed_gem_version"]
version_constraint = plugin_info["gem_version"]
installed_version = 'undefined' if installed_version.to_s.empty?
version_constraint = '> 0' if version_constraint.to_s.empty?
@logger.info(
" - #{plugin_name} = [installed: " \
"#{installed_version} constraint: " \
"#{version_constraint}]"
)
end
begin
Vagrant::Bundler.instance.init!(plugins, **opts)
rescue StandardError, ScriptError => err
@logger.error("Plugin initialization error - #{err.class}: #{err}")
err.backtrace.each do |backtrace_line|
@logger.debug(backtrace_line)
end
raise Vagrant::Errors::PluginInitError, message: err.to_s
end
end
# Installs another plugin into our gem directory.
#
# @param [String] name Name of the plugin (gem)
# @return [Gem::Specification]
def install_plugin(name, **opts)
if opts[:env_local] && @local_file.nil?
raise Errors::PluginNoLocalError
end
if name =~ /\.gem$/
# If this is a gem file, then we install that gem locally.
local_spec = Vagrant::Bundler.instance.install_local(name, opts)
name = local_spec.name
opts[:version] = local_spec.version.to_s
end
plugins = installed_plugins
plugins[name] = {
"require" => opts[:require],
"gem_version" => opts[:version],
"sources" => opts[:sources],
}
if local_spec.nil?
result = nil
install_lambda = lambda do
Vagrant::Bundler.instance.install(plugins, opts[:env_local]).each do |spec|
next if spec.name != name
next if result && result.version >= spec.version
result = spec
end
end
if opts[:verbose]
Vagrant::Bundler.instance.verbose(&install_lambda)
else
install_lambda.call
end
else
result = local_spec
end
if result
# Add the plugin to the state file
plugin_file = opts[:env_local] ? @local_file : @user_file
plugin_file.add_plugin(
result.name,
version: opts[:version],
require: opts[:require],
sources: opts[:sources],
env_local: !!opts[:env_local],
installed_gem_version: result.version.to_s
)
else
r = Gem::Dependency.new(name, opts[:version])
result = Gem::Specification.find { |s|
s.satisfies_requirement?(r) &&
s.activated?
}
raise Errors::PluginInstallFailed,
name: name if result.nil?
@logger.warn("Plugin install returned no result as no new plugins were installed.")
end
# After install clean plugin gems to remove any cruft. This is useful
# for removing outdated dependencies or other versions of an installed
# plugin if the plugin is upgraded/downgraded
Vagrant::Bundler.instance.clean(installed_plugins, local: !!opts[:local])
result
rescue Gem::GemNotFoundException
raise Errors::PluginGemNotFound, name: name
rescue Gem::Exception => e
raise Errors::BundlerError, message: e.to_s
end
# Uninstalls the plugin with the given name.
#
# @param [String] name
def uninstall_plugin(name, **opts)
if @system_file
if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name)
raise Errors::PluginUninstallSystem,
name: name
end
end
if opts[:env_local] && @local_file.nil?
raise Errors::PluginNoLocalError
end
plugin_file = opts[:env_local] ? @local_file : @user_file
if !plugin_file.has_plugin?(name)
raise Errors::PluginNotInstalled,
name: name
end
plugin_file.remove_plugin(name)
# Clean the environment, removing any old plugins
Vagrant::Bundler.instance.clean(installed_plugins)
rescue Gem::Exception => e
raise Errors::BundlerError, message: e.to_s
end
# Updates all or a specific set of plugins.
def update_plugins(specific, **opts)
if opts[:env_local] && @local_file.nil?
raise Errors::PluginNoLocalError
end
plugin_file = opts[:env_local] ? @local_file : @user_file
result = Vagrant::Bundler.instance.update(plugin_file.installed_plugins, specific)
plugin_file.installed_plugins.each do |name, info|
matching_spec = result.detect{|s| s.name == name}
info = Hash[
info.map do |key, value|
[key.to_sym, value]
end
]
if matching_spec
plugin_file.add_plugin(name, **info.merge(
version: "> 0",
installed_gem_version: matching_spec.version.to_s
))
end
end
Vagrant::Bundler.instance.clean(installed_plugins)
result
rescue Gem::Exception => e
raise Errors::BundlerError, message: e.to_s
end
# This returns the list of plugins that should be enabled.
#
# @return [Hash]
def installed_plugins
system = {}
if @system_file
@system_file.installed_plugins.each do |k, v|
system[k] = v.merge("system" => true)
end
end
plugin_list = Util::DeepMerge.deep_merge(system, @user_file.installed_plugins)
if @local_file
plugin_list = Util::DeepMerge.deep_merge(plugin_list,
@local_file.installed_plugins)
end
# Sort plugins by name
Hash[
plugin_list.map{|plugin_name, plugin_info|
[plugin_name, plugin_info]
}.sort_by(&:first)
]
end
# This returns the list of plugins that are installed as
# Gem::Specifications.
#
# @return [Array<Gem::Specification>]
def installed_specs
installed_plugin_info = installed_plugins
installed = Set.new(installed_plugin_info.keys)
installed_versions = Hash[
installed_plugin_info.map{|plugin_name, plugin_info|
gem_version = plugin_info["gem_version"].to_s
gem_version = "> 0" if gem_version.empty?
[plugin_name, Gem::Requirement.new(gem_version)]
}
]
# Go through the plugins installed in this environment and
# get the latest version of each.
installed_map = {}
Gem::Specification.find_all.each do |spec|
# Ignore specs that aren't in our installed list
next if !installed.include?(spec.name)
next if installed_versions[spec.name] &&
!installed_versions[spec.name].satisfied_by?(spec.version)
# If we already have a newer version in our list of installed,
# then ignore it
next if installed_map.key?(spec.name) &&
installed_map[spec.name].version >= spec.version
installed_map[spec.name] = spec
end
installed_map.values
end
# Loads the requested plugins into the Vagrant runtime
#
# @param [Hash] plugins List of plugins to load
# @return [nil]
def load_plugins(plugins)
if !Vagrant.plugins_enabled?
@logger.warn("Plugin loading is disabled")
return
end
if plugins.nil?
@logger.debug("No plugins provided for loading")
return
end
begin
@logger.info("Loading plugins...")
plugins.each do |plugin_name, plugin_info|
if plugin_info["require"].to_s.empty?
begin
@logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`")
require plugin_name
rescue LoadError => err
if plugin_name.include?("-")
plugin_slash = plugin_name.gsub("-", "/")
@logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}")
@logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`")
require plugin_slash
else
raise
end
end
else
@logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`")
require plugin_info["require"]
end
@logger.debug("Successfully loaded plugin `#{plugin_name}`.")
end
if defined?(::Bundler)
@logger.debug("Bundler detected in use. Loading `:plugins` group.")
::Bundler.require(:plugins)
end
rescue ScriptError, StandardError => err
@logger.error("Plugin loading error: #{err.class} - #{err}")
err.backtrace.each do |backtrace_line|
@logger.debug(backtrace_line)
end
raise Vagrant::Errors::PluginLoadError, message: err.to_s
end
nil
end
# Check if the requested plugin is installed
#
# @param [String] name Name of plugin
# @param [String] version Specific version of the plugin
# @return [Boolean]
def plugin_installed?(name, version=nil)
# Make the requirement object
version = Gem::Requirement.new([version.to_s]) if version
# If plugins are loaded, check for match in loaded specs
if ready?
return installed_specs.any? do |s|
match = s.name == name
next match if !version
next match && version.satisfied_by?(s.version)
end
end
# Plugins are not loaded yet so check installed plugin data
plugin_info = installed_plugins[name]
return false if !plugin_info
return !!plugin_info if version.nil? || plugin_info["installed_gem_version"].nil?
installed_version = Gem::Version.new(plugin_info["installed_gem_version"])
version.satisfied_by?(installed_version)
end
end
end
end