summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHomu <homu@barosl.com>2016-08-16 23:55:42 +0900
committerHomu <homu@barosl.com>2016-08-16 23:55:42 +0900
commit138b8c12c4006ec609726cf0e9902f82a5a34a87 (patch)
tree0c0ab3a0b534386f3bcc3f9416de5c796485e0f5
parente9591fde543516ed800cde933d9f06e7e20e6077 (diff)
parentde6580727886bb25503830d51869213d67a83b77 (diff)
downloadbundler-138b8c12c4006ec609726cf0e9902f82a5a34a87.tar.gz
Auto merge of #4815 - asutoshpalai:plugin-hooks, r=segiddins
[Plugin] Life-cycle hooks Implements life-cycle hook plugins. This shows only [`before-install-all` hook](https://github.com/bundler/bundler/pull/4815/files#diff-0964e829e3db904ba7935ca3a933f111R22), but other hooks can be easily added in the similar fashion.
-rw-r--r--lib/bundler/installer.rb1
-rw-r--r--lib/bundler/plugin.rb55
-rw-r--r--lib/bundler/plugin/api.rb4
-rw-r--r--lib/bundler/plugin/index.rb23
-rw-r--r--spec/bundler/plugin/api_spec.rb11
-rw-r--r--spec/bundler/plugin/index_spec.rb96
-rw-r--r--spec/bundler/plugin_spec.rb61
-rw-r--r--spec/plugins/hook_spec.rb28
8 files changed, 227 insertions, 52 deletions
diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb
index aa0c5f1c8e..528dee177e 100644
--- a/lib/bundler/installer.rb
+++ b/lib/bundler/installer.rb
@@ -20,6 +20,7 @@ module Bundler
# For more information see the #run method on this class.
def self.install(root, definition, options = {})
installer = new(root, definition)
+ Plugin.hook("before-install-all", definition.dependencies)
installer.run(options)
installer
end
diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb
index cd3843fc0d..1f0297f29b 100644
--- a/lib/bundler/plugin.rb
+++ b/lib/bundler/plugin.rb
@@ -16,8 +16,16 @@ module Bundler
module_function
- @commands = {}
- @sources = {}
+ 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
#
@@ -30,7 +38,7 @@ module Bundler
save_plugins names, specs
rescue PluginError => e
specs.values.map {|spec| Bundler.rm_rf(spec.full_gem_path) } if specs
- Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n #{e.backtrace[0]}"
+ Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n #{e.backtrace.join("\n ")}"
end
# Evaluates the Gemfile with a limited DSL and installs the plugins
@@ -137,6 +145,29 @@ module Bundler
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)
+ @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.settings[:plugins]
+
+ plugins = index.hook_plugins(event)
+ return unless plugins.any?
+
+ (plugins - @loaded_plugin_names).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
@@ -144,14 +175,6 @@ module Bundler
Index.new.installed?(plugin)
end
- # Used by specs
- def reset!
- instance_variables.each {|i| remove_instance_variable(i) }
-
- @sources = {}
- @commands = {}
- end
-
# Post installation processing and registering with index
#
# @param [Array<String>] plugins list to be installed
@@ -162,7 +185,7 @@ module Bundler
plugins.each do |name|
spec = specs[name]
validate_plugin! Pathname.new(spec.full_gem_path)
- installed = register_plugin name, spec, optional_plugins.include?(name)
+ installed = register_plugin(name, spec, optional_plugins.include?(name))
Bundler.ui.info "Installed plugin #{name}" if installed
end
end
@@ -190,9 +213,11 @@ module Bundler
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
add_to_load_path(load_paths)
@@ -208,12 +233,14 @@ module Bundler
Bundler.rm_rf(path)
false
else
- index.register_plugin name, path.to_s, load_paths, @commands.keys, @sources.keys
+ 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
@@ -228,6 +255,8 @@ module Bundler
add_to_load_path(index.load_paths(name))
load path.join(PLUGIN_FILE_NAME)
+
+ @loaded_plugin_names << name
rescue => e
Bundler.ui.error "Failed loading plugin #{name}: #{e.message}"
raise
diff --git a/lib/bundler/plugin/api.rb b/lib/bundler/plugin/api.rb
index 9ff4b7385b..5bd8792e55 100644
--- a/lib/bundler/plugin/api.rb
+++ b/lib/bundler/plugin/api.rb
@@ -44,6 +44,10 @@ module Bundler
Plugin.add_source source, cls
end
+ def self.hook(event, &block)
+ Plugin.add_hook(event, &block)
+ end
+
# The cache dir to be used by the plugins for storage
#
# @return [Pathname] path of the cache dir
diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb
index 918b3a4392..7f89d26178 100644
--- a/lib/bundler/plugin/index.rb
+++ b/lib/bundler/plugin/index.rb
@@ -24,6 +24,7 @@ module Bundler
@plugin_paths = {}
@commands = {}
@sources = {}
+ @hooks = {}
@load_paths = {}
load_index(global_index_file, true)
@@ -39,7 +40,7 @@ module Bundler
# @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)
+ def register_plugin(name, path, load_paths, commands, sources, hooks)
old_commands = @commands.dup
common = commands & @commands.keys
@@ -50,6 +51,8 @@ module Bundler
raise SourceConflict.new(name, common) unless common.empty?
sources.each {|k| @sources[k] = name }
+ hooks.each {|e| (@hooks[e] ||= []) << name }
+
@plugin_paths[name] = path
@load_paths[name] = load_paths
save_index
@@ -98,6 +101,11 @@ module Bundler
@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
@@ -112,12 +120,14 @@ module Bundler
break unless valid_file
data = index_f.read
+
require "bundler/yaml_serializer"
index = YAMLSerializer.load(data)
- @plugin_paths.merge!(index["plugin_paths"])
- @load_paths.merge!(index["load_paths"])
@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
@@ -127,10 +137,11 @@ module Bundler
# to be only String key value pairs)
def save_index
index = {
+ "commands" => @commands,
+ "hooks" => @hooks,
+ "load_paths" => @load_paths,
"plugin_paths" => @plugin_paths,
- "load_paths" => @load_paths,
- "commands" => @commands,
- "sources" => @sources,
+ "sources" => @sources,
}
require "bundler/yaml_serializer"
diff --git a/spec/bundler/plugin/api_spec.rb b/spec/bundler/plugin/api_spec.rb
index a227d31591..0eba52301a 100644
--- a/spec/bundler/plugin/api_spec.rb
+++ b/spec/bundler/plugin/api_spec.rb
@@ -38,6 +38,17 @@ describe Bundler::Plugin::API do
UserPluginClass.source "a_source", NewClass
end
end
+
+ describe "#hook" do
+ it "accepts a block and passes it to Plugin module" do
+ foo = double("tester")
+ expect(foo).to receive(:called)
+
+ expect(Bundler::Plugin).to receive(:add_hook).with("post-foo").and_yield
+
+ Bundler::Plugin::API.hook("post-foo") { foo.called }
+ end
+ end
end
context "bundler interfaces provided" do
diff --git a/spec/bundler/plugin/index_spec.rb b/spec/bundler/plugin/index_spec.rb
index 337182abf1..5a754f355c 100644
--- a/spec/bundler/plugin/index_spec.rb
+++ b/spec/bundler/plugin/index_spec.rb
@@ -4,86 +4,113 @@ require "spec_helper"
describe Bundler::Plugin::Index do
Index = Bundler::Plugin::Index
- subject(:index) { Index.new }
-
before do
gemfile ""
+ path = lib_path(plugin_name)
+ index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], commands, sources, hooks)
end
- describe "#register plugin" do
- before do
- path = lib_path("new-plugin")
- index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], [], [])
- end
+ let(:plugin_name) { "new-plugin" }
+ let(:commands) { [] }
+ let(:sources) { [] }
+ let(:hooks) { [] }
+ subject(:index) { Index.new }
+
+ describe "#register plugin" do
it "is available for retrieval" do
- expect(index.plugin_path("new-plugin")).to eq(lib_path("new-plugin"))
+ expect(index.plugin_path(plugin_name)).to eq(lib_path(plugin_name))
end
it "load_paths is available for retrival" do
- expect(index.load_paths("new-plugin")).to eq([lib_path("new-plugin").join("lib").to_s])
+ expect(index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s])
end
it "is persistent" do
new_index = Index.new
- expect(new_index.plugin_path("new-plugin")).to eq(lib_path("new-plugin"))
+ expect(new_index.plugin_path(plugin_name)).to eq(lib_path(plugin_name))
end
it "load_paths are persistant" do
new_index = Index.new
- expect(new_index.load_paths("new-plugin")).to eq([lib_path("new-plugin").join("lib").to_s])
+ expect(new_index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s])
end
end
describe "commands" do
- before do
- path = lib_path("cplugin")
- index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["newco"], [])
- end
+ let(:commands) { ["newco"] }
it "returns the plugins name on query" do
- expect(index.command_plugin("newco")).to eq("cplugin")
+ expect(index.command_plugin("newco")).to eq(plugin_name)
end
it "raises error on conflict" do
expect do
- index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [])
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [], [])
end.to raise_error(Index::CommandConflict)
end
it "is persistent" do
new_index = Index.new
- expect(new_index.command_plugin("newco")).to eq("cplugin")
+ expect(new_index.command_plugin("newco")).to eq(plugin_name)
end
end
describe "source" do
- before do
- path = lib_path("splugin")
- index.register_plugin("splugin", path.to_s, [path.join("lib").to_s], [], ["new_source"])
- end
+ let(:sources) { ["new_source"] }
it "returns the plugins name on query" do
- expect(index.source_plugin("new_source")).to eq("splugin")
+ expect(index.source_plugin("new_source")).to eq(plugin_name)
end
it "raises error on conflict" do
expect do
- index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"])
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"], [])
end.to raise_error(Index::SourceConflict)
end
it "is persistent" do
new_index = Index.new
- expect(new_index.source_plugin("new_source")).to eq("splugin")
+ expect(new_index.source_plugin("new_source")).to eq(plugin_name)
+ end
+ end
+
+ describe "hook" do
+ let(:hooks) { ["after-bar"] }
+
+ it "returns the plugins name on query" do
+ expect(index.hook_plugins("after-bar")).to include(plugin_name)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.hook_plugins("after-bar")).to eq([plugin_name])
+ end
+
+ context "that are not registered", :focused do
+ let(:file) { double("index-file") }
+
+ before do
+ index.hook_plugins("not-there")
+ allow(File).to receive(:open).and_yield(file)
+ end
+
+ it "should not save it with next registed hook" do
+ expect(file).to receive(:puts) do |content|
+ expect(content).not_to include("not-there")
+ end
+
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], [], [])
+ end
end
end
describe "global index" do
before do
Dir.chdir(tmp) do
+ Bundler::Plugin.reset!
path = lib_path("gplugin")
- index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"])
+ index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"], [])
end
end
@@ -94,10 +121,9 @@ describe Bundler::Plugin::Index do
end
describe "after conflict" do
- before do
- path = lib_path("aplugin")
- index.register_plugin("aplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"])
- end
+ let(:commands) { ["foo"] }
+ let(:sources) { ["bar"] }
+ let(:hooks) { ["hoook"] }
shared_examples "it cleans up" do
it "the path" do
@@ -111,13 +137,17 @@ describe Bundler::Plugin::Index do
it "the source" do
expect(index.source_plugin("xbar")).to be_falsy
end
+
+ it "the hook" do
+ expect(index.hook_plugins("xhoook")).to be_empty
+ end
end
context "on command conflict it cleans up" do
before do
expect do
path = lib_path("cplugin")
- index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"])
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xhoook"])
end.to raise_error(Index::CommandConflict)
end
@@ -128,7 +158,7 @@ describe Bundler::Plugin::Index do
before do
expect do
path = lib_path("cplugin")
- index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"])
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xhoook"])
end.to raise_error(Index::SourceConflict)
end
@@ -139,7 +169,7 @@ describe Bundler::Plugin::Index do
before do
expect do
path = lib_path("cplugin")
- index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"])
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xhoook"])
end.to raise_error(Index::CommandConflict)
end
diff --git a/spec/bundler/plugin_spec.rb b/spec/bundler/plugin_spec.rb
index f6f10201b3..540278036c 100644
--- a/spec/bundler/plugin_spec.rb
+++ b/spec/bundler/plugin_spec.rb
@@ -228,4 +228,65 @@ describe Bundler::Plugin do
end
end
end
+
+ describe "#hook" do
+ before do
+ path = lib_path("foo-plugin")
+ build_lib "foo-plugin", :path => path do |s|
+ s.write "plugins.rb", code
+ end
+
+ allow(index).to receive(:hook_plugins).with(event).
+ and_return(["foo-plugin"])
+ allow(index).to receive(:plugin_path).with("foo-plugin").and_return(path)
+ allow(index).to receive(:load_paths).with("foo-plugin").and_return([])
+ end
+
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" }
+ RUBY
+
+ let(:event) { "event-1" }
+
+ it "executes the hook" do
+ out = capture(:stdout) do
+ Plugin.hook("event-1")
+ end.strip
+
+ expect(out).to eq("hook for event 1")
+ end
+
+ context "single plugin declaring more than one hook" do
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook("event-1") {}
+ Bundler::Plugin::API.hook("event-2") {}
+ puts "loaded"
+ RUBY
+
+ let(:event) { /event-1|event-2/ }
+
+ it "evals plugins.rb once" do
+ out = capture(:stdout) do
+ Plugin.hook("event-1")
+ Plugin.hook("event-2")
+ end.strip
+
+ expect(out).to eq("loaded")
+ end
+ end
+
+ context "a block is passed" do
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook("#{event}") { |&blk| blk.call }
+ RUBY
+
+ it "is passed to the hook" do
+ out = capture(:stdout) do
+ Plugin.hook("event-1") { puts "win" }
+ end.strip
+
+ expect(out).to eq("win")
+ end
+ end
+ end
end
diff --git a/spec/plugins/hook_spec.rb b/spec/plugins/hook_spec.rb
new file mode 100644
index 0000000000..bafe688d5e
--- /dev/null
+++ b/spec/plugins/hook_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+describe "hook plugins" do
+ before do
+ build_repo2 do
+ build_plugin "before-install-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook "before-install-all" do |deps|
+ puts "gems to be installed \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-install-plugin --source file://#{gem_repo2}"
+ end
+
+ it "runs after a rubygem is installed" do
+ install_gemfile <<-G
+ source "file://#{gem_repo1}"
+ gem "rake"
+ gem "rack"
+ G
+
+ expect(out).to include "gems to be installed rake, rack"
+ end
+end