summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThe Bundler Bot <bot@bundler.io>2017-04-08 18:38:14 +0000
committerThe Bundler Bot <bot@bundler.io>2017-04-08 18:38:14 +0000
commita2582d710577cc771b15916784d8a04b76680832 (patch)
treeb63838ee52f2de88853efeaf9dc27fb09ac3010b
parentcdae66803a527067fd80686301551c4e7aa7fad9 (diff)
parent5b3b3ad0116d319aaf5c4c6d6dd2796480912d59 (diff)
downloadbundler-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.rb1
-rw-r--r--lib/bundler/remote_specification.rb15
-rw-r--r--lib/bundler/rubygems_integration.rb39
-rw-r--r--lib/bundler/source/git.rb9
-rw-r--r--lib/bundler/source/path.rb10
-rw-r--r--lib/bundler/stub_specification.rb66
-rw-r--r--spec/bundler/plugin/installer_spec.rb3
-rw-r--r--spec/runtime/require_spec.rb47
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