diff options
Diffstat (limited to 'lib/bundler/plugin/index.rb')
| -rw-r--r-- | lib/bundler/plugin/index.rb | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb new file mode 100644 index 0000000000..1dfb061304 --- /dev/null +++ b/lib/bundler/plugin/index.rb @@ -0,0 +1,241 @@ +# 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 PermissionError + # no need to fail when on a read-only FS, for example + nil + rescue ArgumentError => e + # ruby 3.4 checks writability in Dir.tmpdir + raise unless e.message&.include?("could not find a temporary directory") + 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 up_to_date?(spec) + path = installed?(spec.name) + + path == spec.full_gem_path + 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 + + # This plugin is installed inside the .bundle/plugin directory, + # and thus is managed solely by Bundler + def installed_in_plugin_root?(name) + return false unless (path = installed?(name)) + + path.start_with?("#{Plugin.root}/") + 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) + base = base_for_index(global) + + SharedHelpers.filesystem_access(index_file, :read) do |index_f| + valid_file = 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!(transform_index_paths(index["load_paths"]) {|p| absolutize_path(p, base) }) + @plugin_paths.merge!(transform_index_paths(index["plugin_paths"]) {|p| absolutize_path(p, base) }) + @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 + base = base_for_index(false) + + index = { + "commands" => @commands, + "hooks" => @hooks, + "load_paths" => transform_index_paths(@load_paths) {|p| relativize_path(p, base) }, + "plugin_paths" => transform_index_paths(@plugin_paths) {|p| relativize_path(p, base) }, + "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 + + def base_for_index(global) + global ? Plugin.global_root : Plugin.root + end + + def transform_index_paths(paths) + return {} unless paths + + paths.transform_values do |value| + if value.is_a?(Array) + value.map {|path| yield path } + else + yield value + end + end + end + + def relativize_path(path, base) + pathname = Pathname.new(path) + return path unless pathname.absolute? + + base_path = Pathname.new(base) + if pathname == base_path || pathname.to_s.start_with?(base_path.to_s + File::SEPARATOR) + pathname.relative_path_from(base_path).to_s + else + path + end + end + + def absolutize_path(path, base) + pathname = Pathname.new(path) + pathname = Pathname.new(base).join(pathname) unless pathname.absolute? + pathname.to_s + end + end + end +end |
