Skip to content

Commit afb6285

Browse files
authored
Merge pull request #9488 from ruby/add-plugin-hooks-eval-fetch
Add plugin hooks for Gemfile evaluation and source fetching
2 parents 2406589 + 93fe617 commit afb6285

6 files changed

Lines changed: 200 additions & 18 deletions

File tree

bundler/lib/bundler/definition.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ def self.build(gemfile, lockfile, unlock)
3838

3939
raise GemfileNotFound, "#{gemfile} not found" unless gemfile.file?
4040

41-
Dsl.evaluate(gemfile, lockfile, unlock)
41+
Plugin.hook(Plugin::Events::GEM_BEFORE_EVAL, gemfile, lockfile)
42+
Dsl.evaluate(gemfile, lockfile, unlock).tap do |definition|
43+
Plugin.hook(Plugin::Events::GEM_AFTER_EVAL, definition)
44+
end
4245
end
4346

4447
#

bundler/lib/bundler/plugin/events.rb

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,54 @@ def self.defined_event?(event)
3030
@events.key?(event)
3131
end
3232

33+
# @!parse
34+
# A hook called before the Gemfile is evaluated
35+
# Includes the Gemfile path and the Lockfile path
36+
# GEM_BEFORE_EVAL = "before-eval"
37+
define :GEM_BEFORE_EVAL, "before-eval"
38+
39+
# @!parse
40+
# A hook called after the Gemfile is evaluated
41+
# Includes a Bundler::Definition
42+
# GEM_AFTER_EVAL = "after-eval"
43+
define :GEM_AFTER_EVAL, "after-eval"
44+
45+
# @!parse
46+
# A hook called before any gems install
47+
# Includes an Array of Bundler::Dependency objects
48+
# GEM_BEFORE_INSTALL_ALL = "before-install-all"
49+
define :GEM_BEFORE_INSTALL_ALL, "before-install-all"
50+
51+
# @!parse
52+
# A hook called before each individual gem is downloaded from a remote source.
53+
# Includes a spec-like object responding to the Gem::Specification API
54+
# (for example, a Bundler spec proxy such as Bundler::EndpointSpecification
55+
# or Bundler::RemoteSpecification). Does not fire when the gem is already
56+
# present at the initial download-cache check.
57+
# GEM_BEFORE_FETCH = "before-fetch"
58+
define :GEM_BEFORE_FETCH, "before-fetch"
59+
60+
# @!parse
61+
# A hook called after each individual gem is downloaded from a remote source.
62+
# Includes a spec-like object responding to the Gem::Specification API
63+
# (for example, a Bundler spec proxy such as Bundler::EndpointSpecification
64+
# or Bundler::RemoteSpecification). Does not fire when the gem is already
65+
# present at the initial download-cache check.
66+
# GEM_AFTER_FETCH = "after-fetch"
67+
define :GEM_AFTER_FETCH, "after-fetch"
68+
69+
# @!parse
70+
# A hook called before a git source is fetched or checked out.
71+
# Includes a Bundler::Source::Git reference.
72+
# GIT_BEFORE_FETCH = "before-git-fetch"
73+
define :GIT_BEFORE_FETCH, "before-git-fetch"
74+
75+
# @!parse
76+
# A hook called after a git source is fetched or checked out.
77+
# Includes a Bundler::Source::Git reference.
78+
# GIT_AFTER_FETCH = "after-git-fetch"
79+
define :GIT_AFTER_FETCH, "after-git-fetch"
80+
3381
# @!parse
3482
# A hook called before each individual gem is installed
3583
# Includes a Bundler::ParallelInstaller::SpecInstallation.
@@ -45,18 +93,18 @@ def self.defined_event?(event)
4593
# GEM_AFTER_INSTALL = "after-install"
4694
define :GEM_AFTER_INSTALL, "after-install"
4795

