RFC: A new approach to working with gems under development #1641

Closed
wants to merge 11 commits into
from
View
@@ -29,11 +29,12 @@ def self.build(gemfile, lockfile, unlock)
specs, then we can try to resolve locally.
=end
- def initialize(lockfile, dependencies, sources, unlock)
+ def initialize(lockfile, dependencies, sources, unlock, local_overrides = nil)
@dependencies, @sources, @unlock = dependencies, sources, unlock
@remote = false
@specs = nil
@lockfile_contents = ""
+ @local_overrides = local_overrides
if lockfile && File.exists?(lockfile)
@lockfile_contents = Bundler.read_file(lockfile)
@@ -87,7 +88,8 @@ def resolve_remotely!
def specs
@specs ||= begin
- specs = resolve.materialize(requested_dependencies)
+ resolved = resolve_with_local_override
+ specs = resolved.materialize(requested_dependencies)
unless specs["bundler"].any?
local = Bundler.settings[:frozen] ? rubygems_index : index
@@ -135,28 +137,57 @@ def specs_for(groups)
specs.for(expand_dependencies(deps))
end
- def resolve
- @resolve ||= begin
- if Bundler.settings[:frozen]
- @locked_specs
- else
- last_resolve = converge_locked_specs
-
- # Record the specs available in each gem's source, so that those
- # specs will be available later when the resolver knows where to
- # look for that gemspec (or its dependencies)
- source_requirements = {}
- dependencies.each do |dep|
- next unless dep.source
- source_requirements[dep.name] = dep.source.specs
+ def resolve_specs(with_local_override)
+ if Bundler.settings[:frozen]
+ @locked_specs
+ else
+ last_resolve = converge_locked_specs
+
+ # Record the specs available in each gem's source, so that those
+ # specs will be available later when the resolver knows where to
+ # look for that gemspec (or its dependencies)
+ source_requirements = {}
+ dependencies.each do |dep|
+ next unless dep.source
+ source_requirements[dep.name] = dep.source.specs
+ end
+
+ override_index = Index.new
+
+ if with_local_override && @local_overrides
+ @local_overrides && @local_overrides.each do |s|
+ override_index.add_source s.specs
end
+ end
+
+ local_resolve = Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve, override_index)
- # Run a resolve against the locally available gems
- last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve)
+ # for all of the local-override gems that were resolved, pop them out of the lockfile-resolved list
+ # so that we don't duplicate gems
+ local_resolve.to_a.each do |s|
+ if override_index[s.name].any?
+ o = override_index[s.name].first
+
+ if local_resolve[s.name] && local_resolve[s.name].first.source.class == Bundler::Source::Path
+ last_resolve.delete(s.name)
+ else
+ Bundler.ui.warn("Couln't use #{o.name} from #{o.source.path}, perhaps it's out of date?")
+ end
+ end
end
+
+ last_resolve.merge local_resolve
end
end
+ def resolve
+ @resolve ||= resolve_specs(false)
+ end
+
+ def resolve_with_local_override
+ resolve_specs(true)
+ end
+
def index
@index ||= Index.build do |idx|
other_sources = @sources.find_all{|s| !s.is_a?(Bundler::Source::Rubygems) }
View
@@ -14,6 +14,7 @@ def initialize
@rubygems_source = Source::Rubygems.new
@source = nil
@sources = []
+ @local_overrides = []
@dependencies = []
@groups = []
@platforms = []
@@ -129,7 +130,7 @@ def git(uri, options = {}, source_options = {}, &blk)
def to_definition(lockfile, unlock)
@sources << @rubygems_source unless @sources.include?(@rubygems_source)
- Definition.new(lockfile, @dependencies, @sources, unlock)
+ Definition.new(lockfile, @dependencies, @sources, unlock, @local_overrides)
end
def group(*args, &blk)
@@ -185,7 +186,7 @@ def _normalize_hash(opts)
def _normalize_options(name, version, opts)
_normalize_hash(opts)
- invalid_keys = opts.keys - %w(group groups git github path name branch ref tag require submodules platform platforms type)
+ invalid_keys = opts.keys - %w(group groups git github local path name branch ref tag require submodules platform platforms type)
if invalid_keys.any?
plural = invalid_keys.size > 1
message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
@@ -216,6 +217,12 @@ def _normalize_options(name, version, opts)
opts["git"] = "git://github.com/#{github}.git"
end
+ if local_path = opts.delete("local")
+ if File.directory?(File.expand_path(local_path, Bundler.root))
+ @local_overrides << Bundler::Source::Path.new("name" => name, "path" => local_path)
+ end
+ end
+
["git", "path"].each do |type|
if param = opts[type]
if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/
View
@@ -29,7 +29,7 @@ def initialize_copy(o)
end
def inspect
- "<Index sources=#{sources.map{|s| s.inspect}} specs.size=#{specs.size}>"
+ "<Index sources=#{sources.map{|s| s.to_s}} specs.size=#{specs.size}>"
end
def empty?
View
@@ -121,9 +121,10 @@ def __dependencies
# ==== Returns
# <GemBundle>,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 = [])
+ def self.resolve(requirements, index, source_requirements = {}, base = [], local_overrides = [])
base = SpecSet.new(base) unless base.is_a?(SpecSet)
- resolver = new(index, source_requirements, base)
+
+ resolver = new(index, source_requirements, base, local_overrides)
result = catch(:success) do
resolver.start(requirements)
raise resolver.version_conflict
@@ -132,10 +133,11 @@ def self.resolve(requirements, index, source_requirements = {}, base = [])
SpecSet.new(result)
end
- def initialize(index, source_requirements, base)
+ def initialize(index, source_requirements, base, local_overrides = nil)
@errors = {}
@stack = []
@base = base
+ @local_overrides = local_overrides
@index = index
@deps_for = {}
@missing_gems = Hash.new(0)
@@ -163,9 +165,13 @@ def start(reqs)
def resolve(reqs, activated)
# If the requirements are empty, then we are in a success state. Aka, all
# gem dependencies have been resolved.
+ if reqs.empty?
+ debug { "resolved!" }
+ end
throw :success, successify(activated) if reqs.empty?
- debug { print "\e[2J\e[f" ; "==== Iterating ====\n\n" }
+ #debug { print "\e[2J\e[f" ; "==== Iterating ====\n\n" }
+ debug { "iterating" }
# Sort dependencies so that the ones that are easiest to resolve are first.
# Easiest to resolve is defined by:
@@ -179,7 +185,7 @@ def resolve(reqs, activated)
activated[a.name] ? 0 : gems_size(a) ]
end
- debug { "Activated:\n" + activated.values.map {|a| " #{a}" }.join("\n") }
+ debug { "Activated:\n" + activated.values.map {|a| " #{a} - #{a.source}" }.join("\n") }
debug { "Requirements:\n" + reqs.map {|r| " #{r}"}.join("\n") }
activated = activated.dup
@@ -249,6 +255,7 @@ def resolve(reqs, activated)
# Fetch all gem versions matching the requirement
matching_versions = search(current)
+ debug { matching_versions.join(" - ") }
# If we found no versions that match the current requirement
if matching_versions.empty?
@@ -356,17 +363,34 @@ def clear_search_cache
@deps_for = {}
end
- def search(dep)
+ def reqs_for(dep)
+ if @local_overrides and (local = @local_overrides[dep.name]) and local.any?
+ d = dep.dep
+ if @local_overrides.search(d, nil).any?
+ return d
+ end
+ end
+
if base = @base[dep.name] and base.any?
reqs = [dep.requirement.as_list, base.first.version.to_s].flatten.compact
- d = Gem::Dependency.new(base.first.name, *reqs)
+ Gem::Dependency.new(base.first.name, *reqs)
else
- d = dep.dep
+ dep.dep
end
+ end
+
+ def search(dep)
+ d = reqs_for(dep)
@deps_for[d.hash] ||= begin
- index = @source_requirements[d.name] || @index
- results = index.search(d, @base[d.name])
+ if @local_overrides
+ results = @local_overrides.search(d, nil)
+ end
+
+ if results.nil? or results.empty?
+ index = @source_requirements[d.name] || @index
+ results = index.search(d, @base[d.name])
+ end
if results.any?
version = results.first.version
@@ -383,6 +407,7 @@ def search(dep)
deps = []
end
end
+
end
def clean_req(req)
View
@@ -65,6 +65,14 @@ def []=(key, value)
value
end
+ def delete(key)
+ @specs.delete_if { |s| s.name == key }
+ @lookup = nil
+ @sorted = nil
+ lookup
+ sorted
+ end
+
def sort!
self
end
@@ -51,7 +51,7 @@ def ask(statement, color=nil)
#
def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/))
message = message.to_s
- message = set_color(message, color) if color
+ message = set_color(message, color) if color && stdout.isatty
spaces = " " * padding
View
@@ -0,0 +1,50 @@
+require "spec_helper"
+
+describe "Bundler.setup with local-override" do
+ before do
+ @path = bundled_app(File.join('vendor', 'rack'))
+
+ build_lib "rack", "1.1", :path => @path do |s|
+ s.write "lib/rack.rb", "puts 'LOCAL'"
+ end
+ end
+
+ describe "on top of a git source" do
+ before do
+ build_git "rack", "1.1" do |s|
+ s.write "lib/rack.rb", "puts 'GIT'"
+ end
+
+ install_gemfile <<-G
+ gem 'rack', :git => "#{lib_path('rack-1.1')}", :local => "#{@path}"
+ G
+ end
+
+ it "prefers the path specificed in local" do
+ run "require 'rack'"
+ out.should == "LOCAL"
+ end
+
+ describe "when the path is missing" do
+ it "quietly falls back" do
+ FileUtils.rm_rf(@path)
+ run "require 'rack'"
+ out.should == "GIT"
+ end
+ end
+ end
+
+ describe "on top of a rubygems source" do
+ before do
+ install_gemfile <<-G
+ source "file://#{gem_repo1}"
+ gem 'rack', :local => "#{@path}"
+ G
+ end
+
+ it "prefers the path specificed in local" do
+ run "require 'rack'"
+ out.should == "LOCAL"
+ end
+ end
+end