Class: Bundler::Resolver
- Inherits:
-
Object
- Object
- Bundler::Resolver
- Defined in:
- lib/bundler/resolver.rb
Defined Under Namespace
Classes: SpecGroup
Constant Summary collapse
- ALL =
Bundler::Dependency::PLATFORM_MAP.values.uniq.freeze
Instance Attribute Summary collapse
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
Class Method Summary collapse
-
.resolve(requirements, index, source_requirements = {}, base = [], sort = true) ⇒ Object
Figures out the best possible configuration of gems that satisfies the list of passed dependencies and any child dependencies without causing any gem activation errors.
Instance Method Summary collapse
- #clean_req(req) ⇒ Object
- #debug ⇒ Object
- #error_message ⇒ Object
-
#gem_message(requirement) ⇒ Object
For a given conflicted requirement, print out what exactly went wrong.
-
#initialize(index, source_requirements, base, sort) ⇒ Resolver
constructor
A new instance of Resolver.
- #resolve(reqs, activated) ⇒ Object
- #resolve_requirement(spec_group, requirement, reqs, activated) ⇒ Object
- #search(dep) ⇒ Object
- #start(reqs) ⇒ Object
- #successify(activated) ⇒ Object
- #version_conflict ⇒ Object
Constructor Details
#initialize(index, source_requirements, base, sort) ⇒ Resolver
Returns a new instance of Resolver.
135 136 137 138 139 140 141 142 143 |
# File 'lib/bundler/resolver.rb', line 135 def initialize(index, source_requirements, base, sort) @errors = {} @stack = [] @base = base @index = index @missing_gems = Hash.new(0) @source_requirements = source_requirements @sort = sort end |
Instance Attribute Details
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
112 113 114 |
# File 'lib/bundler/resolver.rb', line 112 def errors @errors end |
Class Method Details
.resolve(requirements, index, source_requirements = {}, base = [], sort = true) ⇒ Object
Figures out the best possible configuration of gems that satisfies the list of passed dependencies and any child dependencies without causing any gem activation errors.
Parameters
- *dependencies<Gem::Dependency>
-
The list of dependencies to resolve
Returns
- <GemBundle>,nil
-
If the list of dependencies can be resolved, a
collection of gemspecs is returned. Otherwise, nil is returned.
124 125 126 127 128 129 130 131 132 133 |
# File 'lib/bundler/resolver.rb', line 124 def self.resolve(requirements, index, source_requirements = {}, base = [], sort = true) base = SpecSet.new(base) unless base.is_a?(SpecSet) resolver = new(index, source_requirements, base, sort) result = catch(:success) do resolver.start(requirements) raise resolver.version_conflict nil end SpecSet.new(result) end |
Instance Method Details
#clean_req(req) ⇒ Object
380 381 382 383 384 385 386 |
# File 'lib/bundler/resolver.rb', line 380 def clean_req(req) if req.to_s.include?(">= 0") req.to_s.gsub(/ \(.*?\)$/, '') else req.to_s.gsub(/\, (runtime|development)\)$/, ')') end end |
#debug ⇒ Object
145 146 147 148 149 150 151 |
# File 'lib/bundler/resolver.rb', line 145 def debug if ENV['DEBUG_RESOLVER'] debug_info = yield debug_info = debug_info.inpsect unless debug_info.is_a?(String) $stderr.puts debug_info end end |
#error_message ⇒ Object
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 |
# File 'lib/bundler/resolver.rb', line 407 def output = errors.inject("") do |o, (conflict, (origin, requirement))| # origin is the SpecSet of specs from the Gemfile that is conflicted with if origin o << %{Bundler could not find compatible versions for gem "#{origin.name}":\n} o << " In Gemfile:\n" o << (requirement) # If the origin is "bundler", the conflict is us if origin.name == "bundler" o << " Current Bundler version:\n" newer_bundler_required = requirement.requirement > Gem::Requirement.new(origin.version) # If the origin is a LockfileParser, it does not respond_to :required_by elsif !origin.respond_to?(:required_by) || !(required_by = origin.required_by.first) o << " In snapshot (Gemfile.lock):\n" end o << (origin) # If the bundle wants a newer bundler than the running bundler, explain if origin.name == "bundler" && newer_bundler_required o << "Your version of Bundler is older than the one requested by the Gemfile.\n" o << "Perhaps you need to update Bundler by running `gem install bundler`." end # origin is nil if the required gem and version cannot be found in any of # the specified sources else # if the gem cannot be found because of a version conflict between lockfile and gemfile, # print a useful error that suggests running `bundle update`, which may fix things # # @base is a SpecSet of the gems in the lockfile # conflict is the name of the gem that could not be found if locked = @base[conflict].first o << "Bundler could not find compatible versions for gem #{conflict.inspect}:\n" o << " In snapshot (Gemfile.lock):\n" o << " #{clean_req(locked)}\n\n" o << " In Gemfile:\n" o << (requirement) o << "Running `bundle update` will rebuild your snapshot from scratch, using only\n" o << "the gems in your Gemfile, which may resolve the conflict.\n" # the rest of the time, the gem cannot be found because it does not exist in the known sources else if requirement.required_by.first o << "Could not find gem '#{clean_req(requirement)}', required by '#{clean_req(requirement.required_by.first)}', in any of the sources\n" else o << "Could not find gem '#{clean_req(requirement)} in any of the sources\n" end end end o end end |
#gem_message(requirement) ⇒ Object
For a given conflicted requirement, print out what exactly went wrong
393 394 395 396 397 398 399 400 401 402 403 404 405 |
# File 'lib/bundler/resolver.rb', line 393 def (requirement) m = "" # A requirement that is required by itself is actually in the Gemfile, and does # not "depend on" itself if requirement.required_by.first && requirement.required_by.first.name != requirement.name m << " #{clean_req(requirement.required_by.first)} depends on\n" m << " #{clean_req(requirement)}\n" else m << " #{clean_req(requirement)}\n" end m << "\n" end |
#resolve(reqs, activated) ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/bundler/resolver.rb', line 163 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, successify(activated) if reqs.empty? debug { print "\e[2J\e[f" ; "==== Iterating ====\n\n" } # Sort dependencies so that the ones that are easiest to resolve are first. # Easiest to resolve is defined by: # 1) Is this gem already activated? # 2) Do the version requirements include prereleased gems? # 3) Sort by number of gems available in the source. # This is a pretty expensive operation, so we skip it when the bundle is # locked and sorting isn't necessary. reqs = reqs.sort_by do |a| [ activated[a.name] ? 0 : 1, a.requirement.prerelease? ? 0 : 1, @errors[a.name] ? 0 : 1, activated[a.name] ? 0 : search(a).size ] end if @sort debug { "Activated:\n" + activated.values.map { |a| " #{a.name} (#{a.version})" }.join("\n") } debug { "Requirements:\n" + reqs.map { |r| " #{r.name} (#{r.requirement})"}.join("\n") } activated = activated.dup # Pull off the first requirement so that we can resolve it current = reqs.shift debug { "Attempting:\n #{current.name} (#{current.requirement})"} # Check if the gem has already been activated, if it has, we will make sure # that the currently activated gem satisfies the requirement. existing = activated[current.name] if existing || current.name == 'bundler' # Force the current if current.name == 'bundler' && !existing existing = search(DepProxy.new(Gem::Dependency.new('bundler', VERSION), Gem::Platform::RUBY)).first raise GemNotFound, %Q{Bundler could not find gem "bundler" (#{VERSION})} unless existing existing.required_by << existing activated['bundler'] = existing end if current.requirement.satisfied_by?(existing.version) debug { " * [SUCCESS] Already activated" } @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.__platform) resolve(reqs, activated) else debug { " * [FAIL] Already activated" } @errors[existing.name] = [existing, current] debug { current.required_by.map {|d| " * #{d.name} (#{d.requirement})" }.join("\n") } # debug { " * All current conflicts:\n" + @errors.keys.map { |c| " - #{c}" }.join("\n") } # Since the current requirement conflicts with an activated gem, we need # to backtrack to the current requirement's parent and try another version # of it (maybe the current requirement won't be present anymore). If the # current requirement is a root level requirement, we need to jump back to # where the conflicting gem was activated. parent = current.required_by.last # `existing` could not respond to required_by if it is part of the base set # of specs that was passed to the resolver (aka, instance of LazySpecification) parent ||= existing.required_by.last if existing.respond_to?(:required_by) # We track the spot where the current gem was activated because we need # to keep a list of every spot a failure happened. if parent && parent.name != 'bundler' debug { " -> Jumping to: #{parent.name}" } required_by = existing.respond_to?(:required_by) && existing.required_by.last throw parent.name, required_by && required_by.name 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 version_conflict end end else # There are no activated gems for the current requirement, so we are going # to find all gems that match the current requirement and try them in decending # order. We also need to keep a set of all conflicts that happen while trying # this gem. This is so that if no versions work, we can figure out the best # place to backtrack to. conflicts = Set.new # Fetch all gem versions matching the requirement # # TODO: Warn / error when no matching versions are found. matching_versions = search(current) if matching_versions.empty? if current.required_by.empty? if base = @base[current.name] and !base.empty? version = base.first.version = "You have requested:\n" \ " #{current.name} #{current.requirement}\n\n" \ "The bundle currently has #{current.name} locked at #{version}.\n" \ "Try running `bundle update #{current.name}`" elsif current.source name = current.name versions = @source_requirements[name][name].map { |s| s.version } = "Could not find gem '#{current}' in #{current.source}.\n" if versions.any? << "Source contains '#{name}' at: #{versions.join(', ')}" else << "Source does not contain any versions of '#{current}'" end else = "Could not find gem '#{current}' " if @index.sources.include?(Bundler::Source::Rubygems) << "in any of the gem sources listed in your Gemfile." else << "in the gems available on this machine." end end raise GemNotFound, else if @missing_gems[current] >= 5 = "Bundler could not find find gem #{current.required_by.last}," << "which is required by gem #{current}." raise GemNotFound, end @missing_gems[current] += 1 debug { " Could not find #{current} by #{current.required_by.last}" } @errors[current.name] = [nil, current] end end 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 # can figure out the best spot to backtrack to. if current.required_by.empty? && !conflicts.empty? # Check the current "catch" stack for the first one that is included in the # conflicts set. That is where the parent of the conflicting gem was required. # By jumping back to this spot, we can try other version of the parent of # the conflicting gem, hopefully finding a combination that activates correctly. @stack.reverse_each do |savepoint| if conflicts.include?(savepoint) debug { " -> Jumping to: #{savepoint}" } throw savepoint end end end end end |
#resolve_requirement(spec_group, requirement, reqs, activated) ⇒ Object
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/bundler/resolver.rb', line 317 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_group.required_by.replace requirement.required_by spec_group.required_by << requirement activated[spec_group.name] = spec_group debug { " Activating: #{spec_group.name} (#{spec_group.version})" } debug { spec_group.required_by.map { |d| " * #{d.name} (#{d.requirement})" }.join("\n") } dependencies = spec_group.activate_platform(requirement.__platform) # Now, we have to loop through all child dependencies and add them to our # array of requirements. debug { " Dependencies"} dependencies.each do |dep| next if dep.type == :development debug { " * #{dep.name} (#{dep.requirement})" } dep.required_by.replace(requirement.required_by) dep.required_by << requirement reqs << dep end # We create a savepoint and mark it by the name of the requirement that caused # the gem to be activated. If the activated gem ever conflicts, we are able to # jump back to this point and try another version of the gem. length = @stack.length @stack << requirement.name retval = catch(requirement.name) do resolve(reqs, activated) end # Since we're doing a lot of throw / catches. A push does not necessarily match # up to a pop. So, we simply slice the stack back to what it was before the catch # block. @stack.slice!(length..-1) retval end |
#search(dep) ⇒ Object
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/bundler/resolver.rb', line 355 def search(dep) if base = @base[dep.name] and base.any? d = Gem::Dependency.new(base.first.name, *[dep.requirement.as_list, base.first.version].flatten) else d = dep.dep end index = @source_requirements[d.name] || @index results = index.search_for_all_platforms(d, @base[d.name]) 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) }.select { |sg| sg.for?(dep.__platform) } else [] end end |
#start(reqs) ⇒ Object
157 158 159 160 161 |
# File 'lib/bundler/resolver.rb', line 157 def start(reqs) activated = {} resolve(reqs, activated) end |
#successify(activated) ⇒ Object
153 154 155 |
# File 'lib/bundler/resolver.rb', line 153 def successify(activated) activated.values.map { |s| s.to_specs }.flatten.compact end |
#version_conflict ⇒ Object
388 389 390 |
# File 'lib/bundler/resolver.rb', line 388 def version_conflict VersionConflict.new(errors.keys, ) end |