moneypools-bundler 0.7.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +275 -0
- data/Rakefile +60 -0
- data/lib/bundler.rb +35 -0
- data/lib/bundler/cli.rb +69 -0
- data/lib/bundler/commands/bundle_command.rb +72 -0
- data/lib/bundler/commands/exec_command.rb +36 -0
- data/lib/bundler/dependency.rb +64 -0
- data/lib/bundler/dsl.rb +173 -0
- data/lib/bundler/environment.rb +181 -0
- data/lib/bundler/finder.rb +51 -0
- data/lib/bundler/gem_bundle.rb +11 -0
- data/lib/bundler/gem_ext.rb +33 -0
- data/lib/bundler/remote_specification.rb +50 -0
- data/lib/bundler/repository.rb +256 -0
- data/lib/bundler/resolver.rb +234 -0
- data/lib/bundler/runtime.rb +2 -0
- data/lib/bundler/source.rb +326 -0
- data/lib/bundler/templates/app_script.erb +3 -0
- data/lib/bundler/templates/environment.erb +142 -0
- data/lib/rubygems_plugin.rb +6 -0
- metadata +79 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Bundler
|
2
|
+
# Finder behaves like a rubygems source index in that it responds
|
3
|
+
# to #search. It also resolves a list of dependencies finding the
|
4
|
+
# best possible configuration of gems that satisifes all requirements
|
5
|
+
# without causing any gem activation errors.
|
6
|
+
class Finder
|
7
|
+
|
8
|
+
# Takes an array of gem sources and fetches the full index of
|
9
|
+
# gems from each one. It then combines the indexes together keeping
|
10
|
+
# track of the original source so that any resolved gem can be
|
11
|
+
# fetched from the correct source.
|
12
|
+
#
|
13
|
+
# ==== Parameters
|
14
|
+
# *sources<String>:: URI pointing to the gem repository
|
15
|
+
def initialize(*sources)
|
16
|
+
@cache = {}
|
17
|
+
@index = {}
|
18
|
+
@sources = sources
|
19
|
+
end
|
20
|
+
|
21
|
+
# Searches for a gem that matches the dependency
|
22
|
+
#
|
23
|
+
# ==== Parameters
|
24
|
+
# dependency<Gem::Dependency>:: The gem dependency to search for
|
25
|
+
#
|
26
|
+
# ==== Returns
|
27
|
+
# [Gem::Specification]:: A collection of gem specifications
|
28
|
+
# matching the search
|
29
|
+
def search(dependency)
|
30
|
+
@cache[dependency.hash] ||= begin
|
31
|
+
find_by_name(dependency.name).select do |spec|
|
32
|
+
dependency =~ spec
|
33
|
+
end.sort_by {|s| s.version }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def find_by_name(name)
|
40
|
+
matches = @index[name] ||= begin
|
41
|
+
versions = {}
|
42
|
+
@sources.reverse_each do |source|
|
43
|
+
versions.merge! source.specs[name] || {}
|
44
|
+
end
|
45
|
+
versions
|
46
|
+
end
|
47
|
+
matches.values
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Gem
|
2
|
+
class Installer
|
3
|
+
remove_method(:app_script_text) if method_defined?(:app_script_text)
|
4
|
+
|
5
|
+
def app_script_text(bin_file_name)
|
6
|
+
path = @gem_home
|
7
|
+
template = File.read(File.join(File.dirname(__FILE__), "templates", "app_script.erb"))
|
8
|
+
erb = ERB.new(template, nil, '-')
|
9
|
+
erb.result(binding)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Specification
|
14
|
+
attr_accessor :source
|
15
|
+
attr_accessor :location
|
16
|
+
|
17
|
+
remove_method(:specification_version) if method_defined?(:specification_version)
|
18
|
+
|
19
|
+
# Hack to fix github's strange marshal file
|
20
|
+
def specification_version
|
21
|
+
@specification_version && @specification_version.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
alias full_gem_path_without_location full_gem_path
|
25
|
+
def full_gem_path
|
26
|
+
if defined?(@location) && @location
|
27
|
+
@location
|
28
|
+
else
|
29
|
+
full_gem_path_without_location
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Bundler
|
2
|
+
# Represents a lazily loaded gem specification, where the full specification
|
3
|
+
# is on the source server in rubygems' "quick" index. The proxy object is to
|
4
|
+
# be seeded with what we're given from the source's abbreviated index - the
|
5
|
+
# full specification will only be fetched when necesary.
|
6
|
+
class RemoteSpecification
|
7
|
+
attr_reader :name, :version, :platform
|
8
|
+
attr_accessor :source
|
9
|
+
|
10
|
+
def initialize(name, version, platform, source_uri)
|
11
|
+
@name = name
|
12
|
+
@version = version
|
13
|
+
@platform = platform
|
14
|
+
@source_uri = source_uri
|
15
|
+
end
|
16
|
+
|
17
|
+
def full_name
|
18
|
+
if platform == Gem::Platform::RUBY or platform.nil? then
|
19
|
+
"#{@name}-#{@version}"
|
20
|
+
else
|
21
|
+
"#{@name}-#{@version}-#{platform}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Because Rubyforge cannot be trusted to provide valid specifications
|
26
|
+
# once the remote gem is donwloaded, the backend specification will
|
27
|
+
# be swapped out.
|
28
|
+
def __swap__(spec)
|
29
|
+
@specification = spec
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def _remote_uri
|
35
|
+
"#{@source_uri}/quick/Marshal.4.8/#{@name}-#{@version}.gemspec.rz"
|
36
|
+
end
|
37
|
+
|
38
|
+
def _remote_specification
|
39
|
+
@specification ||= begin
|
40
|
+
deflated = Gem::RemoteFetcher.fetcher.fetch_path(_remote_uri)
|
41
|
+
inflated = Gem.inflate(deflated)
|
42
|
+
Marshal.load(inflated)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(method, *args, &blk)
|
47
|
+
_remote_specification.send(method, *args, &blk)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
module Bundler
|
2
|
+
class InvalidRepository < StandardError ; end
|
3
|
+
|
4
|
+
class Repository
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize(path, bindir)
|
8
|
+
FileUtils.mkdir_p(path)
|
9
|
+
|
10
|
+
@path = Pathname.new(path)
|
11
|
+
@bindir = Pathname.new(bindir)
|
12
|
+
|
13
|
+
@cache = GemDirectorySource.new(:location => @path.join("cache"))
|
14
|
+
end
|
15
|
+
|
16
|
+
def install(dependencies, sources, options = {})
|
17
|
+
# TODO: clean this up
|
18
|
+
sources.each do |s|
|
19
|
+
s.repository = self
|
20
|
+
s.local = options[:cached]
|
21
|
+
end
|
22
|
+
|
23
|
+
source_requirements = {}
|
24
|
+
dependencies = dependencies.map do |dep|
|
25
|
+
source_requirements[dep.name] = dep.source if dep.source
|
26
|
+
dep.to_gem_dependency
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check to see whether the existing cache meets all the requirements
|
30
|
+
begin
|
31
|
+
valid = nil
|
32
|
+
# valid = Resolver.resolve(dependencies, [source_index], source_requirements)
|
33
|
+
rescue Bundler::GemNotFound
|
34
|
+
end
|
35
|
+
|
36
|
+
sources = only_local(sources) if options[:cached]
|
37
|
+
|
38
|
+
# Check the remote sources if the existing cache does not meet the requirements
|
39
|
+
# or the user passed --update
|
40
|
+
if options[:update] || !valid
|
41
|
+
Bundler.logger.info "Calculating dependencies..."
|
42
|
+
bundle = Resolver.resolve(dependencies, [@cache] + sources, source_requirements)
|
43
|
+
download(bundle, options)
|
44
|
+
do_install(bundle, options)
|
45
|
+
valid = bundle
|
46
|
+
end
|
47
|
+
|
48
|
+
generate_bins(valid, options)
|
49
|
+
cleanup(valid, options)
|
50
|
+
configure(valid, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def cache(*gemfiles)
|
54
|
+
FileUtils.mkdir_p(@path.join("cache"))
|
55
|
+
gemfiles.each do |gemfile|
|
56
|
+
Bundler.logger.info "Caching: #{File.basename(gemfile)}"
|
57
|
+
FileUtils.cp(gemfile, @path.join("cache"))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def prune(dependencies, sources)
|
62
|
+
sources.each do |s|
|
63
|
+
s.repository = self
|
64
|
+
s.local = true
|
65
|
+
end
|
66
|
+
|
67
|
+
sources = only_local(sources)
|
68
|
+
bundle = Resolver.resolve(dependencies, [@cache] + sources)
|
69
|
+
@cache.gems.each do |name, specs|
|
70
|
+
specs.each do |spec|
|
71
|
+
unless bundle.any? { |s| s.name == spec.name && s.version == spec.version }
|
72
|
+
Bundler.logger.info "Pruning #{spec.name} (#{spec.version}) from the cache"
|
73
|
+
FileUtils.rm @path.join("cache", "#{spec.full_name}.gem")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def gems
|
80
|
+
source_index.gems.values
|
81
|
+
end
|
82
|
+
|
83
|
+
def outdated_gems
|
84
|
+
source_index.outdated.sort
|
85
|
+
end
|
86
|
+
|
87
|
+
def source_index
|
88
|
+
index = Gem::SourceIndex.from_gems_in(@path.join("specifications"))
|
89
|
+
index.each { |n, spec| spec.loaded_from = @path.join("specifications", "#{spec.full_name}.gemspec") }
|
90
|
+
index
|
91
|
+
end
|
92
|
+
|
93
|
+
def download_path_for(type)
|
94
|
+
@repos[type].download_path_for
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def only_local(sources)
|
100
|
+
sources.select { |s| s.can_be_local? }
|
101
|
+
end
|
102
|
+
|
103
|
+
def download(bundle, options)
|
104
|
+
bundle.sort_by {|s| s.full_name.downcase }.each do |spec|
|
105
|
+
next if options[:no_bundle].include?(spec.name)
|
106
|
+
spec.source.download(spec)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def do_install(bundle, options)
|
111
|
+
bundle.each do |spec|
|
112
|
+
next if options[:no_bundle].include?(spec.name)
|
113
|
+
spec.loaded_from = @path.join("specifications", "#{spec.full_name}.gemspec")
|
114
|
+
# Do nothing if the gem is already expanded
|
115
|
+
next if @path.join("gems", spec.full_name).directory?
|
116
|
+
|
117
|
+
case spec.source
|
118
|
+
when GemSource, GemDirectorySource, SystemGemSource
|
119
|
+
expand_gemfile(spec, options)
|
120
|
+
else
|
121
|
+
expand_vendored_gem(spec, options)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def generate_bins(bundle, options)
|
127
|
+
bundle.each do |spec|
|
128
|
+
next if options[:no_bundle].include?(spec.name)
|
129
|
+
# HAX -- Generate the bin
|
130
|
+
bin_dir = @bindir
|
131
|
+
path = @path
|
132
|
+
installer = Gem::Installer.allocate
|
133
|
+
installer.instance_eval do
|
134
|
+
@spec = spec
|
135
|
+
@bin_dir = bin_dir
|
136
|
+
@gem_dir = path.join("gems", "#{spec.full_name}")
|
137
|
+
@gem_home = path
|
138
|
+
@wrappers = true
|
139
|
+
@format_executable = false
|
140
|
+
@env_shebang = false
|
141
|
+
end
|
142
|
+
installer.generate_bin
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def expand_gemfile(spec, options)
|
147
|
+
Bundler.logger.info "Installing #{spec.name} (#{spec.version})"
|
148
|
+
|
149
|
+
gemfile = @path.join("cache", "#{spec.full_name}.gem").to_s
|
150
|
+
|
151
|
+
if build_args = options[:build_options] && options[:build_options][spec.name]
|
152
|
+
Gem::Command.build_args = build_args.map {|k,v| "--with-#{k}=#{v}"}
|
153
|
+
end
|
154
|
+
|
155
|
+
installer = Gem::Installer.new(gemfile, options.merge(
|
156
|
+
:install_dir => @path,
|
157
|
+
:ignore_dependencies => true,
|
158
|
+
:env_shebang => true,
|
159
|
+
:wrappers => true,
|
160
|
+
:bin_dir => @bindir
|
161
|
+
))
|
162
|
+
installer.install
|
163
|
+
ensure
|
164
|
+
Gem::Command.build_args = []
|
165
|
+
end
|
166
|
+
|
167
|
+
def expand_vendored_gem(spec, options)
|
168
|
+
add_spec(spec)
|
169
|
+
FileUtils.mkdir_p(@path.join("gems"))
|
170
|
+
File.symlink(spec.location, @path.join("gems", spec.full_name))
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_spec(spec)
|
174
|
+
destination = path.join('specifications')
|
175
|
+
destination.mkdir unless destination.exist?
|
176
|
+
|
177
|
+
File.open(destination.join("#{spec.full_name}.gemspec"), 'w') do |f|
|
178
|
+
f.puts spec.to_ruby
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def cleanup(valid, options)
|
183
|
+
to_delete = gems
|
184
|
+
to_delete.delete_if do |spec|
|
185
|
+
valid.any? { |other| spec.name == other.name && spec.version == other.version }
|
186
|
+
end
|
187
|
+
|
188
|
+
valid_executables = valid.map { |s| s.executables }.flatten.compact
|
189
|
+
|
190
|
+
to_delete.each do |spec|
|
191
|
+
Bundler.logger.info "Deleting gem: #{spec.name} (#{spec.version})"
|
192
|
+
FileUtils.rm_rf(@path.join("specifications", "#{spec.full_name}.gemspec"))
|
193
|
+
FileUtils.rm_rf(@path.join("gems", spec.full_name))
|
194
|
+
# Cleanup the bin directory
|
195
|
+
spec.executables.each do |bin|
|
196
|
+
next if valid_executables.include?(bin)
|
197
|
+
Bundler.logger.info "Deleting bin file: #{bin}"
|
198
|
+
FileUtils.rm_rf(@bindir.join(bin))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def expand(options)
|
204
|
+
each_repo do |repo|
|
205
|
+
repo.expand(options)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def configure(specs, options)
|
210
|
+
generate_environment(specs, options)
|
211
|
+
end
|
212
|
+
|
213
|
+
def generate_environment(specs, options)
|
214
|
+
FileUtils.mkdir_p(path)
|
215
|
+
|
216
|
+
load_paths = load_paths_for_specs(specs, options)
|
217
|
+
bindir = @bindir.relative_path_from(path).to_s
|
218
|
+
filename = options[:manifest].relative_path_from(path).to_s
|
219
|
+
|
220
|
+
File.open(path.join("environment.rb"), "w") do |file|
|
221
|
+
template = File.read(File.join(File.dirname(__FILE__), "templates", "environment.erb"))
|
222
|
+
erb = ERB.new(template, nil, '-')
|
223
|
+
file.puts erb.result(binding)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def load_paths_for_specs(specs, options)
|
228
|
+
load_paths = []
|
229
|
+
specs.each do |spec|
|
230
|
+
next if options[:no_bundle].include?(spec.name)
|
231
|
+
gem_path = Pathname.new(spec.full_gem_path)
|
232
|
+
load_paths << load_path_for(gem_path, spec.bindir) if spec.bindir
|
233
|
+
spec.require_paths.each do |path|
|
234
|
+
load_paths << load_path_for(gem_path, path)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
load_paths
|
238
|
+
end
|
239
|
+
|
240
|
+
def load_path_for(gem_path, path)
|
241
|
+
gem_path.join(path).relative_path_from(@path).to_s
|
242
|
+
end
|
243
|
+
|
244
|
+
def spec_file_for(spec)
|
245
|
+
spec.loaded_from.relative_path_from(@path).to_s
|
246
|
+
end
|
247
|
+
|
248
|
+
def require_code(file, dep)
|
249
|
+
constraint = case
|
250
|
+
when dep.only then %{ if #{dep.only.inspect}.include?(env)}
|
251
|
+
when dep.except then %{ unless #{dep.except.inspect}.include?(env)}
|
252
|
+
end
|
253
|
+
"require #{file.inspect}#{constraint}"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# This is the latest iteration of the gem dependency resolving algorithm. As of now,
|
2
|
+
# it can resolve (as a success of failure) any set of gem dependencies we throw at it
|
3
|
+
# in a reasonable amount of time. The most iterations I've seen it take is about 150.
|
4
|
+
# The actual implementation of the algorithm is not as good as it could be yet, but that
|
5
|
+
# can come later.
|
6
|
+
|
7
|
+
# Extending Gem classes to add necessary tracking information
|
8
|
+
module Gem
|
9
|
+
class Dependency
|
10
|
+
def required_by
|
11
|
+
@required_by ||= []
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class Specification
|
15
|
+
def required_by
|
16
|
+
@required_by ||= []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Bundler
|
22
|
+
class GemNotFound < StandardError; end
|
23
|
+
class VersionConflict < StandardError; end
|
24
|
+
|
25
|
+
class Resolver
|
26
|
+
|
27
|
+
attr_reader :errors
|
28
|
+
|
29
|
+
# Figures out the best possible configuration of gems that satisfies
|
30
|
+
# the list of passed dependencies and any child dependencies without
|
31
|
+
# causing any gem activation errors.
|
32
|
+
#
|
33
|
+
# ==== Parameters
|
34
|
+
# *dependencies<Gem::Dependency>:: The list of dependencies to resolve
|
35
|
+
#
|
36
|
+
# ==== Returns
|
37
|
+
# <GemBundle>,nil:: If the list of dependencies can be resolved, a
|
38
|
+
# collection of gemspecs is returned. Otherwise, nil is returned.
|
39
|
+
def self.resolve(requirements, sources, source_requirements = {})
|
40
|
+
resolver = new(sources, source_requirements)
|
41
|
+
result = catch(:success) do
|
42
|
+
resolver.resolve(requirements, {})
|
43
|
+
output = resolver.errors.inject("") do |o, (conflict, (origin, requirement))|
|
44
|
+
o << " Conflict on: #{conflict.inspect}:\n"
|
45
|
+
o << " * #{conflict} (#{origin.version}) activated by #{origin.required_by.first}\n"
|
46
|
+
o << " * #{requirement} required by #{requirement.required_by.first}\n"
|
47
|
+
o << " All possible versions of origin requirements conflict."
|
48
|
+
end
|
49
|
+
raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{output}"
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
if result
|
53
|
+
# Order gems in order of dependencies. Every gem's dependency is at
|
54
|
+
# a smaller index in the array.
|
55
|
+
ordered = []
|
56
|
+
result.values.each do |spec1|
|
57
|
+
index = nil
|
58
|
+
place = ordered.detect do |spec2|
|
59
|
+
spec1.dependencies.any? { |d| d.name == spec2.name }
|
60
|
+
end
|
61
|
+
place ?
|
62
|
+
ordered.insert(ordered.index(place), spec1) :
|
63
|
+
ordered << spec1
|
64
|
+
end
|
65
|
+
ordered.reverse
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(sources, source_requirements)
|
70
|
+
@errors = {}
|
71
|
+
@stack = []
|
72
|
+
@specs = Hash.new { |h,k| h[k] = [] }
|
73
|
+
@by_gem = source_requirements
|
74
|
+
@cache = {}
|
75
|
+
@index = {}
|
76
|
+
|
77
|
+
sources.each do |source|
|
78
|
+
source.gems.each do |name, specs|
|
79
|
+
# Hack to work with a regular Gem::SourceIndex
|
80
|
+
[specs].flatten.compact.each do |spec|
|
81
|
+
next if @specs[spec.name].any? { |s| s.version == spec.version }
|
82
|
+
@specs[spec.name] << spec
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def debug
|
89
|
+
puts yield if defined?($debug) && $debug
|
90
|
+
end
|
91
|
+
|
92
|
+
def resolve(reqs, activated)
|
93
|
+
# If the requirements are empty, then we are in a success state. Aka, all
|
94
|
+
# gem dependencies have been resolved.
|
95
|
+
throw :success, activated if reqs.empty?
|
96
|
+
|
97
|
+
debug { STDIN.gets ; print "\e[2J\e[f" ; "==== Iterating ====\n\n" }
|
98
|
+
|
99
|
+
# Sort dependencies so that the ones that are easiest to resolve are first.
|
100
|
+
# Easiest to resolve is defined by:
|
101
|
+
# 1) Is this gem already activated?
|
102
|
+
# 2) Do the version requirements include prereleased gems?
|
103
|
+
# 3) Sort by number of gems available in the source.
|
104
|
+
reqs = reqs.sort_by do |a|
|
105
|
+
[ activated[a.name] ? 0 : 1,
|
106
|
+
a.version_requirements.prerelease? ? 0 : 1,
|
107
|
+
@errors[a.name] ? 0 : 1,
|
108
|
+
activated[a.name] ? 0 : search(a).size ]
|
109
|
+
end
|
110
|
+
|
111
|
+
debug { "Activated:\n" + activated.values.map { |a| " #{a.name} (#{a.version})" }.join("\n") }
|
112
|
+
debug { "Requirements:\n" + reqs.map { |r| " #{r.name} (#{r.version_requirements})"}.join("\n") }
|
113
|
+
|
114
|
+
activated = activated.dup
|
115
|
+
# Pull off the first requirement so that we can resolve it
|
116
|
+
current = reqs.shift
|
117
|
+
|
118
|
+
debug { "Attempting:\n #{current.name} (#{current.version_requirements})"}
|
119
|
+
|
120
|
+
# Check if the gem has already been activated, if it has, we will make sure
|
121
|
+
# that the currently activated gem satisfies the requirement.
|
122
|
+
if existing = activated[current.name]
|
123
|
+
if current.version_requirements.satisfied_by?(existing.version)
|
124
|
+
debug { " * [SUCCESS] Already activated" }
|
125
|
+
@errors.delete(existing.name)
|
126
|
+
# Since the current requirement is satisfied, we can continue resolving
|
127
|
+
# the remaining requirements.
|
128
|
+
resolve(reqs, activated)
|
129
|
+
else
|
130
|
+
debug { " * [FAIL] Already activated" }
|
131
|
+
@errors[existing.name] = [existing, current]
|
132
|
+
debug { current.required_by.map {|d| " * #{d.name} (#{d.version_requirements})" }.join("\n") }
|
133
|
+
# debug { " * All current conflicts:\n" + @errors.keys.map { |c| " - #{c}" }.join("\n") }
|
134
|
+
# Since the current requirement conflicts with an activated gem, we need
|
135
|
+
# to backtrack to the current requirement's parent and try another version
|
136
|
+
# of it (maybe the current requirement won't be present anymore). If the
|
137
|
+
# current requirement is a root level requirement, we need to jump back to
|
138
|
+
# where the conflicting gem was activated.
|
139
|
+
parent = current.required_by.last || existing.required_by.last
|
140
|
+
# We track the spot where the current gem was activated because we need
|
141
|
+
# to keep a list of every spot a failure happened.
|
142
|
+
debug { " -> Jumping to: #{parent.name}" }
|
143
|
+
throw parent.name, existing.required_by.last.name
|
144
|
+
end
|
145
|
+
else
|
146
|
+
# There are no activated gems for the current requirement, so we are going
|
147
|
+
# to find all gems that match the current requirement and try them in decending
|
148
|
+
# order. We also need to keep a set of all conflicts that happen while trying
|
149
|
+
# this gem. This is so that if no versions work, we can figure out the best
|
150
|
+
# place to backtrack to.
|
151
|
+
conflicts = Set.new
|
152
|
+
|
153
|
+
# Fetch all gem versions matching the requirement
|
154
|
+
#
|
155
|
+
# TODO: Warn / error when no matching versions are found.
|
156
|
+
matching_versions = search(current)
|
157
|
+
|
158
|
+
if matching_versions.empty?
|
159
|
+
if current.required_by.empty?
|
160
|
+
raise GemNotFound, "Could not find gem '#{current}' in any of the sources"
|
161
|
+
end
|
162
|
+
Bundler.logger.warn "Could not find gem '#{current}' (required by '#{current.required_by.last}') in any of the sources"
|
163
|
+
end
|
164
|
+
|
165
|
+
matching_versions.reverse_each do |spec|
|
166
|
+
conflict = resolve_requirement(spec, current, reqs.dup, activated.dup)
|
167
|
+
conflicts << conflict if conflict
|
168
|
+
end
|
169
|
+
# If the current requirement is a root level gem and we have conflicts, we
|
170
|
+
# can figure out the best spot to backtrack to.
|
171
|
+
if current.required_by.empty? && !conflicts.empty?
|
172
|
+
# Check the current "catch" stack for the first one that is included in the
|
173
|
+
# conflicts set. That is where the parent of the conflicting gem was required.
|
174
|
+
# By jumping back to this spot, we can try other version of the parent of
|
175
|
+
# the conflicting gem, hopefully finding a combination that activates correctly.
|
176
|
+
@stack.reverse_each do |savepoint|
|
177
|
+
if conflicts.include?(savepoint)
|
178
|
+
debug { " -> Jumping to: #{savepoint}" }
|
179
|
+
throw savepoint
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def resolve_requirement(spec, requirement, reqs, activated)
|
187
|
+
# We are going to try activating the spec. We need to keep track of stack of
|
188
|
+
# requirements that got us to the point of activating this gem.
|
189
|
+
spec.required_by.replace requirement.required_by
|
190
|
+
spec.required_by << requirement
|
191
|
+
|
192
|
+
activated[spec.name] = spec
|
193
|
+
debug { " Activating: #{spec.name} (#{spec.version})" }
|
194
|
+
debug { spec.required_by.map { |d| " * #{d.name} (#{d.version_requirements})" }.join("\n") }
|
195
|
+
|
196
|
+
# Now, we have to loop through all child dependencies and add them to our
|
197
|
+
# array of requirements.
|
198
|
+
debug { " Dependencies"}
|
199
|
+
spec.dependencies.each do |dep|
|
200
|
+
next if dep.type == :development
|
201
|
+
debug { " * #{dep.name} (#{dep.version_requirements})" }
|
202
|
+
dep.required_by.replace(requirement.required_by)
|
203
|
+
dep.required_by << requirement
|
204
|
+
reqs << dep
|
205
|
+
end
|
206
|
+
|
207
|
+
# We create a savepoint and mark it by the name of the requirement that caused
|
208
|
+
# the gem to be activated. If the activated gem ever conflicts, we are able to
|
209
|
+
# jump back to this point and try another version of the gem.
|
210
|
+
length = @stack.length
|
211
|
+
@stack << requirement.name
|
212
|
+
retval = catch(requirement.name) do
|
213
|
+
resolve(reqs, activated)
|
214
|
+
end
|
215
|
+
# Since we're doing a lot of throw / catches. A push does not necessarily match
|
216
|
+
# up to a pop. So, we simply slice the stack back to what it was before the catch
|
217
|
+
# block.
|
218
|
+
@stack.slice!(length..-1)
|
219
|
+
retval
|
220
|
+
end
|
221
|
+
|
222
|
+
def search(dependency)
|
223
|
+
@cache[dependency.hash] ||= begin
|
224
|
+
collection = @by_gem[dependency.name].gems if @by_gem[dependency.name]
|
225
|
+
collection ||= @specs
|
226
|
+
collection[dependency.name].select do |spec|
|
227
|
+
match = dependency =~ spec
|
228
|
+
match &= dependency.version_requirements.prerelease? if spec.version.prerelease?
|
229
|
+
match
|
230
|
+
end.sort_by {|s| s.version }
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|