Skip to content

Commit

Permalink
Introduce Gem::AvailableSet
Browse files Browse the repository at this point in the history
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 432fc48
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 49 deletions.
95 changes: 95 additions & 0 deletions lib/rubygems/available_set.rb
@@ -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
53 changes: 22 additions & 31 deletions lib/rubygems/dependency_installer.rb
Expand Up @@ -6,6 +6,7 @@
require 'rubygems/user_interaction' require 'rubygems/user_interaction'
require 'rubygems/source_local' require 'rubygems/source_local'
require 'rubygems/source_specific_file' require 'rubygems/source_specific_file'
require 'rubygems/available_set'


## ##
# Installs a gem along with all its dependencies from local and remote gems. # Installs a gem along with all its dependencies from local and remote gems.
Expand Down Expand Up @@ -120,14 +121,14 @@ def consider_remote?
# local gems preferred over remote gems. # local gems preferred over remote gems.


def find_gems_with_sources(dep) def find_gems_with_sources(dep)
gems_and_sources = [] set = Gem::AvailableSet.new


if consider_local? if consider_local?
sl = Gem::Source::Local.new sl = Gem::Source::Local.new


if spec = sl.find_gem(dep.name) if spec = sl.find_gem(dep.name)
if dep.matches_spec? spec if dep.matches_spec? spec
gems_and_sources << [spec, sl] set.add spec, sl
end end
end end
end end
Expand All @@ -142,7 +143,7 @@ def find_gems_with_sources(dep)
@errors = errors @errors = errors
end end


gems_and_sources.push(*found) set << found


rescue Gem::RemoteFetcher::FetchError => e rescue Gem::RemoteFetcher::FetchError => e
# FIX if there is a problem talking to the network, we either need to always tell # FIX if there is a problem talking to the network, we either need to always tell
Expand All @@ -156,17 +157,15 @@ def find_gems_with_sources(dep)
end end
end end


gems_and_sources.sort_by do |gem, source| set
[gem, source] # local gems win
end
end end


## ##
# Gathers all dependencies necessary for the installation from local and # Gathers all dependencies necessary for the installation from local and
# remote sources unless the ignore_dependencies was given. # remote sources unless the ignore_dependencies was given.


def gather_dependencies def gather_dependencies
specs = @specs_and_sources.map { |spec,_| spec } specs = @available.all_specs


# these gems were listed by the user, always install them # these gems were listed by the user, always install them
keep_names = specs.map { |spec| spec.full_name } keep_names = specs.map { |spec| spec.full_name }
Expand Down Expand Up @@ -232,21 +231,14 @@ def add_found_dependencies to_do, dependency_list


results = find_gems_with_sources(dep) results = find_gems_with_sources(dep)


results.reject! do |dep_spec,| results.sorted.each do |t|
to_do.push dep_spec to_do.push t.spec

# already locally installed
Gem::Specification.any? do |installed_spec|
dep.name == installed_spec.name and
dep.requirement.satisfied_by? installed_spec.version
end
end end


results.each do |dep_spec, source_uri| results.remove_installed! dep
@specs_and_sources << [dep_spec, source_uri]


dependency_list.add dep_spec @available << results
end results.inject_into_list dependency_list
end end
end end


Expand All @@ -262,39 +254,37 @@ def find_spec_by_name_and_version(gem_name,
version = Gem::Requirement.default, version = Gem::Requirement.default,
prerelease = false) prerelease = false)


spec_and_source = nil set = Gem::AvailableSet.new


if consider_local? if consider_local?
if File.exists? gem_name if File.exists? gem_name
src = Gem::Source::SpecificFile.new(gem_name) src = Gem::Source::SpecificFile.new(gem_name)
spec_and_source = [src.spec, src] set.add src.spec, src
else else
local = Gem::Source::Local.new local = Gem::Source::Local.new


if s = local.find_gem(gem_name, version) if s = local.find_gem(gem_name, version)
spec_and_source = [s, local] set.add s, local
end end
end end
end end


unless spec_and_source then if set.empty?
dep = Gem::Dependency.new gem_name, version dep = Gem::Dependency.new gem_name, version
# HACK Dependency objects should be immutable # HACK Dependency objects should be immutable
dep.prerelease = true if prerelease dep.prerelease = true if prerelease


spec_and_sources = find_gems_with_sources(dep) set = find_gems_with_sources(dep)
spec_and_source = spec_and_sources.find { |spec, source| set.match_platform!
Gem::Platform.match spec.platform
}
end end


if spec_and_source.nil? then if set.empty?
raise Gem::GemNotFoundException.new( raise Gem::GemNotFoundException.new(
"Could not find a valid gem '#{gem_name}' (#{version}) locally or in a repository", "Could not find a valid gem '#{gem_name}' (#{version}) locally or in a repository",
gem_name, version, @errors) gem_name, version, @errors)
end end


@specs_and_sources = [spec_and_source] @available = set
end end


## ##
Expand All @@ -317,7 +307,7 @@ def install dep_or_name, version = Gem::Requirement.default
else else
dep = dep_or_name.dup dep = dep_or_name.dup
dep.prerelease = @prerelease dep.prerelease = @prerelease
@specs_and_sources = [find_gems_with_sources(dep).first] @available = find_gems_with_sources(dep).pick_best!
end end


@installed_gems = [] @installed_gems = []
Expand All @@ -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 # TODO: make this sorta_verbose so other users can benefit from it
say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose


_, source = @specs_and_sources.assoc spec source = @available.source_for spec

begin begin
# REFACTOR make the fetcher to use configurable # REFACTOR make the fetcher to use configurable
local_gem_path = source.download spec, @cache_dir local_gem_path = source.download spec, @cache_dir
Expand Down
106 changes: 106 additions & 0 deletions test/rubygems/test_gem_available_set.rb
@@ -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

0 comments on commit 432fc48

Please sign in to comment.