summaryrefslogtreecommitdiff
path: root/lib/bundler/plugin.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/plugin.rb')
-rw-r--r--lib/bundler/plugin.rb144
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)