diff options
author | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-16 18:49:26 +0900 |
---|---|---|
committer | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-30 00:39:06 +0900 |
commit | fcbf63e62c627deae76c1b8cb8c0876c536ed811 (patch) | |
tree | 64cb17de3f41a2b6fef2368028fbd00349946994 /jni/ruby/lib/rubygems/resolver |
Fresh start
Diffstat (limited to 'jni/ruby/lib/rubygems/resolver')
24 files changed, 2094 insertions, 0 deletions
diff --git a/jni/ruby/lib/rubygems/resolver/activation_request.rb b/jni/ruby/lib/rubygems/resolver/activation_request.rb new file mode 100644 index 0000000..56c6363 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/activation_request.rb @@ -0,0 +1,172 @@ +## +# 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. + # + # +others_possible+ indicates that other specifications may also match this + # activation request. + + def initialize spec, request, others_possible = true + @spec = spec + @request = request + @others_possible = others_possible + end + + def == other # :nodoc: + case other + when Gem::Specification + @spec == other + when Gem::Resolver::ActivationRequest + @spec == other.spec && @request == other.request + else + false + end + 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 + if @spec.respond_to? :source + source = @spec.source + else + source = Gem.sources.first + end + + Gem.ensure_gem_subdirectories path + + source.download full_spec, path + end + + ## + # The full name of the specification to be activated. + + def full_name + @spec.full_name + end + + ## + # The Gem::Specification for this activation request. + + def full_spec + Gem::Specification === @spec ? @spec : @spec.spec + end + + def inspect # :nodoc: + others = + case @others_possible + when true then # TODO remove at RubyGems 3 + ' (others possible)' + when false then # TODO remove at RubyGems 3 + nil + else + unless @others_possible.empty? then + others = @others_possible.map { |s| s.full_name } + " (others possible: #{others.join ', '})" + end + end + + '#<%s for %p from %s%s>' % [ + self.class, @spec, @request, others + ] + 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 + end + end + end + + ## + # The name of this activation request's specification + + def name + @spec.name + end + + ## + # Indicate if this activation is one of a set of possible + # requests for the same Dependency request. + + def others_possible? + case @others_possible + when true, false then + @others_possible + else + not @others_possible.empty? + end + 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 + + case @others_possible + when false then + when true then + q.breakable + q.text 'others possible' + else + unless @others_possible.empty? then + q.breakable + q.text 'others ' + q.pp @others_possible.map { |s| s.full_name } + end + end + end + end + + ## + # The version of this activation request's specification + + def version + @spec.version + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/api_set.rb b/jni/ruby/lib/rubygems/resolver/api_set.rb new file mode 100644 index 0000000..17d602f --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/api_set.rb @@ -0,0 +1,125 @@ +## +# The global rubygems pool, available via the rubygems.org API. +# Returns instances of APISpecification. + +class Gem::Resolver::APISet < Gem::Resolver::Set + + ## + # The URI for the dependency 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 RubyGems + # API URL +dep_uri+ which is described at + # http://guides.rubygems.org/rubygems-org-api + + def initialize dep_uri = 'https://rubygems.org/api/v1/dependencies' + super() + + dep_uri = URI dep_uri unless URI === dep_uri # for ruby 1.8 + + @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] + 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, [] + + uri = @dep_uri + "?gems=#{needed.sort.join ','}" + str = Gem::RemoteFetcher.fetcher.fetch_path uri + + loaded = [] + + Marshal.load(str).each do |ver| + name = ver[:name] + + @data[name] << ver + loaded << name + end + + (needed - loaded).each do |missing| + @data[missing] = [] + 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 + "?gems=#{name}" + str = Gem::RemoteFetcher.fetcher.fetch_path uri + + Marshal.load(str).each do |ver| + @data[ver[:name]] << ver + end + + @data[name] + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/api_specification.rb b/jni/ruby/lib/rubygems/resolver/api_specification.rb new file mode 100644 index 0000000..4960e66 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/api_specification.rb @@ -0,0 +1,85 @@ +## +# Represents a specification retrieved via the rubygems.org 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 + + ## + # Creates an APISpecification for the given +set+ from the rubygems.org + # +api_data+. + # + # See http://guides.rubygems.org/rubygems-org-api/#misc_methods 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] + @platform = Gem::Platform.new api_data[:platform] + @dependencies = api_data[:dependencies].map do |name, ver| + Gem::Dependency.new name, ver.split(/\s*,\s*/) + end + end + + def == other # :nodoc: + self.class === other and + @set == other.set and + @name == other.name and + @version == other.version and + @platform == other.platform and + @dependencies == other.dependencies + 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 @platform + 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 + end + end + + def source # :nodoc: + @set.source + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/best_set.rb b/jni/ruby/lib/rubygems/resolver/best_set.rb new file mode 100644 index 0000000..7e2d7e2 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/best_set.rb @@ -0,0 +1,78 @@ +## +# 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 + end + end + + def find_all req # :nodoc: + pick_sets if @remote and @sets.empty? + + super + rescue Gem::RemoteFetcher::FetchError => e + replace_failed_api_set e + + retry + end + + def prefetch reqs # :nodoc: + pick_sets if @remote and @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 + + ## + # Replaces a failed APISet for the URI in +error+ with an IndexSet. + # + # If no matching APISet can be found the original +error+ is raised. + # + # The calling method must retry the exception to repeat the lookup. + + def replace_failed_api_set error # :nodoc: + uri = error.uri + uri = URI uri unless URI === uri + uri.query = nil + + raise error unless api_set = @sets.find { |set| + Gem::Resolver::APISet === set and set.dep_uri == uri + } + + index_set = Gem::Resolver::IndexSet.new api_set.source + + @sets.map! do |set| + next set unless set == api_set + index_set + end + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/composed_set.rb b/jni/ruby/lib/rubygems/resolver/composed_set.rb new file mode 100644 index 0000000..5b08f12 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/composed_set.rb @@ -0,0 +1,66 @@ +## +# 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.map { |set| set.errors }.flatten + end + + ## + # Finds all specs matching +req+ in all sets. + + def find_all req + @sets.map do |s| + s.find_all req + end.flatten + end + + ## + # Prefetches +reqs+ in all sets. + + def prefetch reqs + @sets.each { |s| s.prefetch(reqs) } + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/conflict.rb b/jni/ruby/lib/rubygems/resolver/conflict.rb new file mode 100644 index 0000000..902c286 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/conflict.rb @@ -0,0 +1,160 @@ +## +# 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 and + @dependency == other.dependency and + @activated == other.activated and + @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 { |spec| spec.full_name } + + unless alternates.empty? then + matching = <<-MATCHING.chomp + + Gems matching %s: + %s + MATCHING + + matching = matching % [ + dependency, + alternates.join(', '), + ] + end + + explanation = <<-EXPLANATION + Activated %s + which does not match conflicting dependency (%s) + + Conflicting dependency chains: + %s + + versus: + %s +%s + EXPLANATION + + 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 then + 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}" + + 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 + +## +# TODO: Remove in RubyGems 3 + +Gem::Resolver::DependencyConflict = Gem::Resolver::Conflict # :nodoc: + diff --git a/jni/ruby/lib/rubygems/resolver/current_set.rb b/jni/ruby/lib/rubygems/resolver/current_set.rb new file mode 100644 index 0000000..4e8d340 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/current_set.rb @@ -0,0 +1,13 @@ +## +# 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/jni/ruby/lib/rubygems/resolver/dependency_request.rb b/jni/ruby/lib/rubygems/resolver/dependency_request.rb new file mode 100644 index 0000000..79690be --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/dependency_request.rb @@ -0,0 +1,116 @@ +## +# 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 && @requester == other.requester + 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 + + ## + # 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/jni/ruby/lib/rubygems/resolver/git_set.rb b/jni/ruby/lib/rubygems/resolver/git_set.rb new file mode 100644 index 0000000..5f1b368 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/git_set.rb @@ -0,0 +1,122 @@ +## +# 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() + + @git = ENV['git'] || 'git' + @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/jni/ruby/lib/rubygems/resolver/git_specification.rb b/jni/ruby/lib/rubygems/resolver/git_specification.rb new file mode 100644 index 0000000..55e180e --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/git_specification.rb @@ -0,0 +1,59 @@ +## +# 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 and + @set == other.set and + @spec == other.spec and + @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 'rubygems/installer' + + installer = Gem::Installer.new '', options + installer.spec = spec + + 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/jni/ruby/lib/rubygems/resolver/index_set.rb b/jni/ruby/lib/rubygems/resolver/index_set.rb new file mode 100644 index 0000000..7c56c2b --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/index_set.rb @@ -0,0 +1,80 @@ +## +# 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 then + 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| + if req.match? n, @prerelease then + res << Gem::Resolver::IndexSpecification.new( + self, n.name, n.version, uri, n.platform) + end + 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.map do |tuples| + tuples.map do |_, tuple| + tuple.full_name + end + end.flatten + + q.seplist names do |name| + q.text name + end + end + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/index_specification.rb b/jni/ruby/lib/rubygems/resolver/index_specification.rb new file mode 100644 index 0000000..56fecb5 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/index_specification.rb @@ -0,0 +1,69 @@ +## +# 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 = platform.to_s + + @spec = nil + end + + ## + # The dependencies of the gem for this specification + + def dependencies + spec.dependencies + end + + def inspect # :nodoc: + '#<%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 Gem::Platform::RUBY == @platform then + 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, @platform + + @source.fetch_spec tuple + end + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/installed_specification.rb b/jni/ruby/lib/rubygems/resolver/installed_specification.rb new file mode 100644 index 0000000..2a2b89a --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/installed_specification.rb @@ -0,0 +1,58 @@ +## +# An InstalledSpecification represents a gem that is already installed +# locally. + +class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification + + def == other # :nodoc: + self.class === other and + @set == other.set and + @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.kind_of? 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/jni/ruby/lib/rubygems/resolver/installer_set.rb b/jni/ruby/lib/rubygems/resolver/installer_set.rb new file mode 100644 index 0000000..a68ff09 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/installer_set.rb @@ -0,0 +1,224 @@ +## +# 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: + + ## + # Creates a new InstallerSet that will look for gems in +domain+. + + def initialize domain + super() + + @domain = domain + @remote = consider_remote? + + @f = Gem::SpecFetcher.fetcher + + @always_install = [] + @ignore_dependencies = false + @ignore_installed = false + @local = {} + @remote_set = Gem::Resolver::BestSet.new + @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 { |s| + s.version.prerelease? and not s.local? + } unless dependency.prerelease? + + found = found.select do |s| + Gem::Source::SpecificFile === s.source or + Gem::Platform::RUBY == s.platform or + Gem::Platform.local === s.platform + end + + if found.empty? then + exc = Gem::UnsatisfiableDependencyError.new request + exc.errors = errors + + raise exc + end + + newest = found.max_by do |s| + [s.version, s.platform == Gem::Platform::RUBY ? -1 : 1] + 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 or @domain == :local + end + + ## + # Should remote gems should be considered? + + def consider_remote? # :nodoc: + @domain == :both or @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 and + @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 + + if consider_local? then + 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 + + local_source = Gem::Source::Local.new + + if local_spec = local_source.find_gem(name, dep.requirement) then + res << Gem::Resolver::IndexSpecification.new( + self, local_spec.name, local_spec.version, + local_source, local_spec.platform) + end + end + + res.delete_if do |spec| + spec.version.prerelease? and not dep.prerelease? + end + + res.concat @remote_set.find_all req if consider_remote? + + 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 { |s| s.full_name } + + '#<%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 + +end + diff --git a/jni/ruby/lib/rubygems/resolver/local_specification.rb b/jni/ruby/lib/rubygems/resolver/local_specification.rb new file mode 100644 index 0000000..20a283f --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/local_specification.rb @@ -0,0 +1,41 @@ +## +# 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.kind_of? 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/jni/ruby/lib/rubygems/resolver/lock_set.rb b/jni/ruby/lib/rubygems/resolver/lock_set.rb new file mode 100644 index 0000000..4ede597 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/lock_set.rb @@ -0,0 +1,84 @@ +## +# 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 = @sources.map do |source| + Gem::Resolver::LockSpecification.new self, name, version, source, + platform + end + + @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 and 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 { |spec| spec.full_name } + end + end + +end + diff --git a/jni/ruby/lib/rubygems/resolver/lock_specification.rb b/jni/ruby/lib/rubygems/resolver/lock_specification.rb new file mode 100644 index 0000000..0013171 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/lock_specification.rb @@ -0,0 +1,84 @@ +## +# 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 + + def initialize set, name, version, source, platform + super() + + @name = name + @platform = platform + @set = set + @source = source + @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) then + 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 then + q.breakable + q.text "platform: #{@platform}" + end + + unless @dependencies.empty? then + 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 { |spec| + spec.name == @name and spec.version == @version + } + + @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/jni/ruby/lib/rubygems/resolver/requirement_list.rb b/jni/ruby/lib/rubygems/resolver/requirement_list.rb new file mode 100644 index 0000000..a6bfaab --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/requirement_list.rb @@ -0,0 +1,81 @@ +## +# 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/jni/ruby/lib/rubygems/resolver/set.rb b/jni/ruby/lib/rubygems/resolver/set.rb new file mode 100644 index 0000000..b26dc45 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/set.rb @@ -0,0 +1,56 @@ +## +# 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/jni/ruby/lib/rubygems/resolver/spec_specification.rb b/jni/ruby/lib/rubygems/resolver/spec_specification.rb new file mode 100644 index 0000000..1350e8a --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/spec_specification.rb @@ -0,0 +1,56 @@ +## +# 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 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 + +end + diff --git a/jni/ruby/lib/rubygems/resolver/specification.rb b/jni/ruby/lib/rubygems/resolver/specification.rb new file mode 100644 index 0000000..4d77293 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/specification.rb @@ -0,0 +1,110 @@ +## +# 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 + + ## + # Sets default instance variables for the specification. + + def initialize + @dependencies = nil + @name = nil + @platform = nil + @set = nil + @source = nil + @version = nil + 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 'rubygems/installer' + + destination = options[:install_dir] || Gem.dir + + Gem.ensure_gem_subdirectories destination + + gem = source.download spec, destination + + installer = Gem::Installer.new gem, options + + yield installer if block_given? + + @spec = installer.install + end + + ## + # Returns true if this specification is installable on this platform. + + def installable_platform? + Gem::Platform.match spec.platform + end + + def local? # :nodoc: + false + end +end + diff --git a/jni/ruby/lib/rubygems/resolver/stats.rb b/jni/ruby/lib/rubygems/resolver/stats.rb new file mode 100644 index 0000000..c31e5be --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/stats.rb @@ -0,0 +1,44 @@ +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/jni/ruby/lib/rubygems/resolver/vendor_set.rb b/jni/ruby/lib/rubygems/resolver/vendor_set.rb new file mode 100644 index 0000000..614bd05 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/vendor_set.rb @@ -0,0 +1,87 @@ +## +# 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/jni/ruby/lib/rubygems/resolver/vendor_specification.rb b/jni/ruby/lib/rubygems/resolver/vendor_specification.rb new file mode 100644 index 0000000..a99b5f3 --- /dev/null +++ b/jni/ruby/lib/rubygems/resolver/vendor_specification.rb @@ -0,0 +1,24 @@ +## +# 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 and + @set == other.set and + @spec == other.spec and + @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 + |