From 3edfddbf7ebe4b942e1e2b6d693afd582e9a8147 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Thu, 3 Jun 2010 15:32:32 -0700 Subject: Hacked together some crap that doesn't really work --- lib/bundler.rb | 12 ++- lib/bundler/index.rb | 15 +++- lib/bundler/resolver.rb | 171 ++++++++++++++++++++++++++++++++++------- spec/resolver/basic_spec.rb | 11 ++- spec/resolver/platform_spec.rb | 43 +++++++++++ spec/support/builders.rb | 22 +++++- spec/support/indexes.rb | 43 +++++++++-- 7 files changed, 278 insertions(+), 39 deletions(-) create mode 100644 spec/resolver/platform_spec.rb diff --git a/lib/bundler.rb b/lib/bundler.rb index 14784dee..1faf2b6f 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -39,7 +39,6 @@ module Bundler class GemfileNotFound < BundlerError; status_code(10) ; end class GemNotFound < BundlerError; status_code(7) ; end - class VersionConflict < BundlerError; status_code(6) ; end class GemfileError < BundlerError; status_code(4) ; end class GemfileChanged < GemfileError; status_code(4) ; end class PathError < BundlerError; status_code(13) ; end @@ -50,6 +49,17 @@ module Bundler class GemspecError < BundlerError; status_code(14) ; end class InvalidOption < BundlerError; status_code(15) ; end + class VersionConflict < BundlerError + attr_reader :conflicts + + def initialize(conflicts, msg = nil) + super(msg) + @conflicts = conflicts + end + + status_code(6) + end + # Internal errors, should be rescued class InvalidSpecSet < StandardError; end diff --git a/lib/bundler/index.rb b/lib/bundler/index.rb index 95c6d70e..8b7975cc 100644 --- a/lib/bundler/index.rb +++ b/lib/bundler/index.rb @@ -31,6 +31,20 @@ module Bundler end end + def search_for_all_platforms(dependency) + specs = @specs[dependency.name] + + wants_prerelease = dependency.requirement.prerelease? + only_prerelease = specs.all? {|spec| spec.version.prerelease? } + found = specs.select { |spec| dependency =~ spec } + + unless wants_prerelease || only_prerelease + found.reject! { |spec| spec.version.prerelease? } + end + + found.sort_by {|s| [s.version, s.platform.to_s == 'ruby' ? "\0" : s.platform.to_s] } + end + def sources @specs.values.map do |specs| specs.map{|s| s.source.class } @@ -88,6 +102,5 @@ module Bundler found.sort_by {|s| [s.version, s.platform.to_s == 'ruby' ? "\0" : s.platform.to_s] } end end - end end \ No newline at end of file diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 9cbb0167..ab523ef8 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -7,20 +7,108 @@ require 'set' # Extending Gem classes to add necessary tracking information module Gem - class Dependency + class Platform + def hash + Platform.hash + end + alias eql? == + end + class Specification def required_by @required_by ||= [] end + def match_platform(p) + platform.nil? or p == platform or (p != Gem::Platform::RUBY and p =~ platform) + end end - class Specification + class Dependency def required_by @required_by ||= [] end + alias eql? == end end module Bundler class Resolver + RUBY = Gem::Platform.new('ruby') + JAVA = Gem::Platform.new('java') + MSWIN = Gem::Platform.new('mswin32') + MING = Gem::Platform.new('mingw32') + + class DepProxy + + undef to_s + undef type + + attr_reader :required_by, :__platform, :dep + + def initialize(dep, platform) + @dep, @__platform, @required_by = dep, platform, [] + end + + private + + def method_missing(*args) + @dep.send(*args) + end + + end + + class SpecGroup < Array + attr_reader :activated, :required_by + + def initialize(a) + super + @required_by = [] + @activated = [] + @dependencies = {} + + [RUBY, JAVA, MSWIN, MING].each do |p| + deps = [] + spec = find { |s| s.match_platform(p) } + deps = spec.dependencies.select { |d| d.type != :development } if spec + @dependencies[p] = deps + end + end + + def initialize_copy(o) + super + @required_by = o.required_by.dup + @activated = o.activated.dup + end + + def to_specs + @activated.map do |p| + find { |s| s.match_platform(p) } + end.compact + end + + def activate_platform(req, platforms) + platforms -= @activated + deps = dependencies_for(platforms) - dependencies_for(@activated) + @activated.concat platforms + deps.map { |d| DepProxy.new(d, req.__platform) } + end + + def name + @name ||= first.name + end + + def version + @version ||= first.version + end + + private + + def dependencies_for(platforms) + deps = [] + platforms.each do |p| + deps |= @dependencies[p] + end + deps + end + end attr_reader :errors @@ -34,22 +122,25 @@ module Bundler # ==== Returns # ,nil:: If the list of dependencies can be resolved, a # collection of gemspecs is returned. Otherwise, nil is returned. - def self.resolve(requirements, index, source_requirements = {}, base = []) - resolver = new(index, source_requirements) + def self.resolve(requirements, index, source_requirements = {}, base = [], platforms = []) + resolver = new(index, source_requirements, platforms.any? ? platforms : [RUBY]) result = catch(:success) do activated = {} - base.each { |s| activated[s.name] = s } - resolver.resolve(requirements, activated) - raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{resolver.error_message}" + # base.each { |s| activated[s.name] = s } + requirements = requirements.dup + base.each { |s| requirements << Gem::Dependency.new(s.name, s.version) } + resolver.resolve(requirements.map { |d| DepProxy.new(d, nil) }, activated) + raise resolver.version_conflict nil end - SpecSet.new(result.values) + SpecSet.new(result) end - def initialize(index, source_requirements) + def initialize(index, source_requirements, platforms) @errors = {} @stack = [] @index = index + @platforms = platforms @source_requirements = source_requirements end @@ -61,10 +152,14 @@ module Bundler end end + def successify(activated) + activated.values.map { |s| s.to_specs }.flatten.compact + end + def resolve(reqs, activated) # If the requirements are empty, then we are in a success state. Aka, all # gem dependencies have been resolved. - throw :success, activated if reqs.empty? + throw :success, successify(activated) if reqs.empty? debug { print "\e[2J\e[f" ; "==== Iterating ====\n\n" } @@ -85,14 +180,8 @@ module Bundler activated = activated.dup - if reqs.first.name == "bundler" && !activated["bundler"] - # activate the current version of bundler before other versions - bundler_version = ENV["BUNDLER_VERSION"] || Bundler::VERSION - current = Gem::Dependency.new("bundler", bundler_version, reqs.first.type) - else - # Pull off the first requirement so that we can resolve it - current = reqs.shift - end + # Pull off the first requirement so that we can resolve it + current = reqs.shift debug { "Attempting:\n #{current.name} (#{current.requirement})"} @@ -104,6 +193,12 @@ module Bundler @errors.delete(existing.name) # Since the current requirement is satisfied, we can continue resolving # the remaining requirements. + + # I have no idea if this is the right way to do it, but let's see if it works + # The current requirement might activate some other platforms, so let's try + # adding those requirements here. + reqs.concat existing.activate_platform(current, Array(current.__platform || @platforms)) + resolve(reqs, activated) else debug { " * [FAIL] Already activated" } @@ -127,7 +222,7 @@ module Bundler else # The original set of dependencies conflict with the base set of specs # passed to the resolver. This is by definition an impossible resolve. - raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{error_message}" + raise version_conflict end end else @@ -168,8 +263,8 @@ module Bundler end end - matching_versions.reverse_each do |spec| - conflict = resolve_requirement(spec, current, reqs.dup, activated.dup) + matching_versions.reverse_each do |spec_group| + conflict = resolve_requirement(spec_group, current, reqs.dup, activated.dup) conflicts << conflict if conflict end # If the current requirement is a root level gem and we have conflicts, we @@ -189,20 +284,22 @@ module Bundler end end - def resolve_requirement(spec, requirement, reqs, activated) + def resolve_requirement(spec_group, requirement, reqs, activated) # We are going to try activating the spec. We need to keep track of stack of # requirements that got us to the point of activating this gem. - spec.required_by.replace requirement.required_by - spec.required_by << requirement + spec_group.required_by.replace requirement.required_by + spec_group.required_by << requirement - activated[spec.name] = spec + activated[spec_group.name] = spec_group debug { " Activating: #{spec.name} (#{spec.version})" } debug { spec.required_by.map { |d| " * #{d.name} (#{d.requirement})" }.join("\n") } + dependencies = spec_group.activate_platform(requirement, Array(requirement.__platform || @platforms)) + # Now, we have to loop through all child dependencies and add them to our # array of requirements. debug { " Dependencies"} - spec.dependencies.each do |dep| + dependencies.each do |dep| next if dep.type == :development debug { " * #{dep.name} (#{dep.requirement})" } dep.required_by.replace(requirement.required_by) @@ -227,7 +324,27 @@ module Bundler def search(dep) index = @source_requirements[dep.name] || @index - index.search(dep) + results = index.search_for_all_platforms(dep.dep) + if results.any? + version = results.first.version + nested = [[]] + results.each do |spec| + if spec.version != version + nested << [] + version = spec.version + end + nested.last << spec + end + nested.map { |a| SpecGroup.new(a) } + else + [] + end + end + + def version_conflict + VersionConflict.new( + errors.keys, + "No compatible versions could be found for required dependencies:\n #{error_message}") end def error_message diff --git a/spec/resolver/basic_spec.rb b/spec/resolver/basic_spec.rb index a202db66..177a7b0c 100644 --- a/spec/resolver/basic_spec.rb +++ b/spec/resolver/basic_spec.rb @@ -3,13 +3,18 @@ require "spec_helper" describe "Resolving" do before :each do - @deps = [] @index = an_awesome_index end - it "resolves" do + it "resolves a single gem" do dep "rack" - should_resolve_as [gem("rack", "1.1")] + should_resolve_as %w(rack-1.1) + end + + it "resolves a gem with dependencies" do + dep "actionpack" + + should_resolve_as %w(actionpack-2.3.5 activesupport-2.3.5 rack-1.0) end end \ No newline at end of file diff --git a/spec/resolver/platform_spec.rb b/spec/resolver/platform_spec.rb new file mode 100644 index 00000000..bc7bce45 --- /dev/null +++ b/spec/resolver/platform_spec.rb @@ -0,0 +1,43 @@ +require "spec_helper" + +describe "Resolving platform craziness" do + describe "with semi real cases" do + before :each do + @index = an_awesome_index + end + + it "resolves a simple multi platform gem" do + dep "nokogiri" + platforms "ruby", "java" + + should_resolve_as %w(nokogiri-1.4.2.1 nokogiri-1.4.2.1-java weakling-0.0.3) + end + end + + describe "with conflicting cases" do + before :each do + @index = build_index do + gem "foo", "1.0.0" do + dep "bar", ">= 0" + end + + gem 'bar', "1.0.0" do + dep "baz", "~> 1.0.0" + end + + gem "bar", "1.0.0", "java" do + dep "baz", " ~> 1.1.0" + end + + gem "baz", %w(1.0.0 1.1.0 1.2.0) + end + end + + it "does something" do + platforms "ruby", "java" + dep "foo" + + should_conflict_on "baz" + end + end +end \ No newline at end of file diff --git a/spec/support/builders.rb b/spec/support/builders.rb index e5072fa5..9459b299 100644 --- a/spec/support/builders.rb +++ b/spec/support/builders.rb @@ -8,6 +8,10 @@ module Spec Gem::Version.new(version) end + def pl(platform) + Gem::Platform.new(pl) + end + def build_repo1 build_repo gem_repo1 do build_gem "rack", %w(0.9.1 1.0.0) do |s| @@ -239,8 +243,9 @@ module Spec def build_spec(name, version, platform = nil, &block) Array(version).map do |v| Gem::Specification.new do |s| - s.name = name - s.version = Gem::Version.new(v) + s.name = name + s.version = Gem::Version.new(v) + s.platform = platform DepBuilder.run(s, &block) if block_given? end end @@ -308,6 +313,19 @@ module Spec end end + def platforms(platforms) + platforms.split(/\s+/).each do |platform| + platform = 'x86-mswin32' if platform == 'mswin32' + platform = Gem::Platform.new(platform) + if String === platform + class << platform + alias =~ == + end + end + yield Gem::Platform.new(platform) + end + end + def versions(versions) versions.split(/\s+/).each { |version| yield v(version) } end diff --git a/spec/support/indexes.rb b/spec/support/indexes.rb index da168ada..7afa88ff 100644 --- a/spec/support/indexes.rb +++ b/spec/support/indexes.rb @@ -1,14 +1,35 @@ module Spec module Indexes def dep(name, reqs = nil) + @deps ||= [] @deps << Bundler::Dependency.new(name, :version => reqs) end + def platform(*args) + @platforms ||= [] + @platforms.concat args.map { |p| Gem::Platform.new(p) } + end + + alias platforms platform + + def resolve + Bundler::Resolver.resolve(@deps, @index, {}, [], @platforms || ['ruby']) + end + def should_resolve_as(specs) - got = Bundler::Resolver.resolve(@deps, @index) - got = got.map { |s| s.full_name } + got = resolve + got = got.map { |s| s.full_name }.sort + + got.should == specs.sort + end - got.should == specs.flatten.map { |s| s.full_name } + def should_conflict_on(names) + begin + got = resolve + flunk "The resolve succeeded with: #{got.map { |s| s.full_name }.sort.inspect}" + rescue Bundler::VersionConflict => e + names.sort.should == e.conflicts.sort + end end def gem(*args, &blk) @@ -28,8 +49,8 @@ module Spec if version >= v('3.0.0.beta') dep "rack", '~> 1.1' dep "rack-mount", ">= 0.5" - elsif version > v('2.3.5') then dep "rack", '~> 1.0' - elsif version > v('2.0.0') then dep "rack", '~> 0.9' + elsif version > v('2.3') then dep "rack", '~> 1.0.0' + elsif version > v('2.0.0') then dep "rack", '~> 0.9.0' end end gem "activerecord", version do @@ -59,6 +80,18 @@ module Spec end end + versions '1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1' do |version| + platforms "ruby java mswin32" do |platform| + gem "nokogiri", version, platform do + dep "weakling", ">= 0.0.3" if platform =~ 'java' + end + end + end + + versions '0.0.1 0.0.2 0.0.3' do |version| + gem "weakling", version #, pl('java') + end + # --- Rails related versions '1.2.3 2.2.3 2.3.5' do |version| gem "activemerchant", version do -- cgit v1.2.3