48-
# @!parse
49-
# A hook called before any gems install
50-
# Includes an Array of Bundler::Dependency objects
51-
# GEM_BEFORE_INSTALL_ALL = "before-install-all"
52-
define :GEM_BEFORE_INSTALL_ALL, "before-install-all"
53-
5496
# @!parse
5597
# A hook called after any gems install
5698
# Includes an Array of Bundler::Dependency objects
5799
# GEM_AFTER_INSTALL_ALL = "after-install-all"
58100
define :GEM_AFTER_INSTALL_ALL, "after-install-all"
59101

102+
# @!parse
103+
# A hook called before any gems require
104+
# Includes an Array of Bundler::Dependency objects.
105+
# GEM_BEFORE_REQUIRE_ALL = "before-require-all"
106+
define :GEM_BEFORE_REQUIRE_ALL, "before-require-all"
107+
60108
# @!parse
61109
# A hook called before each individual gem is required
62110
# Includes a Bundler::Dependency.
@@ -69,17 +117,11 @@ def self.defined_event?(event)
69117
# GEM_AFTER_REQUIRE = "after-require"
70118
define :GEM_AFTER_REQUIRE, "after-require"
71119

72-
# @!parse
73-
# A hook called before any gems require
74-
# Includes an Array of Bundler::Dependency objects.
75-
# GEM_BEFORE_REQUIRE_ALL = "before-require-all"
76-
define :GEM_BEFORE_REQUIRE_ALL, "before-require-all"
77-
78120
# @!parse
79121
# A hook called after all gems required
80122
# Includes an Array of Bundler::Dependency objects.
81123
# GEM_AFTER_REQUIRE_ALL = "after-require-all"
82-
define :GEM_AFTER_REQUIRE_ALL, "after-require-all"
124+
define :GEM_AFTER_REQUIRE_ALL, "after-require-all"
83125
end
84126
end
85127
end

bundler/lib/bundler/source/git.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,13 @@ def specs(*)
191191
set_cache_path!(app_cache_path) if use_app_cache?
192192

193193
if requires_checkout? && !@copied
194-
fetch unless use_app_cache?
195-
checkout
194+
Plugin.hook(Plugin::Events::GIT_BEFORE_FETCH, self)
195+
begin
196+
fetch unless use_app_cache?
197+
checkout
198+
ensure
199+
Plugin.hook(Plugin::Events::GIT_AFTER_FETCH, self)
200+
end
196201
end
197202

198203
local_specs

bundler/lib/bundler/source/rubygems.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,13 @@ def download_gem(spec, download_cache_path, previous_spec = nil)
477477
Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}")
478478
gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher
479479

480-
Gem.time("Downloaded #{spec.name} in", 0, true) do
481-
Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher)
480+
Plugin.hook(Plugin::Events::GEM_BEFORE_FETCH, spec)
481+
begin
482+
Gem.time("Downloaded #{spec.name} in", 0, true) do
483+
Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher)
484+
end
485+
ensure
486+
Plugin.hook(Plugin::Events::GEM_AFTER_FETCH, spec)
482487
end
483488
end
484489

spec/commands/doctor_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
3535
allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [unwritable_file] }
3636
allow(File).to receive(:exist?).and_call_original
37+
allow(File).to receive(:writable?).and_call_original
38+
allow(File).to receive(:readable?).and_call_original
3739
allow(File).to receive(:exist?).with(unwritable_file).and_return(true)
3840
allow(File).to receive(:stat).with(unwritable_file) { stat }
3941
allow(stat).to receive(:uid) { Process.uid }
@@ -108,6 +110,8 @@
108110
allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
109111
allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@unwritable_file] }
110112
allow(File).to receive(:exist?).and_call_original
113+
allow(File).to receive(:writable?).and_call_original
114+
allow(File).to receive(:readable?).and_call_original
111115
allow(File).to receive(:exist?).with(@unwritable_file) { true }
112116
allow(File).to receive(:stat).with(@unwritable_file) { @stat }
113117
end

spec/plugins/hook_spec.rb

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,129 @@
193193
end
194194
end
195195

