diff options
author | Homu <homu@barosl.com> | 2016-08-16 23:55:42 +0900 |
---|---|---|
committer | Homu <homu@barosl.com> | 2016-08-16 23:55:42 +0900 |
commit | 138b8c12c4006ec609726cf0e9902f82a5a34a87 (patch) | |
tree | 0c0ab3a0b534386f3bcc3f9416de5c796485e0f5 | |
parent | e9591fde543516ed800cde933d9f06e7e20e6077 (diff) | |
parent | de6580727886bb25503830d51869213d67a83b77 (diff) | |
download | bundler-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.rb | 1 | ||||
-rw-r--r-- | lib/bundler/plugin.rb | 55 | ||||
-rw-r--r-- | lib/bundler/plugin/api.rb | 4 | ||||
-rw-r--r-- | lib/bundler/plugin/index.rb | 23 | ||||
-rw-r--r-- | spec/bundler/plugin/api_spec.rb | 11 | ||||
-rw-r--r-- | spec/bundler/plugin/index_spec.rb | 96 | ||||
-rw-r--r-- | spec/bundler/plugin_spec.rb | 61 | ||||
-rw-r--r-- | spec/plugins/hook_spec.rb | 28 |
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 |