This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Gemspec < Path
|
||||
attr_reader :gemspec
|
||||
|
||||
def initialize(options)
|
||||
super
|
||||
@gemspec = options["gemspec"]
|
||||
end
|
||||
|
||||
def as_path_source
|
||||
Path.new(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,380 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../vendored_fileutils"
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Git < Path
|
||||
autoload :GitProxy, File.expand_path("git/git_proxy", __dir__)
|
||||
|
||||
attr_reader :uri, :ref, :branch, :options, :glob, :submodules
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
@checksum_store = Checksum::Store.new
|
||||
@glob = options["glob"] || DEFAULT_GLOB
|
||||
|
||||
@allow_cached = false
|
||||
@allow_remote = false
|
||||
|
||||
# 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 = 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"]
|
||||
@submodules = options["submodules"]
|
||||
@name = options["name"]
|
||||
@version = options["version"].to_s.strip.gsub("-", ".pre.")
|
||||
|
||||
@copied = false
|
||||
@local = false
|
||||
end
|
||||
|
||||
def self.from_lock(options)
|
||||
new(options.merge("uri" => options.delete("remote")))
|
||||
end
|
||||
|
||||
def to_lock
|
||||
out = String.new("GIT\n")
|
||||
out << " remote: #{@uri}\n"
|
||||
out << " revision: #{revision}\n"
|
||||
%w[ref branch tag submodules].each do |opt|
|
||||
out << " #{opt}: #{options[opt]}\n" if options[opt]
|
||||
end
|
||||
out << " glob: #{@glob}\n" unless default_glob?
|
||||
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
|
||||
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 &&
|
||||
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 = humanized_ref || current_branch
|
||||
|
||||
rev = "at #{at}@#{shortref_for_display(revision)}"
|
||||
rescue GitError
|
||||
""
|
||||
end
|
||||
|
||||
uri_with_specifiers([rev, glob_for_display])
|
||||
end
|
||||
|
||||
def identifier
|
||||
uri_with_specifiers([humanized_ref, cached_revision, glob_for_display])
|
||||
end
|
||||
|
||||
def uri_with_specifiers(specifiers)
|
||||
specifiers.compact!
|
||||
|
||||
suffix =
|
||||
if specifiers.any?
|
||||
" (#{specifiers.join(", ")})"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
"#{@safe_uri}#{suffix}"
|
||||
end
|
||||
|
||||
def name
|
||||
File.basename(@uri, ".git")
|
||||
end
|
||||
|
||||
# This is the path which is going to contain a specific
|
||||
# checkout of the git repository. When using local git
|
||||
# repos, this is set to the local repo.
|
||||
def install_path
|
||||
@install_path ||= begin
|
||||
git_scope = "#{base_name}-#{shortref_for_path(revision)}"
|
||||
|
||||
Bundler.install_path.join(git_scope)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :path, :install_path
|
||||
|
||||
def extension_dir_name
|
||||
"#{base_name}-#{shortref_for_path(revision)}"
|
||||
end
|
||||
|
||||
def unlock!
|
||||
git_proxy.revision = nil
|
||||
options["revision"] = nil
|
||||
|
||||
@unlocked = true
|
||||
end
|
||||
|
||||
def local_override!(path)
|
||||
return false if local?
|
||||
|
||||
original_path = path
|
||||
path = Pathname.new(path)
|
||||
path = path.expand_path(Bundler.root) unless path.relative?
|
||||
|
||||
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"
|
||||
end
|
||||
|
||||
unless path.exist?
|
||||
raise GitError, "Cannot use local override for #{name} because #{path} " \
|
||||
"does not exist. Run `bundle config unset local.#{override_for(original_path)}` to remove the local override"
|
||||
end
|
||||
|
||||
set_local!(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, options)
|
||||
|
||||
if current_branch != branch && !Bundler.settings[:disable_local_branch_check]
|
||||
raise GitError, "Local override for #{name} at #{path} is using branch " \
|
||||
"#{current_branch} but Gemfile specifies #{branch}"
|
||||
end
|
||||
|
||||
changed = cached_revision && cached_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)} " \
|
||||
"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
|
||||
|
||||
changed
|
||||
end
|
||||
|
||||
def specs(*)
|
||||
set_local!(app_cache_path) if has_app_cache? && !local?
|
||||
|
||||
if requires_checkout? && !@copied
|
||||
fetch
|
||||
git_proxy.copy_to(install_path, submodules)
|
||||
serialize_gemspecs_in(install_path)
|
||||
@copied = true
|
||||
end
|
||||
|
||||
local_specs
|
||||
end
|
||||
|
||||
def install(spec, options = {})
|
||||
return if Bundler.settings[:no_install]
|
||||
force = options[:force]
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
generate_bin_options = { disable_extensions: !Bundler.rubygems.spec_missing_extensions?(spec), build_args: options[:build_args] }
|
||||
generate_bin(spec, generate_bin_options)
|
||||
|
||||
requires_checkout? ? spec.post_install_message : nil
|
||||
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)
|
||||
end
|
||||
|
||||
def load_spec_files
|
||||
super
|
||||
rescue PathError => e
|
||||
Bundler.ui.trace e
|
||||
raise GitError, "#{self} is not yet checked out. Run `bundle install` first."
|
||||
end
|
||||
|
||||
# This is the path which is going to contain a cache
|
||||
# of the git repository. When using the same git repository
|
||||
# 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?
|
||||
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)}"
|
||||
end
|
||||
|
||||
def revision
|
||||
git_proxy.revision
|
||||
end
|
||||
|
||||
def current_branch
|
||||
git_proxy.current_branch
|
||||
end
|
||||
|
||||
def allow_git_ops?
|
||||
@allow_remote || @allow_cached
|
||||
end
|
||||
|
||||
def local?
|
||||
@local
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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|
|
||||
# Evaluate gemspecs and cache the result. Gemspecs
|
||||
# in git might require git or other dependencies.
|
||||
# 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
|
||||
end
|
||||
|
||||
def set_local!(path)
|
||||
@local = true
|
||||
@local_specs = @git_proxy = nil
|
||||
@cache_path = @install_path = path
|
||||
end
|
||||
|
||||
def has_app_cache?
|
||||
cached_revision && super
|
||||
end
|
||||
|
||||
def requires_checkout?
|
||||
allow_git_ops? && !local? && !cached_revision_checked_out?
|
||||
end
|
||||
|
||||
def cached_revision_checked_out?
|
||||
cached_revision && cached_revision == revision && install_path.exist?
|
||||
end
|
||||
|
||||
def base_name
|
||||
File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git")
|
||||
end
|
||||
|
||||
def shortref_for_display(ref)
|
||||
ref[0..6]
|
||||
end
|
||||
|
||||
def shortref_for_path(ref)
|
||||
ref[0..11]
|
||||
end
|
||||
|
||||
def glob_for_display
|
||||
default_glob? ? nil : "glob: #{@glob}"
|
||||
end
|
||||
|
||||
def default_glob?
|
||||
@glob == DEFAULT_GLOB
|
||||
end
|
||||
|
||||
def uri_hash
|
||||
if %r{^\w+://(\w+@)?}.match?(uri)
|
||||
# Downcase the domain component of the URI
|
||||
# and strip off a trailing slash, if one is present
|
||||
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
|
||||
end
|
||||
# We use SHA1 here for historical reason and to preserve backward compatibility.
|
||||
# But a transition to a simpler mangling algorithm would be welcome.
|
||||
Bundler::Digest.sha1(input)
|
||||
end
|
||||
|
||||
def cached_revision
|
||||
options["revision"]
|
||||
end
|
||||
|
||||
def cached?
|
||||
cache_path.exist?
|
||||
end
|
||||
|
||||
def git_proxy
|
||||
@git_proxy ||= GitProxy.new(cache_path, uri, options, cached_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
|
||||
|
||||
# no-op, since we validate when re-serializing the gemspec
|
||||
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)
|
||||
end
|
||||
|
||||
def git_scope
|
||||
"#{base_name}-#{uri_hash}"
|
||||
end
|
||||
|
||||
def extension_cache_slug(_)
|
||||
extension_dir_name
|
||||
end
|
||||
|
||||
def override_for(path)
|
||||
Bundler.settings.local_overrides.key(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,455 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Git
|
||||
class GitNotInstalledError < GitError
|
||||
def initialize
|
||||
msg = String.new
|
||||
msg << "You need to install git to be able to use gems from git repositories. "
|
||||
msg << "For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git"
|
||||
super msg
|
||||
end
|
||||
end
|
||||
|
||||
class GitNotAllowedError < GitError
|
||||
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 << "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
|
||||
super msg
|
||||
end
|
||||
end
|
||||
|
||||
class GitCommandError < GitError
|
||||
attr_reader :command
|
||||
|
||||
def initialize(command, path, extra_info = nil)
|
||||
@command = command
|
||||
|
||||
msg = String.new("Git error: command `#{command}`")
|
||||
msg << " in directory #{path}" if path
|
||||
msg << " has failed."
|
||||
msg << "\n#{extra_info}" if extra_info
|
||||
super msg
|
||||
end
|
||||
end
|
||||
|
||||
class MissingGitRevisionError < GitCommandError
|
||||
def initialize(command, destination_path, ref, repo)
|
||||
msg = "Revision #{ref} does not exist in the repository #{repo}. Maybe you misspelled it?"
|
||||
super command, destination_path, msg
|
||||
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, :branch, :tag, :ref, :explicit_ref
|
||||
attr_writer :revision
|
||||
|
||||
def initialize(path, uri, options = {}, revision = nil, git = nil)
|
||||
@path = path
|
||||
@uri = uri
|
||||
@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 ||= allowed_with_path { find_local_revision }
|
||||
end
|
||||
|
||||
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.match?(/^\* (.*)$/)
|
||||
end
|
||||
end
|
||||
|
||||
def version
|
||||
@version ||= full_version.match(/((\.?\d+)+).*/)[1]
|
||||
end
|
||||
|
||||
def full_version
|
||||
@full_version ||= git_local("--version").sub(/git version\s*/, "").strip
|
||||
end
|
||||
|
||||
def checkout
|
||||
return if has_revision_cached?
|
||||
|
||||
Bundler.ui.info "Fetching #{credential_filtered_uri}"
|
||||
|
||||
extra_fetch_needed = clone_needs_extra_fetch?
|
||||
unshallow_needed = clone_needs_unshallow?
|
||||
return unless extra_fetch_needed || unshallow_needed
|
||||
|
||||
git_remote_fetch(unshallow_needed ? ["--unshallow"] : depth_args)
|
||||
end
|
||||
|
||||
def copy_to(destination, submodules = false)
|
||||
unless File.exist?(destination.join(".git"))
|
||||
begin
|
||||
SharedHelpers.filesystem_access(destination.dirname) do |p|
|
||||
FileUtils.mkdir_p(p)
|
||||
end
|
||||
SharedHelpers.filesystem_access(destination) do |p|
|
||||
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)
|
||||
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 " \
|
||||
"create a directory, but a file exists - #{file_path}. Please delete " \
|
||||
"this file and try again."
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def git_remote_fetch(args)
|
||||
command = ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact
|
||||
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
|
||||
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
|
||||
|
||||
command = ["clone", "--bare", "--no-hardlinks", "--quiet", *extra_clone_args, "--", configured_uri, path.to_s]
|
||||
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")
|
||||
raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri)
|
||||
else
|
||||
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 if full_clone?
|
||||
|
||||
@revision && @revision != head_revision
|
||||
end
|
||||
|
||||
def extra_ref
|
||||
return false if not_pinned?
|
||||
return true unless full_clone?
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def not_pinned?
|
||||
branch || tag || 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)
|
||||
end
|
||||
end
|
||||
|
||||
def git(*command, dir: nil)
|
||||
run_command(*command, dir: dir) do |unredacted_command|
|
||||
check_allowed(unredacted_command)
|
||||
end
|
||||
end
|
||||
|
||||
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 && path.exist?
|
||||
git("cat-file", "-e", @revision, dir: path)
|
||||
true
|
||||
rescue GitError
|
||||
false
|
||||
end
|
||||
|
||||
def find_local_revision
|
||||
return head_revision if explicit_ref.nil?
|
||||
|
||||
find_revision_for(explicit_ref)
|
||||
end
|
||||
|
||||
def head_revision
|
||||
verify("HEAD")
|
||||
end
|
||||
|
||||
def find_revision_for(reference)
|
||||
verify(reference)
|
||||
rescue GitCommandError => e
|
||||
raise MissingGitRevisionError.new(e.command, path, reference, credential_filtered_uri)
|
||||
end
|
||||
|
||||
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
|
||||
elsif File.exist?(uri)
|
||||
"file://#{uri}"
|
||||
else
|
||||
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
|
||||
|
||||
raise GitNotInstalledError.new if allowed && !Bundler.git_present?
|
||||
|
||||
allowed
|
||||
end
|
||||
|
||||
def with_path(&blk)
|
||||
checkout unless path.exist?
|
||||
blk.call
|
||||
end
|
||||
|
||||
def allowed_with_path
|
||||
return with_path { yield } if allow?
|
||||
raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application"
|
||||
end
|
||||
|
||||
def check_allowed(command)
|
||||
command_with_no_credentials = redact_and_check_presence(command)
|
||||
raise GitNotAllowedError.new(command_with_no_credentials) unless allow?
|
||||
command_with_no_credentials
|
||||
end
|
||||
|
||||
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(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
|
||||
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 || tag] if branch || tag
|
||||
args
|
||||
end
|
||||
|
||||
def depth_args
|
||||
return [] if full_clone?
|
||||
|
||||
["--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 full_clone?
|
||||
depth.nil?
|
||||
end
|
||||
|
||||
def supports_minus_c?
|
||||
@supports_minus_c ||= Gem::Version.new(version) >= Gem::Version.new("1.8.5")
|
||||
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_cloning_with_no_tags?
|
||||
@supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Metadata < Source
|
||||
def specs
|
||||
@specs ||= Index.build do |idx|
|
||||
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
|
||||
|
||||
if local_spec = Gem.loaded_specs["bundler"]
|
||||
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]
|
||||
# can't point to the actual gemspec or else the require paths will be wrong
|
||||
s.loaded_from = __dir__
|
||||
end
|
||||
end
|
||||
|
||||
idx.each {|s| s.source = self }
|
||||
end
|
||||
end
|
||||
|
||||
def options
|
||||
{}
|
||||
end
|
||||
|
||||
def install(spec, _opts = {})
|
||||
print_using_message "Using #{version_message(spec)}"
|
||||
nil
|
||||
end
|
||||
|
||||
def to_s
|
||||
"the local ruby installation"
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class
|
||||
end
|
||||
alias_method :eql?, :==
|
||||
|
||||
def hash
|
||||
self.class.hash
|
||||
end
|
||||
|
||||
def version_message(spec)
|
||||
"#{spec.name} #{spec.version}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,261 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Path < Source
|
||||
autoload :Installer, File.expand_path("path/installer", __dir__)
|
||||
|
||||
attr_reader :path, :options, :root_path, :original_path
|
||||
attr_writer :name
|
||||
attr_accessor :version
|
||||
|
||||
protected :original_path
|
||||
|
||||
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)
|
||||
else
|
||||
expanded_path
|
||||
end
|
||||
end
|
||||
|
||||
@name = options["name"]
|
||||
@version = options["version"]
|
||||
|
||||
# Stores the original path. If at any point we move to the
|
||||
# cached directory, we still have the original path to copy from.
|
||||
@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
|
||||
|
||||
def to_lock
|
||||
out = String.new("PATH\n")
|
||||
out << " remote: #{lockfile_path}\n"
|
||||
out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB
|
||||
out << " specs:\n"
|
||||
end
|
||||
|
||||
def to_s
|
||||
"source at `#{@path}`"
|
||||
end
|
||||
|
||||
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 &&
|
||||
version == other.version
|
||||
end
|
||||
|
||||
alias_method :==, :eql?
|
||||
|
||||
def name
|
||||
File.basename(expanded_path.to_s)
|
||||
end
|
||||
|
||||
def install(spec, options = {})
|
||||
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)
|
||||
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 if expand(@original_path).to_s.index(root_path.to_s + "/") == 0
|
||||
|
||||
unless @original_path.exist?
|
||||
raise GemNotFound, "Can't cache gem #{version_message(spec)} because #{self} is missing!"
|
||||
end
|
||||
|
||||
FileUtils.rm_rf(app_cache_path)
|
||||
FileUtils.cp_r("#{@original_path}/.", app_cache_path)
|
||||
FileUtils.touch(app_cache_path.join(".bundlecache"))
|
||||
end
|
||||
|
||||
def local_specs(*)
|
||||
@local_specs ||= load_spec_files
|
||||
end
|
||||
|
||||
def specs
|
||||
if has_app_cache?
|
||||
@path = app_cache_path
|
||||
@expanded_path = nil # Invalidate
|
||||
end
|
||||
local_specs
|
||||
end
|
||||
|
||||
def app_cache_dirname
|
||||
name
|
||||
end
|
||||
|
||||
def root
|
||||
Bundler.root
|
||||
end
|
||||
|
||||
def expanded_original_path
|
||||
@expanded_original_path ||= expand(original_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expanded_path
|
||||
@expanded_path ||= expand(path)
|
||||
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
|
||||
rescue ArgumentError => e
|
||||
Bundler.ui.debug(e)
|
||||
raise PathError, "There was an error while trying to use the path " \
|
||||
"`#{somepath}`.\nThe error message was: #{e.message}."
|
||||
end
|
||||
|
||||
def lockfile_path
|
||||
return relative_path(original_path) if original_path.absolute?
|
||||
expand(original_path).relative_path_from(root)
|
||||
end
|
||||
|
||||
def app_cache_path(custom_path = nil)
|
||||
@app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname)
|
||||
end
|
||||
|
||||
def has_app_cache?
|
||||
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
|
||||
|
||||
def load_spec_files
|
||||
index = Index.new
|
||||
|
||||
if File.directory?(expanded_path)
|
||||
# We sort depth-first since `<<` will override the earlier-found specs
|
||||
Gem::Util.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file|
|
||||
next unless spec = load_gemspec(file)
|
||||
spec.source = self
|
||||
|
||||
# Validation causes extension_dir to be calculated, which depends
|
||||
# on #source, so we validate here instead of load_gemspec
|
||||
validate_spec(spec)
|
||||
index << spec
|
||||
end
|
||||
|
||||
if index.empty? && @name && @version
|
||||
index << Gem::Specification.new do |s|
|
||||
s.name = @name
|
||||
s.source = self
|
||||
s.version = Gem::Version.new(@version)
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = "Fake gemspec for #{@name}"
|
||||
s.relative_loaded_from = "#{@name}.gemspec"
|
||||
s.authors = ["no one"]
|
||||
if expanded_path.join("bin").exist?
|
||||
executables = expanded_path.join("bin").children
|
||||
executables.reject! {|p| File.directory?(p) }
|
||||
s.executables = executables.map {|c| c.basename.to_s }
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
message = String.new("The path `#{expanded_path}` ")
|
||||
message << if File.exist?(expanded_path)
|
||||
"is not a directory."
|
||||
else
|
||||
"does not exist."
|
||||
end
|
||||
raise PathError, message
|
||||
end
|
||||
|
||||
index
|
||||
end
|
||||
|
||||
def relative_path(path = self.path)
|
||||
if path.to_s.start_with?(root_path.to_s)
|
||||
return path.relative_path_from(root_path)
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
def generate_bin(spec, options = {})
|
||||
gem_dir = Pathname.new(spec.full_gem_path)
|
||||
|
||||
# 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)
|
||||
next if File.directory?(path)
|
||||
begin
|
||||
Pathname.new(path).relative_path_from(gem_dir).to_s
|
||||
rescue ArgumentError
|
||||
path
|
||||
end
|
||||
end.compact
|
||||
|
||||
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)
|
||||
)
|
||||
installer.post_install
|
||||
rescue Gem::InvalidSpecificationException => e
|
||||
Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
|
||||
"This prevents bundler from installing bins or native extensions, but " \
|
||||
"that may not affect its functionality."
|
||||
|
||||
if !spec.extensions.empty? && !spec.email.empty?
|
||||
Bundler.ui.warn "If you need to use this package without installing it from a gem " \
|
||||
"repository, please contact #{spec.email} and ask them " \
|
||||
"to modify their .gemspec so it can work with `gem build`."
|
||||
end
|
||||
|
||||
Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../../rubygems_gem_installer"
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Path
|
||||
class Installer < Bundler::RubyGemsGemInstaller
|
||||
attr_reader :spec
|
||||
|
||||
def initialize(spec, options = {})
|
||||
@options = options
|
||||
@spec = spec
|
||||
@gem_dir = Bundler.rubygems.path(spec.full_gem_path)
|
||||
@wrappers = true
|
||||
@env_shebang = true
|
||||
@format_executable = options[:format_executable] || false
|
||||
@build_args = options[:build_args] || Bundler.rubygems.build_args
|
||||
@gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin"
|
||||
@disable_extensions = options[:disable_extensions]
|
||||
@bin_dir = @gem_bin_dir
|
||||
end
|
||||
|
||||
def post_install
|
||||
run_hooks(:pre_install)
|
||||
|
||||
unless @disable_extensions
|
||||
build_extensions
|
||||
run_hooks(:post_build)
|
||||
end
|
||||
|
||||
generate_bin unless spec.executables.empty?
|
||||
|
||||
run_hooks(:post_install)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def run_hooks(type)
|
||||
hooks_meth = "#{type}_hooks"
|
||||
return unless Gem.respond_to?(hooks_meth)
|
||||
Gem.send(hooks_meth).each do |hook|
|
||||
result = hook.call(self)
|
||||
next unless result == false
|
||||
location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
|
||||
message = "#{type} hook#{location} failed for #{spec.full_name}"
|
||||
raise InstallHookError, message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,516 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rubygems/user_interaction"
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Rubygems < Source
|
||||
autoload :Remote, File.expand_path("rubygems/remote", __dir__)
|
||||
|
||||
# Ask for X gems per API request
|
||||
API_REQUEST_SIZE = 50
|
||||
|
||||
attr_reader :remotes
|
||||
|
||||
def initialize(options = {})
|
||||
@options = options
|
||||
@remotes = []
|
||||
@dependency_names = []
|
||||
@allow_remote = false
|
||||
@allow_cached = options["allow_cached"] || false
|
||||
@allow_local = options["allow_local"] || false
|
||||
@checksum_store = Checksum::Store.new
|
||||
|
||||
Array(options["remotes"]).reverse_each {|r| add_remote(r) }
|
||||
end
|
||||
|
||||
def caches
|
||||
@caches ||= [cache_path, *Bundler.rubygems.gem_cache]
|
||||
end
|
||||
|
||||
def local_only!
|
||||
@specs = nil
|
||||
@allow_local = true
|
||||
@allow_cached = false
|
||||
@allow_remote = false
|
||||
end
|
||||
|
||||
def local!
|
||||
return if @allow_local
|
||||
|
||||
@specs = nil
|
||||
@allow_local = true
|
||||
end
|
||||
|
||||
def remote!
|
||||
return if @allow_remote
|
||||
|
||||
@specs = nil
|
||||
@allow_remote = true
|
||||
end
|
||||
|
||||
def cached!
|
||||
return if @allow_cached
|
||||
|
||||
@specs = nil
|
||||
@allow_local = true
|
||||
@allow_cached = true
|
||||
end
|
||||
|
||||
def hash
|
||||
@remotes.hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
other.is_a?(Rubygems) && other.credless_remotes == credless_remotes
|
||||
end
|
||||
|
||||
alias_method :==, :eql?
|
||||
|
||||
def include?(o)
|
||||
o.is_a?(Rubygems) && (o.credless_remotes - credless_remotes).empty?
|
||||
end
|
||||
|
||||
def multiple_remotes?
|
||||
@remotes.size > 1
|
||||
end
|
||||
|
||||
def no_remotes?
|
||||
@remotes.size == 0
|
||||
end
|
||||
|
||||
def can_lock?(spec)
|
||||
return super unless multiple_remotes?
|
||||
include?(spec.source)
|
||||
end
|
||||
|
||||
def options
|
||||
{ "remotes" => @remotes.map(&:to_s) }
|
||||
end
|
||||
|
||||
def self.from_lock(options)
|
||||
options["remotes"] = Array(options.delete("remote")).reverse
|
||||
new(options)
|
||||
end
|
||||
|
||||
def to_lock
|
||||
out = String.new("GEM\n")
|
||||
remotes.reverse_each do |remote|
|
||||
out << " remote: #{suppress_configured_credentials remote}\n"
|
||||
end
|
||||
out << " specs:\n"
|
||||
end
|
||||
|
||||
def to_s
|
||||
if remotes.empty?
|
||||
"locally installed gems"
|
||||
elsif @allow_remote && @allow_cached && @allow_local
|
||||
"rubygems repository #{remote_names}, cached gems or installed locally"
|
||||
elsif @allow_remote && @allow_local
|
||||
"rubygems repository #{remote_names} or installed locally"
|
||||
elsif @allow_remote
|
||||
"rubygems repository #{remote_names}"
|
||||
elsif @allow_cached && @allow_local
|
||||
"cached gems or installed locally"
|
||||
else
|
||||
"locally installed gems"
|
||||
end
|
||||
end
|
||||
|
||||
def identifier
|
||||
if remotes.empty?
|
||||
"locally installed gems"
|
||||
else
|
||||
"rubygems repository #{remote_names}"
|
||||
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.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
|
||||
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 installed?(spec) && !force
|
||||
print_using_message "Using #{version_message(spec, options[:previous_spec])}"
|
||||
return nil # no post-install message
|
||||
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
|
||||
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)
|
||||
)
|
||||
|
||||
if spec.remote
|
||||
s = begin
|
||||
installer.spec
|
||||
rescue Gem::Package::FormatError
|
||||
Bundler.rm_rf(path)
|
||||
raise
|
||||
rescue Gem::Security::Exception => e
|
||||
raise SecurityError,
|
||||
"The gem #{File.basename(path, ".gem")} can't be installed because " \
|
||||
"the security policy didn't allow it, with the message: #{e.message}"
|
||||
end
|
||||
|
||||
spec.__swap__(s)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
spec.full_gem_path = installed_spec.full_gem_path
|
||||
spec.loaded_from = installed_spec.loaded_from
|
||||
|
||||
spec.post_install_message
|
||||
end
|
||||
|
||||
def cache(spec, custom_path = nil)
|
||||
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)}"
|
||||
FileUtils.cp(cached_path, Bundler.app_cache(custom_path))
|
||||
rescue Errno::EACCES => e
|
||||
Bundler.ui.debug(e)
|
||||
raise InstallError, e.message
|
||||
end
|
||||
|
||||
def cached_built_in_gem(spec)
|
||||
cached_path = cached_path(spec)
|
||||
if cached_path.nil?
|
||||
remote_spec = remote_specs.search(spec).first
|
||||
if remote_spec
|
||||
cached_path = fetch_gem(remote_spec)
|
||||
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
|
||||
end
|
||||
cached_path
|
||||
end
|
||||
|
||||
def add_remote(source)
|
||||
uri = normalize_uri(source)
|
||||
@remotes.unshift(uri) unless @remotes.include?(uri)
|
||||
end
|
||||
|
||||
def spec_names
|
||||
if dependency_api_available?
|
||||
remote_specs.spec_names
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def unmet_deps
|
||||
if dependency_api_available?
|
||||
remote_specs.unmet_dependency_names
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def remote_fetchers
|
||||
@remote_fetchers ||= remotes.to_h do |uri|
|
||||
remote = Source::Rubygems::Remote.new(uri)
|
||||
[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 dependency_api_available?
|
||||
|
||||
unmet_dependency_names = unmet_dependency_names.call
|
||||
unless unmet_dependency_names.nil?
|
||||
if api_fetchers.size <= 1
|
||||
# can't do this when there are multiple fetchers because then we might not fetch from _all_
|
||||
# of them
|
||||
unmet_dependency_names -= remote_specs.spec_names # avoid re-fetching things we've already gotten
|
||||
end
|
||||
return if unmet_dependency_names.empty?
|
||||
end
|
||||
|
||||
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, remote_specs)
|
||||
|
||||
specs.use remote_specs
|
||||
end
|
||||
|
||||
def dependency_names_to_double_check
|
||||
names = []
|
||||
remote_specs.each do |spec|
|
||||
case spec
|
||||
when EndpointSpecification, Gem::Specification, StubSpecification, LazySpecification
|
||||
names.concat(spec.runtime_dependencies.map(&:name))
|
||||
when RemoteSpecification # from the full index
|
||||
return nil
|
||||
else
|
||||
raise "unhandled spec type (#{spec.inspect})"
|
||||
end
|
||||
end
|
||||
names
|
||||
end
|
||||
|
||||
def dependency_api_available?
|
||||
@allow_remote && api_fetchers.any?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def remote_names
|
||||
remotes.map(&:to_s).join(", ")
|
||||
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 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
|
||||
|
||||
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 = URINormalizer.normalize_suffix(uri.to_s)
|
||||
require_relative "../vendored_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?(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
|
||||
else
|
||||
remote.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def installed_specs
|
||||
@installed_specs ||= Index.build do |idx|
|
||||
Bundler.rubygems.all_specs.reverse_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
|
||||
end
|
||||
|
||||
def cached_specs
|
||||
@cached_specs ||= begin
|
||||
idx = Index.new
|
||||
|
||||
Dir["#{cache_path}/*.gem"].each do |gemfile|
|
||||
s ||= Bundler.rubygems.spec_from_gem(gemfile)
|
||||
s.source = self
|
||||
idx << s
|
||||
end
|
||||
|
||||
idx
|
||||
end
|
||||
end
|
||||
|
||||
def api_fetchers
|
||||
fetchers.select(&:api_fetcher?)
|
||||
end
|
||||
|
||||
def remote_specs
|
||||
@remote_specs ||= Index.build do |idx|
|
||||
index_fetchers = fetchers - api_fetchers
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 = package_path(cache_path, spec)
|
||||
return gem_path if File.exist?(gem_path)
|
||||
|
||||
SharedHelpers.filesystem_access(cache_path) do |p|
|
||||
FileUtils.mkdir_p(p)
|
||||
end
|
||||
download_gem(spec, cache_path, previous_spec)
|
||||
|
||||
gem_path
|
||||
end
|
||||
|
||||
def installed?(spec)
|
||||
installed_specs[spec].any? && !spec.deleted_gem?
|
||||
end
|
||||
|
||||
def rubygems_dir
|
||||
Bundler.bundle_path
|
||||
end
|
||||
|
||||
def default_cache_path_for(dir)
|
||||
"#{dir}/cache"
|
||||
end
|
||||
|
||||
def cache_path
|
||||
Bundler.app_cache
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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.
|
||||
#
|
||||
# @param [Specification] spec
|
||||
# the spec we want to download or retrieve from the cache.
|
||||
#
|
||||
# @param [String] download_cache_path
|
||||
# the local directory the .gem will end up in.
|
||||
#
|
||||
# @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, 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)
|
||||
end
|
||||
|
||||
# Returns the global cache path of the calling Rubygems::Source object.
|
||||
#
|
||||
# Note that the Source determines the path's subdirectory. We use this
|
||||
# subdirectory in the global cache path so that gems with the same name
|
||||
# -- and possibly different versions -- from different sources are saved
|
||||
# to their respective subdirectories and do not override one another.
|
||||
#
|
||||
# @param [Gem::Specification] specification
|
||||
#
|
||||
# @return [Pathname] The global cache path.
|
||||
#
|
||||
def download_cache_path(spec)
|
||||
return unless Bundler.feature_flag.global_gem_cache?
|
||||
return unless remote = spec.remote
|
||||
return unless cache_slug = remote.cache_slug
|
||||
|
||||
Bundler.user_cache.join("gems", cache_slug)
|
||||
end
|
||||
|
||||
def extension_cache_slug(spec)
|
||||
return unless remote = spec.remote
|
||||
remote.cache_slug
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class Rubygems
|
||||
class Remote
|
||||
attr_reader :uri, :anonymized_uri, :original_uri
|
||||
|
||||
def initialize(uri)
|
||||
orig_uri = uri
|
||||
uri = Bundler.settings.mirror_for(uri)
|
||||
@original_uri = orig_uri if orig_uri != uri
|
||||
fallback_auth = Bundler.settings.credentials_for(uri)
|
||||
|
||||
@uri = apply_auth(uri, fallback_auth).freeze
|
||||
@anonymized_uri = remove_auth(@uri).freeze
|
||||
end
|
||||
|
||||
# @return [String] A slug suitable for use as a cache key for this
|
||||
# remote.
|
||||
#
|
||||
def cache_slug
|
||||
@cache_slug ||= begin
|
||||
return nil unless SharedHelpers.md5_available?
|
||||
|
||||
cache_uri = original_uri || uri
|
||||
|
||||
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[-1] = uri_digest
|
||||
uri_parts.compact.join(".")
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"rubygems remote at #{anonymized_uri}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_auth(uri, auth)
|
||||
if auth && uri.userinfo.nil?
|
||||
uri = uri.dup
|
||||
uri.userinfo = auth
|
||||
end
|
||||
|
||||
uri
|
||||
rescue Gem::URI::InvalidComponentError
|
||||
error_message = "Please CGI escape your usernames and passwords before " \
|
||||
"setting them for authentication."
|
||||
raise HTTPError.new(error_message)
|
||||
end
|
||||
|
||||
def remove_auth(uri)
|
||||
if uri.userinfo
|
||||
uri = uri.dup
|
||||
uri.user = uri.password = nil
|
||||
end
|
||||
|
||||
uri
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Source
|
||||
class RubygemsAggregate
|
||||
attr_reader :source_map, :sources
|
||||
|
||||
def initialize(sources, source_map)
|
||||
@sources = sources
|
||||
@source_map = source_map
|
||||
|
||||
@index = build_index
|
||||
end
|
||||
|
||||
def specs
|
||||
@index
|
||||
end
|
||||
|
||||
def identifier
|
||||
to_s
|
||||
end
|
||||
|
||||
def to_s
|
||||
"any of the sources"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_index
|
||||
Index.build do |idx|
|
||||
dependency_names = source_map.pinned_spec_names
|
||||
|
||||
sources.all_sources.each do |source|
|
||||
source.dependency_names = dependency_names - source_map.pinned_spec_names(source)
|
||||
idx.add_source source.specs
|
||||
dependency_names.concat(source.unmet_deps).uniq!
|
||||
end
|
||||
|
||||
double_check_for_index(idx, dependency_names)
|
||||
end
|
||||
end
|
||||
|
||||
# Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both
|
||||
# sources A and B. At this point, the API request will have found all the versions of Bar in source A,
|
||||
# but will not have found any versions of Bar from source B, which is a problem if the requested version
|
||||
# of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for
|
||||
# each spec we found, we add all possible versions from all sources to the index.
|
||||
def double_check_for_index(idx, dependency_names)
|
||||
pinned_names = source_map.pinned_spec_names
|
||||
|
||||
names = :names # do this so we only have to traverse to get dependency_names from the index once
|
||||
unmet_dependency_names = lambda do
|
||||
return names unless names == :names
|
||||
new_names = sources.all_sources.map(&:dependency_names_to_double_check)
|
||||
return names = nil if new_names.compact!
|
||||
names = new_names.flatten(1).concat(dependency_names)
|
||||
names.uniq!
|
||||
names -= pinned_names
|
||||
names
|
||||
end
|
||||
|
||||
sources.all_sources.each do |source|
|
||||
source.double_check_for(unmet_dependency_names)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user