196+
context "before-eval hook" do
197+
before do
198+
build_repo2 do
199+
build_plugin "before-eval-plugin" do |s|
200+
s.write "plugins.rb", <<-RUBY
201+
Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_EVAL do |gemfile, lockfile|
202+
puts "hooked eval start of \#{File.basename(gemfile)} to \#{File.basename(lockfile)}"
203+
end
204+
RUBY
205+
end
206+
end
207+
208+
bundle "plugin install before-eval-plugin --source https://gem.repo2"
209+
end
210+
211+
it "runs before the Gemfile is evaluated" do
212+
install_gemfile <<-G
213+
source "https://gem.repo1"
214+
gem "rake"
215+
G
216+
217+
expect(out).to include "hooked eval start of Gemfile to Gemfile.lock"
218+
end
219+
end
220+
221+
context "after-eval hook" do
222+
before do
223+
build_repo2 do
224+
build_plugin "after-eval-plugin" do |s|
225+
s.write "plugins.rb", <<-RUBY
226+
Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_EVAL do |defn|
227+
puts "hooked eval after with gems \#{defn.dependencies.map(&:name).join(", ")}"
228+
end
229+
RUBY
230+
end
231+
end
232+
233+
bundle "plugin install after-eval-plugin --source https://gem.repo2"
234+
end
235+
236+
it "runs after the Gemfile is evaluated" do
237+
install_gemfile <<-G
238+
source "https://gem.repo1"
239+
gem "myrack"
240+
gem "rake"
241+
G
242+
243+
expect(out).to include "hooked eval after with gems myrack, rake"
244+
end
245+
end
246+
247+
context "before-fetch and after-fetch hooks" do
248+
before do
249+
build_repo2 do
250+
build_plugin "fetch-timing-plugin" do |s|
251+
s.write "plugins.rb", <<-RUBY
252+
@timing_start = nil
253+
Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_FETCH do |spec|
254+
@timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
255+
puts "gem \#{spec.name} started fetch at \#{@timing_start}"
256+
end
257+
Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_FETCH do |spec|
258+
timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
259+
puts "gem \#{spec.name} took \#{timing_end - @timing_start} to fetch"
260+
@timing_start = nil
261+
end
262+
RUBY
263+
end
264+
end
265+
266+
bundle "plugin install fetch-timing-plugin --source https://gem.repo2"
267+
end
268+
269+
it "runs around each gem download" do
270+
install_gemfile <<-G
271+
source "https://gem.repo1"
272+
gem "rake"
273+
gem "myrack"
274+
G
275+
276+
expect(out).to include "gem rake started fetch at"
277+
expect(out).to match(/gem rake took \d+\.\d+ to fetch/)
278+
expect(out).to include "gem myrack started fetch at"
279+
expect(out).to match(/gem myrack took \d+\.\d+ to fetch/)
280+
end
281+
end
282+
283+
context "before-git-fetch and after-git-fetch hooks" do
284+
before do
285+
build_repo2 do
286+
build_plugin "git-fetch-timing-plugin" do |s|
287+
s.write "plugins.rb", <<-RUBY
288+
@timing_start = nil
289+
Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_BEFORE_FETCH do |source|
290+
@timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
291+
puts "git source \#{source.name} started fetch at \#{@timing_start}"
292+
end
293+
Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_AFTER_FETCH do |source|
294+
timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
295+
puts "git source \#{source.name} took \#{timing_end - @timing_start} to fetch"
296+
@timing_start = nil
297+
end
298+
RUBY
299+
end
300+
end
301+
302+
bundle "plugin install git-fetch-timing-plugin --source https://gem.repo2"
303+
end
304+
305+
it "runs around each git source fetch" do
306+
build_git "foo", "1.0", path: lib_path("foo")
307+
308+
relative_path = lib_path("foo").relative_path_from(bundled_app)
309+
install_gemfile <<-G, verbose: true
310+
source "https://gem.repo1"
311+
gem "foo", :git => "#{relative_path}"
312+
G
313+
314+
expect(out).to include "git source foo started fetch at"
315+
expect(out).to match(/git source foo took \d+\.\d+ to fetch/)
316+
end
317+
end
318+
196319
def install_gemfile_and_bundler_require
197320
install_gemfile <<-G
198321
source "https://gem.repo1"

0 commit comments

Comments
 (0)