diff --git a/bundler/lib/bundler/definition.rb b/bundler/lib/bundler/definition.rb index 6f5ab29b1556..ae51be9e35c3 100644 --- a/bundler/lib/bundler/definition.rb +++ b/bundler/lib/bundler/definition.rb @@ -38,7 +38,10 @@ def self.build(gemfile, lockfile, unlock) raise GemfileNotFound, "#{gemfile} not found" unless gemfile.file? - Dsl.evaluate(gemfile, lockfile, unlock) + Plugin.hook(Plugin::Events::GEM_BEFORE_EVAL, gemfile, lockfile) + Dsl.evaluate(gemfile, lockfile, unlock).tap do |definition| + Plugin.hook(Plugin::Events::GEM_AFTER_EVAL, definition) + end end # diff --git a/bundler/lib/bundler/plugin/events.rb b/bundler/lib/bundler/plugin/events.rb index 29c05098ae14..3fbf60307ec1 100644 --- a/bundler/lib/bundler/plugin/events.rb +++ b/bundler/lib/bundler/plugin/events.rb @@ -30,6 +30,54 @@ def self.defined_event?(event) @events.key?(event) end + # @!parse + # A hook called before the Gemfile is evaluated + # Includes the Gemfile path and the Lockfile path + # GEM_BEFORE_EVAL = "before-eval" + define :GEM_BEFORE_EVAL, "before-eval" + + # @!parse + # A hook called after the Gemfile is evaluated + # Includes a Bundler::Definition + # GEM_AFTER_EVAL = "after-eval" + define :GEM_AFTER_EVAL, "after-eval" + + # @!parse + # A hook called before any gems install + # Includes an Array of Bundler::Dependency objects + # GEM_BEFORE_INSTALL_ALL = "before-install-all" + define :GEM_BEFORE_INSTALL_ALL, "before-install-all" + + # @!parse + # A hook called before each individual gem is downloaded from a remote source. + # Includes a spec-like object responding to the Gem::Specification API + # (for example, a Bundler spec proxy such as Bundler::EndpointSpecification + # or Bundler::RemoteSpecification). Does not fire when the gem is already + # present at the initial download-cache check. + # GEM_BEFORE_FETCH = "before-fetch" + define :GEM_BEFORE_FETCH, "before-fetch" + + # @!parse + # A hook called after each individual gem is downloaded from a remote source. + # Includes a spec-like object responding to the Gem::Specification API + # (for example, a Bundler spec proxy such as Bundler::EndpointSpecification + # or Bundler::RemoteSpecification). Does not fire when the gem is already + # present at the initial download-cache check. + # GEM_AFTER_FETCH = "after-fetch" + define :GEM_AFTER_FETCH, "after-fetch" + + # @!parse + # A hook called before a git source is fetched or checked out. + # Includes a Bundler::Source::Git reference. + # GIT_BEFORE_FETCH = "before-git-fetch" + define :GIT_BEFORE_FETCH, "before-git-fetch" + + # @!parse + # A hook called after a git source is fetched or checked out. + # Includes a Bundler::Source::Git reference. + # GIT_AFTER_FETCH = "after-git-fetch" + define :GIT_AFTER_FETCH, "after-git-fetch" + # @!parse # A hook called before each individual gem is installed # Includes a Bundler::ParallelInstaller::SpecInstallation. @@ -45,18 +93,18 @@ def self.defined_event?(event) # GEM_AFTER_INSTALL = "after-install" define :GEM_AFTER_INSTALL, "after-install" - # @!parse - # A hook called before any gems install - # Includes an Array of Bundler::Dependency objects - # GEM_BEFORE_INSTALL_ALL = "before-install-all" - define :GEM_BEFORE_INSTALL_ALL, "before-install-all" - # @!parse # A hook called after any gems install # Includes an Array of Bundler::Dependency objects # GEM_AFTER_INSTALL_ALL = "after-install-all" define :GEM_AFTER_INSTALL_ALL, "after-install-all" + # @!parse + # A hook called before any gems require + # Includes an Array of Bundler::Dependency objects. + # GEM_BEFORE_REQUIRE_ALL = "before-require-all" + define :GEM_BEFORE_REQUIRE_ALL, "before-require-all" + # @!parse # A hook called before each individual gem is required # Includes a Bundler::Dependency. @@ -69,17 +117,11 @@ def self.defined_event?(event) # GEM_AFTER_REQUIRE = "after-require" define :GEM_AFTER_REQUIRE, "after-require" - # @!parse - # A hook called before any gems require - # Includes an Array of Bundler::Dependency objects. - # GEM_BEFORE_REQUIRE_ALL = "before-require-all" - define :GEM_BEFORE_REQUIRE_ALL, "before-require-all" - # @!parse # A hook called after all gems required # Includes an Array of Bundler::Dependency objects. # GEM_AFTER_REQUIRE_ALL = "after-require-all" - define :GEM_AFTER_REQUIRE_ALL, "after-require-all" + define :GEM_AFTER_REQUIRE_ALL, "after-require-all" end end end diff --git a/bundler/lib/bundler/source/git.rb b/bundler/lib/bundler/source/git.rb index bb669ebba39d..a002a2570a8a 100644 --- a/bundler/lib/bundler/source/git.rb +++ b/bundler/lib/bundler/source/git.rb @@ -191,8 +191,13 @@ def specs(*) set_cache_path!(app_cache_path) if use_app_cache? if requires_checkout? && !@copied - fetch unless use_app_cache? - checkout + Plugin.hook(Plugin::Events::GIT_BEFORE_FETCH, self) + begin + fetch unless use_app_cache? + checkout + ensure + Plugin.hook(Plugin::Events::GIT_AFTER_FETCH, self) + end end local_specs diff --git a/bundler/lib/bundler/source/rubygems.rb b/bundler/lib/bundler/source/rubygems.rb index b5c3b9169d16..7a94c1399afd 100644 --- a/bundler/lib/bundler/source/rubygems.rb +++ b/bundler/lib/bundler/source/rubygems.rb @@ -477,8 +477,13 @@ def download_gem(spec, download_cache_path, previous_spec = nil) Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}") gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher - Gem.time("Downloaded #{spec.name} in", 0, true) do - Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher) + Plugin.hook(Plugin::Events::GEM_BEFORE_FETCH, spec) + begin + Gem.time("Downloaded #{spec.name} in", 0, true) do + Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher) + end + ensure + Plugin.hook(Plugin::Events::GEM_AFTER_FETCH, spec) end end diff --git a/spec/commands/doctor_spec.rb b/spec/commands/doctor_spec.rb index 5ceaf37f29c0..d350b4b3d10d 100644 --- a/spec/commands/doctor_spec.rb +++ b/spec/commands/doctor_spec.rb @@ -34,6 +34,8 @@ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [unwritable_file] } allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:writable?).and_call_original + allow(File).to receive(:readable?).and_call_original allow(File).to receive(:exist?).with(unwritable_file).and_return(true) allow(File).to receive(:stat).with(unwritable_file) { stat } allow(stat).to receive(:uid) { Process.uid } @@ -108,6 +110,8 @@ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@unwritable_file] } allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:writable?).and_call_original + allow(File).to receive(:readable?).and_call_original allow(File).to receive(:exist?).with(@unwritable_file) { true } allow(File).to receive(:stat).with(@unwritable_file) { @stat } end diff --git a/spec/plugins/hook_spec.rb b/spec/plugins/hook_spec.rb index 3f9053bbc83d..ad8a4daeff9c 100644 --- a/spec/plugins/hook_spec.rb +++ b/spec/plugins/hook_spec.rb @@ -193,6 +193,129 @@ end end + context "before-eval hook" do + before do + build_repo2 do + build_plugin "before-eval-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_EVAL do |gemfile, lockfile| + puts "hooked eval start of \#{File.basename(gemfile)} to \#{File.basename(lockfile)}" + end + RUBY + end + end + + bundle "plugin install before-eval-plugin --source https://gem.repo2" + end + + it "runs before the Gemfile is evaluated" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + G + + expect(out).to include "hooked eval start of Gemfile to Gemfile.lock" + end + end + + context "after-eval hook" do + before do + build_repo2 do + build_plugin "after-eval-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_EVAL do |defn| + puts "hooked eval after with gems \#{defn.dependencies.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install after-eval-plugin --source https://gem.repo2" + end + + it "runs after the Gemfile is evaluated" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "rake" + G + + expect(out).to include "hooked eval after with gems myrack, rake" + end + end + + context "before-fetch and after-fetch hooks" do + before do + build_repo2 do + build_plugin "fetch-timing-plugin" do |s| + s.write "plugins.rb", <<-RUBY + @timing_start = nil + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_FETCH do |spec| + @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "gem \#{spec.name} started fetch at \#{@timing_start}" + end + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_FETCH do |spec| + timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "gem \#{spec.name} took \#{timing_end - @timing_start} to fetch" + @timing_start = nil + end + RUBY + end + end + + bundle "plugin install fetch-timing-plugin --source https://gem.repo2" + end + + it "runs around each gem download" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "myrack" + G + + expect(out).to include "gem rake started fetch at" + expect(out).to match(/gem rake took \d+\.\d+ to fetch/) + expect(out).to include "gem myrack started fetch at" + expect(out).to match(/gem myrack took \d+\.\d+ to fetch/) + end + end + + context "before-git-fetch and after-git-fetch hooks" do + before do + build_repo2 do + build_plugin "git-fetch-timing-plugin" do |s| + s.write "plugins.rb", <<-RUBY + @timing_start = nil + Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_BEFORE_FETCH do |source| + @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "git source \#{source.name} started fetch at \#{@timing_start}" + end + Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_AFTER_FETCH do |source| + timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "git source \#{source.name} took \#{timing_end - @timing_start} to fetch" + @timing_start = nil + end + RUBY + end + end + + bundle "plugin install git-fetch-timing-plugin --source https://gem.repo2" + end + + it "runs around each git source fetch" do + build_git "foo", "1.0", path: lib_path("foo") + + relative_path = lib_path("foo").relative_path_from(bundled_app) + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{relative_path}" + G + + expect(out).to include "git source foo started fetch at" + expect(out).to match(/git source foo took \d+\.\d+ to fetch/) + end + end + def install_gemfile_and_bundler_require install_gemfile <<-G source "https://gem.repo1"