diff options
Diffstat (limited to 'lib/rubygems/resolver')
26 files changed, 2110 insertions, 0 deletions
diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb new file mode 100644 index 0000000000..5c722001b1 --- /dev/null +++ b/lib/rubygems/resolver/activation_request.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +## +# Specifies a Specification object that should be activated. Also contains a +# dependency that was used to introduce this activation. + +class Gem::Resolver::ActivationRequest + ## + # The parent request for this activation request. + + attr_reader :request + + ## + # The specification to be activated. + + attr_reader :spec + + ## + # Creates a new ActivationRequest that will activate +spec+. The parent + # +request+ is used to provide diagnostics in case of conflicts. + + def initialize(spec, request) + @spec = spec + @request = request + end + + def ==(other) # :nodoc: + case other + when Gem::Specification + @spec == other + when Gem::Resolver::ActivationRequest + @spec == other.spec + else + false + end + end + + def eql?(other) + self == other + end + + def hash + @spec.hash + end + + ## + # Is this activation request for a development dependency? + + def development? + @request.development? + end + + ## + # Downloads a gem at +path+ and returns the file path. + + def download(path) + Gem.ensure_gem_subdirectories path + + if @spec.respond_to? :sources + exception = nil + path = @spec.sources.find do |source| + source.download full_spec, path + rescue exception + end + return path if path + raise exception if exception + + elsif @spec.respond_to? :source + source = @spec.source + source.download full_spec, path + + else + source = Gem.sources.first + source.download full_spec, path + end + end + + ## + # The full name of the specification to be activated. + + def full_name + name_tuple.full_name + end + + alias_method :to_s, :full_name + + ## + # The Gem::Specification for this activation request. + + def full_spec + Gem::Specification === @spec ? @spec : @spec.spec + end + + def inspect # :nodoc: + format("#<%s for %p from %s>", self.class, @spec, @request) + end + + ## + # True if the requested gem has already been installed. + + def installed? + case @spec + when Gem::Resolver::VendorSpecification then + true + else + this_spec = full_spec + + Gem::Specification.any? do |s| + s == this_spec && s.base_dir == this_spec.base_dir + end + end + end + + ## + # The name of this activation request's specification + + def name + @spec.name + end + + ## + # Return the ActivationRequest that contained the dependency + # that we were activated for. + + def parent + @request.requester + end + + def pretty_print(q) # :nodoc: + q.group 2, "[Activation request", "]" do + q.breakable + q.pp @spec + + q.breakable + q.text " for " + q.pp @request + end + end + + ## + # The version of this activation request's specification + + def version + @spec.version + end + + ## + # The platform of this activation request's specification + + def platform + @spec.platform + end + + private + + def name_tuple + @name_tuple ||= Gem::NameTuple.new(name, version, platform) + end +end diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb new file mode 100644 index 0000000000..3f443519d8 --- /dev/null +++ b/lib/rubygems/resolver/api_set.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +## +# 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 Compact Index API this APISet uses. + + attr_reader :dep_uri # :nodoc: + + ## + # The Gem::Source that gems are fetched from + + attr_reader :source + + ## + # The corresponding place to fetch gems. + + attr_reader :uri + + ## + # 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() + + dep_uri = Gem::URI dep_uri unless Gem::URI === dep_uri + + @dep_uri = dep_uri + @uri = dep_uri + ".." + + @data = Hash.new {|h,k| h[k] = [] } + @source = Gem::Source.new @uri + + @to_fetch = [] + end + + ## + # Return an array of APISpecification objects matching + # DependencyRequest +req+. + + def find_all(req) + res = [] + + return res unless @remote + + if @to_fetch.include?(req.name) + prefetch_now + end + + versions(req.name).each do |ver| + if req.dependency.match? req.name, ver[:number], @prerelease + res << Gem::Resolver::APISpecification.new(self, ver) + end + end + + res + end + + ## + # A hint run by the resolver to allow the Set to fetch + # data for DependencyRequests +reqs+. + + def prefetch(reqs) + return unless @remote + names = reqs.map {|r| r.dependency.name } + needed = names - @data.keys - @to_fetch + + @to_fetch += needed + end + + def prefetch_now # :nodoc: + needed = @to_fetch + @to_fetch = [] + + needed.sort.each do |name| + versions(name) + end + end + + def pretty_print(q) # :nodoc: + q.group 2, "[APISet", "]" do + q.breakable + q.text "URI: #{@dep_uri}" + + q.breakable + q.text "gem names:" + q.pp @data.keys + end + end + + ## + # Return data for all versions of the gem +name+. + + def versions(name) # :nodoc: + if @data.key?(name) + return @data[name] + end + + uri = @dep_uri + name + + begin + str = Gem::RemoteFetcher.fetcher.fetch_path uri + rescue Gem::RemoteFetcher::FetchError + @data[name] = [] + else + lines(str).each do |ver| + number, platform, dependencies, requirements = parse_gem(ver) + + platform ||= "ruby" + dependencies = dependencies.map {|dep_name, reqs| [dep_name, reqs.join(", ")] } + requirements = requirements.map {|req_name, reqs| [req_name.to_sym, reqs] }.to_h + + @data[name] << { name: name, number: number, platform: platform, dependencies: dependencies, requirements: requirements } + end + end + + @data[name] + end + + private + + def lines(str) + lines = str.split("\n") + header = lines.index("---") + header ? lines[header + 1..-1] : lines + end + + def parse_gem(string) + @gem_parser ||= GemParser.new + @gem_parser.parse(string) + end +end diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb new file mode 100644 index 0000000000..4d827f4980 --- /dev/null +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Gem::Resolver::APISet::GemParser + 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) } : [] + requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : [] + [version, platform, dependencies, requirements] + end + + private + + def parse_dependency(string) + dependency = string.split(":", 2) + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] + dependency + end +end diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb new file mode 100644 index 0000000000..ccfd6fe084 --- /dev/null +++ b/lib/rubygems/resolver/api_specification.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +## +# 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. + +class Gem::Resolver::APISpecification < Gem::Resolver::Specification + ## + # We assume that all instances of this class are immutable; + # so avoid duplicated generation for performance. + @@cache = {} + def self.new(set, api_data) + cache_key = [set, api_data] + cache = @@cache[cache_key] + return cache if cache + @@cache[cache_key] = super + end + + ## + # Creates an APISpecification for the given +set+ from the Compact Index API + # +api_data+. + # + # See https://guides.rubygems.org/rubygems-org-compact-index-api for the + # format of the +api_data+. + + def initialize(set, api_data) + super() + + @set = set + @name = api_data[:name] + @version = Gem::Version.new(api_data[:number]).freeze + @platform = Gem::Platform.new(api_data[:platform]).freeze + @original_platform = api_data[:platform].freeze + @dependencies = api_data[:dependencies].map do |name, ver| + Gem::Dependency.new(name, ver.split(/\s*,\s*/)).freeze + end.freeze + @required_ruby_version = Gem::Requirement.new(api_data.dig(:requirements, :ruby)).freeze + @required_rubygems_version = Gem::Requirement.new(api_data.dig(:requirements, :rubygems)).freeze + end + + def ==(other) # :nodoc: + self.class === other && + @set == other.set && + @name == other.name && + @version == other.version && + @platform == other.platform + end + + def hash + @set.hash ^ @name.hash ^ @version.hash ^ @platform.hash + end + + def fetch_development_dependencies # :nodoc: + spec = source.fetch_spec Gem::NameTuple.new @name, @version, @platform + + @dependencies = spec.dependencies + end + + def installable_platform? # :nodoc: + Gem::Platform.match_gem? @platform, @name + end + + def pretty_print(q) # :nodoc: + q.group 2, "[APISpecification", "]" do + q.breakable + q.text "name: #{name}" + + q.breakable + q.text "version: #{version}" + + q.breakable + q.text "platform: #{platform}" + + q.breakable + q.text "dependencies:" + q.breakable + q.pp @dependencies + + q.breakable + q.text "set uri: #{@set.dep_uri}" + end + end + + ## + # Fetches a Gem::Specification for this APISpecification. + + def spec # :nodoc: + @spec ||= + begin + tuple = Gem::NameTuple.new @name, @version, @platform + source.fetch_spec tuple + rescue Gem::RemoteFetcher::FetchError + raise if @original_platform == @platform + + tuple = Gem::NameTuple.new @name, @version, @original_platform + source.fetch_spec tuple + end + end + + def source # :nodoc: + @set.source + end +end diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb new file mode 100644 index 0000000000..e647a2c11b --- /dev/null +++ b/lib/rubygems/resolver/best_set.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +## +# The BestSet chooses the best available method to query a remote index. +# +# It combines IndexSet and APISet + +class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet + ## + # Creates a BestSet for the given +sources+ or Gem::sources if none are + # specified. +sources+ must be a Gem::SourceList. + + def initialize(sources = Gem.sources) + super() + + @sources = sources + end + + ## + # Picks which sets to use for the configured sources. + + def pick_sets # :nodoc: + @sources.each_source do |source| + @sets << source.dependency_resolver_set(@prerelease) + end + end + + def find_all(req) # :nodoc: + pick_sets if @remote && @sets.empty? + + super + end + + def prefetch(reqs) # :nodoc: + pick_sets if @remote && @sets.empty? + + super + end + + def pretty_print(q) # :nodoc: + q.group 2, "[BestSet", "]" do + q.breakable + q.text "sets:" + + q.breakable + q.pp @sets + end + end +end diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb new file mode 100644 index 0000000000..e67dd41754 --- /dev/null +++ b/lib/rubygems/resolver/composed_set.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +## +# A ComposedSet allows multiple sets to be queried like a single set. +# +# To create a composed set with any number of sets use: +# +# Gem::Resolver.compose_sets set1, set2 +# +# This method will eliminate nesting of composed sets. + +class Gem::Resolver::ComposedSet < Gem::Resolver::Set + attr_reader :sets # :nodoc: + + ## + # Creates a new ComposedSet containing +sets+. Use + # Gem::Resolver::compose_sets instead. + + def initialize(*sets) + super() + + @sets = sets + end + + ## + # When +allow_prerelease+ is set to +true+ prereleases gems are allowed to + # match dependencies. + + def prerelease=(allow_prerelease) + super + + sets.each do |set| + set.prerelease = allow_prerelease + end + end + + ## + # Sets the remote network access for all composed sets. + + def remote=(remote) + super + + @sets.each {|set| set.remote = remote } + end + + def errors + @errors + @sets.flat_map(&:errors) + end + + ## + # Finds all specs matching +req+ in all sets. + + def find_all(req) + @sets.flat_map do |s| + s.find_all req + end + end + + ## + # Prefetches +reqs+ in all sets. + + def prefetch(reqs) + @sets.each {|s| s.prefetch(reqs) } + end +end diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb new file mode 100644 index 0000000000..370e445089 --- /dev/null +++ b/lib/rubygems/resolver/current_set.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +## +# A set which represents the installed gems. Respects +# all the normal settings that control where to look +# for installed gems. + +class Gem::Resolver::CurrentSet < Gem::Resolver::Set + def find_all(req) + req.dependency.matching_specs + end +end diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb new file mode 100644 index 0000000000..60b338277f --- /dev/null +++ b/lib/rubygems/resolver/dependency_request.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +## +# Used Internally. Wraps a Dependency object to also track which spec +# contained the Dependency. + +class Gem::Resolver::DependencyRequest + ## + # The wrapped Gem::Dependency + + attr_reader :dependency + + ## + # The request for this dependency. + + attr_reader :requester + + ## + # Creates a new DependencyRequest for +dependency+ from +requester+. + # +requester may be nil if the request came from a user. + + def initialize(dependency, requester) + @dependency = dependency + @requester = requester + end + + def ==(other) # :nodoc: + case other + when Gem::Dependency + @dependency == other + when Gem::Resolver::DependencyRequest + @dependency == other.dependency + else + false + end + end + + ## + # Is this dependency a development dependency? + + def development? + @dependency.type == :development + end + + ## + # Does this dependency request match +spec+? + # + # NOTE: #match? only matches prerelease versions when #dependency is a + # prerelease dependency. + + def match?(spec, allow_prerelease = false) + @dependency.match? spec, nil, allow_prerelease + end + + ## + # Does this dependency request match +spec+? + # + # NOTE: #matches_spec? matches prerelease versions. See also #match? + + def matches_spec?(spec) + @dependency.matches_spec? spec + end + + ## + # The name of the gem this dependency request is requesting. + + def name + @dependency.name + end + + def type + @dependency.type + end + + ## + # Indicate that the request is for a gem explicitly requested by the user + + def explicit? + @requester.nil? + end + + ## + # Indicate that the request is for a gem requested as a dependency of + # another gem + + def implicit? + !explicit? + end + + ## + # Return a String indicating who caused this request to be added (only + # valid for implicit requests) + + def request_context + @requester ? @requester.request : "(unknown)" + end + + def pretty_print(q) # :nodoc: + q.group 2, "[Dependency request ", "]" do + q.breakable + q.text @dependency.to_s + + q.breakable + q.text " requested by " + q.pp @requester + end + end + + ## + # The version requirement for this dependency request + + def requirement + @dependency.requirement + end + + def to_s # :nodoc: + @dependency.to_s + end +end diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb new file mode 100644 index 0000000000..2912378fe7 --- /dev/null +++ b/lib/rubygems/resolver/git_set.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +## +# A GitSet represents gems that are sourced from git repositories. +# +# This is used for gem dependency file support. +# +# Example: +# +# set = Gem::Resolver::GitSet.new +# set.add_git_gem 'rake', 'git://example/rake.git', tag: 'rake-10.1.0' + +class Gem::Resolver::GitSet < Gem::Resolver::Set + ## + # The root directory for git gems in this set. This is usually Gem.dir, the + # installation directory for regular gems. + + attr_accessor :root_dir + + ## + # Contains repositories needing submodules + + attr_reader :need_submodules # :nodoc: + + ## + # A Hash containing git gem names for keys and a Hash of repository and + # git commit reference as values. + + attr_reader :repositories # :nodoc: + + ## + # A hash of gem names to Gem::Resolver::GitSpecifications + + attr_reader :specs # :nodoc: + + def initialize # :nodoc: + super() + + @need_submodules = {} + @repositories = {} + @root_dir = Gem.dir + @specs = {} + end + + def add_git_gem(name, repository, reference, submodules) # :nodoc: + @repositories[name] = [repository, reference] + @need_submodules[repository] = submodules + end + + ## + # Adds and returns a GitSpecification with the given +name+ and +version+ + # which came from a +repository+ at the given +reference+. If +submodules+ + # is true they are checked out along with the repository. + # + # This fills in the prefetch information as enough information about the gem + # is present in the arguments. + + def add_git_spec(name, version, repository, reference, submodules) # :nodoc: + add_git_gem name, repository, reference, submodules + + source = Gem::Source::Git.new name, repository, reference + source.root_dir = @root_dir + + spec = Gem::Specification.new do |s| + s.name = name + s.version = version + end + + git_spec = Gem::Resolver::GitSpecification.new self, spec, source + + @specs[spec.name] = git_spec + + git_spec + end + + ## + # Finds all git gems matching +req+ + + def find_all(req) + prefetch nil + + specs.values.select do |spec| + req.match? spec + end + end + + ## + # Prefetches specifications from the git repositories in this set. + + def prefetch(reqs) + return unless @specs.empty? + + @repositories.each do |name, (repository, reference)| + source = Gem::Source::Git.new name, repository, reference + source.root_dir = @root_dir + source.remote = @remote + + source.specs.each do |spec| + git_spec = Gem::Resolver::GitSpecification.new self, spec, source + + @specs[spec.name] = git_spec + end + end + end + + def pretty_print(q) # :nodoc: + q.group 2, "[GitSet", "]" do + next if @repositories.empty? + q.breakable + + repos = @repositories.map do |name, (repository, reference)| + "#{name}: #{repository}@#{reference}" + end + + q.seplist repos do |repo| + q.text repo + end + end + end +end diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb new file mode 100644 index 0000000000..e587c17d2a --- /dev/null +++ b/lib/rubygems/resolver/git_specification.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +## +# A GitSpecification represents a gem that is sourced from a git repository +# and is being loaded through a gem dependencies file through the +git:+ +# option. + +class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification + def ==(other) # :nodoc: + self.class === other && + @set == other.set && + @spec == other.spec && + @source == other.source + end + + def add_dependency(dependency) # :nodoc: + spec.dependencies << dependency + end + + ## + # Installing a git gem only involves building the extensions and generating + # the executables. + + def install(options = {}) + require_relative "../installer" + + installer = Gem::Installer.for_spec spec, options + + yield installer if block_given? + + installer.run_pre_install_hooks + installer.build_extensions + installer.run_post_build_hooks + installer.generate_bin + installer.run_post_install_hooks + end + + def pretty_print(q) # :nodoc: + q.group 2, "[GitSpecification", "]" do + q.breakable + q.text "name: #{name}" + + q.breakable + q.text "version: #{version}" + + q.breakable + q.text "dependencies:" + q.breakable + q.pp dependencies + + q.breakable + q.text "source:" + q.breakable + q.pp @source + end + 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/index_set.rb b/lib/rubygems/resolver/index_set.rb new file mode 100644 index 0000000000..cddaf8773f --- /dev/null +++ b/lib/rubygems/resolver/index_set.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +## +# The global rubygems pool represented via the traditional +# source index. + +class Gem::Resolver::IndexSet < Gem::Resolver::Set + def initialize(source = nil) # :nodoc: + super() + + @f = + if source + sources = Gem::SourceList.from [source] + + Gem::SpecFetcher.new sources + else + Gem::SpecFetcher.fetcher + end + + @all = Hash.new {|h,k| h[k] = [] } + + list, errors = @f.available_specs :complete + + @errors.concat errors + + list.each do |uri, specs| + specs.each do |n| + @all[n.name] << [uri, n] + end + end + + @specs = {} + end + + ## + # Return an array of IndexSpecification objects matching + # DependencyRequest +req+. + + def find_all(req) + res = [] + + return res unless @remote + + name = req.dependency.name + + @all[name].each do |uri, n| + next unless req.match? n, @prerelease + res << Gem::Resolver::IndexSpecification.new( + self, n.name, n.version, uri, n.platform + ) + end + + res + end + + def pretty_print(q) # :nodoc: + q.group 2, "[IndexSet", "]" do + q.breakable + q.text "sources:" + q.breakable + q.pp @f.sources + + q.breakable + q.text "specs:" + + q.breakable + + names = @all.values.flat_map do |tuples| + tuples.map do |_, tuple| + tuple.full_name + end + end + + q.seplist names do |name| + q.text name + end + end + end +end diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb new file mode 100644 index 0000000000..7b95608071 --- /dev/null +++ b/lib/rubygems/resolver/index_specification.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +## +# Represents a possible Specification object returned from IndexSet. Used to +# delay needed to download full Specification objects when only the +name+ +# and +version+ are needed. + +class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification + ## + # An IndexSpecification is created from the index format described in `gem + # help generate_index`. + # + # The +set+ contains other specifications for this (URL) +source+. + # + # The +name+, +version+ and +platform+ are the name, version and platform of + # the gem. + + def initialize(set, name, version, source, platform) + super() + + @set = set + @name = name + @version = version + @source = source + @platform = Gem::Platform.new(platform.to_s) + @original_platform = platform.to_s + + @spec = nil + end + + ## + # The dependencies of the gem for this specification + + def dependencies + spec.dependencies + end + + ## + # The required_ruby_version constraint for this specification + # + # A fallback is included because when generated, some marshalled specs have it + # set to +nil+. + + def required_ruby_version + spec.required_ruby_version || Gem::Requirement.default + end + + ## + # The required_rubygems_version constraint for this specification + # + # A fallback is included because the original version of the specification + # API didn't include that field, so some marshalled specs in the index have it + # set to +nil+. + + def required_rubygems_version + spec.required_rubygems_version || Gem::Requirement.default + end + + def ==(other) + self.class === other && + @name == other.name && + @version == other.version && + @platform == other.platform + end + + def hash + @name.hash ^ @version.hash ^ @platform.hash + end + + def inspect # :nodoc: + format("#<%s %s source %s>", self.class, full_name, @source) + end + + def pretty_print(q) # :nodoc: + q.group 2, "[Index specification", "]" do + q.breakable + q.text full_name + + unless @platform == Gem::Platform::RUBY + q.breakable + q.text @platform.to_s + end + + q.breakable + q.text "source " + q.pp @source + end + end + + ## + # Fetches a Gem::Specification for this IndexSpecification from the #source. + + def spec # :nodoc: + @spec ||= + begin + tuple = Gem::NameTuple.new @name, @version, @original_platform + + @source.fetch_spec tuple + end + end +end diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb new file mode 100644 index 0000000000..8280ae4672 --- /dev/null +++ b/lib/rubygems/resolver/installed_specification.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +## +# An InstalledSpecification represents a gem that is already installed +# locally. + +class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification + def ==(other) # :nodoc: + self.class === other && + @set == other.set && + @spec == other.spec + end + + ## + # This is a null install as this specification is already installed. + # +options+ are ignored. + + def install(options = {}) + yield nil + end + + ## + # Returns +true+ if this gem is installable for the current platform. + + def installable_platform? + # BACKCOMPAT If the file is coming out of a specified file, then we + # ignore the platform. This code can be removed in RG 3.0. + return true if @source.is_a? Gem::Source::SpecificFile + + super + end + + def pretty_print(q) # :nodoc: + q.group 2, "[InstalledSpecification", "]" do + q.breakable + q.text "name: #{name}" + + q.breakable + q.text "version: #{version}" + + q.breakable + q.text "platform: #{platform}" + + q.breakable + q.text "dependencies:" + q.breakable + q.pp spec.dependencies + end + end + + ## + # The source for this specification + + def source + @source ||= Gem::Source::Installed.new + end +end diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb new file mode 100644 index 0000000000..42ce0890e2 --- /dev/null +++ b/lib/rubygems/resolver/installer_set.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +## +# A set of gems for installation sourced from remote sources and local .gem +# files + +class Gem::Resolver::InstallerSet < Gem::Resolver::Set + ## + # List of Gem::Specification objects that must always be installed. + + attr_reader :always_install # :nodoc: + + ## + # Only install gems in the always_install list + + attr_accessor :ignore_dependencies # :nodoc: + + ## + # Do not look in the installed set when finding specifications. This is + # used by the --install-dir option to `gem install` + + attr_accessor :ignore_installed # :nodoc: + + ## + # The remote_set looks up remote gems for installation. + + attr_reader :remote_set # :nodoc: + + ## + # Ignore ruby & rubygems specification constraints. + # + + attr_accessor :force # :nodoc: + + ## + # Creates a new InstallerSet that will look for gems in +domain+. + + def initialize(domain) + super() + + @domain = domain + + @f = Gem::SpecFetcher.fetcher + + @always_install = [] + @ignore_dependencies = false + @ignore_installed = false + @local = {} + @local_source = Gem::Source::Local.new + @remote_set = Gem::Resolver::BestSet.new + @force = false + @specs = {} + end + + ## + # Looks up the latest specification for +dependency+ and adds it to the + # always_install list. + + def add_always_install(dependency) + request = Gem::Resolver::DependencyRequest.new dependency, nil + + found = find_all request + + found.delete_if do |s| + s.version.prerelease? && !s.local? + end unless dependency.prerelease? + + found = found.select do |s| + Gem::Source::SpecificFile === s.source || + Gem::Platform.match_spec?(s) + end + + found = found.sort_by do |s| + [s.version, Gem::Platform.sort_priority(s.platform)] + end + + newest = found.last + + unless newest + exc = Gem::UnsatisfiableDependencyError.new request + exc.errors = errors + + raise exc + end + + unless @force + found_matching_metadata = found.reverse.find do |spec| + metadata_satisfied?(spec) + end + + if found_matching_metadata.nil? + ensure_required_ruby_version_met(newest.spec) + ensure_required_rubygems_version_met(newest.spec) + else + newest = found_matching_metadata + end + end + + @always_install << newest.spec + end + + ## + # Adds a local gem requested using +dep_name+ with the given +spec+ that can + # be loaded and installed using the +source+. + + def add_local(dep_name, spec, source) + @local[dep_name] = [spec, source] + end + + ## + # Should local gems should be considered? + + def consider_local? # :nodoc: + @domain == :both || @domain == :local + end + + ## + # Should remote gems should be considered? + + def consider_remote? # :nodoc: + @domain == :both || @domain == :remote + end + + ## + # Errors encountered while resolving gems + + def errors + @errors + @remote_set.errors + end + + ## + # Returns an array of IndexSpecification objects matching DependencyRequest + # +req+. + + def find_all(req) + res = [] + + dep = req.dependency + + return res if @ignore_dependencies && + @always_install.none? {|spec| dep.match? spec } + + name = dep.name + + dep.matching_specs.each do |gemspec| + next if @always_install.any? {|spec| spec.name == gemspec.name } + + res << Gem::Resolver::InstalledSpecification.new(self, gemspec) + end unless @ignore_installed + + matching_local = [] + + if consider_local? + matching_local = @local.values.select do |spec, _| + req.match? spec + end.map do |spec, source| + Gem::Resolver::LocalSpecification.new self, spec, source + end + + res.concat matching_local + + begin + @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 + ) + end + rescue Gem::Package::FormatError + # ignore + end + end + + res.concat @remote_set.find_all req if consider_remote? && matching_local.empty? + + res + end + + def prefetch(reqs) + @remote_set.prefetch(reqs) if consider_remote? + end + + def prerelease=(allow_prerelease) + super + + @remote_set.prerelease = allow_prerelease + end + + def inspect # :nodoc: + always_install = @always_install.map(&:full_name) + + format("#<%s domain: %s specs: %p always install: %p>", self.class, @domain, @specs.keys, always_install) + end + + ## + # Called from IndexSpecification to get a true Specification + # object. + + def load_spec(name, ver, platform, source) # :nodoc: + key = "#{name}-#{ver}-#{platform}" + + @specs.fetch key do + tuple = Gem::NameTuple.new name, ver, platform + + @specs[key] = source.fetch_spec tuple + end + end + + ## + # Has a local gem for +dep_name+ been added to this set? + + def local?(dep_name) # :nodoc: + spec, _ = @local[dep_name] + + spec + end + + def pretty_print(q) # :nodoc: + q.group 2, "[InstallerSet", "]" do + q.breakable + q.text "domain: #{@domain}" + + q.breakable + q.text "specs: " + q.pp @specs.keys + + q.breakable + q.text "always install: " + q.pp @always_install + end + end + + def remote=(remote) # :nodoc: + case @domain + when :local then + @domain = :both if remote + when :remote then + @domain = nil unless remote + when :both then + @domain = :local unless remote + end + end + + private + + def metadata_satisfied?(spec) + spec.required_ruby_version.satisfied_by?(Gem.ruby_version) && + spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version) + end + + def ensure_required_ruby_version_met(spec) # :nodoc: + if rrv = spec.required_ruby_version + ruby_version = Gem.ruby_version + unless rrv.satisfied_by? ruby_version + raise Gem::RuntimeRequirementNotMetError, + "#{spec.full_name} requires Ruby version #{rrv}. The current ruby version is #{ruby_version}." + end + end + end + + def ensure_required_rubygems_version_met(spec) # :nodoc: + if rrgv = spec.required_rubygems_version + unless rrgv.satisfied_by? Gem.rubygems_version + rg_version = Gem::VERSION + raise Gem::RuntimeRequirementNotMetError, + "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " \ + "Try 'gem update --system' to update RubyGems itself." + end + end + end +end diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb new file mode 100644 index 0000000000..b57d40e795 --- /dev/null +++ b/lib/rubygems/resolver/local_specification.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +## +# A LocalSpecification comes from a .gem file on the local filesystem. + +class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification + ## + # Returns +true+ if this gem is installable for the current platform. + + def installable_platform? + return true if @source.is_a? Gem::Source::SpecificFile + + super + end + + def local? # :nodoc: + true + end + + def pretty_print(q) # :nodoc: + q.group 2, "[LocalSpecification", "]" do + q.breakable + q.text "name: #{name}" + + q.breakable + q.text "version: #{version}" + + q.breakable + q.text "platform: #{platform}" + + q.breakable + q.text "dependencies:" + q.breakable + q.pp dependencies + + q.breakable + q.text "source: #{@source.path}" + end + end +end diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb new file mode 100644 index 0000000000..e5ee32a9a6 --- /dev/null +++ b/lib/rubygems/resolver/lock_set.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +## +# A set of gems from a gem dependencies lockfile. + +class Gem::Resolver::LockSet < Gem::Resolver::Set + attr_reader :specs # :nodoc: + + ## + # Creates a new LockSet from the given +sources+ + + def initialize(sources) + super() + + @sources = sources.map do |source| + Gem::Source::Lock.new source + end + + @specs = [] + end + + ## + # Creates a new IndexSpecification in this set using the given +name+, + # +version+ and +platform+. + # + # The specification's set will be the current set, and the source will be + # the current set's source. + + def add(name, version, platform) # :nodoc: + version = Gem::Version.new version + specs = [ + Gem::Resolver::LockSpecification.new(self, name, version, @sources, platform), + ] + + @specs.concat specs + + specs + end + + ## + # Returns an Array of IndexSpecification objects matching the + # DependencyRequest +req+. + + def find_all(req) + @specs.select do |spec| + req.match? spec + end + end + + ## + # Loads a Gem::Specification with the given +name+, +version+ and + # +platform+. +source+ is ignored. + + def load_spec(name, version, platform, source) # :nodoc: + dep = Gem::Dependency.new name, version + + found = @specs.find do |spec| + dep.matches_spec?(spec) && spec.platform == platform + end + + tuple = Gem::NameTuple.new found.name, found.version, found.platform + + found.source.fetch_spec tuple + end + + def pretty_print(q) # :nodoc: + q.group 2, "[LockSet", "]" do + q.breakable + q.text "source:" + + q.breakable + q.pp @source + + q.breakable + q.text "specs:" + + q.breakable + q.pp @specs.map(&:full_name) + end + end +end diff --git a/lib/rubygems/resolver/lock_specification.rb b/lib/rubygems/resolver/lock_specification.rb new file mode 100644 index 0000000000..06f912dd85 --- /dev/null +++ b/lib/rubygems/resolver/lock_specification.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +## +# The LockSpecification comes from a lockfile (Gem::RequestSet::Lockfile). +# +# A LockSpecification's dependency information is pre-filled from the +# lockfile. + +class Gem::Resolver::LockSpecification < Gem::Resolver::Specification + attr_reader :sources + + def initialize(set, name, version, sources, platform) + super() + + @name = name + @platform = platform + @set = set + @source = sources.first + @sources = sources + @version = version + + @dependencies = [] + @spec = nil + end + + ## + # This is a null install as a locked specification is considered installed. + # +options+ are ignored. + + def install(options = {}) + destination = options[:install_dir] || Gem.dir + + if File.exist? File.join(destination, "specifications", spec.spec_name) + yield nil + return + end + + super + end + + ## + # Adds +dependency+ from the lockfile to this specification + + def add_dependency(dependency) # :nodoc: + @dependencies << dependency + end + + def pretty_print(q) # :nodoc: + q.group 2, "[LockSpecification", "]" do + q.breakable + q.text "name: #{@name}" + + q.breakable + q.text "version: #{@version}" + + unless @platform == Gem::Platform::RUBY + q.breakable + q.text "platform: #{@platform}" + end + + unless @dependencies.empty? + q.breakable + q.text "dependencies:" + q.breakable + q.pp @dependencies + end + end + end + + ## + # A specification constructed from the lockfile is returned + + def spec + @spec ||= Gem::Specification.find do |spec| + spec.name == @name && spec.version == @version + end + + @spec ||= Gem::Specification.new do |s| + s.name = @name + s.version = @version + s.platform = @platform + + s.dependencies.concat @dependencies + end + end +end diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb new file mode 100644 index 0000000000..6f86f0f412 --- /dev/null +++ b/lib/rubygems/resolver/requirement_list.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +## +# The RequirementList is used to hold the requirements being considered +# while resolving a set of gems. +# +# The RequirementList acts like a queue where the oldest items are removed +# first. + +class Gem::Resolver::RequirementList + include Enumerable + + ## + # Creates a new RequirementList. + + def initialize + @exact = [] + @list = [] + end + + def initialize_copy(other) # :nodoc: + @exact = @exact.dup + @list = @list.dup + end + + ## + # Adds Resolver::DependencyRequest +req+ to this requirements list. + + def add(req) + if req.requirement.exact? + @exact.push req + else + @list.push req + end + req + end + + ## + # Enumerates requirements in the list + + def each # :nodoc: + return enum_for __method__ unless block_given? + + @exact.each do |requirement| + yield requirement + end + + @list.each do |requirement| + yield requirement + end + end + + ## + # How many elements are in the list + + def size + @exact.size + @list.size + end + + ## + # Is the list empty? + + def empty? + @exact.empty? && @list.empty? + end + + ## + # Remove the oldest DependencyRequest from the list. + + def remove + return @exact.shift unless @exact.empty? + @list.shift + end + + ## + # Returns the oldest five entries from the list. + + def next5 + x = @exact[0,5] + x + @list[0,5 - x.size] + end +end diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb new file mode 100644 index 0000000000..243fee5fd5 --- /dev/null +++ b/lib/rubygems/resolver/set.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +## +# Resolver sets are used to look up specifications (and their +# dependencies) used in resolution. This set is abstract. + +class Gem::Resolver::Set + ## + # Set to true to disable network access for this set + + attr_accessor :remote + + ## + # Errors encountered when resolving gems + + attr_accessor :errors + + ## + # When true, allows matching of requests to prerelease gems. + + attr_accessor :prerelease + + def initialize # :nodoc: + @prerelease = false + @remote = true + @errors = [] + end + + ## + # The find_all method must be implemented. It returns all Resolver + # Specification objects matching the given DependencyRequest +req+. + + def find_all(req) + raise NotImplementedError + end + + ## + # The #prefetch method may be overridden, but this is not necessary. This + # default implementation does nothing, which is suitable for sets where + # looking up a specification is cheap (such as installed gems). + # + # When overridden, the #prefetch method should look up specifications + # matching +reqs+. + + def prefetch(reqs) + end + + ## + # When true, this set is allowed to access the network when looking up + # specifications or dependencies. + + def remote? # :nodoc: + @remote + end +end diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb new file mode 100644 index 0000000000..074b473edc --- /dev/null +++ b/lib/rubygems/resolver/source_set.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +## +# The SourceSet chooses the best available method to query a remote index. +# +# Kind off like BestSet but filters the sources for gems + +class Gem::Resolver::SourceSet < Gem::Resolver::Set + ## + # Creates a SourceSet for the given +sources+ or Gem::sources if none are + # specified. +sources+ must be a Gem::SourceList. + + def initialize + super() + + @links = {} + @sets = {} + end + + def find_all(req) # :nodoc: + if set = get_set(req.dependency.name) + set.find_all req + else + [] + end + end + + # potentially no-op + def prefetch(reqs) # :nodoc: + reqs.each do |req| + if set = get_set(req.dependency.name) + set.prefetch reqs + end + end + end + + def add_source_gem(name, source) + @links[name] = source + end + + private + + def get_set(name) + link = @links[name] + @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link + end +end diff --git a/lib/rubygems/resolver/spec_specification.rb b/lib/rubygems/resolver/spec_specification.rb new file mode 100644 index 0000000000..00ef9fdba0 --- /dev/null +++ b/lib/rubygems/resolver/spec_specification.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +## +# The Resolver::SpecSpecification contains common functionality for +# Resolver specifications that are backed by a Gem::Specification. + +class Gem::Resolver::SpecSpecification < Gem::Resolver::Specification + ## + # A SpecSpecification is created for a +set+ for a Gem::Specification in + # +spec+. The +source+ is either where the +spec+ came from, or should be + # loaded from. + + def initialize(set, spec, source = nil) + @set = set + @source = source + @spec = spec + end + + ## + # The dependencies of the gem for this specification + + def dependencies + spec.dependencies + end + + ## + # The required_ruby_version constraint for this specification + + def required_ruby_version + spec.required_ruby_version + end + + ## + # The required_rubygems_version constraint for this specification + + def required_rubygems_version + spec.required_rubygems_version + end + + ## + # The name and version of the specification. + # + # Unlike Gem::Specification#full_name, the platform is not included. + + def full_name + "#{spec.name}-#{spec.version}" + end + + ## + # The name of the gem for this specification + + def name + spec.name + end + + ## + # The platform this gem works on. + + def platform + spec.platform + end + + ## + # The version of the gem for this specification. + + def version + spec.version + end + + ## + # The hash value for this specification. + + def hash + spec.hash + end +end diff --git a/lib/rubygems/resolver/specification.rb b/lib/rubygems/resolver/specification.rb new file mode 100644 index 0000000000..d2098ef0e2 --- /dev/null +++ b/lib/rubygems/resolver/specification.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +## +# A Resolver::Specification contains a subset of the information +# contained in a Gem::Specification. Only the information necessary for +# dependency resolution in the resolver is included. + +class Gem::Resolver::Specification + ## + # The dependencies of the gem for this specification + + attr_reader :dependencies + + ## + # The name of the gem for this specification + + attr_reader :name + + ## + # The platform this gem works on. + + attr_reader :platform + + ## + # The set this specification came from. + + attr_reader :set + + ## + # The source for this specification + + attr_reader :source + + ## + # The Gem::Specification for this Resolver::Specification. + # + # Implementers, note that #install updates @spec, so be sure to cache the + # Gem::Specification in @spec when overriding. + + attr_reader :spec + + ## + # The version of the gem for this specification. + + attr_reader :version + + ## + # The required_ruby_version constraint for this specification. + + attr_reader :required_ruby_version + + ## + # The required_ruby_version constraint for this specification. + + attr_reader :required_rubygems_version + + ## + # Sets default instance variables for the specification. + + def initialize + @dependencies = nil + @name = nil + @platform = nil + @set = nil + @source = nil + @version = nil + @required_ruby_version = Gem::Requirement.default + @required_rubygems_version = Gem::Requirement.default + end + + ## + # Fetches development dependencies if the source does not provide them by + # default (see APISpecification). + + def fetch_development_dependencies # :nodoc: + end + + ## + # The name and version of the specification. + # + # Unlike Gem::Specification#full_name, the platform is not included. + + def full_name + "#{@name}-#{@version}" + end + + ## + # Installs this specification using the Gem::Installer +options+. The + # install method yields a Gem::Installer instance, which indicates the + # gem will be installed, or +nil+, which indicates the gem is already + # installed. + # + # After installation #spec is updated to point to the just-installed + # specification. + + def install(options = {}) + require_relative "../installer" + + gem = download options + + installer = Gem::Installer.at gem, options + + yield installer if block_given? + + @spec = installer.install + end + + def download(options) + dir = options[:install_dir] || Gem.dir + + Gem.ensure_gem_subdirectories dir + + source.download spec, dir + end + + ## + # Returns true if this specification is installable on this platform. + + def installable_platform? + Gem::Platform.match_spec? spec + end + + def local? # :nodoc: + false + 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/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb new file mode 100644 index 0000000000..293a1e3331 --- /dev/null +++ b/lib/rubygems/resolver/vendor_set.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +## +# A VendorSet represents gems that have been unpacked into a specific +# directory that contains a gemspec. +# +# This is used for gem dependency file support. +# +# Example: +# +# set = Gem::Resolver::VendorSet.new +# +# set.add_vendor_gem 'rake', 'vendor/rake' +# +# The directory vendor/rake must contain an unpacked rake gem along with a +# rake.gemspec (watching the given name). + +class Gem::Resolver::VendorSet < Gem::Resolver::Set + ## + # The specifications for this set. + + attr_reader :specs # :nodoc: + + def initialize # :nodoc: + super() + + @directories = {} + @specs = {} + end + + ## + # Adds a specification to the set with the given +name+ which has been + # unpacked into the given +directory+. + + def add_vendor_gem(name, directory) # :nodoc: + gemspec = File.join directory, "#{name}.gemspec" + + spec = Gem::Specification.load gemspec + + raise Gem::GemNotFoundException, + "unable to find #{gemspec} for gem #{name}" unless spec + + spec.full_gem_path = File.expand_path directory + + @specs[spec.name] = spec + @directories[spec] = directory + + spec + end + + ## + # Returns an Array of VendorSpecification objects matching the + # DependencyRequest +req+. + + def find_all(req) + @specs.values.select do |spec| + req.match? spec + end.map do |spec| + source = Gem::Source::Vendor.new @directories[spec] + Gem::Resolver::VendorSpecification.new self, spec, source + end + end + + ## + # Loads a spec with the given +name+. +version+, +platform+ and +source+ are + # ignored. + + def load_spec(name, version, platform, source) # :nodoc: + @specs.fetch name + end + + def pretty_print(q) # :nodoc: + q.group 2, "[VendorSet", "]" do + next if @directories.empty? + q.breakable + + dirs = @directories.map do |spec, directory| + "#{spec.full_name}: #{directory}" + end + + q.seplist dirs do |dir| + q.text dir + end + end + end +end diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb new file mode 100644 index 0000000000..ac78f54558 --- /dev/null +++ b/lib/rubygems/resolver/vendor_specification.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +## +# A VendorSpecification represents a gem that has been unpacked into a project +# and is being loaded through a gem dependencies file through the +path:+ +# option. + +class Gem::Resolver::VendorSpecification < Gem::Resolver::SpecSpecification + def ==(other) # :nodoc: + self.class === other && + @set == other.set && + @spec == other.spec && + @source == other.source + end + + ## + # This is a null install as this gem was unpacked into a directory. + # +options+ are ignored. + + def install(options = {}) + yield nil + end +end |
