1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/bundler/plugin/index.rb
Andre Arko c5f78ade5a
[rubygems/rubygems] fix dangling empty hooks
it turns out that running `bundle plugin uninstall some-plugin` would remove that plugin from the list of hooks, but if the list of hooks for an event was now empty, we would serialize the empty array into yaml as an empty single bullet item. which would then get unserialized as a plugin with the name empty string. which we would then try to load and explode. 😬

https://github.com/rubygems/rubygems/commit/545ebba9a5
2021-07-16 15:40:08 +09:00

185 lines
5.4 KiB
Ruby

# frozen_string_literal: true
module Bundler
# Manages which plugins are installed and their sources. This also is supposed to map
# which plugin does what (currently the features are not implemented so this class is
# now a stub class).
module Plugin
class Index
class CommandConflict < PluginError
def initialize(plugin, commands)
msg = "Command(s) `#{commands.join("`, `")}` declared by #{plugin} are already registered."
super msg
end
end
class SourceConflict < PluginError
def initialize(plugin, sources)
msg = "Source(s) `#{sources.join("`, `")}` declared by #{plugin} are already registered."
super msg
end
end
attr_reader :commands
def initialize
@plugin_paths = {}
@commands = {}
@sources = {}
@hooks = {}
@load_paths = {}
begin
load_index(global_index_file, true)
rescue GenericSystemCallError
# no need to fail when on a read-only FS, for example
nil
end
load_index(local_index_file) if SharedHelpers.in_bundle?
end
# This function is to be called when a new plugin is installed. This
# function shall add the functions of the plugin to existing maps and also
# the name to source location.
#
# @param [String] name of the plugin to be registered
# @param [String] path where the plugin is installed
# @param [Array<String>] load_paths for the plugin
# @param [Array<String>] commands that are handled by the plugin
# @param [Array<String>] sources that are handled by the plugin
def register_plugin(name, path, load_paths, commands, sources, hooks)
old_commands = @commands.dup
common = commands & @commands.keys
raise CommandConflict.new(name, common) unless common.empty?
commands.each {|c| @commands[c] = name }
common = sources & @sources.keys
raise SourceConflict.new(name, common) unless common.empty?
sources.each {|k| @sources[k] = name }
hooks.each do |event|
event_hooks = (@hooks[event] ||= []) << name
event_hooks.uniq!
end
@plugin_paths[name] = path
@load_paths[name] = load_paths
save_index
rescue StandardError
@commands = old_commands
raise
end
def unregister_plugin(name)
@commands.delete_if {|_, v| v == name }
@sources.delete_if {|_, v| v == name }
@hooks.each do |hook, names|
names.delete(name)
@hooks.delete(hook) if names.empty?
end
@plugin_paths.delete(name)
@load_paths.delete(name)
save_index
end
# Path of default index file
def index_file
Plugin.root.join("index")
end
# Path where the global index file is stored
def global_index_file
Plugin.global_root.join("index")
end
# Path where the local index file is stored
def local_index_file
Plugin.local_root.join("index")
end
def plugin_path(name)
Pathname.new @plugin_paths[name]
end
def load_paths(name)
@load_paths[name]
end
# Fetch the name of plugin handling the command
def command_plugin(command)
@commands[command]
end
def installed?(name)
@plugin_paths[name]
end
def installed_plugins
@plugin_paths.keys
end
def plugin_commands(plugin)
@commands.find_all {|_, n| n == plugin }.map(&:first)
end
def source?(source)
@sources.key? source
end
def source_plugin(name)
@sources[name]
end
# Returns the list of plugin names handling the passed event
def hook_plugins(event)
@hooks[event] || []
end
private
# Reads the index file from the directory and initializes the instance
# variables.
#
# It skips the sources if the second param is true
# @param [Pathname] index file path
# @param [Boolean] is the index file global index
def load_index(index_file, global = false)
SharedHelpers.filesystem_access(index_file, :read) do |index_f|
valid_file = index_f && index_f.exist? && !index_f.size.zero?
break unless valid_file
data = index_f.read
require_relative "../yaml_serializer"
index = YAMLSerializer.load(data)
@commands.merge!(index["commands"])
@hooks.merge!(index["hooks"])
@load_paths.merge!(index["load_paths"])
@plugin_paths.merge!(index["plugin_paths"])
@sources.merge!(index["sources"]) unless global
end
end
# Should be called when any of the instance variables change. Stores the
# instance variables in YAML format. (The instance variables are supposed
# to be only String key value pairs)
def save_index
index = {
"commands" => @commands,
"hooks" => @hooks,
"load_paths" => @load_paths,
"plugin_paths" => @plugin_paths,
"sources" => @sources,
}
require_relative "../yaml_serializer"
SharedHelpers.filesystem_access(index_file) do |index_f|
FileUtils.mkdir_p(index_f.dirname)
File.open(index_f, "w") {|f| f.puts YAMLSerializer.dump(index) }
end
end
end
end
end