diff options
Diffstat (limited to 'lib/bundler/plugin.rb')
| -rw-r--r-- | lib/bundler/plugin.rb | 144 |
1 files changed, 103 insertions, 41 deletions
diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb index 158c69e1a1..081ec8e180 100644 --- a/lib/bundler/plugin.rb +++ b/lib/bundler/plugin.rb @@ -4,18 +4,27 @@ require_relative "plugin/api" module Bundler module Plugin - autoload :DSL, File.expand_path("plugin/dsl", __dir__) - autoload :Events, File.expand_path("plugin/events", __dir__) - autoload :Index, File.expand_path("plugin/index", __dir__) - autoload :Installer, File.expand_path("plugin/installer", __dir__) - autoload :SourceList, File.expand_path("plugin/source_list", __dir__) + autoload :DSL, File.expand_path("plugin/dsl", __dir__) + autoload :Events, File.expand_path("plugin/events", __dir__) + autoload :Index, File.expand_path("plugin/index", __dir__) + autoload :Installer, File.expand_path("plugin/installer", __dir__) + autoload :SourceList, File.expand_path("plugin/source_list", __dir__) + autoload :UnloadedSource, File.expand_path("plugin/unloaded_source", __dir__) class MalformattedPlugin < PluginError; end class UndefinedCommandError < PluginError; end class UnknownSourceError < PluginError; end class PluginInstallError < PluginError; end - PLUGIN_FILE_NAME = "plugins.rb".freeze + PLUGIN_FILE_NAME = "plugins.rb" + + # Module-level flag set while .gemfile_install parses the Gemfile and + # consulted by .from_lock to substitute plugin sources with + # UnloadedSource. It relies on definitions being built one at a time in + # a single thread; if they are ever built concurrently or reentrantly, + # this needs to be replaced by explicit state passed down to the + # lockfile parser. + @gemfile_parse = false module_function @@ -26,6 +35,7 @@ module Bundler @commands = {} @hooks_by_event = Hash.new {|h, k| h[k] = [] } @loaded_plugin_names = [] + @index = nil end reset! @@ -36,9 +46,11 @@ module Bundler # @param [Hash] options various parameters as described in description. # Refer to cli/plugin for available options def install(names, options) + raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"] + specs = Installer.new.install(names, options) - save_plugins names, specs + save_plugins specs.slice(*names) rescue PluginError specs_to_delete = specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) } specs_to_delete.each_value {|spec| Bundler.rm_rf(spec.full_gem_path) } @@ -60,7 +72,8 @@ module Bundler if names.any? names.each do |name| if index.installed?(name) - Bundler.rm_rf(index.plugin_path(name)) + path = index.plugin_path(name).to_s + Bundler.rm_rf(path) if index.installed_in_plugin_root?(name) index.unregister_plugin(name) Bundler.ui.info "Uninstalled plugin #{name}" else @@ -97,29 +110,44 @@ module Bundler # # @param [Pathname] gemfile path # @param [Proc] block that can be evaluated for (inline) Gemfile - def gemfile_install(gemfile = nil, &inline) - Bundler.settings.temporary(:frozen => false, :deployment => false) do - builder = DSL.new - if block_given? - builder.instance_eval(&inline) - else - builder.eval_gemfile(gemfile) - end - builder.check_primary_source_safety - definition = builder.to_definition(nil, true) - - return if definition.dependencies.empty? + def gemfile_install(gemfile = nil, lockfile = nil, unlock = {}, &inline) + @gemfile_parse = true + Bundler.configure + builder = DSL.new + if block_given? + builder.instance_eval(&inline) + else + builder.eval_gemfile(gemfile) + end + builder.check_primary_source_safety - plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p } - installed_specs = Installer.new.install_definition(definition) + plugins = builder.dependencies.map(&:name) + return if plugins.empty? - save_plugins plugins, installed_specs, builder.inferred_plugins + # skip the update if unlocking specific gems, but none of them are plugins + # declared in the Gemfile + if unlock.is_a?(Hash) && unlock[:gems] && !unlock[:gems].empty? && + (unlock[:gems] & plugins).empty? + unlock = {} end + + # resolve remotely when unlocking, so that plugins can be updated. + # Definition#initialize consumes the unlock hash, so this must be decided + # before building the definition. + updating = unlock == true || (unlock.is_a?(Hash) && !unlock.empty?) + + definition = builder.to_definition(lockfile, unlock) + + installed_specs = Installer.new.install_definition(definition, updating) + + save_plugins installed_specs.slice(*plugins), builder.inferred_plugins rescue RuntimeError => e unless e.is_a?(GemfileError) Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}" end raise + ensure + @gemfile_parse = false end # The index object used to store the details about the plugin @@ -180,25 +208,35 @@ module Bundler # Checks if any plugin declares the source def source?(name) - !index.source_plugin(name.to_s).nil? + !!source_plugin(name) + end + + # Returns the plugin that handles the source +name+ if any + def source_plugin(name) + index.source_plugin(name.to_s) end # @return [Class] that handles the source. The class includes API::Source def source(name) - raise UnknownSourceError, "Source #{name} not found" unless source? name + raise UnknownSourceError, "Source #{name} not found" unless source_plugin(name) load_plugin(index.source_plugin(name)) unless @sources.key? name @sources[name] end - # @param [Hash] The options that are present in the lock file + # @param [Hash] The options that are present in the lockfile # @return [API::Source] the instance of the class that handles the source # type passed in locked_opts - def source_from_lock(locked_opts) + def from_lock(locked_opts) + opts = locked_opts.merge("uri" => locked_opts["remote"]) + # when reading the lockfile while doing the plugin-install-from-gemfile phase, + # we need to ignore any plugin sources + return UnloadedSource.new(opts) if @gemfile_parse + src = source(locked_opts["type"]) - src.new(locked_opts.merge("uri" => locked_opts["remote"])) + src.new(opts) end # To be called via the API to register a hooks and corresponding block that @@ -217,7 +255,7 @@ module Bundler # # @param [String] event def hook(event, *args, &arg_blk) - return unless Bundler.feature_flag.plugins? + return unless Bundler.settings[:plugins] unless Events.defined_event?(event) raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events" end @@ -225,7 +263,7 @@ module Bundler plugins = index.hook_plugins(event) return unless plugins.any? - (plugins - @loaded_plugin_names).each {|name| load_plugin(name) } + plugins.each {|name| load_plugin(name) } @hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) } end @@ -234,21 +272,23 @@ module Bundler # # @return [String, nil] installed path def installed?(plugin) - Index.new.installed?(plugin) + (path = index.installed?(plugin)) && + index.plugin_path(plugin).join(PLUGIN_FILE_NAME).file? && + path + end + + # @return [true, false] whether the plugin is loaded + def loaded?(plugin) + @loaded_plugin_names.include?(plugin) end # Post installation processing and registering with index # - # @param [Array<String>] plugins list to be installed # @param [Hash] specs of plugins mapped to installation path (currently they # contain all the installed specs, including plugins) # @param [Array<String>] names of inferred source plugins that can be ignored - def save_plugins(plugins, specs, optional_plugins = []) - plugins.each do |name| - next if index.installed?(name) - - spec = specs[name] - + def save_plugins(specs, optional_plugins = []) + specs.each do |name, spec| save_plugin(name, spec, optional_plugins.include?(name)) end end @@ -273,6 +313,8 @@ module Bundler # # @raise [PluginInstallError] if validation or registration raises any error def save_plugin(name, spec, optional_plugin = false) + return if index.up_to_date?(spec) + validate_plugin! Pathname.new(spec.full_gem_path) installed = register_plugin(name, spec, optional_plugin) Bundler.ui.info "Installed plugin #{name}" if installed @@ -299,7 +341,7 @@ module Bundler @hooks_by_event = Hash.new {|h, k| h[k] = [] } load_paths = spec.load_paths - Bundler.rubygems.add_to_load_path(load_paths) + Gem.add_to_load_path(*load_paths) path = Pathname.new spec.full_gem_path begin @@ -308,7 +350,7 @@ module Bundler raise MalformattedPlugin, "#{e.class}: #{e.message}" end - if optional_plugin && @sources.keys.any? {|s| source? s } + if optional_plugin && @sources.keys.any? {|s| source_plugin(s) } Bundler.rm_rf(path) false else @@ -327,13 +369,33 @@ module Bundler # @param [String] name of the plugin def load_plugin(name) return unless name && !name.empty? + return if loaded?(name) # Need to ensure before this that plugin root where the rest of gems # are installed to be on load path to support plugin deps. Currently not # done to avoid conflicts path = index.plugin_path(name) - Bundler.rubygems.add_to_load_path(index.load_paths(name)) + paths = index.load_paths(name) + invalid_paths = paths.reject {|p| File.directory?(p) } + + if invalid_paths.any? + Bundler.ui.warn <<~MESSAGE + The following plugin paths don't exist: #{invalid_paths.join(", ")}. + + This can happen if the plugin was installed with a different version of Ruby that has since been uninstalled. + + If you would like to reinstall the plugin, run: + + bundler plugin uninstall #{name} && bundler plugin install #{name} + + Continuing without installing plugin #{name}. + MESSAGE + + return + end + + Gem.add_to_load_path(*paths) load path.join(PLUGIN_FILE_NAME) |
