Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,19 @@
# frozen_string_literal: true
module Bundler
class Source
class Gemspec < Path
attr_reader :gemspec
attr_writer :checksum_store
def initialize(options)
super
@gemspec = options["gemspec"]
end
def to_s
"gemspec at `#{@path}`"
end
end
end
end
@@ -0,0 +1,453 @@
# 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 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
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, glob, submodules].hash
end
def eql?(other)
other.is_a?(Git) && uri == other.uri && ref == other.ref &&
branch == other.branch && name == other.name &&
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
at = "#{at}@" if at
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, locked_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
@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, 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 = locked_revision && locked_revision != 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
changed
end
def specs(*)
set_cache_path!(app_cache_path) if use_app_cache?
if requires_checkout? && !@copied
fetch unless use_app_cache?
checkout
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
checkout
end
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)
cache_to(custom_path, try_migrate: false)
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.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(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
def local?
@local
end
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?
SharedHelpers.glob_files_in_dir(@glob, destination.to_s).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
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_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?
locked_revision && super
end
def use_app_cache?
has_app_cache? && !local?
end
def requires_checkout?
allow_git_ops? && !local? && !locked_revision_checked_out?
end
def locked_revision_checked_out?
locked_revision && locked_revision == revision && installed?
end
def installed?
git_proxy.installed_to?(install_path)
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 locked_revision
options["revision"]
end
def cached?
cache_path.exist?
end
def git_proxy
@git_proxy ||= GitProxy.new(cache_path, uri, options, locked_revision, self)
end
def fetch
git_proxy.checkout
rescue GitError => e
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)
# Expand the path before the chdir below, since resolving it inside the
# block would base it on the gemspec directory instead of `root`.
gemspec_path = Pathname.new(file).expand_path(root)
SharedHelpers.chdir(gemspec_path.dirname.to_s) do
stub = Gem::StubSpecification.gemspec_stub(gemspec_path.to_s, install_path.parent, install_path.parent)
stub.full_gem_path = gemspec_path.dirname.to_s
StubSpecification.from_stub(stub)
end
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
def bare_repo?(path)
File.exist?(path.join("objects")) && File.exist?(path.join("HEAD"))
end
end
end
end
@@ -0,0 +1,579 @@
# 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/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
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 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
@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
self.class.version
end
def full_version
self.class.full_version
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
# The copy is cloned from the local bare cache, which holds no Git LFS
# objects, so point origin back at the real remote and let git-lfs derive
# its endpoint from there when checking out. Use the credential-filtered
# URI to avoid persisting secrets in the copy's .git/config; auth is left
# to git's credential helper.
git "remote", "set-url", "origin", credential_filtered_uri, dir: destination
File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination)
rescue Errno::EEXIST => e
file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1]
raise GitError, "Bundler could not install a gem because it needs to " \
"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
def installed_to?(destination)
# if copy_to is interrupted, it may leave a partially installed directory that
# contains .git but no other files -- consider this not to be installed
Dir.exist?(destination) && (Dir.children(destination) - [".git"]).any?
end
private
def git_remote_fetch(args)
command = fetch_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")
default_branch = renamed_remote_default_branch if tracking_remote_default_branch?
if default_branch
out = follow_remote_default_branch(args, default_branch)
return out if out
end
raise MissingGitRevisionError.new(command_with_no_credentials, path, commit || explicit_ref || current_branch, 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
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_option || ref.nil?
end
def tracking_remote_default_branch?
explicit_ref.nil? && commit.nil?
end
# Returns nil on any failure, leaving HEAD untouched so the caller reports
# the original fetch failure. Runs inside the retry block, so it must not
# touch the caller's command locals.
def follow_remote_default_branch(args, default_branch)
reference = "refs/heads/#{default_branch}"
command = fetch_command(args, "#{reference}:#{reference}")
check_allowed(command)
out, err, status = capture(command, path)
unless status.success?
Bundler.ui.debug "Could not fetch #{reference} from #{credential_filtered_uri}: #{err}"
return
end
previous_branch = current_branch
begin
git "symbolic-ref", "HEAD", reference, dir: path
rescue GitError => e
Bundler.ui.debug "Could not repoint the cached clone at #{reference}: #{e.message}"
return
end
@current_branch = nil
Bundler.ui.warn "#{credential_filtered_uri} no longer has #{previous_branch}, " \
"now following its default branch #{default_branch}"
out
end
# The cached clone's HEAD branch is gone from the remote, so the remote's
# own idea of its default branch is the only thing left to follow.
def renamed_remote_default_branch
default_branch = remote_default_branch
return if default_branch.nil? || default_branch == current_branch
default_branch
end
def remote_default_branch
command = ["ls-remote", "--symref", "--", configured_uri, "HEAD"]
check_allowed(command)
out, err, status = capture(command, path)
unless status.success?
Bundler.ui.debug "Could not ask #{credential_filtered_uri} for its default branch: #{err}"
return
end
# A remote is free to advertise a ref name that is not valid UTF-8, and
# matching that as text raises out of the GitError family.
out.b[%r{^ref:\s+refs/heads/(.+?)\s+HEAD}, 1]
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 commit && path.exist?
git("cat-file", "-e", commit, 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. This is the URI given to git, so it's
# also the one command output must be filtered against.
def configured_uri
@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.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}", configured_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, configured_uri)
return [filtered_out, status] if ignore_err
filtered_err = URICredentialsFilter.credential_filtered_string(err, configured_uri)
[filtered_out, filtered_err, status]
end
end
def capture3_args_for(cmd, dir)
# Disable automatic maintenance so a background commit-graph write in
# the source repo can't race the hardlinking local clone and fail with
# "hardlink different from source".
opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
return ["git", *opts, *cmd] unless dir
# With safe.bareRepository=explicit, git refuses to discover a bare
# repository from -C, so the cache clone is named with --git-dir.
# Working trees, like a local override, still go through -C.
location = bare_repo?(dir) ? "--git-dir" : "-C"
["git", location, dir.to_s, *opts, *cmd]
end
def bare_repo?(dir)
File.exist?(File.join(dir, "objects")) && File.exist?(File.join(dir, "HEAD"))
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, spec = refspec)
["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, spec].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_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,67 @@
# 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"]
raise CorruptBundlerInstallError.new(local_spec) if local_spec.version.to_s != Bundler::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 }
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
def checksum_store
@checksum_store ||= Checksum::Store.new
end
end
end
end
@@ -0,0 +1,256 @@
# 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
@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(File.expand_path(root_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 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
alias_method :identifier, :to_s
alias_method :to_gemfile, :path
def hash
[self.class, expanded_path, version].hash
end
def eql?(other)
[Gemspec, Path].include?(other.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.settings[: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)
somepath.expand_path(root_path)
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)
spec.installed_by_version = Gem::VERSION
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
SharedHelpers.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
# 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)
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.filter_map do |path|
pathname = Pathname.new(path)
next path unless pathname.absolute?
next if File.directory?(path)
begin
pathname.relative_path_from(gem_dir).to_s
rescue ArgumentError
path
end
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)
)
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,606 @@
# 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 = 100
REQUIRE_MUTEX = Mutex.new
attr_accessor :remotes, :remote_cooldowns
def initialize(options = {})
@options = options
@remotes = []
@remote_cooldowns = {}
@dependency_names = []
@allow_remote = false
@allow_cached = false
@allow_local = options["allow_local"] || false
@prefer_local = false
@checksum_store = Checksum::Store.new
@gem_installers = {}
@gem_installers_mutex = Mutex.new
@remote_specs_mutex = Mutex.new
cooldown = options["cooldown"]
Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
@lockfile_remotes = @remotes if options["from_lockfile"]
end
def caches
@caches ||= [cache_path, *Bundler.rubygems.gem_cache]
end
def prefer_local!
@prefer_local = true
end
def local_only!
@specs = nil
@allow_local = true
@allow_cached = false
@allow_remote = false
end
def local_only?
@allow_local && !@allow_remote
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 unless File.exist?(cache_path)
return if @allow_cached
@specs = nil
@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.merge("from_lockfile" => true))
end
def to_lock
out = String.new("GEM\n")
lockfile_remotes.reverse_each do |remote|
out << " remote: #{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
# Snapshot per-version `created_at` from the remote info before installed
# / cached specs overwrite the EndpointSpecification objects that carry
# it. The cooldown filter consults `created_at` on every candidate, so
# local stubs need the published date back-filled to participate.
remote_created_at = collect_remote_created_at(index)
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
backfill_created_at(index, remote_created_at) unless remote_created_at.empty?
index
end
end
def download(spec, options = {})
if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
return true
end
installer = rubygems_gem_installer(spec, options)
if spec.remote
s = begin
installer.spec
rescue Gem::Package::FormatError
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
spec
end
def install(spec, options = {})
if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
print_using_message "Using #{version_message(spec, options[:previous_spec])}"
return nil # no post-install message
end
return if Bundler.settings[:no_install]
installer = rubygems_gem_installer(spec, options)
spec.source.checksum_store.register(spec, installer.gem_checksum)
message = "Installing #{version_message(spec, options[:previous_spec])}"
message += " with native extensions" if spec.extensions.any?
Bundler.ui.confirm message
installed_spec = nil
Gem.time("Installed #{spec.name} in", 0, true) do
installed_spec = installer.install
end
spec.full_gem_path = installed_spec.full_gem_path
spec.loaded_from = installed_spec.loaded_from
spec.base_dir = installed_spec.base_dir
spec.post_install_message
end
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, local: false)
cached_path = cached_gem(spec)
if cached_path.nil? && !local
remote_spec = remote_spec_for(spec)
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
end
cached_path
end
def add_remote(source, cooldown: nil)
uri = normalize_uri(source)
@remotes.unshift(uri) unless @remotes.include?(uri)
@remote_cooldowns[uri] = cooldown if cooldown
end
def cooldown_for(uri)
@remote_cooldowns[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, cooldown: cooldown_for(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
def clear_cache
@specs = nil
@installed_specs = nil
@default_specs = nil
@cached_specs = nil
end
def release_resolution_memory!
@specs = nil
@remote_specs_mutex.synchronize { @remote_specs = nil }
@fetchers&.each(&:release_resolution_memory!)
end
protected
def remote_names
remotes.map(&:to_s).join(", ")
end
def credless_remotes
remotes.map(&method(:remove_auth))
end
def cached_gem(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 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.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
idx << spec
end
end
end
def cached_specs
@cached_specs ||= begin
idx = Index.new
SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s).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 ||= @remote_specs_mutex.synchronize do
@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
end
# Looks up a single spec in the remote sources, fetching only its own
# name when the full remote index is not already materialized.
def remote_spec_for(spec)
return remote_specs.search(spec).first if @remote_specs || api_fetchers.empty?
index = Index.build do |idx|
fetch_names(api_fetchers, [spec.name], idx)
end
index.search(spec).first
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.installation_missing?
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
def collect_remote_created_at(index)
return {} unless @allow_remote
snapshot = {}
index.each do |spec|
next unless spec.respond_to?(:created_at) && spec.created_at
# Remember the remote that supplied the date too: when a source has
# several remotes with different per-URI cooldown settings we must
# restore the same one during backfill so `effective_cooldown` agrees.
snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote]
end
snapshot
end
def backfill_created_at(index, snapshot)
index.each do |spec|
next unless spec.respond_to?(:created_at=)
next if spec.created_at
remote_created_at, remote = snapshot[[spec.name, spec.version]]
next unless remote_created_at
spec.created_at = remote_created_at
spec.remote ||= remote if remote && spec.respond_to?(:remote=)
end
end
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.
#
# @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
Gem.time("Downloaded #{spec.name} in", 0, true) do
Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher)
end
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.settings[: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
# We are using a mutex to reaed 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 threads 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)
)
@gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer }
end
end
end
end
end
@@ -0,0 +1,86 @@
# frozen_string_literal: true
module Bundler
class Source
class Rubygems
class Remote
attr_reader :uri, :anonymized_uri, :original_uri, :cooldown
def initialize(uri, cooldown: nil)
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
@cooldown = cooldown
end
# Returns the cooldown days that apply to this remote, resolving the
# precedence CLI > config > Gemfile per-source. Returns nil if no
# cooldown applies.
def effective_cooldown
override = Bundler.settings[:cooldown]
return override if override
@cooldown
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.
#
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_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?
shortened_host_parts = host_parts[0...MAX_CACHE_SLUG_HOST_SIZE]
[shortened_host_parts, uri_digest].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,71 @@
# frozen_string_literal: true
module Bundler
class Source
class RubygemsAggregate
attr_reader :source_map, :sources
def initialize(sources, source_map, excluded_sources = [])
@sources = sources
@source_map = source_map
@excluded_sources = excluded_sources
@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|
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!
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