diff options
author | The Bundler Bot <bot@bundler.io> | 2017-04-08 18:38:14 +0000 |
---|---|---|
committer | The Bundler Bot <bot@bundler.io> | 2017-04-08 18:38:14 +0000 |
commit | a2582d710577cc771b15916784d8a04b76680832 (patch) | |
tree | b63838ee52f2de88853efeaf9dc27fb09ac3010b | |
parent | cdae66803a527067fd80686301551c4e7aa7fad9 (diff) | |
parent | 5b3b3ad0116d319aaf5c4c6d6dd2796480912d59 (diff) | |
download | bundler-a2582d710577cc771b15916784d8a04b76680832.tar.gz |
Auto merge of #5568 - bundler:seg-stubs-no-load-full-spec, r=indirect
[StubSpecification] Avoid loading the full spec when possible
This should save us the hit of needing to `eval` all the gemspecs we use when the user has a modern (>= 2.3) RubyGems
-rw-r--r-- | lib/bundler/lazy_specification.rb | 1 | ||||
-rw-r--r-- | lib/bundler/remote_specification.rb | 15 | ||||
-rw-r--r-- | lib/bundler/rubygems_integration.rb | 39 | ||||
-rw-r--r-- | lib/bundler/source/git.rb | 9 | ||||
-rw-r--r-- | lib/bundler/source/path.rb | 10 | ||||
-rw-r--r-- | lib/bundler/stub_specification.rb | 66 | ||||
-rw-r--r-- | spec/bundler/plugin/installer_spec.rb | 3 | ||||
-rw-r--r-- | spec/runtime/require_spec.rb | 47 |
8 files changed, 181 insertions, 9 deletions
diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index 231bffcae8..46f50844e1 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -79,6 +79,7 @@ module Bundler "To use the platform-specific version of the gem, run `bundle config specific_platform true` and install again." search = source.specs.search(self).last end + search.dependencies = dependencies if search.is_a?(RemoteSpecification) search end end diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index e5f9c78b00..82b5cb8c6f 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -11,6 +11,7 @@ module Bundler include Comparable attr_reader :name, :version, :platform + attr_writer :dependencies attr_accessor :source, :remote def initialize(name, version, platform, spec_fetcher) @@ -18,6 +19,7 @@ module Bundler @version = Gem::Version.create version @platform = platform @spec_fetcher = spec_fetcher + @dependencies = nil end # Needed before installs, since the arch matters then and quick @@ -76,8 +78,21 @@ module Bundler "#<#{self.class} name=#{name} version=#{version} platform=#{platform}>" end + def dependencies + @dependencies || method_missing(:dependencies) + end + + def git_version + return unless loaded_from && source.is_a?(Bundler::Source::Git) + " #{source.revision[0..6]}" + end + private + def to_ary + nil + end + def _remote_specification @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @platform]) @_remote_specification || raise(GemspecError, "Gemspec data for #{full_name} was" \ diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 86912376c1..66497add02 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -80,6 +80,10 @@ module Bundler default end + def stub_set_spec(stub, spec) + stub.instance_variable_set(:@spec, spec) + end + def path(obj) obj.to_s end @@ -320,17 +324,27 @@ module Bundler end end + def binstubs_call_gem? + true + end + + def stubs_provide_full_functionality? + false + end + def replace_gem(specs, specs_by_name) reverse_rubygems_kernel_mixin - executables = specs.map(&:executables).flatten + executables = nil kernel = (class << ::Kernel; self; end) [kernel, ::Kernel].each do |kernel_class| redefine_method(kernel_class, :gem) do |dep, *reqs| - if executables.include? File.basename(caller.first.split(":").first) + executables ||= specs.map(&:executables).flatten if ::Bundler.rubygems.binstubs_call_gem? + if executables && executables.include?(File.basename(caller.first.split(":").first)) break end + reqs.pop if reqs.last.is_a?(Hash) unless dep.respond_to?(:name) && dep.respond_to?(:requirement) @@ -432,8 +446,10 @@ module Bundler # Copy of Rubygems activate_bin_path impl requirement = args.last spec = find_spec_for_exe name, exec_name, [requirement] - Gem::LOADED_SPECS_MUTEX.synchronize { spec.activate } - spec.bin_file exec_name + + gem_bin = File.join(spec.full_gem_path, spec.bindir, exec_name) + gem_from_path_bin = File.join(File.dirname(spec.loaded_from), spec.bindir, exec_name) + File.exist?(gem_bin) ? gem_bin : gem_from_path_bin end redefine_method(gem_class, :bin_path) do |name, *args| @@ -795,6 +811,21 @@ module Bundler activated_spec_names = runtime.requested_specs.map(&:to_spec).sort_by(&:name) [Gemdeps.new(runtime), activated_spec_names] end + + if provides?(">= 2.5.2") + # RubyGems-generated binstubs call Kernel#gem + def binstubs_call_gem? + false + end + + # only 2.5.2+ has all of the stub methods we want to use, and since this + # is a performance optimization _only_, + # we'll restrict ourselves to the most + # recent RG versions instead of all versions that have stubs + def stubs_provide_full_functionality? + true + end + end end end diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index c95c94583f..f6efd588f9 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -234,6 +234,7 @@ module Bundler # The gemspecs we cache should already be evaluated. spec = Bundler.load_gemspec(spec_path) next unless spec + Bundler.rubygems.set_installed_by_version(spec) Bundler.rubygems.validate(spec) File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) } end @@ -302,6 +303,14 @@ module Bundler # no-op, since we validate when re-serializing the gemspec def validate_spec(_spec); end + + if Bundler.rubygems.stubs_provide_full_functionality? + def load_gemspec(file) + stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent) + stub.full_gem_path = Pathname.new(file).dirname.expand_path(root).to_s.untaint + StubSpecification.from_stub(stub) + end + end end end end diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 4b01496aac..0ad0a029f0 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -144,6 +144,12 @@ module Bundler SharedHelpers.in_bundle? && app_cache_path.exist? end + def load_gemspec(file) + return unless spec = Bundler.load_gemspec(file) + Bundler.rubygems.set_installed_by_version(spec) + spec + end + def validate_spec(spec) Bundler.rubygems.validate(spec) end @@ -154,9 +160,9 @@ module Bundler if File.directory?(expanded_path) # We sort depth-first since `<<` will override the earlier-found specs Dir["#{expanded_path}/#{@glob}"].sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| - next unless spec = Bundler.load_gemspec(file) + next unless spec = load_gemspec(file) spec.source = self - Bundler.rubygems.set_installed_by_version(spec) + # Validation causes extension_dir to be calculated, which depends # on #source, so we validate here instead of load_gemspec validate_spec(spec) diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb index cbcadee269..257a472224 100644 --- a/lib/bundler/stub_specification.rb +++ b/lib/bundler/stub_specification.rb @@ -9,12 +9,14 @@ module Bundler spec end - attr_accessor :stub + attr_accessor :stub, :ignored def to_yaml _remote_specification.to_yaml end + # @!group Stub Delegates + if Bundler.rubygems.provides?(">= 2.3") # This is defined directly to avoid having to load every installed spec def missing_extensions? @@ -22,10 +24,70 @@ module Bundler end end + def activated + stub.activated + end + + def activated=(activated) + stub.instance_variable_set(:@activated, activated) + end + + def default_gem + stub.default_gem + end + + def full_gem_path + # deleted gems can have their stubs return nil, so in that case grab the + # expired path from the full spec + stub.full_gem_path || method_missing(:full_gem_path) + end + + if Bundler.rubygems.provides?(">= 2.2.0") + def full_require_paths + stub.full_require_paths + end + + # This is what we do in bundler/rubygems_ext + # full_require_paths is always implemented in >= 2.2.0 + def load_paths + full_require_paths + end + end + + def loaded_from + stub.loaded_from + end + + if Bundler.rubygems.stubs_provide_full_functionality? + def matches_for_glob(glob) + stub.matches_for_glob(glob) + end + end + + def raw_require_paths + stub.raw_require_paths + end + private def _remote_specification - stub.to_spec + @_remote_specification ||= begin + rs = stub.to_spec + if rs.equal?(self) # happens when to_spec gets the spec from Gem.loaded_specs + rs = Gem::Specification.load(loaded_from) + Bundler.rubygems.stub_set_spec(stub, rs) + end + + unless rs + raise GemspecError, "The gemspec for #{full_name} at #{loaded_from}" \ + " was missing or broken. Try running `gem pristine #{name} -v #{version}`" \ + " to fix the cached spec." + end + + rs.source = source + + rs + end end end end diff --git a/spec/bundler/plugin/installer_spec.rb b/spec/bundler/plugin/installer_spec.rb index 2454ef00f9..e8d5941e33 100644 --- a/spec/bundler/plugin/installer_spec.rb +++ b/spec/bundler/plugin/installer_spec.rb @@ -54,7 +54,8 @@ RSpec.describe Bundler::Plugin::Installer do end it "returns the installed spec after installing" do - expect(result["ga-plugin"]).to be_kind_of(Gem::Specification) + spec = result["ga-plugin"] + expect(spec.full_name).to eq "ga-plugin-1.0" end it "has expected full gem path" do diff --git a/spec/runtime/require_spec.rb b/spec/runtime/require_spec.rb index 2d8935d2af..b68313726b 100644 --- a/spec/runtime/require_spec.rb +++ b/spec/runtime/require_spec.rb @@ -360,6 +360,53 @@ RSpec.describe "Bundler.require" do end end end + + it "does not load rubygems gemspecs that are used", :rubygems => ">= 2.5.2" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + run! <<-R + path = File.join(Gem.dir, "specifications", "rack-1.0.0.gemspec") + contents = File.read(path) + contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join + File.open(path, "w") do |f| + f.write contents + end + R + + run! <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "does not load git gemspecs that are used", :rubygems => ">= 2.5.2" do + build_git "foo" + + install_gemfile! <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + path = Gem.loaded_specs["foo"].loaded_from + contents = File.read(path) + contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join + File.open(path, "w") do |f| + f.write contents + end + R + + run! <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end end RSpec.describe "Bundler.require with platform specific dependencies" do |