mirror of
https://github.com/ruby/ruby.git
synced 2025-08-23 21:14:23 +02:00

Running `bundle update --bundler` on a rails app locally:
```
==> memprof.after.txt <==
Total allocated: 301.90 kB (3794 objects)
Total retained: 73.24 kB (698 objects)
==> memprof.before.txt <==
Total allocated: 14.47 MB (196378 objects)
Total retained: 25.93 kB (202 objects)
```
So for a slight increase in retained memory (all keys are now retained),
we go from about 200k allocations in the settings file to under 4k
e64debb6ae
570 lines
14 KiB
Ruby
570 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Bundler
|
|
class Settings
|
|
autoload :Mirror, File.expand_path("mirror", __dir__)
|
|
autoload :Mirrors, File.expand_path("mirror", __dir__)
|
|
autoload :Validator, File.expand_path("settings/validator", __dir__)
|
|
|
|
BOOL_KEYS = %w[
|
|
allow_deployment_source_credential_changes
|
|
allow_offline_install
|
|
auto_clean_without_path
|
|
auto_install
|
|
cache_all
|
|
cache_all_platforms
|
|
clean
|
|
default_install_uses_path
|
|
deployment
|
|
disable_checksum_validation
|
|
disable_exec_load
|
|
disable_local_branch_check
|
|
disable_local_revision_check
|
|
disable_shared_gems
|
|
disable_version_check
|
|
force_ruby_platform
|
|
forget_cli_options
|
|
frozen
|
|
gem.changelog
|
|
gem.coc
|
|
gem.mit
|
|
git.allow_insecure
|
|
global_gem_cache
|
|
ignore_messages
|
|
init_gems_rb
|
|
inline
|
|
no_install
|
|
no_prune
|
|
path_relative_to_cwd
|
|
path.system
|
|
plugins
|
|
prefer_patch
|
|
print_only_version_number
|
|
setup_makes_kernel_gem_public
|
|
silence_deprecations
|
|
silence_root_warning
|
|
update_requires_all_flag
|
|
].freeze
|
|
|
|
NUMBER_KEYS = %w[
|
|
jobs
|
|
redirect
|
|
retry
|
|
ssl_verify_mode
|
|
timeout
|
|
].freeze
|
|
|
|
ARRAY_KEYS = %w[
|
|
only
|
|
with
|
|
without
|
|
].freeze
|
|
|
|
STRING_KEYS = %w[
|
|
bin
|
|
cache_path
|
|
console
|
|
gem.ci
|
|
gem.github_username
|
|
gem.linter
|
|
gem.rubocop
|
|
gem.test
|
|
gemfile
|
|
path
|
|
shebang
|
|
system_bindir
|
|
trust-policy
|
|
version
|
|
].freeze
|
|
|
|
DEFAULT_CONFIG = {
|
|
"BUNDLE_SILENCE_DEPRECATIONS" => false,
|
|
"BUNDLE_DISABLE_VERSION_CHECK" => true,
|
|
"BUNDLE_PREFER_PATCH" => false,
|
|
"BUNDLE_REDIRECT" => 5,
|
|
"BUNDLE_RETRY" => 3,
|
|
"BUNDLE_TIMEOUT" => 10,
|
|
"BUNDLE_VERSION" => "lockfile",
|
|
}.freeze
|
|
|
|
def initialize(root = nil)
|
|
@root = root
|
|
@local_config = load_config(local_config_file)
|
|
|
|
@env_config = ENV.to_h
|
|
@env_config.select! {|key, _value| key.start_with?("BUNDLE_") }
|
|
@env_config.delete("BUNDLE_")
|
|
|
|
@global_config = load_config(global_config_file)
|
|
@temporary = {}
|
|
|
|
@key_cache = {}
|
|
end
|
|
|
|
def [](name)
|
|
key = key_for(name)
|
|
|
|
value = nil
|
|
configs.each do |_, config|
|
|
value = config[key]
|
|
next if value.nil?
|
|
break
|
|
end
|
|
|
|
converted_value(value, name)
|
|
end
|
|
|
|
def set_command_option(key, value)
|
|
if Bundler.feature_flag.forget_cli_options?
|
|
temporary(key => value)
|
|
value
|
|
else
|
|
set_local(key, value)
|
|
end
|
|
end
|
|
|
|
def set_command_option_if_given(key, value)
|
|
return if value.nil?
|
|
set_command_option(key, value)
|
|
end
|
|
|
|
def set_local(key, value)
|
|
local_config_file || raise(GemfileNotFound, "Could not locate Gemfile")
|
|
|
|
set_key(key, value, @local_config, local_config_file)
|
|
end
|
|
|
|
def temporary(update)
|
|
existing = Hash[update.map {|k, _| [k, @temporary[key_for(k)]] }]
|
|
update.each do |k, v|
|
|
set_key(k, v, @temporary, nil)
|
|
end
|
|
return unless block_given?
|
|
begin
|
|
yield
|
|
ensure
|
|
existing.each {|k, v| set_key(k, v, @temporary, nil) }
|
|
end
|
|
end
|
|
|
|
def set_global(key, value)
|
|
set_key(key, value, @global_config, global_config_file)
|
|
end
|
|
|
|
def all
|
|
keys = @temporary.keys.union(@global_config.keys, @local_config.keys, @env_config.keys)
|
|
|
|
keys.map! do |key|
|
|
key = key.delete_prefix("BUNDLE_")
|
|
key.gsub!("___", "-")
|
|
key.gsub!("__", ".")
|
|
key.downcase!
|
|
key
|
|
end.sort!
|
|
keys
|
|
end
|
|
|
|
def local_overrides
|
|
repos = {}
|
|
all.each do |k|
|
|
repos[k.delete_prefix("local.")] = self[k] if k.start_with?("local.")
|
|
end
|
|
repos
|
|
end
|
|
|
|
def mirror_for(uri)
|
|
if uri.is_a?(String)
|
|
require_relative "vendored_uri"
|
|
uri = Bundler::URI(uri)
|
|
end
|
|
|
|
gem_mirrors.for(uri.to_s).uri
|
|
end
|
|
|
|
def credentials_for(uri)
|
|
self[uri.to_s] || self[uri.host]
|
|
end
|
|
|
|
def gem_mirrors
|
|
all.inject(Mirrors.new) do |mirrors, k|
|
|
mirrors.parse(k, self[k]) if k.start_with?("mirror.")
|
|
mirrors
|
|
end
|
|
end
|
|
|
|
def locations(key)
|
|
key = key_for(key)
|
|
configs.keys.inject({}) do |partial_locations, level|
|
|
value_on_level = configs[level][key]
|
|
partial_locations[level] = value_on_level unless value_on_level.nil?
|
|
partial_locations
|
|
end
|
|
end
|
|
|
|
def pretty_values_for(exposed_key)
|
|
key = key_for(exposed_key)
|
|
|
|
locations = []
|
|
|
|
if value = @temporary[key]
|
|
locations << "Set for the current command: #{printable_value(value, exposed_key).inspect}"
|
|
end
|
|
|
|
if value = @local_config[key]
|
|
locations << "Set for your local app (#{local_config_file}): #{printable_value(value, exposed_key).inspect}"
|
|
end
|
|
|
|
if value = @env_config[key]
|
|
locations << "Set via #{key}: #{printable_value(value, exposed_key).inspect}"
|
|
end
|
|
|
|
if value = @global_config[key]
|
|
locations << "Set for the current user (#{global_config_file}): #{printable_value(value, exposed_key).inspect}"
|
|
end
|
|
|
|
return ["You have not configured a value for `#{exposed_key}`"] if locations.empty?
|
|
locations
|
|
end
|
|
|
|
def processor_count
|
|
require "etc"
|
|
Etc.nprocessors
|
|
rescue StandardError
|
|
1
|
|
end
|
|
|
|
# for legacy reasons, in Bundler 2, we do not respect :disable_shared_gems
|
|
def path
|
|
configs.each do |_level, settings|
|
|
path = value_for("path", settings)
|
|
path_system = value_for("path.system", settings)
|
|
disabled_shared_gems = value_for("disable_shared_gems", settings)
|
|
next if path.nil? && path_system.nil? && disabled_shared_gems.nil?
|
|
system_path = path_system || (disabled_shared_gems == false)
|
|
return Path.new(path, system_path)
|
|
end
|
|
|
|
path = "vendor/bundle" if self[:deployment]
|
|
|
|
Path.new(path, false)
|
|
end
|
|
|
|
Path = Struct.new(:explicit_path, :system_path) do
|
|
def path
|
|
path = base_path
|
|
path = File.join(path, Bundler.ruby_scope) unless use_system_gems?
|
|
path
|
|
end
|
|
|
|
def use_system_gems?
|
|
return true if system_path
|
|
return false if explicit_path
|
|
!Bundler.feature_flag.default_install_uses_path?
|
|
end
|
|
|
|
def base_path
|
|
path = explicit_path
|
|
path ||= ".bundle" unless use_system_gems?
|
|
path ||= Bundler.rubygems.gem_dir
|
|
path
|
|
end
|
|
|
|
def base_path_relative_to_pwd
|
|
base_path = Pathname.new(self.base_path)
|
|
expanded_base_path = base_path.expand_path(Bundler.root)
|
|
relative_path = expanded_base_path.relative_path_from(Pathname.pwd)
|
|
if relative_path.to_s.start_with?("..")
|
|
relative_path = base_path if base_path.absolute?
|
|
else
|
|
relative_path = Pathname.new(File.join(".", relative_path))
|
|
end
|
|
relative_path
|
|
rescue ArgumentError
|
|
expanded_base_path
|
|
end
|
|
|
|
def validate!
|
|
return unless explicit_path && system_path
|
|
path = Bundler.settings.pretty_values_for(:path)
|
|
path.unshift(nil, "path:") unless path.empty?
|
|
system_path = Bundler.settings.pretty_values_for("path.system")
|
|
system_path.unshift(nil, "path.system:") unless system_path.empty?
|
|
disable_shared_gems = Bundler.settings.pretty_values_for(:disable_shared_gems)
|
|
disable_shared_gems.unshift(nil, "disable_shared_gems:") unless disable_shared_gems.empty?
|
|
raise InvalidOption,
|
|
"Using a custom path while using system gems is unsupported.\n#{path.join("\n")}\n#{system_path.join("\n")}\n#{disable_shared_gems.join("\n")}"
|
|
end
|
|
end
|
|
|
|
def ignore_config?
|
|
ENV["BUNDLE_IGNORE_CONFIG"]
|
|
end
|
|
|
|
def app_cache_path
|
|
@app_cache_path ||= self[:cache_path] || "vendor/cache"
|
|
end
|
|
|
|
def validate!
|
|
all.each do |raw_key|
|
|
[@local_config, @env_config, @global_config].each do |settings|
|
|
value = value_for(raw_key, settings)
|
|
Validator.validate!(raw_key, value, settings.dup)
|
|
end
|
|
end
|
|
end
|
|
|
|
def key_for(key)
|
|
@key_cache[key] ||= self.class.key_for(key)
|
|
end
|
|
|
|
private
|
|
|
|
def configs
|
|
@configs ||= {
|
|
:temporary => @temporary,
|
|
:local => @local_config,
|
|
:env => @env_config,
|
|
:global => @global_config,
|
|
:default => DEFAULT_CONFIG,
|
|
}
|
|
end
|
|
|
|
def value_for(name, config)
|
|
converted_value(config[key_for(name)], name)
|
|
end
|
|
|
|
def parent_setting_for(name)
|
|
split_specific_setting_for(name)[0]
|
|
end
|
|
|
|
def specific_gem_for(name)
|
|
split_specific_setting_for(name)[1]
|
|
end
|
|
|
|
def split_specific_setting_for(name)
|
|
name.split(".")
|
|
end
|
|
|
|
def is_bool(name)
|
|
name = self.class.key_to_s(name)
|
|
BOOL_KEYS.include?(name) || BOOL_KEYS.include?(parent_setting_for(name))
|
|
end
|
|
|
|
def is_string(name)
|
|
name = self.class.key_to_s(name)
|
|
STRING_KEYS.include?(name) || name.start_with?("local.") || name.start_with?("mirror.") || name.start_with?("build.")
|
|
end
|
|
|
|
def to_bool(value)
|
|
case value
|
|
when String
|
|
value.match?(/\A(false|f|no|n|0|)\z/i) ? false : true
|
|
when nil, false
|
|
false
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def is_num(key)
|
|
NUMBER_KEYS.include?(self.class.key_to_s(key))
|
|
end
|
|
|
|
def is_array(key)
|
|
ARRAY_KEYS.include?(self.class.key_to_s(key))
|
|
end
|
|
|
|
def is_credential(key)
|
|
key == "gem.push_key"
|
|
end
|
|
|
|
def is_userinfo(value)
|
|
value.include?(":")
|
|
end
|
|
|
|
def to_array(value)
|
|
return [] unless value
|
|
value.tr(" ", ":").split(":").map(&:to_sym)
|
|
end
|
|
|
|
def array_to_s(array)
|
|
array = Array(array)
|
|
return nil if array.empty?
|
|
array.join(":").tr(" ", ":")
|
|
end
|
|
|
|
def set_key(raw_key, value, hash, file)
|
|
raw_key = self.class.key_to_s(raw_key)
|
|
value = array_to_s(value) if is_array(raw_key)
|
|
|
|
key = key_for(raw_key)
|
|
|
|
return if hash[key] == value
|
|
|
|
hash[key] = value
|
|
hash.delete(key) if value.nil?
|
|
|
|
Validator.validate!(raw_key, converted_value(value, raw_key), hash)
|
|
|
|
return unless file
|
|
SharedHelpers.filesystem_access(file) do |p|
|
|
FileUtils.mkdir_p(p.dirname)
|
|
p.open("w") {|f| f.write(serializer_class.dump(hash)) }
|
|
end
|
|
end
|
|
|
|
def converted_value(value, key)
|
|
key = self.class.key_to_s(key)
|
|
|
|
if is_array(key)
|
|
to_array(value)
|
|
elsif value.nil?
|
|
nil
|
|
elsif is_bool(key) || value == "false"
|
|
to_bool(value)
|
|
elsif is_num(key)
|
|
value.to_i
|
|
else
|
|
value.to_s
|
|
end
|
|
end
|
|
|
|
def printable_value(value, key)
|
|
converted = converted_value(value, key)
|
|
return converted unless converted.is_a?(String)
|
|
|
|
if is_string(key)
|
|
converted
|
|
elsif is_credential(key)
|
|
"[REDACTED]"
|
|
elsif is_userinfo(converted)
|
|
username, pass = converted.split(":", 2)
|
|
|
|
if pass == "x-oauth-basic"
|
|
username = "[REDACTED]"
|
|
else
|
|
pass = "[REDACTED]"
|
|
end
|
|
|
|
[username, pass].join(":")
|
|
else
|
|
converted
|
|
end
|
|
end
|
|
|
|
def global_config_file
|
|
if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty?
|
|
Pathname.new(ENV["BUNDLE_CONFIG"])
|
|
elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty?
|
|
Pathname.new(ENV["BUNDLE_USER_CONFIG"])
|
|
elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty?
|
|
Pathname.new(ENV["BUNDLE_USER_HOME"]).join("config")
|
|
elsif Bundler.rubygems.user_home && !Bundler.rubygems.user_home.empty?
|
|
Pathname.new(Bundler.rubygems.user_home).join(".bundle/config")
|
|
end
|
|
end
|
|
|
|
def local_config_file
|
|
Pathname.new(@root).join("config") if @root
|
|
end
|
|
|
|
def load_config(config_file)
|
|
return {} if !config_file || ignore_config?
|
|
SharedHelpers.filesystem_access(config_file, :read) do |file|
|
|
valid_file = file.exist? && !file.size.zero?
|
|
return {} unless valid_file
|
|
serializer_class.load(file.read).inject({}) do |config, (k, v)|
|
|
if k.include?("-")
|
|
Bundler.ui.warn "Your #{file} config includes `#{k}`, which contains the dash character (`-`).\n" \
|
|
"This is deprecated, because configuration through `ENV` should be possible, but `ENV` keys cannot include dashes.\n" \
|
|
"Please edit #{file} and replace any dashes in configuration keys with a triple underscore (`___`)."
|
|
|
|
# string hash keys are frozen
|
|
k = k.gsub("-", "___")
|
|
end
|
|
|
|
config[k] = v
|
|
config
|
|
end
|
|
end
|
|
end
|
|
|
|
def serializer_class
|
|
require "rubygems/yaml_serializer"
|
|
Gem::YAMLSerializer
|
|
rescue LoadError
|
|
# TODO: Remove this when RubyGems 3.4 is EOL
|
|
require_relative "yaml_serializer"
|
|
YAMLSerializer
|
|
end
|
|
|
|
PER_URI_OPTIONS = %w[
|
|
fallback_timeout
|
|
].freeze
|
|
|
|
NORMALIZE_URI_OPTIONS_PATTERN =
|
|
/
|
|
\A
|
|
(\w+\.)? # optional prefix key
|
|
(https?.*?) # URI
|
|
(\.#{Regexp.union(PER_URI_OPTIONS)})? # optional suffix key
|
|
\z
|
|
/ix.freeze
|
|
|
|
def self.key_for(key)
|
|
key = normalize_uri(key).to_s if key.is_a?(String) && key.start_with?("http", "mirror.http")
|
|
key = key_to_s(key).gsub(".", "__")
|
|
key.gsub!("-", "___")
|
|
key.upcase!
|
|
|
|
key.prepend("BUNDLE_")
|
|
end
|
|
|
|
# TODO: duplicates Rubygems#normalize_uri
|
|
# TODO: is this the correct place to validate mirror URIs?
|
|
def self.normalize_uri(uri)
|
|
uri = uri.to_s
|
|
if uri =~ NORMALIZE_URI_OPTIONS_PATTERN
|
|
prefix = $1
|
|
uri = $2
|
|
suffix = $3
|
|
end
|
|
uri = URINormalizer.normalize_suffix(uri)
|
|
require_relative "vendored_uri"
|
|
uri = Bundler::URI(uri)
|
|
unless uri.absolute?
|
|
raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri)
|
|
end
|
|
"#{prefix}#{uri}#{suffix}"
|
|
end
|
|
|
|
# This is a hot method, so avoid respond_to? checks on every invocation
|
|
if :read.respond_to?(:name)
|
|
def self.key_to_s(key)
|
|
case key
|
|
when String
|
|
key
|
|
when Symbol
|
|
key.name
|
|
when Bundler::URI::HTTP
|
|
key.to_s
|
|
else
|
|
raise ArgumentError, "Invalid key: #{key.inspect}"
|
|
end
|
|
end
|
|
else
|
|
def self.key_to_s(key)
|
|
case key
|
|
when String
|
|
key
|
|
when Symbol
|
|
key.to_s
|
|
when Bundler::URI::HTTP
|
|
key.to_s
|
|
else
|
|
raise ArgumentError, "Invalid key: #{key.inspect}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|