diff options
Diffstat (limited to 'lib/rubygems')
152 files changed, 5575 insertions, 4656 deletions
diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index a09e8ed0e1..0ed7fc60bb 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -34,15 +34,6 @@ class Gem::BasicSpecification internal_init end - def self.default_specifications_dir - Gem.default_specifications_dir - end - - class << self - extend Gem::Deprecate - rubygems_deprecate :default_specifications_dir, "Gem.default_specifications_dir" - end - ## # The path to the gem.build_complete file within the extension install # directory. @@ -199,6 +190,9 @@ class Gem::BasicSpecification File.expand_path(File.join(gems_dir, full_name, "data", name)) end + extend Gem::Deprecate + rubygems_deprecate :datadir, :none, "4.1" + ## # Full path of the target library file. # If the file is not in this gem, return nil. @@ -256,6 +250,13 @@ class Gem::BasicSpecification raise NotImplementedError end + def installable_on_platform?(target_platform) # :nodoc: + return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform) + return true if Gem::Platform.new(platform) === target_platform + + false + end + def raw_require_paths # :nodoc: raise NotImplementedError end diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index 4ebbad1c0c..bbe7bf0ab5 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -2,11 +2,17 @@ module Gem::BundlerVersionFinder def self.bundler_version + bcv = bundle_config_version + return if bcv == "system" + v = ENV["BUNDLER_VERSION"] + v = nil if v&.empty? v ||= bundle_update_bundler_version return if v == true + v ||= bcv unless bcv == "lockfile" + v ||= lockfile_version return unless v @@ -46,6 +52,67 @@ module Gem::BundlerVersionFinder private_class_method :lockfile_version def self.lockfile_contents + gemfile = gemfile_path + + return unless gemfile + + lockfile = ENV["BUNDLE_LOCKFILE"] + lockfile = nil if lockfile&.empty? + + lockfile ||= case gemfile + when "gems.rb" then "gems.locked" + else "#{gemfile}.lock" + end + + return unless File.file?(lockfile) + + File.read(lockfile) + end + private_class_method :lockfile_contents + + def self.bundle_config_version + env_version = ENV["BUNDLE_VERSION"] + return env_version if env_version && !env_version.empty? + + version = nil + + [bundler_local_config_file, bundler_global_config_file].each do |config_file| + next unless config_file && File.file?(config_file) + + contents = File.read(config_file) + contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/ + + version = $1 + break if version + end + + version + end + private_class_method :bundle_config_version + + def self.bundler_global_config_file + # see Bundler::Settings#global_config_file + if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? + ENV["BUNDLE_CONFIG"] + elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty? + ENV["BUNDLE_USER_CONFIG"] + elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty? + ENV["BUNDLE_USER_HOME"] + "config" + elsif Gem.user_home && !Gem.user_home.empty? + Gem.user_home + ".bundle/config" + end + end + private_class_method :bundler_global_config_file + + def self.bundler_local_config_file + gemfile = gemfile_path + return unless gemfile + + File.join(File.dirname(gemfile), ".bundle", "config") + end + private_class_method :bundler_local_config_file + + def self.gemfile_path gemfile = ENV["BUNDLE_GEMFILE"] gemfile = nil if gemfile&.empty? @@ -62,16 +129,7 @@ module Gem::BundlerVersionFinder end end - return unless gemfile - - lockfile = case gemfile - when "gems.rb" then "gems.locked" - else "#{gemfile}.lock" - end - - return unless File.file?(lockfile) - - File.read(lockfile) + gemfile end - private_class_method :lockfile_contents + private_class_method :gemfile_path end diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb index 3a149ea38e..d38363f293 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -117,7 +117,7 @@ class Gem::Command # Unhandled arguments (gem names, files, etc.) are left in # <tt>options[:args]</tt>. - def initialize(command, summary=nil, defaults={}) + def initialize(command, summary = nil, defaults = {}) @command = command @summary = summary @program_name = "gem #{command}" diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 15834ce4dd..76b2fba835 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -58,7 +58,6 @@ class Gem::CommandManager :owner, :pristine, :push, - :query, :rdoc, :rebuild, :search, @@ -118,7 +117,7 @@ class Gem::CommandManager ## # Register the Symbol +command+ as a gem command. - def register_command(command, obj=false) + def register_command(command, obj = false) @commands[command] = obj end @@ -148,7 +147,7 @@ class Gem::CommandManager ## # Run the command specified by +args+. - def run(args, build_args=nil) + def run(args, build_args = nil) process_args(args, build_args) rescue StandardError, Gem::Timeout::Error => ex if ex.respond_to?(:detailed_message) @@ -165,7 +164,7 @@ class Gem::CommandManager terminate_interaction(1) end - def process_args(args, build_args=nil) + def process_args(args, build_args = nil) if args.empty? say Gem::Command::HELP terminate_interaction 1 diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 2ec8324141..cfe1f8ec3c 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -25,13 +25,6 @@ class Gem::Commands::BuildCommand < Gem::Command add_option "-o", "--output FILE", "output gem with the given filename" do |value, options| options[:output] = value end - - add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options| - options[:build_path] = value - end - deprecate_option "-C", - version: "4.0", - extra_msg: "-C is a global flag now. Use `gem -C PATH build GEMSPEC_FILE [options]` instead" end def arguments # :nodoc: diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index 72dcf1dd17..fe03841ddb 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -158,7 +158,7 @@ class Gem::Commands::CertCommand < Gem::Command cert = Gem::Security.create_cert_email( email, key, - (Gem::Security::ONE_DAY * expiration_length_days) + Gem::Security::ONE_DAY * expiration_length_days ) Gem::Security.write cert, "gem-public_cert.pem" diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index aea8c0d7de..a5eb521a53 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -38,6 +38,7 @@ keys: :verbose: Verbosity of the gem command. false, true, and :really are the levels :update_sources: Enable/disable automatic updating of repository metadata + :concurrent_downloads: The number of gem downloads to perform concurrently :backtrace: Print backtrace when RubyGems encounters an error :gempath: The paths in which to look for gems :disable_default_gem_server: Force specification of gem server host on push diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb index 519539c694..1feafbdd35 100644 --- a/lib/rubygems/commands/exec_command.rb +++ b/lib/rubygems/commands/exec_command.rb @@ -173,6 +173,9 @@ to the same gem path as user-installed gems. rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" terminate_interaction 1 + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + terminate_interaction 2 rescue Gem::GemNotFoundException => e show_lookup_failure e.name, e.version, e.errors, false @@ -195,7 +198,7 @@ to the same gem path as user-installed gems. argv = ARGV.clone ARGV.replace options[:args] - exe = executable = options[:executable] + executable = options[:executable] contains_executable = Gem.loaded_specs.values.select do |spec| spec.executables.include?(executable) @@ -206,13 +209,22 @@ to the same gem path as user-installed gems. end if contains_executable.empty? - if (spec = Gem.loaded_specs[executable]) && (exe = spec.executable) - contains_executable << spec - else + spec = Gem.loaded_specs[executable] + + if spec.nil? || spec.executables.empty? alert_error "Failed to load executable `#{executable}`," \ " are you sure the gem `#{options[:gem_name]}` contains it?" terminate_interaction 1 end + + if spec.executables.size > 1 + alert_error "Ambiguous which executable from gem `#{executable}` should be run: " \ + "the options are #{spec.executables.sort}, specify one via COMMAND, and use `-g` and `-v` to specify gem and version" + terminate_interaction 1 + end + + contains_executable << spec + executable = spec.executable end if contains_executable.size > 1 @@ -223,8 +235,8 @@ to the same gem path as user-installed gems. end old_exe = $0 - $0 = exe - load Gem.activate_bin_path(contains_executable.first.name, exe, ">= 0.a") + $0 = executable + load Gem.activate_bin_path(contains_executable.first.name, executable, ">= 0.a") ensure $0 = old_exe if old_exe ARGV.replace argv diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index c524cf7922..8e64a18cee 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -56,7 +56,7 @@ then repackaging it. if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index 1619b152e7..664f400561 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -90,7 +90,9 @@ Use #gem to declare which gems you directly depend upon: To depend on a specific set of versions: - gem 'rake', '~> 10.3', '>= 10.3.2' + gem 'rake', '>= 10.3.2' + # or for multiple version restrictions + gem 'rake', '>= 10.3.2', "< 13" RubyGems will require the gem name when activating the gem using the RUBYGEMS_GEMDEPS environment variable or Gem::use_gemdeps. Use the diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 2888b6c55a..6d3beec0b4 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -140,7 +140,7 @@ You can use `i` command instead of `install`. if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end @@ -224,6 +224,9 @@ You can use `i` command instead of `install`. rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + exit_code |= 2 rescue Gem::UnsatisfiableDependencyError => e show_lookup_failure e.name, e.version, e.errors, suppress_suggestions, "'#{gem_name}' (#{gem_version})" @@ -239,11 +242,7 @@ You can use `i` command instead of `install`. # Loads post-install hooks def load_hooks # :nodoc: - if options[:install_as_default] - require_relative "../install_default_message" - else - require_relative "../install_message" - end + require_relative "../install_message" require_relative "../rdoc" end diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 12bfe3a834..675e866734 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -75,11 +75,12 @@ permission to. end with_response response do |resp| - owners = Gem::SafeYAML.load clean_text(resp.body) + owners = Gem::SafeYAML.safe_load clean_text(resp.body) say "Owners for gem: #{name}" owners.each do |owner| - say "- #{owner["email"] || owner["handle"] || owner["id"]}" + identifier = owner["email"] || owner["handle"] || owner["id"] + say "- #{identifier} (#{owner["role"]})" end end end diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 97f1646ba0..10978c2af7 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -88,6 +88,10 @@ If you have made modifications to an installed gem, the pristine command will revert them. All extensions are rebuilt and all bin stubs for the gem are regenerated after checking for modifications. +Rebuilding extensions also refreshes C-extension gems against updated system +libraries (for example after OS or package upgrades) to avoid mismatches like +outdated library version warnings. + If the cached gem cannot be found it will be downloaded. If --no-extensions is provided pristine will not attempt to restore a gem @@ -128,6 +132,11 @@ extensions will be restored. specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } if specs.to_a.empty? + if options[:only_missing_extensions] + say "No gems with missing extensions to restore" + return + end + raise Gem::Exception, "Failed to find gems #{options[:args]} #{options[:version]}" end @@ -137,11 +146,14 @@ extensions will be restored. specs.group_by(&:full_name_with_location).values.each do |grouped_specs| spec = grouped_specs.find {|s| !s.default_gem? } || grouped_specs.first - unless only_executables_or_plugins? + only_executables = options[:only_executables] + only_plugins = options[:only_plugins] + + unless only_executables || only_plugins # Default gemspecs include changes provided by ruby-core installer that # can't currently be pristined (inclusion of compiled extension targets in # the file list). So stick to resetting executables if it's a default gem. - options[:only_executables] = true if spec.default_gem? + only_executables = true if spec.default_gem? end if options.key? :skip @@ -151,14 +163,14 @@ extensions will be restored. end end - unless spec.extensions.empty? || options[:extensions] || only_executables_or_plugins? + unless spec.extensions.empty? || options[:extensions] || only_executables || only_plugins say "Skipped #{spec.full_name_with_location}, it needs to compile an extension" next end gem = spec.cache_file - unless File.exist?(gem) || only_executables_or_plugins? + unless File.exist?(gem) || only_executables || only_plugins require_relative "../remote_fetcher" say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..." @@ -194,10 +206,10 @@ extensions will be restored. bin_dir: bin_dir, } - if options[:only_executables] + if only_executables installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_bin - elsif options[:only_plugins] + elsif only_plugins installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_plugins else @@ -208,10 +220,4 @@ extensions will be restored. say "Restored #{spec.full_name_with_location}" end end - - private - - def only_executables_or_plugins? - options[:only_executables] || options[:only_plugins] - end end diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 726191377a..02931b3025 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -92,20 +92,77 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo private def send_push_request(name, args) - rubygems_api_request(*args, scope: get_push_scope) do |request| + # Always honor explicit --attestation option + # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby) + if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"]) + send_push_request_with_attestation(name, args) + else + send_push_request_without_attestation(name, args) + end + end + + def send_push_request_without_attestation(name, args) + scope = get_push_scope + rubygems_api_request(*args, scope: scope) do |request| body = Gem.read_binary name - if options[:attestations].any? - request.set_form([ - ["gem", body, { filename: name, content_type: "application/octet-stream" }], - get_attestations_part, - ], "multipart/form-data") - else - request.body = body - request.add_field "Content-Type", "application/octet-stream" - request.add_field "Content-Length", request.body.size + request.body = body + request.add_field "Content-Type", "application/octet-stream" + request.add_field "Content-Length", request.body.size + request.add_field "Authorization", api_key + end + end + + def send_push_request_with_attestation(name, args) + attestations = if options[:attestations].any? + options[:attestations].map do |attestation| + Gem.read_binary(attestation) end + else + bundle_path = attest!(name) + begin + [Gem.read_binary(bundle_path)] + ensure + File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path) + end + end + bundles = "[" + attestations.join(",") + "]" + + rubygems_api_request(*args, scope: get_push_scope) do |request| + request.set_form([ + ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }], + ["attestations", bundles, { content_type: "application/json" }], + ], "multipart/form-data") request.add_field "Authorization", api_key end + rescue StandardError => e + message = "Failed to push with attestation, retrying without attestation.\n" + message += if Gem.configuration.really_verbose + e.full_message + else + e.message + end + alert_warning message + send_push_request_without_attestation(name, args) + end + + def attest!(name) + require "open3" + require "tempfile" + + tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"]) + bundle = tempfile.path + tempfile.close(false) + + env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h + out, st = Open3.capture2e( + env, + Gem.ruby, "-S", "gem", "exec", "--conservative", + "sigstore-cli", "sign", name, "--bundle", bundle, + unsetenv_others: true + ) + raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? + + bundle end def get_hosts_for(name) @@ -121,14 +178,8 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo :push_rubygem end - def get_attestations_part - bundles = "[" + options[:attestations].map do |attestation| - Gem.read_binary(attestation) - end.join(",") + "]" - [ - "attestations", - bundles, - { content_type: "application/json" }, - ] + def attestation_supported_host? + host = (@host || Gem.host).to_s.chomp("/") + host == Gem::DEFAULT_HOST end end diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb deleted file mode 100644 index 3b527974a3..0000000000 --- a/lib/rubygems/commands/query_command.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require_relative "../command" -require_relative "../query_utils" -require_relative "../deprecate" - -class Gem::Commands::QueryCommand < Gem::Command - extend Gem::Deprecate - rubygems_deprecate_command - - include Gem::QueryUtils - - alias_method :warning_without_suggested_alternatives, :deprecation_warning - def deprecation_warning - warning_without_suggested_alternatives - - message = "It is recommended that you use `gem search` or `gem list` instead.\n" - alert_warning message unless Gem::Deprecate.skip - end - - def initialize(name = "query", summary = "Query gem information in local or remote repositories") - super name, summary, - domain: :local, details: false, versions: true, - installed: nil, version: Gem::Requirement.default - - add_option("-n", "--name-matches REGEXP", - "Name of gem(s) to query on matches the", - "provided REGEXP") do |value, options| - options[:name] = /#{value}/i - end - - add_query_options - end - - def description # :nodoc: - <<-EOF -The query command is the basis for the list and search commands. - -You should really use the list and search commands instead. This command -is too hard to use. - EOF - end -end diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb index 77a474ef1d..23b9d7b3ba 100644 --- a/lib/rubygems/commands/rebuild_command.rb +++ b/lib/rubygems/commands/rebuild_command.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "date" require "digest" require "fileutils" require "tmpdir" diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 301ce25314..175599967c 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -7,8 +7,8 @@ require_relative "../command" # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command - HISTORY_HEADER = %r{^#\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$} - VERSION_MATCHER = %r{^#\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$} + HISTORY_HEADER = %r{^##\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$} + VERSION_MATCHER = %r{^##\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$} ENV_PATHS = %w[/usr/bin/env /bin/env].freeze @@ -393,16 +393,20 @@ By default, this RubyGems will install gem as: Dir.chdir("bundler") do built_gem = Gem::Package.build(new_bundler_spec) begin - Gem::Installer.at( + installer = Gem::Installer.at( built_gem, env_shebang: options[:env_shebang], format_executable: options[:format_executable], force: options[:force], - install_as_default: true, bin_dir: bin_dir, install_dir: default_dir, wrappers: true - ).install + ) + # We need to install only executable and default spec files. + # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory. + installer.extract_bin + installer.generate_bin + installer.write_default_spec ensure FileUtils.rm_f built_gem end diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index 976f4a4ea2..b399af2bd3 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -18,6 +18,14 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:add] = value end + add_option "--append SOURCE_URI", "Append source (can be used multiple times)" do |value, options| + options[:append] = value + end + + add_option "-p", "--prepend SOURCE_URI", "Prepend source (can be used multiple times)" do |value, options| + options[:prepend] = value + end + add_option "-l", "--list", "List sources" do |value, options| options[:list] = value end @@ -26,8 +34,7 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:remove] = value end - add_option "-c", "--clear-all", - "Remove all sources (clear the cache)" do |value, options| + add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options| options[:clear_all] = value end @@ -43,11 +50,8 @@ class Gem::Commands::SourcesCommand < Gem::Command end def add_source(source_uri) # :nodoc: - check_rubygems_https source_uri - - source = Gem::Source.new source_uri - - check_typo_squatting(source) + source = build_new_source(source_uri) + source_uri = source.uri.to_s begin if Gem.sources.include? source @@ -68,6 +72,54 @@ class Gem::Commands::SourcesCommand < Gem::Command end end + def append_source(source_uri) # :nodoc: + source = build_new_source(source_uri) + source_uri = source.uri.to_s + + begin + source.load_specs :released + was_present = Gem.sources.include?(source) + Gem.sources.append source + Gem.configuration.write + + if was_present + say "#{source_uri} moved to end of sources" + else + say "#{source_uri} added to sources" + end + rescue Gem::URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" + terminate_interaction 1 + end + end + + def prepend_source(source_uri) # :nodoc: + source = build_new_source(source_uri) + source_uri = source.uri.to_s + + begin + source.load_specs :released + was_present = Gem.sources.include?(source) + Gem.sources.prepend source + Gem.configuration.write + + if was_present + say "#{source_uri} moved to top of sources" + else + say "#{source_uri} added to sources" + end + rescue Gem::URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" + terminate_interaction 1 + end + end + def check_typo_squatting(source) if source.typo_squatting?("rubygems.org") question = <<-QUESTION.chomp @@ -80,6 +132,19 @@ Do you want to add this source? end end + def normalize_source_uri(source_uri) # :nodoc: + # Ensure the source URI has a trailing slash for proper RFC 2396 path merging + # Without a trailing slash, the last path segment is treated as a file and removed + # during relative path resolution (e.g., "/blish" + "gems/foo.gem" = "/gems/foo.gem") + # With a trailing slash, it's treated as a directory (e.g., "/blish/" + "gems/foo.gem" = "/blish/gems/foo.gem") + uri = Gem::URI.parse(source_uri) + uri.path = uri.path.gsub(%r{/+\z}, "") + "/" if uri.path && !uri.path.empty? + uri.to_s + rescue Gem::URI::Error + # If parsing fails, return the original URI and let later validation handle it + source_uri + end + def check_rubygems_https(source_uri) # :nodoc: uri = Gem::URI source_uri @@ -128,7 +193,7 @@ yourself to use your own gem server. Without any arguments the sources lists your currently configured sources: $ gem sources - *** CURRENT SOURCES *** + *** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW *** https://rubygems.org @@ -147,33 +212,49 @@ Since all of these sources point to the same set of gems you only need one of them in your list. https://rubygems.org is recommended as it brings the protections of an SSL connection to gem downloads. -To add a source use the --add argument: +To add a private gem source use the --prepend argument to insert it before +the default source. This is usually the best place for private gem sources: - $ gem sources --add https://rubygems.org - https://rubygems.org added to sources + $ gem sources --prepend https://my.private.source + https://my.private.source added to sources RubyGems will check to see if gems can be installed from the source given before it is added. +To add or move a source after all other sources, use --append: + + $ gem sources --append https://rubygems.org + https://rubygems.org moved to end of sources + To remove a source use the --remove argument: - $ gem sources --remove https://rubygems.org/ - https://rubygems.org/ removed from sources + $ gem sources --remove https://my.private.source/ + https://my.private.source/ removed from sources EOF end def list # :nodoc: - say "*** CURRENT SOURCES ***" + if configured_sources + header = "*** CURRENT SOURCES ***" + list = configured_sources + else + header = "*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***" + list = Gem.sources + end + + say header say - Gem.sources.each do |src| + list.each do |src| say src end end def list? # :nodoc: !(options[:add] || + options[:prepend] || + options[:append] || options[:clear_all] || options[:remove] || options[:update]) @@ -182,11 +263,13 @@ To remove a source use the --remove argument: def execute clear_all if options[:clear_all] - source_uri = options[:add] - add_source source_uri if source_uri + add_source options[:add] if options[:add] + + prepend_source options[:prepend] if options[:prepend] + + append_source options[:append] if options[:append] - source_uri = options[:remove] - remove_source source_uri if source_uri + remove_source options[:remove] if options[:remove] update if options[:update] @@ -194,13 +277,22 @@ To remove a source use the --remove argument: end def remove_source(source_uri) # :nodoc: - if Gem.sources.include? source_uri - Gem.sources.delete source_uri + source = build_source(source_uri) + source_uri = source.uri.to_s + + if configured_sources&.include? source + Gem.sources.delete source Gem.configuration.write - say "#{source_uri} removed from sources" + if default_sources.include?(source) && configured_sources.one? + alert_warning "Removing a default source when it is the only source has no effect. Add a different source to #{config_file_name} if you want to stop using it as a source." + else + say "#{source_uri} removed from sources" + end + elsif configured_sources + say "source #{source_uri} cannot be removed because it's not present in #{config_file_name}" else - say "source #{source_uri} not present in cache" + say "source #{source_uri} cannot be removed because there are no configured sources in #{config_file_name}" end end @@ -224,4 +316,33 @@ To remove a source use the --remove argument: say "*** Unable to remove #{desc} source cache ***" end end + + private + + def default_sources + Gem::SourceList.from(Gem.default_sources) + end + + def configured_sources + return @configured_sources if defined?(@configured_sources) + + configuration_sources = Gem.configuration.sources + @configured_sources = Gem::SourceList.from(configuration_sources) if configuration_sources + end + + def config_file_name + Gem.configuration.config_file_name + end + + def build_source(source_uri) + source_uri = normalize_source_uri(source_uri) + Gem::Source.new(source_uri) + end + + def build_new_source(source_uri) + source = build_source(source_uri) + check_rubygems_https(source.uri.to_s) + check_typo_squatting(source) + source + end end diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index a21ed35be3..15e543f1a6 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -42,7 +42,7 @@ class Gem::Commands::SpecificationCommand < Gem::Command def arguments # :nodoc: <<-ARGS -GEMFILE name of gem to show the gemspec for +GEM_OR_FILE gem name or a .gem file to show the gemspec for FIELD name of gemspec field to show ARGS end @@ -68,7 +68,7 @@ Specific fields in the specification can be extracted in YAML format: end def usage # :nodoc: - "#{program_name} [GEMFILE] [FIELD]" + "#{program_name} [GEM_OR_FILE] [FIELD]" end def execute @@ -77,7 +77,7 @@ Specific fields in the specification can be extracted in YAML format: unless gem raise Gem::CommandLineError, - "Please specify a gem name or file on the command line" + "Please specify a gem name or a .gem file on the command line" end case v = options[:version] @@ -147,7 +147,7 @@ Specific fields in the specification can be extracted in YAML format: say case options[:format] when :ruby then s.to_ruby when :marshal then Marshal.dump s - else s.to_yaml + else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s) end say "\n" diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 3d6e41e49e..3c26074f93 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -117,7 +117,7 @@ that is a dependency of an existing gem. You can use the if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end diff --git a/lib/rubygems/compatibility.rb b/lib/rubygems/compatibility.rb deleted file mode 100644 index 0d9df56f8a..0000000000 --- a/lib/rubygems/compatibility.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -#-- -# This file contains all sorts of little compatibility hacks that we've -# had to introduce over the years. Quarantining them into one file helps -# us know when we can get rid of them. -# -# Ruby 1.9.x has introduced some things that are awkward, and we need to -# support them, so we define some constants to use later. -# -# TODO remove at RubyGems 4 -#++ - -module Gem - # :stopdoc: - - RubyGemsVersion = VERSION - deprecate_constant(:RubyGemsVersion) - - RbConfigPriorities = %w[ - MAJOR - MINOR - TEENY - EXEEXT RUBY_SO_NAME arch bindir datadir libdir ruby_install_name - ruby_version rubylibprefix sitedir sitelibdir vendordir vendorlibdir - rubylibdir - ].freeze - - if defined?(ConfigMap) - RbConfigPriorities.each do |key| - ConfigMap[key.to_sym] = RbConfig::CONFIG[key] - end - else - ## - # Configuration settings from ::RbConfig - ConfigMap = Hash.new do |cm, key| - cm[key] = RbConfig::CONFIG[key.to_s] - end - deprecate_constant(:ConfigMap) - end -end diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index a2bcb6dfbc..d5e9eb4e33 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -26,9 +26,22 @@ require "rbconfig" # RubyGems options use symbol keys. Valid options are: # # +:backtrace+:: See #backtrace -# +:sources+:: Sets Gem::sources +# +:bulk_threshold+:: See #bulk_threshold # +:verbose+:: See #verbose +# +:update_sources+:: See #update_sources # +:concurrent_downloads+:: See #concurrent_downloads +# +:cert_expiration_length_days+:: See #cert_expiration_length_days +# +:install_extension_in_lib+:: See #install_extension_in_lib +# +:ipv4_fallback_enabled+:: See #ipv4_fallback_enabled +# +:global_gem_cache+:: See #global_gem_cache +# +:use_psych+:: See #use_psych +# +:gemhome+:: See #home +# +:gempath+:: See #path +# +:sources+:: Sets Gem::sources +# +:disable_default_gem_server+:: See #disable_default_gem_server +# +:ssl_verify_mode+:: See #ssl_verify_mode +# +:ssl_ca_cert+:: See #ssl_ca_cert +# +:ssl_client_cert+:: See #ssl_client_cert # # gemrc files may exist in various locations and are read and merged in # the following order: @@ -47,8 +60,9 @@ class Gem::ConfigFile DEFAULT_CONCURRENT_DOWNLOADS = 8 DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365 DEFAULT_IPV4_FALLBACK_ENABLED = false - # TODO: Use false as default value for this option in RubyGems 4.0 DEFAULT_INSTALL_EXTENSION_IN_LIB = true + DEFAULT_GLOBAL_GEM_CACHE = false + DEFAULT_USE_PSYCH = false ## # For Ruby packagers to set configuration defaults. Set in @@ -156,6 +170,17 @@ class Gem::ConfigFile attr_accessor :ipv4_fallback_enabled ## + # Use a global cache for .gem files shared across all Ruby installations. + # When enabled, gems are cached to ~/.cache/gem/gems (or XDG_CACHE_HOME/gem/gems). + + attr_accessor :global_gem_cache + + ## + # Use Psych (C extension YAML parser) instead of the pure Ruby YAMLSerializer. + + attr_accessor :use_psych + + ## # Path name of directory or file of openssl client certificate, used for remote https connection with client authentication attr_reader :ssl_client_cert @@ -192,6 +217,8 @@ class Gem::ConfigFile @cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS @install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB @ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED + @global_gem_cache = ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] == "true" || DEFAULT_GLOBAL_GEM_CACHE + @use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || DEFAULT_USE_PSYCH operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS) platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS) @@ -213,8 +240,9 @@ class Gem::ConfigFile @hash.transform_keys! do |k| # gemhome and gempath are not working with symbol keys if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days - install_extension_in_lib ipv4_fallback_enabled sources disable_default_gem_server - ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k) + concurrent_downloads install_extension_in_lib ipv4_fallback_enabled + global_gem_cache use_psych sources + disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k) k.to_sym else k @@ -226,10 +254,12 @@ class Gem::ConfigFile @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold @verbose = @hash[:verbose] if @hash.key? :verbose @update_sources = @hash[:update_sources] if @hash.key? :update_sources - # TODO: We should handle concurrent_downloads same as other options + @concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads @cert_expiration_length_days = @hash[:cert_expiration_length_days] if @hash.key? :cert_expiration_length_days @install_extension_in_lib = @hash[:install_extension_in_lib] if @hash.key? :install_extension_in_lib @ipv4_fallback_enabled = @hash[:ipv4_fallback_enabled] if @hash.key? :ipv4_fallback_enabled + @global_gem_cache = @hash[:global_gem_cache] if @hash.key? :global_gem_cache + @use_psych = @hash[:use_psych] if @hash.key? :use_psych @home = @hash[:gemhome] if @hash.key? :gemhome @path = @hash[:gempath] if @hash.key? :gempath @@ -345,7 +375,7 @@ if you believe they were disclosed to a third party. require "fileutils" FileUtils.mkdir_p(dirname) - permissions = 0o600 & (~File.umask) + permissions = 0o600 & ~File.umask File.open(credentials_path, "w", permissions) do |f| f.write self.class.dump_with_rubygems_yaml(config) end @@ -369,7 +399,9 @@ if you believe they were disclosed to a third party. begin config = self.class.load_with_rubygems_config_hash(File.read(filename)) - if config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") } + has_invalid_keys = config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") } + has_invalid_values = config.values.any? {|v| v.is_a?(String) && v.gsub(%r{https?:\/\/}, "").match?(/\A\S+: /) } + if has_invalid_keys || has_invalid_values warn "Failed to load #{filename} because it doesn't contain valid YAML hash" return {} else @@ -554,7 +586,9 @@ if you believe they were disclosed to a third party. def self.load_with_rubygems_config_hash(yaml) require_relative "yaml_serializer" - content = Gem::YAMLSerializer.load(yaml) + content = Gem::YAMLSerializer.load(yaml, permitted_classes: []) + return {} unless content.is_a?(Hash) + deep_transform_config_keys!(content) end @@ -588,7 +622,7 @@ if you believe they were disclosed to a third party. else v end - elsif v.empty? + elsif v.respond_to?(:empty?) && v.empty? nil elsif v.is_a?(Hash) deep_transform_config_keys!(v) diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb index 073966b696..3a9bdbdc9d 100644 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -64,8 +64,11 @@ module Kernel rp end - Kernel.send(:gem, name, Gem::Requirement.default_prerelease) unless - resolved_path + next if resolved_path + + Kernel.send(:gem, name, Gem::Requirement.default_prerelease) + + Gem.load_bundler_extensions(Gem.loaded_specs[name].version) if name == "bundler" next end diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index 1bd208feb9..2247c49c81 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -13,7 +13,7 @@ module Gem # An Array of the default sources that come with RubyGems def self.default_sources - %w[https://rubygems.org/] + @default_sources ||= %w[https://rubygems.org/] end ## @@ -149,6 +149,15 @@ module Gem end ## + # The path to the global gem cache directory. + # This is used when global_gem_cache is enabled to share .gem files + # across all Ruby installations. + + def self.global_gem_cache_path + File.join(cache_home, "gem", "gems") + end + + ## # The path to standard location of the user's data directory. def self.data_home @@ -239,7 +248,7 @@ module Gem # Enables automatic installation into user directory def self.default_user_install # :nodoc: - if !ENV.key?("GEM_HOME") && (File.exist?(Gem.dir) && !File.writable?(Gem.dir)) + if !ENV.key?("GEM_HOME") && File.exist?(Gem.dir) && !File.writable?(Gem.dir) Gem.ui.say "Defaulting to user installation because default installation directory (#{Gem.dir}) is not writable." return true end diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index d1ec9222af..1e91f493a6 100644 --- a/lib/rubygems/dependency.rb +++ b/lib/rubygems/dependency.rb @@ -217,7 +217,7 @@ class Gem::Dependency # NOTE: Unlike #matches_spec? this method does not return true when the # version is a prerelease version unless this is a prerelease dependency. - def match?(obj, version=nil, allow_prerelease=false) + def match?(obj, version = nil, allow_prerelease = false) if !version name = obj.name version = obj.version diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index b119dca1cf..c842714d95 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -7,14 +7,12 @@ require_relative "installer" require_relative "spec_fetcher" require_relative "user_interaction" require_relative "available_set" -require_relative "deprecate" ## # Installs a gem along with all its dependencies from local and remote gems. class Gem::DependencyInstaller include Gem::UserInteraction - extend Gem::Deprecate DEFAULT_OPTIONS = { # :nodoc: env_shebang: false, @@ -28,7 +26,6 @@ class Gem::DependencyInstaller wrappers: true, build_args: nil, build_docs_in_background: false, - install_as_default: false, }.freeze ## @@ -86,11 +83,13 @@ class Gem::DependencyInstaller @user_install = options[:user_install] @wrappers = options[:wrappers] @build_args = options[:build_args] + @build_jobs = options[:build_jobs] @build_docs_in_background = options[:build_docs_in_background] - @install_as_default = options[:install_as_default] @dir_mode = options[:dir_mode] @data_mode = options[:data_mode] @prog_mode = options[:prog_mode] + @build_extension = options[:build_extension] + @install_plugin = options[:install_plugin] # Indicates that we should not try to update any deps unless # we absolutely must. @@ -121,78 +120,6 @@ class Gem::DependencyInstaller @domain == :both || @domain == :remote end - ## - # Returns a list of pairs of gemspecs and source_uris that match - # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) - # sources. Gems are sorted with newer gems preferred over older gems, and - # local gems preferred over remote gems. - - def find_gems_with_sources(dep, best_only=false) # :nodoc: - set = Gem::AvailableSet.new - - if consider_local? - sl = Gem::Source::Local.new - - if spec = sl.find_gem(dep.name) - if dep.matches_spec? spec - set.add spec, sl - end - end - end - - if consider_remote? - begin - # This is pulled from #spec_for_dependency to allow - # us to filter tuples before fetching specs. - tuples, errors = Gem::SpecFetcher.fetcher.search_for_dependency dep - - if best_only && !tuples.empty? - tuples.sort! do |a,b| - if b[0].version == a[0].version - if b[0].platform != Gem::Platform::RUBY - 1 - else - -1 - end - else - b[0].version <=> a[0].version - end - end - tuples = [tuples.first] - end - - specs = [] - tuples.each do |tup, source| - spec = source.fetch_spec(tup) - rescue Gem::RemoteFetcher::FetchError => e - errors << Gem::SourceFetchProblem.new(source, e) - else - specs << [spec, source] - end - - if @errors - @errors += errors - else - @errors = errors - end - - set << specs - rescue Gem::RemoteFetcher::FetchError => e - # FIX if there is a problem talking to the network, we either need to always tell - # the user (no really_verbose) or fail hard, not silently tell them that we just - # couldn't find their requested gem. - verbose do - "Error fetching remote data:\t\t#{e.message}\n" \ - "Falling back to local-only install" - end - @domain = :local - end - end - - set - end - rubygems_deprecate :find_gems_with_sources - def in_background(what) # :nodoc: fork_happened = false if @build_docs_in_background && Process.respond_to?(:fork) @@ -230,6 +157,7 @@ class Gem::DependencyInstaller options = { bin_dir: @bin_dir, build_args: @build_args, + build_jobs: @build_jobs, document: @document, env_shebang: @env_shebang, force: @force, @@ -240,10 +168,11 @@ class Gem::DependencyInstaller user_install: @user_install, wrappers: @wrappers, build_root: @build_root, - install_as_default: @install_as_default, dir_mode: @dir_mode, data_mode: @data_mode, prog_mode: @prog_mode, + build_extension: @build_extension, + install_plugin: @install_plugin, } options[:install_dir] = @install_dir if @only_install_dir diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb index ad5e59e8c1..d50cfe2d54 100644 --- a/lib/rubygems/dependency_list.rb +++ b/lib/rubygems/dependency_list.rb @@ -7,7 +7,6 @@ #++ require_relative "vendored_tsort" -require_relative "deprecate" ## # Gem::DependencyList is used for installing and uninstalling gems in the @@ -140,7 +139,7 @@ class Gem::DependencyList # If removing the gemspec creates breaks a currently ok dependency, then it # is NOT ok to remove the gemspec. - def ok_to_remove?(full_name, check_dev=true) + def ok_to_remove?(full_name, check_dev = true) gem_to_remove = find_name full_name # If the state is inconsistent, at least don't crash diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 7d24f9cbfc..eb503bb269 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -1,75 +1,75 @@ # frozen_string_literal: true -## -# Provides 3 methods for declaring when something is going away. -# -# +deprecate(name, repl, year, month)+: -# Indicate something may be removed on/after a certain date. -# -# +rubygems_deprecate(name, replacement=:none)+: -# Indicate something will be removed in the next major RubyGems version, -# and (optionally) a replacement for it. -# -# +rubygems_deprecate_command+: -# Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be -# removed in the next RubyGems version. -# -# Also provides +skip_during+ for temporarily turning off deprecation warnings. -# This is intended to be used in the test suite, so deprecation warnings -# don't cause test failures if you need to make sure stderr is otherwise empty. -# -# -# Example usage of +deprecate+ and +rubygems_deprecate+: -# -# class Legacy -# def self.some_class_method -# # ... -# end -# -# def some_instance_method -# # ... -# end -# -# def some_old_method -# # ... -# end -# -# extend Gem::Deprecate -# deprecate :some_instance_method, "X.z", 2011, 4 -# rubygems_deprecate :some_old_method, "Modern#some_new_method" -# -# class << self -# extend Gem::Deprecate -# deprecate :some_class_method, :none, 2011, 4 -# end -# end -# -# -# Example usage of +rubygems_deprecate_command+: -# -# class Gem::Commands::QueryCommand < Gem::Command -# extend Gem::Deprecate -# rubygems_deprecate_command -# -# # ... -# end -# -# -# Example usage of +skip_during+: -# -# class TestSomething < Gem::Testcase -# def test_some_thing_with_deprecations -# Gem::Deprecate.skip_during do -# actual_stdout, actual_stderr = capture_output do -# Gem.something_deprecated -# end -# assert_empty actual_stdout -# assert_equal(expected, actual_stderr) -# end -# end -# end - module Gem + ## + # Provides 3 methods for declaring when something is going away. + # + # <tt>deprecate(name, repl, year, month)</tt>: + # Indicate something may be removed on/after a certain date. + # + # <tt>rubygems_deprecate(name, replacement=:none)</tt>: + # Indicate something will be removed in the next major RubyGems version, + # and (optionally) a replacement for it. + # + # +rubygems_deprecate_command+: + # Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be + # removed in the next RubyGems version. + # + # Also provides +skip_during+ for temporarily turning off deprecation warnings. + # This is intended to be used in the test suite, so deprecation warnings + # don't cause test failures if you need to make sure stderr is otherwise empty. + # + # + # Example usage of +deprecate+ and +rubygems_deprecate+: + # + # class Legacy + # def self.some_class_method + # # ... + # end + # + # def some_instance_method + # # ... + # end + # + # def some_old_method + # # ... + # end + # + # extend Gem::Deprecate + # deprecate :some_instance_method, "X.z", 2011, 4 + # rubygems_deprecate :some_old_method, "Modern#some_new_method" + # + # class << self + # extend Gem::Deprecate + # deprecate :some_class_method, :none, 2011, 4 + # end + # end + # + # + # Example usage of +rubygems_deprecate_command+: + # + # class Gem::Commands::QueryCommand < Gem::Command + # extend Gem::Deprecate + # rubygems_deprecate_command + # + # # ... + # end + # + # + # Example usage of +skip_during+: + # + # class TestSomething < Gem::Testcase + # def test_some_thing_with_deprecations + # Gem::Deprecate.skip_during do + # actual_stdout, actual_stderr = capture_output do + # Gem.something_deprecated + # end + # assert_empty actual_stdout + # assert_equal(expected, actual_stderr) + # end + # end + # end + module Deprecate def self.skip # :nodoc: @skip ||= false @@ -126,17 +126,18 @@ module Gem # telling the user of +repl+ (unless +repl+ is :none) and the # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none) + def rubygems_deprecate(name, replacement = :none, version = nil) class_eval do old = "_deprecated_#{name}" alias_method old, name define_method name do |*args, &block| klass = is_a? Module target = klass ? "#{self}." : "#{self.class}#" + version ||= Gem::Deprecate.next_rubygems_major_version msg = [ "NOTE: #{target}#{name} is deprecated", replacement == :none ? " with no replacement" : "; use #{replacement} instead", - ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}", + ". It will be removed in Rubygems #{version}", "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", ] warn "#{msg.join}." unless Gem::Deprecate.skip @@ -147,13 +148,14 @@ module Gem end # Deprecation method to deprecate Rubygems commands - def rubygems_deprecate_command(version = Gem::Deprecate.next_rubygems_major_version) + def rubygems_deprecate_command(version = nil) class_eval do define_method "deprecated?" do true end define_method "deprecation_warning" do + version ||= Gem::Deprecate.next_rubygems_major_version msg = [ "#{command} command is deprecated", ". It will be removed in Rubygems #{version}.\n", diff --git a/lib/rubygems/doctor.rb b/lib/rubygems/doctor.rb index 56b7c081eb..4f26260d83 100644 --- a/lib/rubygems/doctor.rb +++ b/lib/rubygems/doctor.rb @@ -113,7 +113,7 @@ class Gem::Doctor next if installed_specs.include? basename next if /^rubygems-\d/.match?(basename) next if sub_directory == "specifications" && basename == "default" - next if sub_directory == "plugins" && Gem.plugin_suffix_regexp =~ (basename) + next if sub_directory == "plugins" && Gem.plugin_suffix_regexp =~ basename type = File.directory?(child) ? "directory" : "file" diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb index 57fb3eb120..4bbc5217e0 100644 --- a/lib/rubygems/errors.rb +++ b/lib/rubygems/errors.rb @@ -26,7 +26,7 @@ module Gem # system. Instead of rescuing from this class, make sure to rescue from the # superclass Gem::LoadError to catch all types of load errors. class MissingSpecError < Gem::LoadError - def initialize(name, requirement, extra_message=nil) + def initialize(name, requirement, extra_message = nil) @name = name @requirement = requirement @extra_message = extra_message diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 793324b875..e00a70c662 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative "deprecate" require_relative "unknown_command_spell_checker" ## @@ -21,20 +20,11 @@ class Gem::UnknownCommandError < Gem::Exception end def self.attach_correctable - return if defined?(@attached) + return if method_defined?(:corrections) - if defined?(DidYouMean::SPELL_CHECKERS) && defined?(DidYouMean::Correctable) - if DidYouMean.respond_to?(:correct_error) - DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker) - else - DidYouMean::SPELL_CHECKERS["Gem::UnknownCommandError"] = - Gem::UnknownCommandSpellChecker - - prepend DidYouMean::Correctable - end + if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error) + DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker) end - - @attached = true end end @@ -43,22 +33,24 @@ class Gem::DependencyError < Gem::Exception; end class Gem::DependencyRemovalException < Gem::Exception; end ## -# Raised by Gem::Resolver when a Gem::Dependency::Conflict reaches the -# toplevel. Indicates which dependencies were incompatible through #conflict -# and #conflicting_dependencies +# Raised by Gem::Resolver when dependency resolution fails. class Gem::DependencyResolutionError < Gem::DependencyError - attr_reader :conflict - def initialize(conflict) - @conflict = conflict - a, b = conflicting_dependencies + @explanation = conflict.explanation + super @explanation + end - super "conflicting dependencies #{a} and #{b}\n#{@conflict.explanation}" + def explanation + @explanation + end + + def conflict + nil end def conflicting_dependencies - @conflict.conflicting_dependencies + [] end end @@ -110,7 +102,7 @@ class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException # and +version+. Any +errors+ encountered when attempting to find the gem # are also stored. - def initialize(name, version, errors=nil) + def initialize(name, version, errors = nil) super "Could not find a valid gem '#{name}' (#{version}) locally or in a repository" @name = name @@ -136,40 +128,6 @@ end Gem.deprecate_constant :SpecificGemNotFoundException -## -# Raised by Gem::Resolver when dependencies conflict and create the -# inability to find a valid possible spec for a request. - -class Gem::ImpossibleDependenciesError < Gem::Exception - attr_reader :conflicts - attr_reader :request - - def initialize(request, conflicts) - @request = request - @conflicts = conflicts - - super build_message - end - - def build_message # :nodoc: - requester = @request.requester - requester = requester ? requester.spec.full_name : "The user" - dependency = @request.dependency - - message = "#{requester} requires #{dependency} but it conflicted:\n".dup - - @conflicts.each do |_, conflict| - message << conflict.explanation - end - - message - end - - def dependency - @request.dependency - end -end - class Gem::InstallError < Gem::Exception; end class Gem::RuntimeRequirementNotMetError < Gem::InstallError @@ -261,7 +219,7 @@ class Gem::UnsatisfiableDependencyError < Gem::DependencyError # Creates a new UnsatisfiableDependencyError for the unsatisfiable # Gem::Resolver::DependencyRequest +dep+ - def initialize(dep, platform_mismatch=nil) + def initialize(dep, platform_mismatch = nil) if platform_mismatch && !platform_mismatch.empty? plats = platform_mismatch.map {|x| x.platform.to_s }.sort.uniq super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(", ")}" diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 12eb62ef16..e00cf159da 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -7,11 +7,13 @@ #++ require_relative "../user_interaction" -require_relative "../shellwords" class Gem::Ext::Builder include Gem::UserInteraction + class NoMakefileError < Gem::InstallError + end + attr_accessor :build_args # :nodoc: def self.class_name @@ -20,19 +22,30 @@ class Gem::Ext::Builder end def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"], - target_rbconfig: Gem.target_rbconfig) + target_rbconfig: Gem.target_rbconfig, n_jobs: nil) unless File.exist? File.join(make_dir, "Makefile") - raise Gem::InstallError, "Makefile not found" + # No makefile exists, nothing to do. + raise NoMakefileError, "No Makefile found in #{make_dir}" end # try to find make program from Ruby configure arguments first target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/ make_program_name = ENV["MAKE"] || ENV["make"] || $1 make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make" - make_program = Shellwords.split(make_program_name) + make_program = shellsplit(make_program_name) + is_nmake = /\bnmake/i.match?(make_program_name) # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform. - destdir = /\bnmake/i !~ make_program_name || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" + destdir = !is_nmake || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" + + # nmake doesn't support parallel build + unless is_nmake + have_make_arguments = make_program.size > 1 + + if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs + make_program << "-j#{n_jobs}" + end + end env = [destdir] @@ -58,7 +71,7 @@ class Gem::Ext::Builder def self.ruby # Gem.ruby is quoted if it contains whitespace - cmd = Shellwords.split(Gem.ruby) + cmd = shellsplit(Gem.ruby) # This load_path is only needed when running rubygems test without a proper installation. # Prepending it in a normal installation will cause problem with order of $LOAD_PATH. @@ -83,13 +96,14 @@ class Gem::Ext::Builder p(command) end results << "current directory: #{dir}" - results << Shellwords.join(command) + results << shelljoin(command) require "open3" # Set $SOURCE_DATE_EPOCH for the subprocess. build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env) output, status = begin - Open3.popen2e(build_env, *command, chdir: dir) do |_stdin, stdouterr, wait_thread| + Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread| + stdin.close output = String.new while line = stdouterr.gets output << line @@ -127,18 +141,29 @@ class Gem::Ext::Builder end end + def self.shellsplit(command) + require "shellwords" + + Shellwords.split(command) + end + + def self.shelljoin(command) + require "shellwords" + + Shellwords.join(command) + end + ## # Creates a new extension builder for +spec+. If the +spec+ does not yet # have build arguments, saved, set +build_args+ which is an ARGV-style # array. - def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig) + def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig, build_jobs = nil) @spec = spec @build_args = build_args @gem_dir = spec.full_gem_path @target_rbconfig = target_rbconfig - - @ran_rake = false + @build_jobs = build_jobs end ## @@ -151,10 +176,9 @@ class Gem::Ext::Builder when /configure/ then Gem::Ext::ConfigureBuilder when /rakefile/i, /mkrf_conf/i then - @ran_rake = true Gem::Ext::RakeBuilder when /CMakeLists.txt/ then - Gem::Ext::CmakeBuilder + Gem::Ext::CmakeBuilder.new when /Cargo.toml/ then Gem::Ext::CargoBuilder.new else @@ -193,7 +217,7 @@ EOF FileUtils.mkdir_p dest_path results = builder.build(extension, dest_path, - results, @build_args, lib_dir, extension_dir, @target_rbconfig) + results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs) verbose { results.join("\n") } @@ -224,8 +248,6 @@ EOF FileUtils.rm_f @spec.gem_build_complete_path @spec.extensions.each do |extension| - break if @ran_rake - build_extension extension, dest_path end diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 453f8d5bcb..516459dd60 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "../shellwords" - # This class is used by rubygems to build Rust extensions. It is a thin-wrapper # over the `cargo rustc` command which takes care of building Rust code in a way # that Ruby can use. @@ -17,7 +15,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder end def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd, - target_rbconfig=Gem.target_rbconfig) + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) require "tempfile" require "fileutils" @@ -159,7 +157,11 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder # We want to use the same linker that Ruby uses, so that the linker flags from # mkmf work properly. def linker_args - cc_flag = Shellwords.split(makefile_config("CC")) + cc_flag = self.class.shellsplit(makefile_config("CC")) + # Avoid to ccache like tool from Rust build + # see https://github.com/ruby/rubygems/pull/8521#issuecomment-2689854359 + # ex. CC="ccache gcc" or CC="sccache clang --any --args" + cc_flag.shift if cc_flag.size >= 2 && !cc_flag[1].start_with?("-") linker = cc_flag.shift link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] } @@ -178,7 +180,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder def libruby_args(dest_dir) libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED") - raw_libs = Shellwords.split(libs) + raw_libs = self.class.shellsplit(libs) raw_libs.flat_map {|l| ldflag_to_link_modifier(l) } end @@ -225,10 +227,9 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder raise Gem::InstallError, "cargo metadata failed#{exit_reason}" end - # cargo metadata output is specified as json, but with the - # --format-version 1 option the output is compatible with YAML, so we can - # avoid the json dependency - metadata = Gem::SafeYAML.safe_load(output) + # cargo metadata output is specified as json + require "json" + metadata = JSON.parse(output) package = metadata["packages"].find {|pkg| normalize_path(pkg["manifest_path"]) == manifest_path } unless package found = metadata["packages"].map {|md| "#{md["name"]} at #{md["manifest_path"]}" } @@ -261,7 +262,7 @@ EOF end def split_flags(var) - Shellwords.split(RbConfig::CONFIG.fetch(var, "")) + self.class.shellsplit(RbConfig::CONFIG.fetch(var, "")) end def ldflag_to_link_modifier(arg) diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index 34564f668d..e660ed558b 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -1,21 +1,110 @@ # frozen_string_literal: true +# This builder creates extensions defined using CMake. Its is invoked if a Gem's spec file +# sets the `extension` property to a string that contains `CMakeLists.txt`. +# +# In general, CMake projects are built in two steps: +# +# * configure +# * build +# +# The builder follow this convention. First it runs a configuration step and then it runs a build step. +# +# CMake projects can be quite configurable - it is likely you will want to specify options when +# installing a gem. To pass options to CMake specify them after `--` in the gem install command. For example: +# +# gem install <gem_name> -- --preset <preset_name> +# +# Note that options are ONLY sent to the configure step - it is not currently possible to specify +# options for the build step. If this becomes and issue then the CMake builder can be updated to +# support build options. +# +# Useful options to know are: +# +# -G to specify a generator (-G Ninja is recommended) +# -D<CMAKE_VARIABLE> to set a CMake variable (for example -DCMAKE_BUILD_TYPE=Release) +# --preset <preset_name> to use a preset +# +# If the Gem author provides presets, via CMakePresets.json file, you will likely want to use one of them. +# If not, you may wish to specify a generator. Ninja is recommended because it can build projects in parallel +# and thus much faster than building them serially like Make does. + class Gem::Ext::CmakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, cmake_dir=Dir.pwd, - target_rbconfig=Gem.target_rbconfig) + attr_accessor :runner, :profile + def initialize + @runner = self.class.method(:run) + @profile = :release + end + + def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) if target_rbconfig.path warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring" end - unless File.exist?(File.join(cmake_dir, "Makefile")) - require_relative "../command" - cmd = ["cmake", ".", "-DCMAKE_INSTALL_PREFIX=#{dest_path}", *Gem::Command.build_args] + # Figure the build dir + build_dir = File.join(cmake_dir, "build") - run cmd, results, class_name, cmake_dir - end + # Check if the gem defined presets + check_presets(cmake_dir, args, results) + + # Configure + configure(cmake_dir, build_dir, dest_path, args, results) - make dest_path, results, cmake_dir, target_rbconfig: target_rbconfig + # Compile + compile(cmake_dir, build_dir, args, results) results end + + def configure(cmake_dir, build_dir, install_dir, args, results) + cmd = ["cmake", + cmake_dir, + "-B", + build_dir, + "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=#{install_dir}", # Windows + "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=#{install_dir}", # Not Windows + *Gem::Command.build_args, + *args] + + runner.call(cmd, results, "cmake_configure", cmake_dir) + end + + def compile(cmake_dir, build_dir, args, results) + cmd = ["cmake", + "--build", + build_dir.to_s, + "--config", + @profile.to_s] + + runner.call(cmd, results, "cmake_compile", cmake_dir) + end + + private + + def check_presets(cmake_dir, args, results) + # Return if the user specified a preset + return unless args.grep(/--preset/i).empty? + + cmd = ["cmake", + "--list-presets"] + + presets = Array.new + begin + runner.call(cmd, presets, "cmake_presets", cmake_dir) + + # Remove the first two lines of the array which is the current_directory and the command + # that was run + presets = presets[2..].join + results << <<~EOS + The gem author provided a list of presets that can be used to build the gem. To use a preset specify it on the command line: + + gem install <gem_name> -- --preset <preset_name> + + #{presets} + EOS + rescue Gem::InstallError + # Do nothing, CMakePresets.json was not included in the Gem + end + end end diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index d91b1ec5e8..230b214b3c 100644 --- a/lib/rubygems/ext/configure_builder.rb +++ b/lib/rubygems/ext/configure_builder.rb @@ -7,8 +7,8 @@ #++ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd, - target_rbconfig=Gem.target_rbconfig) + def self.build(extension, dest_path, results, args = [], lib_dir = nil, configure_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) if target_rbconfig.path warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring" end @@ -19,7 +19,7 @@ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder run cmd, results, class_name, configure_dir end - make dest_path, results, configure_dir, target_rbconfig: target_rbconfig + make dest_path, results, configure_dir, target_rbconfig: target_rbconfig, n_jobs: n_jobs results end diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index e652a221f8..822454355d 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -7,8 +7,8 @@ #++ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd, - target_rbconfig=Gem.target_rbconfig) + def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) require "fileutils" require "tempfile" @@ -41,7 +41,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder ENV["DESTDIR"] = nil - make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig + make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs full_tmp_dest = File.join(extension_dir, tmp_dest_relative) @@ -66,6 +66,10 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder end results + rescue Gem::Ext::Builder::NoMakefileError => error + results << error.message + results << "Skipping make for #{extension} as no Makefile was found." + # We are good, do not re-raise the error. ensure FileUtils.rm_rf tmp_dest if tmp_dest end diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 42393a4a06..d702d7f339 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "../shellwords" - #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -9,8 +7,8 @@ require_relative "../shellwords" #++ class Gem::Ext::RakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd, - target_rbconfig=Gem.target_rbconfig) + def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) if target_rbconfig.path warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring" end @@ -22,7 +20,7 @@ class Gem::Ext::RakeBuilder < Gem::Ext::Builder rake = ENV["rake"] if rake - rake = Shellwords.split(rake) + rake = shellsplit(rake) else begin rake = ruby << "-rrubygems" << Gem.bin_path("rake", "rake") diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index 4cb924677f..e60cebd0cb 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -8,7 +8,6 @@ require_relative "../rubygems" require_relative "command_manager" -require_relative "deprecate" ## # Run an instance of the gem program. diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index d3176d4564..9c22c14fad 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -154,10 +154,11 @@ module Gem::GemcutterUtilities def sign_in(sign_in_host = nil, scope: nil) sign_in_host ||= host - return if api_key - pretty_host = pretty_host(sign_in_host) - + if api_key + say "You are already signed in on #{pretty_host}." + return + end say "Enter your #{pretty_host} credentials." say "Don't have an account yet? " \ "Create one at #{sign_in_host}/sign_up" @@ -263,7 +264,10 @@ module Gem::GemcutterUtilities port = server.addr[1].to_s url_with_port = "#{webauthn_url}?port=#{port}" - say "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option." + say "You have enabled multi-factor authentication. Please visit the following URL to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option." + say "" + say url_with_port + say "" threads = [WebauthnListener.listener_thread(host, server), WebauthnPoller.poll_thread(options, host, webauthn_url, credentials)] otp_thread = wait_for_otp_thread(*threads) @@ -316,7 +320,7 @@ module Gem::GemcutterUtilities end def get_scope_params(scope) - scope_params = { index_rubygems: true } + scope_params = { index_rubygems: true, push_rubygem: true } if scope scope_params = { scope => true } diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb index abf65efe37..3f56a077c9 100644 --- a/lib/rubygems/gemcutter_utilities/webauthn_listener.rb +++ b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb @@ -85,10 +85,17 @@ module Gem::GemcutterUtilities end def parse_otp_from_uri(uri) - require "cgi" + query = uri.query + return unless query && !query.empty? - return if uri.query.nil? - CGI.parse(uri.query).dig("code", 0) + query.split("&") do |param| + key, value = param.split("=", 2) + if value && Gem::URI.decode_www_form_component(key) == "code" + return Gem::URI.decode_www_form_component(value) + end + end + + nil end class SocketResponder diff --git a/lib/rubygems/install_default_message.rb b/lib/rubygems/install_default_message.rb deleted file mode 100644 index 0640eaaf08..0000000000 --- a/lib/rubygems/install_default_message.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require_relative "../rubygems" -require_relative "user_interaction" - -## -# A post-install hook that displays "Successfully installed -# some_gem-1.0 as a default gem" - -Gem.post_install do |installer| - ui = Gem::DefaultUserInteraction.ui - ui.say "Successfully installed #{installer.spec.full_name} as a default gem" -end diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 0d0f0dc211..e8859cadaf 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -31,6 +31,15 @@ module Gem::InstallUpdateOptions options[:bin_dir] = File.expand_path(value) end + add_option(:"Install/Update", "-j", "--build-jobs VALUE", Integer, + "Specify the number of jobs to pass to `make` when installing", + "gems with native extensions.", + "Defaults to the number of processors.", + "This option is ignored on the mswin platform or", + "if the MAKEFLAGS environment variable is set.") do |value, options| + options[:build_jobs] = value + end + add_option(:"Install/Update", "--document [TYPES]", Array, "Generate documentation for installed gems", "List the documentation types you wish to", @@ -158,10 +167,9 @@ module Gem::InstallUpdateOptions options[:without_groups].concat v.map(&:intern) end - add_option(:"Install/Update", "--default", + add_option(:Deprecated, "--default", "Add the gem's full specification to", "specifications/default and extract only its bin") do |v,_o| - options[:install_as_default] = v end add_option(:"Install/Update", "--explain", @@ -184,6 +192,18 @@ module Gem::InstallUpdateOptions "rbconfig.rb for the deployment target platform") do |v, _o| Gem.set_target_rbconfig(v) end + + add_option(:"Install/Update", "--[no-]build-extension", + "Build native extensions during installation.", + "Defaults to true") do |v, _o| + options[:build_extension] = v + end + + add_option(:"Install/Update", "--[no-]install-plugin", + "Install plugins during installation.", + "Defaults to true") do |v, _o| + options[:install_plugin] = v + end end ## diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 7f5d913ac4..15d6aac0fd 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -8,7 +8,6 @@ require_relative "installer_uninstaller_utils" require_relative "exceptions" -require_relative "deprecate" require_relative "package" require_relative "ext" require_relative "user_interaction" @@ -27,8 +26,6 @@ require_relative "user_interaction" # file. See Gem.pre_install and Gem.post_install for details. class Gem::Installer - extend Gem::Deprecate - ## # Paths where env(1) might live. Some systems are broken and have it in # /bin @@ -67,23 +64,6 @@ class Gem::Installer attr_reader :package class << self - # - # Changes in rubygems to lazily loading `rubygems/command` (in order to - # lazily load `optparse` as a side effect) affect bundler's custom installer - # which uses `Gem::Command` without requiring it (up until bundler 2.2.29). - # This hook is to compensate for that missing require. - # - # TODO: Remove when rubygems no longer supports running on bundler older - # than 2.2.29. - - def inherited(klass) - if klass.name == "Bundler::RubyGemsGemInstaller" - require "rubygems/command" - end - - super(klass) - end - ## # Overrides the executable format. # @@ -170,7 +150,7 @@ class Gem::Installer # process. If not set, then Gem::Command.build_args is used # :post_install_message:: Print gem post install message if true - def initialize(package, options={}) + def initialize(package, options = {}) require "fileutils" @options = options @@ -228,8 +208,7 @@ class Gem::Installer ruby_executable = true existing = io.read.slice(/ ^\s*( - gem \s | - load \s Gem\.bin_path\( | + Gem\.activate_and_load_bin_path\( | load \s Gem\.activate_bin_path\( ) (['"])(.*?)(\2), @@ -292,11 +271,7 @@ class Gem::Installer run_pre_install_hooks # Set loaded_from to ensure extension_dir is correct - if @options[:install_as_default] - spec.loaded_from = default_spec_file - else - spec.loaded_from = spec_file - end + spec.loaded_from = spec_file # Completely remove any previous gem files FileUtils.rm_rf gem_dir @@ -305,32 +280,30 @@ class Gem::Installer dir_mode = options[:dir_mode] FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755 - if @options[:install_as_default] - extract_bin - write_default_spec - else - extract_files + extract_files - build_extensions - write_build_info_file - run_post_build_hooks - end + build_extensions + write_build_info_file + run_post_build_hooks generate_bin - generate_plugins - - unless @options[:install_as_default] - write_spec - write_cache_file + if options[:install_plugin] == false + remove_stale_plugins + warn_skipped_plugins + else + generate_plugins end + write_spec + write_cache_file + File.chmod(dir_mode, gem_dir) if dir_mode say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? Gem::Specification.add_spec(spec) unless @install_dir - load_plugin + load_plugin unless options[:install_plugin] == false run_post_install_hooks @@ -411,15 +384,6 @@ class Gem::Installer end ## - # Unpacks the gem into the given directory. - - def unpack(directory) - @gem_dir = directory - extract_files - end - rubygems_deprecate :unpack - - ## # The location of the spec file that is installed. # @@ -427,12 +391,18 @@ class Gem::Installer File.join gem_home, "specifications", "#{spec.full_name}.gemspec" end + def default_spec_dir + dir = File.join(gem_home, "specifications", "default") + FileUtils.mkdir_p dir + dir + end + ## # The location of the default spec file for default gems. # def default_spec_file - File.join gem_home, "specifications", "default", "#{spec.full_name}.gemspec" + File.join default_spec_dir, "#{spec.full_name}.gemspec" end ## @@ -670,6 +640,7 @@ class Gem::Installer @build_root = options[:build_root] @build_args = options[:build_args] + @build_jobs = options[:build_jobs] @gem_home = @install_dir || user_install_dir || Gem.dir @@ -749,54 +720,53 @@ class Gem::Installer def app_script_text(bin_file_name) # NOTE: that the `load` lines cannot be indented, as old RG versions match # against the beginning of the line - <<-TEXT -#{shebang bin_file_name} -# -# This file was generated by RubyGems. -# -# The application '#{spec.name}' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require 'rubygems' -#{gemdeps_load(spec.name)} -version = "#{Gem::Requirement.default_prerelease}" - -str = ARGV.first -if str - str = str.b[/\\A_(.*)_\\z/, 1] - if str and Gem::Version.correct?(str) - #{explicit_version_requirement(spec.name)} - ARGV.shift - end -end + <<~TEXT + #{shebang bin_file_name} + # + # This file was generated by RubyGems. + # + # The application '#{spec.name}' is installed as part of a gem, and + # this file is here to facilitate running it. + # + + require 'rubygems' + #{gemdeps_load(spec.name)} + version = "#{Gem::Requirement.default_prerelease}" + + str = ARGV.first + if str + str = str.b[/\\A_(.*)_\\z/, 1] + if str and Gem::Version.correct?(str) + #{explicit_version_requirement(spec.name)} + ARGV.shift + end + end -if Gem.respond_to?(:activate_bin_path) -load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version) -else -gem #{spec.name.dump}, version -load Gem.bin_path(#{spec.name.dump}, #{bin_file_name.dump}, version) -end -TEXT + if Gem.respond_to?(:activate_and_load_bin_path) + Gem.activate_and_load_bin_path('#{spec.name}', '#{bin_file_name}', version) + else + load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version) + end + TEXT end def gemdeps_load(name) return "" if name == "bundler" - <<-TEXT + <<~TEXT -Gem.use_gemdeps -TEXT + Gem.use_gemdeps + TEXT end def explicit_version_requirement(name) code = "version = str" return code unless name == "bundler" - code += <<-TEXT + code += <<~TEXT - ENV['BUNDLER_VERSION'] = str -TEXT + ENV['BUNDLER_VERSION'] = str + TEXT end ## @@ -811,9 +781,9 @@ TEXT if File.exist?(File.join(bindir, ruby_exe)) # stub & ruby.exe within same folder. Portable - <<-TEXT -@ECHO OFF -@"%~dp0#{ruby_exe}" "%~dpn0" %* + <<~TEXT + @ECHO OFF + @"%~dp0#{ruby_exe}" "%~dpn0" %* TEXT elsif bindir.downcase.start_with? rb_topdir.downcase # stub within ruby folder, but not standard bin. Portable @@ -821,16 +791,16 @@ TEXT from = Pathname.new bindir to = Pathname.new "#{rb_topdir}/bin" rel = to.relative_path_from from - <<-TEXT -@ECHO OFF -@"%~dp0#{rel}/#{ruby_exe}" "%~dpn0" %* + <<~TEXT + @ECHO OFF + @"%~dp0#{rel}/#{ruby_exe}" "%~dpn0" %* TEXT else # outside ruby folder, maybe -user-install or bundler. Portable, but ruby # is dependent on PATH - <<-TEXT -@ECHO OFF -@#{ruby_exe} "%~dpn0" %* + <<~TEXT + @ECHO OFF + @#{ruby_exe} "%~dpn0" %* TEXT end end @@ -839,11 +809,37 @@ TEXT # configure scripts and rakefiles or mkrf_conf files. def build_extensions - builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig + if options[:build_extension] == false + warn_skipped_extensions + return + end + + builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs builder.build_extensions end + def warn_skipped_extensions # :nodoc: + return if spec.extensions.empty? + + alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \ + "To build extensions, run: gem pristine #{spec.name} --extensions" + end + + def warn_skipped_plugins # :nodoc: + return if spec.plugins.empty? + + alert_warning "#{spec.full_name} contains plugins that were not installed.\n" \ + "To install plugins, run: gem pristine #{spec.name} --only-plugins" + end + + def remove_stale_plugins # :nodoc: + return unless spec.plugins.empty? + + ensure_writable_dir @plugins_dir + remove_plugins_for(spec, @plugins_dir) + end + ## # Reads the file index and extracts each file into the gem directory. # @@ -908,11 +904,7 @@ TEXT ensure_loadable_spec - if options[:install_as_default] - Gem.ensure_default_gem_subdirectories gem_home - else - Gem.ensure_gem_subdirectories gem_home - end + Gem.ensure_gem_subdirectories gem_home return true if @force @@ -953,11 +945,8 @@ TEXT end def ensure_writable_dir(dir) # :nodoc: - begin - Dir.mkdir dir, *[options[:dir_mode] && 0o755].compact - rescue SystemCallError - raise unless File.directory? dir - end + require "fileutils" + FileUtils.mkdir_p dir, mode: options[:dir_mode] && 0o755 raise Gem::FilePermissionError.new(dir) unless File.writable? dir end @@ -984,6 +973,15 @@ TEXT end end + def build_jobs + @build_jobs ||= begin + require "etc" + Etc.nprocessors + 1 + rescue LoadError + 1 + end + end + def rb_config Gem.target_rbconfig end @@ -1023,9 +1021,10 @@ TEXT # are loaded at the same time. return unless specs.size == 1 - plugin_files = spec.plugins.map do |plugin| - File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + plugin_files = spec.plugins.filter_map do |plugin| + path = File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + path if File.exist?(path) end - Gem.load_plugin_files(plugin_files) + Gem.load_plugin_files(plugin_files) unless plugin_files.empty? end end diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb index 3f4a6fcf3d..cbdf4d7ac5 100644 --- a/lib/rubygems/name_tuple.rb +++ b/lib/rubygems/name_tuple.rb @@ -6,7 +6,7 @@ # wrap the data returned from the indexes. class Gem::NameTuple - def initialize(name, version, platform=Gem::Platform::RUBY) + def initialize(name, version, platform = Gem::Platform::RUBY) @name = name @version = version @@ -81,6 +81,12 @@ class Gem::NameTuple [@name, @version, @platform] end + alias_method :deconstruct, :to_a + + def deconstruct_keys(keys) + { name: @name, version: @version, platform: @platform } + end + def inspect # :nodoc: "#<Gem::NameTuple #{@name}, #{@version}, #{@platform}>" end diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index c855423ed7..7e41b18f66 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -7,6 +7,7 @@ # rubocop:enable Style/AsciiComments +require_relative "win_platform" require_relative "security" require_relative "user_interaction" @@ -231,7 +232,11 @@ class Gem::Package tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io| gzip_to io do |gz_io| - Psych.dump checksums_by_algorithm, gz_io + if Gem.use_psych? + Psych.dump checksums_by_algorithm, gz_io + else + gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm) + end end end end @@ -267,7 +272,7 @@ class Gem::Package tar.add_file_simple file, stat.mode, stat.size do |dst_io| File.open file, "rb" do |src_io| - copy_stream(src_io, dst_io) + copy_stream(src_io, dst_io, stat.size) end end end @@ -436,8 +441,6 @@ EOM symlinks << [full_name, link_target, destination, real_destination] end - FileUtils.rm_rf destination - mkdir = if entry.directory? destination @@ -450,9 +453,20 @@ EOM directories << mkdir end + real_mkdir = File.realpath(mkdir) + unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/")) + raise Gem::Package::PathError.new(real_mkdir, destination_dir) + end + if entry.file? - File.open(destination, "wb") {|out| copy_stream(entry, out) } - FileUtils.chmod file_mode(entry.header.mode) & ~File.umask, destination + File.open(destination, "wb") do |out| + copy_stream(tar.io, out, entry.size) + # Flush needs to happen before chmod because there could be data + # in the IO buffer that needs to be written, and that could be + # written after the chmod (on close) which would mess up the perms + out.flush + out.chmod file_mode(entry.header.mode) & ~File.umask + end end verbose destination @@ -461,7 +475,7 @@ EOM symlinks.each do |name, target, destination, real_destination| if File.exist?(real_destination) - File.symlink(target, destination) + create_symlink(target, destination) else alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring" end @@ -514,10 +528,12 @@ EOM destination end - def normalize_path(pathname) - if Gem.win_platform? + if Gem.win_platform? + def normalize_path(pathname) # :nodoc: pathname.downcase - else + end + else + def normalize_path(pathname) # :nodoc: pathname end end @@ -525,7 +541,7 @@ EOM ## # Loads a Gem::Specification from the TarEntry +entry+ - def load_spec(entry) # :nodoc: + def load_spec_from_metadata(entry) # :nodoc: limit = 10 * 1024 * 1024 case entry.full_name when "metadata" then @@ -545,6 +561,15 @@ EOM tar = Gem::Package::TarReader.new gzio yield tar + ensure + # Consume remaining gzip data to prevent the + # "attempt to close unfinished zstream; reset forced" warning + # when the GzipReader is closed with unconsumed compressed data. + begin + IO.copy_stream(gzio, IO::NULL) + rescue Zlib::GzipFile::Error, IOError + nil + end end end @@ -635,6 +660,8 @@ EOM raise Gem::Package::FormatError.new e.message, @gem end + private + ## # Verifies the +checksums+ against the +digests+. This check is not # cryptographically secure. Missing checksums are ignored. @@ -669,12 +696,7 @@ EOM digest entry end - case file_name - when "metadata", "metadata.gz" then - load_spec entry - when "data.tar.gz" then - verify_gz entry - end + load_spec_from_metadata entry rescue StandardError warn "Exception while verifying #{@gem.path}" raise @@ -702,25 +724,13 @@ EOM end end - ## - # Verifies that +entry+ is a valid gzipped file. - - def verify_gz(entry) # :nodoc: - Zlib::GzipReader.wrap entry do |gzio| - # TODO: read into a buffer once zlib supports it - gzio.read 16_384 until gzio.eof? # gzip checksum verification - end - rescue Zlib::GzipFile::Error => e - raise Gem::Package::FormatError.new(e.message, entry.full_name) - end - if RUBY_ENGINE == "truffleruby" - def copy_stream(src, dst) # :nodoc: - dst.write src.read + def copy_stream(src, dst, size) # :nodoc: + dst.write src.read(size) end else - def copy_stream(src, dst) # :nodoc: - IO.copy_stream(src, dst) + def copy_stream(src, dst, size) # :nodoc: + IO.copy_stream(src, dst, size) end end @@ -729,6 +739,23 @@ EOM raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit bytes end + + if Gem.win_platform? + # Create a symlink and fallback to copy the file or directory on Windows, + # where symlink creation needs special privileges in form of the Developer Mode. + # JRuby on Windows raises TypeError from the wincode path-conversion helper + # when it cannot create the symlink, so fall back to copy in that case too. + def create_symlink(old_name, new_name) + File.symlink(old_name, new_name) + rescue Errno::EACCES, TypeError + from = File.expand_path(old_name, File.dirname(new_name)) + FileUtils.cp_r(from, new_name) + end + else + def create_symlink(old_name, new_name) + File.symlink(old_name, new_name) + end + end end require_relative "package/digest_io" diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index 0ebcbd789d..dd20d65080 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -56,7 +56,7 @@ class Gem::Package::TarHeader ## # Pack format for a tar header - PACK_FORMAT = "a100" + # name + PACK_FORMAT = ("a100" + # name "a8" + # mode "a8" + # uid "a8" + # gid @@ -71,12 +71,12 @@ class Gem::Package::TarHeader "a32" + # gname "a8" + # devmajor "a8" + # devminor - "a155" # prefix + "a155").freeze # prefix ## # Unpack format for a tar header - UNPACK_FORMAT = "A100" + # name + UNPACK_FORMAT = ("A100" + # name "A8" + # mode "A8" + # uid "A8" + # gid @@ -91,7 +91,7 @@ class Gem::Package::TarHeader "A32" + # gname "A8" + # devmajor "A8" + # devminor - "A155" # prefix + "A155").freeze # prefix attr_reader(*FIELDS) diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb index 25f9b2f945..b66a8a62bc 100644 --- a/lib/rubygems/package/tar_reader.rb +++ b/lib/rubygems/package/tar_reader.rb @@ -30,6 +30,8 @@ class Gem::Package::TarReader nil end + attr_reader :io # :nodoc: + ## # Creates a new tar file reader on +io+ which needs to respond to #pos, # #eof?, #read, #getc and #pos= diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index b24bdb63e7..39fed9e2af 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -95,10 +95,11 @@ class Gem::Package::TarWriter end ## - # Adds file +name+ with permissions +mode+, and yields an IO for writing the - # file to + # Adds file +name+ with permissions +mode+ and mtime +mtime+ (sets + # Gem.source_date_epoch if not specified), and yields an IO for + # writing the file to - def add_file(name, mode) # :yields: io + def add_file(name, mode, mtime = nil) # :yields: io check_closed name, prefix = split_name name @@ -118,7 +119,7 @@ class Gem::Package::TarWriter header = Gem::Package::TarHeader.new name: name, mode: mode, size: size, prefix: prefix, - mtime: Gem.source_date_epoch + mtime: mtime || Gem.source_date_epoch @io.write header @io.pos = final_pos diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index 450c214167..367b00e7e1 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "deprecate" - ## # Available list of platforms for targeting Gem installations. # @@ -21,15 +19,6 @@ class Gem::Platform end end - def self.match(platform) - match_platforms?(platform, Gem.platforms) - end - - class << self - extend Gem::Deprecate - rubygems_deprecate :match, "Gem::Platform.match_spec? or match_gem?" - end - def self.match_platforms?(platform, platforms) platform = Gem::Platform.new(platform) unless platform.is_a?(Gem::Platform) platforms.any? do |local_platform| @@ -88,56 +77,45 @@ class Gem::Platform when Array then @cpu, @os, @version = arch when String then - arch = arch.split "-" - - if arch.length > 2 && !arch.last.match?(/\d+(\.\d+)?$/) # reassemble x86-linux-{libc} - extra = arch.pop - arch.last << "-#{extra}" - end + cpu, os = arch.sub(/-+$/, "").split("-", 2) - cpu = arch.shift - - @cpu = case cpu - when /i\d86/ then "x86" - else cpu - end - - if arch.length == 2 && arch.last.match?(/^\d+(\.\d+)?$/) # for command-line - @os, @version = arch - return + @cpu = if cpu&.match?(/i\d86/) + "x86" + else + cpu end - os, = arch if os.nil? @cpu = nil os = cpu end # legacy jruby @os, @version = case os - when /aix(\d+)?/ then ["aix", $1] - when /cygwin/ then ["cygwin", nil] - when /darwin(\d+)?/ then ["darwin", $1] - when /^macruby$/ then ["macruby", nil] - when /freebsd(\d+)?/ then ["freebsd", $1] - when /^java$/, /^jruby$/ then ["java", nil] - when /^java([\d.]*)/ then ["java", $1] - when /^dalvik(\d+)?$/ then ["dalvik", $1] - when /^dotnet$/ then ["dotnet", nil] - when /^dotnet([\d.]*)/ then ["dotnet", $1] - when /linux-?(\w+)?/ then ["linux", $1] - when /mingw32/ then ["mingw32", nil] - when /mingw-?(\w+)?/ then ["mingw", $1] - when /(mswin\d+)(\_(\d+))?/ then + when /aix-?(\d+)?/ then ["aix", $1] + when /cygwin/ then ["cygwin", nil] + when /darwin-?(\d+)?/ then ["darwin", $1] + when "macruby" then ["macruby", nil] + when /^macruby-?(\d+(?:\.\d+)*)?/ then ["macruby", $1] + when /freebsd-?(\d+)?/ then ["freebsd", $1] + when "java", "jruby" then ["java", nil] + when /^java-?(\d+(?:\.\d+)*)?/ then ["java", $1] + when /^dalvik-?(\d+)?$/ then ["dalvik", $1] + when /^dotnet$/ then ["dotnet", nil] + when /^dotnet-?(\d+(?:\.\d+)*)?/ then ["dotnet", $1] + when /linux-?(\w+)?/ then ["linux", $1] + when /mingw32/ then ["mingw32", nil] + when /mingw-?(\w+)?/ then ["mingw", $1] + when /(mswin\d+)(?:[_-](\d+))?/ then os = $1 - version = $3 - @cpu = "x86" if @cpu.nil? && os =~ /32$/ + version = $2 + @cpu = "x86" if @cpu.nil? && os.end_with?("32") [os, version] - when /netbsdelf/ then ["netbsdelf", nil] - when /openbsd(\d+\.\d+)?/ then ["openbsd", $1] - when /solaris(\d+\.\d+)?/ then ["solaris", $1] - when /wasi/ then ["wasi", nil] + when /netbsdelf/ then ["netbsdelf", nil] + when /openbsd-?(\d+\.\d+)?/ then ["openbsd", $1] + when /solaris-?(\d+\.\d+)?/ then ["solaris", $1] + when /wasi/ then ["wasi", nil] # test - when /^(\w+_platform)(\d+)?/ then [$1, $2] + when /^(\w+_platform)-?(\d+)?/ then [$1, $2] else ["unknown", nil] end when Gem::Platform then @@ -154,7 +132,38 @@ class Gem::Platform end def to_s - to_a.compact.join "-" + to_a.compact.join(@cpu.nil? ? "" : "-") + end + + ## + # Deconstructs the platform into an array for pattern matching. + # Returns [cpu, os, version]. + # + # Gem::Platform.new("x86_64-linux").deconstruct #=> ["x86_64", "linux", nil] + # + # This enables array pattern matching: + # + # case Gem::Platform.new("arm64-darwin-21") + # in ["arm64", "darwin", version] + # # version => "21" + # end + alias_method :deconstruct, :to_a + + ## + # Deconstructs the platform into a hash for pattern matching. + # Returns a hash with keys +:cpu+, +:os+, and +:version+. + # + # Gem::Platform.new("x86_64-darwin-20").deconstruct_keys(nil) + # #=> { cpu: "x86_64", os: "darwin", version: "20" } + # + # This enables hash pattern matching: + # + # case Gem::Platform.new("x86_64-linux") + # in cpu: "x86_64", os: "linux" + # # Matches Linux on x86_64 + # end + def deconstruct_keys(keys) + { cpu: @cpu, os: @os, version: @version } end ## @@ -266,4 +275,118 @@ class Gem::Platform # This will be replaced with Gem::Platform::local. CURRENT = "current" + + JAVA = Gem::Platform.new("java") # :nodoc: + MSWIN = Gem::Platform.new("mswin32") # :nodoc: + MSWIN64 = Gem::Platform.new("mswin64") # :nodoc: + MINGW = Gem::Platform.new("x86-mingw32") # :nodoc: + X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc: + X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc: + UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc: + WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc: + X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc: + X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc: + + GENERICS = [JAVA, *WINDOWS].freeze # :nodoc: + private_constant :GENERICS + + GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc: + private_constant :GENERIC_CACHE + + class << self + ## + # Returns the generic platform for the given platform. + + def generic(platform) + return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY + + GENERIC_CACHE[platform] ||= begin + found = GENERICS.find do |match| + platform === match + end + found || Gem::Platform::RUBY + end + end + + ## + # Returns the platform specificity match for the given spec platform and user platform. + + def platform_specificity_match(spec_platform, user_platform) + return -1 if spec_platform == user_platform + return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY + + os_match(spec_platform, user_platform) + + cpu_match(spec_platform, user_platform) * 10 + + version_match(spec_platform, user_platform) * 100 + end + + ## + # Sorts and filters the best platform match for the given matching specs and platform. + + def sort_and_filter_best_platform_match(matching, platform) + return matching if matching.one? + + exact = matching.select {|spec| spec.platform == platform } + return exact if exact.any? + + sorted_matching = sort_best_platform_match(matching, platform) + exemplary_spec = sorted_matching.first + + sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) } + end + + ## + # Sorts the best platform match for the given matching specs and platform. + + def sort_best_platform_match(matching, platform) + matching.sort_by.with_index do |spec, i| + [ + platform_specificity_match(spec.platform, platform), + i, # for stable sort + ] + end + end + + private + + def same_specificity?(platform, spec, exemplary_spec) + platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform) + end + + def same_deps?(spec, exemplary_spec) + spec.required_ruby_version == exemplary_spec.required_ruby_version && + spec.required_rubygems_version == exemplary_spec.required_rubygems_version && + spec.dependencies.sort == exemplary_spec.dependencies.sort + end + + def os_match(spec_platform, user_platform) + if spec_platform.os == user_platform.os + 0 + else + 1 + end + end + + def cpu_match(spec_platform, user_platform) + if spec_platform.cpu == user_platform.cpu + 0 + elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm") + 0 + elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal" + 1 + else + 2 + end + end + + def version_match(spec_platform, user_platform) + if spec_platform.version == user_platform.version + 0 + elsif spec_platform.version.nil? + 1 + else + 2 + end + end + end end diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb index 24857adb9d..8b4c425a33 100644 --- a/lib/rubygems/psych_tree.rb +++ b/lib/rubygems/psych_tree.rb @@ -22,7 +22,7 @@ module Gem def register(target, obj) end - # This is ported over from the yaml_tree in 1.9.3 + # This is ported over from the YAMLTree implementation in Ruby 1.9.3 def format_time(time) if time.utc? time.strftime("%Y-%m-%d %H:%M:%S.%9N Z") diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 4b5c74e0ea..5b83dc6f6f 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -72,7 +72,7 @@ class Gem::RemoteFetcher # +headers+: A set of additional HTTP headers to be sent to the server when # fetching the gem. - def initialize(proxy=nil, dns=nil, headers={}) + def initialize(proxy = nil, dns = nil, headers = {}) require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled require_relative "vendored_net_http" require_relative "vendor/uri/lib/uri" @@ -82,6 +82,7 @@ class Gem::RemoteFetcher @proxy = proxy @pools = {} @pool_lock = Thread::Mutex.new + @pool_size = 1 @cert_files = Gem::Request.get_cert_files @headers = headers @@ -110,9 +111,13 @@ class Gem::RemoteFetcher # always replaced. def download(spec, source_uri, install_dir = Gem.dir) + gem_file_name = File.basename spec.cache_file + install_cache_dir = File.join install_dir, "cache" cache_dir = - if Dir.pwd == install_dir # see fetch_command + if Gem.configuration.global_gem_cache + Gem.global_gem_cache_path + elsif Dir.pwd == install_dir # see fetch_command install_dir elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && !File.exist?(install_cache_dir)) install_cache_dir @@ -120,7 +125,6 @@ class Gem::RemoteFetcher File.join Gem.user_dir, "cache" end - gem_file_name = File.basename spec.cache_file local_gem_path = File.join cache_dir, gem_file_name require "fileutils" @@ -173,7 +177,7 @@ class Gem::RemoteFetcher end verbose "Using local gem #{local_gem_path}" - when nil then # TODO: test for local overriding cache + when nil then source_path = if Gem.win_platform? && source_uri.scheme && !source_uri.path.include?(":") "#{source_uri.scheme}:#{source_uri.path}" @@ -233,7 +237,9 @@ class Gem::RemoteFetcher fetch_http(location, last_modified, head, depth + 1) else - raise FetchError.new("bad response #{response.message} #{response.code}", uri) + custom_error = response["X-Error-Message"] + error_detail = custom_error || response.message + raise FetchError.new("Bad response #{error_detail} #{response.code}", uri) end end @@ -245,11 +251,14 @@ class Gem::RemoteFetcher def fetch_path(uri, mtime = nil, head = false) uri = Gem::Uri.new uri - unless uri.scheme - raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" - end + method = { + "http" => "fetch_http", + "https" => "fetch_http", + "s3" => "fetch_s3", + "file" => "fetch_file", + }.fetch(uri.scheme) { raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" } - data = send "fetch_#{uri.scheme}", uri, mtime, head + data = send method, uri, mtime, head if data && !head && uri.to_s.end_with?(".gz") begin @@ -267,7 +276,7 @@ class Gem::RemoteFetcher def fetch_s3(uri, mtime = nil, head = false) begin - public_uri = s3_uri_signer(uri).sign + public_uri = s3_uri_signer(uri, head ? "HEAD" : "GET").sign rescue Gem::S3URISigner::ConfigurationError, Gem::S3URISigner::InstanceProfileError => e raise FetchError.new(e.message, "s3://#{uri.host}") end @@ -275,8 +284,8 @@ class Gem::RemoteFetcher end # we have our own signing code here to avoid a dependency on the aws-sdk gem - def s3_uri_signer(uri) - Gem::S3URISigner.new(uri) + def s3_uri_signer(uri, method) + Gem::S3URISigner.new(uri, method) end ## @@ -335,7 +344,7 @@ class Gem::RemoteFetcher def pools_for(proxy) @pool_lock.synchronize do - @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files + @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files, @pool_size end end end diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index 9116785231..e817ee5704 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -2,6 +2,7 @@ require_relative "vendored_net_http" require_relative "user_interaction" +require_relative "uri_formatter" class Gem::Request extend Gem::UserInteraction diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb index 6c1b04ab65..01e7e0629a 100644 --- a/lib/rubygems/request/connection_pools.rb +++ b/lib/rubygems/request/connection_pools.rb @@ -7,11 +7,12 @@ class Gem::Request::ConnectionPools # :nodoc: attr_accessor :client end - def initialize(proxy_uri, cert_files) + def initialize(proxy_uri, cert_files, pool_size = 1) @proxy_uri = proxy_uri @cert_files = cert_files @pools = {} @pool_mutex = Thread::Mutex.new + @pool_size = pool_size end def pool_for(uri) @@ -20,9 +21,9 @@ class Gem::Request::ConnectionPools # :nodoc: @pool_mutex.synchronize do @pools[key] ||= if https? uri - Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri) + Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri, @pool_size) else - Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri) + Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri, @pool_size) end end end diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb index 52543de41f..468502ca6b 100644 --- a/lib/rubygems/request/http_pool.rb +++ b/lib/rubygems/request/http_pool.rb @@ -9,12 +9,14 @@ class Gem::Request::HTTPPool # :nodoc: attr_reader :cert_files, :proxy_uri - def initialize(http_args, cert_files, proxy_uri) + def initialize(http_args, cert_files, proxy_uri, pool_size) @http_args = http_args @cert_files = cert_files @proxy_uri = proxy_uri - @queue = Thread::SizedQueue.new 1 - @queue << nil + @pool_size = pool_size + + @queue = Thread::SizedQueue.new @pool_size + setup_queue end def checkout @@ -31,7 +33,8 @@ class Gem::Request::HTTPPool # :nodoc: connection.finish end end - @queue.push(nil) + + setup_queue end private @@ -44,4 +47,8 @@ class Gem::Request::HTTPPool # :nodoc: connection.start connection end + + def setup_queue + @pool_size.times { @queue.push(nil) } + end end diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 875df7e019..eb8b4658f3 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -181,13 +181,10 @@ class Gem::RequestSet # Install requested gems after they have been downloaded sorted_requests.each do |req| - if req.installed? - req.spec.spec.build_extensions - - if @always_install.none? {|spec| spec == req.spec.spec } - yield req, nil if block_given? - next - end + if req.installed? && @always_install.none? {|spec| spec == req.spec.spec } + req.spec.spec.build_extensions unless options[:build_extension] == false + yield req, nil if block_given? + next end spec = @@ -237,10 +234,6 @@ class Gem::RequestSet sorted_requests.each do |spec| puts " #{spec.full_name}" end - - if Gem.configuration.really_verbose - @resolver.stats.display - end else installed = install options, &block @@ -325,11 +318,8 @@ class Gem::RequestSet @git_set.root_dir = @install_dir lock_file = "#{File.expand_path(path)}.lock" - begin - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file lock_file - parser = tokenizer.make_parser self, [] - parser.parse - rescue Errno::ENOENT + if File.exist?(lock_file) + load_lockfile lock_file end gf = Gem::RequestSet::GemDependencyAPI.new self, path @@ -338,6 +328,63 @@ class Gem::RequestSet gf.load end + def load_lockfile(lock_file) # :nodoc: + require "bundler" + require "bundler/lockfile_parser" + + # Bundler::Source::Path resolves relative `remote:` paths against + # Bundler.root, which raises when there is no Gemfile in the working + # directory. Anchor it to the lockfile's directory so PATH sections in a + # `gem install -g` lockfile can be parsed without a Bundler environment. + previous_root = Bundler.instance_variable_get(:@root) + Bundler.instance_variable_set(:@root, Pathname.new(File.expand_path(File.dirname(lock_file)))) + + parser = Bundler::LockfileParser.new(File.read(lock_file), lockfile_path: lock_file) + + parser.specs.group_by(&:source).each do |source, specs| + case source + when Bundler::Source::Rubygems + remotes = source.remotes.map {|remote| Gem::Source.new(remote.to_s) } + remotes << Gem::Source.new(Gem::DEFAULT_HOST) if remotes.empty? + lock_set = Gem::Resolver::LockSet.new(remotes) + specs.each do |spec| + added = lock_set.add(spec.name, spec.version.to_s, spec.platform) + spec.dependencies.each do |dep| + added.each {|s| s.add_dependency dep } + end + end + @sets << lock_set + when Bundler::Source::Git + git_set = Gem::Resolver::GitSet.new + git_set.root_dir = @install_dir + specs.each do |spec| + git_spec = git_set.add_git_spec( + spec.name, + spec.version.to_s, + source.uri.to_s, + source.revision, + source.submodules || false + ) + spec.dependencies.each {|dep| git_spec.add_dependency dep } + end + @sets << git_set + when Bundler::Source::Path + vendor_set = Gem::Resolver::VendorSet.new + specs.each do |spec| + loaded = vendor_set.add_vendor_gem(spec.name, source.path.to_s) + spec.dependencies.each {|dep| loaded.dependencies << dep } + end + @sets << vendor_set + end + end + + parser.dependencies.each_value do |dep| + gem dep.name, *dep.requirement.as_list + end + ensure + Bundler.instance_variable_set(:@root, previous_root) if defined?(previous_root) + end + def pretty_print(q) # :nodoc: q.group 2, "[RequestSet:", "]" do q.breakable @@ -465,4 +512,3 @@ end require_relative "request_set/gem_dependency_api" require_relative "request_set/lockfile" -require_relative "request_set/lockfile/tokenizer" diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb index c446b3ae51..8b9c9690d6 100644 --- a/lib/rubygems/request_set/lockfile.rb +++ b/lib/rubygems/request_set/lockfile.rb @@ -38,7 +38,7 @@ class Gem::RequestSet::Lockfile end ## - # Creates a new Lockfile for the given +request_set+ and +gem_deps_file+ + # Creates a new Lockfile for the given Gem::RequestSet and +gem_deps_file+ # location. def self.build(request_set, gem_deps_file, dependencies = nil) @@ -231,5 +231,3 @@ class Gem::RequestSet::Lockfile @set.sorted_requests end end - -require_relative "lockfile/tokenizer" diff --git a/lib/rubygems/request_set/lockfile/parser.rb b/lib/rubygems/request_set/lockfile/parser.rb deleted file mode 100644 index e751a1445e..0000000000 --- a/lib/rubygems/request_set/lockfile/parser.rb +++ /dev/null @@ -1,344 +0,0 @@ -# frozen_string_literal: true - -class Gem::RequestSet::Lockfile::Parser - ### - # Parses lockfiles - - def initialize(tokenizer, set, platforms, filename = nil) - @tokens = tokenizer - @filename = filename - @set = set - @platforms = platforms - end - - def parse - until @tokens.empty? do - token = get - - case token.type - when :section then - @tokens.skip :newline - - case token.value - when "DEPENDENCIES" then - parse_DEPENDENCIES - when "GIT" then - parse_GIT - when "GEM" then - parse_GEM - when "PATH" then - parse_PATH - when "PLATFORMS" then - parse_PLATFORMS - else - token = get until @tokens.empty? || peek.first == :section - end - else - raise "BUG: unhandled token #{token.type} (#{token.value.inspect}) at line #{token.line} column #{token.column}" - end - end - end - - ## - # Gets the next token for a Lockfile - - def get(expected_types = nil, expected_value = nil) # :nodoc: - token = @tokens.shift - - if expected_types && !Array(expected_types).include?(token.type) - unget token - - message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " \ - "expected #{expected_types.inspect}" - - raise Gem::RequestSet::Lockfile::ParseError.new message, token.column, token.line, @filename - end - - if expected_value && expected_value != token.value - unget token - - message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " \ - "expected [#{expected_types.inspect}, " \ - "#{expected_value.inspect}]" - - raise Gem::RequestSet::Lockfile::ParseError.new message, token.column, token.line, @filename - end - - token - end - - def parse_DEPENDENCIES # :nodoc: - while !@tokens.empty? && peek.type == :text do - token = get :text - - requirements = [] - - case peek[0] - when :bang then - get :bang - - requirements << pinned_requirement(token.value) - when :l_paren then - get :l_paren - - loop do - op = get(:requirement).value - version = get(:text).value - - requirements << "#{op} #{version}" - - break unless peek.type == :comma - - get :comma - end - - get :r_paren - - if peek[0] == :bang - requirements.clear - requirements << pinned_requirement(token.value) - - get :bang - end - end - - @set.gem token.value, *requirements - - skip :newline - end - end - - def parse_GEM # :nodoc: - sources = [] - - while peek.first(2) == [:entry, "remote"] do - get :entry, "remote" - data = get(:text).value - skip :newline - - sources << Gem::Source.new(data) - end - - sources << Gem::Source.new(Gem::DEFAULT_HOST) if sources.empty? - - get :entry, "specs" - - skip :newline - - set = Gem::Resolver::LockSet.new sources - last_specs = nil - - while !@tokens.empty? && peek.type == :text do - token = get :text - name = token.value - column = token.column - - case peek[0] - when :newline then - last_specs.each do |spec| - spec.add_dependency Gem::Dependency.new name if column == 6 - end - when :l_paren then - get :l_paren - - token = get [:text, :requirement] - type = token.type - data = token.value - - if type == :text && column == 4 - version, platform = data.split "-", 2 - - platform = - platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY - - last_specs = set.add name, version, platform - else - dependency = parse_dependency name, data - - last_specs.each do |spec| - spec.add_dependency dependency - end - end - - get :r_paren - else - raise "BUG: unknown token #{peek}" - end - - skip :newline - end - - @set.sets << set - end - - def parse_GIT # :nodoc: - get :entry, "remote" - repository = get(:text).value - - skip :newline - - get :entry, "revision" - revision = get(:text).value - - skip :newline - - type = peek.type - value = peek.value - if type == :entry && %w[branch ref tag].include?(value) - get - get :text - - skip :newline - end - - get :entry, "specs" - - skip :newline - - set = Gem::Resolver::GitSet.new - set.root_dir = @set.install_dir - - last_spec = nil - - while !@tokens.empty? && peek.type == :text do - token = get :text - name = token.value - column = token.column - - case peek[0] - when :newline then - last_spec.add_dependency Gem::Dependency.new name if column == 6 - when :l_paren then - get :l_paren - - token = get [:text, :requirement] - type = token.type - data = token.value - - if type == :text && column == 4 - last_spec = set.add_git_spec name, data, repository, revision, true - else - dependency = parse_dependency name, data - - last_spec.add_dependency dependency - end - - get :r_paren - else - raise "BUG: unknown token #{peek}" - end - - skip :newline - end - - @set.sets << set - end - - def parse_PATH # :nodoc: - get :entry, "remote" - directory = get(:text).value - - skip :newline - - get :entry, "specs" - - skip :newline - - set = Gem::Resolver::VendorSet.new - last_spec = nil - - while !@tokens.empty? && peek.first == :text do - token = get :text - name = token.value - column = token.column - - case peek[0] - when :newline then - last_spec.add_dependency Gem::Dependency.new name if column == 6 - when :l_paren then - get :l_paren - - token = get [:text, :requirement] - type = token.type - data = token.value - - if type == :text && column == 4 - last_spec = set.add_vendor_gem name, directory - else - dependency = parse_dependency name, data - - last_spec.dependencies << dependency - end - - get :r_paren - else - raise "BUG: unknown token #{peek}" - end - - skip :newline - end - - @set.sets << set - end - - def parse_PLATFORMS # :nodoc: - while !@tokens.empty? && peek.first == :text do - name = get(:text).value - - @platforms << name - - skip :newline - end - end - - ## - # Parses the requirements following the dependency +name+ and the +op+ for - # the first token of the requirements and returns a Gem::Dependency object. - - def parse_dependency(name, op) # :nodoc: - return Gem::Dependency.new name, op unless peek[0] == :text - - version = get(:text).value - - requirements = ["#{op} #{version}"] - - while peek.type == :comma do - get :comma - op = get(:requirement).value - version = get(:text).value - - requirements << "#{op} #{version}" - end - - Gem::Dependency.new name, requirements - end - - private - - def skip(type) # :nodoc: - @tokens.skip type - end - - ## - # Peeks at the next token for Lockfile - - def peek # :nodoc: - @tokens.peek - end - - def pinned_requirement(name) # :nodoc: - requirement = Gem::Dependency.new name - specification = @set.sets.flat_map do |set| - set.find_all(requirement) - end.compact.first - - specification&.version - end - - ## - # Ungets the last token retrieved by #get - - def unget(token) # :nodoc: - @tokens.unshift token - end -end diff --git a/lib/rubygems/request_set/lockfile/tokenizer.rb b/lib/rubygems/request_set/lockfile/tokenizer.rb deleted file mode 100644 index 65cef3baa0..0000000000 --- a/lib/rubygems/request_set/lockfile/tokenizer.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -# ) frozen_string_literal: true -require_relative "parser" - -class Gem::RequestSet::Lockfile::Tokenizer - Token = Struct.new :type, :value, :column, :line - EOF = Token.new :EOF - - def self.from_file(file) - new File.read(file), file - end - - def initialize(input, filename = nil, line = 0, pos = 0) - @line = line - @line_pos = pos - @tokens = [] - @filename = filename - tokenize input - end - - def make_parser(set, platforms) - Gem::RequestSet::Lockfile::Parser.new self, set, platforms, @filename - end - - def to_a - @tokens.map {|token| [token.type, token.value, token.column, token.line] } - end - - def skip(type) - @tokens.shift while !@tokens.empty? && peek.type == type - end - - ## - # Calculates the column (by byte) and the line of the current token based on - # +byte_offset+. - - def token_pos(byte_offset) # :nodoc: - [byte_offset - @line_pos, @line] - end - - def empty? - @tokens.empty? - end - - def unshift(token) - @tokens.unshift token - end - - def next_token - @tokens.shift - end - alias_method :shift, :next_token - - def peek - @tokens.first || EOF - end - - private - - def tokenize(input) - require "strscan" - s = StringScanner.new input - - until s.eos? do - pos = s.pos - - pos = s.pos if leading_whitespace = s.scan(/ +/) - - if s.scan(/[<|=>]{7}/) - message = "your #{@filename} contains merge conflict markers" - column, line = token_pos pos - - raise Gem::RequestSet::Lockfile::ParseError.new message, column, line, @filename - end - - @tokens << - if s.scan(/\r?\n/) - - token = Token.new(:newline, nil, *token_pos(pos)) - @line_pos = s.pos - @line += 1 - token - elsif s.scan(/[A-Z]+/) - - if leading_whitespace - text = s.matched - text += s.scan(/[^\s)]*/).to_s # in case of no match - Token.new(:text, text, *token_pos(pos)) - else - Token.new(:section, s.matched, *token_pos(pos)) - end - elsif s.scan(/([a-z]+):\s/) - - s.pos -= 1 # rewind for possible newline - Token.new(:entry, s[1], *token_pos(pos)) - elsif s.scan(/\(/) - - Token.new(:l_paren, nil, *token_pos(pos)) - elsif s.scan(/\)/) - - Token.new(:r_paren, nil, *token_pos(pos)) - elsif s.scan(/<=|>=|=|~>|<|>|!=/) - - Token.new(:requirement, s.matched, *token_pos(pos)) - elsif s.scan(/,/) - - Token.new(:comma, nil, *token_pos(pos)) - elsif s.scan(/!/) - - Token.new(:bang, nil, *token_pos(pos)) - elsif s.scan(/[^\s),!]*/) - - Token.new(:text, s.matched, *token_pos(pos)) - else - raise "BUG: can't create token for: #{s.string[s.pos..-1].inspect}" - end - end - - @tokens - end -end diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 35d83abd2d..788206c056 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -2,7 +2,6 @@ require_relative "dependency" require_relative "exceptions" -require_relative "util/list" ## # Given a set of Gem::Dependency objects as +needed+ and a way to query the @@ -11,7 +10,7 @@ require_relative "util/list" # all the requirements. class Gem::Resolver - require_relative "vendored_molinillo" + require_relative "vendored_pub_grub" ## # If the DEBUG_RESOLVER environment variable is set then debugging mode is @@ -36,11 +35,6 @@ class Gem::Resolver attr_accessor :ignore_dependencies ## - # List of dependencies that could not be found in the configured sources. - - attr_reader :stats - - ## # Hash of gems to skip resolution. Keyed by gem name, with arrays of # gem specifications as values. @@ -105,219 +99,449 @@ class Gem::Resolver @ignore_dependencies = false @skip_gems = {} @soft_missing = false - @stats = Gem::Resolver::Stats.new - end - def explain(stage, *data) # :nodoc: - return unless DEBUG_RESOLVER + @root_package = RootPackage.new + @root_version = Gem::PubGrub::Package.root_version + + @packages = {} - d = data.map(&:pretty_inspect).join(", ") - $stderr.printf "%10s %s\n", stage.to_s.upcase, d + @unfiltered_specs = Hash.new {|h, name| h[name] = find_unfiltered_specs_for(name) } + @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) } + @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } + @sorted_versions = Hash.new do |h, pkg| + h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : @all_versions[pkg] + end + @cached_dependencies = Hash.new do |h, pkg| + h[pkg] = if Gem::PubGrub::Package.root?(pkg) + { @root_version => root_dependencies } + else + Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } + end + end + @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h } + @versions_for_cache = Hash.new {|h, pkg| h[pkg] = {} } + @spec_for_cache = Hash.new {|h, name| h[name] = build_spec_for_cache(name) } end - def explain_list(stage) # :nodoc: - return unless DEBUG_RESOLVER + ## + # Proceed with resolution! Returns an array of ActivationRequest objects. + + def resolve + # Pre-check: raise UnsatisfiableDependencyError for root deps with no + # platform match. We filter by platform ONLY here (not required_ruby_version + # / required_rubygems_version): a foreign-platform gem is genuinely "not + # found", but a gem that exists yet is incompatible with the running Ruby + # should flow through the solver to a DependencyResolutionError that names + # the Ruby requirement. That matches Bundler (which models Ruby as a + # synthetic dependency, so this surfaces as a solve failure) and gives a + # clearer message than the platform-oriented UnsatisfiableDependencyError. + @needed.each do |dep| + next if @soft_missing + dep_request = DependencyRequest.new(dep, nil) + all = @set.find_all(dep_request) + matching = select_local_platforms(all) + + next unless matching.empty? + + exc = Gem::UnsatisfiableDependencyError.new(dep_request, all) + exc.errors = @set.errors + raise exc + end - data = yield - $stderr.printf "%10s (%d entries)\n", stage.to_s.upcase, data.size - unless data.empty? - require "pp" - PP.pp data, $stderr + solver = Gem::PubGrub::VersionSolver.new( + source: self, + root: @root_package, + strategy: Gem::Resolver::Strategy.new(self), + logger: make_logger + ) + result = solver.solve + + # Convert to Array<ActivationRequest> + needed_by_name = @needed.group_by(&:name) + result.filter_map do |package, version| + next if Gem::PubGrub::Package.root?(package) + spec = spec_for(package.to_s, version) + dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s) + dep_request = DependencyRequest.new(dep, nil) + ActivationRequest.new(spec, dep_request) + end + rescue Gem::PubGrub::SolveFailure => e + extended = extract_extended_explanation(e.incompatibility) + if extended + message = "#{e.explanation}\n\n#{extended}" + raise Gem::DependencyResolutionError, Struct.new(:explanation).new(message) + else + raise Gem::DependencyResolutionError, e end end - ## - # Creates an ActivationRequest for the given +dep+ and the last +possible+ - # specification. - # - # Returns the Specification and the ActivationRequest + # PubGrub source interface methods + + def all_versions_for(package) + versions = @sorted_versions[package].reverse # highest first + name = package.to_s + + if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty? + # Conservative mode: float the already-installed (skip) versions to the + # front so the solver prefers them. This sets *preference* only (it feeds + # the strategy's version-index map); it does not restrict availability, so + # every version stays selectable via versions_for. When an installed + # version is made impossible by a downstream conflict, the solver + # backtracks to a newer version instead of failing. Molinillo instead + # hard-restricted the candidate set to skip versions and raised. + # + # This reaches the same outcome as Bundler (upgrade-over-raise) for the + # common single-blocked-gem case, though the mechanism differs: Bundler + # hard-pins locked gems and selectively unlocks + re-solves on conflict, + # whereas we float as a preference and let PubGrub backtrack in one solve. + # The float can therefore over-upgrade when several installed gems are + # jointly involved in a conflict; that outcome-level divergence is + # accepted (see test_conservative_upgrades_when_installed_blocked). + skip_versions = skip_dep_gems.map(&:version) + preferred, rest = versions.partition {|v| skip_versions.include?(v) } + preferred + rest + else + # Prefer already-installed versions to avoid unnecessary upgrades + installed_versions = @all_specs[name]. + select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }. + map(&:version) + if installed_versions.any? + preferred, rest = versions.partition {|v| installed_versions.include?(v) } + preferred + rest + else + versions + end + end + end + + def versions_for(package, range = Gem::PubGrub::VersionRange.any) + @versions_for_cache[package][range] ||= begin + candidates = range.select_versions(@sorted_versions[package]) + + if Gem::PubGrub::Package.root?(package) || + (@set.respond_to?(:prerelease) && @set.prerelease) || + range_admits_prerelease?(range) + candidates + elsif @all_versions[package].any? {|v| !v.prerelease? } + candidates.reject(&:prerelease?) + else + # Only prereleases exist for this gem; fall back to them so + # dependencies like `>= 1.0` can still be satisfied. + candidates + end + end + end - def activation_request(dep, possible) # :nodoc: - spec = possible.pop + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term) - explain :activate, [spec.full_name, possible.size] - explain :possible, possible + name = unsatisfied_term.package.to_s + constraint = unsatisfied_term.constraint + extended_explanation = build_extended_explanation(name, constraint) - activation_request = - Gem::Resolver::ActivationRequest.new spec, dep, possible + custom_explanation = if extended_explanation + "#{constraint} could not be found in any repository" + end - [spec, activation_request] + Gem::Resolver::Incompatibility.new( + [unsatisfied_term], + cause: cause, + custom_explanation: custom_explanation, + extended_explanation: extended_explanation + ) end - def requests(s, act, reqs=[]) # :nodoc: - return reqs if @ignore_dependencies + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].filter_map do |dep_package_name, dep_constraint| + dep_package = dep_constraint.package - s.fetch_development_dependencies if @development + low = high = @version_to_index[package][version] - s.dependencies.reverse_each do |d| - next if d.type == :development && !@development - next if d.type == :development && @development_shallow && - act.development? - next if d.type == :development && @development_shallow && - act.parent + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package_name] == dep_constraint + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package_name] == dep_constraint + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?) + self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range) + + # No specs anywhere means an unknown package. Check @unfiltered_specs, not + # the filtered set, so a dep filtered out by platform/Ruby/prerelease falls + # through to NoVersions for proper hints instead. The band-scoped + # self_constraint lets clean sibling versions still resolve via backtracking. + if @unfiltered_specs[dep_package_name].empty? + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + self_term = Gem::PubGrub::Term.new(self_constraint, true) + # PubGrub's default InvalidDependency rendering drops the version + # requirement ("depends on unknown package bar"). Supply a custom + # explanation so the missing dependency's constraint is preserved + # ("depends on bar = 0.5 which could not be found in any repository"), + # matching Molinillo's diagnostics. + return [Gem::PubGrub::Incompatibility.new( + [self_term], + cause: cause, + custom_explanation: "#{self_term.to_s(allow_every: true)} depends on #{dep_constraint} which could not be found in any repository" + )] + end - reqs << Gem::Resolver::DependencyRequest.new(d, act) - @stats.requirement! + # An empty range means the requirement is self-contradictory (e.g. `> 2, < 1`). + if dep_constraint.range.empty? + return [Gem::Resolver::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint), + custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}" + )] + end + + Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true), Gem::PubGrub::Term.new(dep_constraint, false)], + cause: :dependency + ) end + end + + ## + # Returns the gems in +specs+ that match the local platform. - @set.prefetch reqs + def select_local_platforms(specs) # :nodoc: + specs.select do |spec| + Gem::Platform.installable? spec + end + end - @stats.record_requirements reqs + private - reqs + def package_for(name) + @packages[name] ||= Gem::PubGrub::Package.new(name) end - include Gem::Molinillo::UI + def root_dependencies + deps = {} + @needed.each do |dep| + constraint = Gem::PubGrub::RubyGems.requirement_to_constraint(package_for(dep.name), dep.requirement) + deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint + end + deps + end - def output - @output ||= debug? ? $stdout : File.open(IO::NULL, "w") + # Only the min bound is inspected: `~>` synthesises a max like `X.A` + # whose suffix looks prerelease to Gem::Version but is not the user's + # intent, so checking max would mis-admit prereleases for every `~>`. + def range_admits_prerelease?(range) + range.ranges.any? do |r| + next false if r.empty? + r.min&.prerelease? + end end - def debug? - DEBUG_RESOLVER + def find_unfiltered_specs_for(name) + dep = Gem::Dependency.new(name, ">= 0.a") + dep_request = DependencyRequest.new(dep, nil) + @set.find_all(dep_request) end - include Gem::Molinillo::SpecificationProvider + def filter_specs(specs) + filtered = select_local_platforms(specs) - ## - # Proceed with resolution! Returns an array of ActivationRequest objects. + unless @soft_missing + filtered = filtered.select do |s| + s.required_ruby_version.satisfied_by?(Gem.ruby_version) && + s.required_rubygems_version.satisfied_by?(Gem.rubygems_version) + rescue StandardError + true + end + end - def resolve - Gem::Molinillo::Resolver.new(self, self).resolve(@needed.map {|d| DependencyRequest.new d, nil }).tsort.filter_map(&:payload) - rescue Gem::Molinillo::VersionConflict => e - conflict = e.conflicts.values.first - raise Gem::DependencyResolutionError, Conflict.new(conflict.requirement_trees.first.first, conflict.existing, conflict.requirement) - ensure - @output.close if defined?(@output) && !debug? + filtered end - ## - # Extracts the specifications that may be able to fulfill +dependency+ and - # returns those that match the local platform and all those that match. + def spec_for(name, version) + @spec_for_cache[name][version] + end - def find_possible(dependency) # :nodoc: - all = @set.find_all dependency + def build_spec_for_cache(name) + # Rank sources by the order they were first supplied so that, when multiple + # sources offer the same version and platform, the earlier source wins. + source_rank = {} + @all_specs[name].each do |s| + source_rank[s.source] ||= source_rank.size + end - if (skip_dep_gems = skip_gems[dependency.name]) && !skip_dep_gems.empty? - matching = all.select do |api_spec| - skip_dep_gems.any? {|s| api_spec.version == s.version } - end + @all_specs[name].group_by(&:version).transform_values do |candidates| + next candidates.first if candidates.length == 1 + + # Prefer already-installed specs to avoid unnecessary downloads + installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) } + next installed.first if installed.length == 1 + candidates = installed if installed.any? - all = matching unless matching.empty? + # Among remaining candidates, prefer the most specific platform, then the + # earlier-supplied source. + candidates.min_by do |s| + [Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local), + source_rank[s.source]] + end end + end - matching_platform = select_local_platforms all + def compute_dependencies(package, version) + spec = spec_for(package.to_s, version) + return {} unless spec + return {} if @ignore_dependencies - [matching_platform, all] - end + spec.fetch_development_dependencies if @development && spec.respond_to?(:fetch_development_dependencies) - ## - # Returns the gems in +specs+ that match the local platform. + deps = {} + root_names = @needed.map(&:name) - def select_local_platforms(specs) # :nodoc: - specs.select do |spec| - Gem::Platform.installable? spec + spec.dependencies.each do |d| + next if d.name == package.to_s + next if d.type == :development && !@development + next if d.type == :development && @development_shallow && !root_names.include?(package.to_s) + + dep_package = package_for(d.name) + + # In force mode, skip deps that can't be satisfied - either no + # specs at all, or no specs matching the version requirement. + if @soft_missing + dep_specs = @all_specs[d.name] + matching = dep_specs.select {|s| d.requirement.satisfied_by?(s.version) } + next if matching.empty? + end + + deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement) end + + deps end - def search_for(dependency) - possibles, all = find_possible(dependency) - if !@soft_missing && possibles.empty? - exc = Gem::UnsatisfiableDependencyError.new dependency, all - exc.errors = @set.errors - raise exc - end + def build_extended_explanation(name, constraint) + unfiltered = @unfiltered_specs[name] + return if unfiltered.empty? + + filtered = @all_specs[name] + pkg = package_for(name) - groups = Hash.new {|hash, key| hash[key] = [] } + # A prerelease hint applies when the source would strip prereleases for + # this constraint (global prerelease flag off and the constraint's range + # doesn't itself reach into prerelease territory) AND a prerelease of + # the gem exists somewhere. + prerelease_gated = !(@set.respond_to?(:prerelease) && @set.prerelease) && + !range_admits_prerelease?(constraint.range) + has_prerelease_candidate = prerelease_gated && + @all_versions[pkg].any?(&:prerelease?) - # create groups & sources in the same loop - sources = possibles.map do |spec| - source = spec.source - groups[source] << spec - source - end.uniq.reverse + return if filtered.length == unfiltered.length && !has_prerelease_candidate - activation_requests = [] + hints = [] - sources.each do |source| - groups[source]. - sort_by {|spec| [spec.version, spec.platform =~ Gem::Platform.local ? 1 : 0] }. # rubocop:disable Performance/RegexpMatch - map {|spec| ActivationRequest.new spec, dependency }. - each {|activation_request| activation_requests << activation_request } + # Check for specs that exist for other platforms + platform_specs = unfiltered.select do |s| + !Gem::Platform.installable?(s) && constraint.range.include?(s.version) + end + if platform_specs.any? + label = "#{name} (#{constraint.constraint_string})" + hints << "The source contains the following gems matching '#{label}':" + platform_specs.each do |s| + actual = s.respond_to?(:spec) ? s.spec : s + hints << " * #{actual.full_name}" + end end - activation_requests - end + # Check for specs filtered by Ruby version + installable = select_local_platforms(unfiltered) + ruby_specs = installable.select do |s| + actual = s.respond_to?(:spec) ? s.spec : s + constraint.range.include?(s.version) && + !actual.required_ruby_version.satisfied_by?(Gem.ruby_version) + rescue StandardError + false + end + if ruby_specs.any? + versions = ruby_specs.map(&:version).uniq.sort.reverse.first(3) + sample = ruby_specs.find {|s| s.version == versions.first } + actual = sample.respond_to?(:spec) ? sample.spec : sample + ruby_req = actual.required_ruby_version + hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})" + end + + # Check for specs filtered by prerelease status + if prerelease_gated + prerelease_versions = @all_versions[pkg].select(&:prerelease?) + if prerelease_versions.any? + versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output + hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems." + end + end - def dependencies_for(specification) - return [] if @ignore_dependencies - spec = specification.spec - requests(spec, specification) + hints.empty? ? nil : hints.join("\n") end - def requirement_satisfied_by?(requirement, activated, spec) - matches_spec = requirement.matches_spec? spec - return matches_spec if @soft_missing + def extract_extended_explanation(incompatibility) + while incompatibility.cause.is_a?(Gem::PubGrub::Incompatibility::ConflictCause) + cause = incompatibility.cause - matches_spec && - spec.spec.required_ruby_version.satisfied_by?(Gem.ruby_version) && - spec.spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version) - end + [cause.conflict, cause.other].each do |incompat| + if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) && + incompat.respond_to?(:extended_explanation) && + incompat.extended_explanation + return incompat.extended_explanation + end + end + + incompatibility = cause.conflict + end - def name_for(dependency) - dependency.name + nil end - def allow_missing?(dependency) - @soft_missing + def make_logger + DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new end - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by.with_index do |dependency, i| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - amount_constrained(dependency), - conflicts[name] ? 0 : 1, - activated.vertex_named(name).payload ? 0 : search_for(dependency).count, - i, # for stable sort - ] + # Custom root package so error messages say "your request depends on..." + # instead of PubGrub's default "root depends on...". + class RootPackage < Gem::PubGrub::Package + def initialize + super(:root) end - end - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY = 1_000_000 - private_constant :SINGLE_POSSIBILITY_CONSTRAINT_PENALTY if defined?(private_constant) + def root? + true + end - # returns an integer \in (-\infty, 0] - # a number closer to 0 means the dependency is less constraining - # - # dependencies w/ 0 or 1 possibilities (ignoring version requirements) - # are given very negative values, so they _always_ sort first, - # before dependencies that are unconstrained - def amount_constrained(dependency) - @amount_constrained ||= {} - @amount_constrained[dependency.name] ||= begin - name_dependency = Gem::Dependency.new(dependency.name) - dependency_request_for_name = Gem::Resolver::DependencyRequest.new(name_dependency, dependency.requester) - all = @set.find_all(dependency_request_for_name).size - - if all <= 1 - all - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY - else - search = search_for(dependency).size - search - all - end + def to_s + "your request" end end - private :amount_constrained end require_relative "resolver/activation_request" -require_relative "resolver/conflict" require_relative "resolver/dependency_request" +require_relative "resolver/incompatibility" +require_relative "resolver/strategy" require_relative "resolver/requirement_list" -require_relative "resolver/stats" - require_relative "resolver/set" require_relative "resolver/api_set" require_relative "resolver/composed_set" diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb index 9f6695a6a9..3f443519d8 100644 --- a/lib/rubygems/resolver/api_set.rb +++ b/lib/rubygems/resolver/api_set.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true ## -# The global rubygems pool, available via the rubygems.org API. +# The global rubygems pool, available via the Compact Index API. # Returns instances of APISpecification. class Gem::Resolver::APISet < Gem::Resolver::Set autoload :GemParser, File.expand_path("api_set/gem_parser", __dir__) ## - # The URI for the dependency API this APISet uses. + # The URI for the Compact Index API this APISet uses. attr_reader :dep_uri # :nodoc: @@ -23,9 +23,9 @@ class Gem::Resolver::APISet < Gem::Resolver::Set attr_reader :uri ## - # Creates a new APISet that will retrieve gems from +uri+ using the RubyGems - # API URL +dep_uri+ which is described at - # https://guides.rubygems.org/rubygems-org-api + # Creates a new APISet that will retrieve gems from +uri+ using the Compact + # Index API URL +dep_uri+ which is described at + # https://guides.rubygems.org/rubygems-org-compact-index-api def initialize(dep_uri = "https://index.rubygems.org/info/") super() diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb index 643b857107..4d827f4980 100644 --- a/lib/rubygems/resolver/api_set/gem_parser.rb +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -1,22 +1,19 @@ # frozen_string_literal: true class Gem::Resolver::APISet::GemParser - EMPTY_ARRAY = [].freeze - private_constant :EMPTY_ARRAY - def parse(line) version_and_platform, rest = line.split(" ", 2) version, platform = version_and_platform.split("-", 2) dependencies, requirements = rest.split("|", 2).map! {|s| s.split(",") } if rest - dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : EMPTY_ARRAY - requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : EMPTY_ARRAY + dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : [] + requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : [] [version, platform, dependencies, requirements] end private def parse_dependency(string) - dependency = string.split(":") + dependency = string.split(":", 2) dependency[-1] = dependency[-1].split("&") if dependency.size > 1 dependency[0] = -dependency[0] dependency diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb index a14bcbfeb1..ccfd6fe084 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true ## -# Represents a specification retrieved via the rubygems.org API. +# Represents a specification retrieved via the Compact Index API. # # This is used to avoid loading the full Specification object when all we need # is the name, version, and dependencies. @@ -19,10 +19,10 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification end ## - # Creates an APISpecification for the given +set+ from the rubygems.org + # Creates an APISpecification for the given +set+ from the Compact Index API # +api_data+. # - # See https://guides.rubygems.org/rubygems-org-api/#misc-methods for the + # See https://guides.rubygems.org/rubygems-org-compact-index-api for the # format of the +api_data+. def initialize(set, api_data) diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index e2307f6e02..e647a2c11b 100644 --- a/lib/rubygems/resolver/best_set.rb +++ b/lib/rubygems/resolver/best_set.rb @@ -21,7 +21,7 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet def pick_sets # :nodoc: @sources.each_source do |source| - @sets << source.dependency_resolver_set + @sets << source.dependency_resolver_set(@prerelease) end end diff --git a/lib/rubygems/resolver/conflict.rb b/lib/rubygems/resolver/conflict.rb deleted file mode 100644 index 367a36b43d..0000000000 --- a/lib/rubygems/resolver/conflict.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -## -# Used internally to indicate that a dependency conflicted -# with a spec that would be activated. - -class Gem::Resolver::Conflict - ## - # The specification that was activated prior to the conflict - - attr_reader :activated - - ## - # The dependency that is in conflict with the activated gem. - - attr_reader :dependency - - attr_reader :failed_dep # :nodoc: - - ## - # Creates a new resolver conflict when +dependency+ is in conflict with an - # already +activated+ specification. - - def initialize(dependency, activated, failed_dep=dependency) - @dependency = dependency - @activated = activated - @failed_dep = failed_dep - end - - def ==(other) # :nodoc: - self.class === other && - @dependency == other.dependency && - @activated == other.activated && - @failed_dep == other.failed_dep - end - - ## - # A string explanation of the conflict. - - def explain - "<Conflict wanted: #{@failed_dep}, had: #{activated.spec.full_name}>" - end - - ## - # Return the 2 dependency objects that conflicted - - def conflicting_dependencies - [@failed_dep.dependency, @activated.request.dependency] - end - - ## - # Explanation of the conflict used by exceptions to print useful messages - - def explanation - activated = @activated.spec.full_name - dependency = @failed_dep.dependency - requirement = dependency.requirement - alternates = dependency.matching_specs.map(&:full_name) - - unless alternates.empty? - matching = <<-MATCHING.chomp - - Gems matching %s: - %s - MATCHING - - matching = format(matching, dependency, alternates.join(", ")) - end - - explanation = <<-EXPLANATION - Activated %s - which does not match conflicting dependency (%s) - - Conflicting dependency chains: - %s - - versus: - %s -%s - EXPLANATION - - format(explanation, activated, requirement, request_path(@activated).reverse.join(", depends on\n "), request_path(@failed_dep).reverse.join(", depends on\n "), matching) - end - - ## - # Returns true if the conflicting dependency's name matches +spec+. - - def for_spec?(spec) - @dependency.name == spec.name - end - - def pretty_print(q) # :nodoc: - q.group 2, "[Dependency conflict: ", "]" do - q.breakable - - q.text "activated " - q.pp @activated - - q.breakable - q.text " dependency " - q.pp @dependency - - q.breakable - if @dependency == @failed_dep - q.text " failed" - else - q.text " failed dependency " - q.pp @failed_dep - end - end - end - - ## - # Path of activations from the +current+ list. - - def request_path(current) - path = [] - - while current do - case current - when Gem::Resolver::ActivationRequest then - path << - "#{current.request.dependency}, #{current.spec.version} activated" - - current = current.parent - when Gem::Resolver::DependencyRequest then - path << current.dependency.to_s - - current = current.requester - else - raise Gem::Exception, "[BUG] unknown request class #{current.class}" - end - end - - path = ["user request (gem command or Gemfile)"] if path.empty? - - path - end - - ## - # Return the Specification that listed the dependency - - def requester - @failed_dep.requester - end -end diff --git a/lib/rubygems/resolver/incompatibility.rb b/lib/rubygems/resolver/incompatibility.rb new file mode 100644 index 0000000000..57a60affb4 --- /dev/null +++ b/lib/rubygems/resolver/incompatibility.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Gem::Resolver::Incompatibility < Gem::PubGrub::Incompatibility + attr_reader :extended_explanation + + def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil) + @extended_explanation = extended_explanation + super(terms, cause: cause, custom_explanation: custom_explanation) + end +end diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index d9fe36c589..42ce0890e2 100644 --- a/lib/rubygems/resolver/installer_set.rb +++ b/lib/rubygems/resolver/installer_set.rb @@ -160,7 +160,7 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set res.concat matching_local begin - if local_spec = @local_source.find_gem(name, dep.requirement) + @local_source.find_all_gems(name, dep.requirement).each do |local_spec| res << Gem::Resolver::IndexSpecification.new( self, local_spec.name, local_spec.version, @local_source, local_spec.platform diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb index 296cf41078..074b473edc 100644 --- a/lib/rubygems/resolver/source_set.rb +++ b/lib/rubygems/resolver/source_set.rb @@ -42,6 +42,6 @@ class Gem::Resolver::SourceSet < Gem::Resolver::Set def get_set(name) link = @links[name] - @sets[link] ||= Gem::Source.new(link).dependency_resolver_set if link + @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link end end diff --git a/lib/rubygems/resolver/stats.rb b/lib/rubygems/resolver/stats.rb deleted file mode 100644 index 9920976b2a..0000000000 --- a/lib/rubygems/resolver/stats.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class Gem::Resolver::Stats - def initialize - @max_depth = 0 - @max_requirements = 0 - @requirements = 0 - @backtracking = 0 - @iterations = 0 - end - - def record_depth(stack) - if stack.size > @max_depth - @max_depth = stack.size - end - end - - def record_requirements(reqs) - if reqs.size > @max_requirements - @max_requirements = reqs.size - end - end - - def requirement! - @requirements += 1 - end - - def backtracking! - @backtracking += 1 - end - - def iteration! - @iterations += 1 - end - - PATTERN = "%20s: %d\n" - - def display - $stdout.puts "=== Resolver Statistics ===" - $stdout.printf PATTERN, "Max Depth", @max_depth - $stdout.printf PATTERN, "Total Requirements", @requirements - $stdout.printf PATTERN, "Max Requirements", @max_requirements - $stdout.printf PATTERN, "Backtracking #", @backtracking - $stdout.printf PATTERN, "Iteration #", @iterations - end -end diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb new file mode 100644 index 0000000000..bf0dbb6adc --- /dev/null +++ b/lib/rubygems/resolver/strategy.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Custom PubGrub strategy with caching for version selection. +# Modeled after Bundler's strategy to avoid redundant versions_for +# calls during the solver's package selection loop. + +class Gem::Resolver::Strategy + def initialize(source) + @source = source + @package_priority_cache = Hash.new {|h, pkg| h[pkg] = {} } + + @version_indexes = Hash.new do |h, k| + if Gem::PubGrub::Package.root?(k) + h[k] = { Gem::PubGrub::Package.root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + indexes = @version_indexes[package] + versions.min_by {|version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + @package_priority_cache[package][range] ||= begin + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index 7c95a9d4f5..148cba38c4 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true require_relative "openssl" +require_relative "user_interaction" ## # S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems # More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html class Gem::S3URISigner + include Gem::UserInteraction + class ConfigurationError < Gem::Exception def initialize(message) super message @@ -27,9 +30,11 @@ class Gem::S3URISigner end attr_accessor :uri + attr_accessor :method - def initialize(uri) + def initialize(uri, method) @uri = uri + @method = method end ## @@ -38,7 +43,7 @@ class Gem::S3URISigner s3_config = fetch_s3_config current_time = Time.now.utc - date_time = current_time.strftime("%Y%m%dT%H%m%SZ") + date_time = current_time.strftime("%Y%m%dT%H%M%SZ") date = date_time[0,8] credential_info = "#{date}/#{s3_config.region}/s3/aws4_request" @@ -73,7 +78,7 @@ class Gem::S3URISigner def generate_canonical_request(canonical_host, query_params) [ - "GET", + method.upcase, uri.path, query_params, "host:#{canonical_host}", @@ -145,17 +150,40 @@ class Gem::S3URISigner require_relative "request/connection_pools" require "json" - iam_info = ec2_metadata_request(EC2_IAM_INFO) + # First try V2 fallback to V1 + res = nil + begin + res = ec2_metadata_credentials_imds_v2 + rescue InstanceProfileError + alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1" + res = ec2_metadata_credentials_imds_v1 + end + res + end + + def ec2_metadata_credentials_imds_v2 + token = ec2_metadata_token + iam_info = ec2_metadata_request(EC2_IAM_INFO, token:) # Expected format: arn:aws:iam::<id>:instance-profile/<role_name> role_name = iam_info["InstanceProfileArn"].split("/").last - ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name) + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:) end - def ec2_metadata_request(url) - uri = Gem::URI(url) - @request_pool ||= create_request_pool(uri) - request = Gem::Request.new(uri, Gem::Net::HTTP::Get, nil, @request_pool) - response = request.fetch + def ec2_metadata_credentials_imds_v1 + iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil) + # Expected format: arn:aws:iam::<id>:instance-profile/<role_name> + role_name = iam_info["InstanceProfileArn"].split("/").last + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil) + end + + def ec2_metadata_request(url, token:) + request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get) + + response = request.fetch do |req| + if token + req.add_field "X-aws-ec2-metadata-token", token + end + end case response when Gem::Net::HTTPOK then @@ -165,6 +193,26 @@ class Gem::S3URISigner end end + def ec2_metadata_token + request = ec2_iam_request(Gem::URI(EC2_IAM_TOKEN), Gem::Net::HTTP::Put) + + response = request.fetch do |req| + req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60 + end + + case response + when Gem::Net::HTTPOK then + response.body + else + raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}") + end + end + + def ec2_iam_request(uri, verb) + @request_pool ||= create_request_pool(uri) + Gem::Request.new(uri, verb, nil, @request_pool) + end + def create_request_pool(uri) proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme)) certs = Gem::Request.get_cert_files @@ -172,6 +220,7 @@ class Gem::S3URISigner end BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze + EC2_IAM_TOKEN = "http://169.254.169.254/latest/api/token" EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info" EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/" end diff --git a/lib/rubygems/safe_marshal.rb b/lib/rubygems/safe_marshal.rb index b81d1a0a47..871f24727d 100644 --- a/lib/rubygems/safe_marshal.rb +++ b/lib/rubygems/safe_marshal.rb @@ -54,6 +54,7 @@ module Gem "Gem::NameTuple" => %w[@name @version @platform], "Gem::Platform" => %w[@os @cpu @version], "Psych::PrivateType" => %w[@value @type_id], + "YAML::PrivateType" => %w[@value @type_id], }.freeze private_constant :PERMITTED_IVARS diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb index 6a02a48230..f4bba00136 100644 --- a/lib/rubygems/safe_yaml.rb +++ b/lib/rubygems/safe_yaml.rb @@ -35,11 +35,21 @@ module Gem end def self.safe_load(input) - ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled) + if Gem.use_psych? + ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, + permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled) + else + Gem::YAMLSerializer.load( + input, + permitted_classes: PERMITTED_CLASSES, + permitted_symbols: PERMITTED_SYMBOLS, + aliases: aliases_enabled? + ) + end end - def self.load(input) - ::Psych.safe_load(input, permitted_classes: [::Symbol]) + class << self + alias_method :load, :safe_load end end end diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 7b86ac5763..128958ab80 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -143,7 +143,7 @@ class Gem::Security::Policy end ## - # Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and + # Ensures the root of +chain+ has a trusted certificate in Gem::Security.trust_dir and # the digests of the two certificates match according to +digester+ def check_trust(chain, digester, trust_dir) diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index 5732fb57fd..eeeeb52906 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -52,7 +52,7 @@ class Gem::Security::Signer re_signed_cert = Gem::Security.re_sign( expired_cert, private_key, - (Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days) + Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days ) Gem::Security.write(re_signed_cert, expired_cert_path) diff --git a/lib/rubygems/shellwords.rb b/lib/rubygems/shellwords.rb deleted file mode 100644 index 741dccb363..0000000000 --- a/lib/rubygems/shellwords.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -autoload :Shellwords, "shellwords" diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index bee5681dab..86717e3e71 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -5,7 +5,7 @@ require_relative "text" # A Source knows how to list and fetch gems from a RubyGems marshal index. # # There are other Source subclasses for installed gems, local gems, the -# bundler dependency API and so-forth. +# Compact Index API and so-forth. class Gem::Source include Comparable @@ -67,28 +67,11 @@ class Gem::Source ## # Returns a Set that can fetch specifications from this source. - - def dependency_resolver_set # :nodoc: - return Gem::Resolver::IndexSet.new self if uri.scheme == "file" - - fetch_uri = if uri.host == "rubygems.org" - index_uri = uri.dup - index_uri.host = "index.rubygems.org" - index_uri - else - uri - end - - bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions" - - begin - fetcher = Gem::RemoteFetcher.fetcher - response = fetcher.fetch_path bundler_api_uri, nil, true - rescue Gem::RemoteFetcher::FetchError - Gem::Resolver::IndexSet.new self - else - Gem::Resolver::APISet.new response.uri + "./info/" - end + # + # The set will optionally fetch prereleases if requested. + # + def dependency_resolver_set(prerelease = false) + new_dependency_resolver_set.tap {|set| set.prerelease = prerelease } end def hash # :nodoc: @@ -119,7 +102,7 @@ class Gem::Source end ## - # Fetches a specification for the given +name_tuple+. + # Fetches a specification for the given Gem::NameTuple. def fetch_spec(name_tuple) fetcher = Gem::RemoteFetcher.fetcher @@ -207,7 +190,7 @@ class Gem::Source # Downloads +spec+ and writes it to +dir+. See also # Gem::RemoteFetcher#download. - def download(spec, dir=Dir.pwd) + def download(spec, dir = Dir.pwd) fetcher = Gem::RemoteFetcher.fetcher fetcher.download spec, uri.to_s, dir end @@ -227,13 +210,36 @@ class Gem::Source end end - def typo_squatting?(host, distance_threshold=4) + def typo_squatting?(host, distance_threshold = 4) return if @uri.host.nil? levenshtein_distance(@uri.host, host).between? 1, distance_threshold end private + def new_dependency_resolver_set + return Gem::Resolver::IndexSet.new self if uri.scheme == "file" + + fetch_uri = if uri.host == "rubygems.org" + index_uri = uri.dup + index_uri.host = "index.rubygems.org" + index_uri + else + uri + end + + bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions" + + begin + fetcher = Gem::RemoteFetcher.fetcher + response = fetcher.fetch_path bundler_api_uri, nil, true + rescue Gem::RemoteFetcher::FetchError + Gem::Resolver::IndexSet.new self + else + Gem::Resolver::APISet.new response.uri + "./info/" + end + end + def enforce_trailing_slash(uri) uri.merge(uri.path.gsub(%r{/+$}, "") + "/") end diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index ba6eea1f9a..4bef31a265 100644 --- a/lib/rubygems/source/local.rb +++ b/lib/rubygems/source/local.rb @@ -76,6 +76,10 @@ class Gem::Source::Local < Gem::Source end def find_gem(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc: + find_all_gems(gem_name, version, prerelease).max_by(&:version) + end + + def find_all_gems(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc: load_specs :complete found = [] @@ -93,7 +97,7 @@ class Gem::Source::Local < Gem::Source end end - found.max_by(&:version) + found end def fetch_spec(name) # :nodoc: diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb index 33db64fbc1..19bf4595c4 100644 --- a/lib/rubygems/source_list.rb +++ b/lib/rubygems/source_list.rb @@ -60,6 +60,42 @@ class Gem::SourceList end ## + # Prepends +obj+ to the beginning of the source list which may be a Gem::Source, Gem::URI or URI + # Moves +obj+ to the beginning of the list if already present. + # String. + + def prepend(obj) + src = case obj + when Gem::Source + obj + else + Gem::Source.new(obj) + end + + @sources.delete(src) if @sources.include?(src) + @sources.unshift(src) + src + end + + ## + # Appends +obj+ to the end of the source list, moving it if already present. + # +obj+ may be a Gem::Source, Gem::URI or URI String. + # Moves +obj+ to the end of the list if already present. + + def append(obj) + src = case obj + when Gem::Source + obj + else + Gem::Source.new(obj) + end + + @sources.delete(src) if @sources.include?(src) + @sources << src + src + end + + ## # Replaces this SourceList with the sources in +other+ See #<< for # acceptable items in +other+. diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb index 285f117b39..835dedf948 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -83,7 +83,7 @@ class Gem::SpecFetcher # # If +matching_platform+ is false, gems for all platforms are returned. - def search_for_dependency(dependency, matching_platform=true) + def search_for_dependency(dependency, matching_platform = true) found = {} rejected_specs = {} @@ -130,7 +130,7 @@ class Gem::SpecFetcher ## # Return all gem name tuples who's names match +obj+ - def detect(type=:complete) + def detect(type = :complete) tuples = [] list, _ = available_specs(type) @@ -150,7 +150,7 @@ class Gem::SpecFetcher # # If +matching_platform+ is false, gems for all platforms are returned. - def spec_for_dependency(dependency, matching_platform=true) + def spec_for_dependency(dependency, matching_platform = true) tuples, errors = search_for_dependency(dependency, matching_platform) specs = [] @@ -280,7 +280,7 @@ class Gem::SpecFetcher # Retrieves NameTuples from +source+ of the given +type+ (:prerelease, # etc.). If +gracefully_ignore+ is true, errors are ignored. - def tuples_for(source, type, gracefully_ignore=false) # :nodoc: + def tuples_for(source, type, gracefully_ignore = false) # :nodoc: @caches[type][source.uri] ||= source.load_specs(type).sort_by(&:name) rescue Gem::RemoteFetcher::FetchError diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 121b72f90a..51729d755b 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -7,12 +7,10 @@ # See LICENSE.txt for permissions. #++ -require_relative "deprecate" require_relative "basic_specification" require_relative "stub_specification" require_relative "platform" require_relative "specification_record" -require_relative "util/list" require "rbconfig" @@ -36,10 +34,17 @@ require "rbconfig" # Starting in RubyGems 2.0, a Specification can hold arbitrary # metadata. See #metadata for restrictions on the format and size of metadata # items you may add to a specification. +# +# Specifications must be deterministic, as in the example above. For instance, +# you cannot define attributes conditionally: +# +# # INVALID: do not do this. +# unless RUBY_ENGINE == "jruby" +# s.extensions << "ext/example/extconf.rb" +# end +# class Gem::Specification < Gem::BasicSpecification - extend Gem::Deprecate - # REFACTOR: Consider breaking out this version stuff into a separate # module. There's enough special stuff around it that it may justify # a separate class. @@ -488,8 +493,6 @@ class Gem::Specification < Gem::BasicSpecification end @platform = @new_platform.to_s - - invalidate_memoized_attributes end ## @@ -738,14 +741,6 @@ class Gem::Specification < Gem::BasicSpecification attr_accessor :autorequire # :nodoc: ## - # Sets the default executable for this gem. - # - # Deprecated: You must now specify the executable name to Gem.bin_path. - - attr_writer :default_executable - rubygems_deprecate :default_executable= - - ## # Allows deinstallation of gems with legacy platforms. attr_writer :original_platform # :nodoc: @@ -974,6 +969,15 @@ class Gem::Specification < Gem::BasicSpecification ## # Return the best specification that contains the file matching +path+ + # amongst the specs that are not loaded. This method is different than + # +find_inactive_by_path+ as it will filter out loaded specs by their name. + + def self.find_unloaded_by_path(path) + specification_record.find_unloaded_by_path(path) + end + + ## + # Return the best specification that contains the file matching +path+ # amongst the specs that are not activated. def self.find_inactive_by_path(path) @@ -1002,7 +1006,7 @@ class Gem::Specification < Gem::BasicSpecification def self.find_in_unresolved_tree(path) unresolved_specs.each do |spec| spec.traverse do |_from_spec, _dep, to_spec, trail| - if to_spec.has_conflicts? || to_spec.conficts_when_loaded_with?(trail) + if to_spec.has_conflicts? || to_spec.conflicts_when_loaded_with?(trail) :next else return trail.reverse if to_spec.contains_requirable_file? path @@ -1271,7 +1275,7 @@ class Gem::Specification < Gem::BasicSpecification raise unless message.include?("YAML::") unless Object.const_defined?(:YAML) - Object.const_set "YAML", Psych + Object.const_set "YAML", Module.new yaml_set = true end @@ -1280,7 +1284,7 @@ class Gem::Specification < Gem::BasicSpecification YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") && !YAML::Syck.const_defined?(:DefaultKey) elsif message.include?("YAML::PrivateType") && !YAML.const_defined?(:PrivateType) - YAML.const_set "PrivateType", Class.new + YAML.const_set "PrivateType", Class.new { attr_accessor :type_id, :value } end retry_count += 1 @@ -1321,7 +1325,7 @@ class Gem::Specification < Gem::BasicSpecification spec.instance_variable_set :@authors, array[12] spec.instance_variable_set :@description, array[13] spec.instance_variable_set :@homepage, array[14] - spec.instance_variable_set :@has_rdoc, array[15] + # offset due to has_rdoc removal spec.instance_variable_set :@licenses, array[17] spec.instance_variable_set :@metadata, array[18] spec.instance_variable_set :@loaded, false @@ -1620,14 +1624,14 @@ class Gem::Specification < Gem::BasicSpecification # spec's cached gem. def cache_dir - @cache_dir ||= File.join base_dir, "cache" + File.join base_dir, "cache" end ## # Returns the full path to the cached gem for this spec. def cache_file - @cache_file ||= File.join cache_dir, "#{full_name}.gem" + File.join cache_dir, "#{full_name}.gem" end ## @@ -1649,7 +1653,7 @@ class Gem::Specification < Gem::BasicSpecification ## # return true if there will be conflict when spec if loaded together with the list of specs. - def conficts_when_loaded_with?(list_of_specs) # :nodoc: + def conflicts_when_loaded_with?(list_of_specs) # :nodoc: result = list_of_specs.any? do |spec| spec.runtime_dependencies.any? {|dep| (dep.name == name) && !satisfies_requirement?(dep) } end @@ -1717,24 +1721,6 @@ class Gem::Specification < Gem::BasicSpecification end ## - # The default executable for this gem. - # - # Deprecated: The name of the gem is assumed to be the name of the - # executable now. See Gem.bin_path. - - def default_executable # :nodoc: - if defined?(@default_executable) && @default_executable - result = @default_executable - elsif @executables && @executables.size == 1 - result = Array(@executables).first - else - result = nil - end - result - end - rubygems_deprecate :default_executable - - ## # The default value for specification attribute +name+ def default_value(name) @@ -1757,7 +1743,7 @@ class Gem::Specification < Gem::BasicSpecification # # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]] - def dependent_gems(check_dev=true) + def dependent_gems(check_dev = true) out = [] Gem::Specification.each do |spec| deps = check_dev ? spec.dependencies : spec.runtime_dependencies @@ -1903,10 +1889,6 @@ class Gem::Specification < Gem::BasicSpecification spec end - def full_name - @full_name ||= super - end - ## # Work around old bundler versions removing my methods # Can be removed once RubyGems can no longer install Bundler 2.5 @@ -1920,29 +1902,6 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Deprecated and ignored, defaults to true. - # - # Formerly used to indicate this gem was RDoc-capable. - - def has_rdoc # :nodoc: - true - end - rubygems_deprecate :has_rdoc - - ## - # Deprecated and ignored. - # - # Formerly used to indicate this gem was RDoc-capable. - - def has_rdoc=(ignored) # :nodoc: - @has_rdoc = true - end - rubygems_deprecate :has_rdoc= - - alias_method :has_rdoc?, :has_rdoc # :nodoc: - rubygems_deprecate :has_rdoc? - - ## # True if this gem has files in test_files def has_unit_tests? # :nodoc: @@ -2044,17 +2003,6 @@ class Gem::Specification < Gem::BasicSpecification end end - ## - # Expire memoized instance variables that can incorrectly generate, replace - # or miss files due changes in certain attributes used to compute them. - - def invalidate_memoized_attributes - @full_name = nil - @cache_file = nil - end - - private :invalidate_memoized_attributes - def inspect # :nodoc: if $DEBUG super @@ -2093,8 +2041,6 @@ class Gem::Specification < Gem::BasicSpecification def internal_init # :nodoc: super @bin_dir = nil - @cache_dir = nil - @cache_file = nil @doc_dir = nil @ri_dir = nil @spec_dir = nil @@ -2124,6 +2070,7 @@ class Gem::Specification < Gem::BasicSpecification # probably want to build_extensions def missing_extensions? + return false if RUBY_ENGINE == "jruby" return false if extensions.empty? return false if default_gem? return false if File.exist? gem_build_complete_path @@ -2144,11 +2091,11 @@ class Gem::Specification < Gem::BasicSpecification @files.concat(@extra_rdoc_files) end - @files = @files.uniq if @files - @extensions = @extensions.uniq if @extensions - @test_files = @test_files.uniq if @test_files - @executables = @executables.uniq if @executables - @extra_rdoc_files = @extra_rdoc_files.uniq if @extra_rdoc_files + @files = @files.uniq.sort if @files + @extensions = @extensions.uniq.sort if @extensions + @test_files = @test_files.uniq.sort if @test_files + @executables = @executables.uniq.sort if @executables + @extra_rdoc_files = @extra_rdoc_files.uniq.sort if @extra_rdoc_files end ## @@ -2244,10 +2191,13 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Sets rdoc_options to +value+, ensuring it is an array. + # Sets rdoc_options to +value+, ensuring it is a flat array of strings. + # Handles malformed gemspecs where rdoc_options may be a Hash or contain Hashes. def rdoc_options=(options) - @rdoc_options = Array options + @rdoc_options = Array(options).flat_map do |opt| + opt.is_a?(Hash) ? opt.to_a.flatten.map(&:to_s) : opt + end end ## @@ -2447,8 +2397,6 @@ class Gem::Specification < Gem::BasicSpecification :required_rubygems_version, :specification_version, :version, - :has_rdoc, - :default_executable, :metadata, :signing_key, ] @@ -2511,24 +2459,28 @@ class Gem::Specification < Gem::BasicSpecification def to_yaml(opts = {}) # :nodoc: Gem.load_yaml - # Because the user can switch the YAML engine behind our - # back, we have to check again here to make sure that our - # psych code was properly loaded, and load it if not. - unless Gem.const_defined?(:NoAliasYAMLTree) - require_relative "psych_tree" - end + if Gem.use_psych? + # Because the user can switch the YAML engine behind our + # back, we have to check again here to make sure that our + # psych code was properly loaded, and load it if not. + unless Gem.const_defined?(:NoAliasYAMLTree) + require_relative "psych_tree" + end - builder = Gem::NoAliasYAMLTree.create - builder << self - ast = builder.tree + builder = Gem::NoAliasYAMLTree.create + builder << self + ast = builder.tree - require "stringio" - io = StringIO.new - io.set_encoding Encoding::UTF_8 + require "stringio" + io = StringIO.new + io.set_encoding Encoding::UTF_8 - Psych::Visitors::Emitter.new(io).accept(ast) + Psych::Visitors::Emitter.new(io).accept(ast) - io.string.gsub(/ !!null \n/, " \n") + io.string.gsub(/ !!null \n/, " \n") + else + Gem::YAMLSerializer.dump(self) + end end ## @@ -2586,29 +2538,11 @@ class Gem::Specification < Gem::BasicSpecification Gem::SpecificationPolicy.new(self).validate_for_resolution end - def validate_metadata - Gem::SpecificationPolicy.new(self).validate_metadata - end - rubygems_deprecate :validate_metadata - - def validate_dependencies - Gem::SpecificationPolicy.new(self).validate_dependencies - end - rubygems_deprecate :validate_dependencies - - def validate_permissions - Gem::SpecificationPolicy.new(self).validate_permissions - end - rubygems_deprecate :validate_permissions - ## # Set the version to +version+. def version=(version) - @version = Gem::Version.create(version) - return if @version.nil? - - invalidate_memoized_attributes + @version = version.nil? ? version : Gem::Version.create(version) end def stubbed? @@ -2623,6 +2557,10 @@ class Gem::Specification < Gem::BasicSpecification self.date = val when "platform" self.platform = val + when "rdoc_options" + self.rdoc_options = val + when "requirements" + self.requirements = val else instance_variable_set "@#{ivar}", val end diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index d79ee7df92..478e294e09 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -190,53 +190,16 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: ## # Checks that the gem does not depend on itself. - # Checks that dependencies use requirements as we recommend. Warnings are - # issued when dependencies are open-ended or overly strict for semantic - # versioning. def validate_dependencies # :nodoc: - warning_messages = [] + error_messages = [] @specification.dependencies.each do |dep| - if dep.name == @specification.name # warn on self reference - warning_messages << "Self referencing dependency is unnecessary and strongly discouraged." - end - - prerelease_dep = dep.requirements_list.any? do |req| - Gem::Requirement.new(req).prerelease? - end - - warning_messages << "prerelease dependency on #{dep} is not recommended" if - prerelease_dep && !@specification.version.prerelease? - - open_ended = dep.requirement.requirements.all? do |op, version| - !version.prerelease? && [">", ">="].include?(op) + if dep.name == @specification.name # error on self reference + error_messages << "Dependencies of this gem include a self-reference." end - - next unless open_ended - op, dep_version = dep.requirement.requirements.first - - segments = dep_version.segments - - base = segments.first 2 - - recommendation = if [">", ">="].include?(op) && segments == [0] - " use a bounded requirement, such as \"~> x.y\"" - else - bugfix = if op == ">" - ", \"> #{dep_version}\"" - elsif op == ">=" && base != segments - ", \">= #{dep_version}\"" - end - - " if #{dep.name} is semantically versioned, use:\n" \ - " add_#{dep.type}_dependency \"#{dep.name}\", \"~> #{base.join "."}\"#{bugfix}" - end - - warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n" - end - if warning_messages.any? - warning_messages.each {|warning_message| warning warning_message } end + + error error_messages.join if error_messages.any? end def validate_required_ruby_version @@ -472,6 +435,7 @@ or set it to nil if you don't want to specify a license. warning "deprecated autorequire specified" if @specification.autorequire @specification.executables.each do |executable| + validate_executable(executable) validate_shebang_line_in(executable) end @@ -485,6 +449,13 @@ or set it to nil if you don't want to specify a license. warning("no #{attribute} specified") if value.nil? || value.empty? end + def validate_executable(executable) + separators = [File::SEPARATOR, File::ALT_SEPARATOR, File::PATH_SEPARATOR].compact.map {|sep| Regexp.escape(sep) }.join + return unless executable.match?(/[\s#{separators}]/) + + error "executable \"#{executable}\" contains invalid characters" + end + def validate_shebang_line_in(executable) executable_path = File.join(@specification.bindir, executable) return if File.read(executable_path, 2) == "#!" @@ -504,6 +475,7 @@ or set it to nil if you don't want to specify a license. validate_rake_extensions(builder) validate_rust_extensions(builder) + validate_extension_require_relative end def validate_rust_extensions(builder) # :nodoc: @@ -524,6 +496,33 @@ You have specified rake based extension, but rake is not added as runtime depend WARNING end + def validate_extension_require_relative # :nodoc: + return unless @specification.extensions.any? + + require_paths = @specification.require_paths + + @specification.files.each do |rb_file| + next unless rb_file.end_with?(".rb") + next unless require_paths.any? {|rp| rb_file.start_with?("#{rp}/") } + next unless File.file?(rb_file) + + File.foreach(rb_file).with_index(1) do |line, lineno| + next unless line =~ /^\s*require_relative\s+["']([^"']+)["']/ + + required_path = Regexp.last_match(1) + resolved = File.join(File.dirname(rb_file), required_path) + + next if @specification.files.any? {|f| f == "#{resolved}.rb" || f == resolved } + + warning <<~WARNING + #{rb_file}:#{lineno} uses `require_relative "#{required_path}"` to load a compiled extension. + This will break in RubyGems 4.2, which will stop copying compiled extensions into the gem's lib directory. + Use `require` instead of `require_relative` to load compiled extensions. + WARNING + end + end + end + def validate_unique_links links = @specification.metadata.slice(*METADATA_LINK_KEYS) grouped = links.group_by {|_key, uri| uri } diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb index 195a355496..c7e5cbedb5 100644 --- a/lib/rubygems/specification_record.rb +++ b/lib/rubygems/specification_record.rb @@ -73,7 +73,7 @@ module Gem end ## - # Adds +spec+ to the the record, keeping the collection properly sorted. + # Adds +spec+ to the record, keeping the collection properly sorted. def add_spec(spec) return if all.include? spec @@ -155,6 +155,19 @@ module Gem end ## + # Return the best specification that contains the file matching +path+ + # amongst the specs that are not loaded. This method is different than + # +find_inactive_by_path+ as it will filter out loaded specs by their name. + + def find_unloaded_by_path(path) + stub = stubs.find do |s| + next if Gem.loaded_specs[s.name] + s.contains_requirable_file? path + end + stub&.to_spec + end + + ## # Return the best specification in the record that contains the file # matching +path+ amongst the specs that are not activated. diff --git a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA_R3.pem b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem index 8afb219058..8afb219058 100644 --- a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA_R3.pem +++ b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem diff --git a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA.pem b/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA.pem deleted file mode 100644 index f4ce4ca43d..0000000000 --- a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG -A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv -b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw -MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i -YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT -aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ -jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp -xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp -1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG -snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ -U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 -9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B -AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz -yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE -38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP -AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad -DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME -HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb index 4f6a70ba4b..53b337ed85 100644 --- a/lib/rubygems/stub_specification.rb +++ b/lib/rubygems/stub_specification.rb @@ -140,6 +140,7 @@ class Gem::StubSpecification < Gem::BasicSpecification end def missing_extensions? + return false if RUBY_ENGINE == "jruby" return false if default_gem? return false if extensions.empty? return false if File.exist? gem_build_complete_path diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index da0795b771..88d4ce59b4 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -21,7 +21,7 @@ module Gem::Text # Wraps +text+ to +wrap+ characters and optionally indents by +indent+ # characters - def format_text(text, wrap, indent=0) + def format_text(text, wrap, indent = 0) result = [] work = clean_text(text) diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index 991bc6fb95..fe4c3a80cf 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -42,10 +42,25 @@ class Gem::Uninstaller attr_reader :spec ## - # Constructs an uninstaller that will uninstall +gem+ + # Constructs an uninstaller that will uninstall gem named +gem+. + # +options+ is a Hash with the following keys: + # + # :version:: Version requirement for the gem to uninstall. If not specified, + # uses Gem::Requirement.default. + # :install_dir:: The directory where the gem is installed. If not specified, + # uses Gem.dir. + # :executables:: Whether executables should be removed without confirmation or not. If nil, asks the user explicitly. + # :all:: If more than one version matches the requirement, whether to forcefully remove all matching versions or ask the user to select specific matching versions that should be removed. + # :ignore:: Ignore broken dependency checks when uninstalling. + # :bin_dir:: Directory containing executables to remove. If not specified, + # uses Gem.bindir. + # :format_executable:: In order to find executables to be removed, format executable names using Gem::Installer.exec_format. + # :abort_on_dependent:: Directly abort uninstallation if dependencies would be broken, rather than asking the user for confirmation. + # :check_dev:: When checking if uninstalling gem would leave broken dependencies around, also consider development dependencies. + # :force:: Set both :all and :ignore to true for forced uninstallation. + # :user_install:: Uninstall from user gem directory instead of system directory. def initialize(gem, options = {}) - # TODO: document the valid options @gem = gem @version = options[:version] || Gem::Requirement.default @install_dir = options[:install_dir] @@ -57,10 +72,6 @@ class Gem::Uninstaller @bin_dir = options[:bin_dir] @format_executable = options[:format_executable] @abort_on_dependent = options[:abort_on_dependent] - - # Indicate if development dependencies should be checked when - # uninstalling. (default: false) - # @check_dev = options[:check_dev] if options[:force] diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb index 517ce33637..8856fdadd2 100644 --- a/lib/rubygems/uri_formatter.rb +++ b/lib/rubygems/uri_formatter.rb @@ -17,7 +17,8 @@ class Gem::UriFormatter # Creates a new URI formatter for +uri+. def initialize(uri) - require "cgi" + require "cgi/escape" + require "cgi/util" unless defined?(CGI::EscapeExt) @uri = uri end diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 0172c4ee89..9fe3e755c4 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -6,7 +6,6 @@ # See LICENSE.txt for permissions. #++ -require_relative "deprecate" require_relative "text" ## @@ -170,8 +169,6 @@ end # Gem::StreamUI implements a simple stream based user interface. class Gem::StreamUI - extend Gem::Deprecate - ## # The input stream @@ -193,7 +190,7 @@ class Gem::StreamUI # then special operations (like asking for passwords) will use the TTY # commands to disable character echo. - def initialize(in_stream, out_stream, err_stream=$stderr, usetty=true) + def initialize(in_stream, out_stream, err_stream = $stderr, usetty = true) @ins = in_stream @outs = out_stream @errs = err_stream @@ -246,7 +243,7 @@ class Gem::StreamUI # to a tty, raises an exception if default is nil, otherwise returns # default. - def ask_yes_no(question, default=nil) + def ask_yes_no(question, default = nil) unless tty? if default.nil? raise Gem::OperationNotSupportedError, @@ -325,14 +322,14 @@ class Gem::StreamUI ## # Display a statement. - def say(statement="") + def say(statement = "") @outs.puts statement end ## # Display an informational alert. Will ask +question+ if it is not nil. - def alert(statement, question=nil) + def alert(statement, question = nil) @outs.puts "INFO: #{statement}" ask(question) if question end @@ -340,7 +337,7 @@ class Gem::StreamUI ## # Display a warning on stderr. Will ask +question+ if it is not nil. - def alert_warning(statement, question=nil) + def alert_warning(statement, question = nil) @errs.puts "WARNING: #{statement}" ask(question) if question end @@ -349,7 +346,7 @@ class Gem::StreamUI # Display an error message in a location expected to get error messages. # Will ask +question+ if it is not nil. - def alert_error(statement, question=nil) + def alert_error(statement, question = nil) @errs.puts "ERROR: #{statement}" ask(question) if question end diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index 51f9c2029f..ee4106c6ce 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "deprecate" - ## # This module contains various utility methods as module methods. @@ -57,26 +55,6 @@ module Gem::Util end ## - # Invokes system, but silences all output. - - def self.silent_system(*command) - opt = { out: IO::NULL, err: [:child, :out] } - if Hash === command.last - opt.update(command.last) - cmds = command[0...-1] - else - cmds = command.dup - end - system(*(cmds << opt)) - end - - class << self - extend Gem::Deprecate - - rubygems_deprecate :silent_system - end - - ## # Enumerates the parents of +directory+. def self.traverse_parents(directory, &block) diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb new file mode 100644 index 0000000000..32767c6a79 --- /dev/null +++ b/lib/rubygems/util/atomic_file_writer.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Based on ActiveSupport's AtomicFile implementation +# Copyright (c) David Heinemeier Hansson +# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb +# Licensed under the MIT License + +module Gem + class AtomicFileWriter + ## + # Write to a file atomically. Useful for situations where you don't + # want other processes or threads to see half-written files. + + def self.open(file_name) + require "securerandom" unless defined?(SecureRandom) + + old_stat = begin + File.stat(file_name) + rescue SystemCallError + nil + end + + # Names can't be longer than 255B + tmp_suffix = ".tmp.#{SecureRandom.hex}" + dirname = File.dirname(file_name) + basename = File.basename(file_name) + tmp_path = File.join(dirname, ".#{basename.byteslice(0, 254 - tmp_suffix.bytesize)}#{tmp_suffix}") + + flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY + flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE) + + File.open(tmp_path, flags) do |temp_file| + temp_file.binmode + if old_stat + # Set correct permissions on new file + begin + File.chown(old_stat.uid, old_stat.gid, temp_file.path) + # This operation will affect filesystem ACL's + File.chmod(old_stat.mode, temp_file.path) + rescue Errno::EPERM, Errno::EACCES + # Changing file ownership failed, moving on. + end + end + + return_val = yield temp_file + rescue StandardError => error + begin + temp_file.close + rescue StandardError + nil + end + + begin + File.unlink(temp_file.path) + rescue StandardError + nil + end + + raise error + else + begin + File.rename(temp_file.path, file_name) + rescue StandardError + begin + File.unlink(temp_file.path) + rescue StandardError + end + + raise + end + + return_val + end + end + end +end diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb index 3d9df5ebcd..caf53d0b7e 100644 --- a/lib/rubygems/util/licenses.rb +++ b/lib/rubygems/util/licenses.rb @@ -27,6 +27,7 @@ class Gem::Licenses AGPL-1.0-or-later AGPL-3.0-only AGPL-3.0-or-later + ALGLIB-Documentation AMD-newlib AMDPLPA AML @@ -48,6 +49,7 @@ class Gem::Licenses Adobe-Display-PostScript Adobe-Glyph Adobe-Utopia + Advanced-Cryptics-Dictionary Afmparse Aladdin Apache-1.0 @@ -59,12 +61,16 @@ class Gem::Licenses Artistic-1.0-Perl Artistic-1.0-cl8 Artistic-2.0 + Artistic-dist + Aspell-RU + BOLA-1.1 BSD-1-Clause BSD-2-Clause BSD-2-Clause-Darwin BSD-2-Clause-Patent BSD-2-Clause-Views BSD-2-Clause-first-lines + BSD-2-Clause-pkgconf-disclaimer BSD-3-Clause BSD-3-Clause-Attribution BSD-3-Clause-Clear @@ -77,6 +83,7 @@ class Gem::Licenses BSD-3-Clause-No-Nuclear-Warranty BSD-3-Clause-Open-MPI BSD-3-Clause-Sun + BSD-3-Clause-Tso BSD-3-Clause-acpica BSD-3-Clause-flex BSD-4-Clause @@ -87,6 +94,7 @@ class Gem::Licenses BSD-Advertising-Acknowledgement BSD-Attribution-HPND-disclaimer BSD-Inferno-Nettverk + BSD-Mark-Modifications BSD-Protection BSD-Source-Code BSD-Source-beginning-file @@ -108,9 +116,11 @@ class Gem::Licenses Borceux Brian-Gladman-2-Clause Brian-Gladman-3-Clause + Buddy C-UDA-1.0 CAL-1.0 CAL-1.0-Combined-Work-Exception + CAPEC-tou CATOSL-1.1 CC-BY-1.0 CC-BY-2.0 @@ -205,6 +215,7 @@ class Gem::Licenses Cornell-Lossless-JPEG Cronyx Crossword + CryptoSwift CrystalStacker Cube D-FSL-1.0 @@ -215,6 +226,7 @@ class Gem::Licenses DRL-1.0 DRL-1.1 DSDP + DocBook-DTD DocBook-Schema DocBook-Stylesheet DocBook-XML @@ -226,6 +238,9 @@ class Gem::Licenses EPICS EPL-1.0 EPL-2.0 + ESA-PL-permissive-2.4 + ESA-PL-strong-copyleft-2.4 + ESA-PL-weak-copyleft-2.4 EUDatagrid EUPL-1.0 EUPL-1.1 @@ -240,7 +255,10 @@ class Gem::Licenses FSFAP-no-warranty-disclaimer FSFUL FSFULLR + FSFULLRSD FSFULLRWD + FSL-1.1-ALv2 + FSL-1.1-MIT FTL Fair Ferguson-Twofish @@ -276,11 +294,13 @@ class Gem::Licenses GPL-2.0-or-later GPL-3.0-only GPL-3.0-or-later + Game-Programming-Gems Giftware Glide Glulxe Graphics-Gems Gutmann + HDF5 HIDAPI HP-1986 HP-1989 @@ -294,6 +314,7 @@ class Gem::Licenses HPND-Markus-Kuhn HPND-Netrek HPND-Pbmplus + HPND-SMC HPND-UC HPND-UC-export-US HPND-doc @@ -308,6 +329,7 @@ class Gem::Licenses HPND-sell-variant HPND-sell-variant-MIT-disclaimer HPND-sell-variant-MIT-disclaimer-rev + HPND-sell-variant-critical-systems HTMLTIDY HaskellReport Hippocratic-2.1 @@ -320,6 +342,7 @@ class Gem::Licenses IPL-1.0 ISC ISC-Veillard + ISO-permission ImageMagick Imlib2 Info-ZIP @@ -377,6 +400,7 @@ class Gem::Licenses MIT-Festival MIT-Khronos-old MIT-Modern-Variant + MIT-STK MIT-Wu MIT-advertising MIT-enna @@ -385,6 +409,7 @@ class Gem::Licenses MIT-testregex MITNFA MMIXware + MMPL-1.0.1 MPEG-SSG MPL-1.0 MPL-1.1 @@ -416,6 +441,7 @@ class Gem::Licenses NGPL NICTA-1.0 NIST-PD + NIST-PD-TNT NIST-PD-fallback NIST-Software NLOD-1.0 @@ -426,6 +452,7 @@ class Gem::Licenses NPL-1.1 NPOSL-3.0 NRL + NTIA-PD NTP NTP-0 Naumen @@ -474,12 +501,15 @@ class Gem::Licenses OPL-1.0 OPL-UK-3.0 OPUBL-1.0 + OSC-1.0 OSET-PL-2.1 OSL-1.0 OSL-1.1 OSL-2.0 OSL-2.1 OSL-3.0 + OSSP + OpenMDW-1.0 OpenPBS-2.3 OpenSSL OpenSSL-standalone @@ -490,6 +520,7 @@ class Gem::Licenses PHP-3.01 PPL PSF-2.0 + ParaType-Free-Font-1.3 Parity-6.0.0 Parity-7.0.0 Pixar @@ -518,6 +549,7 @@ class Gem::Licenses SGI-B-1.1 SGI-B-2.0 SGI-OpenGL + SGMLUG-PM SGP4 SHL-0.5 SHL-0.51 @@ -528,11 +560,13 @@ class Gem::Licenses SMLNJ SMPPL SNIA + SOFA SPL-1.0 SSH-OpenSSH SSH-short SSLeay-standalone SSPL-1.0 + SUL-1.0 SWL Saxpath SchemeReport @@ -563,6 +597,7 @@ class Gem::Licenses TTYP0 TU-Berlin-1.0 TU-Berlin-2.0 + TekHVC TermReadKey ThirdEye TrustedQSL @@ -572,24 +607,31 @@ class Gem::Licenses UPL-1.0 URT-RLE Ubuntu-font-1.0 + UnRAR Unicode-3.0 Unicode-DFS-2015 Unicode-DFS-2016 Unicode-TOU UnixCrypt Unlicense + Unlicense-libtelnet + Unlicense-libwhirlpool VOSTROM VSL-1.0 Vim + Vixie-Cron W3C W3C-19980720 W3C-20150513 + WTFNMFPL WTFPL Watcom-1.0 Widget-Workshop + WordNet Wsuipa X11 X11-distribute-modifications-variant + X11-no-permit-persons X11-swapped XFree86-1.1 XSkat @@ -630,7 +672,10 @@ class Gem::Licenses gnuplot gtkbook hdparm + hyphen-bulgarian iMatix + jove + libpng-1.6.35 libpng-2.0 libselinux-1.0 libtiff @@ -638,10 +683,12 @@ class Gem::Licenses lsof magaz mailprio + man2html metamail mpi-permissive mpich2 mplus + ngrep pkgconf pnmstitch psfrag @@ -715,7 +762,9 @@ class Gem::Licenses CGAL-linking-exception CLISP-exception-2.0 Classpath-exception-2.0 + Classpath-exception-2.0-short DigiRule-FOSS-exception + Digia-Qt-LGPL-exception-1.1 FLTK-exception Fawkes-Runtime-exception Font-exception-2.0 @@ -755,6 +804,7 @@ class Gem::Licenses SHL-2.0 SHL-2.1 SWI-exception + Simple-Library-Usage-exception Swift-exception Texinfo-exception UBDL-exception @@ -768,11 +818,15 @@ class Gem::Licenses gnu-javamail-exception harbour-exception i2p-gpl-java-exception + kvirc-openssl-exception libpri-OpenH323-exception mif-exception mxml-exception openvpn-openssl-exception + polyparse-exception romic-exception + rsync-linking-exception + sqlitestudio-OpenSSL-exception stunnel-exception u-boot-exception-2.0 vsftpd-openssl-exception diff --git a/lib/rubygems/util/list.rb b/lib/rubygems/util/list.rb deleted file mode 100644 index 2899e8a2b9..0000000000 --- a/lib/rubygems/util/list.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gem - # The Gem::List class is currently unused and will be removed in the next major rubygems version - class List # :nodoc: - include Enumerable - attr_accessor :value, :tail - - def initialize(value = nil, tail = nil) - @value = value - @tail = tail - end - - def each - n = self - while n - yield n.value - n = n.tail - end - end - - def to_a - super.reverse - end - - def prepend(value) - List.new value, self - end - - def pretty_print(q) # :nodoc: - q.pp to_a - end - - def self.prepend(list, value) - return List.new(value) unless list - List.new value, list - end - end - deprecate_constant :List -end diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb index 57e0747eb4..eb5b513570 100644 --- a/lib/rubygems/validator.rb +++ b/lib/rubygems/validator.rb @@ -59,7 +59,7 @@ class Gem::Validator #-- # TODO needs further cleanup - def alien(gems=[]) + def alien(gems = []) errors = Hash.new {|h,k| h[k] = {} } Gem::Specification.each do |spec| diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo.rb b/lib/rubygems/vendor/molinillo/lib/molinillo.rb deleted file mode 100644 index dd5600c9e3..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative 'molinillo/gem_metadata' -require_relative 'molinillo/errors' -require_relative 'molinillo/resolver' -require_relative 'molinillo/modules/ui' -require_relative 'molinillo/modules/specification_provider' - -# Gem::Molinillo is a generic dependency resolution algorithm. -module Gem::Molinillo -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb deleted file mode 100644 index 34842d46d5..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # @!visibility private - module Delegates - # Delegates all {Gem::Molinillo::ResolutionState} methods to a `#state` property. - module ResolutionState - # (see Gem::Molinillo::ResolutionState#name) - def name - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.name - end - - # (see Gem::Molinillo::ResolutionState#requirements) - def requirements - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.requirements - end - - # (see Gem::Molinillo::ResolutionState#activated) - def activated - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.activated - end - - # (see Gem::Molinillo::ResolutionState#requirement) - def requirement - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.requirement - end - - # (see Gem::Molinillo::ResolutionState#possibilities) - def possibilities - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.possibilities - end - - # (see Gem::Molinillo::ResolutionState#depth) - def depth - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.depth - end - - # (see Gem::Molinillo::ResolutionState#conflicts) - def conflicts - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.conflicts - end - - # (see Gem::Molinillo::ResolutionState#unused_unwind_options) - def unused_unwind_options - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.unused_unwind_options - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb deleted file mode 100644 index 8417721537..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - module Delegates - # Delegates all {Gem::Molinillo::SpecificationProvider} methods to a - # `#specification_provider` property. - module SpecificationProvider - # (see Gem::Molinillo::SpecificationProvider#search_for) - def search_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.search_for(dependency) - end - end - - # (see Gem::Molinillo::SpecificationProvider#dependencies_for) - def dependencies_for(specification) - with_no_such_dependency_error_handling do - specification_provider.dependencies_for(specification) - end - end - - # (see Gem::Molinillo::SpecificationProvider#requirement_satisfied_by?) - def requirement_satisfied_by?(requirement, activated, spec) - with_no_such_dependency_error_handling do - specification_provider.requirement_satisfied_by?(requirement, activated, spec) - end - end - - # (see Gem::Molinillo::SpecificationProvider#dependencies_equal?) - def dependencies_equal?(dependencies, other_dependencies) - with_no_such_dependency_error_handling do - specification_provider.dependencies_equal?(dependencies, other_dependencies) - end - end - - # (see Gem::Molinillo::SpecificationProvider#name_for) - def name_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.name_for(dependency) - end - end - - # (see Gem::Molinillo::SpecificationProvider#name_for_explicit_dependency_source) - def name_for_explicit_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_explicit_dependency_source - end - end - - # (see Gem::Molinillo::SpecificationProvider#name_for_locking_dependency_source) - def name_for_locking_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_locking_dependency_source - end - end - - # (see Gem::Molinillo::SpecificationProvider#sort_dependencies) - def sort_dependencies(dependencies, activated, conflicts) - with_no_such_dependency_error_handling do - specification_provider.sort_dependencies(dependencies, activated, conflicts) - end - end - - # (see Gem::Molinillo::SpecificationProvider#allow_missing?) - def allow_missing?(dependency) - with_no_such_dependency_error_handling do - specification_provider.allow_missing?(dependency) - end - end - - private - - # Ensures any raised {NoSuchDependencyError} has its - # {NoSuchDependencyError#required_by} set. - # @yield - def with_no_such_dependency_error_handling - yield - rescue NoSuchDependencyError => error - if state - vertex = activated.vertex_named(name_for(error.dependency)) - error.required_by += vertex.incoming_edges.map { |e| e.origin.name } - error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty? - end - raise - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb deleted file mode 100644 index 2dbbc589dc..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb +++ /dev/null @@ -1,255 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../../../vendored_tsort' - -require_relative 'dependency_graph/log' -require_relative 'dependency_graph/vertex' - -module Gem::Molinillo - # A directed acyclic graph that is tuned to hold named dependencies - class DependencyGraph - include Enumerable - - # Enumerates through the vertices of the graph. - # @return [Array<Vertex>] The graph's vertices. - def each - return vertices.values.each unless block_given? - vertices.values.each { |v| yield v } - end - - include Gem::TSort - - # @!visibility private - alias tsort_each_node each - - # @!visibility private - def tsort_each_child(vertex, &block) - vertex.successors.each(&block) - end - - # Topologically sorts the given vertices. - # @param [Enumerable<Vertex>] vertices the vertices to be sorted, which must - # all belong to the same graph. - # @return [Array<Vertex>] The sorted vertices. - def self.tsort(vertices) - Gem::TSort.tsort( - lambda { |b| vertices.each(&b) }, - lambda { |v, &b| (v.successors & vertices).each(&b) } - ) - end - - # A directed edge of a {DependencyGraph} - # @attr [Vertex] origin The origin of the directed edge - # @attr [Vertex] destination The destination of the directed edge - # @attr [Object] requirement The requirement the directed edge represents - Edge = Struct.new(:origin, :destination, :requirement) - - # @return [{String => Vertex}] the vertices of the dependency graph, keyed - # by {Vertex#name} - attr_reader :vertices - - # @return [Log] the op log for this graph - attr_reader :log - - # Initializes an empty dependency graph - def initialize - @vertices = {} - @log = Log.new - end - - # Tags the current state of the dependency as the given tag - # @param [Object] tag an opaque tag for the current state of the graph - # @return [Void] - def tag(tag) - log.tag(self, tag) - end - - # Rewinds the graph to the state tagged as `tag` - # @param [Object] tag the tag to rewind to - # @return [Void] - def rewind_to(tag) - log.rewind_to(self, tag) - end - - # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices} - # are properly copied. - # @param [DependencyGraph] other the graph to copy. - def initialize_copy(other) - super - @vertices = {} - @log = other.log.dup - traverse = lambda do |new_v, old_v| - return if new_v.outgoing_edges.size == old_v.outgoing_edges.size - old_v.outgoing_edges.each do |edge| - destination = add_vertex(edge.destination.name, edge.destination.payload) - add_edge_no_circular(new_v, destination, edge.requirement) - traverse.call(destination, edge.destination) - end - end - other.vertices.each do |name, vertex| - new_vertex = add_vertex(name, vertex.payload, vertex.root?) - new_vertex.explicit_requirements.replace(vertex.explicit_requirements) - traverse.call(new_vertex, vertex) - end - end - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{vertices.values.inspect}" - end - - # @param [Hash] options options for dot output. - # @return [String] Returns a dot format representation of the graph - def to_dot(options = {}) - edge_label = options.delete(:edge_label) - raise ArgumentError, "Unknown options: #{options.keys}" unless options.empty? - - dot_vertices = [] - dot_edges = [] - vertices.each do |n, v| - dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]" - v.outgoing_edges.each do |e| - label = edge_label ? edge_label.call(e) : e.requirement - dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]" - end - end - - dot_vertices.uniq! - dot_vertices.sort! - dot_edges.uniq! - dot_edges.sort! - - dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}') - dot.join("\n") - end - - # @param [DependencyGraph] other - # @return [Boolean] whether the two dependency graphs are equal, determined - # by a recursive traversal of each {#root_vertices} and its - # {Vertex#successors} - def ==(other) - return false unless other - return true if equal?(other) - vertices.each do |name, vertex| - other_vertex = other.vertex_named(name) - return false unless other_vertex - return false unless vertex.payload == other_vertex.payload - return false unless other_vertex.successors.to_set == vertex.successors.to_set - end - end - - # @param [String] name - # @param [Object] payload - # @param [Array<String>] parent_names - # @param [Object] requirement the requirement that is requiring the child - # @return [void] - def add_child_vertex(name, payload, parent_names, requirement) - root = !parent_names.delete(nil) { true } - vertex = add_vertex(name, payload, root) - vertex.explicit_requirements << requirement if root - parent_names.each do |parent_name| - parent_vertex = vertex_named(parent_name) - add_edge(parent_vertex, vertex, requirement) - end - vertex - end - - # Adds a vertex with the given name, or updates the existing one. - # @param [String] name - # @param [Object] payload - # @return [Vertex] the vertex that was added to `self` - def add_vertex(name, payload, root = false) - log.add_vertex(self, name, payload, root) - end - - # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively - # removing any non-root vertices that were orphaned in the process - # @param [String] name - # @return [Array<Vertex>] the vertices which have been detached - def detach_vertex_named(name) - log.detach_vertex_named(self, name) - end - - # @param [String] name - # @return [Vertex,nil] the vertex with the given name - def vertex_named(name) - vertices[name] - end - - # @param [String] name - # @return [Vertex,nil] the root vertex with the given name - def root_vertex_named(name) - vertex = vertex_named(name) - vertex if vertex && vertex.root? - end - - # Adds a new {Edge} to the dependency graph - # @param [Vertex] origin - # @param [Vertex] destination - # @param [Object] requirement the requirement that this edge represents - # @return [Edge] the added edge - def add_edge(origin, destination, requirement) - if destination.path_to?(origin) - raise CircularDependencyError.new(path(destination, origin)) - end - add_edge_no_circular(origin, destination, requirement) - end - - # Deletes an {Edge} from the dependency graph - # @param [Edge] edge - # @return [Void] - def delete_edge(edge) - log.delete_edge(self, edge.origin.name, edge.destination.name, edge.requirement) - end - - # Sets the payload of the vertex with the given name - # @param [String] name the name of the vertex - # @param [Object] payload the payload - # @return [Void] - def set_payload(name, payload) - log.set_payload(self, name, payload) - end - - private - - # Adds a new {Edge} to the dependency graph without checking for - # circularity. - # @param (see #add_edge) - # @return (see #add_edge) - def add_edge_no_circular(origin, destination, requirement) - log.add_edge_no_circular(self, origin.name, destination.name, requirement) - end - - # Returns the path between two vertices - # @raise [ArgumentError] if there is no path between the vertices - # @param [Vertex] from - # @param [Vertex] to - # @return [Array<Vertex>] the shortest path from `from` to `to` - def path(from, to) - distances = Hash.new(vertices.size + 1) - distances[from.name] = 0 - predecessors = {} - each do |vertex| - vertex.successors.each do |successor| - if distances[successor.name] > distances[vertex.name] + 1 - distances[successor.name] = distances[vertex.name] + 1 - predecessors[successor] = vertex - end - end - end - - path = [to] - while before = predecessors[to] - path << before - to = before - break if to == from - end - - unless path.last.equal?(from) - raise ArgumentError, "There is no path from #{from.name} to #{to.name}" - end - - path.reverse - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb deleted file mode 100644 index 8707ec451d..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - class DependencyGraph - # An action that modifies a {DependencyGraph} that is reversible. - # @abstract - class Action - # rubocop:disable Lint/UnusedMethodArgument - - # @return [Symbol] The name of the action. - def self.action_name - raise 'Abstract' - end - - # Performs the action on the given graph. - # @param [DependencyGraph] graph the graph to perform the action on. - # @return [Void] - def up(graph) - raise 'Abstract' - end - - # Reverses the action on the given graph. - # @param [DependencyGraph] graph the graph to reverse the action on. - # @return [Void] - def down(graph) - raise 'Abstract' - end - - # @return [Action,Nil] The previous action - attr_accessor :previous - - # @return [Action,Nil] The next action - attr_accessor :next - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb deleted file mode 100644 index aa9815c5ae..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_edge_no_circular) - class AddEdgeNoCircular < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - delete_first(edge.origin.outgoing_edges, edge) - delete_first(edge.destination.incoming_edges, edge) - end - - # @!group AddEdgeNoCircular - - # @return [String] the name of the origin of the edge - attr_reader :origin - - # @return [String] the name of the destination of the edge - attr_reader :destination - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin the name of the origin of the edge - # @param [String] destination the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin, destination, requirement) - @origin = origin - @destination = destination - @requirement = requirement - end - - private - - def delete_first(array, item) - return unless index = array.index(item) - array.delete_at(index) - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb deleted file mode 100644 index 9c7066a669..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_vertex) - class AddVertex < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - if existing = graph.vertices[name] - @existing_payload = existing.payload - @existing_root = existing.root - end - vertex = existing || Vertex.new(name, payload) - graph.vertices[vertex.name] = vertex - vertex.payload ||= payload - vertex.root ||= root - vertex - end - - # (see Action#down) - def down(graph) - if defined?(@existing_payload) - vertex = graph.vertices[name] - vertex.payload = @existing_payload - vertex.root = @existing_root - else - graph.vertices.delete(name) - end - end - - # @!group AddVertex - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # @return [Boolean] whether the vertex is root or not - attr_reader :root - - # Initialize an action to add a vertex to a dependency graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - # @param [Boolean] root whether the vertex is root or not - def initialize(name, payload, root) - @name = name - @payload = payload - @root = root - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb deleted file mode 100644 index 1e62c0a0b6..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#delete_edge) - class DeleteEdge < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :delete_edge - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges.delete(edge) - edge.destination.incoming_edges.delete(edge) - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # @!group DeleteEdge - - # @return [String] the name of the origin of the edge - attr_reader :origin_name - - # @return [String] the name of the destination of the edge - attr_reader :destination_name - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new( - graph.vertex_named(origin_name), - graph.vertex_named(destination_name), - requirement - ) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin_name the name of the origin of the edge - # @param [String] destination_name the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin_name, destination_name, requirement) - @origin_name = origin_name - @destination_name = destination_name - @requirement = requirement - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb deleted file mode 100644 index 6132f969b9..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#detach_vertex_named - class DetachVertexNamed < Action - # @!group Action - - # (see Action#name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - return [] unless @vertex = graph.vertices.delete(name) - - removed_vertices = [@vertex] - @vertex.outgoing_edges.each do |e| - v = e.destination - v.incoming_edges.delete(e) - if !v.root? && v.incoming_edges.empty? - removed_vertices.concat graph.detach_vertex_named(v.name) - end - end - - @vertex.incoming_edges.each do |e| - v = e.origin - v.outgoing_edges.delete(e) - end - - removed_vertices - end - - # (see Action#down) - def down(graph) - return unless @vertex - graph.vertices[@vertex.name] = @vertex - @vertex.outgoing_edges.each do |e| - e.destination.incoming_edges << e - end - @vertex.incoming_edges.each do |e| - e.origin.outgoing_edges << e - end - end - - # @!group DetachVertexNamed - - # @return [String] the name of the vertex to detach - attr_reader :name - - # Initialize an action to detach a vertex from a dependency graph - # @param [String] name the name of the vertex to detach - def initialize(name) - @name = name - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb deleted file mode 100644 index 6954c4b1f8..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require_relative 'add_edge_no_circular' -require_relative 'add_vertex' -require_relative 'delete_edge' -require_relative 'detach_vertex_named' -require_relative 'set_payload' -require_relative 'tag' - -module Gem::Molinillo - class DependencyGraph - # A log for dependency graph actions - class Log - # Initializes an empty log - def initialize - @current_action = @first_action = nil - end - - # @!macro [new] action - # {include:DependencyGraph#$0} - # @param [Graph] graph the graph to perform the action on - # @param (see DependencyGraph#$0) - # @return (see DependencyGraph#$0) - - # @macro action - def tag(graph, tag) - push_action(graph, Tag.new(tag)) - end - - # @macro action - def add_vertex(graph, name, payload, root) - push_action(graph, AddVertex.new(name, payload, root)) - end - - # @macro action - def detach_vertex_named(graph, name) - push_action(graph, DetachVertexNamed.new(name)) - end - - # @macro action - def add_edge_no_circular(graph, origin, destination, requirement) - push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement)) - end - - # {include:DependencyGraph#delete_edge} - # @param [Graph] graph the graph to perform the action on - # @param [String] origin_name - # @param [String] destination_name - # @param [Object] requirement - # @return (see DependencyGraph#delete_edge) - def delete_edge(graph, origin_name, destination_name, requirement) - push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement)) - end - - # @macro action - def set_payload(graph, name, payload) - push_action(graph, SetPayload.new(name, payload)) - end - - # Pops the most recent action from the log and undoes the action - # @param [DependencyGraph] graph - # @return [Action] the action that was popped off the log - def pop!(graph) - return unless action = @current_action - unless @current_action = action.previous - @first_action = nil - end - action.down(graph) - action - end - - extend Enumerable - - # @!visibility private - # Enumerates each action in the log - # @yield [Action] - def each - return enum_for unless block_given? - action = @first_action - loop do - break unless action - yield action - action = action.next - end - self - end - - # @!visibility private - # Enumerates each action in the log in reverse order - # @yield [Action] - def reverse_each - return enum_for(:reverse_each) unless block_given? - action = @current_action - loop do - break unless action - yield action - action = action.previous - end - self - end - - # @macro action - def rewind_to(graph, tag) - loop do - action = pop!(graph) - raise "No tag #{tag.inspect} found" unless action - break if action.class.action_name == :tag && action.tag == tag - end - end - - private - - # Adds the given action to the log, running the action - # @param [DependencyGraph] graph - # @param [Action] action - # @return The value returned by `action.up` - def push_action(graph, action) - action.previous = @current_action - @current_action.next = action if @current_action - @current_action = action - @first_action ||= action - action.up(graph) - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb deleted file mode 100644 index 9bcaaae0f9..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#set_payload - class SetPayload < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :set_payload - end - - # (see Action#up) - def up(graph) - vertex = graph.vertex_named(name) - @old_payload = vertex.payload - vertex.payload = payload - end - - # (see Action#down) - def down(graph) - graph.vertex_named(name).payload = @old_payload - end - - # @!group SetPayload - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # Initialize an action to add set the payload for a vertex in a dependency - # graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - def initialize(name, payload) - @name = name - @payload = payload - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb deleted file mode 100644 index 62f243a2af..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#tag - class Tag < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :tag - end - - # (see Action#up) - def up(graph) - end - - # (see Action#down) - def down(graph) - end - - # @!group Tag - - # @return [Object] An opaque tag - attr_reader :tag - - # Initialize an action to tag a state of a dependency graph - # @param [Object] tag an opaque tag - def initialize(tag) - @tag = tag - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb deleted file mode 100644 index 074de369be..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - class DependencyGraph - # A vertex in a {DependencyGraph} that encapsulates a {#name} and a - # {#payload} - class Vertex - # @return [String] the name of the vertex - attr_accessor :name - - # @return [Object] the payload the vertex holds - attr_accessor :payload - - # @return [Array<Object>] the explicit requirements that required - # this vertex - attr_reader :explicit_requirements - - # @return [Boolean] whether the vertex is considered a root vertex - attr_accessor :root - alias root? root - - # Initializes a vertex with the given name and payload. - # @param [String] name see {#name} - # @param [Object] payload see {#payload} - def initialize(name, payload) - @name = name.frozen? ? name : name.dup.freeze - @payload = payload - @explicit_requirements = [] - @outgoing_edges = [] - @incoming_edges = [] - end - - # @return [Array<Object>] all of the requirements that required - # this vertex - def requirements - (incoming_edges.map(&:requirement) + explicit_requirements).uniq - end - - # @return [Array<Edge>] the edges of {#graph} that have `self` as their - # {Edge#origin} - attr_accessor :outgoing_edges - - # @return [Array<Edge>] the edges of {#graph} that have `self` as their - # {Edge#destination} - attr_accessor :incoming_edges - - # @return [Array<Vertex>] the vertices of {#graph} that have an edge with - # `self` as their {Edge#destination} - def predecessors - incoming_edges.map(&:origin) - end - - # @return [Set<Vertex>] the vertices of {#graph} where `self` is a - # {#descendent?} - def recursive_predecessors - _recursive_predecessors - end - - # @param [Set<Vertex>] vertices the set to add the predecessors to - # @return [Set<Vertex>] the vertices of {#graph} where `self` is a - # {#descendent?} - def _recursive_predecessors(vertices = new_vertex_set) - incoming_edges.each do |edge| - vertex = edge.origin - next unless vertices.add?(vertex) - vertex._recursive_predecessors(vertices) - end - - vertices - end - protected :_recursive_predecessors - - # @return [Array<Vertex>] the vertices of {#graph} that have an edge with - # `self` as their {Edge#origin} - def successors - outgoing_edges.map(&:destination) - end - - # @return [Set<Vertex>] the vertices of {#graph} where `self` is an - # {#ancestor?} - def recursive_successors - _recursive_successors - end - - # @param [Set<Vertex>] vertices the set to add the successors to - # @return [Set<Vertex>] the vertices of {#graph} where `self` is an - # {#ancestor?} - def _recursive_successors(vertices = new_vertex_set) - outgoing_edges.each do |edge| - vertex = edge.destination - next unless vertices.add?(vertex) - vertex._recursive_successors(vertices) - end - - vertices - end - protected :_recursive_successors - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{name}(#{payload.inspect})" - end - - # @return [Boolean] whether the two vertices are equal, determined - # by a recursive traversal of each {Vertex#successors} - def ==(other) - return true if equal?(other) - shallow_eql?(other) && - successors.to_set == other.successors.to_set - end - - # @param [Vertex] other the other vertex to compare to - # @return [Boolean] whether the two vertices are equal, determined - # solely by {#name} and {#payload} equality - def shallow_eql?(other) - return true if equal?(other) - other && - name == other.name && - payload == other.payload - end - - alias eql? == - - # @return [Fixnum] a hash for the vertex based upon its {#name} - def hash - name.hash - end - - # Is there a path from `self` to `other` following edges in the - # dependency graph? - # @return whether there is a path following edges within this {#graph} - def path_to?(other) - _path_to?(other) - end - - alias descendent? path_to? - - # @param [Vertex] other the vertex to check if there's a path to - # @param [Set<Vertex>] visited the vertices of {#graph} that have been visited - # @return [Boolean] whether there is a path to `other` from `self` - def _path_to?(other, visited = new_vertex_set) - return false unless visited.add?(self) - return true if equal?(other) - successors.any? { |v| v._path_to?(other, visited) } - end - protected :_path_to? - - # Is there a path from `other` to `self` following edges in the - # dependency graph? - # @return whether there is a path following edges within this {#graph} - def ancestor?(other) - other.path_to?(self) - end - - alias is_reachable_from? ancestor? - - def new_vertex_set - require 'set' - Set.new - end - private :new_vertex_set - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb deleted file mode 100644 index 07ea5fdf37..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # An error that occurred during the resolution process - class ResolverError < StandardError; end - - # An error caused by searching for a dependency that is completely unknown, - # i.e. has no versions available whatsoever. - class NoSuchDependencyError < ResolverError - # @return [Object] the dependency that could not be found - attr_accessor :dependency - - # @return [Array<Object>] the specifications that depended upon {#dependency} - attr_accessor :required_by - - # Initializes a new error with the given missing dependency. - # @param [Object] dependency @see {#dependency} - # @param [Array<Object>] required_by @see {#required_by} - def initialize(dependency, required_by = []) - @dependency = dependency - @required_by = required_by.uniq - super() - end - - # The error message for the missing dependency, including the specifications - # that had this dependency. - def message - sources = required_by.map { |r| "`#{r}`" }.join(' and ') - message = "Unable to find a specification for `#{dependency}`" - message += " depended upon by #{sources}" unless sources.empty? - message - end - end - - # An error caused by attempting to fulfil a dependency that was circular - # - # @note This exception will be thrown if and only if a {Vertex} is added to a - # {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an - # existing {DependencyGraph::Vertex} - class CircularDependencyError < ResolverError - # [Set<Object>] the dependencies responsible for causing the error - attr_reader :dependencies - - # Initializes a new error with the given circular vertices. - # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency - # that caused the error - def initialize(vertices) - super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" - @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set - end - end - - # An error caused by conflicts in version - class VersionConflict < ResolverError - # @return [{String => Resolution::Conflict}] the conflicts that caused - # resolution to fail - attr_reader :conflicts - - # @return [SpecificationProvider] the specification provider used during - # resolution - attr_reader :specification_provider - - # Initializes a new error with the given version conflicts. - # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} - # @param [SpecificationProvider] specification_provider see {#specification_provider} - def initialize(conflicts, specification_provider) - pairs = [] - conflicts.values.flat_map(&:requirements).each do |conflicting| - conflicting.each do |source, conflict_requirements| - conflict_requirements.each do |c| - pairs << [c, source] - end - end - end - - super "Unable to satisfy the following requirements:\n\n" \ - "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" - - @conflicts = conflicts - @specification_provider = specification_provider - end - - require_relative 'delegates/specification_provider' - include Delegates::SpecificationProvider - - # @return [String] An error message that includes requirement trees, - # which is much more detailed & customizable than the default message - # @param [Hash] opts the options to create a message with. - # @option opts [String] :solver_name The user-facing name of the solver - # @option opts [String] :possibility_type The generic name of a possibility - # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees - # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements - # @option opts [Proc] :additional_message_for_conflict A proc that appends additional - # messages for each conflict - # @option opts [Proc] :version_for_spec A proc that returns the version number for a - # possibility - def message_with_trees(opts = {}) - solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } - possibility_type = opts.delete(:possibility_type) { 'possibility named' } - reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } - printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } - additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } - version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } - incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do - proc do |name, _conflict| - %(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":) - end - end - - full_message_for_conflict = opts.delete(:full_message_for_conflict) do - proc do |name, conflict| - o = "\n".dup << incompatible_version_message_for_conflict.call(name, conflict) << "\n" - if conflict.locked_requirement - o << %( In snapshot (#{name_for_locking_dependency_source}):\n) - o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) - o << %(\n) - end - o << %( In #{name_for_explicit_dependency_source}:\n) - trees = reduce_trees.call(conflict.requirement_trees) - - o << trees.map do |tree| - t = ''.dup - depth = 2 - tree.each do |req| - t << ' ' * depth << printable_requirement.call(req) - unless tree.last == req - if spec = conflict.activated_by_name[name_for(req)] - t << %( was resolved to #{version_for_spec.call(spec)}, which) - end - t << %( depends on) - end - t << %(\n) - depth += 1 - end - t - end.join("\n") - - additional_message_for_conflict.call(o, name, conflict) - - o - end - end - - conflicts.sort.reduce(''.dup) do |o, (name, conflict)| - o << full_message_for_conflict.call(name, conflict) - end.strip - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb deleted file mode 100644 index 8ed3a920a2..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # The version of Gem::Molinillo. - VERSION = '0.8.0'.freeze -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb deleted file mode 100644 index 85860902fc..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # Provides information about specifications and dependencies to the resolver, - # allowing the {Resolver} class to remain generic while still providing power - # and flexibility. - # - # This module contains the methods that users of Gem::Molinillo must to implement, - # using knowledge of their own model classes. - module SpecificationProvider - # Search for the specifications that match the given dependency. - # The specifications in the returned array will be considered in reverse - # order, so the latest version ought to be last. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [Array<Object>] the specifications that satisfy the given - # `dependency`. - def search_for(dependency) - [] - end - - # Returns the dependencies of `specification`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `specification` parameter. - # - # @param [Object] specification - # @return [Array<Object>] the dependencies that are required by the given - # `specification`. - def dependencies_for(specification) - [] - end - - # Determines whether the given `requirement` is satisfied by the given - # `spec`, in the context of the current `activated` dependency graph. - # - # @param [Object] requirement - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [Object] spec - # @return [Boolean] whether `requirement` is satisfied by `spec` in the - # context of the current `activated` dependency graph. - def requirement_satisfied_by?(requirement, activated, spec) - true - end - - # Determines whether two arrays of dependencies are equal, and thus can be - # grouped. - # - # @param [Array<Object>] dependencies - # @param [Array<Object>] other_dependencies - # @return [Boolean] whether `dependencies` and `other_dependencies` should - # be considered equal. - def dependencies_equal?(dependencies, other_dependencies) - dependencies == other_dependencies - end - - # Returns the name for the given `dependency`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [String] the name for the given `dependency`. - def name_for(dependency) - dependency.to_s - end - - # @return [String] the name of the source of explicit dependencies, i.e. - # those passed to {Resolver#resolve} directly. - def name_for_explicit_dependency_source - 'user-specified dependency' - end - - # @return [String] the name of the source of 'locked' dependencies, i.e. - # those passed to {Resolver#resolve} directly as the `base` - def name_for_locking_dependency_source - 'Lockfile' - end - - # Sort dependencies so that the ones that are easiest to resolve are first. - # Easiest to resolve is (usually) defined by: - # 1) Is this dependency already activated? - # 2) How relaxed are the requirements? - # 3) Are there any conflicts for this dependency? - # 4) How many possibilities are there to satisfy this dependency? - # - # @param [Array<Object>] dependencies - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [{String => Array<Conflict>}] conflicts - # @return [Array<Object>] a sorted copy of `dependencies`. - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by do |dependency| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - conflicts[name] ? 0 : 1, - ] - end - end - - # Returns whether this dependency, which has no possible matching - # specifications, can safely be ignored. - # - # @param [Object] dependency - # @return [Boolean] whether this dependency can safely be skipped. - def allow_missing?(dependency) - false - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb deleted file mode 100644 index 464722902e..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # Conveys information about the resolution process to a user. - module UI - # The {IO} object that should be used to print output. `STDOUT`, by default. - # - # @return [IO] - def output - STDOUT - end - - # Called roughly every {#progress_rate}, this method should convey progress - # to the user. - # - # @return [void] - def indicate_progress - output.print '.' unless debug? - end - - # How often progress should be conveyed to the user via - # {#indicate_progress}, in seconds. A third of a second, by default. - # - # @return [Float] - def progress_rate - 0.33 - end - - # Called before resolution begins. - # - # @return [void] - def before_resolution - output.print 'Resolving dependencies...' - end - - # Called after resolution ends (either successfully or with an error). - # By default, prints a newline. - # - # @return [void] - def after_resolution - output.puts - end - - # Conveys debug information to the user. - # - # @param [Integer] depth the current depth of the resolution process. - # @return [void] - def debug(depth = 0) - if debug? - debug_info = yield - debug_info = debug_info.inspect unless debug_info.is_a?(String) - debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } - output.puts debug_info - end - end - - # Whether or not debug messages should be printed. - # By default, whether or not the `MOLINILLO_DEBUG` environment variable is - # set. - # - # @return [Boolean] - def debug? - return @debug_mode if defined?(@debug_mode) - @debug_mode = ENV['MOLINILLO_DEBUG'] - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb deleted file mode 100644 index 84ec6cb095..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb +++ /dev/null @@ -1,839 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - class Resolver - # A specific resolution from a given {Resolver} - class Resolution - # A conflict that the resolution process encountered - # @attr [Object] requirement the requirement that immediately led to the conflict - # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict - # @attr [Object, nil] existing the existing spec that was in conflict with - # the {#possibility} - # @attr [Object] possibility_set the set of specs that was unable to be - # activated due to a conflict. - # @attr [Object] locked_requirement the relevant locking requirement. - # @attr [Array<Array<Object>>] requirement_trees the different requirement - # trees that led to every requirement for the conflicting name. - # @attr [{String=>Object}] activated_by_name the already-activated specs. - # @attr [Object] underlying_error an error that has occurred during resolution, and - # will be raised at the end of it if no resolution is found. - Conflict = Struct.new( - :requirement, - :requirements, - :existing, - :possibility_set, - :locked_requirement, - :requirement_trees, - :activated_by_name, - :underlying_error - ) - - class Conflict - # @return [Object] a spec that was unable to be activated due to a conflict - def possibility - possibility_set && possibility_set.latest_version - end - end - - # A collection of possibility states that share the same dependencies - # @attr [Array] dependencies the dependencies for this set of possibilities - # @attr [Array] possibilities the possibilities - PossibilitySet = Struct.new(:dependencies, :possibilities) - - class PossibilitySet - # String representation of the possibility set, for debugging - def to_s - "[#{possibilities.join(', ')}]" - end - - # @return [Object] most up-to-date dependency in the possibility set - def latest_version - possibilities.last - end - end - - # Details of the state to unwind to when a conflict occurs, and the cause of the unwind - # @attr [Integer] state_index the index of the state to unwind to - # @attr [Object] state_requirement the requirement of the state we're unwinding to - # @attr [Array] requirement_tree for the requirement we're relaxing - # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict - # @attr [Array] requirement_trees for the conflict - # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind - UnwindDetails = Struct.new( - :state_index, - :state_requirement, - :requirement_tree, - :conflicting_requirements, - :requirement_trees, - :requirements_unwound_to_instead - ) - - class UnwindDetails - include Comparable - - # We compare UnwindDetails when choosing which state to unwind to. If - # two options have the same state_index we prefer the one most - # removed from a requirement that caused the conflict. Both options - # would unwind to the same state, but a `grandparent` option will - # filter out fewer of its possibilities after doing so - where a state - # is both a `parent` and a `grandparent` to requirements that have - # caused a conflict this is the correct behaviour. - # @param [UnwindDetail] other UnwindDetail to be compared - # @return [Integer] integer specifying ordering - def <=>(other) - if state_index > other.state_index - 1 - elsif state_index == other.state_index - reversed_requirement_tree_index <=> other.reversed_requirement_tree_index - else - -1 - end - end - - # @return [Integer] index of state requirement in reversed requirement tree - # (the conflicting requirement itself will be at position 0) - def reversed_requirement_tree_index - @reversed_requirement_tree_index ||= - if state_requirement - requirement_tree.reverse.index(state_requirement) - else - 999_999 - end - end - - # @return [Boolean] where the requirement of the state we're unwinding - # to directly caused the conflict. Note: in this case, it is - # impossible for the state we're unwinding to be a parent of - # any of the other conflicting requirements (or we would have - # circularity) - def unwinding_to_primary_requirement? - requirement_tree.last == state_requirement - end - - # @return [Array] array of sub-dependencies to avoid when choosing a - # new possibility for the state we've unwound to. Only relevant for - # non-primary unwinds - def sub_dependencies_to_avoid - @requirements_to_avoid ||= - requirement_trees.map do |tree| - index = tree.index(state_requirement) - tree[index + 1] if index - end.compact - end - - # @return [Array] array of all the requirements that led to the need for - # this unwind - def all_requirements - @all_requirements ||= requirement_trees.flatten(1) - end - end - - # @return [SpecificationProvider] the provider that knows about - # dependencies, requirements, specifications, versions, etc. - attr_reader :specification_provider - - # @return [UI] the UI that knows how to communicate feedback about the - # resolution process back to the user - attr_reader :resolver_ui - - # @return [DependencyGraph] the base dependency graph to which - # dependencies should be 'locked' - attr_reader :base - - # @return [Array] the dependencies that were explicitly required - attr_reader :original_requested - - # Initializes a new resolution. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui see {#resolver_ui} - # @param [Array] requested see {#original_requested} - # @param [DependencyGraph] base see {#base} - def initialize(specification_provider, resolver_ui, requested, base) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - @original_requested = requested - @base = base - @states = [] - @iteration_counter = 0 - @parents_of = Hash.new { |h, k| h[k] = [] } - end - - # Resolves the {#original_requested} dependencies into a full dependency - # graph - # @raise [ResolverError] if successful resolution is impossible - # @return [DependencyGraph] the dependency graph of successfully resolved - # dependencies - def resolve - start_resolution - - while state - break if !state.requirement && state.requirements.empty? - indicate_progress - if state.respond_to?(:pop_possibility_state) # DependencyState - debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } - state.pop_possibility_state.tap do |s| - if s - states.push(s) - activated.tag(s) - end - end - end - process_topmost_state - end - - resolve_activated_specs - ensure - end_resolution - end - - # @return [Integer] the number of resolver iterations in between calls to - # {#resolver_ui}'s {UI#indicate_progress} method - attr_accessor :iteration_rate - private :iteration_rate - - # @return [Time] the time at which resolution began - attr_accessor :started_at - private :started_at - - # @return [Array<ResolutionState>] the stack of states for the resolution - attr_accessor :states - private :states - - private - - # Sets up the resolution process - # @return [void] - def start_resolution - @started_at = Time.now - - push_initial_state - - debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" } - resolver_ui.before_resolution - end - - def resolve_activated_specs - activated.vertices.each do |_, vertex| - next unless vertex.payload - - latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| - vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } - end - - activated.set_payload(vertex.name, latest_version) - end - activated.freeze - end - - # Ends the resolution process - # @return [void] - def end_resolution - resolver_ui.after_resolution - debug do - "Finished resolution (#{@iteration_counter} steps) " \ - "(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})" - end - debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state - debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state - end - - require_relative 'state' - require_relative 'modules/specification_provider' - - require_relative 'delegates/resolution_state' - require_relative 'delegates/specification_provider' - - include Gem::Molinillo::Delegates::ResolutionState - include Gem::Molinillo::Delegates::SpecificationProvider - - # Processes the topmost available {RequirementState} on the stack - # @return [void] - def process_topmost_state - if possibility - attempt_to_activate - else - create_conflict - unwind_for_conflict - end - rescue CircularDependencyError => underlying_error - create_conflict(underlying_error) - unwind_for_conflict - end - - # @return [Object] the current possibility that the resolution is trying - # to activate - def possibility - possibilities.last - end - - # @return [RequirementState] the current state the resolution is - # operating upon - def state - states.last - end - - # Creates and pushes the initial state for the resolution, based upon the - # {#requested} dependencies - # @return [void] - def push_initial_state - graph = DependencyGraph.new.tap do |dg| - original_requested.each do |requested| - vertex = dg.add_vertex(name_for(requested), nil, true) - vertex.explicit_requirements << requested - end - dg.tag(:initial_state) - end - - push_state_for_requirements(original_requested, true, graph) - end - - # Unwinds the states stack because a conflict has been encountered - # @return [void] - def unwind_for_conflict - details_for_unwind = build_details_for_unwind - unwind_options = unused_unwind_options - debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } - conflicts.tap do |c| - sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) - raise_error_unless_state(c) - activated.rewind_to(sliced_states.first || :initial_state) if sliced_states - state.conflicts = c - state.unused_unwind_options = unwind_options - filter_possibilities_after_unwind(details_for_unwind) - index = states.size - 1 - @parents_of.each { |_, a| a.reject! { |i| i >= index } } - state.unused_unwind_options.reject! { |uw| uw.state_index >= index } - end - end - - # Raises a VersionConflict error, or any underlying error, if there is no - # current state - # @return [void] - def raise_error_unless_state(conflicts) - return if state - - error = conflicts.values.map(&:underlying_error).compact.first - raise error || VersionConflict.new(conflicts, specification_provider) - end - - # @return [UnwindDetails] Details of the nearest index to which we could unwind - def build_details_for_unwind - # Get the possible unwinds for the current conflict - current_conflict = conflicts[name] - binding_requirements = binding_requirements_for_conflict(current_conflict) - unwind_details = unwind_options_for_requirements(binding_requirements) - - last_detail_for_current_unwind = unwind_details.sort.last - current_detail = last_detail_for_current_unwind - - # Look for past conflicts that could be unwound to affect the - # requirement tree for the current conflict - all_reqs = last_detail_for_current_unwind.all_requirements - all_reqs_size = all_reqs.size - relevant_unused_unwinds = unused_unwind_options.select do |alternative| - diff_reqs = all_reqs - alternative.requirements_unwound_to_instead - next if diff_reqs.size == all_reqs_size - # Find the highest index unwind whilst looping through - current_detail = alternative if alternative > current_detail - alternative - end - - # Add the current unwind options to the `unused_unwind_options` array. - # The "used" option will be filtered out during `unwind_for_conflict`. - state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } - - # Update the requirements_unwound_to_instead on any relevant unused unwinds - relevant_unused_unwinds.each do |d| - (d.requirements_unwound_to_instead << current_detail.state_requirement).uniq! - end - unwind_details.each do |d| - (d.requirements_unwound_to_instead << current_detail.state_requirement).uniq! - end - - current_detail - end - - # @param [Array<Object>] binding_requirements array of requirements that combine to create a conflict - # @return [Array<UnwindDetails>] array of UnwindDetails that have a chance - # of resolving the passed requirements - def unwind_options_for_requirements(binding_requirements) - unwind_details = [] - - trees = [] - binding_requirements.reverse_each do |r| - partial_tree = [r] - trees << partial_tree - unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) - - # If this requirement has alternative possibilities, check if any would - # satisfy the other requirements that created this conflict - requirement_state = find_state_for(r) - if conflict_fixing_possibilities?(requirement_state, binding_requirements) - unwind_details << UnwindDetails.new( - states.index(requirement_state), - r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Next, look at the parent of this requirement, and check if the requirement - # could have been avoided if an alternative PossibilitySet had been chosen - parent_r = parent_of(r) - next if parent_r.nil? - partial_tree.unshift(parent_r) - requirement_state = find_state_for(parent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - parent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Finally, look at the grandparent and up of this requirement, looking - # for any possibilities that wouldn't create their parent requirement - grandparent_r = parent_of(parent_r) - until grandparent_r.nil? - partial_tree.unshift(grandparent_r) - requirement_state = find_state_for(grandparent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - grandparent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - parent_r = grandparent_r - grandparent_r = parent_of(parent_r) - end - end - - unwind_details - end - - # @param [DependencyState] state - # @param [Array] binding_requirements array of requirements - # @return [Boolean] whether or not the given state has any possibilities - # that could satisfy the given requirements - def conflict_fixing_possibilities?(state, binding_requirements) - return false unless state - - state.possibilities.any? do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, binding_requirements) - end - end - end - - # Filter's a state's possibilities to remove any that would not fix the - # conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just - # unwound from - # @return [void] - def filter_possibilities_after_unwind(unwind_details) - return unless state && !state.possibilities.empty? - - if unwind_details.unwinding_to_primary_requirement? - filter_possibilities_for_primary_unwind(unwind_details) - else - filter_possibilities_for_parent_unwind(unwind_details) - end - end - - # Filter's a state's possibilities to remove any that would not satisfy - # the requirements in the conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_primary_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements) - - state.possibilities.reject! do |possibility_set| - possibility_set.possibilities.none? do |poss| - unwind_requirement_sets.any? do |requirements| - possibility_satisfies_requirements?(poss, requirements) - end - end - end - end - - # @param [Object] possibility a single possibility - # @param [Array] requirements an array of requirements - # @return [Boolean] whether the possibility satisfies all of the - # given requirements - def possibility_satisfies_requirements?(possibility, requirements) - name = name_for(possibility) - - activated.tag(:swap) - activated.set_payload(name, possibility) if activated.vertex_named(name) - satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } - activated.rewind_to(:swap) - - satisfied - end - - # Filter's a state's possibilities to remove any that would (eventually) - # create a requirement in the conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_parent_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - - primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq - parent_unwinds = unwinds_to_state.uniq - primary_unwinds - - allowed_possibility_sets = primary_unwinds.flat_map do |unwind| - states[unwind.state_index].possibilities.select do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) - end - end - end - - requirements_to_avoid = parent_unwinds.flat_map(&:sub_dependencies_to_avoid) - - state.possibilities.reject! do |possibility_set| - !allowed_possibility_sets.include?(possibility_set) && - (requirements_to_avoid - possibility_set.dependencies).empty? - end - end - - # @param [Conflict] conflict - # @return [Array] minimal array of requirements that would cause the passed - # conflict to occur. - def binding_requirements_for_conflict(conflict) - return [conflict.requirement] if conflict.possibility.nil? - - possible_binding_requirements = conflict.requirements.values.flatten(1).uniq - - # When there's a `CircularDependency` error the conflicting requirement - # (the one causing the circular) won't be `conflict.requirement` - # (which won't be for the right state, because we won't have created it, - # because it's circular). - # We need to make sure we have that requirement in the conflict's list, - # otherwise we won't be able to unwind properly, so we just return all - # the requirements for the conflict. - return possible_binding_requirements if conflict.underlying_error - - possibilities = search_for(conflict.requirement) - - # If all the requirements together don't filter out all possibilities, - # then the only two requirements we need to consider are the initial one - # (where the dependency's version was first chosen) and the last - if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) - return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact - end - - # Loop through the possible binding requirements, removing each one - # that doesn't bind. Use a `reverse_each` as we want the earliest set of - # binding requirements, and don't use `reject!` as we wish to refine the - # array *on each iteration*. - binding_requirements = possible_binding_requirements.dup - possible_binding_requirements.reverse_each do |req| - next if req == conflict.requirement - unless binding_requirement_in_set?(req, binding_requirements, possibilities) - binding_requirements -= [req] - end - end - - binding_requirements - end - - # @param [Object] requirement we wish to check - # @param [Array] possible_binding_requirements array of requirements - # @param [Array] possibilities array of possibilities the requirements will be used to filter - # @return [Boolean] whether or not the given requirement is required to filter - # out all elements of the array of possibilities. - def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) - possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) - end - end - - # @param [Object] requirement - # @return [Object] the requirement that led to `requirement` being added - # to the list of requirements. - def parent_of(requirement) - return unless requirement - return unless index = @parents_of[requirement].last - return unless parent_state = @states[index] - parent_state.requirement - end - - # @param [String] name - # @return [Object] the requirement that led to a version of a possibility - # with the given name being activated. - def requirement_for_existing_name(name) - return nil unless vertex = activated.vertex_named(name) - return nil unless vertex.payload - states.find { |s| s.name == name }.requirement - end - - # @param [Object] requirement - # @return [ResolutionState] the state whose `requirement` is the given - # `requirement`. - def find_state_for(requirement) - return nil unless requirement - states.find { |i| requirement == i.requirement } - end - - # @param [Object] underlying_error - # @return [Conflict] a {Conflict} that reflects the failure to activate - # the {#possibility} in conjunction with the current {#state} - def create_conflict(underlying_error = nil) - vertex = activated.vertex_named(name) - locked_requirement = locked_requirement_named(name) - - requirements = {} - unless vertex.explicit_requirements.empty? - requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements - end - requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement - vertex.incoming_edges.each do |edge| - (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) - end - - activated_by_name = {} - activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } - conflicts[name] = Conflict.new( - requirement, - requirements, - vertex.payload && vertex.payload.latest_version, - possibility, - locked_requirement, - requirement_trees, - activated_by_name, - underlying_error - ) - end - - # @return [Array<Array<Object>>] The different requirement - # trees that led to every requirement for the current spec. - def requirement_trees - vertex = activated.vertex_named(name) - vertex.requirements.map { |r| requirement_tree_for(r) } - end - - # @param [Object] requirement - # @return [Array<Object>] the list of requirements that led to - # `requirement` being required. - def requirement_tree_for(requirement) - tree = [] - while requirement - tree.unshift(requirement) - requirement = parent_of(requirement) - end - tree - end - - # Indicates progress roughly once every second - # @return [void] - def indicate_progress - @iteration_counter += 1 - @progress_rate ||= resolver_ui.progress_rate - if iteration_rate.nil? - if Time.now - started_at >= @progress_rate - self.iteration_rate = @iteration_counter - end - end - - if iteration_rate && (@iteration_counter % iteration_rate) == 0 - resolver_ui.indicate_progress - end - end - - # Calls the {#resolver_ui}'s {UI#debug} method - # @param [Integer] depth the depth of the {#states} stack - # @param [Proc] block a block that yields a {#to_s} - # @return [void] - def debug(depth = 0, &block) - resolver_ui.debug(depth, &block) - end - - # Attempts to activate the current {#possibility} - # @return [void] - def attempt_to_activate - debug(depth) { 'Attempting to activate ' + possibility.to_s } - existing_vertex = activated.vertex_named(name) - if existing_vertex.payload - debug(depth) { "Found existing spec (#{existing_vertex.payload})" } - attempt_to_filter_existing_spec(existing_vertex) - else - latest = possibility.latest_version - possibility.possibilities.select! do |possibility| - requirement_satisfied_by?(requirement, activated, possibility) - end - if possibility.latest_version.nil? - # ensure there's a possibility for better error messages - possibility.possibilities << latest if latest - create_conflict - unwind_for_conflict - else - activate_new_spec - end - end - end - - # Attempts to update the existing vertex's `PossibilitySet` with a filtered version - # @return [void] - def attempt_to_filter_existing_spec(vertex) - filtered_set = filtered_possibility_set(vertex) - if !filtered_set.possibilities.empty? - activated.set_payload(name, filtered_set) - new_requirements = requirements.dup - push_state_for_requirements(new_requirements, false) - else - create_conflict - debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } - unwind_for_conflict - end - end - - # Generates a filtered version of the existing vertex's `PossibilitySet` using the - # current state's `requirement` - # @param [Object] vertex existing vertex - # @return [PossibilitySet] filtered possibility set - def filtered_possibility_set(vertex) - PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities) - end - - # @param [String] requirement_name the spec name to search for - # @return [Object] the locked spec named `requirement_name`, if one - # is found on {#base} - def locked_requirement_named(requirement_name) - vertex = base.vertex_named(requirement_name) - vertex && vertex.payload - end - - # Add the current {#possibility} to the dependency graph of the current - # {#state} - # @return [void] - def activate_new_spec - conflicts.delete(name) - debug(depth) { "Activated #{name} at #{possibility}" } - activated.set_payload(name, possibility) - require_nested_dependencies_for(possibility) - end - - # Requires the dependencies that the recently activated spec has - # @param [Object] possibility_set the PossibilitySet that has just been - # activated - # @return [void] - def require_nested_dependencies_for(possibility_set) - nested_dependencies = dependencies_for(possibility_set.latest_version) - debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } - nested_dependencies.each do |d| - activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) - parent_index = states.size - 1 - parents = @parents_of[d] - parents << parent_index if parents.empty? - end - - push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?) - end - - # Pushes a new {DependencyState} that encapsulates both existing and new - # requirements - # @param [Array] new_requirements - # @param [Boolean] requires_sort - # @param [Object] new_activated - # @return [void] - def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) - new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort - new_requirement = nil - loop do - new_requirement = new_requirements.shift - break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } - end - new_name = new_requirement ? name_for(new_requirement) : ''.freeze - possibilities = possibilities_for_requirement(new_requirement) - handle_missing_or_push_dependency_state DependencyState.new( - new_name, new_requirements, new_activated, - new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup - ) - end - - # Checks a proposed requirement with any existing locked requirement - # before generating an array of possibilities for it. - # @param [Object] requirement the proposed requirement - # @param [Object] activated - # @return [Array] possibilities - def possibilities_for_requirement(requirement, activated = self.activated) - return [] unless requirement - if locked_requirement_named(name_for(requirement)) - return locked_requirement_possibility_set(requirement, activated) - end - - group_possibilities(search_for(requirement)) - end - - # @param [Object] requirement the proposed requirement - # @param [Object] activated - # @return [Array] possibility set containing only the locked requirement, if any - def locked_requirement_possibility_set(requirement, activated = self.activated) - all_possibilities = search_for(requirement) - locked_requirement = locked_requirement_named(name_for(requirement)) - - # Longwinded way to build a possibilities array with either the locked - # requirement or nothing in it. Required, since the API for - # locked_requirement isn't guaranteed. - locked_possibilities = all_possibilities.select do |possibility| - requirement_satisfied_by?(locked_requirement, activated, possibility) - end - - group_possibilities(locked_possibilities) - end - - # Build an array of PossibilitySets, with each element representing a group of - # dependency versions that all have the same sub-dependency version constraints - # and are contiguous. - # @param [Array] possibilities an array of possibilities - # @return [Array<PossibilitySet>] an array of possibility sets - def group_possibilities(possibilities) - possibility_sets = [] - current_possibility_set = nil - - possibilities.reverse_each do |possibility| - dependencies = dependencies_for(possibility) - if current_possibility_set && dependencies_equal?(current_possibility_set.dependencies, dependencies) - current_possibility_set.possibilities.unshift(possibility) - else - possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility])) - current_possibility_set = possibility_sets.first - end - end - - possibility_sets - end - - # Pushes a new {DependencyState}. - # If the {#specification_provider} says to - # {SpecificationProvider#allow_missing?} that particular requirement, and - # there are no possibilities for that requirement, then `state` is not - # pushed, and the vertex in {#activated} is removed, and we continue - # resolving the remaining requirements. - # @param [DependencyState] state - # @return [void] - def handle_missing_or_push_dependency_state(state) - if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement) - state.activated.detach_vertex_named(state.name) - push_state_for_requirements(state.requirements.dup, false, state.activated) - else - states.push(state).tap { activated.tag(state) } - end - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb deleted file mode 100644 index 86229c3fa1..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative 'dependency_graph' - -module Gem::Molinillo - # This class encapsulates a dependency resolver. - # The resolver is responsible for determining which set of dependencies to - # activate, with feedback from the {#specification_provider} - # - # - class Resolver - require_relative 'resolution' - - # @return [SpecificationProvider] the specification provider used - # in the resolution process - attr_reader :specification_provider - - # @return [UI] the UI module used to communicate back to the user - # during the resolution process - attr_reader :resolver_ui - - # Initializes a new resolver. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui - # see {#resolver_ui} - def initialize(specification_provider, resolver_ui) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - end - - # Resolves the requested dependencies into a {DependencyGraph}, - # locking to the base dependency graph (if specified) - # @param [Array] requested an array of 'requested' dependencies that the - # {#specification_provider} can understand - # @param [DependencyGraph,nil] base the base dependency graph to which - # dependencies should be 'locked' - def resolve(requested, base = DependencyGraph.new) - Resolution.new(specification_provider, - resolver_ui, - requested, - base). - resolve - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb deleted file mode 100644 index c48ec6af9c..0000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # A state that a {Resolution} can be in - # @attr [String] name the name of the current requirement - # @attr [Array<Object>] requirements currently unsatisfied requirements - # @attr [DependencyGraph] activated the graph of activated dependencies - # @attr [Object] requirement the current requirement - # @attr [Object] possibilities the possibilities to satisfy the current requirement - # @attr [Integer] depth the depth of the resolution - # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name - # @attr [Array<UnwindDetails>] unused_unwind_options unwinds for previous conflicts that weren't explored - ResolutionState = Struct.new( - :name, - :requirements, - :activated, - :requirement, - :possibilities, - :depth, - :conflicts, - :unused_unwind_options - ) - - class ResolutionState - # Returns an empty resolution state - # @return [ResolutionState] an empty state - def self.empty - new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) - end - end - - # A state that encapsulates a set of {#requirements} with an {Array} of - # possibilities - class DependencyState < ResolutionState - # Removes a possibility from `self` - # @return [PossibilityState] a state with a single possibility, - # the possibility that was removed from `self` - def pop_possibility_state - PossibilityState.new( - name, - requirements.dup, - activated, - requirement, - [possibilities.pop], - depth + 1, - conflicts.dup, - unused_unwind_options.dup - ).tap do |state| - state.activated.tag(state) - end - end - end - - # A state that encapsulates a single possibility to fulfill the given - # {#requirement} - class PossibilityState < ResolutionState - end -end diff --git a/lib/rubygems/vendor/net-http/lib/net/http.rb b/lib/rubygems/vendor/net-http/lib/net/http.rb index ad3e646cca..4800cd25f1 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http.rb @@ -46,7 +46,7 @@ module Gem::Net #:nodoc: # == Strategies # # - If you will make only a few GET requests, - # consider using {OpenURI}[rdoc-ref:OpenURI]. + # consider using {OpenURI}[https://docs.ruby-lang.org/en/master/OpenURI.html]. # - If you will make only a few requests of all kinds, # consider using the various singleton convenience methods in this class. # Each of the following methods automatically starts and finishes @@ -102,14 +102,14 @@ module Gem::Net #:nodoc: # # == URIs # - # On the internet, a URI + # On the internet, a Gem::URI # ({Universal Resource Identifier}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]) # is a string that identifies a particular resource. # It consists of some or all of: scheme, hostname, path, query, and fragment; - # see {URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax]. + # see {Gem::URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax]. # - # A Ruby {Gem::URI::Generic}[rdoc-ref:Gem::URI::Generic] object - # represents an internet URI. + # A Ruby {Gem::URI::Generic}[https://docs.ruby-lang.org/en/master/Gem::URI/Generic.html] object + # represents an internet Gem::URI. # It provides, among others, methods # +scheme+, +hostname+, +path+, +query+, and +fragment+. # @@ -144,7 +144,7 @@ module Gem::Net #:nodoc: # # === Queries # - # A host-specific query adds name/value pairs to the URI: + # A host-specific query adds name/value pairs to the Gem::URI: # # _uri = uri.dup # params = {userId: 1, completed: false} @@ -154,7 +154,7 @@ module Gem::Net #:nodoc: # # === Fragments # - # A {URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect + # A {Gem::URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect # in \Gem::Net::HTTP; # the same data is returned, regardless of whether a fragment is included. # @@ -327,9 +327,9 @@ module Gem::Net #:nodoc: # res = http.request(req) # end # - # Or if you simply want to make a GET request, you may pass in a URI + # Or if you simply want to make a GET request, you may pass in a Gem::URI # object that has an \HTTPS URL. \Gem::Net::HTTP automatically turns on TLS - # verification if the URI object has a 'https' :URI scheme: + # verification if the Gem::URI object has a 'https' Gem::URI scheme: # # uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/> # Gem::Net::HTTP.get(uri) @@ -374,7 +374,7 @@ module Gem::Net #:nodoc: # # When environment variable <tt>'http_proxy'</tt> # is set to a \Gem::URI string, - # the returned +http+ will have the server at that URI as its proxy; + # the returned +http+ will have the server at that Gem::URI as its proxy; # note that the \Gem::URI string must have a protocol # such as <tt>'http'</tt> or <tt>'https'</tt>: # @@ -460,7 +460,7 @@ module Gem::Net #:nodoc: # # First, what's elsewhere. Class Gem::Net::HTTP: # - # - Inherits from {class Object}[rdoc-ref:Object@What-27s+Here]. + # - Inherits from {class Object}[https://docs.ruby-lang.org/en/master/Object.html#class-Object-label-What-27s+Here]. # # This is a categorized summary of methods and attributes. # @@ -475,8 +475,7 @@ module Gem::Net #:nodoc: # # - {::start}[rdoc-ref:Gem::Net::HTTP.start]: # Begins a new session in a new \Gem::Net::HTTP object. - # - {#started?}[rdoc-ref:Gem::Net::HTTP#started?] - # (aliased as {#active?}[rdoc-ref:Gem::Net::HTTP#active?]): + # - {#started?}[rdoc-ref:Gem::Net::HTTP#started?]: # Returns whether in a session. # - {#finish}[rdoc-ref:Gem::Net::HTTP#finish]: # Ends an active session. @@ -556,18 +555,15 @@ module Gem::Net #:nodoc: # Sends a PUT request and returns a response object. # - {#request}[rdoc-ref:Gem::Net::HTTP#request]: # Sends a request and returns a response object. - # - {#request_get}[rdoc-ref:Gem::Net::HTTP#request_get] - # (aliased as {#get2}[rdoc-ref:Gem::Net::HTTP#get2]): + # - {#request_get}[rdoc-ref:Gem::Net::HTTP#request_get]: # Sends a GET request and forms a response object; # if a block given, calls the block with the object, # otherwise returns the object. - # - {#request_head}[rdoc-ref:Gem::Net::HTTP#request_head] - # (aliased as {#head2}[rdoc-ref:Gem::Net::HTTP#head2]): + # - {#request_head}[rdoc-ref:Gem::Net::HTTP#request_head]: # Sends a HEAD request and forms a response object; # if a block given, calls the block with the object, # otherwise returns the object. - # - {#request_post}[rdoc-ref:Gem::Net::HTTP#request_post] - # (aliased as {#post2}[rdoc-ref:Gem::Net::HTTP#post2]): + # - {#request_post}[rdoc-ref:Gem::Net::HTTP#request_post]: # Sends a POST request and forms a response object; # if a block given, calls the block with the object, # otherwise returns the object. @@ -605,8 +601,7 @@ module Gem::Net #:nodoc: # Returns whether +self+ is a proxy class. # - {#proxy?}[rdoc-ref:Gem::Net::HTTP#proxy?]: # Returns whether +self+ has a proxy. - # - {#proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address] - # (aliased as {#proxyaddr}[rdoc-ref:Gem::Net::HTTP#proxyaddr]): + # - {#proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]: # Returns the proxy address. # - {#proxy_from_env?}[rdoc-ref:Gem::Net::HTTP#proxy_from_env?]: # Returns whether the proxy is taken from an environment variable. @@ -718,8 +713,7 @@ module Gem::Net #:nodoc: # === \HTTP Version # # - {::version_1_2?}[rdoc-ref:Gem::Net::HTTP.version_1_2?] - # (aliased as {::is_version_1_2?}[rdoc-ref:Gem::Net::HTTP.is_version_1_2?] - # and {::version_1_2}[rdoc-ref:Gem::Net::HTTP.version_1_2]): + # (aliased as {::version_1_2}[rdoc-ref:Gem::Net::HTTP.version_1_2]): # Returns true; retained for compatibility. # # === Debugging @@ -730,7 +724,7 @@ module Gem::Net #:nodoc: class HTTP < Protocol # :stopdoc: - VERSION = "0.6.0" + VERSION = "0.9.1" HTTPVersion = '1.1' begin require 'zlib' @@ -796,7 +790,7 @@ module Gem::Net #:nodoc: # "completed": false # } # - # With URI object +uri+ and optional hash argument +headers+: + # With Gem::URI object +uri+ and optional hash argument +headers+: # # uri = Gem::URI('https://jsonplaceholder.typicode.com/todos/1') # headers = {'Content-type' => 'application/json; charset=UTF-8'} @@ -869,7 +863,7 @@ module Gem::Net #:nodoc: # Posts data to a host; returns a Gem::Net::HTTPResponse object. # - # Argument +url+ must be a URI; + # Argument +url+ must be a Gem::URI; # argument +data+ must be a hash: # # _uri = uri.dup @@ -1185,6 +1179,7 @@ module Gem::Net #:nodoc: @debug_output = options[:debug_output] @response_body_encoding = options[:response_body_encoding] @ignore_eof = options[:ignore_eof] + @tcpsocket_supports_open_timeout = nil @proxy_from_env = false @proxy_uri = nil @@ -1293,7 +1288,7 @@ module Gem::Net #:nodoc: # - The name of an encoding. # - An alias for an encoding name. # - # See {Encoding}[rdoc-ref:Encoding]. + # See {Encoding}[https://docs.ruby-lang.org/en/master/Encoding.html]. # # Examples: # @@ -1327,6 +1322,9 @@ module Gem::Net #:nodoc: # Sets the proxy password; # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. attr_writer :proxy_pass + + # Sets whether the proxy uses SSL; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. attr_writer :proxy_use_ssl # Returns the IP address for the connection. @@ -1533,9 +1531,9 @@ module Gem::Net #:nodoc: :verify_depth, :verify_mode, :verify_hostname, - ] # :nodoc: + ].freeze # :nodoc: - SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym } # :nodoc: + SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc: # Sets or returns the path to a CA certification file in PEM format. attr_accessor :ca_file @@ -1552,11 +1550,11 @@ module Gem::Net #:nodoc: attr_accessor :cert_store # Sets or returns the available SSL ciphers. - # See {OpenSSL::SSL::SSLContext#ciphers=}[rdoc-ref:OpenSSL::SSL::SSLContext#ciphers-3D]. + # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=]. attr_accessor :ciphers # Sets or returns the extra X509 certificates to be added to the certificate chain. - # See {OpenSSL::SSL::SSLContext#add_certificate}[rdoc-ref:OpenSSL::SSL::SSLContext#add_certificate]. + # See {OpenSSL::SSL::SSLContext#add_certificate}[OpenSSL::SSL::SSL::Context#add_certificate]. attr_accessor :extra_chain_cert # Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. @@ -1566,15 +1564,15 @@ module Gem::Net #:nodoc: attr_accessor :ssl_timeout # Sets or returns the SSL version. - # See {OpenSSL::SSL::SSLContext#ssl_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#ssl_version-3D]. + # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=]. attr_accessor :ssl_version # Sets or returns the minimum SSL version. - # See {OpenSSL::SSL::SSLContext#min_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#min_version-3D]. + # See {OpenSSL::SSL::SSLContext#min_version=}[OpenSSL::SSL::SSL::Context#min_version=]. attr_accessor :min_version # Sets or returns the maximum SSL version. - # See {OpenSSL::SSL::SSLContext#max_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#max_version-3D]. + # See {OpenSSL::SSL::SSLContext#max_version=}[OpenSSL::SSL::SSL::Context#max_version=]. attr_accessor :max_version # Sets or returns the callback for the server certification verification. @@ -1590,7 +1588,7 @@ module Gem::Net #:nodoc: # Sets or returns whether to verify that the server certificate is valid # for the hostname. - # See {OpenSSL::SSL::SSLContext#verify_hostname=}[rdoc-ref:OpenSSL::SSL::SSLContext#attribute-i-verify_mode]. + # See {OpenSSL::SSL::SSLContext#verify_hostname=}[OpenSSL::SSL::SSL::Context#verify_hostname=]. attr_accessor :verify_hostname # Returns the X509 certificate chain (an array of strings) @@ -1638,6 +1636,21 @@ module Gem::Net #:nodoc: self end + # Finishes the \HTTP session: + # + # http = Gem::Net::HTTP.new(hostname) + # http.start + # http.started? # => true + # http.finish # => nil + # http.started? # => false + # + # Raises IOError if not in a session. + def finish + raise IOError, 'HTTP session not yet started' unless started? + do_finish + end + + # :stopdoc: def do_start connect @started = true @@ -1660,14 +1673,15 @@ module Gem::Net #:nodoc: end debug "opening connection to #{conn_addr}:#{conn_port}..." - s = Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) { - begin - TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) - rescue => e - raise e, "Failed to open TCP connection to " + - "#{conn_addr}:#{conn_port} (#{e.message})" + begin + s = timeouted_connect(conn_addr, conn_port) + rescue => e + if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions + e = Gem::Net::OpenTimeout.new(e) end - } + raise e, "Failed to open TCP connection to " + + "#{conn_addr}:#{conn_port} (#{e.message})" + end s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) debug "opened" if use_ssl? @@ -1760,23 +1774,30 @@ module Gem::Net #:nodoc: end private :connect - def on_connect + tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters + TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]] + tcp_socket_parameters.include?([:key, :open_timeout]) + else + # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize + # See discussion in https://github.com/ruby/net-http/pull/224 + Socket.method(:tcp).parameters.include?([:key, :open_timeout]) end - private :on_connect + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT - # Finishes the \HTTP session: - # - # http = Gem::Net::HTTP.new(hostname) - # http.start - # http.started? # => true - # http.finish # => nil - # http.started? # => false - # - # Raises IOError if not in a session. - def finish - raise IOError, 'HTTP session not yet started' unless started? - do_finish + def timeouted_connect(conn_addr, conn_port) + if TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout) + else + Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) { + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + } + end + end + private :timeouted_connect + + def on_connect end + private :on_connect def do_finish @started = false @@ -1827,6 +1848,8 @@ module Gem::Net #:nodoc: } end + # :startdoc: + class << HTTP # Returns true if self is a class which was created by HTTP::Proxy. def proxy_class? @@ -1866,7 +1889,7 @@ module Gem::Net #:nodoc: @proxy_from_env end - # The proxy URI determined from the environment for this connection. + # The proxy Gem::URI determined from the environment for this connection. def proxy_uri # :nodoc: return if @proxy_uri == false @proxy_uri ||= Gem::URI::HTTP.new( @@ -1921,9 +1944,11 @@ module Gem::Net #:nodoc: alias proxyport proxy_port #:nodoc: obsolete private + # :stopdoc: def unescape(value) - require 'cgi/util' + require 'cgi/escape' + require 'cgi/util' unless defined?(CGI::EscapeExt) CGI.unescape(value) end @@ -1948,6 +1973,7 @@ module Gem::Net #:nodoc: path end end + # :startdoc: # # HTTP operations @@ -2402,7 +2428,9 @@ module Gem::Net #:nodoc: res end - IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc: + # :stopdoc: + + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: def transport_request(req) count = 0 @@ -2559,7 +2587,7 @@ module Gem::Net #:nodoc: alias_method :D, :debug end - # for backward compatibility until Ruby 3.5 + # for backward compatibility until Ruby 4.0 # https://bugs.ruby-lang.org/issues/20900 # https://github.com/bblimke/webmock/pull/1081 HTTPSession = HTTP diff --git a/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb index c629c0113b..218df9a8bd 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb @@ -3,7 +3,7 @@ module Gem::Net # Gem::Net::HTTP exception class. # You cannot use Gem::Net::HTTPExceptions directly; instead, you must use # its subclasses. - module HTTPExceptions + module HTTPExceptions # :nodoc: def initialize(msg, res) #:nodoc: super msg @response = res @@ -12,6 +12,7 @@ module Gem::Net alias data response #:nodoc: obsolete end + # :stopdoc: class HTTPError < ProtocolError include HTTPExceptions end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb index 5cfe75a7cd..d6496d4ac1 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb @@ -19,16 +19,13 @@ class Gem::Net::HTTPGenericRequest if Gem::URI === uri_or_path then raise ArgumentError, "not an HTTP Gem::URI" unless Gem::URI::HTTP === uri_or_path - hostname = uri_or_path.hostname + hostname = uri_or_path.host raise ArgumentError, "no host component for Gem::URI" unless (hostname && hostname.length > 0) @uri = uri_or_path.dup - host = @uri.hostname.dup - host << ":" << @uri.port.to_s if @uri.port != @uri.default_port @path = uri_or_path.request_uri raise ArgumentError, "no HTTP request path given" unless @path else @uri = nil - host = nil raise ArgumentError, "no HTTP request path given" unless uri_or_path raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? @path = uri_or_path.dup @@ -51,7 +48,7 @@ class Gem::Net::HTTPGenericRequest initialize_http_header initheader self['Accept'] ||= '*/*' self['User-Agent'] ||= 'Ruby' - self['Host'] ||= host if host + self['Host'] ||= @uri.authority if @uri @body = nil @body_stream = nil @body_data = nil @@ -102,6 +99,31 @@ class Gem::Net::HTTPGenericRequest "\#<#{self.class} #{@method}>" end + # Returns a string representation of the request with the details for pp: + # + # require 'pp' + # post = Gem::Net::HTTP::Post.new(uri) + # post.inspect # => "#<Gem::Net::HTTP::Post POST>" + # post.pretty_inspect + # # => #<Gem::Net::HTTP::Post + # POST + # path="/" + # headers={"accept-encoding" => ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], + # "accept" => ["*/*"], + # "user-agent" => ["Ruby"], + # "host" => ["www.ruby-lang.org"]}> + # + def pretty_print(q) + q.object_group(self) { + q.breakable + q.text @method + q.breakable + q.text "path="; q.pp @path + q.breakable + q.text "headers="; q.pp to_hash + } + end + ## # Don't automatically decode response content-encoding if the user indicates # they want to handle it. @@ -220,7 +242,7 @@ class Gem::Net::HTTPGenericRequest end if host = self['host'] - host.sub!(/:.*/m, '') + host = Gem::URI.parse("//#{host}").host # Remove a port component from the existing Host header elsif host = @uri.host else host = addr @@ -239,6 +261,8 @@ class Gem::Net::HTTPGenericRequest private + # :stopdoc: + class Chunker #:nodoc: def initialize(sock) @sock = sock @@ -260,7 +284,6 @@ class Gem::Net::HTTPGenericRequest def send_request_with_body(sock, ver, path, body) self.content_length = body.bytesize delete 'Transfer-Encoding' - supply_default_content_type write_header sock, ver, path wait_for_continue sock, ver if sock.continue_timeout sock.write body @@ -271,7 +294,6 @@ class Gem::Net::HTTPGenericRequest raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" end - supply_default_content_type write_header sock, ver, path wait_for_continue sock, ver if sock.continue_timeout if chunked? @@ -373,12 +395,6 @@ class Gem::Net::HTTPGenericRequest buf.clear end - def supply_default_content_type - return if content_type() - warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE - set_content_type 'application/x-www-form-urlencoded' - end - ## # Waits up to the continue timeout for a response from the server provided # we're speaking HTTP 1.1 and are expecting a 100-continue response. @@ -411,4 +427,3 @@ class Gem::Net::HTTPGenericRequest end end - diff --git a/lib/rubygems/vendor/net-http/lib/net/http/header.rb b/lib/rubygems/vendor/net-http/lib/net/http/header.rb index 5cb1da01ec..bc68cd2eef 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http/header.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http/header.rb @@ -179,7 +179,9 @@ # - #each_value: Passes each string field value to the block. # module Gem::Net::HTTPHeader + # The maximum length of HTTP header keys. MAX_KEY_LENGTH = 1024 + # The maximum length of HTTP header values. MAX_FIELD_LENGTH = 65536 def initialize_http_header(initheader) #:nodoc: @@ -267,6 +269,7 @@ module Gem::Net::HTTPHeader end end + # :stopdoc: private def set_field(key, val) case val when Enumerable @@ -294,6 +297,7 @@ module Gem::Net::HTTPHeader ary.push val end end + # :startdoc: # Returns the array field value for the given +key+, # or +nil+ if there is no such field; @@ -490,7 +494,7 @@ module Gem::Net::HTTPHeader alias canonical_each each_capitalized - def capitalize(name) + def capitalize(name) # :nodoc: name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze) end private :capitalize @@ -957,12 +961,12 @@ module Gem::Net::HTTPHeader @header['proxy-authorization'] = [basic_encode(account, password)] end - def basic_encode(account, password) + def basic_encode(account, password) # :nodoc: 'Basic ' + ["#{account}:#{password}"].pack('m0') end private :basic_encode -# Returns whether the HTTP session is to be closed. + # Returns whether the HTTP session is to be closed. def connection_close? token = /(?:\A|,)\s*close\s*(?:\z|,)/i @header['connection']&.grep(token) {return true} @@ -970,7 +974,7 @@ module Gem::Net::HTTPHeader false end -# Returns whether the HTTP session is to be kept alive. + # Returns whether the HTTP session is to be kept alive. def connection_keep_alive? token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i @header['connection']&.grep(token) {return true} diff --git a/lib/rubygems/vendor/net-http/lib/net/http/requests.rb b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb index 45727e7f61..f990761042 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http/requests.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb @@ -29,6 +29,7 @@ # - Gem::Net::HTTP#get: sends +GET+ request, returns response object. # class Gem::Net::HTTP::Get < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'GET' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true @@ -60,6 +61,7 @@ end # - Gem::Net::HTTP#head: sends +HEAD+ request, returns response object. # class Gem::Net::HTTP::Head < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'HEAD' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = false @@ -95,6 +97,7 @@ end # - Gem::Net::HTTP#post: sends +POST+ request, returns response object. # class Gem::Net::HTTP::Post < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'POST' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -130,6 +133,7 @@ end # - Gem::Net::HTTP#put: sends +PUT+ request, returns response object. # class Gem::Net::HTTP::Put < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'PUT' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -162,6 +166,7 @@ end # - Gem::Net::HTTP#delete: sends +DELETE+ request, returns response object. # class Gem::Net::HTTP::Delete < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'DELETE' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true @@ -193,6 +198,7 @@ end # - Gem::Net::HTTP#options: sends +OPTIONS+ request, returns response object. # class Gem::Net::HTTP::Options < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'OPTIONS' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true @@ -224,6 +230,7 @@ end # - Gem::Net::HTTP#trace: sends +TRACE+ request, returns response object. # class Gem::Net::HTTP::Trace < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'TRACE' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true @@ -258,6 +265,7 @@ end # - Gem::Net::HTTP#patch: sends +PATCH+ request, returns response object. # class Gem::Net::HTTP::Patch < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'PATCH' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -285,6 +293,7 @@ end # - Gem::Net::HTTP#propfind: sends +PROPFIND+ request, returns response object. # class Gem::Net::HTTP::Propfind < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'PROPFIND' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -308,6 +317,7 @@ end # - Gem::Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object. # class Gem::Net::HTTP::Proppatch < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'PROPPATCH' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -331,6 +341,7 @@ end # - Gem::Net::HTTP#mkcol: sends +MKCOL+ request, returns response object. # class Gem::Net::HTTP::Mkcol < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'MKCOL' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -354,6 +365,7 @@ end # - Gem::Net::HTTP#copy: sends +COPY+ request, returns response object. # class Gem::Net::HTTP::Copy < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'COPY' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true @@ -377,6 +389,7 @@ end # - Gem::Net::HTTP#move: sends +MOVE+ request, returns response object. # class Gem::Net::HTTP::Move < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'MOVE' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true @@ -400,6 +413,7 @@ end # - Gem::Net::HTTP#lock: sends +LOCK+ request, returns response object. # class Gem::Net::HTTP::Lock < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'LOCK' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true @@ -423,8 +437,8 @@ end # - Gem::Net::HTTP#unlock: sends +UNLOCK+ request, returns response object. # class Gem::Net::HTTP::Unlock < Gem::Net::HTTPRequest + # :stopdoc: METHOD = 'UNLOCK' REQUEST_HAS_BODY = true RESPONSE_HAS_BODY = true end - diff --git a/lib/rubygems/vendor/net-http/lib/net/http/response.rb b/lib/rubygems/vendor/net-http/lib/net/http/response.rb index cbbd191d87..dc164f1504 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http/response.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http/response.rb @@ -153,6 +153,7 @@ class Gem::Net::HTTPResponse end private + # :stopdoc: def read_status_line(sock) str = sock.readline @@ -259,7 +260,7 @@ class Gem::Net::HTTPResponse # header. attr_accessor :ignore_eof - def inspect + def inspect # :nodoc: "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/responses.rb b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb index 0f26ae6c26..62ce1cba1b 100644 --- a/lib/rubygems/vendor/net-http/lib/net/http/responses.rb +++ b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb @@ -4,7 +4,9 @@ module Gem::Net + # Unknown HTTP response class HTTPUnknownResponse < HTTPResponse + # :stopdoc: HAS_BODY = true EXCEPTION_TYPE = HTTPError # end @@ -19,6 +21,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response]. # class HTTPInformation < HTTPResponse + # :stopdoc: HAS_BODY = false EXCEPTION_TYPE = HTTPError # end @@ -34,6 +37,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success]. # class HTTPSuccess < HTTPResponse + # :stopdoc: HAS_BODY = true EXCEPTION_TYPE = HTTPError # end @@ -49,6 +53,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection]. # class HTTPRedirection < HTTPResponse + # :stopdoc: HAS_BODY = true EXCEPTION_TYPE = HTTPRetriableError # end @@ -63,6 +68,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors]. # class HTTPClientError < HTTPResponse + # :stopdoc: HAS_BODY = true EXCEPTION_TYPE = HTTPClientException # end @@ -77,6 +83,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors]. # class HTTPServerError < HTTPResponse + # :stopdoc: HAS_BODY = true EXCEPTION_TYPE = HTTPFatalError # end @@ -94,6 +101,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#100]. # class HTTPContinue < HTTPInformation + # :stopdoc: HAS_BODY = false end @@ -111,6 +119,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#101]. # class HTTPSwitchProtocol < HTTPInformation + # :stopdoc: HAS_BODY = false end @@ -127,6 +136,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#102]. # class HTTPProcessing < HTTPInformation + # :stopdoc: HAS_BODY = false end @@ -145,6 +155,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#103]. # class HTTPEarlyHints < HTTPInformation + # :stopdoc: HAS_BODY = false end @@ -162,6 +173,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#200]. # class HTTPOK < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -179,6 +191,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#201]. # class HTTPCreated < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -196,6 +209,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#202]. # class HTTPAccepted < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -215,6 +229,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#203]. # class HTTPNonAuthoritativeInformation < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -232,6 +247,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#204]. # class HTTPNoContent < HTTPSuccess + # :stopdoc: HAS_BODY = false end @@ -250,6 +266,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#205]. # class HTTPResetContent < HTTPSuccess + # :stopdoc: HAS_BODY = false end @@ -268,6 +285,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#206]. # class HTTPPartialContent < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -285,6 +303,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#207]. # class HTTPMultiStatus < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -304,6 +323,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208]. # class HTTPAlreadyReported < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -321,6 +341,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#226]. # class HTTPIMUsed < HTTPSuccess + # :stopdoc: HAS_BODY = true end @@ -338,6 +359,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#300]. # class HTTPMultipleChoices < HTTPRedirection + # :stopdoc: HAS_BODY = true end HTTPMultipleChoice = HTTPMultipleChoices @@ -356,6 +378,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#301]. # class HTTPMovedPermanently < HTTPRedirection + # :stopdoc: HAS_BODY = true end @@ -373,6 +396,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#302]. # class HTTPFound < HTTPRedirection + # :stopdoc: HAS_BODY = true end HTTPMovedTemporarily = HTTPFound @@ -390,6 +414,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#303]. # class HTTPSeeOther < HTTPRedirection + # :stopdoc: HAS_BODY = true end @@ -407,6 +432,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304]. # class HTTPNotModified < HTTPRedirection + # :stopdoc: HAS_BODY = false end @@ -423,6 +449,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#305]. # class HTTPUseProxy < HTTPRedirection + # :stopdoc: HAS_BODY = false end @@ -440,6 +467,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#307]. # class HTTPTemporaryRedirect < HTTPRedirection + # :stopdoc: HAS_BODY = true end @@ -456,6 +484,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#308]. # class HTTPPermanentRedirect < HTTPRedirection + # :stopdoc: HAS_BODY = true end @@ -472,6 +501,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#400]. # class HTTPBadRequest < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -488,6 +518,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#401]. # class HTTPUnauthorized < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -504,6 +535,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#402]. # class HTTPPaymentRequired < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -521,6 +553,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#403]. # class HTTPForbidden < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -537,6 +570,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#404]. # class HTTPNotFound < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -553,6 +587,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#405]. # class HTTPMethodNotAllowed < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -570,6 +605,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#406]. # class HTTPNotAcceptable < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -586,6 +622,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#407]. # class HTTPProxyAuthenticationRequired < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -602,6 +639,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#408]. # class HTTPRequestTimeout < HTTPClientError + # :stopdoc: HAS_BODY = true end HTTPRequestTimeOut = HTTPRequestTimeout @@ -619,6 +657,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#409]. # class HTTPConflict < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -636,6 +675,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#410]. # class HTTPGone < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -653,6 +693,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#411]. # class HTTPLengthRequired < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -670,6 +711,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#412]. # class HTTPPreconditionFailed < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -686,6 +728,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#413]. # class HTTPPayloadTooLarge < HTTPClientError + # :stopdoc: HAS_BODY = true end HTTPRequestEntityTooLarge = HTTPPayloadTooLarge @@ -703,6 +746,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#414]. # class HTTPURITooLong < HTTPClientError + # :stopdoc: HAS_BODY = true end HTTPRequestURITooLong = HTTPURITooLong @@ -721,6 +765,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#415]. # class HTTPUnsupportedMediaType < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -737,6 +782,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#416]. # class HTTPRangeNotSatisfiable < HTTPClientError + # :stopdoc: HAS_BODY = true end HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable @@ -754,6 +800,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#417]. # class HTTPExpectationFailed < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -774,6 +821,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421]. # class HTTPMisdirectedRequest < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -790,6 +838,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#422]. # class HTTPUnprocessableEntity < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -805,6 +854,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#423]. # class HTTPLocked < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -821,6 +871,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424]. # class HTTPFailedDependency < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -840,6 +891,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#426]. # class HTTPUpgradeRequired < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -856,6 +908,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#428]. # class HTTPPreconditionRequired < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -872,6 +925,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429]. # class HTTPTooManyRequests < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -889,6 +943,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#431]. # class HTTPRequestHeaderFieldsTooLarge < HTTPClientError + # :stopdoc: HAS_BODY = true end @@ -906,6 +961,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#451]. # class HTTPUnavailableForLegalReasons < HTTPClientError + # :stopdoc: HAS_BODY = true end # 444 No Response - Nginx @@ -926,6 +982,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500]. # class HTTPInternalServerError < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -943,6 +1000,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#501]. # class HTTPNotImplemented < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -960,6 +1018,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#502]. # class HTTPBadGateway < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -977,6 +1036,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#503]. # class HTTPServiceUnavailable < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -994,6 +1054,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#504]. # class HTTPGatewayTimeout < HTTPServerError + # :stopdoc: HAS_BODY = true end HTTPGatewayTimeOut = HTTPGatewayTimeout @@ -1011,6 +1072,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#505]. # class HTTPVersionNotSupported < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -1027,6 +1089,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#506]. # class HTTPVariantAlsoNegotiates < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -1043,6 +1106,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#507]. # class HTTPInsufficientStorage < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -1059,6 +1123,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#508]. # class HTTPLoopDetected < HTTPServerError + # :stopdoc: HAS_BODY = true end # 509 Bandwidth Limit Exceeded - Apache bw/limited extension @@ -1076,6 +1141,7 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#510]. # class HTTPNotExtended < HTTPServerError + # :stopdoc: HAS_BODY = true end @@ -1092,19 +1158,21 @@ module Gem::Net # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#511]. # class HTTPNetworkAuthenticationRequired < HTTPServerError + # :stopdoc: HAS_BODY = true end end class Gem::Net::HTTPResponse + # :stopdoc: CODE_CLASS_TO_OBJ = { '1' => Gem::Net::HTTPInformation, '2' => Gem::Net::HTTPSuccess, '3' => Gem::Net::HTTPRedirection, '4' => Gem::Net::HTTPClientError, '5' => Gem::Net::HTTPServerError - } + }.freeze CODE_TO_OBJ = { '100' => Gem::Net::HTTPContinue, '101' => Gem::Net::HTTPSwitchProtocol, @@ -1170,5 +1238,5 @@ class Gem::Net::HTTPResponse '508' => Gem::Net::HTTPLoopDetected, '510' => Gem::Net::HTTPNotExtended, '511' => Gem::Net::HTTPNetworkAuthenticationRequired, - } + }.freeze end diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb index 537d06f229..d39d9dd4e0 100644 --- a/lib/rubygems/vendor/optparse/lib/optparse.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse.rb @@ -7,6 +7,7 @@ # # See Gem::OptionParser for documentation. # +require 'set' unless defined?(Set) #-- # == Developer Documentation (not for RDoc output) @@ -142,7 +143,7 @@ # Used: # # $ ruby optparse-test.rb -r -# optparse-test.rb:9:in `<main>': missing argument: -r (Gem::OptionParser::MissingArgument) +# optparse-test.rb:9:in '<main>': missing argument: -r (Gem::OptionParser::MissingArgument) # $ ruby optparse-test.rb -r my-library # You required my-library! # @@ -235,7 +236,7 @@ # $ ruby optparse-test.rb --user 2 # #<struct User id=2, name="Gandalf"> # $ ruby optparse-test.rb --user 3 -# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError) +# optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError) # # === Store options to a Hash # @@ -425,7 +426,8 @@ # class Gem::OptionParser # The version string - Gem::OptionParser::Version = "0.6.0" + VERSION = "0.8.0" + Version = VERSION # for compatibility # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze @@ -461,6 +463,10 @@ class Gem::OptionParser candidates end + def self.completable?(key) + String.try_convert(key) or defined?(key.id2name) + end + def candidate(key, icase = false, pat = nil, &_) Completion.candidate(key, icase, pat, &method(:each)) end @@ -496,7 +502,6 @@ class Gem::OptionParser end end - # # Map from option/keyword string to object with completion. # @@ -504,7 +509,6 @@ class Gem::OptionParser include Completion end - # # Individual switch class. Not important to the user. # @@ -546,11 +550,11 @@ class Gem::OptionParser def initialize(pattern = nil, conv = nil, short = nil, long = nil, arg = nil, - desc = ([] if short or long), block = nil, &_block) + desc = ([] if short or long), block = nil, values = nil, &_block) raise if Array === pattern block ||= _block - @pattern, @conv, @short, @long, @arg, @desc, @block = - pattern, conv, short, long, arg, desc, block + @pattern, @conv, @short, @long, @arg, @desc, @block, @values = + pattern, conv, short, long, arg, desc, block, values end # @@ -583,11 +587,15 @@ class Gem::OptionParser # exception. # def conv_arg(arg, val = []) # :nodoc: + v, = *val if conv val = conv.call(*val) else val = proc {|v| v}.call(*val) end + if @values + @values.include?(val) or raise InvalidArgument, v + end return arg, block, val end private :conv_arg @@ -668,7 +676,7 @@ class Gem::OptionParser (sopts+lopts).each do |opt| # "(-x -c -r)-l[left justify]" - if /^--\[no-\](.+)$/ =~ opt + if /\A--\[no-\](.+)$/ =~ opt o = $1 yield("--#{o}", desc.join("")) yield("--no-#{o}", desc.join("")) @@ -1032,7 +1040,6 @@ class Gem::OptionParser DefaultList.short['-'] = Switch::NoArgument.new {} DefaultList.long[''] = Switch::NoArgument.new {throw :terminate} - COMPSYS_HEADER = <<'XXX' # :nodoc: typeset -A opt_args @@ -1051,16 +1058,16 @@ XXX end def help_exit - if STDOUT.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?}) + if $stdout.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?}) less = ENV["LESS"] - args = [{"LESS" => "#{!less || less.empty? ? '-' : less}Fe"}, pager, "w"] + args = [{"LESS" => "#{less} -Fe"}, pager, "w"] print = proc do |f| f.puts help rescue Errno::EPIPE # pager terminated end if Process.respond_to?(:fork) and false - IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call(STDOUT)} + IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call($stdout)} # unreachable end IO.popen(*args, &print) @@ -1102,7 +1109,7 @@ XXX # Officious['*-completion-zsh'] = proc do |parser| Switch::OptionalArgument.new do |arg| - parser.compsys(STDOUT, arg) + parser.compsys($stdout, arg) exit end end @@ -1288,7 +1295,15 @@ XXX # to $0. # def program_name - @program_name || File.basename($0, '.*') + @program_name || strip_ext(File.basename($0)) + end + + private def strip_ext(name) # :nodoc: + exts = /#{ + require "rbconfig" + Regexp.union(*RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" ")) + }\z/o + name.sub(exts, "") end # for experimental cascading :-) @@ -1467,6 +1482,7 @@ XXX klass = nil q, a = nil has_arg = false + values = nil opts.each do |o| # argument class @@ -1480,7 +1496,7 @@ XXX end # directly specified pattern(any object possible to match) - if (!(String === o || Symbol === o)) and o.respond_to?(:match) + if !Completion.completable?(o) and o.respond_to?(:match) pattern = notwice(o, pattern, 'pattern') if pattern.respond_to?(:convert) conv = pattern.method(:convert).to_proc @@ -1494,7 +1510,12 @@ XXX case o when Proc, Method block = notwice(o, block, 'block') - when Array, Hash + when Array, Hash, Set + if Array === o + o, v = o.partition {|v,| Completion.completable?(v)} + values = notwice(v, values, 'values') unless v.empty? + next if o.empty? + end case pattern when CompletingHash when nil @@ -1504,11 +1525,13 @@ XXX raise ArgumentError, "argument pattern given twice" end o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}} + when Range + values = notwice(o, values, 'values') when Module raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4)) when *ArgumentStyle.keys style = notwice(ArgumentStyle[o], style, 'style') - when /^--no-([^\[\]=\s]*)(.+)?/ + when /\A--no-([^\[\]=\s]*)(.+)?/ q, a = $1, $2 o = notwice(a ? Object : TrueClass, klass, 'type') not_pattern, not_conv = search(:atype, o) unless not_style @@ -1519,7 +1542,7 @@ XXX (q = q.downcase).tr!('_', '-') long << "no-#{q}" nolong << q - when /^--\[no-\]([^\[\]=\s]*)(.+)?/ + when /\A--\[no-\]([^\[\]=\s]*)(.+)?/ q, a = $1, $2 o = notwice(a ? Object : TrueClass, klass, 'type') if a @@ -1532,7 +1555,7 @@ XXX not_pattern, not_conv = search(:atype, FalseClass) unless not_style not_style = Switch::NoArgument nolong << "no-#{o}" - when /^--([^\[\]=\s]*)(.+)?/ + when /\A--([^\[\]=\s]*)(.+)?/ q, a = $1, $2 if a o = notwice(NilClass, klass, 'type') @@ -1542,7 +1565,7 @@ XXX ldesc << "--#{q}" (o = q.downcase).tr!('_', '-') long << o - when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ + when /\A-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ q, a = $1, $2 o = notwice(Object, klass, 'type') if a @@ -1553,7 +1576,7 @@ XXX end sdesc << "-#{q}" short << Regexp.new(q) - when /^-(.)(.+)?/ + when /\A-(.)(.+)?/ q, a = $1, $2 if a o = notwice(NilClass, klass, 'type') @@ -1562,7 +1585,7 @@ XXX end sdesc << "-#{q}" short << q - when /^=/ + when /\A=/ style = notwice(default_style.guess(arg = o), style, 'style') default_pattern, conv = search(:atype, Object) unless default_pattern else @@ -1571,12 +1594,18 @@ XXX end default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern + if Range === values and klass + unless (!values.begin or klass === values.begin) and + (!values.end or klass === values.end) + raise ArgumentError, "range does not match class" + end + end if !(short.empty? and long.empty?) if has_arg and default_style == Switch::NoArgument default_style = Switch::RequiredArgument end s = (style || default_style).new(pattern || default_pattern, - conv, sdesc, ldesc, arg, desc, block) + conv, sdesc, ldesc, arg, desc, block, values) elsif !block if style or pattern raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller) @@ -1585,7 +1614,7 @@ XXX else short << pattern s = (style || default_style).new(pattern, - conv, nil, nil, arg, desc, block) + conv, nil, nil, arg, desc, block, values) end return s, short, long, (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style), @@ -1827,7 +1856,7 @@ XXX # def permute!(argv = default_argv, **keywords) nonopts = [] - order!(argv, **keywords, &nonopts.method(:<<)) + order!(argv, **keywords) {|nonopt| nonopts << nonopt} argv[0, 0] = nonopts argv end @@ -1880,13 +1909,16 @@ XXX single_options, *long_options = *args result = {} + setter = (symbolize_names ? + ->(name, val) {result[name.to_sym] = val} + : ->(name, val) {result[name] = val}) single_options.scan(/(.)(:)?/) do |opt, val| if val - result[opt] = nil + setter[opt, nil] define("-#{opt} VAL") else - result[opt] = false + setter[opt, false] define("-#{opt}") end end if single_options @@ -1895,16 +1927,16 @@ XXX arg, desc = arg.split(';', 2) opt, val = arg.split(':', 2) if val - result[opt] = val.empty? ? nil : val + setter[opt, (val unless val.empty?)] define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact) else - result[opt] = false + setter[opt, false] define("--#{opt}", *[desc].compact) end end - parse_in_order(argv, result.method(:[]=), **keywords) - symbolize_names ? result.transform_keys(&:to_sym) : result + parse_in_order(argv, setter, **keywords) + result end # @@ -1954,7 +1986,7 @@ XXX visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw} } exc = ambiguous ? AmbiguousOption : InvalidOption - raise exc.new(opt, additional: self.method(:additional_message).curry[typ]) + raise exc.new(opt, additional: proc {|o| additional_message(typ, o)}) end private :complete @@ -2019,19 +2051,27 @@ XXX def load(filename = nil, **keywords) unless filename basename = File.basename($0, '.*') - return true if load(File.expand_path(basename, '~/.options'), **keywords) rescue nil + return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil basename << ".options" + if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty? + # https://specifications.freedesktop.org/basedir-spec/latest/#variables + # + # If $XDG_CONFIG_HOME is either not set or empty, a default + # equal to $HOME/.config should be used. + xdg = ['~/.config', true] + end return [ - # XDG - ENV['XDG_CONFIG_HOME'], - '~/.config', + xdg, + *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR), # Haiku - '~/config/settings', - ].any? {|dir| + ['~/config/settings', true], + ].any? {|dir, expand| next if !dir or dir.empty? - load(File.expand_path(basename, dir), **keywords) rescue nil + filename = File.join(dir, basename) + filename = File.expand_path(filename) if expand + load(filename, **keywords) rescue nil } end begin @@ -2237,9 +2277,10 @@ XXX argv end + DIR = File.join(__dir__, '') def self.filter_backtrace(array) unless $DEBUG - array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~)) + array.delete_if {|bt| bt.start_with?(DIR)} end array end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb new file mode 100644 index 0000000000..818e947477 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb @@ -0,0 +1,53 @@ +require_relative "pub_grub/package" +require_relative "pub_grub/static_package_source" +require_relative "pub_grub/term" +require_relative "pub_grub/version_range" +require_relative "pub_grub/version_constraint" +require_relative "pub_grub/version_union" +require_relative "pub_grub/version_solver" +require_relative "pub_grub/incompatibility" +require_relative 'pub_grub/solve_failure' +require_relative 'pub_grub/failure_writer' +require_relative 'pub_grub/version' + +module Gem::PubGrub + # Minimal logger that doesn't require the 'logger' gem + class NullLogger + def info(&block); end + def debug(&block); end + def warn(&block); end + def error(&block); end + end + + class StderrLogger + def info(&block) + $stderr.puts "INFO: #{block.call}" if block + end + + def debug(&block) + $stderr.puts "DEBUG: #{block.call}" if block + end + + def warn(&block) + $stderr.puts "WARN: #{block.call}" if block + end + + def error(&block) + $stderr.puts "ERROR: #{block.call}" if block + end + end + + class << self + attr_writer :logger + + def logger + @logger || default_logger + end + + private + + def default_logger + @logger = $DEBUG ? StderrLogger.new : NullLogger.new + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb new file mode 100644 index 0000000000..7a11cf0933 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb @@ -0,0 +1,20 @@ +module Gem::PubGrub + class Assignment + attr_reader :term, :cause, :decision_level, :index + def initialize(term, cause, decision_level, index) + @term = term + @cause = cause + @decision_level = decision_level + @index = index + end + + def self.decision(package, version, decision_level, index) + term = Term.new(VersionConstraint.exact(package, version), true) + new(term, :decision, decision_level, index) + end + + def decision? + cause == :decision + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb new file mode 100644 index 0000000000..c8dbf2a5ab --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb @@ -0,0 +1,169 @@ +require_relative 'version_constraint' +require_relative 'incompatibility' + +module Gem::PubGrub + # Types: + # + # Where possible, Gem::PubGrub will accept user-defined types, so long as they quack. + # + # ## "Package": + # + # This class will be used to represent the various packages being solved for. + # .to_s will be called when displaying errors and debugging info, it should + # probably return the package's name. + # It must also have a reasonable definition of #== and #hash + # + # Example classes: String ("rails") + # + # + # ## "Version": + # + # This class will be used to represent a single version number. + # + # Versions don't need to store their associated package, however they will + # only be compared against other versions of the same package. + # + # It must be Comparible (and implement <=> reasonably) + # + # Example classes: Gem::Version, Integer + # + # + # ## "Dependency" + # + # This class represents the requirement one package has on another. It is + # returned by dependencies_for(package, version) and will be passed to + # parse_dependency to convert it to a format Gem::PubGrub understands. + # + # It must also have a reasonable definition of #== + # + # Example classes: String ("~> 1.0"), Gem::Requirement + # + class BasicPackageSource + # Override me! + # + # This is called per package to find all possible versions of a package. + # + # It is called at most once per-package + # + # Returns: Array of versions for a package, in preferred order of selection + def all_versions_for(package) + raise NotImplementedError + end + + # Override me! + # + # Returns: Hash in the form of { package => requirement, ... } + def dependencies_for(package, version) + raise NotImplementedError + end + + # Override me! + # + # Convert a (user-defined) dependency into a format Gem::PubGrub understands. + # + # Package is passed to this method but for many implementations is not + # needed. + # + # Returns: either a Gem::PubGrub::VersionRange, Gem::PubGrub::VersionUnion, or a + # Gem::PubGrub::VersionConstraint + def parse_dependency(package, dependency) + raise NotImplementedError + end + + # Override me! + # + # If not overridden, this will call dependencies_for with the root package. + # + # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for) + def root_dependencies + dependencies_for(@root_package, @root_version) + end + + def initialize + @root_package = Package.root + @root_version = Package.root_version + + @sorted_versions = Hash.new do |h,k| + if k == @root_package + h[k] = [@root_version] + else + h[k] = all_versions_for(k).sort + end + end + + @cached_dependencies = Hash.new do |packages, package| + if package == @root_package + packages[package] = { + @root_version => root_dependencies + } + else + packages[package] = Hash.new do |versions, version| + versions[version] = dependencies_for(package, version) + end + end + end + end + + def versions_for(package, range=VersionRange.any) + range.select_versions(@sorted_versions[package]) + end + + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Incompatibility::NoVersions.new(unsatisfied_term) + + Incompatibility.new([unsatisfied_term], cause: cause) + end + + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].map do |dep_package, dep_constraint_name| + low = high = sorted_versions.index(version) + + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package] == dep_constraint_name + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = VersionRange.new(min: low, max: high, include_min: !low.nil?) + + self_constraint = VersionConstraint.new(package, range: range) + + if !@packages.include?(dep_package) + # no such package -> this version is invalid + end + + dep_constraint = parse_dependency(dep_package, dep_constraint_name) + if !dep_constraint + # falsey indicates this dependency was invalid + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name) + return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)] + elsif !dep_constraint.is_a?(VersionConstraint) + # Upgrade range/union to VersionConstraint + dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint) + end + + Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency) + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb new file mode 100644 index 0000000000..d8bfde0286 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb @@ -0,0 +1,182 @@ +module Gem::PubGrub + class FailureWriter + def initialize(root) + @root = root + + # { Incompatibility => Integer } + @derivations = {} + + # [ [ String, Integer or nil ] ] + @lines = [] + + # { Incompatibility => Integer } + @line_numbers = {} + + count_derivations(root) + end + + def write + return @root.to_s unless @root.conflict? + + visit(@root) + + padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length + + @lines.map do |message, number| + next "" if message.empty? + + lead = number ? "(#{number}) " : "" + lead = lead.ljust(padding) + message = message.gsub("\n", "\n" + " " * (padding + 2)) + "#{lead}#{message}" + end.join("\n") + end + + private + + def write_line(incompatibility, message, numbered:) + if numbered + number = @line_numbers.length + 1 + @line_numbers[incompatibility] = number + end + + @lines << [message, number] + end + + def visit(incompatibility, conclusion: false) + raise unless incompatibility.conflict? + + numbered = conclusion || @derivations[incompatibility] > 1; + conjunction = conclusion || incompatibility == @root ? "So," : "And" + + cause = incompatibility.cause + + if cause.conflict.conflict? && cause.other.conflict? + conflict_line = @line_numbers[cause.conflict] + other_line = @line_numbers[cause.other] + + if conflict_line && other_line + write_line( + incompatibility, + "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif conflict_line || other_line + with_line = conflict_line ? cause.conflict : cause.other + without_line = conflict_line ? cause.other : cause.conflict + line = @line_numbers[with_line] + + visit(without_line); + write_line( + incompatibility, + "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.", + numbered: numbered + ) + else + single_line_conflict = single_line?(cause.conflict.cause) + single_line_other = single_line?(cause.other.cause) + + if single_line_conflict || single_line_other + first = single_line_other ? cause.conflict : cause.other + second = single_line_other ? cause.other : cause.conflict + visit(first) + visit(second) + write_line( + incompatibility, + "Thus, #{incompatibility}.", + numbered: numbered + ) + else + visit(cause.conflict, conclusion: true) + @lines << ["", nil] + visit(cause.other) + + write_line( + incompatibility, + "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.", + numbered: numbered + ) + end + end + elsif cause.conflict.conflict? || cause.other.conflict? + derived = cause.conflict.conflict? ? cause.conflict : cause.other + ext = cause.conflict.conflict? ? cause.other : cause.conflict + + derived_line = @line_numbers[derived] + if derived_line + write_line( + incompatibility, + "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif collapsible?(derived) + derived_cause = derived.cause + if derived_cause.conflict.conflict? + collapsed_derived = derived_cause.conflict + collapsed_ext = derived_cause.other + else + collapsed_derived = derived_cause.other + collapsed_ext = derived_cause.conflict + end + + visit(collapsed_derived) + + write_line( + incompatibility, + "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.", + numbered: numbered + ) + else + visit(derived) + write_line( + incompatibility, + "#{conjunction} because #{ext},\n#{incompatibility}.", + numbered: numbered + ) + end + else + write_line( + incompatibility, + "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.", + numbered: numbered + ) + end + end + + def single_line?(cause) + !cause.conflict.conflict? && !cause.other.conflict? + end + + def collapsible?(incompatibility) + return false if @derivations[incompatibility] > 1 + + cause = incompatibility.cause + # If incompatibility is derived from two derived incompatibilities, + # there are too many transitive causes to display concisely. + return false if cause.conflict.conflict? && cause.other.conflict? + + # If incompatibility is derived from two external incompatibilities, it + # tends to be confusing to collapse it. + return false unless cause.conflict.conflict? || cause.other.conflict? + + # If incompatibility's internal cause is numbered, collapsing it would + # get too noisy. + complex = cause.conflict.conflict? ? cause.conflict : cause.other + + !@line_numbers.has_key?(complex) + end + + def count_derivations(incompatibility) + if @derivations.has_key?(incompatibility) + @derivations[incompatibility] += 1 + else + @derivations[incompatibility] = 1 + if incompatibility.conflict? + cause = incompatibility.cause + count_derivations(cause.conflict) + count_derivations(cause.other) + end + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb new file mode 100644 index 0000000000..b5652b5e01 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb @@ -0,0 +1,150 @@ +module Gem::PubGrub + class Incompatibility + ConflictCause = Struct.new(:incompatibility, :satisfier) do + alias_method :conflict, :incompatibility + alias_method :other, :satisfier + end + + InvalidDependency = Struct.new(:package, :constraint) do + end + + NoVersions = Struct.new(:constraint) do + end + + attr_reader :terms, :cause + + def initialize(terms, cause:, custom_explanation: nil) + @cause = cause + @terms = cleanup_terms(terms) + @custom_explanation = custom_explanation + + if cause == :dependency && @terms.length != 2 + raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}" + end + end + + def hash + cause.hash ^ terms.hash + end + + def eql?(other) + cause.eql?(other.cause) && + terms.eql?(other.terms) + end + + def failure? + terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?) + end + + def conflict? + ConflictCause === cause + end + + # Returns all external incompatibilities in this incompatibility's + # derivation graph + def external_incompatibilities + if conflict? + [ + cause.conflict, + cause.other + ].flat_map(&:external_incompatibilities) + else + [this] + end + end + + def to_s + return @custom_explanation if @custom_explanation + + case cause + when :root + "(root dependency)" + when :dependency + "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}" + when Gem::PubGrub::Incompatibility::InvalidDependency + "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}" + when Gem::PubGrub::Incompatibility::NoVersions + "no versions satisfy #{cause.constraint}" + when Gem::PubGrub::Incompatibility::ConflictCause + if failure? + "version solving has failed" + elsif terms.length == 1 + term = terms[0] + if term.positive? + if term.constraint.any? + "#{term.package} cannot be used" + else + "#{term.to_s(allow_every: true)} cannot be used" + end + else + "#{term.invert} is required" + end + else + if terms.all?(&:positive?) + if terms.length == 2 + "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}" + else + "one of #{terms.map(&:to_s).join(" or ")} must be false" + end + elsif terms.all?(&:negative?) + if terms.length == 2 + "either #{terms[0].invert} or #{terms[1].invert}" + else + "one of #{terms.map(&:invert).join(" or ")} must be true"; + end + else + positive = terms.select(&:positive?) + negative = terms.select(&:negative?).map(&:invert) + + if positive.length == 1 + "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}" + else + "if #{positive.join(" and ")} then #{negative.join(" or ")}" + end + end + end + else + raise "unhandled cause: #{cause.inspect}" + end + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def pretty_print(q) + q.group 2, "#<#{self.class}", ">" do + q.breakable + q.text to_s + + q.breakable + q.text " caused by " + q.pp @cause + end + end + + private + + def cleanup_terms(terms) + terms.each do |term| + raise "#{term.inspect} must be a term" unless term.is_a?(Term) + end + + if terms.length != 1 && ConflictCause === cause + terms = terms.reject do |term| + term.positive? && Package.root?(term.package) + end + end + + # Optimized simple cases + return terms if terms.length <= 1 + return terms if terms.length == 2 && terms[0].package != terms[1].package + + terms.group_by(&:package).map do |package, common_terms| + common_terms.inject do |acc, term| + acc.intersect(term) + end + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb new file mode 100644 index 0000000000..6baa908f60 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class Package + + attr_reader :name + + def initialize(name) + @name = name + end + + def inspect + "#<#{self.class} #{name.inspect}>" + end + + def <=>(other) + name <=> other.name + end + + ROOT = Package.new(:root) + ROOT_VERSION = 0 + + def self.root + ROOT + end + + def self.root_version + ROOT_VERSION + end + + def self.root?(package) + if package.respond_to?(:root?) + package.root? + else + package == root + end + end + + def to_s + name.to_s + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb new file mode 100644 index 0000000000..f6a6ae6964 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb @@ -0,0 +1,121 @@ +require_relative 'assignment' + +module Gem::PubGrub + class PartialSolution + attr_reader :assignments, :decisions + attr_reader :attempted_solutions + + def initialize + reset! + + @attempted_solutions = 1 + @backtracking = false + end + + def decision_level + @decisions.length + end + + def relation(term) + package = term.package + return :overlap if !@terms.key?(package) + + @relation_cache[package][term] ||= + @terms[package].relation(term) + end + + def satisfies?(term) + relation(term) == :subset + end + + def derive(term, cause) + add_assignment(Assignment.new(term, cause, decision_level, assignments.length)) + end + + def satisfier(term) + assignment = + @assignments_by[term.package].bsearch do |assignment_by| + @cumulative_assignments[assignment_by].satisfies?(term) + end + + assignment || raise("#{term} unsatisfied") + end + + # A list of unsatisfied terms + def unsatisfied + @required.keys.reject do |package| + @decisions.key?(package) + end.map do |package| + @terms[package] + end + end + + def decide(package, version) + @attempted_solutions += 1 if @backtracking + @backtracking = false; + + decisions[package] = version + assignment = Assignment.decision(package, version, decision_level, assignments.length) + add_assignment(assignment) + end + + def backtrack(previous_level) + @backtracking = true + + new_assignments = assignments.select do |assignment| + assignment.decision_level <= previous_level + end + + new_decisions = Hash[decisions.first(previous_level)] + + reset! + + @decisions = new_decisions + + new_assignments.each do |assignment| + add_assignment(assignment) + end + end + + private + + def reset! + # { Array<Assignment> } + @assignments = [] + + # { Package => Array<Assignment> } + @assignments_by = Hash.new { |h,k| h[k] = [] } + @cumulative_assignments = {}.compare_by_identity + + # { Package => Package::Version } + @decisions = {} + + # { Package => Term } + @terms = {} + @relation_cache = Hash.new { |h,k| h[k] = {} } + + # { Package => Boolean } + @required = {} + end + + def add_assignment(assignment) + term = assignment.term + package = term.package + + @assignments << assignment + @assignments_by[package] << assignment + + @required[package] = true if term.positive? + + if @terms.key?(package) + old_term = @terms[package] + @terms[package] = old_term.intersect(term) + else + @terms[package] = term + end + @relation_cache[package].clear + + @cumulative_assignments[assignment] = @terms[package] + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb new file mode 100644 index 0000000000..60ca3ca2ea --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb @@ -0,0 +1,45 @@ +module Gem::PubGrub + module RubyGems + extend self + + def requirement_to_range(requirement) + ranges = requirement.requirements.map do |(op, ver)| + case op + when "~>" + name = "~> #{ver}" + bump = ver.class.new(ver.bump.to_s + ".A") + VersionRange.new(name: name, min: ver, max: bump, include_min: true) + when ">" + VersionRange.new(min: ver) + when ">=" + VersionRange.new(min: ver, include_min: true) + when "<" + VersionRange.new(max: ver) + when "<=" + VersionRange.new(max: ver, include_max: true) + when "=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true) + when "!=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert + else + raise "bad version specifier: #{op}" + end + end + + ranges.inject(&:intersect) + end + + def requirement_to_constraint(package, requirement) + Gem::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement)) + end + + def parse_range(dep) + requirement_to_range(Gem::Requirement.new(dep)) + end + + def parse_constraint(package, dep) + range = parse_range(dep) + Gem::PubGrub::VersionConstraint.new(package, range: range) + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb new file mode 100644 index 0000000000..c4181d2b25 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb @@ -0,0 +1,19 @@ +require_relative 'failure_writer' + +module Gem::PubGrub + class SolveFailure < StandardError + attr_reader :incompatibility + + def initialize(incompatibility) + @incompatibility = incompatibility + end + + def to_s + "Could not find compatible versions\n\n#{explanation}" + end + + def explanation + @explanation ||= FailureWriter.new(@incompatibility).write + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb new file mode 100644 index 0000000000..9e1de7d7a1 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb @@ -0,0 +1,61 @@ +require_relative 'package' +require_relative 'rubygems' +require_relative 'version_constraint' +require_relative 'incompatibility' +require_relative 'basic_package_source' + +module Gem::PubGrub + class StaticPackageSource < BasicPackageSource + class DSL + def initialize(packages, root_deps) + @packages = packages + @root_deps = root_deps + end + + def root(deps:) + @root_deps.update(deps) + end + + def add(name, version, deps: {}) + version = Gem::Version.new(version) + @packages[name] ||= {} + raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version) + @packages[name][version] = clean_deps(name, version, deps) + end + + private + + # Exclude redundant self-referencing dependencies + def clean_deps(name, version, deps) + deps.reject {|dep_name, req| name == dep_name && Gem::PubGrub::RubyGems.parse_range(req).include?(version) } + end + end + + def initialize + @root_deps = {} + @packages = {} + + yield DSL.new(@packages, @root_deps) + + super() + end + + def all_versions_for(package) + @packages[package].keys + end + + def root_dependencies + @root_deps + end + + def dependencies_for(package, version) + @packages[package][version] + end + + def parse_dependency(package, dependency) + return false unless @packages.key?(package) + + Gem::PubGrub::RubyGems.parse_constraint(package, dependency) + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb new file mode 100644 index 0000000000..b9874cdece --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb @@ -0,0 +1,42 @@ +module Gem::PubGrub + class Strategy + def initialize(source) + @source = source + + @root_package = Package.root + @root_version = Package.root_version + + @version_indexes = Hash.new do |h,k| + if k == @root_package + h[k] = { @root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + + indexes = @version_indexes[package] + versions.min_by { |version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb new file mode 100644 index 0000000000..bb26bdc911 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb @@ -0,0 +1,105 @@ +module Gem::PubGrub + class Term + attr_reader :package, :constraint, :positive + + def initialize(constraint, positive) + @constraint = constraint + @package = @constraint.package + @positive = positive + end + + def to_s(allow_every: false) + if positive + @constraint.to_s(allow_every: allow_every) + else + "not #{@constraint}" + end + end + + def hash + constraint.hash ^ positive.hash + end + + def eql?(other) + positive == other.positive && + constraint.eql?(other.constraint) + end + + def invert + self.class.new(@constraint, !@positive) + end + alias_method :inverse, :invert + + def intersect(other) + raise ArgumentError, "packages must match" if package != other.package + + if positive? && other.positive? + self.class.new(constraint.intersect(other.constraint), true) + elsif negative? && other.negative? + self.class.new(constraint.union(other.constraint), false) + else + positive = positive? ? self : other + negative = negative? ? self : other + self.class.new(positive.constraint.intersect(negative.constraint.invert), true) + end + end + + def difference(other) + intersect(other.invert) + end + + def relation(other) + if positive? && other.positive? + constraint.relation(other.constraint) + elsif negative? && other.positive? + if constraint.allows_all?(other.constraint) + :disjoint + else + :overlap + end + elsif positive? && other.negative? + if !other.constraint.allows_any?(constraint) + :subset + elsif other.constraint.allows_all?(constraint) + :disjoint + else + :overlap + end + elsif negative? && other.negative? + if constraint.allows_all?(other.constraint) + :subset + else + :overlap + end + else + raise + end + end + + def normalized_constraint + @normalized_constraint ||= positive ? constraint : constraint.invert + end + + def satisfies?(other) + raise ArgumentError, "packages must match" unless package == other.package + + relation(other) == :subset + end + + def positive? + @positive + end + + def negative? + !positive? + end + + def empty? + @empty ||= normalized_constraint.empty? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb new file mode 100644 index 0000000000..5701bf0656 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb @@ -0,0 +1,3 @@ +module Gem::PubGrub + VERSION = "0.5.0" +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb new file mode 100644 index 0000000000..ee998b3271 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb @@ -0,0 +1,129 @@ +require_relative 'version_range' + +module Gem::PubGrub + class VersionConstraint + attr_reader :package, :range + + # @param package [Gem::PubGrub::Package] + # @param range [Gem::PubGrub::VersionRange] + def initialize(package, range: nil) + @package = package + @range = range + end + + def hash + package.hash ^ range.hash + end + + def ==(other) + package == other.package && + range == other.range + end + + def eql?(other) + package.eql?(other.package) && + range.eql?(other.range) + end + + class << self + def exact(package, version) + range = VersionRange.new(min: version, max: version, include_min: true, include_max: true) + new(package, range: range) + end + + def any(package) + new(package, range: VersionRange.any) + end + + def empty(package) + new(package, range: VersionRange.empty) + end + end + + def intersect(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.intersect(other.range)) + end + + def union(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.union(other.range)) + end + + def invert + new_range = range.invert + self.class.new(package, range: new_range) + end + + def difference(other) + intersect(other.invert) + end + + def allows_all?(other) + range.allows_all?(other.range) + end + + def allows_any?(other) + range.intersects?(other.range) + end + + def subset?(other) + other.allows_all?(self) + end + + def overlap?(other) + other.allows_any?(self) + end + + def disjoint?(other) + !overlap?(other) + end + + def relation(other) + if subset?(other) + :subset + elsif overlap?(other) + :overlap + else + :disjoint + end + end + + def to_s(allow_every: false) + if Package.root?(package) + package.to_s + elsif allow_every && any? + "every version of #{package}" + else + "#{package} #{constraint_string}" + end + end + + def constraint_string + if any? + ">= 0" + else + range.to_s + end + end + + def empty? + range.empty? + end + + # Does this match every version of the package + def any? + range.any? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb new file mode 100644 index 0000000000..fa0e2d5742 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb @@ -0,0 +1,423 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class VersionRange + attr_reader :min, :max, :include_min, :include_max + + alias_method :include_min?, :include_min + alias_method :include_max?, :include_max + + class Empty < VersionRange + undef_method :min, :max + undef_method :include_min, :include_min? + undef_method :include_max, :include_max? + + def initialize + end + + def empty? + true + end + + def eql?(other) + other.empty? + end + + def hash + [].hash + end + + def intersects?(_) + false + end + + def intersect(other) + self + end + + def allows_all?(other) + other.empty? + end + + def include?(_) + false + end + + def any? + false + end + + def to_s + "(no versions)" + end + + def ==(other) + other.class == self.class + end + + def invert + VersionRange.any + end + + def select_versions(_) + [] + end + end + + EMPTY = Empty.new + Empty.singleton_class.undef_method(:new) + + def self.empty + EMPTY + end + + def self.any + new + end + + def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil) + raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true + raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true + + @min = min + @max = max + @include_min = include_min + @include_max = include_max + @name = name + end + + def hash + @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash + end + + def eql?(other) + if other.is_a?(VersionRange) + !other.empty? && + min.eql?(other.min) && + max.eql?(other.max) && + include_min.eql?(other.include_min) && + include_max.eql?(other.include_max) + else + ranges.eql?(other.ranges) + end + end + + def ranges + [self] + end + + def include?(version) + compare_version(version) == 0 + end + + # Partitions passed versions into [lower, within, higher] + # + # versions must be sorted + def partition_versions(versions) + min_index = + if !min || versions.empty? + 0 + elsif include_min? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min } + end + + lower = versions.slice(0, min_index) + versions = versions.slice(min_index, versions.size) + + max_index = + if !max || versions.empty? + versions.size + elsif include_max? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max } + end + + [ + lower, + versions.slice(0, max_index), + versions.slice(max_index, versions.size) + ] + end + + # Returns versions which are included by this range. + # + # versions must be sorted + def select_versions(versions) + return versions if any? + + partition_versions(versions)[1] + end + + def compare_version(version) + if min + case version <=> min + when -1 + return -1 + when 0 + return -1 if !include_min + when 1 + end + end + + if max + case version <=> max + when -1 + when 0 + return 1 if !include_max + when 1 + return 1 + end + end + + 0 + end + + def strictly_lower?(other) + return false if !max || !other.min + + case max <=> other.min + when 0 + !include_max || !other.include_min + when -1 + true + when 1 + false + end + end + + def strictly_higher?(other) + other.strictly_lower?(self) + end + + def intersects?(other) + return false if other.empty? + return other.intersects?(self) if other.is_a?(VersionUnion) + !strictly_lower?(other) && !strictly_higher?(other) + end + alias_method :allows_any?, :intersects? + + def intersect(other) + return other if other.empty? + return other.intersect(self) if other.is_a?(VersionUnion) + + min_range = + if !min + other + elsif !other.min + self + else + case min <=> other.min + when 0 + include_min ? other : self + when -1 + other + when 1 + self + end + end + + max_range = + if !max + other + elsif !other.max + self + else + case max <=> other.max + when 0 + include_max ? other : self + when -1 + self + when 1 + other + end + end + + if !min_range.equal?(max_range) && min_range.min && max_range.max + case min_range.min <=> max_range.max + when -1 + when 0 + if !min_range.include_min || !max_range.include_max + return EMPTY + end + when 1 + return EMPTY + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + # The span covered by two ranges + # + # If self and other are contiguous, this builds a union of the two ranges. + # (if they aren't you are probably calling the wrong method) + def span(other) + return self if other.empty? + + min_range = + if !min + self + elsif !other.min + other + else + case min <=> other.min + when 0 + include_min ? self : other + when -1 + self + when 1 + other + end + end + + max_range = + if !max + self + elsif !other.max + other + else + case max <=> other.max + when 0 + include_max ? self : other + when -1 + other + when 1 + self + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + def union(other) + return other.union(self) if other.is_a?(VersionUnion) + + if contiguous_to?(other) + span(other) + else + VersionUnion.union([self, other]) + end + end + + def contiguous_to?(other) + return false if other.empty? + return true if any? + + intersects?(other) || contiguous_below?(other) || contiguous_above?(other) + end + + def contiguous_below?(other) + return false if !max || !other.min + + max == other.min && (include_max || other.include_min) + end + + def contiguous_above?(other) + other.contiguous_below?(self) + end + + def allows_all?(other) + return true if other.empty? + + if other.is_a?(VersionUnion) + return VersionUnion.new([self]).allows_all?(other) + end + + return false if max && !other.max + return false if min && !other.min + + if min + case min <=> other.min + when -1 + when 0 + return false if !include_min && other.include_min + when 1 + return false + end + end + + if max + case max <=> other.max + when -1 + return false + when 0 + return false if !include_max && other.include_max + when 1 + end + end + + true + end + + def any? + !min && !max + end + + def empty? + false + end + + def to_s + @name ||= constraints.join(", ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def upper_invert + return self.class.empty unless max + + VersionRange.new(min: max, include_min: !include_max) + end + + def invert + return self.class.empty if any? + + low = -> { VersionRange.new(max: min, include_max: !include_min) } + high = -> { VersionRange.new(min: max, include_min: !include_max) } + + if !min + high.call + elsif !max + low.call + else + low.call.union(high.call) + end + end + + def ==(other) + self.class == other.class && + min == other.min && + max == other.max && + include_min == other.include_min && + include_max == other.include_max + end + + private + + def constraints + return ["any"] if any? + return ["= #{min}"] if min.to_s == max.to_s + + c = [] + c << "#{include_min ? ">=" : ">"} #{min}" if min + c << "#{include_max ? "<=" : "<"} #{max}" if max + c + end + + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb new file mode 100644 index 0000000000..3341d8fe3b --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb @@ -0,0 +1,236 @@ +require_relative 'partial_solution' +require_relative 'term' +require_relative 'incompatibility' +require_relative 'solve_failure' +require_relative 'strategy' + +module Gem::PubGrub + class VersionSolver + attr_reader :logger + attr_reader :source + attr_reader :solution + attr_reader :strategy + + def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Gem::PubGrub.logger) + @logger = logger + + @source = source + @strategy = strategy + + # { package => [incompatibility, ...]} + @incompatibilities = Hash.new do |h, k| + h[k] = [] + end + + @seen_incompatibilities = {} + + @solution = PartialSolution.new + + add_incompatibility Incompatibility.new([ + Term.new(VersionConstraint.any(root), false) + ], cause: :root) + + propagate(root) + end + + def solved? + solution.unsatisfied.empty? + end + + # Returns true if there is more work to be done, false otherwise + def work + unsatisfied_terms = solution.unsatisfied + if unsatisfied_terms.empty? + logger.info { "Solution found after #{solution.attempted_solutions} attempts:" } + solution.decisions.each do |package, version| + next if Package.root?(package) + logger.info { "* #{package} #{version}" } + end + + return false + end + + next_package = choose_package_version_from(unsatisfied_terms) + propagate(next_package) + + true + end + + def solve + while work; end + + solution.decisions + end + + alias_method :result, :solve + + private + + def propagate(initial_package) + changed = [initial_package] + while package = changed.shift + @incompatibilities[package].reverse_each do |incompatibility| + result = propagate_incompatibility(incompatibility) + if result == :conflict + root_cause = resolve_conflict(incompatibility) + changed.clear + changed << propagate_incompatibility(root_cause) + elsif result # should be a Package + changed << result + end + end + changed.uniq! + end + end + + def propagate_incompatibility(incompatibility) + unsatisfied = nil + incompatibility.terms.each do |term| + relation = solution.relation(term) + if relation == :disjoint + return nil + elsif relation == :overlap + # If more than one term is inconclusive, we can't deduce anything + return nil if unsatisfied + unsatisfied = term + end + end + + if !unsatisfied + return :conflict + end + + logger.debug { "derived: #{unsatisfied.invert}" } + + solution.derive(unsatisfied.invert, incompatibility) + + unsatisfied.package + end + + def choose_package_version_from(unsatisfied_terms) + remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h + + package, version = strategy.next_package_and_version(remaining) + + logger.debug { "attempting #{package} #{version}" } + + if version.nil? + unsatisfied_term = unsatisfied_terms.find { |t| t.package == package } + add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term) + return package + end + + conflict = false + + source.incompatibilities_for(package, version).each do |incompatibility| + if @seen_incompatibilities.include?(incompatibility) + logger.debug { "knew: #{incompatibility}" } + next + end + @seen_incompatibilities[incompatibility] = true + + add_incompatibility incompatibility + + conflict ||= incompatibility.terms.all? do |term| + term.package == package || solution.satisfies?(term) + end + end + + unless conflict + logger.info { "selected #{package} #{version}" } + + solution.decide(package, version) + else + logger.info { "conflict: #{conflict.inspect}" } + end + + package + end + + def resolve_conflict(incompatibility) + logger.info { "conflict: #{incompatibility}" } + + new_incompatibility = nil + + while !incompatibility.failure? + most_recent_term = nil + most_recent_satisfier = nil + difference = nil + + previous_level = 1 + + incompatibility.terms.each do |term| + satisfier = solution.satisfier(term) + + if most_recent_satisfier.nil? + most_recent_term = term + most_recent_satisfier = satisfier + elsif most_recent_satisfier.index < satisfier.index + previous_level = [previous_level, most_recent_satisfier.decision_level].max + most_recent_term = term + most_recent_satisfier = satisfier + difference = nil + else + previous_level = [previous_level, satisfier.decision_level].max + end + + if most_recent_term == term + difference = most_recent_satisfier.term.difference(most_recent_term) + if difference.empty? + difference = nil + else + difference_satisfier = solution.satisfier(difference.inverse) + previous_level = [previous_level, difference_satisfier.decision_level].max + end + end + end + + if previous_level < most_recent_satisfier.decision_level || + most_recent_satisfier.decision? + + logger.info { "backtracking to #{previous_level}" } + solution.backtrack(previous_level) + + if new_incompatibility + add_incompatibility(new_incompatibility) + end + + return incompatibility + end + + new_terms = [] + new_terms += incompatibility.terms - [most_recent_term] + new_terms += most_recent_satisfier.cause.terms.reject { |term| + term.package == most_recent_satisfier.term.package + } + if difference + new_terms << difference.invert + end + + new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause)) + + if incompatibility.to_s == new_incompatibility.to_s + logger.info { "!! failed to resolve conflicts, this shouldn't have happened" } + break + end + + incompatibility = new_incompatibility + + partially = difference ? " partially" : "" + logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" } + logger.info { "! which is caused by #{most_recent_satisfier.cause}" } + logger.info { "! thus #{incompatibility}" } + end + + raise SolveFailure.new(incompatibility) + end + + def add_incompatibility(incompatibility) + logger.debug { "fact: #{incompatibility}" } + incompatibility.terms.each do |term| + package = term.package + @incompatibilities[package] << incompatibility + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb new file mode 100644 index 0000000000..4166318a98 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class VersionUnion + attr_reader :ranges + + def self.normalize_ranges(ranges) + ranges = ranges.flat_map do |range| + range.ranges + end + + ranges.reject!(&:empty?) + + return [] if ranges.empty? + + mins, ranges = ranges.partition { |r| !r.min } + original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] } + ranges = [original_ranges.shift] + original_ranges.each do |range| + if ranges.last.contiguous_to?(range) + ranges << ranges.pop.span(range) + else + ranges << range + end + end + + ranges + end + + def self.union(ranges, normalize: true) + ranges = normalize_ranges(ranges) if normalize + + if ranges.size == 0 + VersionRange.empty + elsif ranges.size == 1 + ranges[0] + else + new(ranges) + end + end + + def initialize(ranges) + raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) } + @ranges = ranges + end + + def hash + ranges.hash + end + + def eql?(other) + ranges.eql?(other.ranges) + end + + def include?(version) + !!ranges.bsearch {|r| r.compare_version(version) } + end + + def select_versions(all_versions) + versions = [] + ranges.inject(all_versions) do |acc, range| + _, matching, higher = range.partition_versions(acc) + versions.concat matching + higher + end + versions + end + + def intersects?(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + if my_range.intersects?(other_range) + return true + end + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + end + alias_method :allows_any?, :intersects? + + def allows_all?(other) + my_ranges = ranges.dup + + my_range = my_ranges.shift + + other.ranges.all? do |other_range| + while my_range + break if my_range.allows_all?(other_range) + my_range = my_ranges.shift + end + + !!my_range + end + end + + def empty? + false + end + + def any? + false + end + + def intersect(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + new_ranges = [] + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + new_ranges << my_range.intersect(other_range) + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + new_ranges.reject!(&:empty?) + VersionUnion.union(new_ranges, normalize: false) + end + + def upper_invert + ranges.last.upper_invert + end + + def invert + ranges.map(&:invert).inject(:intersect) + end + + def union(other) + VersionUnion.union([self, other]) + end + + def to_s + output = [] + + ranges = self.ranges.dup + while !ranges.empty? + ne = [] + range = ranges.shift + while !ranges.empty? && ranges[0].min.to_s == range.max.to_s + ne << range.max + range = range.span(ranges.shift) + end + + ne.map! {|x| "!= #{x}" } + if ne.empty? + output << range.to_s + elsif range.any? + output << ne.join(', ') + else + output << "#{range}, #{ne.join(', ')}" + end + end + + output.join(" OR ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def ==(other) + self.class == other.class && + self.ranges == other.ranges + end + end +end diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb index 4d95e5fc7f..4f48e0642b 100644 --- a/lib/rubygems/vendor/resolv/lib/resolv.rb +++ b/lib/rubygems/vendor/resolv/lib/resolv.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true require 'socket' -require_relative '../../timeout/lib/timeout' +require_relative '../../../vendored_timeout' require 'io/wait' require_relative '../../../vendored_securerandom' +require 'rbconfig' # Gem::Resolv is a thread-aware DNS resolver library written in Ruby. Gem::Resolv can # handle multiple DNS requests concurrently without blocking the entire Ruby @@ -33,7 +34,8 @@ require_relative '../../../vendored_securerandom' class Gem::Resolv - VERSION = "0.6.0" + # The version string + VERSION = "0.7.0" ## # Looks up the first IP address for +name+. @@ -177,14 +179,15 @@ class Gem::Resolv # Gem::Resolv::Hosts is a hostname resolver that uses the system hosts file. class Hosts - if /mswin|mingw|cygwin/ =~ RUBY_PLATFORM and + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/ begin - require 'win32/resolv' - DefaultFileName = Win32::Resolv.get_hosts_path || IO::NULL + require 'win32/resolv' unless defined?(Win32::Resolv) + hosts = Win32::Resolv.get_hosts_path || IO::NULL rescue LoadError end end - DefaultFileName ||= '/etc/hosts' + # The default file name for host names + DefaultFileName = hosts || '/etc/hosts' ## # Creates a new Gem::Resolv::Hosts, using +filename+ for its data source. @@ -522,6 +525,8 @@ class Gem::Resolv } end + # :stopdoc: + def fetch_resource(name, typeclass) lazy_initialize truncated = {} @@ -659,8 +664,20 @@ class Gem::Resolv } end - def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: - begin + case RUBY_PLATFORM + when *[ + # https://www.rfc-editor.org/rfc/rfc6056.txt + # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations + /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/, + /darwin/, # the same as FreeBSD + ] then + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + udpsock.bind(bind_host, 0) + end + else + # Sequential port assignment + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + # Ephemeral port number range recommended by RFC 6056 port = random(1024..65535) udpsock.bind(bind_host, port) rescue Errno::EADDRINUSE, # POSIX @@ -983,13 +1000,13 @@ class Gem::Resolv next unless keyword case keyword when 'nameserver' - nameserver.concat(args) + nameserver.concat(args.each(&:freeze)) when 'domain' next if args.empty? - search = [args[0]] + search = [args[0].freeze] when 'search' next if args.empty? - search = args + search = args.each(&:freeze) when 'options' args.each {|arg| case arg @@ -1000,22 +1017,21 @@ class Gem::Resolv end } } - return { :nameserver => nameserver, :search => search, :ndots => ndots } + return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze end def Config.default_config_hash(filename="/etc/resolv.conf") if File.exist? filename - config_hash = Config.parse_resolv_conf(filename) + Config.parse_resolv_conf(filename) + elsif defined?(Win32::Resolv) + search, nameserver = Win32::Resolv.get_resolv_info + config_hash = {} + config_hash[:nameserver] = nameserver if nameserver + config_hash[:search] = [search].flatten if search + config_hash else - if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM - require 'win32/resolv' - search, nameserver = Win32::Resolv.get_resolv_info - config_hash = {} - config_hash[:nameserver] = nameserver if nameserver - config_hash[:search] = [search].flatten if search - end + {} end - config_hash || {} end def lazy_initialize @@ -1664,6 +1680,7 @@ class Gem::Resolv prev_index = @index save_index = nil d = [] + size = -1 while true raise DecodeError.new("limit exceeded") if @limit <= @index case @data.getbyte(@index) @@ -1684,7 +1701,10 @@ class Gem::Resolv end @index = idx else - d << self.get_label + l = self.get_label + d << l + size += 1 + l.string.bytesize + raise DecodeError.new("name label data exceed 255 octets") if size > 255 end end end @@ -2110,7 +2130,14 @@ class Gem::Resolv attr_reader :ttl - ClassHash = {} # :nodoc: + ClassHash = Module.new do + module_function + + def []=(type_class_value, klass) + type_value, class_value = type_class_value + Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass) + end + end def encode_rdata(msg) # :nodoc: raise NotImplementedError.new @@ -2148,7 +2175,9 @@ class Gem::Resolv end def self.get_class(type_value, class_value) # :nodoc: - return ClassHash[[type_value, class_value]] || + cache = :"Type#{type_value}_Class#{class_value}" + + return (const_defined?(cache) && const_get(cache)) || Generic.create(type_value, class_value) end @@ -2577,7 +2606,7 @@ class Gem::Resolv end ## - # Flags for this proprty: + # Flags for this property: # - Bit 0 : 0 = not critical, 1 = critical attr_reader :flags @@ -2898,15 +2927,21 @@ class Gem::Resolv class IPv4 - ## - # Regular expression IPv4 addresses must match. - Regex256 = /0 |1(?:[0-9][0-9]?)? |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? - |[3-9][0-9]?/x + |[3-9][0-9]?/x # :nodoc: + + ## + # Regular expression IPv4 addresses must match. Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ + ## + # Creates a new IPv4 address from +arg+ which may be: + # + # IPv4:: returns +arg+. + # String:: +arg+ must match the IPv4::Regex constant + def self.create(arg) case arg when IPv4 @@ -3215,13 +3250,15 @@ class Gem::Resolv end - module LOC + module LOC # :nodoc: ## # A Gem::Resolv::LOC::Size class Size + # Regular expression LOC size must match. + Regex = /^(\d+\.*\d*)[m]$/ ## @@ -3247,6 +3284,7 @@ class Gem::Resolv end end + # Internal use; use self.create. def initialize(scalar) @scalar = scalar end @@ -3284,6 +3322,8 @@ class Gem::Resolv class Coord + # Regular expression LOC Coord must match. + Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/ ## @@ -3313,6 +3353,7 @@ class Gem::Resolv end end + # Internal use; use self.create. def initialize(coordinates,orientation) unless coordinates.kind_of?(String) raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") @@ -3375,6 +3416,8 @@ class Gem::Resolv class Alt + # Regular expression LOC Alt must match. + Regex = /^([+-]*\d+\.*\d*)[m]$/ ## @@ -3400,6 +3443,7 @@ class Gem::Resolv end end + # Internal use; use self.create. def initialize(altitude) @altitude = altitude end diff --git a/lib/rubygems/vendor/timeout/lib/timeout.rb b/lib/rubygems/vendor/timeout/lib/timeout.rb index 455c504f47..376b8c0e2b 100644 --- a/lib/rubygems/vendor/timeout/lib/timeout.rb +++ b/lib/rubygems/vendor/timeout/lib/timeout.rb @@ -20,7 +20,7 @@ module Gem::Timeout # The version - VERSION = "0.4.3" + VERSION = "0.4.4" # Internal error raised to when a timeout is triggered. class ExitException < Exception @@ -123,6 +123,9 @@ module Gem::Timeout def self.ensure_timeout_thread_created unless @timeout_thread and @timeout_thread.alive? + # If the Mutex is already owned we are in a signal handler. + # In that case, just return and let the main thread create the @timeout_thread. + return if TIMEOUT_THREAD_MUTEX.owned? TIMEOUT_THREAD_MUTEX.synchronize do unless @timeout_thread and @timeout_thread.alive? @timeout_thread = create_timeout_thread diff --git a/lib/rubygems/vendor/uri/lib/uri/common.rb b/lib/rubygems/vendor/uri/lib/uri/common.rb index f0755f5fdc..e9bdfa6a07 100644 --- a/lib/rubygems/vendor/uri/lib/uri/common.rb +++ b/lib/rubygems/vendor/uri/lib/uri/common.rb @@ -30,6 +30,9 @@ module Gem::URI remove_const(:Parser) if defined?(::Gem::URI::Parser) const_set("Parser", parser.class) + remove_const(:PARSER) if defined?(::Gem::URI::PARSER) + const_set("PARSER", parser) + remove_const(:REGEXP) if defined?(::Gem::URI::REGEXP) remove_const(:PATTERN) if defined?(::Gem::URI::PATTERN) if Parser == RFC2396_Parser @@ -49,10 +52,10 @@ module Gem::URI warn "Gem::URI::REGEXP is obsolete. Use Gem::URI::RFC2396_REGEXP explicitly.", uplevel: 1 if $VERBOSE Gem::URI::RFC2396_REGEXP elsif value = RFC2396_PARSER.regexp[const] - warn "Gem::URI::#{const} is obsolete. Use RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE + warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE value elsif value = RFC2396_Parser.const_get(const) - warn "Gem::URI::#{const} is obsolete. Use RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE + warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE value else super @@ -92,6 +95,40 @@ module Gem::URI end module Schemes # :nodoc: + class << self + ReservedChars = ".+-" + EscapedChars = "\u01C0\u01C1\u01C2" + # Use Lo category chars as escaped chars for TruffleRuby, which + # does not allow Symbol categories as identifiers. + + def escape(name) + unless name and name.ascii_only? + return nil + end + name.upcase.tr(ReservedChars, EscapedChars) + end + + def unescape(name) + name.tr(EscapedChars, ReservedChars).encode(Encoding::US_ASCII).upcase + end + + def find(name) + const_get(name, false) if name and const_defined?(name, false) + end + + def register(name, klass) + unless scheme = escape(name) + raise ArgumentError, "invalid character as scheme - #{name}" + end + const_set(scheme, klass) + end + + def list + constants.map { |name| + [unescape(name.to_s), const_get(name)] + }.to_h + end + end end private_constant :Schemes @@ -104,7 +141,7 @@ module Gem::URI # Note that after calling String#upcase on +scheme+, it must be a valid # constant name. def self.register_scheme(scheme, klass) - Schemes.const_set(scheme.to_s.upcase, klass) + Schemes.register(scheme, klass) end # Returns a hash of the defined schemes: @@ -122,14 +159,14 @@ module Gem::URI # # Related: Gem::URI.register_scheme. def self.scheme_list - Schemes.constants.map { |name| - [name.to_s.upcase, Schemes.const_get(name)] - }.to_h + Schemes.list end + # :stopdoc: INITIAL_SCHEMES = scheme_list private_constant :INITIAL_SCHEMES Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor) + # :startdoc: # Returns a new object constructed from the given +scheme+, +arguments+, # and +default+: @@ -148,12 +185,10 @@ module Gem::URI # # => #<Gem::URI::HTTP foo://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> # def self.for(scheme, *arguments, default: Generic) - const_name = scheme.to_s.upcase + const_name = Schemes.escape(scheme) uri_class = INITIAL_SCHEMES[const_name] - uri_class ||= if /\A[A-Z]\w*\z/.match?(const_name) && Schemes.const_defined?(const_name, false) - Schemes.const_get(const_name, false) - end + uri_class ||= Schemes.find(const_name) uri_class ||= default return uri_class.new(scheme, *arguments) @@ -195,7 +230,7 @@ module Gem::URI # ["fragment", "top"]] # def self.split(uri) - DEFAULT_PARSER.split(uri) + PARSER.split(uri) end # Returns a new \Gem::URI object constructed from the given string +uri+: @@ -205,11 +240,11 @@ module Gem::URI # Gem::URI.parse('http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') # # => #<Gem::URI::HTTP http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> # - # It's recommended to first ::escape string +uri+ + # It's recommended to first Gem::URI::RFC2396_PARSER.escape string +uri+ # if it may contain invalid Gem::URI characters. # def self.parse(uri) - DEFAULT_PARSER.parse(uri) + PARSER.parse(uri) end # Merges the given Gem::URI strings +str+ @@ -265,7 +300,7 @@ module Gem::URI # def self.extract(str, schemes = nil, &block) # :nodoc: warn "Gem::URI.extract is obsolete", uplevel: 1 if $VERBOSE - DEFAULT_PARSER.extract(str, schemes, &block) + PARSER.extract(str, schemes, &block) end # @@ -302,7 +337,7 @@ module Gem::URI # def self.regexp(schemes = nil)# :nodoc: warn "Gem::URI.regexp is obsolete", uplevel: 1 if $VERBOSE - DEFAULT_PARSER.make_regexp(schemes) + PARSER.make_regexp(schemes) end TBLENCWWWCOMP_ = {} # :nodoc: @@ -407,6 +442,8 @@ module Gem::URI _decode_uri_component(/%\h\h/, str, enc) end + # Returns a string derived from the given string +str+ with + # Gem::URI-encoded characters matching +regexp+ according to +table+. def self._encode_uri_component(regexp, table, str, enc) str = str.to_s.dup if str.encoding != Encoding::ASCII_8BIT @@ -421,6 +458,8 @@ module Gem::URI end private_class_method :_encode_uri_component + # Returns a string decoding characters matching +regexp+ from the + # given \URL-encoded string +str+. def self._decode_uri_component(regexp, str, enc) raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/.match?(str) str.b.gsub(regexp, TBLDECWWWCOMP_).force_encoding(enc) @@ -859,6 +898,7 @@ module Gem # Returns a \Gem::URI object derived from the given +uri+, # which may be a \Gem::URI string or an existing \Gem::URI object: # + # require 'rubygems/vendor/uri/lib/uri' # # Returns a new Gem::URI. # uri = Gem::URI('http://github.com/ruby/ruby') # # => #<Gem::URI::HTTP http://github.com/ruby/ruby> @@ -866,6 +906,8 @@ module Gem # Gem::URI(uri) # # => #<Gem::URI::HTTP http://github.com/ruby/ruby> # + # You must require 'rubygems/vendor/uri/lib/uri' to use this method. + # def URI(uri) if uri.is_a?(Gem::URI::Generic) uri diff --git a/lib/rubygems/vendor/uri/lib/uri/file.rb b/lib/rubygems/vendor/uri/lib/uri/file.rb index 768755fc2d..391c499716 100644 --- a/lib/rubygems/vendor/uri/lib/uri/file.rb +++ b/lib/rubygems/vendor/uri/lib/uri/file.rb @@ -47,7 +47,7 @@ module Gem::URI # :path => '/ruby/src'}) # uri2.to_s # => "file://host.example.com/ruby/src" # - # uri3 = Gem::URI::File.build({:path => Gem::URI::escape('/path/my file.txt')}) + # uri3 = Gem::URI::File.build({:path => Gem::URI::RFC2396_PARSER.escape('/path/my file.txt')}) # uri3.to_s # => "file:///path/my%20file.txt" # def self.build(args) diff --git a/lib/rubygems/vendor/uri/lib/uri/generic.rb b/lib/rubygems/vendor/uri/lib/uri/generic.rb index 2eabe2b4e3..d0bc77dfda 100644 --- a/lib/rubygems/vendor/uri/lib/uri/generic.rb +++ b/lib/rubygems/vendor/uri/lib/uri/generic.rb @@ -73,7 +73,7 @@ module Gem::URI # # At first, tries to create a new Gem::URI::Generic instance using # Gem::URI::Generic::build. But, if exception Gem::URI::InvalidComponentError is raised, - # then it does Gem::URI::Escape.escape all Gem::URI components and tries again. + # then it does Gem::URI::RFC2396_PARSER.escape all Gem::URI components and tries again. # def self.build2(args) begin @@ -126,9 +126,9 @@ module Gem::URI end end else - component = self.class.component rescue ::Gem::URI::Generic::COMPONENT + component = self.component rescue ::Gem::URI::Generic::COMPONENT raise ArgumentError, - "expected Array of or Hash of components of #{self.class} (#{component.join(', ')})" + "expected Array of or Hash of components of #{self} (#{component.join(', ')})" end tmp << nil @@ -186,18 +186,18 @@ module Gem::URI if arg_check self.scheme = scheme - self.userinfo = userinfo self.hostname = host self.port = port + self.userinfo = userinfo self.path = path self.query = query self.opaque = opaque self.fragment = fragment else self.set_scheme(scheme) - self.set_userinfo(userinfo) self.set_host(host) self.set_port(port) + self.set_userinfo(userinfo) self.set_path(path) self.query = query self.set_opaque(opaque) @@ -284,7 +284,7 @@ module Gem::URI # Returns the parser to be used. # - # Unless a Gem::URI::Parser is defined, DEFAULT_PARSER is used. + # Unless the +parser+ is defined, DEFAULT_PARSER is used. # def parser if !defined?(@parser) || !@parser @@ -315,7 +315,7 @@ module Gem::URI end # - # Checks the scheme +v+ component against the Gem::URI::Parser Regexp for :SCHEME. + # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME. # def check_scheme(v) if v && parser.regexp[:SCHEME] !~ v @@ -385,7 +385,7 @@ module Gem::URI # # Checks the user +v+ component for RFC2396 compliance - # and against the Gem::URI::Parser Regexp for :USERINFO. + # and against the +parser+ Regexp for :USERINFO. # # Can not have a registry or opaque component defined, # with a user component defined. @@ -409,7 +409,7 @@ module Gem::URI # # Checks the password +v+ component for RFC2396 compliance - # and against the Gem::URI::Parser Regexp for :USERINFO. + # and against the +parser+ Regexp for :USERINFO. # # Can not have a registry or opaque component defined, # with a user component defined. @@ -511,7 +511,7 @@ module Gem::URI user, password = split_userinfo(user) end @user = user - @password = password if password + @password = password [@user, @password] end @@ -522,7 +522,7 @@ module Gem::URI # See also Gem::URI::Generic.user=. # def set_user(v) - set_userinfo(v, @password) + set_userinfo(v, nil) v end protected :set_user @@ -574,6 +574,12 @@ module Gem::URI @password end + # Returns the authority info (array of user, password, host and + # port), if any is set. Or returns +nil+. + def authority + return @user, @password, @host, @port if @user || @password || @host || @port + end + # Returns the user component after Gem::URI decoding. def decoded_user Gem::URI.decode_uri_component(@user) if @user @@ -586,7 +592,7 @@ module Gem::URI # # Checks the host +v+ component for RFC2396 compliance - # and against the Gem::URI::Parser Regexp for :HOST. + # and against the +parser+ Regexp for :HOST. # # Can not have a registry or opaque component defined, # with a host component defined. @@ -615,6 +621,13 @@ module Gem::URI end protected :set_host + # Protected setter for the authority info (+user+, +password+, +host+ + # and +port+). If +port+ is +nil+, +default_port+ will be set. + # + protected def set_authority(user, password, host, port = nil) + @user, @password, @host, @port = user, password, host, port || self.default_port + end + # # == Args # @@ -639,6 +652,7 @@ module Gem::URI def host=(v) check_host(v) set_host(v) + set_userinfo(nil) v end @@ -675,7 +689,7 @@ module Gem::URI # # Checks the port +v+ component for RFC2396 compliance - # and against the Gem::URI::Parser Regexp for :PORT. + # and against the +parser+ Regexp for :PORT. # # Can not have a registry or opaque component defined, # with a port component defined. @@ -729,6 +743,7 @@ module Gem::URI def port=(v) check_port(v) set_port(v) + set_userinfo(nil) port end @@ -748,7 +763,7 @@ module Gem::URI # # Checks the path +v+ component for RFC2396 compliance - # and against the Gem::URI::Parser Regexp + # and against the +parser+ Regexp # for :ABS_PATH and :REL_PATH. # # Can not have a opaque component defined, @@ -853,7 +868,7 @@ module Gem::URI # # Checks the opaque +v+ component for RFC2396 compliance and - # against the Gem::URI::Parser Regexp for :OPAQUE. + # against the +parser+ Regexp for :OPAQUE. # # Can not have a host, port, user, or path component defined, # with an opaque component defined. @@ -905,7 +920,7 @@ module Gem::URI end # - # Checks the fragment +v+ component against the Gem::URI::Parser Regexp for :FRAGMENT. + # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT. # # # == Args @@ -1121,7 +1136,7 @@ module Gem::URI base = self.dup - authority = rel.userinfo || rel.host || rel.port + authority = rel.authority # RFC2396, Section 5.2, 2) if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query @@ -1134,9 +1149,7 @@ module Gem::URI # RFC2396, Section 5.2, 4) if authority - base.set_userinfo(rel.userinfo) - base.set_host(rel.host) - base.set_port(rel.port || base.default_port) + base.set_authority(*authority) base.set_path(rel.path) elsif base.path && rel.path base.set_path(merge_path(base.path, rel.path)) @@ -1527,7 +1540,7 @@ module Gem::URI else unless proxy_uri = env[name] if proxy_uri = env[name.upcase] - warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.', uplevel: 1 + warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1 end end end diff --git a/lib/rubygems/vendor/uri/lib/uri/http.rb b/lib/rubygems/vendor/uri/lib/uri/http.rb index 658e9941dd..99c78358ac 100644 --- a/lib/rubygems/vendor/uri/lib/uri/http.rb +++ b/lib/rubygems/vendor/uri/lib/uri/http.rb @@ -61,6 +61,18 @@ module Gem::URI super(tmp) end + # Do not allow empty host names, as they are not allowed by RFC 3986. + def check_host(v) + ret = super + + if ret && v.empty? + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + ret + end + # # == Description # diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb index 04a1d0c869..2bb4181649 100644 --- a/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb +++ b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb @@ -67,7 +67,7 @@ module Gem::URI # # == Synopsis # - # Gem::URI::Parser.new([opts]) + # Gem::URI::RFC2396_Parser.new([opts]) # # == Args # @@ -86,7 +86,7 @@ module Gem::URI # # == Examples # - # p = Gem::URI::Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") + # p = Gem::URI::RFC2396_Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") # u = p.parse("http://example.jp/%uABCD") #=> #<Gem::URI::HTTP http://example.jp/%uABCD> # Gem::URI.parse(u.to_s) #=> raises Gem::URI::InvalidURIError # @@ -108,12 +108,12 @@ module Gem::URI # The Hash of patterns. # - # See also Gem::URI::Parser.initialize_pattern. + # See also #initialize_pattern. attr_reader :pattern # The Hash of Regexp. # - # See also Gem::URI::Parser.initialize_regexp. + # See also #initialize_regexp. attr_reader :regexp # Returns a split Gem::URI against +regexp[:ABS_URI]+. @@ -202,8 +202,7 @@ module Gem::URI # # == Usage # - # p = Gem::URI::Parser.new - # p.parse("ldap://ldap.example.com/dc=example?user=john") + # Gem::URI::RFC2396_PARSER.parse("ldap://ldap.example.com/dc=example?user=john") # #=> #<Gem::URI::LDAP ldap://ldap.example.com/dc=example?user=john> # def parse(uri) @@ -244,7 +243,7 @@ module Gem::URI # If no +block+ given, then returns the result, # else it calls +block+ for each element in result. # - # See also Gem::URI::Parser.make_regexp. + # See also #make_regexp. # def extract(str, schemes = nil) if block_given? @@ -263,7 +262,7 @@ module Gem::URI unless schemes @regexp[:ABS_URI_REF] else - /(?=#{Regexp.union(*schemes)}:)#{@pattern[:X_ABS_URI]}/x + /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x end end @@ -524,6 +523,8 @@ module Gem::URI ret end + # Returns +uri+ as-is if it is Gem::URI, or convert it to Gem::URI if it is + # a String. def convert_to_uri(uri) if uri.is_a?(Gem::URI::Generic) uri diff --git a/lib/rubygems/vendor/uri/lib/uri/version.rb b/lib/rubygems/vendor/uri/lib/uri/version.rb index c2f617ce25..7ee577887b 100644 --- a/lib/rubygems/vendor/uri/lib/uri/version.rb +++ b/lib/rubygems/vendor/uri/lib/uri/version.rb @@ -1,6 +1,6 @@ module Gem::URI # :stopdoc: - VERSION_CODE = '010003'.freeze - VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.join('.').freeze + VERSION = '1.1.1'.freeze + VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze # :startdoc: end diff --git a/lib/rubygems/vendored_molinillo.rb b/lib/rubygems/vendored_molinillo.rb deleted file mode 100644 index 45906c0e5c..0000000000 --- a/lib/rubygems/vendored_molinillo.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "vendor/molinillo/lib/molinillo" diff --git a/lib/rubygems/vendored_pub_grub.rb b/lib/rubygems/vendored_pub_grub.rb new file mode 100644 index 0000000000..844d243ab3 --- /dev/null +++ b/lib/rubygems/vendored_pub_grub.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/pub_grub/lib/pub_grub" diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 3ac3676d0a..306733c1d7 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true -require_relative "deprecate" +#-- +# Workaround for directly loading Gem::Version in some cases +module Gem; end +#++ ## # The Version class processes string versions into comparable @@ -27,136 +30,153 @@ require_relative "deprecate" # 4. 0.9 # # If you want to specify a version restriction that includes both prereleases -# and regular releases of the 1.x series this is the best way: +# and regular releases of 1.x or later versions: # -# s.add_dependency 'example', '>= 1.0.0.a', '< 2.0.0' +# s.add_dependency 'example', '>= 1.0.0.a' # # == How Software Changes # -# Users expect to be able to specify a version constraint that gives them -# some reasonable expectation that new versions of a library will work with -# their software if the version constraint is true, and not work with their -# software if the version constraint is false. In other words, the perfect -# system will accept all compatible versions of the library and reject all -# incompatible versions. -# -# Libraries change in 3 ways (well, more than 3, but stay focused here!). -# -# 1. The change may be an implementation detail only and have no effect on -# the client software. -# 2. The change may add new features, but do so in a way that client software -# written to an earlier version is still compatible. -# 3. The change may change the public interface of the library in such a way -# that old software is no longer compatible. +# Libraries generally change in 3 ways: # -# Some examples are appropriate at this point. Suppose I have a Stack class -# that supports a <tt>push</tt> and a <tt>pop</tt> method. +# 1. The change is an implementation detail, bug fix, security fix, or +# optimization, and has no behavioral effect on the software using it. # -# === Examples of Category 1 changes: +# 2. The change adds new features, and software using those new features is +# not compatible with previous versions of the library, but software using +# previous versions of the library is compatible with the change. # -# * Switch from an array based implementation to a linked-list based -# implementation. -# * Provide an automatic (and transparent) backing store for large stacks. +# 3. The change modifies the public interface of some part of the library in +# such a way that software that uses that part of the library must be +# modified to work. # -# === Examples of Category 2 changes might be: -# -# * Add a <tt>depth</tt> method to return the current depth of the stack. -# * Add a <tt>top</tt> method that returns the current top of stack (without -# changing the stack). -# * Change <tt>push</tt> so that it returns the item pushed (previously it -# had no usable return value). -# -# === Examples of Category 3 changes might be: -# -# * Changes <tt>pop</tt> so that it no longer returns a value (you must use -# <tt>top</tt> to get the top of the stack). -# * Rename the methods to <tt>push_item</tt> and <tt>pop_item</tt>. -# -# == RubyGems Rational Versioning +# == RubyGems Rational Versioning (the recommended approach) # # * Versions shall be represented by three non-negative integers, separated -# by periods (e.g. 3.1.4). The first integers is the "major" version +# by periods (e.g. 3.1.4). The first integer is the "major" version # number, the second integer is the "minor" version number, and the third -# integer is the "build" number. +# integer is the "patch" version number. # -# * A category 1 change (implementation detail) will increment the build -# number. +# * A category 1 change (implementation detail, bug fix, or security fix) +# will increment the patch number. # # * A category 2 change (backwards compatible) will increment the minor -# version number and reset the build number. -# -# * A category 3 change (incompatible) will increment the major build number -# and reset the minor and build numbers. -# -# * Any "public" release of a gem should have a different version. Normally -# that means incrementing the build number. This means a developer can -# generate builds all day long, but as soon as they make a public release, -# the version must be updated. +# version number and reset the patch number. # -# === Examples +# * A category 3 change (incompatible) will increment the major version number +# and reset the minor and patch numbers. # -# Let's work through a project lifecycle using our Stack example from above. +# * Any "public" release of a gem should have a different version. # -# Version 0.0.1:: The initial Stack class is release. -# Version 0.0.2:: Switched to a linked=list implementation because it is -# cooler. -# Version 0.1.0:: Added a <tt>depth</tt> method. -# Version 1.0.0:: Added <tt>top</tt> and made <tt>pop</tt> return nil -# (<tt>pop</tt> used to return the old top item). -# Version 1.1.0:: <tt>push</tt> now returns the value pushed (it used it -# return nil). -# Version 1.1.1:: Fixed a bug in the linked list implementation. -# Version 1.1.2:: Fixed a bug introduced in the last fix. +# == Optimistic Vs. Pessimistic Dependency Versioning # -# Client A needs a stack with basic push/pop capability. They write to the -# original interface (no <tt>top</tt>), so their version constraint looks like: -# -# gem 'stack', '>= 0.0' -# -# Essentially, any version is OK with Client A. An incompatible change to -# the library will cause them grief, but they are willing to take the chance -# (we call Client A optimistic). -# -# Client B is just like Client A except for two things: (1) They use the -# <tt>depth</tt> method and (2) they are worried about future -# incompatibilities, so they write their version constraint like this: -# -# gem 'stack', '~> 0.1' -# -# The <tt>depth</tt> method was introduced in version 0.1.0, so that version -# or anything later is fine, as long as the version stays below version 1.0 -# where incompatibilities are introduced. We call Client B pessimistic -# because they are worried about incompatible future changes (it is OK to be -# pessimistic!). -# -# == Preventing Version Catastrophe: -# -# From: https://www.zenspider.com/ruby/2008/10/rubygems-how-to-preventing-catastrophe.html -# -# Let's say you're depending on the fnord gem version 2.y.z. If you -# specify your dependency as ">= 2.0.0" then, you're good, right? What -# happens if fnord 3.0 comes out and it isn't backwards compatible -# with 2.y.z? Your stuff will break as a result of using ">=". The -# better route is to specify your dependency with an "approximate" version -# specifier ("~>"). They're a tad confusing, so here is how the dependency -# specifiers work: -# -# Specification From ... To (exclusive) -# ">= 3.0" 3.0 ... ∞ -# "~> 3.0" 3.0 ... 4.0 -# "~> 3.0.0" 3.0.0 ... 3.1 -# "~> 3.5" 3.5 ... 4.0 -# "~> 3.5.0" 3.5.0 ... 3.6 -# "~> 3" 3.0 ... 4.0 -# -# For the last example, single-digit versions are automatically extended with -# a zero to give a sensible result. +# Users expect to be able to specify a version constraint that gives them +# a reasonable expectation that new versions of a library will work with +# their software if the version constraint is true, and not work with their +# software if the version constraint is false. In other words, the perfect +# system will accept all compatible versions of the library and reject all +# incompatible versions. Unfortunately, there is no perfect system, as you +# cannot predict the future. You can never know whether a future version of +# a library will contain which type of change. +# +# There are two common outlooks on dependency versioning: +# +# 1. Optimistic. This does not set an upper bound on a dependency. It is +# possible that a future version of a dependency will break the software, +# and in that case, the dependency version will need to be updated and +# changes will need to be made. +# +# 2. Pessimistic. This assumes all major version changes of a dependency will +# break the software, and that patch or minor changes of a dependency will +# not break the software. If there is a major version of a dependency +# released, the dependency version must be updated in order to use it, even +# if no code changes are actually needed. +# +# In general, optimistic versioning is superior to pessimistic versioning. +# Pessimistic versioning is often wrong in both directions. Dependencies can +# release patch or minor versions that contain incompatibilities. One +# common reason is that a security fix may require a backwards-incompatible API +# change. In this case, even though pessimistic versioning was used, it +# didn't even save effort, as you still need to make code changes and adjust +# dependency versions. Similarly, for all but the smallest dependencies, just +# because the dependency made a backwards incompatible change to one interface +# doesn't mean the dependency made a backwards incompatible change to an +# interface that the software is using. It is a common problem that a +# dependency will release a new major version and the software does not require +# any changes in order to use it. In this case, being pessimistic results in +# additional work for no benefit. +# +# When a library uses pessimistic versioning of dependencies, it causes +# significant problems if that library is not diligent about updating +# dependency versions and any library is depending on that library. +# For example: +# +# * Library A is currently on release 1.2.3. +# +# * Library B is at version 2.3.4 and has a pessimistic dependency on +# library A, using ~> 1.0 (>= 1.0, < 2). +# +# * Library C is at version 3.4.5 and has an optimistic dependency on +# library A, using >= 1.0. +# +# * Library D has optimistic dependencies on both libraries B and C. +# +# * Library A releases a new major version, 2.0.0, with new features, which +# is mostly backwards compatible, but does contain some backwards +# incompatible changes. +# +# * Library B would work with A 2.0.0, but cannot use it due to pessimistic +# versioning. +# +# * Library C wants to use the new features in the major release of library +# A to implement its own new features, so it does so, bumps the +# dependency version of A to >= 2.0, and releases version 3.5.0. +# +# * Library D cannot upgrade to the new version of library C, because it +# depends on library B, which has a pessimistic dependency on library A. +# +# * Library C releases a security fix patch version 3.5.1 to fix a +# vulnerability present in all previous versions. +# +# * Library D is now in a terrible situation. It cannot upgrade to library +# C 3.5.1, as that requires library A > 2.0, because it depends on library +# B, which requires library A > 1.0, < 2, even though library B would work +# fine with library A 2.0.0. +# +# This type of situation brought on by pessimistic versioning is unfortunately +# both common and serious in practice. +# +# This is not to say that optimistic versioning never causes a problem. +# However, with optimistic versioning, if there is a problem, it can be solved +# with the addition of a single dependency. For example, continuing the +# previous example: +# +# * Library A releases a new major version, 3.0.0, which makes backwards +# incompatible changes that break library C. +# +# * Until library C releases an updated version with new changes, library +# D only needs to set a specific dependency on library A for > 2.0, < 3, +# until library C is updated to work with the new version of library A. +# +# Both optimistic versioning and pessimistic versioning have problems in +# certain cases. However, it's significantly easier to fix optimistic +# versioning problems than to fix pessimistic versioning problems. +# +# That is not to say that pessimistic versioning is never appropriate. If the +# dependency is a library that adds a single method, where any change resulting +# in a major version bump would probably break a library using it, then using +# pessimistic versioning may be warranted. Additionally, if a dependency has +# already announced or committed backwards incompatible changes that would +# break a library's use of it, then having that library use a pessimistic +# version constraint would likely be warranted. However, outside of +# specific situations, you should avoid using pessimistic versioning, as the +# costs typically exceed the benefits. class Gem::Version include Comparable VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc: ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc: + RADIX_OPT = [9_500, 3_500, 260_000, 22_227, 24].freeze # :nodoc: ## # A string representation of this Version. @@ -171,9 +191,7 @@ class Gem::Version # True if the +version+ string matches RubyGems' requirements. def self.correct?(version) - nil_versions_are_discouraged! if version.nil? - - ANCHORED_VERSION_PATTERN.match?(version.to_s) + version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s) end ## @@ -182,15 +200,10 @@ class Gem::Version # # ver1 = Version.create('1.3.17') # -> (Version object) # ver2 = Version.create(ver1) # -> (ver1) - # ver3 = Version.create(nil) # -> nil def self.create(input) if self === input # check yourself before you wreck yourself input - elsif input.nil? - nil_versions_are_discouraged! - - nil else new input end @@ -206,14 +219,6 @@ class Gem::Version @@all[version] ||= super end - def self.nil_versions_are_discouraged! - unless Gem::Deprecate.skip - warn "nil versions are discouraged and will be deprecated in Rubygems 4" - end - end - - private_class_method :nil_versions_are_discouraged! - ## # Constructs a Version from the +version+ string. A version string is a # series of digits or ASCII letters separated by dots. @@ -224,7 +229,7 @@ class Gem::Version end # If version is an empty string convert it to 0 - version = 0 if version.is_a?(String) && /\A\s*\Z/.match?(version) + version = 0 if version.nil? || (version.is_a?(String) && /\A\s*\Z/.match?(version)) @version = version.to_s @@ -236,6 +241,7 @@ class Gem::Version end @version = -@version @segments = nil + @sort_key = compute_sort_key end ## @@ -337,7 +343,7 @@ class Gem::Version end ## - # A recommended version for use with a ~> Requirement. + # A recommended version for use with a >= Requirement. def approximate_recommendation segments = self.segments @@ -346,7 +352,7 @@ class Gem::Version segments.pop while segments.size > 2 segments.push 0 while segments.size < 2 - recommendation = "~> #{segments.join(".")}" + recommendation = ">= #{segments.join(".")}" recommendation += ".a" if prerelease? recommendation end @@ -354,37 +360,62 @@ class Gem::Version ## # Compares this version with +other+ returning -1, 0, or 1 if the # other version is larger, the same, or smaller than this - # one. Attempts to compare to something that's not a - # <tt>Gem::Version</tt> or a valid version String return +nil+. + # one. +other+ must be an instance of Gem::Version, comparing with + # other types may raise an exception. def <=>(other) - return self <=> self.class.new(other) if (String === other) && self.class.correct?(other) - - return unless Gem::Version === other - return 0 if @version == other.version || canonical_segments == other.canonical_segments - - lhsegments = canonical_segments - rhsegments = other.canonical_segments - - lhsize = lhsegments.size - rhsize = rhsegments.size - limit = (lhsize > rhsize ? lhsize : rhsize) - 1 - - i = 0 - - while i <= limit - lhs = lhsegments[i] || 0 - rhs = rhsegments[i] || 0 - i += 1 - - next if lhs == rhs - return -1 if String === lhs && Numeric === rhs - return 1 if Numeric === lhs && String === rhs - - return lhs <=> rhs + if Gem::Version === other + # Fast path for comparison when available. + if @sort_key && other.sort_key + return @sort_key <=> other.sort_key + end + + return 0 if @version == other.version || canonical_segments == other.canonical_segments + + lhsegments = canonical_segments + rhsegments = other.canonical_segments + + lhsize = lhsegments.size + rhsize = rhsegments.size + limit = (lhsize > rhsize ? rhsize : lhsize) + + i = 0 + + while i < limit + lhs = lhsegments[i] + rhs = rhsegments[i] + i += 1 + + next if lhs == rhs + return -1 if String === lhs && Numeric === rhs + return 1 if Numeric === lhs && String === rhs + + return lhs <=> rhs + end + + lhs = lhsegments[i] + + if lhs.nil? + rhs = rhsegments[i] + + while i < rhsize + return 1 if String === rhs + return -1 unless rhs.zero? + rhs = rhsegments[i += 1] + end + else + while i < lhsize + return -1 if String === lhs + return 1 unless lhs.zero? + lhs = lhsegments[i += 1] + end + end + + 0 + elsif String === other + return unless self.class.correct?(other) + self <=> self.class.new(other) end - - 0 end # remove trailing zeros segments before first letter or at the end of the version @@ -408,6 +439,24 @@ class Gem::Version protected + attr_reader :sort_key # :nodoc: + + def compute_sort_key + return if prerelease? + + segments = canonical_segments + return if segments.size > 5 + + key = 0 + RADIX_OPT.each_with_index do |radix, i| + seg = segments.fetch(i, 0) + return nil if seg >= radix + key = key * radix + seg + end + + key + end + def _segments # segments is lazy so it can pick up version values that come from # old marshaled versions, which don't go through marshal_load. diff --git a/lib/rubygems/win_platform.rb b/lib/rubygems/win_platform.rb new file mode 100644 index 0000000000..10556871b2 --- /dev/null +++ b/lib/rubygems/win_platform.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Gem + ## + # An Array of Regexps that match windows Ruby platforms. + + WIN_PATTERNS = [ + /bccwin/i, + /djgpp/i, + /mingw/i, + /mswin/i, + /wince/i, + ].freeze + + @@win_platform = nil + + ## + # Is this a windows platform? + + def self.win_platform? + if @@win_platform.nil? + ruby_platform = RbConfig::CONFIG["host_os"] + @@win_platform = !WIN_PATTERNS.find {|r| ruby_platform =~ r }.nil? + end + + @@win_platform + end +end diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index f89004f32a..b2547b136b 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -1,98 +1,845 @@ # frozen_string_literal: true +unless defined?(Psych::VERSION) + module Psych + class Exception < ::RuntimeError; end + class SyntaxError < Exception; end + class DisallowedClass < Exception; end + class BadAlias < Exception; end + class AliasesNotEnabled < BadAlias; end + end +end + module Gem - # A stub yaml serializer that can handle only hashes and strings (as of now). module YAMLSerializer - module_function + Scalar = Struct.new(:value, :tag, :anchor, keyword_init: true) - def dump(hash) - yaml = String.new("---") - yaml << dump_hash(hash) + Mapping = Struct.new(:pairs, :tag, :anchor, keyword_init: true) do + def initialize(pairs: [], tag: nil, anchor: nil) + super + end + end + + Sequence = Struct.new(:items, :tag, :anchor, keyword_init: true) do + def initialize(items: [], tag: nil, anchor: nil) + super + end end - def dump_hash(hash) - yaml = String.new("\n") - hash.each do |k, v| - yaml << k << ":" - if v.is_a?(Hash) - yaml << dump_hash(v).gsub(/^(?!$)/, " ") # indent all non-empty lines - elsif v.is_a?(Array) # Expected to be array of strings - if v.empty? - yaml << " []\n" + AliasRef = Struct.new(:name, keyword_init: true) + + class Parser + MAPPING_KEY_RE = /^((?:[^#:]|:[^ ])+):(?:[ ]+(.*))?$/ + MAX_NESTING_DEPTH = 1_000 + + def initialize(source) + @lines = source.split("\n") + @anchors = {} + @depth = 0 + strip_document_prefix + end + + def parse + return nil if @lines.empty? + + root = nil + while @lines.any? + before = @lines.size + node = parse_node(-1) + @lines.shift if @lines.size == before && @lines.any? + + if root.is_a?(Mapping) && node.is_a?(Mapping) + root.pairs.concat(node.pairs) + elsif root.nil? + root = node + end + end + root + end + + private + + def strip_document_prefix + return if @lines.empty? + return unless @lines[0]&.start_with?("---") + + if @lines[0].strip == "---" + @lines.shift + else + @lines[0] = @lines[0].sub(/^---\s*/, "") + end + end + + def parse_node(base_indent) + @depth += 1 + raise_max_nesting! if @depth > MAX_NESTING_DEPTH + + skip_blank_and_comments + return nil if @lines.empty? + + line = @lines[0] + stripped = line.lstrip + indent = line.size - stripped.size + return nil if indent < base_indent + + return parse_alias_ref if stripped.start_with?("*") + + anchor = consume_anchor + + if anchor + line = @lines[0] + stripped = line.lstrip + end + + if stripped.start_with?("- ") || stripped == "-" + parse_sequence(indent, anchor) + elsif stripped.start_with?("\"") && stripped.end_with?("\"") + # We don't need to care about the following case here: + # 1. "value with comment" # ... + # 2. "key": "value" + # + # 1. must not happen because YAMLSerializer doesn't emit any + # comment. YAMLSerializer parses only YAML that is generated + # by YAMLSerializer. + # + # 2. must not happen because #parse_node isn't used non + # top-level mapping. Non top-level mapping always uses + # #parse_mapping. Top-level mapping never use the '"key": + # "value"' form because all top-level keys + # ("!ruby/object:Gem::Specification"'s keys) are known and + # #emit_specification doesn't quote anything. + parse_plain_scalar(indent, anchor) + elsif stripped.start_with?("'") && stripped.end_with?("'") + # See also the above note for double quotation. + parse_plain_scalar(indent, anchor) + elsif stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:") + parse_mapping(indent, anchor) + elsif stripped.start_with?("!ruby/object:") + parse_tagged_node(indent, anchor) + elsif stripped.start_with?("|") + modifier = stripped[1..].to_s.strip + @lines.shift + register_anchor(anchor, Scalar.new(value: parse_block_scalar(indent, modifier))) + else + parse_plain_scalar(indent, anchor) + end + ensure + @depth -= 1 + end + + def parse_sequence(indent, anchor) + items = [] + while @lines.any? + line = @lines[0] + stripped = line.lstrip + break unless line.size - stripped.size == indent && + (stripped.start_with?("- ") || stripped == "-") + content = @lines.shift.lstrip[1..].strip + item_anchor, content = extract_item_anchor(content) + item = parse_sequence_item(content, indent) + items << register_anchor(item_anchor, item) + end + register_anchor(anchor, Sequence.new(items: items)) + end + + def parse_sequence_item(content, indent) + if content.start_with?("*") + parse_inline_alias(content) + elsif content.empty? + @lines.any? && current_indent > indent ? parse_node(indent) : nil + elsif content.start_with?("!ruby/object:") + parse_tagged_content(content.strip, indent) + elsif content.start_with?("!binary ") + parse_binary_value(content, indent) + elsif content.start_with?("-") + @lines.unshift("#{" " * (indent + 2)}#{content}") + parse_node(indent) + elsif content =~ MAPPING_KEY_RE && !content.start_with?("!ruby/object:") + @lines.unshift("#{" " * (indent + 2)}#{content}") + parse_node(indent) + elsif content.start_with?("|") + Scalar.new(value: parse_block_scalar(indent, content[1..].to_s.strip)) + else + parse_inline_scalar(content, indent) + end + end + + def parse_mapping(indent, anchor) + pairs = [] + while @lines.any? + line = @lines[0] + stripped = line.lstrip + break unless line.size - stripped.size == indent && + stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:") + key = $1.strip + @lines.shift + val = strip_comment($2.to_s.strip) + + key = decode_binary_tag(key) if key.start_with?("!binary ") + + val_anchor, val = consume_value_anchor(val) + value = parse_mapping_value(val, indent) + value = register_anchor(val_anchor, value) if val_anchor + + pairs << [Scalar.new(value: key), value] + end + register_anchor(anchor, Mapping.new(pairs: pairs)) + end + + def parse_mapping_value(val, indent) + if val.start_with?("*") + parse_inline_alias(val) + elsif val.start_with?("!ruby/object:") + parse_tagged_content(val.strip, indent) + elsif val.start_with?("!binary ") + parse_binary_value(val, indent) + elsif val.empty? + next_stripped = nil + next_indent = nil + if @lines.any? + next_stripped = @lines[0].lstrip + next_indent = @lines[0].size - next_stripped.size + end + if next_stripped && + (next_stripped.start_with?("- ") || next_stripped == "-") && + next_indent == indent + parse_node(indent) else - yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n" + parse_node(indent + 1) end + elsif val == "[]" + Sequence.new + elsif val == "{}" + Mapping.new + elsif val.start_with?("|") + Scalar.new(value: parse_block_scalar(indent, val[1..].to_s.strip)) else - yaml << " " << v.to_s.gsub(/\s+/, " ").inspect << "\n" + parse_inline_scalar(val, indent) end end - yaml - end - ARRAY_REGEX = / - ^ - (?:[ ]*-[ ]) # '- ' before array items - (['"]?) # optional opening quote - (.*) # value - \1 # matching closing quote - $ - /xo - - HASH_REGEX = / - ^ - ([ ]*) # indentations - ([^#]+) # key excludes comment char '#' - (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) - [ ]? - (['"]?) # optional opening quote - (.*) # value - \3 # matching closing quote - $ - /xo - - def load(str) - res = {} - stack = [res] - last_hash = nil - last_empty_key = nil - str.split(/\r?\n/) do |line| - if match = HASH_REGEX.match(line) - indent, key, quote, val = match.captures - val = strip_comment(val) - - depth = indent.size / 2 - if quote.empty? && val.empty? - new_hash = {} - stack[depth][key] = new_hash - stack[depth + 1] = new_hash - last_empty_key = key - last_hash = stack[depth] + def parse_tagged_node(indent, anchor) + tag = @lines.shift.strip + nested = parse_node(indent) + apply_tag(nested, tag, anchor) + end + + def parse_tagged_content(tag, indent) + nested = parse_node(indent) + apply_tag(nested, tag, nil) + end + + def apply_tag(node, tag, anchor) + if node.is_a?(Mapping) + node.tag = tag + node.anchor = anchor + node + else + Mapping.new(pairs: [[Scalar.new(value: "value"), node]], tag: tag, anchor: anchor) + end + end + + def parse_block_scalar(base_indent, modifier) + parts = [] + block_indent = nil + + while @lines.any? + line = @lines[0] + if line.strip.empty? + parts << "\n" + @lines.shift else - val = [] if val == "[]" # empty array - stack[depth][key] = val + line_indent = line.size - line.lstrip.size + break if line_indent <= base_indent + block_indent ||= line_indent + parts << @lines.shift[block_indent..].to_s << "\n" + end + end + + res = parts.join + res.chomp! if modifier == "-" && res.end_with?("\n") + res + end + + def parse_plain_scalar(indent, anchor) + result = coerce(@lines.shift.strip) + return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence) + + while result.is_a?(String) && @lines.any? && + !@lines[0].strip.empty? && current_indent > indent + result << " " << @lines.shift.strip + end + register_anchor(anchor, Scalar.new(value: result)) + end + + def parse_inline_scalar(val, indent) + result = coerce(val) + return result if result.is_a?(Mapping) || result.is_a?(Sequence) + + while result.is_a?(String) && @lines.any? && + !@lines[0].strip.empty? && current_indent > indent + result << " " << @lines.shift.strip + end + Scalar.new(value: result) + end + + def coerce(val, depth = 0) + raise_max_nesting! if depth > MAX_NESTING_DEPTH + + val = val.sub(/^! /, "") if val.start_with?("! ") + + if val =~ /^"(.*)"$/ + $1.gsub(/\\["nrt\\]/) do |m| + case m + when '\\"' then '"' + when "\\n" then "\n" + when "\\r" then "\r" + when "\\t" then "\t" + when "\\\\" then "\\" + end + end + elsif val =~ /^'(.*)'$/ + $1.gsub(/''/, "'") + elsif val == "true" + true + elsif val == "false" + false + elsif ["~", "null"].include?(val) + nil + elsif val == "{}" + Mapping.new + elsif val =~ /^\[(.*)\]$/ + inner = $1.strip + return Sequence.new if inner.empty? + items = inner.split(/\s*,\s*/).reject(&:empty?).map {|e| Scalar.new(value: coerce(e, depth + 1)) } + Sequence.new(items: items) + elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val) + begin + Time.new(val) + rescue ArgumentError + # date-only format like "2024-06-15" is not supported by Time.new + if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val) + Time.utc($1.to_i, $2.to_i, $3.to_i) + else + val + end end - elsif match = ARRAY_REGEX.match(line) - _, val = match.captures - val = strip_comment(val) + elsif /^-?\d+$/.match?(val) + val.to_i + else + val + end + end - last_hash[last_empty_key] = [] unless last_hash[last_empty_key].is_a?(Array) + def decode_binary_tag(str) + content = str.sub(/\A!binary\s+/, "") + content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/ + content.unpack1("m") + end - last_hash[last_empty_key].push(val) + def parse_binary_value(val, indent) + rest = val.sub(/\A!binary\s+/, "") + if rest.start_with?("|") + content = parse_block_scalar(indent, rest[1..].to_s.strip) + Scalar.new(value: content.unpack1("m")) + else + Scalar.new(value: decode_binary_tag(val)) end end - res - end - def strip_comment(val) - if val.include?("#") && !val.start_with?("#") - val.split("#", 2).first.strip - else + def parse_alias_ref + AliasRef.new(name: @lines.shift.lstrip[1..].strip) + end + + def parse_inline_alias(content) + AliasRef.new(name: content[1..].strip) + end + + def current_indent + line = @lines[0] + line.size - line.lstrip.size + end + + def consume_anchor + line = @lines[0] + stripped = line.lstrip + return nil unless stripped.start_with?("&") && stripped =~ /^&(\S+)\s+/ + + anchor = $1 + @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "") + anchor + end + + def extract_item_anchor(content) + return [nil, content] unless content =~ /^&(\S+)/ + + anchor = $1 + [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")] + end + + def consume_value_anchor(val) + return [nil, val] unless val =~ /^&(\S+)\s+/ + + anchor = $1 + [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")] + end + + def register_anchor(name, node) + if name + @anchors[name] = node + node.anchor = name if node.respond_to?(:anchor=) + end + node + end + + def raise_max_nesting! + message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})" + if defined?(Psych::VERSION) + raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil) + else + raise Psych::SyntaxError, message + end + end + + def skip_blank_and_comments + while @lines.any? + line = @lines[0] + stripped = line.lstrip + break unless stripped.empty? || stripped.start_with?("#") + @lines.shift + end + end + + def strip_comment(val) + return val unless val.include?("#") + return val if val.lstrip.start_with?("#") + + in_single = false + in_double = false + escape = false + + val.each_char.with_index do |ch, i| + if escape + escape = false + next + end + + if in_single + in_single = false if ch == "'" + elsif in_double + if ch == "\\" + escape = true + elsif ch == '"' + in_double = false + end + else + case ch + when "'" then in_single = true + when '"' then in_double = true + when "#" then return val[0...i].rstrip + end + end + end + val end end - class << self - private :dump_hash + class Builder + VALID_OPS = %w[= != > < >= <= ~>].freeze + ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze + MAX_ALIAS_RESOLUTIONS = 1_000 + + def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) + @permitted_classes = permitted_classes.map {|c| "!ruby/object:#{c}" } + @permitted_symbols = permitted_symbols + @aliases = aliases + @anchor_values = {} + @alias_count = 0 + end + + def build(node) + return nil if node.nil? + + result = build_node(node) + + if result.is_a?(Hash) && result[:tag] == "!ruby/object:Gem::Specification" + build_specification(result) + else + result + end + end + + private + + def build_node(node) + case node + when nil then nil + when AliasRef then resolve_alias(node) + when Scalar then store_anchor(node.anchor, node.value) + when Mapping then build_mapping(node) + when Sequence then store_anchor(node.anchor, node.items.map {|item| build_node(item) }) + else node # already a Ruby object + end + end + + def resolve_alias(node) + raise Psych::AliasesNotEnabled unless @aliases + @alias_count += 1 + if @alias_count > MAX_ALIAS_RESOLUTIONS + raise Psych::BadAlias, "exceeded maximum alias resolutions (#{MAX_ALIAS_RESOLUTIONS})" + end + unless @anchor_values.key?(node.name) + klass = defined?(Psych::AnchorNotDefined) ? Psych::AnchorNotDefined : Psych::BadAlias + raise klass, "An alias referenced an unknown anchor: #{node.name}" + end + @anchor_values.fetch(node.name) + end + + def store_anchor(name, value) + @anchor_values[name] = value if name + value + end + + def build_mapping(node) + validate_tag!(node.tag) if node.tag + + result = case node.tag + when "!ruby/object:Gem::Version" + build_version(node) + when "!ruby/object:Gem::Platform" + build_platform(node) + when "!ruby/object:Gem::Requirement", "!ruby/object:Gem::Version::Requirement" + build_requirement(node) + when "!ruby/object:Gem::Dependency" + build_dependency(node) + when nil + build_hash(node) + when "!ruby/object:Gem::Specification" + hash = build_hash(node) + hash[:tag] = node.tag + hash + else + raise ArgumentError, "undefined class/module #{node.tag.sub("!ruby/object:", "")}" + end + + store_anchor(node.anchor, result) + end + + def build_hash(node) + result = {} + node.pairs.each do |key_node, value_node| + key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s + value = build_node(value_node) + + if ARRAY_FIELDS.include?(key) + value = normalize_array_field(value) + end + + result[key] = value + end + result + end + + def build_version(node) + hash = pairs_to_hash(node) + Gem::Version.new((hash["version"] || hash["value"]).to_s) + end + + PLATFORM_FIELDS = %w[cpu os version].freeze + PLATFORM_ALLOWED_IVARS = %w[cpu os version value].freeze + + def build_platform(node) + hash = pairs_to_hash(node) + if (hash.keys & PLATFORM_FIELDS).any? + Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]]) + elsif hash["value"].is_a?(Array) + # Malformed platform (e.g. sequence instead of mapping). + # Return the raw value so yaml_initialize handles it like Psych does. + hash["value"] + else + plat = Gem::Platform.allocate + hash.each do |k, v| + plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k) + end + plat + end + end + + def build_requirement(node) + r = Gem::Requirement.allocate + hash = pairs_to_hash(node) + reqs = hash["requirements"] || hash["value"] + + if reqs.is_a?(Array) && !reqs.empty? + safe_reqs = [] + reqs.each do |item| + if item.is_a?(Array) && item.size == 2 + op = item[0].to_s + ver = item[1] + if VALID_OPS.include?(op) + version_obj = ver.is_a?(Gem::Version) ? ver : Gem::Version.new(ver.to_s) + safe_reqs << [op, version_obj] + end + elsif item.is_a?(String) + parsed = Gem::Requirement.parse(item) + safe_reqs << parsed + end + rescue Gem::Requirement::BadRequirementError, Gem::Version::BadVersionError + # Skip malformed items silently + end + reqs = safe_reqs unless safe_reqs.empty? + end + + r.instance_variable_set(:@requirements, reqs) + r + end + + def build_dependency(node) + hash = pairs_to_hash(node) + d = Gem::Dependency.allocate + d.instance_variable_set(:@name, hash["name"]) + + d.instance_variable_set(:@requirement, hash["requirement"] || hash["version_requirements"]) + + raw_type = hash["type"] + if raw_type + name = raw_type.to_s.sub(/^:/, "") + validate_symbol!(name) + type = name.to_sym + else + type = :runtime + end + d.instance_variable_set(:@type, type) + + d.instance_variable_set(:@prerelease, ["true", true].include?(hash["prerelease"])) + d.instance_variable_set(:@version_requirements, d.instance_variable_get(:@requirement)) + d + end + + def build_specification(hash) + spec = Gem::Specification.allocate + + normalize_specification_version!(hash) + normalize_array_fields!(hash) + + spec.yaml_initialize("!ruby/object:Gem::Specification", hash) + spec + end + + def pairs_to_hash(node) + result = {} + node.pairs.each do |key_node, value_node| + key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s + result[key] = build_node(value_node) + end + result + end + + def validate_tag!(tag) + return if @permitted_classes.include?(tag) + raise_disallowed_class!(tag) + end + + def raise_disallowed_class!(tag) + if defined?(Psych::VERSION) + raise Psych::DisallowedClass.new("load", tag) + else + raise Psych::DisallowedClass, "Tried to load unspecified class: #{tag}" + end + end + + def validate_symbol!(name) + return if @permitted_symbols.empty? || @permitted_symbols.include?(name) + + label = ":#{name}" + if defined?(Psych::VERSION) + raise Psych::DisallowedClass.new("load", label) + else + raise Psych::DisallowedClass, "Tried to load unspecified class: #{label}" + end + end + + def normalize_specification_version!(hash) + val = hash["specification_version"] + return unless val && !val.is_a?(Integer) + hash["specification_version"] = val.to_i if val.is_a?(String) && /\A\d+\z/.match?(val) + end + + def normalize_array_fields!(hash) + ARRAY_FIELDS.each do |field| + hash[field] = normalize_array_field(hash[field]) if hash[field] + end + end + + def normalize_array_field(value) + if value.is_a?(Hash) + value.values.flatten.compact + elsif !value.is_a?(Array) && value + [value].flatten.compact + else + value + end + end + end + + class Emitter + def emit(obj) + "---#{emit_node(obj, 0)}" + end + + private + + def emit_node(obj, indent, quote: false) + case obj + when Gem::Specification then emit_specification(obj, indent) + when Gem::Version then emit_version(obj, indent) + when Gem::Platform then emit_platform(obj, indent) + when Gem::Requirement then emit_requirement(obj, indent) + when Gem::Dependency then emit_dependency(obj, indent) + when Hash then emit_hash(obj, indent) + when Array then emit_array(obj, indent) + when Time then emit_time(obj) + when String then emit_string(obj, indent, quote: quote) + when NilClass + "\n" + when Numeric, Symbol, TrueClass, FalseClass + " #{obj.inspect}\n" + else + " #{obj.to_s.inspect}\n" + end + end + + def emit_specification(spec, indent) + parts = [" !ruby/object:Gem::Specification\n"] + parts << "#{pad(indent)}name:#{emit_node(spec.name, indent + 2)}" + parts << "#{pad(indent)}version:#{emit_node(spec.version, indent + 2)}" + parts << "#{pad(indent)}platform: #{spec.platform}\n" + if spec.platform.to_s != spec.original_platform.to_s + parts << "#{pad(indent)}original_platform: #{spec.original_platform}\n" + end + + attributes = Gem::Specification.attribute_names.map(&:to_s).sort - %w[name version platform] + attributes.each do |name| + val = spec.instance_variable_get("@#{name}") + next if val.nil? + parts << "#{pad(indent)}#{name}:#{emit_node(val, indent + 2)}" + end + + res = parts.join + res << "\n" unless res.end_with?("\n") + res + end + + def emit_version(ver, indent) + " !ruby/object:Gem::Version\n" \ + "#{pad(indent)}version: #{emit_node(ver.version.to_s, indent + 2).lstrip}" + end + + def emit_platform(plat, indent) + " !ruby/object:Gem::Platform\n" \ + "#{pad(indent)}cpu:#{emit_node(plat.cpu, indent + 2)}" \ + "#{pad(indent)}os:#{emit_node(plat.os, indent + 2)}" \ + "#{pad(indent)}version:#{emit_node(plat.version, indent + 2)}" + end + + def emit_requirement(req, indent) + " !ruby/object:Gem::Requirement\n" \ + "#{pad(indent)}requirements:#{emit_node(req.requirements, indent + 2)}" + end + + def emit_dependency(dep, indent) + [ + " !ruby/object:Gem::Dependency\n", + "#{pad(indent)}name: #{emit_node(dep.name, indent + 2).lstrip}", + "#{pad(indent)}requirement:#{emit_node(dep.requirement, indent + 2)}", + "#{pad(indent)}type: #{emit_node(dep.type, indent + 2).lstrip}", + "#{pad(indent)}prerelease: #{emit_node(dep.prerelease?, indent + 2).lstrip}", + "#{pad(indent)}version_requirements:#{emit_node(dep.requirement, indent + 2)}", + ].join + end + + def emit_hash(hash, indent) + if hash.empty? + " {}\n" + else + parts = ["\n"] + hash.each do |k, v| + is_symbol = k.is_a?(Symbol) || (k.is_a?(String) && k.start_with?(":")) + key_str = k.is_a?(Symbol) ? k.inspect : k.to_s + parts << "#{pad(indent)}#{key_str}:#{emit_node(v, indent + 2, quote: is_symbol)}" + end + parts.join + end + end + + def emit_array(arr, indent) + if arr.empty? + " []\n" + else + parts = ["\n"] + arr.each do |v| + parts << "#{pad(indent)}-#{emit_node(v, indent + 2)}" + end + parts.join + end + end + + def emit_time(time) + " #{time.utc.strftime("%Y-%m-%d %H:%M:%S.%N Z")}\n" + end + + def emit_string(str, indent, quote: false) + if str.include?("\n") + emit_block_scalar(str, indent) + elsif needs_quoting?(str, quote) + " #{str.to_s.inspect}\n" + else + " #{str}\n" + end + end + + def emit_block_scalar(str, indent) + parts = [str.end_with?("\n") ? " |\n" : " |-\n"] + str.each_line do |line| + parts << "#{pad(indent + 2)}#{line}" + end + res = parts.join + res << "\n" unless res.end_with?("\n") + res + end + + def needs_quoting?(str, quote) + quote || str.empty? || + str =~ /^[!*&:@%$]/ || str =~ /^-?\d+(\.\d+)?$/ || str =~ /^[<>=-]/ || + str == "true" || str == "false" || str == "nil" || + str.include?(":") || str.include?("#") || str.include?("[") || str.include?("]") || + str.include?("{") || str.include?("}") || str.include?(",") + end + + def pad(indent) + " " * indent + end + end + + module_function + + def dump(obj) + Emitter.new.emit(obj) + end + + def load(str, permitted_classes: [], permitted_symbols: [], aliases: true) + raise TypeError, "no implicit conversion of nil into String" if str.nil? + return nil if str.empty? + + ast = Parser.new(str).parse + return nil if ast.nil? + + Builder.new( + permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + aliases: aliases + ).build(ast) end end end |
