summaryrefslogtreecommitdiff
path: root/lib/bundler/source
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/source')
-rw-r--r--lib/bundler/source/gemspec.rb5
-rw-r--r--lib/bundler/source/git.rb160
-rw-r--r--lib/bundler/source/git/git_proxy.rb95
-rw-r--r--lib/bundler/source/metadata.rb11
-rw-r--r--lib/bundler/source/path.rb49
-rw-r--r--lib/bundler/source/path/installer.rb2
-rw-r--r--lib/bundler/source/rubygems.rb221
-rw-r--r--lib/bundler/source/rubygems/remote.rb14
-rw-r--r--lib/bundler/source/rubygems_aggregate.rb5
9 files changed, 367 insertions, 195 deletions
diff --git a/lib/bundler/source/gemspec.rb b/lib/bundler/source/gemspec.rb
index 7e3447e776..ed766dbe74 100644
--- a/lib/bundler/source/gemspec.rb
+++ b/lib/bundler/source/gemspec.rb
@@ -4,14 +4,15 @@ module Bundler
class Source
class Gemspec < Path
attr_reader :gemspec
+ attr_writer :checksum_store
def initialize(options)
super
@gemspec = options["gemspec"]
end
- def as_path_source
- Path.new(options)
+ def to_s
+ "gemspec at `#{@path}`"
end
end
end
diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb
index 198e335bb6..a002a2570a 100644
--- a/lib/bundler/source/git.rb
+++ b/lib/bundler/source/git.rb
@@ -32,6 +32,20 @@ module Bundler
@local = false
end
+ def remote!
+ return if @allow_remote
+
+ @local_specs = nil
+ @allow_remote = true
+ end
+
+ def cached!
+ return if @allow_cached
+
+ @local_specs = nil
+ @allow_cached = true
+ end
+
def self.from_lock(options)
new(options.merge("uri" => options.delete("remote")))
end
@@ -56,13 +70,13 @@ module Bundler
end
def hash
- [self.class, uri, ref, branch, name, version, glob, submodules].hash
+ [self.class, uri, ref, branch, name, glob, submodules].hash
end
def eql?(other)
other.is_a?(Git) && uri == other.uri && ref == other.ref &&
branch == other.branch && name == other.name &&
- version == other.version && glob == other.glob &&
+ glob == other.glob &&
submodules == other.submodules
end
@@ -88,7 +102,7 @@ module Bundler
end
def identifier
- uri_with_specifiers([humanized_ref, cached_revision, glob_for_display])
+ uri_with_specifiers([humanized_ref, locked_revision, glob_for_display])
end
def uri_with_specifiers(specifiers)
@@ -150,7 +164,8 @@ module Bundler
"does not exist. Run `bundle config unset local.#{override_for(original_path)}` to remove the local override"
end
- set_local!(path)
+ @local = true
+ set_paths!(path)
# Create a new git proxy without the cached revision
# so the Gemfile.lock always picks up the new revision.
@@ -161,10 +176,10 @@ module Bundler
"#{current_branch} but Gemfile specifies #{branch}"
end
- changed = cached_revision && cached_revision != revision
+ changed = locked_revision && locked_revision != revision
- if !Bundler.settings[:disable_local_revision_check] && changed && !@unlocked && !git_proxy.contains?(cached_revision)
- raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(cached_revision)} " \
+ if !Bundler.settings[:disable_local_revision_check] && changed && !@unlocked && !git_proxy.contains?(locked_revision)
+ raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(locked_revision)} " \
"but the current branch in your local override for #{name} does not contain such commit. " \
"Please make sure your branch is up to date."
end
@@ -173,13 +188,16 @@ module Bundler
end
def specs(*)
- set_local!(app_cache_path) if has_app_cache? && !local?
+ set_cache_path!(app_cache_path) if use_app_cache?
if requires_checkout? && !@copied
- fetch
- git_proxy.copy_to(install_path, submodules)
- serialize_gemspecs_in(install_path)
- @copied = true
+ 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
@@ -192,27 +210,25 @@ module Bundler
print_using_message "Using #{version_message(spec, options[:previous_spec])} from #{self}"
if (requires_checkout? && !@copied) || force
- Bundler.ui.debug " * Checking out revision: #{ref}"
- git_proxy.copy_to(install_path, submodules)
- serialize_gemspecs_in(install_path)
- @copied = true
+ checkout
end
- generate_bin_options = { disable_extensions: !Bundler.rubygems.spec_missing_extensions?(spec), build_args: options[:build_args] }
+ generate_bin_options = { disable_extensions: !spec.missing_extensions?, build_args: options[:build_args] }
generate_bin(spec, generate_bin_options)
requires_checkout? ? spec.post_install_message : nil
end
+ def migrate_cache(custom_path = nil, local: false)
+ if local
+ cache_to(custom_path, try_migrate: false)
+ else
+ cache_to(custom_path, try_migrate: true)
+ end
+ end
+
def cache(spec, custom_path = nil)
- app_cache_path = app_cache_path(custom_path)
- return unless Bundler.feature_flag.cache_all?
- return if path == app_cache_path
- cached!
- FileUtils.rm_rf(app_cache_path)
- git_proxy.checkout if requires_checkout?
- git_proxy.copy_to(app_cache_path, @submodules)
- serialize_gemspecs_in(app_cache_path)
+ cache_to(custom_path, try_migrate: false)
end
def load_spec_files
@@ -227,7 +243,7 @@ module Bundler
# across different projects, this cache will be shared.
# When using local git repos, this is set to the local repo.
def cache_path
- @cache_path ||= if Bundler.feature_flag.global_gem_cache?
+ @cache_path ||= if Bundler.settings[:global_gem_cache]
Bundler.user_cache
else
Bundler.bundle_path.join("cache", "bundler")
@@ -235,7 +251,7 @@ module Bundler
end
def app_cache_dirname
- "#{base_name}-#{shortref_for_path(cached_revision || revision)}"
+ "#{base_name}-#{shortref_for_path(locked_revision || revision)}"
end
def revision
@@ -256,6 +272,43 @@ module Bundler
private
+ def cache_to(custom_path, try_migrate: false)
+ return unless Bundler.settings[:cache_all]
+
+ app_cache_path = app_cache_path(custom_path)
+
+ migrate = try_migrate ? bare_repo?(app_cache_path) : false
+
+ set_cache_path!(nil) if migrate
+
+ return if cache_path == app_cache_path
+
+ cached!
+ FileUtils.rm_rf(app_cache_path)
+ git_proxy.checkout if migrate || requires_checkout?
+ git_proxy.copy_to(app_cache_path, @submodules)
+ serialize_gemspecs_in(app_cache_path)
+ end
+
+ def checkout
+ Bundler.ui.debug " * Checking out revision: #{ref}"
+ if use_app_cache? && !bare_repo?(app_cache_path)
+ SharedHelpers.filesystem_access(install_path.dirname) do |p|
+ FileUtils.mkdir_p(p)
+ end
+ FileUtils.cp_r("#{app_cache_path}/.", install_path)
+ else
+ if use_app_cache? && bare_repo?(app_cache_path)
+ Bundler.ui.warn "Installing from cache in old \"bare repository\" format for compatibility. " \
+ "Please run `bundle cache` and commit the updated cache to migrate to the new format and get rid of this warning."
+ end
+
+ git_proxy.copy_to(install_path, submodules)
+ end
+ serialize_gemspecs_in(install_path)
+ @copied = true
+ end
+
def humanized_ref
if local?
path
@@ -278,28 +331,45 @@ 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)
+ spec.installed_by_version = Gem::VERSION
Bundler.rubygems.validate(spec)
File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) }
end
end
- def set_local!(path)
- @local = true
- @local_specs = @git_proxy = nil
- @cache_path = @install_path = path
+ def set_paths!(path)
+ set_cache_path!(path)
+ set_install_path!(path)
+ end
+
+ def set_cache_path!(path)
+ @git_proxy = nil
+ @cache_path = path
+ end
+
+ def set_install_path!(path)
+ @local_specs = nil
+ @install_path = path
end
def has_app_cache?
- cached_revision && super
+ locked_revision && super
+ end
+
+ def use_app_cache?
+ has_app_cache? && !local?
end
def requires_checkout?
- allow_git_ops? && !local? && !cached_revision_checked_out?
+ allow_git_ops? && !local? && !locked_revision_checked_out?
+ end
+
+ def locked_revision_checked_out?
+ locked_revision && locked_revision == revision && installed?
end
- def cached_revision_checked_out?
- cached_revision && cached_revision == revision && install_path.exist?
+ def installed?
+ git_proxy.installed_to?(install_path)
end
def base_name
@@ -336,7 +406,7 @@ module Bundler
Bundler::Digest.sha1(input)
end
- def cached_revision
+ def locked_revision
options["revision"]
end
@@ -345,13 +415,12 @@ module Bundler
end
def git_proxy
- @git_proxy ||= GitProxy.new(cache_path, uri, options, cached_revision, self)
+ @git_proxy ||= GitProxy.new(cache_path, uri, options, locked_revision, self)
end
def fetch
git_proxy.checkout
rescue GitError => e
- raise unless Bundler.feature_flag.allow_offline_install?
Bundler.ui.warn "Using cached git data because of network errors:\n#{e}"
end
@@ -359,9 +428,12 @@ module Bundler
def validate_spec(_spec); end
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
- StubSpecification.from_stub(stub)
+ dirname = Pathname.new(file).dirname
+ SharedHelpers.chdir(dirname.to_s) do
+ stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent)
+ stub.full_gem_path = dirname.expand_path(root).to_s
+ StubSpecification.from_stub(stub)
+ end
end
def git_scope
@@ -375,6 +447,10 @@ module Bundler
def override_for(path)
Bundler.settings.local_overrides.key(path)
end
+
+ def bare_repo?(path)
+ File.exist?(path.join("objects")) && File.exist?(path.join("HEAD"))
+ end
end
end
end
diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb
index 645851286c..72f7dc7710 100644
--- a/lib/bundler/source/git/git_proxy.rb
+++ b/lib/bundler/source/git/git_proxy.rb
@@ -16,7 +16,7 @@ module Bundler
def initialize(command)
msg = String.new
msg << "Bundler is trying to run `#{command}` at runtime. You probably need to run `bundle install`. However, "
- msg << "this error message could probably be more useful. Please submit a ticket at https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md "
+ msg << "this error message could probably be more useful. Please submit a ticket at https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md "
msg << "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
super msg
end
@@ -57,6 +57,29 @@ module Bundler
attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref
attr_writer :revision
+ def self.version
+ @version ||= full_version[/((\.?\d+)+).*/, 1]
+ end
+
+ def self.full_version
+ @full_version ||= begin
+ raise GitNotInstalledError.new unless Bundler.git_present?
+
+ require "open3"
+ out, err, status = Open3.capture3("git", "--version")
+
+ raise GitCommandError.new("--version", SharedHelpers.pwd, err) unless status.success?
+ Bundler.ui.warn err unless err.empty?
+
+ out.sub(/git version\s*/, "").strip
+ end
+ end
+
+ def self.reset
+ @version = nil
+ @full_version = nil
+ end
+
def initialize(path, uri, options = {}, revision = nil, git = nil)
@path = path
@uri = uri
@@ -92,11 +115,11 @@ module Bundler
end
def version
- @version ||= full_version.match(/((\.?\d+)+).*/)[1]
+ self.class.version
end
def full_version
- @full_version ||= git_local("--version").sub(/git version\s*/, "").strip
+ self.class.full_version
end
def checkout
@@ -121,7 +144,7 @@ module Bundler
FileUtils.rm_rf(p)
end
git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
- File.chmod(((File.stat(destination).mode | 0o777) & ~File.umask), destination)
+ File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination)
rescue Errno::EEXIST => e
file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1]
raise GitError, "Bundler could not install a gem because it needs to " \
@@ -137,7 +160,7 @@ module Bundler
git "fetch", "--force", "--quiet", *extra_fetch_args(ref), dir: destination
end
- git "reset", "--hard", @revision, dir: destination
+ git "reset", "--hard", revision, dir: destination
if submodules
git_retry "submodule", "update", "--init", "--recursive", dir: destination
@@ -147,10 +170,16 @@ module Bundler
end
end
+ def installed_to?(destination)
+ # if copy_to is interrupted, it may leave a partially installed directory that
+ # contains .git but no other files -- consider this not to be installed
+ Dir.exist?(destination) && (Dir.children(destination) - [".git"]).any?
+ end
+
private
def git_remote_fetch(args)
- command = ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact
+ command = fetch_command(args)
command_with_no_credentials = check_allowed(command)
Bundler::Retry.new("`#{command_with_no_credentials}` at #{path}", [MissingGitRevisionError]).attempts do
@@ -160,6 +189,11 @@ module Bundler
if err.include?("couldn't find remote ref") || err.include?("not our ref")
raise MissingGitRevisionError.new(command_with_no_credentials, path, commit || explicit_ref, credential_filtered_uri)
else
+ if shallow?
+ args -= depth_args
+ command = fetch_command(args)
+ command_with_no_credentials = check_allowed(command)
+ end
raise GitCommandError.new(command_with_no_credentials, path, err)
end
end
@@ -172,16 +206,23 @@ module Bundler
FileUtils.mkdir_p(p)
end
- command = ["clone", "--bare", "--no-hardlinks", "--quiet", *extra_clone_args, "--", configured_uri, path.to_s]
+ clone_args = extra_clone_args
+ command = clone_command(clone_args)
command_with_no_credentials = check_allowed(command)
Bundler::Retry.new("`#{command_with_no_credentials}`", [MissingGitRevisionError]).attempts do
_, err, status = capture(command, nil)
return extra_ref if status.success?
- if err.include?("Could not find remote branch")
+ if err.include?("Could not find remote branch") || # git up to 2.49
+ err.include?("Remote branch #{branch_option} not found") # git 2.49 or higher
raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri)
else
+ if shallow?
+ clone_args -= depth_args
+ command = clone_command(clone_args)
+ command_with_no_credentials = check_allowed(command)
+ end
raise GitCommandError.new(command_with_no_credentials, path, err)
end
end
@@ -189,14 +230,14 @@ module Bundler
def clone_needs_unshallow?
return false unless path.join("shallow").exist?
- return true if full_clone?
+ return true unless shallow?
@revision && @revision != head_revision
end
def extra_ref
return false if not_pinned?
- return true unless full_clone?
+ return true if shallow?
ref.start_with?("refs/")
end
@@ -248,7 +289,7 @@ module Bundler
end
def not_pinned?
- branch || tag || ref.nil?
+ branch_option || ref.nil?
end
def pinned_to_full_sha?
@@ -290,8 +331,8 @@ module Bundler
end
def has_revision_cached?
- return unless @revision && path.exist?
- git("cat-file", "-e", @revision, dir: path)
+ return unless commit && path.exist?
+ git("cat-file", "-e", commit, dir: path)
true
rescue GitError
false
@@ -324,8 +365,6 @@ module Bundler
config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host]
remote.userinfo ||= config_auth
remote.to_s
- elsif File.exist?(uri)
- "file://#{uri}"
else
uri.to_s
end
@@ -395,11 +434,7 @@ module Bundler
def capture3_args_for(cmd, dir)
return ["git", *cmd] unless dir
- if Bundler.feature_flag.bundler_3_mode? || supports_minus_c?
- ["git", "-C", dir.to_s, *cmd]
- else
- ["git", *cmd, { chdir: dir.to_s }]
- end
+ ["git", "-C", dir.to_s, *cmd]
end
def extra_clone_args
@@ -414,12 +449,20 @@ module Bundler
# anyways.
return args if @revision
- args += ["--branch", branch || tag] if branch || tag
+ args += ["--branch", branch_option] if branch_option
args
end
+ def fetch_command(args)
+ ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact
+ end
+
+ def clone_command(args)
+ ["clone", "--bare", "--no-hardlinks", "--quiet", *args, "--", configured_uri, path.to_s]
+ end
+
def depth_args
- return [] if full_clone?
+ return [] unless shallow?
["--depth", depth.to_s]
end
@@ -430,12 +473,12 @@ module Bundler
extra_args
end
- def full_clone?
- depth.nil?
+ def branch_option
+ branch || tag
end
- def supports_minus_c?
- @supports_minus_c ||= Gem::Version.new(version) >= Gem::Version.new("1.8.5")
+ def shallow?
+ !depth.nil?
end
def needs_allow_any_sha1_in_want?
diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb
index 4d27761365..ecf8895187 100644
--- a/lib/bundler/source/metadata.rb
+++ b/lib/bundler/source/metadata.rb
@@ -11,6 +11,8 @@ module Bundler
end
if local_spec = Gem.loaded_specs["bundler"]
+ raise CorruptBundlerInstallError.new(local_spec) if local_spec.version.to_s != Bundler::VERSION
+
idx << local_spec
else
idx << Gem::Specification.new do |s|
@@ -22,9 +24,8 @@ module Bundler
s.bindir = "exe"
s.homepage = "https://bundler.io"
s.summary = "The best way to manage your application's dependencies"
- s.executables = %w[bundle]
- # can't point to the actual gemspec or else the require paths will be wrong
- s.loaded_from = __dir__
+ s.executables = %w[bundle bundler]
+ s.loaded_from = SharedHelpers.gemspec_path
end
end
@@ -57,6 +58,10 @@ module Bundler
def version_message(spec)
"#{spec.name} #{spec.version}"
end
+
+ def checksum_store
+ @checksum_store ||= Checksum::Store.new
+ end
end
end
end
diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb
index 978b0b2c9f..366a23aea7 100644
--- a/lib/bundler/source/path.rb
+++ b/lib/bundler/source/path.rb
@@ -18,16 +18,13 @@ module Bundler
@options = options.dup
@glob = options["glob"] || DEFAULT_GLOB
- @allow_cached = false
- @allow_remote = false
-
@root_path = options["root_path"] || root
if options["path"]
@path = Pathname.new(options["path"])
expanded_path = expand(@path)
@path = if @path.relative?
- expanded_path.relative_path_from(root_path.expand_path)
+ expanded_path.relative_path_from(File.expand_path(root_path))
else
expanded_path
end
@@ -41,16 +38,6 @@ module Bundler
@original_path = @path
end
- def remote!
- @local_specs = nil
- @allow_remote = true
- end
-
- def cached!
- @local_specs = nil
- @allow_cached = true
- end
-
def self.from_lock(options)
new(options.merge("path" => options.delete("remote")))
end
@@ -66,13 +53,17 @@ module Bundler
"source at `#{@path}`"
end
+ alias_method :identifier, :to_s
+
+ alias_method :to_gemfile, :path
+
def hash
[self.class, expanded_path, version].hash
end
def eql?(other)
- return unless other.class == self.class
- expanded_original_path == other.expanded_original_path &&
+ [Gemspec, Path].include?(other.class) &&
+ expanded_original_path == other.expanded_original_path &&
version == other.version
end
@@ -92,7 +83,7 @@ module Bundler
def cache(spec, custom_path = nil)
app_cache_path = app_cache_path(custom_path)
- return unless Bundler.feature_flag.cache_all?
+ return unless Bundler.settings[:cache_all]
return if expand(@original_path).to_s.index(root_path.to_s + "/") == 0
unless @original_path.exist?
@@ -135,11 +126,7 @@ module Bundler
end
def expand(somepath)
- if Bundler.current_ruby.jruby? # TODO: Unify when https://github.com/rubygems/bundler/issues/7598 fixed upstream and all supported jrubies include the fix
- somepath.expand_path(root_path).expand_path
- else
- somepath.expand_path(root_path)
- end
+ somepath.expand_path(root_path)
rescue ArgumentError => e
Bundler.ui.debug(e)
raise PathError, "There was an error while trying to use the path " \
@@ -161,7 +148,7 @@ module Bundler
def load_gemspec(file)
return unless spec = Bundler.load_gemspec(file)
- Bundler.rubygems.set_installed_by_version(spec)
+ spec.installed_by_version = Gem::VERSION
spec
end
@@ -178,6 +165,13 @@ module Bundler
next unless spec = load_gemspec(file)
spec.source = self
+ # The ignore attribute is for ignoring installed gems that don't
+ # have extensions correctly compiled for activation. In the case of
+ # path sources, there's a single version of each gem in the path
+ # source available to Bundler, so we always certainly want to
+ # consider that for activation and never makes sense to ignore it.
+ spec.ignored = false
+
# Validation causes extension_dir to be calculated, which depends
# on #source, so we validate here instead of load_gemspec
validate_spec(spec)
@@ -225,15 +219,16 @@ module Bundler
# Some gem authors put absolute paths in their gemspec
# and we have to save them from themselves
- spec.files = spec.files.map do |path|
- next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path)
+ spec.files = spec.files.filter_map do |path|
+ pathname = Pathname.new(path)
+ next path unless pathname.absolute?
next if File.directory?(path)
begin
- Pathname.new(path).relative_path_from(gem_dir).to_s
+ pathname.relative_path_from(gem_dir).to_s
rescue ArgumentError
path
end
- end.compact
+ end
installer = Path::Installer.new(
spec,
diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb
index 0af28fe770..39765e5da2 100644
--- a/lib/bundler/source/path/installer.rb
+++ b/lib/bundler/source/path/installer.rb
@@ -24,7 +24,7 @@ module Bundler
def post_install
run_hooks(:pre_install)
- unless @disable_extensions
+ unless @disable_extensions || Bundler.settings[:no_build_extension]
build_extensions
run_hooks(:post_build)
end
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
index 04cfc0a850..d610ce3fdf 100644
--- a/lib/bundler/source/rubygems.rb
+++ b/lib/bundler/source/rubygems.rb
@@ -8,26 +8,36 @@ module Bundler
autoload :Remote, File.expand_path("rubygems/remote", __dir__)
# Ask for X gems per API request
- API_REQUEST_SIZE = 50
+ API_REQUEST_SIZE = 100
+ REQUIRE_MUTEX = Mutex.new
- attr_reader :remotes
+ attr_accessor :remotes
def initialize(options = {})
@options = options
@remotes = []
@dependency_names = []
@allow_remote = false
- @allow_cached = options["allow_cached"] || false
+ @allow_cached = false
@allow_local = options["allow_local"] || false
+ @prefer_local = false
@checksum_store = Checksum::Store.new
+ @gem_installers = {}
+ @gem_installers_mutex = Mutex.new
Array(options["remotes"]).reverse_each {|r| add_remote(r) }
+
+ @lockfile_remotes = @remotes if options["from_lockfile"]
end
def caches
@caches ||= [cache_path, *Bundler.rubygems.gem_cache]
end
+ def prefer_local!
+ @prefer_local = true
+ end
+
def local_only!
@specs = nil
@allow_local = true
@@ -35,6 +45,10 @@ module Bundler
@allow_remote = false
end
+ def local_only?
+ @allow_local && !@allow_remote
+ end
+
def local!
return if @allow_local
@@ -50,10 +64,11 @@ module Bundler
end
def cached!
+ return unless File.exist?(cache_path)
+
return if @allow_cached
@specs = nil
- @allow_local = true
@allow_cached = true
end
@@ -90,13 +105,13 @@ module Bundler
def self.from_lock(options)
options["remotes"] = Array(options.delete("remote")).reverse
- new(options)
+ new(options.merge("from_lockfile" => true))
end
def to_lock
out = String.new("GEM\n")
- remotes.reverse_each do |remote|
- out << " remote: #{suppress_configured_credentials remote}\n"
+ lockfile_remotes.reverse_each do |remote|
+ out << " remote: #{remote}\n"
end
out << " specs:\n"
end
@@ -135,77 +150,70 @@ module Bundler
index = @allow_remote ? remote_specs.dup : Index.new
index.merge!(cached_specs) if @allow_cached
index.merge!(installed_specs) if @allow_local
- index
- end
- end
- def install(spec, options = {})
- force = options[:force]
- ensure_builtin_gems_cached = options[:ensure_builtin_gems_cached]
-
- if ensure_builtin_gems_cached && spec.default_gem? && !cached_path(spec)
- cached_built_in_gem(spec) unless spec.remote
- force = true
- end
+ if @allow_local
+ if @prefer_local
+ index.merge!(default_specs)
+ else
+ # complete with default specs, only if not already available in the
+ # index through remote, cached, or installed specs
+ index.use(default_specs)
+ end
+ end
- if installed?(spec) && !force
- print_using_message "Using #{version_message(spec, options[:previous_spec])}"
- return nil # no post-install message
+ index
end
+ end
- if spec.remote
- # Check for this spec from other sources
- uris = [spec.remote, *remotes_for_spec(spec)].map(&:anonymized_uri).uniq
- Installer.ambiguous_gems << [spec.name, *uris] if uris.length > 1
+ def download(spec, options = {})
+ if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
+ return true
end
- path = fetch_gem_if_possible(spec, options[:previous_spec])
- raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path
-
- return if Bundler.settings[:no_install]
-
- install_path = rubygems_dir
- bin_path = Bundler.system_bindir
-
- require_relative "../rubygems_gem_installer"
-
- installer = Bundler::RubyGemsGemInstaller.at(
- path,
- security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]],
- install_dir: install_path.to_s,
- bin_dir: bin_path.to_s,
- ignore_dependencies: true,
- wrappers: true,
- env_shebang: true,
- build_args: options[:build_args],
- bundler_extension_cache_path: extension_cache_path(spec)
- )
+ installer = rubygems_gem_installer(spec, options)
if spec.remote
s = begin
installer.spec
rescue Gem::Package::FormatError
- Bundler.rm_rf(path)
+ Bundler.rm_rf(installer.gem)
raise
rescue Gem::Security::Exception => e
raise SecurityError,
- "The gem #{File.basename(path, ".gem")} can't be installed because " \
+ "The gem #{installer.gem} can't be installed because " \
"the security policy didn't allow it, with the message: #{e.message}"
end
spec.__swap__(s)
end
+ spec
+ end
+
+ def install(spec, options = {})
+ if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
+ print_using_message "Using #{version_message(spec, options[:previous_spec])}"
+ return nil # no post-install message
+ end
+
+ return if Bundler.settings[:no_install]
+
+ installer = rubygems_gem_installer(spec, options)
spec.source.checksum_store.register(spec, installer.gem_checksum)
message = "Installing #{version_message(spec, options[:previous_spec])}"
message += " with native extensions" if spec.extensions.any?
Bundler.ui.confirm message
- installed_spec = installer.install
+ installed_spec = nil
+
+ Gem.time("Installed #{spec.name} in", 0, true) do
+ installed_spec = installer.install
+ end
spec.full_gem_path = installed_spec.full_gem_path
spec.loaded_from = installed_spec.loaded_from
+ spec.base_dir = installed_spec.base_dir
spec.post_install_message
end
@@ -221,12 +229,13 @@ module Bundler
raise InstallError, e.message
end
- def cached_built_in_gem(spec)
- cached_path = cached_path(spec)
- if cached_path.nil?
+ def cached_built_in_gem(spec, local: false)
+ cached_path = cached_gem(spec)
+ if cached_path.nil? && !local
remote_spec = remote_specs.search(spec).first
if remote_spec
cached_path = fetch_gem(remote_spec)
+ spec.remote = remote_spec.remote
else
Bundler.ui.warn "#{spec.full_name} is built in to Ruby, and can't be cached because your Gemfile doesn't have any sources that contain it."
end
@@ -305,6 +314,13 @@ module Bundler
@allow_remote && api_fetchers.any?
end
+ def clear_cache
+ @specs = nil
+ @installed_specs = nil
+ @default_specs = nil
+ @cached_specs = nil
+ end
+
protected
def remote_names
@@ -312,29 +328,10 @@ module Bundler
end
def credless_remotes
- if Bundler.settings[:allow_deployment_source_credential_changes]
- remotes.map(&method(:remove_auth))
- else
- remotes.map(&method(:suppress_configured_credentials))
- end
- end
-
- def remotes_for_spec(spec)
- specs.search_all(spec.name).inject([]) do |uris, s|
- uris << s.remote if s.remote
- uris
- end
+ remotes.map(&method(:remove_auth))
end
def cached_gem(spec)
- if spec.default_gem?
- cached_built_in_gem(spec)
- else
- cached_path(spec)
- end
- end
-
- def cached_path(spec)
global_cache_path = download_cache_path(spec)
caches << global_cache_path if global_cache_path
@@ -355,15 +352,6 @@ module Bundler
uri
end
- def suppress_configured_credentials(remote)
- remote_nouser = remove_auth(remote)
- if remote.userinfo && remote.userinfo == Bundler.settings[remote_nouser]
- remote_nouser
- else
- remote
- end
- end
-
def remove_auth(remote)
if remote.user || remote.password
remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s
@@ -374,12 +362,18 @@ module Bundler
def installed_specs
@installed_specs ||= Index.build do |idx|
- Bundler.rubygems.all_specs.reverse_each do |spec|
+ Bundler.rubygems.installed_specs.reverse_each do |spec|
+ spec.source = self
+ next if spec.ignored?
+ idx << spec
+ end
+ end
+ end
+
+ def default_specs
+ @default_specs ||= Index.build do |idx|
+ Bundler.rubygems.default_specs.each do |spec|
spec.source = self
- if Bundler.rubygems.spec_missing_extensions?(spec, false)
- Bundler.ui.debug "Source #{self} is ignoring #{spec} because it is missing extensions"
- next
- end
idx << spec
end
end
@@ -452,7 +446,7 @@ module Bundler
end
def installed?(spec)
- installed_specs[spec].any? && !spec.deleted_gem?
+ installed_specs[spec].any? && !spec.installation_missing?
end
def rubygems_dir
@@ -469,6 +463,10 @@ module Bundler
private
+ def lockfile_remotes
+ @lockfile_remotes || credless_remotes
+ end
+
# Checks if the requested spec exists in the global cache. If it does,
# we copy it to the download path, and if it does not, we download it.
#
@@ -485,7 +483,15 @@ module Bundler
uri = spec.remote.uri
Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}")
gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher
- 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
# Returns the global cache path of the calling Rubygems::Source object.
@@ -500,17 +506,52 @@ module Bundler
# @return [Pathname] The global cache path.
#
def download_cache_path(spec)
- return unless Bundler.feature_flag.global_gem_cache?
+ return unless Bundler.settings[:global_gem_cache]
return unless remote = spec.remote
return unless cache_slug = remote.cache_slug
- Bundler.user_cache.join("gems", cache_slug)
+ if Gem.respond_to?(:global_gem_cache_path)
+ Pathname.new(Gem.global_gem_cache_path).join(cache_slug)
+ else
+ # Fall back to old location for older RubyGems versions
+ Bundler.user_cache.join("gems", cache_slug)
+ end
end
def extension_cache_slug(spec)
return unless remote = spec.remote
remote.cache_slug
end
+
+ # We are using a mutex to read and write from/to the hash.
+ # The reason this double synchronization was added is for performance
+ # and to lock the mutex for the shortest possible amount of time. Otherwise,
+ # all threads are fighting over this mutex and when it gets acquired it gets locked
+ # until a thread finishes downloading a gem, leaving the other threads waiting
+ # doing nothing.
+ def rubygems_gem_installer(spec, options)
+ @gem_installers_mutex.synchronize { @gem_installers[spec.name] } || begin
+ path = fetch_gem_if_possible(spec, options[:previous_spec])
+ raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path
+
+ REQUIRE_MUTEX.synchronize { require_relative "../rubygems_gem_installer" }
+
+ installer = Bundler::RubyGemsGemInstaller.at(
+ path,
+ security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]],
+ install_dir: rubygems_dir.to_s,
+ bin_dir: Bundler.system_bindir.to_s,
+ ignore_dependencies: true,
+ wrappers: true,
+ env_shebang: true,
+ build_args: options[:build_args],
+ bundler_extension_cache_path: extension_cache_path(spec),
+ build_extension: Bundler.settings[:no_build_extension] ? false : nil,
+ install_plugin: Bundler.settings[:no_install_plugin] ? false : nil
+ )
+ @gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer }
+ end
+ end
end
end
end
diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb
index 9c5c06de24..ed55912a99 100644
--- a/lib/bundler/source/rubygems/remote.rb
+++ b/lib/bundler/source/rubygems/remote.rb
@@ -16,6 +16,9 @@ module Bundler
@anonymized_uri = remove_auth(@uri).freeze
end
+ MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length
+ private_constant :MAX_CACHE_SLUG_HOST_SIZE
+
# @return [String] A slug suitable for use as a cache key for this
# remote.
#
@@ -28,10 +31,15 @@ module Bundler
host = cache_uri.to_s.start_with?("file://") ? nil : cache_uri.host
uri_parts = [host, cache_uri.user, cache_uri.port, cache_uri.path]
- uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.compact.join("."))
+ uri_parts.compact!
+ uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.join("."))
+
+ uri_parts.pop
+ host_parts = uri_parts.join(".")
+ return uri_digest if host_parts.empty?
- uri_parts[-1] = uri_digest
- uri_parts.compact.join(".")
+ shortened_host_parts = host_parts[0...MAX_CACHE_SLUG_HOST_SIZE]
+ [shortened_host_parts, uri_digest].join(".")
end
end
diff --git a/lib/bundler/source/rubygems_aggregate.rb b/lib/bundler/source/rubygems_aggregate.rb
index 99ef81ad54..8aeaa375fa 100644
--- a/lib/bundler/source/rubygems_aggregate.rb
+++ b/lib/bundler/source/rubygems_aggregate.rb
@@ -5,9 +5,10 @@ module Bundler
class RubygemsAggregate
attr_reader :source_map, :sources
- def initialize(sources, source_map)
+ def initialize(sources, source_map, excluded_sources = [])
@sources = sources
@source_map = source_map
+ @excluded_sources = excluded_sources
@index = build_index
end
@@ -31,6 +32,8 @@ module Bundler
dependency_names = source_map.pinned_spec_names
sources.all_sources.each do |source|
+ next if @excluded_sources.include?(source)
+
source.dependency_names = dependency_names - source_map.pinned_spec_names(source)
idx.add_source source.specs
dependency_names.concat(source.unmet_deps).uniq!