1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
|
# frozen_string_literal: true
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__)
class MalformattedPlugin < PluginError; end
class UndefinedCommandError < PluginError; end
class UnknownSourceError < PluginError; end
class PluginInstallError < PluginError; end
PLUGIN_FILE_NAME = "plugins.rb"
module_function
def reset!
instance_variables.each {|i| remove_instance_variable(i) }
@sources = {}
@commands = {}
@hooks_by_event = Hash.new {|h, k| h[k] = [] }
@loaded_plugin_names = []
end
reset!
# Installs a new plugin by the given name
#
# @param [Array<String>] names the name of plugin to be installed
# @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
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) }
raise
end
# Uninstalls plugins by the given names
#
# @param [Array<String>] names the names of plugins to be uninstalled
def uninstall(names, options)
if names.empty? && !options[:all]
Bundler.ui.error "No plugins to uninstall. Specify at least 1 plugin to uninstall.\n"\
"Use --all option to uninstall all the installed plugins."
return
end
names = index.installed_plugins if options[:all]
if names.any?
names.each do |name|
if index.installed?(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
Bundler.ui.error "Plugin #{name} is not installed \n"
end
end
else
Bundler.ui.info "No plugins installed"
end
end
# List installed plugins and commands
#
def list
installed_plugins = index.installed_plugins
if installed_plugins.any?
output = String.new
installed_plugins.each do |plugin|
output << "#{plugin}\n"
output << "-----\n"
index.plugin_commands(plugin).each do |command|
output << " #{command}\n"
end
output << "\n"
end
else
output = "No plugins installed"
end
Bundler.ui.info output
end
# Evaluates the Gemfile with a limited DSL and installs the plugins
# specified by plugin method
#
# @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?
plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p }
installed_specs = Installer.new.install_definition(definition)
save_plugins plugins, installed_specs, builder.inferred_plugins
end
rescue RuntimeError => e
unless e.is_a?(GemfileError)
Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}"
end
raise
end
# The index object used to store the details about the plugin
def index
@index ||= Index.new
end
# The directory root for all plugin related data
#
# If run in an app, points to local root, in app_config_path
# Otherwise, points to global root, in Bundler.user_bundle_path("plugin")
def root
@root ||= if SharedHelpers.in_bundle?
local_root
else
global_root
end
end
def local_root
Bundler.app_config_path.join("plugin")
end
# The global directory root for all plugin related data
def global_root
Bundler.user_bundle_path("plugin")
end
# The cache directory for plugin stuffs
def cache
@cache ||= root.join("cache")
end
# To be called via the API to register to handle a command
def add_command(command, cls)
@commands[command] = cls
end
# Checks if any plugin handles the command
def command?(command)
!index.command_plugin(command).nil?
end
# To be called from Cli class to pass the command and argument to
# appropriate plugin class
def exec_command(command, args)
raise UndefinedCommandError, "Command `#{command}` not found" unless command? command
load_plugin index.command_plugin(command) unless @commands.key? command
@commands[command].new.exec(command, args)
end
# To be called via the API to register to handle a source plugin
def add_source(source, cls)
@sources[source] = cls
end
# Checks if any plugin declares the source
def source?(name)
!index.source_plugin(name.to_s).nil?
end
# @return [Class] that handles the source. The class includes API::Source
def source(name)
raise UnknownSourceError, "Source #{name} not found" unless source? name
load_plugin(index.source_plugin(name)) unless @sources.key? name
@sources[name]
end
# @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 from_lock(locked_opts)
src = source(locked_opts["type"])
src.new(locked_opts.merge("uri" => locked_opts["remote"]))
end
# To be called via the API to register a hooks and corresponding block that
# will be called to handle the hook
def add_hook(event, &block)
unless Events.defined_event?(event)
raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events"
end
@hooks_by_event[event.to_s] << block
end
# Runs all the hooks that are registered for the passed event
#
# It passes the passed arguments and block to the block registered with
# the api.
#
# @param [String] event
def hook(event, *args, &arg_blk)
return unless Bundler.feature_flag.plugins?
unless Events.defined_event?(event)
raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events"
end
plugins = index.hook_plugins(event)
return unless plugins.any?
plugins.each {|name| load_plugin(name) }
@hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) }
end
# currently only intended for specs
#
# @return [String, nil] installed path
def installed?(plugin)
Index.new.installed?(plugin)
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]
save_plugin(name, spec, optional_plugins.include?(name))
end
end
# Checks if the gem is good to be a plugin
#
# At present it only checks whether it contains plugins.rb file
#
# @param [Pathname] plugin_path the path plugin is installed at
# @raise [MalformattedPlugin] if plugins.rb file is not found
def validate_plugin!(plugin_path)
plugin_file = plugin_path.join(PLUGIN_FILE_NAME)
raise MalformattedPlugin, "#{PLUGIN_FILE_NAME} was not found in the plugin." unless plugin_file.file?
end
# Validates and registers a plugin.
#
# @param [String] name the name of the plugin
# @param [Specification] spec of installed plugin
# @param [Boolean] optional_plugin, removed if there is conflict with any
# other plugin (used for default source plugins)
#
# @raise [PluginInstallError] if validation or registration raises any error
def save_plugin(name, spec, optional_plugin = false)
validate_plugin! Pathname.new(spec.full_gem_path)
installed = register_plugin(name, spec, optional_plugin)
Bundler.ui.info "Installed plugin #{name}" if installed
rescue PluginError => e
raise PluginInstallError, "Failed to install plugin `#{spec.name}`, due to #{e.class} (#{e.message})"
end
# Runs the plugins.rb file in an isolated namespace, records the plugin
# actions it registers for and then passes the data to index to be stored.
#
# @param [String] name the name of the plugin
# @param [Specification] spec of installed plugin
# @param [Boolean] optional_plugin, removed if there is conflict with any
# other plugin (used for default source plugins)
#
# @raise [MalformattedPlugin] if plugins.rb raises any error
def register_plugin(name, spec, optional_plugin = false)
commands = @commands
sources = @sources
hooks = @hooks_by_event
@commands = {}
@sources = {}
@hooks_by_event = Hash.new {|h, k| h[k] = [] }
load_paths = spec.load_paths
Gem.add_to_load_path(*load_paths)
path = Pathname.new spec.full_gem_path
begin
load path.join(PLUGIN_FILE_NAME), true
rescue StandardError => e
raise MalformattedPlugin, "#{e.class}: #{e.message}"
end
if optional_plugin && @sources.keys.any? {|s| source? s }
Bundler.rm_rf(path)
false
else
index.register_plugin(name, path.to_s, load_paths, @commands.keys,
@sources.keys, @hooks_by_event.keys)
true
end
ensure
@commands = commands
@sources = sources
@hooks_by_event = hooks
end
# Executes the plugins.rb file
#
# @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)
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)
@loaded_plugin_names << name
rescue RuntimeError => e
Bundler.ui.error "Failed loading plugin #{name}: #{e.message}"
raise
end
class << self
private :load_plugin, :register_plugin, :save_plugins, :validate_plugin!
end
end
end
|