diff options
Diffstat (limited to 'lib/bundler/settings.rb')
| -rw-r--r-- | lib/bundler/settings.rb | 541 |
1 files changed, 394 insertions, 147 deletions
diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 1898738b7c..fd77c2f7fc 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -1,128 +1,186 @@ # frozen_string_literal: true -require "uri" module Bundler class Settings - autoload :Mirror, "bundler/mirror" - autoload :Mirrors, "bundler/mirror" + 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_offline_install + BOOL_KEYS = %w[ auto_install cache_all cache_all_platforms + clean + deployment disable_checksum_validation disable_exec_load disable_local_branch_check + disable_local_revision_check disable_shared_gems disable_version_check force_ruby_platform frozen + gem.changelog gem.coc gem.mit + gem.bundle + git.allow_insecure + global_gem_cache ignore_messages - major_deprecations + init_gems_rb + inline + lockfile_checksums + no_build_extension no_install + no_install_plugin no_prune - only_update_to_newer_versions + path.system plugins + prefer_patch + silence_deprecations silence_root_warning - ).freeze + update_requires_all_flag + verbose + ].freeze - NUMBER_KEYS = %w( + NUMBER_KEYS = %w[ + cooldown + jobs redirect retry ssl_verify_mode timeout - ).freeze + ].freeze + + ARRAY_KEYS = %w[ + only + with + without + ].freeze + + STRING_KEYS = %w[ + bin + cache_path + console + default_cli_command + gem.ci + gem.github_username + gem.linter + gem.rubocop + gem.test + gemfile + lockfile + path + shebang + simulate_version + system_bindir + trust-policy + version + ].freeze DEFAULT_CONFIG = { - :redirect => 5, - :retry => 3, - :timeout => 10, + "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", + "BUNDLE_LOCKFILE_CHECKSUMS" => true, + "BUNDLE_CACHE_ALL" => true, + "BUNDLE_PLUGINS" => true, + "BUNDLE_GLOBAL_GEM_CACHE" => false, + "BUNDLE_UPDATE_REQUIRES_ALL_FLAG" => false, }.freeze - attr_accessor :cli_flags_given - def initialize(root = nil) @root = root @local_config = load_config(local_config_file) + @local_root = root || Pathname.new(".bundle").expand_path + + @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) - @cli_flags_given = false @temporary = {} + + @key_cache = {} end def [](name) key = key_for(name) - value = @temporary.fetch(name) do - @local_config.fetch(key) do - ENV.fetch(key) do - @global_config.fetch(key) do - DEFAULT_CONFIG.fetch(name) do - nil - end end end end end + + value = nil + configs.each do |_, config| + value = config[key] + next if value.nil? + break + end converted_value(value, name) end - def []=(key, value) - if cli_flags_given - command = if value.nil? - "bundle config --delete #{key}" - else - "bundle config #{key} #{Array(value).join(":")}" - end + def set_command_option(key, value) + temporary(key => value) + value + 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 = @local_root.join("config") - Bundler::SharedHelpers.major_deprecation \ - "flags passed to commands " \ - "will no longer be automatically remembered. Instead please set flags " \ - "you want remembered between commands using `bundle config " \ - "<setting name> <setting value>`, i.e. `#{command}`" - end - local_config_file || raise(GemfileNotFound, "Could not locate Gemfile") set_key(key, value, @local_config, local_config_file) end - alias_method :set_local, :[]= def temporary(update) - existing = Hash[update.map {|k, _| [k, @temporary[k]] }] - @temporary.update(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| v.nil? ? @temporary.delete(k) : @temporary[k] = v } + existing.each {|k, v| set_key(k, v, @temporary, nil) } end end - def delete(key) - @local_config.delete(key_for(key)) - end - def set_global(key, value) set_key(key, value, @global_config, global_config_file) end def all - env_keys = ENV.keys.select {|k| k =~ /BUNDLE_.*/ } + keys = @temporary.keys.union(@global_config.keys, @local_config.keys, @env_config.keys) - keys = @global_config.keys | @local_config.keys | env_keys - - keys.map do |key| - key.sub(/^BUNDLE_/, "").gsub(/__/, ".").downcase - end + 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[$'] = self[k] if k =~ /^local\./ + repos[k.delete_prefix("local.")] = self[k] if k.start_with?("local.") end repos end def mirror_for(uri) - uri = URI(uri.to_s) unless uri.is_a?(URI) + if uri.is_a?(String) + require_relative "vendored_uri" + uri = Gem::URI(uri) + end + gem_mirrors.for(uri.to_s).uri end @@ -132,72 +190,113 @@ module Bundler def gem_mirrors all.inject(Mirrors.new) do |mirrors, k| - mirrors.parse(k, self[k]) if k =~ /^mirror\./ + mirrors.parse(k, self[k]) if k.start_with?("mirror.") mirrors end end def locations(key) key = key_for(key) - locations = {} - locations[:local] = @local_config[key] if @local_config.key?(key) - locations[:env] = ENV[key] if ENV[key] - locations[:global] = @global_config[key] if @global_config.key?(key) - locations[:default] = DEFAULT_CONFIG[key] if DEFAULT_CONFIG.key?(key) - locations + 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 @local_config.key?(key) - locations << "Set for your local app (#{local_config_file}): #{converted_value(@local_config[key], exposed_key).inspect}" + + 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[key] - locations << "Set via #{key}: #{converted_value(value, exposed_key).inspect}" + if value = @env_config[key] + locations << "Set via #{key}: #{printable_value(value, exposed_key).inspect}" end - if @global_config.key?(key) - locations << "Set for the current user (#{global_config_file}): #{converted_value(@global_config[key], exposed_key).inspect}" + 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 without=(array) - set_array(:without, array) + def processor_count + require "etc" + Etc.nprocessors + rescue StandardError + 1 end - def with=(array) - set_array(:with, array) - 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 - def without - get_array(:without) - end + path = "vendor/bundle" if self[:deployment] - def with - get_array(:with) + Path.new(path, false) end - # @local_config["BUNDLE_PATH"] should be prioritized over ENV["BUNDLE_PATH"] - def path - key = key_for(:path) - path = ENV[key] || @global_config[key] - return path if path && !@local_config.key?(key) + 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 - if path = self[:path] - "#{path}/#{Bundler.ruby_scope}" - else - Bundler.rubygems.gem_dir + def use_system_gems? + return true if system_path + return false if explicit_path + !Bundler.feature_flag.bundler_5_mode? + 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 - end - def allow_sudo? - !@local_config.key?(key_for(:path)) + 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? @@ -205,95 +304,174 @@ module Bundler end def app_cache_path - @app_cache_path ||= begin - path = self[:cache_path] || "vendor/cache" - raise InvalidOption, "Cache path must be relative to the bundle path" if path.start_with?("/") - path - end + @app_cache_path ||= self[:cache_path] || "vendor/cache" end - private + def installation_parallelization + self[:jobs] || processor_count + 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 = Settings.normalize_uri(key).to_s if key.is_a?(String) && /https?:/ =~ key - key = key.to_s.gsub(".", "__").upcase - "BUNDLE_#{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_specfic_setting_for(name)[0] + split_specific_setting_for(name)[0] end - def specfic_gem_for(name) - split_specfic_setting_for(name)[1] + def specific_gem_for(name) + split_specific_setting_for(name)[1] end - def split_specfic_setting_for(name) + def split_specific_setting_for(name) name.split(".") end def is_bool(name) - BOOL_KEYS.include?(name.to_s) || BOOL_KEYS.include?(parent_setting_for(name.to_s)) + 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 nil, /\A(false|f|no|n|0|)\z/i, false + 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(value) - NUMBER_KEYS.include?(value.to_s) + def is_num(key) + NUMBER_KEYS.include?(self.class.key_to_s(key)) end - def get_array(key) - self[key] ? self[key].split(":").map(&:to_sym) : [] + def is_array(key) + ARRAY_KEYS.include?(self.class.key_to_s(key)) end - def set_array(key, array) - self[key] = (array.empty? ? nil : array.join(":")) if array + def is_credential(key) + key == "gem.push_key" end - def set_key(key, value, hash, file) - key = key_for(key) + def is_userinfo(value) + value.include?(":") + end - unless hash[key] == value - hash[key] = value - hash.delete(key) if value.nil? - SharedHelpers.filesystem_access(file) do |p| - FileUtils.mkdir_p(p.dirname) - require "bundler/yaml_serializer" - p.open("w") {|f| f.write(YAMLSerializer.dump(hash)) } - 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.dirname, :create) do |p| + FileUtils.mkdir_p(p) end - value + SharedHelpers.filesystem_access(file) do |p| + p.open("w") {|f| f.write(serializer_class.dump(hash)) } + end end def converted_value(value, key) - if value.nil? + 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 + 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"]) - else - begin - Bundler.user_bundle_path.join("config") - rescue PermissionError, GenericSystemCallError - nil - end + 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 @@ -301,40 +479,109 @@ module Bundler Pathname.new(@root).join("config") if @root end - CONFIG_REGEX = %r{ # rubocop:disable Style/RegexpLiteral - ^ - (BUNDLE_.+):\s # the key - (?: !\s)? # optional exclamation mark found with ruby 1.9.3 - (['"]?) # optional opening quote - (.* # contents of the value - (?: # optionally, up until the next key - (\n(?!BUNDLE).+)* - ) - ) - \2 # matching closing quote - $ - }xo - 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 - require "bundler/yaml_serializer" - YAMLSerializer.load file.read + (serializer_class.load(file.read) || {}).inject({}) do |config, (k, v)| + k = k.dup + k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}") + k.gsub!(".", "__") + + unless k.start_with?("#") + 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 + end + + 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 + + FALLBACK_TIMEOUT_URI_OPTION = "fallback_timeout" + + NORMALIZE_URI_OPTIONS_PATTERN = + / + \A + (\w+\.)? # optional prefix key + (https?.*?) # URI + (\.#{FALLBACK_TIMEOUT_URI_OPTION})? # optional suffix key + \z + /ix + + def self.key_for(key) + key = key_to_s(key) + key = normalize_uri(key) if key.start_with?("http", "mirror.http") + key = key.gsub(".", "__") + key.gsub!("-", "___") + key.upcase! + + key.gsub(/\A([ #]*)/, '\1BUNDLE_') + 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 - uri = "#{uri}/" unless uri =~ %r{/\Z} - uri = URI(uri) + if uri =~ NORMALIZE_URI_OPTIONS_PATTERN + prefix = $1 + uri = $2 + suffix = $3 + end + uri = URINormalizer.normalize_suffix(uri) + require_relative "vendored_uri" + uri = Gem::URI(uri) unless uri.absolute? raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri) end - uri + "#{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 Gem::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 Gem::URI::HTTP + key.to_s + else + raise ArgumentError, "Invalid key: #{key.inspect}" + end + end end end end |
