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.rb252
-rw-r--r--lib/bundler/source/git/git_proxy.rb411
-rw-r--r--lib/bundler/source/metadata.rb36
-rw-r--r--lib/bundler/source/path.rb68
-rw-r--r--lib/bundler/source/path/installer.rb25
-rw-r--r--lib/bundler/source/rubygems.rb410
-rw-r--r--lib/bundler/source/rubygems/remote.rb16
-rw-r--r--lib/bundler/source/rubygems_aggregate.rb5
9 files changed, 778 insertions, 450 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 a41a2f23e9..a002a2570a 100644
--- a/lib/bundler/source/git.rb
+++ b/lib/bundler/source/git.rb
@@ -11,6 +11,7 @@ module Bundler
def initialize(options)
@options = options
+ @checksum_store = Checksum::Store.new
@glob = options["glob"] || DEFAULT_GLOB
@allow_cached = false
@@ -19,7 +20,7 @@ module Bundler
# Stringify options that could be set as symbols
%w[ref branch tag revision].each {|k| options[k] = options[k].to_s if options[k] }
- @uri = options["uri"] || ""
+ @uri = URINormalizer.normalize_suffix(options["uri"] || "", trailing_slash: false)
@safe_uri = URICredentialsFilter.credential_filtered_uri(@uri)
@branch = options["branch"]
@ref = options["ref"] || options["branch"] || options["tag"]
@@ -31,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
@@ -46,41 +61,53 @@ module Bundler
out << " specs:\n"
end
+ def to_gemfile
+ specifiers = %w[ref branch tag submodules glob].map do |opt|
+ "#{opt}: #{options[opt]}" if options[opt]
+ end
+
+ uri_with_specifiers(specifiers)
+ 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
alias_method :==, :eql?
+ def include?(other)
+ other.is_a?(Git) && uri == other.uri &&
+ name == other.name &&
+ glob == other.glob &&
+ submodules == other.submodules
+ end
+
def to_s
begin
- at = if local?
- path
- elsif user_ref = options["ref"]
- if ref =~ /\A[a-z0-9]{4,}\z/i
- shortref_for_display(user_ref)
- else
- user_ref
- end
- elsif ref
- ref
- else
- git_proxy.branch
- end
+ at = humanized_ref || current_branch
rev = "at #{at}@#{shortref_for_display(revision)}"
rescue GitError
""
end
- specifiers = [rev, glob_for_display].compact
+ uri_with_specifiers([rev, glob_for_display])
+ end
+
+ def identifier
+ uri_with_specifiers([humanized_ref, locked_revision, glob_for_display])
+ end
+
+ def uri_with_specifiers(specifiers)
+ specifiers.compact!
+
suffix =
if specifiers.any?
" (#{specifiers.join(", ")})"
@@ -102,13 +129,7 @@ module Bundler
@install_path ||= begin
git_scope = "#{base_name}-#{shortref_for_path(revision)}"
- path = Bundler.install_path.join(git_scope)
-
- if !path.exist? && Bundler.requires_sudo?
- Bundler.user_bundle_path.join(Bundler.ruby_scope).join(git_scope)
- else
- path
- end
+ Bundler.install_path.join(git_scope)
end
end
@@ -132,7 +153,7 @@ module Bundler
path = Pathname.new(path)
path = path.expand_path(Bundler.root) unless path.relative?
- unless options["branch"] || Bundler.settings[:disable_local_branch_check]
+ unless branch || Bundler.settings[:disable_local_branch_check]
raise GitError, "Cannot use local override for #{name} at #{path} because " \
":branch is not specified in Gemfile. Specify a branch or run " \
"`bundle config unset local.#{override_for(original_path)}` to remove the local override"
@@ -143,21 +164,22 @@ 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.
- @git_proxy = GitProxy.new(path, uri, ref)
+ @git_proxy = GitProxy.new(path, uri, options)
- if git_proxy.branch != options["branch"] && !Bundler.settings[:disable_local_branch_check]
+ if current_branch != branch && !Bundler.settings[:disable_local_branch_check]
raise GitError, "Local override for #{name} at #{path} is using branch " \
- "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}"
+ "#{current_branch} but Gemfile specifies #{branch}"
end
- changed = cached_revision && cached_revision != git_proxy.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
@@ -166,45 +188,47 @@ 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
end
def install(spec, options = {})
+ return if Bundler.settings[:no_install]
force = options[:force]
- print_using_message "Using #{version_message(spec)} from #{self}"
+ 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
@@ -219,23 +243,25 @@ 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 ||= begin
- if Bundler.requires_sudo? || Bundler.feature_flag.global_gem_cache?
- Bundler.user_cache
- else
- Bundler.bundle_path.join("cache", "bundler")
- end.join("git", git_scope)
- end
+ @cache_path ||= if Bundler.settings[:global_gem_cache]
+ Bundler.user_cache
+ else
+ Bundler.bundle_path.join("cache", "bundler")
+ end.join("git", git_scope)
end
def app_cache_dirname
- "#{base_name}-#{shortref_for_path(cached_revision || revision)}"
+ "#{base_name}-#{shortref_for_path(locked_revision || revision)}"
end
def revision
git_proxy.revision
end
+ def current_branch
+ git_proxy.current_branch
+ end
+
def allow_git_ops?
@allow_remote || @allow_cached
end
@@ -246,6 +272,57 @@ 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
+ elsif user_ref = options["ref"]
+ if /\A[a-z0-9]{4,}\z/i.match?(ref)
+ shortref_for_display(user_ref)
+ else
+ user_ref
+ end
+ elsif ref
+ ref
+ end
+ end
+
def serialize_gemspecs_in(destination)
destination = destination.expand_path(Bundler.root) if destination.relative?
Dir["#{destination}/#{@glob}"].each do |spec_path|
@@ -254,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 cached_revision_checked_out?
- cached_revision && cached_revision == revision && install_path.exist?
+ def locked_revision_checked_out?
+ locked_revision && locked_revision == revision && installed?
+ end
+
+ def installed?
+ git_proxy.installed_to?(install_path)
end
def base_name
@@ -299,10 +393,10 @@ module Bundler
end
def uri_hash
- if uri =~ %r{^\w+://(\w+@)?}
+ if %r{^\w+://(\w+@)?}.match?(uri)
# Downcase the domain component of the URI
# and strip off a trailing slash, if one is present
- input = Bundler::URI.parse(uri).normalize.to_s.sub(%r{/$}, "")
+ input = Gem::URI.parse(uri).normalize.to_s.sub(%r{/$}, "")
else
# If there is no URI scheme, assume it is an ssh/git URI
input = uri
@@ -312,7 +406,7 @@ module Bundler
Bundler::Digest.sha1(input)
end
- def cached_revision
+ def locked_revision
options["revision"]
end
@@ -321,13 +415,12 @@ module Bundler
end
def git_proxy
- @git_proxy ||= GitProxy.new(cache_path, uri, ref, 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
@@ -335,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.tap{|x| x.untaint if RUBY_VERSION < "2.7" }
- 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
@@ -351,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 745a7fe118..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
@@ -28,10 +28,10 @@ module Bundler
def initialize(command, path, extra_info = nil)
@command = command
- msg = String.new
- msg << "Git error: command `#{command}` in directory #{path} has failed."
+ msg = String.new("Git error: command `#{command}`")
+ msg << " in directory #{path}" if path
+ msg << " has failed."
msg << "\n#{extra_info}" if extra_info
- msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path.exist?
super msg
end
end
@@ -43,69 +43,98 @@ module Bundler
end
end
+ class AmbiguousGitReference < GitError
+ def initialize(options)
+ msg = "Specification of branch or ref with tag is ambiguous. You specified #{options.inspect}"
+ super msg
+ end
+ end
+
# The GitProxy is responsible to interact with git repositories.
# All actions required by the Git source is encapsulated in this
# object.
class GitProxy
- attr_accessor :path, :uri, :ref
+ attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref
attr_writer :revision
- def initialize(path, uri, ref, revision = nil, git = nil)
+ 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
- @ref = ref
+ @tag = options["tag"]
+ @branch = options["branch"]
+ @ref = options["ref"]
+ if @tag
+ raise AmbiguousGitReference.new(options) if @branch || @ref
+ @explicit_ref = @tag
+ else
+ @explicit_ref = @ref || @branch
+ end
@revision = revision
@git = git
+ @commit_ref = nil
end
def revision
- @revision ||= find_local_revision
+ @revision ||= allowed_with_path { find_local_revision }
end
- def branch
- @branch ||= allowed_with_path do
- git("rev-parse", "--abbrev-ref", "HEAD", :dir => path).strip
+ def current_branch
+ @current_branch ||= with_path do
+ git_local("rev-parse", "--abbrev-ref", "HEAD", dir: path).strip
end
end
def contains?(commit)
allowed_with_path do
- result, status = git_null("branch", "--contains", commit, :dir => path)
- status.success? && result =~ /^\* (.*)$/
+ result, status = git_null("branch", "--contains", commit, dir: path)
+ status.success? && result.match?(/^\* (.*)$/)
end
end
def version
- git("--version").match(/(git version\s*)?((\.?\d+)+).*/)[2]
+ self.class.version
end
def full_version
- git("--version").sub("git version", "").strip
+ self.class.full_version
end
def checkout
- return if path.exist? && has_revision_cached?
- extra_ref = "#{ref}:#{ref}" if ref && ref.start_with?("refs/")
-
- Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}"
+ return if has_revision_cached?
- configured_uri = configured_uri_for(uri).to_s
+ Bundler.ui.info "Fetching #{credential_filtered_uri}"
- unless path.exist?
- SharedHelpers.filesystem_access(path.dirname) do |p|
- FileUtils.mkdir_p(p)
- end
- git_retry "clone", "--bare", "--no-hardlinks", "--quiet", "--", configured_uri, path.to_s
- return unless extra_ref
- end
+ extra_fetch_needed = clone_needs_extra_fetch?
+ unshallow_needed = clone_needs_unshallow?
+ return unless extra_fetch_needed || unshallow_needed
- with_path do
- git_retry(*["fetch", "--force", "--quiet", "--tags", "--", configured_uri, "refs/heads/*:refs/heads/*", extra_ref].compact, :dir => path)
- end
+ git_remote_fetch(unshallow_needed ? ["--unshallow"] : depth_args)
end
def copy_to(destination, submodules = false)
- # method 1
unless File.exist?(destination.join(".git"))
begin
SharedHelpers.filesystem_access(destination.dirname) do |p|
@@ -114,8 +143,8 @@ module Bundler
SharedHelpers.filesystem_access(destination) do |p|
FileUtils.rm_rf(p)
end
- git_retry "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
- File.chmod(((File.stat(destination).mode | 0o777) & ~File.umask), destination)
+ git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
+ 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 " \
@@ -123,89 +152,229 @@ module Bundler
"this file and try again."
end
end
- # method 2
- git_retry "fetch", "--force", "--quiet", "--tags", path.to_s, :dir => destination
- begin
- git "reset", "--hard", @revision, :dir => destination
- rescue GitCommandError => e
- raise MissingGitRevisionError.new(e.command, destination, @revision, URICredentialsFilter.credential_filtered_uri(uri))
+ ref = @commit_ref || (locked_to_full_sha? && @revision)
+ if ref
+ git "config", "uploadpack.allowAnySHA1InWant", "true", dir: path.to_s if @commit_ref.nil? && needs_allow_any_sha1_in_want?
+
+ git "fetch", "--force", "--quiet", *extra_fetch_args(ref), dir: destination
end
+ git "reset", "--hard", revision, dir: destination
+
if submodules
- git_retry "submodule", "update", "--init", "--recursive", :dir => destination
+ git_retry "submodule", "update", "--init", "--recursive", dir: destination
elsif Gem::Version.create(version) >= Gem::Version.create("2.9.0")
inner_command = "git -C $toplevel submodule deinit --force $sm_path"
- git_retry "submodule", "foreach", "--quiet", inner_command, :dir => destination
+ git_retry "submodule", "foreach", "--quiet", inner_command, dir: destination
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_null(*command, dir: nil)
- check_allowed(command)
+ def git_remote_fetch(args)
+ command = fetch_command(args)
+ command_with_no_credentials = check_allowed(command)
+
+ Bundler::Retry.new("`#{command_with_no_credentials}` at #{path}", [MissingGitRevisionError]).attempts do
+ out, err, status = capture(command, path)
+ return out if status.success?
+
+ 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
+ end
+
+ def clone_needs_extra_fetch?
+ return true if path.exist?
+
+ SharedHelpers.filesystem_access(path.dirname) do |p|
+ FileUtils.mkdir_p(p)
+ end
+
+ 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") || # 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
+ end
+
+ def clone_needs_unshallow?
+ return false unless path.join("shallow").exist?
+ return true unless shallow?
+
+ @revision && @revision != head_revision
+ end
+
+ def extra_ref
+ return false if not_pinned?
+ return true if shallow?
+
+ ref.start_with?("refs/")
+ end
+
+ def depth
+ return @depth if defined?(@depth)
+
+ @depth = if !supports_fetching_unreachable_refs?
+ nil
+ elsif not_pinned? || pinned_to_full_sha?
+ 1
+ elsif ref.include?("~")
+ parsed_depth = ref.split("~").last
+ parsed_depth.to_i + 1
+ end
+ end
+
+ def refspec
+ if commit
+ @commit_ref = "refs/#{commit}-sha"
+ return "#{commit}:#{@commit_ref}"
+ end
+
+ reference = fully_qualified_ref
- out, status = SharedHelpers.with_clean_git_env do
- capture_and_ignore_stderr(*capture3_args_for(command, dir))
+ reference ||= if ref.include?("~")
+ ref.split("~").first
+ elsif ref.start_with?("refs/")
+ ref
+ else
+ "refs/*"
+ end
+
+ "#{reference}:#{reference}"
+ end
+
+ def commit
+ @commit ||= pinned_to_full_sha? ? ref : @revision
+ end
+
+ def fully_qualified_ref
+ if branch
+ "refs/heads/#{branch}"
+ elsif tag
+ "refs/tags/#{tag}"
+ elsif ref.nil?
+ "refs/heads/#{current_branch}"
end
+ end
- [URICredentialsFilter.credential_filtered_string(out, uri), status]
+ def not_pinned?
+ branch_option || ref.nil?
+ end
+
+ def pinned_to_full_sha?
+ full_sha_revision?(ref)
+ end
+
+ def locked_to_full_sha?
+ full_sha_revision?(@revision)
+ end
+
+ def full_sha_revision?(ref)
+ ref&.match?(/\A\h{40}\z/)
+ end
+
+ def git_null(*command, dir: nil)
+ check_allowed(command)
+
+ capture(command, dir, ignore_err: true)
end
def git_retry(*command, dir: nil)
command_with_no_credentials = check_allowed(command)
Bundler::Retry.new("`#{command_with_no_credentials}` at #{dir || SharedHelpers.pwd}").attempts do
- git(*command, :dir => dir)
+ git(*command, dir: dir)
end
end
def git(*command, dir: nil)
- command_with_no_credentials = check_allowed(command)
-
- out, status = SharedHelpers.with_clean_git_env do
- capture_and_filter_stderr(*capture3_args_for(command, dir))
+ run_command(*command, dir: dir) do |unredacted_command|
+ check_allowed(unredacted_command)
end
+ end
- filtered_out = URICredentialsFilter.credential_filtered_string(out, uri)
-
- raise GitCommandError.new(command_with_no_credentials, dir || SharedHelpers.pwd, filtered_out) unless status.success?
-
- filtered_out
+ def git_local(*command, dir: nil)
+ run_command(*command, dir: dir) do |unredacted_command|
+ redact_and_check_presence(unredacted_command)
+ end
end
def has_revision_cached?
- return unless @revision
- with_path { git("cat-file", "-e", @revision, :dir => path) }
+ return unless commit && path.exist?
+ git("cat-file", "-e", commit, dir: path)
true
rescue GitError
false
end
- def remove_cache
- FileUtils.rm_rf(path)
+ def find_local_revision
+ return head_revision if explicit_ref.nil?
+
+ find_revision_for(explicit_ref)
end
- def find_local_revision
- allowed_with_path do
- git("rev-parse", "--verify", ref || "HEAD", :dir => path).strip
- end
+ def head_revision
+ verify("HEAD")
+ end
+
+ def find_revision_for(reference)
+ verify(reference)
rescue GitCommandError => e
- raise MissingGitRevisionError.new(e.command, path, ref, URICredentialsFilter.credential_filtered_uri(uri))
+ raise MissingGitRevisionError.new(e.command, path, reference, credential_filtered_uri)
end
- # Adds credentials to the URI as Fetcher#configured_uri_for does
- def configured_uri_for(uri)
- if /https?:/ =~ uri
- remote = Bundler::URI(uri)
+ def verify(reference)
+ git("rev-parse", "--verify", reference, dir: path).strip
+ end
+
+ # Adds credentials to the URI
+ def configured_uri
+ if /https?:/.match?(uri)
+ remote = Gem::URI(uri)
config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host]
remote.userinfo ||= config_auth
remote.to_s
else
- uri
+ uri.to_s
end
end
+ # Removes credentials from the URI
+ def credential_filtered_uri
+ URICredentialsFilter.credential_filtered_uri(uri)
+ end
+
def allow?
allowed = @git ? @git.allow_git_ops? : true
@@ -225,37 +394,103 @@ module Bundler
end
def check_allowed(command)
- require "shellwords"
- command_with_no_credentials = URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", uri)
+ command_with_no_credentials = redact_and_check_presence(command)
raise GitNotAllowedError.new(command_with_no_credentials) unless allow?
command_with_no_credentials
end
- def capture_and_filter_stderr(*cmd)
- require "open3"
- return_value, captured_err, status = Open3.capture3(*cmd)
- Bundler.ui.warn URICredentialsFilter.credential_filtered_string(captured_err, uri) unless captured_err.empty?
- [return_value, status]
+ def redact_and_check_presence(command)
+ raise GitNotInstalledError.new unless Bundler.git_present?
+
+ require "shellwords"
+ URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", uri)
+ end
+
+ def run_command(*command, dir: nil)
+ command_with_no_credentials = yield(command)
+
+ out, err, status = capture(command, dir)
+
+ raise GitCommandError.new(command_with_no_credentials, dir || SharedHelpers.pwd, err) unless status.success?
+
+ Bundler.ui.warn err unless err.empty?
+
+ out
end
- def capture_and_ignore_stderr(*cmd)
- require "open3"
- return_value, _, status = Open3.capture3(*cmd)
- [return_value, status]
+ def capture(cmd, dir, ignore_err: false)
+ SharedHelpers.with_clean_git_env do
+ require "open3"
+ out, err, status = Open3.capture3(*capture3_args_for(cmd, dir))
+
+ filtered_out = URICredentialsFilter.credential_filtered_string(out, uri)
+ return [filtered_out, status] if ignore_err
+
+ filtered_err = URICredentialsFilter.credential_filtered_string(err, uri)
+ [filtered_out, filtered_err, status]
+ end
end
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
+ args = depth_args
+ return [] if args.empty?
+
+ args += ["--single-branch"]
+ args.unshift("--no-tags") if supports_cloning_with_no_tags?
+
+ # If there's a locked revision, no need to clone any specific branch
+ # or tag, since we will end up checking out that locked revision
+ # anyways.
+ return args if @revision
+
+ 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 [] unless shallow?
+
+ ["--depth", depth.to_s]
+ end
+
+ def extra_fetch_args(ref)
+ extra_args = [path.to_s, *depth_args]
+ extra_args.push(ref)
+ extra_args
+ end
+
+ def branch_option
+ branch || tag
+ end
+
+ def shallow?
+ !depth.nil?
+ end
+
+ def needs_allow_any_sha1_in_want?
+ @needs_allow_any_sha1_in_want ||= Gem::Version.new(version) <= Gem::Version.new("2.13.7")
+ end
+
+ def supports_fetching_unreachable_refs?
+ @supports_fetching_unreachable_refs ||= Gem::Version.new(version) >= Gem::Version.new("2.5.0")
end
- def supports_minus_c?
- @supports_minus_c ||= Gem::Version.new(version) >= Gem::Version.new("1.8.5")
+ def supports_cloning_with_no_tags?
+ @supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0")
end
end
end
diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb
index 50b65ce0ea..ecf8895187 100644
--- a/lib/bundler/source/metadata.rb
+++ b/lib/bundler/source/metadata.rb
@@ -5,28 +5,28 @@ module Bundler
class Metadata < Source
def specs
@specs ||= Index.build do |idx|
- idx << Gem::Specification.new("Ruby\0", RubyVersion.system.to_gem_version_with_patchlevel)
+ idx << Gem::Specification.new("Ruby\0", Bundler::RubyVersion.system.gem_version)
idx << Gem::Specification.new("RubyGems\0", Gem::VERSION) do |s|
s.required_rubygems_version = Gem::Requirement.default
end
- idx << Gem::Specification.new do |s|
- s.name = "bundler"
- s.version = VERSION
- s.license = "MIT"
- s.platform = Gem::Platform::RUBY
- s.source = self
- s.authors = ["bundler team"]
- 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 = File.expand_path("..", __FILE__)
- end
+ if local_spec = Gem.loaded_specs["bundler"]
+ raise CorruptBundlerInstallError.new(local_spec) if local_spec.version.to_s != Bundler::VERSION
- if local_spec = Bundler.rubygems.find_name("bundler").find {|s| s.version.to_s == VERSION }
idx << local_spec
+ else
+ idx << Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = VERSION
+ s.license = "MIT"
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["bundler team"]
+ s.bindir = "exe"
+ s.homepage = "https://bundler.io"
+ s.summary = "The best way to manage your application's dependencies"
+ s.executables = %w[bundle bundler]
+ s.loaded_from = SharedHelpers.gemspec_path
+ end
end
idx.each {|s| s.source = self }
@@ -58,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 01f89b204d..366a23aea7 100644
--- a/lib/bundler/source/path.rb
+++ b/lib/bundler/source/path.rb
@@ -11,22 +11,20 @@ module Bundler
protected :original_path
- DEFAULT_GLOB = "{,*,*/*}.gemspec".freeze
+ DEFAULT_GLOB = "{,*,*/*}.gemspec"
def initialize(options)
+ @checksum_store = Checksum::Store.new
@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
@@ -40,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
@@ -65,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
@@ -82,16 +74,16 @@ module Bundler
end
def install(spec, options = {})
- using_message = "Using #{version_message(spec)} from #{self}"
+ using_message = "Using #{version_message(spec, options[:previous_spec])} from #{self}"
using_message += " and installing its executables" unless spec.executables.empty?
print_using_message using_message
- generate_bin(spec, :disable_extensions => true)
+ generate_bin(spec, disable_extensions: true)
nil # no post-install message
end
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?
@@ -134,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 " \
@@ -160,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
@@ -177,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)
@@ -224,22 +219,23 @@ 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 |p|
- next p unless p =~ /\A#{Pathname::SEPARATOR_PAT}/
- next if File.directory?(p)
+ 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(p).relative_path_from(gem_dir).to_s
+ pathname.relative_path_from(gem_dir).to_s
rescue ArgumentError
- p
+ path
end
- end.compact
+ end
installer = Path::Installer.new(
spec,
- :env_shebang => false,
- :disable_extensions => options[:disable_extensions],
- :build_args => options[:build_args],
- :bundler_extension_cache_path => extension_cache_path(spec)
+ env_shebang: false,
+ disable_extensions: options[:disable_extensions],
+ build_args: options[:build_args],
+ bundler_extension_cache_path: extension_cache_path(spec)
)
installer.post_install
rescue Gem::InvalidSpecificationException => e
diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb
index a70973bde7..39765e5da2 100644
--- a/lib/bundler/source/path/installer.rb
+++ b/lib/bundler/source/path/installer.rb
@@ -18,19 +18,13 @@ module Bundler
@build_args = options[:build_args] || Bundler.rubygems.build_args
@gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin"
@disable_extensions = options[:disable_extensions]
-
- if Bundler.requires_sudo?
- @tmp_dir = Bundler.tmp(spec.full_name).to_s
- @bin_dir = "#{@tmp_dir}/bin"
- else
- @bin_dir = @gem_bin_dir
- end
+ @bin_dir = @gem_bin_dir
end
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
@@ -38,25 +32,10 @@ module Bundler
generate_bin unless spec.executables.empty?
run_hooks(:post_install)
- ensure
- Bundler.rm_rf(@tmp_dir) if Bundler.requires_sudo?
end
private
- def generate_bin
- super
-
- if Bundler.requires_sudo?
- SharedHelpers.filesystem_access(@gem_bin_dir) do |p|
- Bundler.mkdir_p(p)
- end
- spec.executables.each do |exe|
- Bundler.sudo "cp -R #{@bin_dir}/#{exe} #{@gem_bin_dir}"
- end
- end
- end
-
def run_hooks(type)
hooks_meth = "#{type}_hooks"
return unless Gem.respond_to?(hooks_meth)
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
index 8bc3aa17e9..d610ce3fdf 100644
--- a/lib/bundler/source/rubygems.rb
+++ b/lib/bundler/source/rubygems.rb
@@ -7,12 +7,11 @@ module Bundler
class Rubygems < Source
autoload :Remote, File.expand_path("rubygems/remote", __dir__)
- # Use the API when installing less than X gems
- API_REQUEST_LIMIT = 500
# Ask for X gems per API request
- API_REQUEST_SIZE = 50
+ API_REQUEST_SIZE = 100
+ REQUIRE_MUTEX = Mutex.new
- attr_reader :remotes, :caches
+ attr_accessor :remotes
def initialize(options = {})
@options = options
@@ -21,9 +20,22 @@ module Bundler
@allow_remote = false
@allow_cached = false
@allow_local = options["allow_local"] || false
- @caches = [cache_path, *Bundler.rubygems.gem_cache]
+ @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!
@@ -33,6 +45,10 @@ module Bundler
@allow_remote = false
end
+ def local_only?
+ @allow_local && !@allow_remote
+ end
+
def local!
return if @allow_local
@@ -48,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
@@ -87,13 +104,14 @@ module Bundler
end
def self.from_lock(options)
- new(options)
+ options["remotes"] = Array(options.delete("remote")).reverse
+ 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
@@ -122,123 +140,86 @@ module Bundler
end
end
alias_method :name, :identifier
+ alias_method :to_gemfile, :identifier
def specs
@specs ||= begin
# remote_specs usually generates a way larger Index than the other
- # sources, and large_idx.use small_idx is way faster than
- # small_idx.use large_idx.
- idx = @allow_remote ? remote_specs.dup : Index.new
- idx.use(cached_specs, :override_dupes) if @allow_cached || @allow_remote
- idx.use(installed_specs, :override_dupes) if @allow_local
- idx
+ # sources, and large_idx.merge! small_idx is way faster than
+ # small_idx.merge! large_idx.
+ index = @allow_remote ? remote_specs.dup : Index.new
+ index.merge!(cached_specs) if @allow_cached
+ index.merge!(installed_specs) if @allow_local
+
+ 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
+
+ index
end
end
- def install(spec, opts = {})
- force = opts[:force]
- ensure_builtin_gems_cached = opts[:ensure_builtin_gems_cached]
-
- if ensure_builtin_gems_cached && spec.default_gem?
- if !cached_path(spec)
- cached_built_in_gem(spec) unless spec.remote
- force = true
- else
- spec.loaded_from = loaded_from(spec)
- end
+ def download(spec, options = {})
+ if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
+ return true
end
- if installed?(spec) && !force
- print_using_message "Using #{version_message(spec)}"
- return nil # no post-install message
- end
+ installer = rubygems_gem_installer(spec, options)
- # Download the gem to get the spec, because some specs that are returned
- # by rubygems.org are broken and wrong.
if spec.remote
- # Check for this spec from other sources
- uris = [spec.remote.anonymized_uri]
- uris += remotes_for_spec(spec).map(&:anonymized_uri)
- uris.uniq!
- Installer.ambiguous_gems << [spec.name, *uris] if uris.length > 1
-
- path = fetch_gem(spec)
- begin
- s = Bundler.rubygems.spec_from_gem(path, Bundler.settings["trust-policy"])
- spec.__swap__(s)
+ 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 #{installer.gem} can't be installed because " \
+ "the security policy didn't allow it, with the message: #{e.message}"
end
+
+ spec.__swap__(s)
end
- unless Bundler.settings[:no_install]
- message = "Installing #{version_message(spec)}"
- message += " with native extensions" if spec.extensions.any?
- Bundler.ui.confirm message
+ spec
+ end
- path = cached_gem(spec)
- raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path
- if requires_sudo?
- install_path = Bundler.tmp(spec.full_name)
- bin_path = install_path.join("bin")
- else
- install_path = rubygems_dir
- bin_path = Bundler.system_bindir
- 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
- Bundler.mkdir_p bin_path, :no_sudo => true unless spec.executables.empty? || Bundler.rubygems.provides?(">= 2.7.5")
+ return if Bundler.settings[:no_install]
- require_relative "../rubygems_gem_installer"
+ installer = rubygems_gem_installer(spec, options)
+ spec.source.checksum_store.register(spec, installer.gem_checksum)
- installed_spec = Bundler::RubyGemsGemInstaller.at(
- path,
- :install_dir => install_path.to_s,
- :bin_dir => bin_path.to_s,
- :ignore_dependencies => true,
- :wrappers => true,
- :env_shebang => true,
- :build_args => opts[:build_args],
- :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum,
- :bundler_extension_cache_path => extension_cache_path(spec)
- ).install
- spec.full_gem_path = installed_spec.full_gem_path
-
- # SUDO HAX
- if requires_sudo?
- Bundler.rubygems.repository_subdirectories.each do |name|
- src = File.join(install_path, name, "*")
- dst = File.join(rubygems_dir, name)
- if name == "extensions" && Dir.glob(src).any?
- src = File.join(src, "*/*")
- ext_src = Dir.glob(src).first
- ext_src.gsub!(src[0..-6], "")
- dst = File.dirname(File.join(dst, ext_src))
- end
- SharedHelpers.filesystem_access(dst) do |p|
- Bundler.mkdir_p(p)
- end
- Bundler.sudo "cp -R #{src} #{dst}" if Dir[src].any?
- end
+ message = "Installing #{version_message(spec, options[:previous_spec])}"
+ message += " with native extensions" if spec.extensions.any?
+ Bundler.ui.confirm message
- spec.executables.each do |exe|
- SharedHelpers.filesystem_access(Bundler.system_bindir) do |p|
- Bundler.mkdir_p(p)
- end
- Bundler.sudo "cp -R #{install_path}/bin/#{exe} #{Bundler.system_bindir}/"
- end
- end
- installed_spec.loaded_from = loaded_from(spec)
+ installed_spec = nil
+
+ Gem.time("Installed #{spec.name} in", 0, true) do
+ installed_spec = installer.install
end
- spec.loaded_from = loaded_from(spec)
+
+ 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
- ensure
- Bundler.rm_rf(install_path) if requires_sudo?
end
def cache(spec, custom_path = nil)
- cached_path = cached_gem(spec)
+ cached_path = Bundler.settings[:cache_all_platforms] ? fetch_gem_if_possible(spec) : cached_gem(spec)
raise GemNotFound, "Missing gem file '#{spec.file_name}'." unless cached_path
return if File.dirname(cached_path) == Bundler.app_cache.to_s
Bundler.ui.info " * #{File.basename(cached_path)}"
@@ -248,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
@@ -267,7 +249,7 @@ module Bundler
end
def spec_names
- if @allow_remote && dependency_api_available?
+ if dependency_api_available?
remote_specs.spec_names
else
[]
@@ -275,22 +257,25 @@ module Bundler
end
def unmet_deps
- if @allow_remote && dependency_api_available?
+ if dependency_api_available?
remote_specs.unmet_dependency_names
else
[]
end
end
- def fetchers
- @fetchers ||= remotes.map do |uri|
+ def remote_fetchers
+ @remote_fetchers ||= remotes.to_h do |uri|
remote = Source::Rubygems::Remote.new(uri)
- Bundler::Fetcher.new(remote)
- end
+ [remote, Bundler::Fetcher.new(remote)]
+ end.freeze
+ end
+
+ def fetchers
+ @fetchers ||= remote_fetchers.values.freeze
end
def double_check_for(unmet_dependency_names)
- return unless @allow_remote
return unless dependency_api_available?
unmet_dependency_names = unmet_dependency_names.call
@@ -305,7 +290,9 @@ module Bundler
Bundler.ui.debug "Double checking for #{unmet_dependency_names || "all specs (due to the size of the request)"} in #{self}"
- fetch_names(api_fetchers, unmet_dependency_names, specs, false)
+ fetch_names(api_fetchers, unmet_dependency_names, remote_specs)
+
+ specs.use remote_specs
end
def dependency_names_to_double_check
@@ -324,7 +311,14 @@ module Bundler
end
def dependency_api_available?
- api_fetchers.any?
+ @allow_remote && api_fetchers.any?
+ end
+
+ def clear_cache
+ @specs = nil
+ @installed_specs = nil
+ @default_specs = nil
+ @cached_specs = nil
end
protected
@@ -334,59 +328,30 @@ 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
- end
-
- def loaded_from(spec)
- "#{rubygems_dir}/specifications/#{spec.full_name}.gemspec"
+ 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
+ caches << global_cache_path if global_cache_path
- possibilities = @caches.map {|p| "#{p}/#{spec.file_name}" }
+ possibilities = caches.map {|p| package_path(p, spec) }
possibilities.find {|p| File.exist?(p) }
end
+ def package_path(cache_path, spec)
+ "#{cache_path}/#{spec.file_name}"
+ end
+
def normalize_uri(uri)
- uri = uri.to_s
- uri = "#{uri}/" unless uri =~ %r{/$}
+ uri = URINormalizer.normalize_suffix(uri.to_s)
require_relative "../vendored_uri"
- uri = Bundler::URI(uri)
+ uri = Gem::URI(uri)
raise ArgumentError, "The source must be an absolute URI. For example:\n" \
- "source 'https://rubygems.org'" if !uri.absolute? || (uri.is_a?(Bundler::URI::HTTP) && uri.host.nil?)
+ "source 'https://rubygems.org'" if !uri.absolute? || (uri.is_a?(Gem::URI::HTTP) && uri.host.nil?)
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
@@ -397,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
@@ -410,10 +381,9 @@ module Bundler
def cached_specs
@cached_specs ||= begin
- idx = @allow_local ? installed_specs.dup : Index.new
+ idx = Index.new
Dir["#{cache_path}/*.gem"].each do |gemfile|
- next if gemfile =~ /^bundler\-[\d\.]+?\.gem/
s ||= Bundler.rubygems.spec_from_gem(gemfile)
s.source = self
idx << s
@@ -424,82 +394,63 @@ module Bundler
end
def api_fetchers
- fetchers.select {|f| f.use_api && f.fetchers.first.api_fetcher? }
+ fetchers.select(&:api_fetcher?)
end
def remote_specs
@remote_specs ||= Index.build do |idx|
index_fetchers = fetchers - api_fetchers
- # gather lists from non-api sites
- fetch_names(index_fetchers, nil, idx, false)
-
- # because ensuring we have all the gems we need involves downloading
- # the gemspecs of those gems, if the non-api sites contain more than
- # about 500 gems, we treat all sites as non-api for speed.
- allow_api = idx.size < API_REQUEST_LIMIT && dependency_names.size < API_REQUEST_LIMIT
- Bundler.ui.debug "Need to query more than #{API_REQUEST_LIMIT} gems." \
- " Downloading full index instead..." unless allow_api
-
- fetch_names(api_fetchers, allow_api && dependency_names, idx, false)
+ if index_fetchers.empty?
+ fetch_names(api_fetchers, dependency_names, idx)
+ else
+ fetch_names(fetchers, nil, idx)
+ end
end
end
- def fetch_names(fetchers, dependency_names, index, override_dupes)
+ def fetch_names(fetchers, dependency_names, index)
fetchers.each do |f|
if dependency_names
Bundler.ui.info "Fetching gem metadata from #{URICredentialsFilter.credential_filtered_uri(f.uri)}", Bundler.ui.debug?
- index.use f.specs_with_retry(dependency_names, self), override_dupes
+ index.use f.specs_with_retry(dependency_names, self)
Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over
else
Bundler.ui.info "Fetching source index from #{URICredentialsFilter.credential_filtered_uri(f.uri)}"
- index.use f.specs_with_retry(nil, self), override_dupes
+ index.use f.specs_with_retry(nil, self)
end
end
end
- def fetch_gem(spec)
- return false unless spec.remote
+ def fetch_gem_if_possible(spec, previous_spec = nil)
+ if spec.remote
+ fetch_gem(spec, previous_spec)
+ else
+ cached_gem(spec)
+ end
+ end
+ def fetch_gem(spec, previous_spec = nil)
spec.fetch_platform
cache_path = download_cache_path(spec) || default_cache_path_for(rubygems_dir)
- gem_path = "#{cache_path}/#{spec.file_name}"
-
- if requires_sudo?
- download_path = Bundler.tmp(spec.full_name)
- download_cache_path = default_cache_path_for(download_path)
- else
- download_cache_path = cache_path
- end
+ gem_path = package_path(cache_path, spec)
+ return gem_path if File.exist?(gem_path)
- SharedHelpers.filesystem_access(download_cache_path) do |p|
+ SharedHelpers.filesystem_access(cache_path) do |p|
FileUtils.mkdir_p(p)
end
- download_gem(spec, download_cache_path)
-
- if requires_sudo?
- SharedHelpers.filesystem_access(cache_path) do |p|
- Bundler.mkdir_p(p)
- end
- Bundler.sudo "mv #{download_cache_path}/#{spec.file_name} #{gem_path}"
- end
+ download_gem(spec, cache_path, previous_spec)
gem_path
- ensure
- Bundler.rm_rf(download_path) if requires_sudo?
end
def installed?(spec)
- installed_specs[spec].any? && !spec.deleted_gem?
- end
-
- def requires_sudo?
- Bundler.requires_sudo?
+ installed_specs[spec].any? && !spec.installation_missing?
end
def rubygems_dir
- Bundler.rubygems.gem_dir
+ Bundler.bundle_path
end
def default_cache_path_for(dir)
@@ -512,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.
#
@@ -521,10 +476,22 @@ module Bundler
# @param [String] download_cache_path
# the local directory the .gem will end up in.
#
- def download_gem(spec, download_cache_path)
+ # @param [Specification] previous_spec
+ # the spec previously locked
+ #
+ def download_gem(spec, download_cache_path, previous_spec = nil)
uri = spec.remote.uri
- Bundler.ui.confirm("Fetching #{version_message(spec)}")
- Bundler.rubygems.download_gem(spec, uri, download_cache_path)
+ Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}")
+ gem_remote_fetcher = remote_fetchers.fetch(spec.remote).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.
@@ -539,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 82c850ffbb..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
@@ -48,7 +56,7 @@ module Bundler
end
uri
- rescue Bundler::URI::InvalidComponentError
+ rescue Gem::URI::InvalidComponentError
error_message = "Please CGI escape your usernames and passwords before " \
"setting them for authentication."
raise HTTPError.new(error_message)
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!