Permalink
Browse files

Introduce Gem::AvailableSet

AvailableSet abstracts tracking and picking which gems to install from
which sources.

Previously, the [[spec, source], ...] array had an implicit order that
was required in order for RubyGems to function properly. This is now
properly documented and maintained by AvailableSet.
  • Loading branch information...
evanphx committed Oct 9, 2012
1 parent a099e44 commit 432fc4818bbf14dfa5e49bbc1950eaa6d3fde133
@@ -0,0 +1,95 @@
+module Gem
+ class AvailableSet
+ Tuple = Struct.new(:spec, :source)
+
+ def initialize
+ @set = []
+ @sorted = nil
+ end
+
+ attr_reader :set
+
+ def add(spec, source)
+ @set << Tuple.new(spec, source)
+ @sorted = nil
+ self
+ end
+
+ def <<(o)
+ case o
+ when AvailableSet
+ s = o.set
+ when Array
+ s = o.map do |sp,so|
+ if !sp.kind_of?(Specification) or !so.kind_of?(Source)
+ raise TypeError, "Array must be in [[spec, source], ...] form"
+ end
+
+ Tuple.new(sp,so)
+ end
+ else
+ raise TypeError, "Must be an AvailableSet"
+ end
+
+ @set += s
+ @sorted = nil
+
+ self
+ end
+
+ def empty?
+ @set.empty?
+ end
+
+ def all_specs
+ @set.map { |t| t.spec }
+ end
+
+ def match_platform!
+ @set.reject! { |t| !Gem::Platform.match(t.spec.platform) }
+ @sorted = nil
+ self
+ end
+
+ def sorted
+ @sorted ||= @set.sort do |a,b|
+ i = b.spec <=> a.spec
+ i != 0 ? i : (a.source <=> b.source)
+ end
+ end
+
+ def size
+ @set.size
+ end
+
+ def source_for(spec)
+ f = @set.find { |t| t.spec == spec }
+ f.source
+ end
+
+ def pick_best!
+ return self if empty?
+
+ @set = [sorted.first]
+ @sorted = nil
+ self
+ end
+
+ def remove_installed!(dep)
+ @set.reject! do |t|
+ # already locally installed
+ Gem::Specification.any? do |installed_spec|
+ dep.name == installed_spec.name and
+ dep.requirement.satisfied_by? installed_spec.version
+ end
+ end
+
+ @sorted = nil
+ self
+ end
+
+ def inject_into_list(dep_list)
+ @set.each { |t| dep_list.add t.spec }
+ end
+ end
+end
@@ -6,6 +6,7 @@
require 'rubygems/user_interaction'
require 'rubygems/source_local'
require 'rubygems/source_specific_file'
+require 'rubygems/available_set'
##
# Installs a gem along with all its dependencies from local and remote gems.
@@ -120,14 +121,14 @@ def consider_remote?
# local gems preferred over remote gems.
def find_gems_with_sources(dep)
- gems_and_sources = []
+ set = Gem::AvailableSet.new
if consider_local?
sl = Gem::Source::Local.new
if spec = sl.find_gem(dep.name)
if dep.matches_spec? spec
- gems_and_sources << [spec, sl]
+ set.add spec, sl
end
end
end
@@ -142,7 +143,7 @@ def find_gems_with_sources(dep)
@errors = errors
end
- gems_and_sources.push(*found)
+ set << found
rescue Gem::RemoteFetcher::FetchError => e
# FIX if there is a problem talking to the network, we either need to always tell
@@ -156,17 +157,15 @@ def find_gems_with_sources(dep)
end
end
- gems_and_sources.sort_by do |gem, source|
- [gem, source] # local gems win
- end
+ set
end
##
# Gathers all dependencies necessary for the installation from local and
# remote sources unless the ignore_dependencies was given.
def gather_dependencies
- specs = @specs_and_sources.map { |spec,_| spec }
+ specs = @available.all_specs
# these gems were listed by the user, always install them
keep_names = specs.map { |spec| spec.full_name }
@@ -232,21 +231,14 @@ def add_found_dependencies to_do, dependency_list
results = find_gems_with_sources(dep)
- results.reject! do |dep_spec,|
- to_do.push dep_spec
-
- # already locally installed
- Gem::Specification.any? do |installed_spec|
- dep.name == installed_spec.name and
- dep.requirement.satisfied_by? installed_spec.version
- end
+ results.sorted.each do |t|
+ to_do.push t.spec
end
- results.each do |dep_spec, source_uri|
- @specs_and_sources << [dep_spec, source_uri]
+ results.remove_installed! dep
- dependency_list.add dep_spec
- end
+ @available << results
+ results.inject_into_list dependency_list
end
end
@@ -262,39 +254,37 @@ def find_spec_by_name_and_version(gem_name,
version = Gem::Requirement.default,
prerelease = false)
- spec_and_source = nil
+ set = Gem::AvailableSet.new
if consider_local?
if File.exists? gem_name
src = Gem::Source::SpecificFile.new(gem_name)
- spec_and_source = [src.spec, src]
+ set.add src.spec, src
else
local = Gem::Source::Local.new
if s = local.find_gem(gem_name, version)
- spec_and_source = [s, local]
+ set.add s, local
end
end
end
- unless spec_and_source then
+ if set.empty?
dep = Gem::Dependency.new gem_name, version
# HACK Dependency objects should be immutable
dep.prerelease = true if prerelease
- spec_and_sources = find_gems_with_sources(dep)
- spec_and_source = spec_and_sources.find { |spec, source|
- Gem::Platform.match spec.platform
- }
+ set = find_gems_with_sources(dep)
+ set.match_platform!
end
- if spec_and_source.nil? then
+ if set.empty?
raise Gem::GemNotFoundException.new(
"Could not find a valid gem '#{gem_name}' (#{version}) locally or in a repository",
gem_name, version, @errors)
end
- @specs_and_sources = [spec_and_source]
+ @available = set
end
##
@@ -317,7 +307,7 @@ def install dep_or_name, version = Gem::Requirement.default
else
dep = dep_or_name.dup
dep.prerelease = @prerelease
- @specs_and_sources = [find_gems_with_sources(dep).first]
+ @available = find_gems_with_sources(dep).pick_best!
end
@installed_gems = []
@@ -335,7 +325,8 @@ def install dep_or_name, version = Gem::Requirement.default
# TODO: make this sorta_verbose so other users can benefit from it
say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose
- _, source = @specs_and_sources.assoc spec
+ source = @available.source_for spec
+
begin
# REFACTOR make the fetcher to use configurable
local_gem_path = source.download spec, @cache_dir
@@ -0,0 +1,106 @@
+require 'rubygems/test_case'
+require 'rubygems/available_set'
+require 'rubygems/security'
+
+class TestGemAvailableSet < Gem::TestCase
+ def setup
+ super
+
+ @source = Gem::Source.new(@gem_repo)
+ end
+
+ def test_add_and_empty
+ a1, _ = util_gem 'a', '1'
+
+ set = Gem::AvailableSet.new
+ assert set.empty?
+
+ set.add a1, @source
+
+ refute set.empty?
+
+ assert_equal [a1], set.all_specs
+ end
+
+ def test_match_platform
+ a1, _ = util_gem 'a', '1' do |g|
+ g.platform = "something-weird-yep"
+ end
+
+ a1c, _ = util_gem 'a', '2' do |g|
+ g.platform = Gem::Platform.local
+ end
+
+ a2, _ = util_gem 'a', '2'
+
+ set = Gem::AvailableSet.new
+ set.add a1, @source
+ set.add a1c, @source
+ set.add a2, @source
+
+ set.match_platform!
+
+ assert_equal [a1c, a2], set.all_specs
+ end
+
+ def test_best
+ a1, _ = util_gem 'a', '1'
+ a2, _ = util_gem 'a', '2'
+
+ set = Gem::AvailableSet.new
+ set.add a1, @source
+ set.add a2, @source
+
+ set.pick_best!
+
+ assert_equal [a2], set.all_specs
+ end
+
+ def test_remove_installed_bang
+ a1, _ = util_gem 'a', '1'
+
+ a1.activate
+
+ set = Gem::AvailableSet.new
+ set.add a1, @source
+
+ dep = Gem::Dependency.new "a", ">= 0"
+
+ set.remove_installed! dep
+
+ assert set.empty?
+ end
+
+ def test_sorted_normal_versions
+ a1, _ = util_gem 'a', '1'
+ a2, _ = util_gem 'a', '2'
+
+ set = Gem::AvailableSet.new
+ set.add a1, @source
+ set.add a2, @source
+
+ g = set.sorted
+
+ assert_equal a2, g[0].spec
+ assert_equal a1, g[1].spec
+ end
+
+ def test_sorted_respect_pre
+ a1a, _ = util_gem 'a', '1.a'
+ a1, _ = util_gem 'a', '1'
+ a2a, _ = util_gem 'a', '2.a'
+ a2, _ = util_gem 'a', '2'
+ a3a, _ = util_gem 'a', '3.a'
+
+ set = Gem::AvailableSet.new
+ set.add a1, @source
+ set.add a1a, @source
+ set.add a3a, @source
+ set.add a2a, @source
+ set.add a2, @source
+
+ g = set.sorted.map { |t| t.spec }
+
+ assert_equal [a3a, a2, a2a, a1, a1a], g
+ end
+end
Oops, something went wrong.

0 comments on commit 432fc48

Please sign in to comment.