2018-11-03 02:07:56 +03:00
# frozen_string_literal: true
module Bundler
2022-11-12 00:00:58 +03:00
#
# This class implements the interface needed by PubGrub for resolution. It is
# equivalent to the `PubGrub::BasicPackageSource` class provided by PubGrub by
# default and used by the most simple PubGrub consumers.
#
2018-11-03 02:07:56 +03:00
class Resolver
2022-11-12 00:00:58 +03:00
require_relative " vendored_pub_grub "
2022-09-05 03:15:30 +03:00
require_relative " resolver/base "
2022-11-12 00:00:58 +03:00
require_relative " resolver/candidate "
2022-12-09 08:45:51 +03:00
require_relative " resolver/incompatibility "
2022-11-12 00:00:58 +03:00
require_relative " resolver/root "
2018-11-03 02:07:56 +03:00
2021-02-01 18:17:16 +03:00
include GemHelpers
2023-01-31 03:35:54 +03:00
def initialize ( base , gem_version_promoter )
@source_requirements = base . source_requirements
@base = base
2018-11-03 02:07:56 +03:00
@gem_version_promoter = gem_version_promoter
end
2023-01-31 03:35:54 +03:00
def start
@requirements = @base . requirements
@packages = @base . packages
2022-12-22 02:20:23 +03:00
root , logger = setup_solver
Bundler . ui . info " Resolving dependencies... " , true
solve_versions ( :root = > root , :logger = > logger )
end
def setup_solver
2022-11-12 00:00:58 +03:00
root = Resolver :: Root . new ( name_for_explicit_dependency_source )
root_version = Resolver :: Candidate . new ( 0 )
2022-12-09 08:45:51 +03:00
@all_specs = Hash . new do | specs , name |
2023-02-21 12:53:57 +03:00
specs [ name ] = source_for ( name ) . specs . search ( name ) . reject do | s |
s . dependencies . any? { | d | d . name == name && ! d . requirement . satisfied_by? ( s . version ) } # ignore versions that depend on themselves incorrectly
end . sort_by { | s | [ s . version , s . platform . to_s ] }
2022-12-09 08:45:51 +03:00
end
2022-11-12 00:00:58 +03:00
@sorted_versions = Hash . new do | candidates , package |
candidates [ package ] = if package . root?
[ root_version ]
else
all_versions_for ( package ) . sort
end
end
2022-12-22 02:20:23 +03:00
root_dependencies = prepare_dependencies ( @requirements , @packages )
2022-11-12 00:00:58 +03:00
@cached_dependencies = Hash . new do | dependencies , package |
dependencies [ package ] = if package . root?
{ root_version = > root_dependencies }
else
Hash . new do | versions , version |
2023-02-21 12:53:57 +03:00
versions [ version ] = to_dependency_hash ( version . dependencies . reject { | d | d . name == package . name } , @packages )
2022-11-12 00:00:58 +03:00
end
end
end
logger = Bundler :: UI :: Shell . new
logger . level = debug? ? " debug " : " warn "
2022-12-22 02:20:23 +03:00
[ root , logger ]
end
def solve_versions ( root : , logger : )
2022-11-12 00:00:58 +03:00
solver = PubGrub :: VersionSolver . new ( :source = > self , :root = > root , :logger = > logger )
result = solver . solve
result . map { | package , version | version . to_specs ( package ) } . flatten . uniq
rescue PubGrub :: SolveFailure = > e
incompatibility = e . incompatibility
2023-01-31 03:35:54 +03:00
names_to_unlock , names_to_allow_prereleases_for , extended_explanation = find_names_to_relax ( incompatibility )
2022-11-12 00:00:58 +03:00
2023-01-31 03:35:54 +03:00
names_to_relax = names_to_unlock + names_to_allow_prereleases_for
2022-08-22 05:52:51 +03:00
2023-01-31 03:35:54 +03:00
if names_to_relax . any?
if names_to_unlock . any?
Bundler . ui . debug " Found conflicts with locked dependencies. Will retry with #{ names_to_unlock . join ( " , " ) } unlocked... " , true
2022-09-05 03:15:30 +03:00
2023-01-31 03:35:54 +03:00
@base . unlock_names ( names_to_unlock )
2022-11-12 00:00:58 +03:00
end
2022-12-22 02:20:23 +03:00
2023-01-31 03:35:54 +03:00
if names_to_allow_prereleases_for . any?
Bundler . ui . debug " Found conflicts with dependencies with prereleases. Will retrying considering prereleases for #{ names_to_allow_prereleases_for . join ( " , " ) } ... " , true
2022-12-22 02:20:23 +03:00
2023-01-31 03:35:54 +03:00
@base . include_prereleases ( names_to_allow_prereleases_for )
end
2022-12-22 02:20:23 +03:00
root , logger = setup_solver
2023-01-31 03:35:54 +03:00
Bundler . ui . debug " Retrying resolution... " , true
2022-09-05 03:15:30 +03:00
retry
end
2022-11-12 00:00:58 +03:00
explanation = e . message
2022-12-09 08:45:51 +03:00
if extended_explanation
2022-11-12 00:00:58 +03:00
explanation << " \n \n "
2022-12-09 08:45:51 +03:00
explanation << extended_explanation
2022-11-12 00:00:58 +03:00
end
raise SolveFailure . new ( explanation )
end
2023-01-31 03:35:54 +03:00
def find_names_to_relax ( incompatibility )
names_to_unlock = [ ]
names_to_allow_prereleases_for = [ ]
extended_explanation = nil
while incompatibility . conflict?
cause = incompatibility . cause
incompatibility = cause . incompatibility
incompatibility . terms . each do | term |
package = term . package
name = package . name
if base_requirements [ name ]
names_to_unlock << name
elsif package . ignores_prereleases?
names_to_allow_prereleases_for << name
end
no_versions_incompat = [ cause . incompatibility , cause . satisfier ] . find { | incompat | incompat . cause . is_a? ( PubGrub :: Incompatibility :: NoVersions ) }
next unless no_versions_incompat
extended_explanation = no_versions_incompat . extended_explanation
end
end
[ names_to_unlock . uniq , names_to_allow_prereleases_for . uniq , extended_explanation ]
end
2022-11-12 00:00:58 +03:00
def parse_dependency ( package , dependency )
range = if repository_for ( package ) . is_a? ( Source :: Gemspec )
PubGrub :: VersionRange . any
else
requirement_to_range ( dependency )
end
PubGrub :: VersionConstraint . new ( package , :range = > range )
2018-11-03 02:07:56 +03:00
end
2022-11-12 00:00:58 +03:00
def versions_for ( package , range = VersionRange . any )
versions = range . select_versions ( @sorted_versions [ package ] )
sort_versions ( package , versions )
end
def no_versions_incompatibility_for ( package , unsatisfied_term )
cause = PubGrub :: Incompatibility :: NoVersions . new ( unsatisfied_term )
2022-12-09 08:45:51 +03:00
name = package . name
constraint = unsatisfied_term . constraint
2022-12-17 02:53:45 +03:00
constraint_string = constraint . constraint_string
2022-12-17 03:20:14 +03:00
requirements = constraint_string . split ( " OR " ) . map { | req | Gem :: Requirement . new ( req . split ( " , " ) ) }
2022-11-12 00:00:58 +03:00
2022-12-09 08:45:51 +03:00
if name == " bundler "
custom_explanation = " the current Bundler version ( #{ Bundler :: VERSION } ) does not satisfy #{ constraint } "
2022-12-17 03:20:14 +03:00
extended_explanation = bundler_not_found_message ( requirements )
2022-11-12 00:00:58 +03:00
else
2022-12-17 03:20:14 +03:00
specs_matching_other_platforms = filter_matching_specs ( @all_specs [ name ] , requirements )
2022-12-09 08:45:51 +03:00
platforms_explanation = specs_matching_other_platforms . any? ? " for any resolution platforms ( #{ package . platforms . join ( " , " ) } ) " : " "
custom_explanation = " #{ constraint } could not be found in #{ repository_for ( package ) } #{ platforms_explanation } "
2022-12-17 02:53:45 +03:00
label = " #{ name } ( #{ constraint_string } ) "
2022-12-09 08:45:51 +03:00
extended_explanation = other_specs_matching_message ( specs_matching_other_platforms , label ) if specs_matching_other_platforms . any?
2022-11-12 00:00:58 +03:00
end
2022-12-09 08:45:51 +03:00
Incompatibility . new ( [ unsatisfied_term ] , :cause = > cause , :custom_explanation = > custom_explanation , :extended_explanation = > extended_explanation )
2018-11-03 02:07:56 +03:00
end
def debug?
2022-11-12 00:00:58 +03:00
ENV [ " BUNDLER_DEBUG_RESOLVER " ] ||
2020-05-08 08:19:04 +03:00
ENV [ " BUNDLER_DEBUG_RESOLVER_TREE " ] ||
ENV [ " DEBUG_RESOLVER " ] ||
ENV [ " DEBUG_RESOLVER_TREE " ] ||
false
2018-11-03 02:07:56 +03:00
end
2022-11-12 00:00:58 +03:00
def incompatibilities_for ( package , version )
package_deps = @cached_dependencies [ package ]
sorted_versions = @sorted_versions [ package ]
package_deps [ version ] . map do | dep_package , dep_constraint |
low = high = sorted_versions . index ( version )
2018-11-03 02:07:56 +03:00
2022-11-12 00:00:58 +03:00
# find version low such that all >= low share the same dep
while low > 0 && package_deps [ sorted_versions [ low - 1 ] ] [ dep_package ] == dep_constraint
low -= 1
end
low =
if low == 0
nil
else
sorted_versions [ low ]
end
2018-11-03 02:07:56 +03:00
2022-11-12 00:00:58 +03:00
# find version high such that all < high share the same dep
while high < sorted_versions . length && package_deps [ sorted_versions [ high ] ] [ dep_package ] == dep_constraint
high += 1
end
high =
if high == sorted_versions . length
nil
else
sorted_versions [ high ]
2022-10-18 09:24:42 +03:00
end
2021-02-01 18:17:16 +03:00
2022-11-12 00:00:58 +03:00
range = PubGrub :: VersionRange . new ( :min = > low , :max = > high , :include_min = > true )
2021-02-01 18:17:16 +03:00
2022-11-12 00:00:58 +03:00
self_constraint = PubGrub :: VersionConstraint . new ( package , :range = > range )
2021-02-01 18:17:16 +03:00
2022-11-12 00:00:58 +03:00
dep_term = PubGrub :: Term . new ( dep_constraint , false )
2022-12-13 18:31:13 +03:00
self_term = PubGrub :: Term . new ( self_constraint , true )
2021-02-01 18:17:16 +03:00
2022-11-12 00:00:58 +03:00
custom_explanation = if dep_package . meta? && package . root?
" current #{ dep_package } version is #{ dep_constraint . constraint_string } "
2018-11-03 02:07:56 +03:00
end
2022-11-12 00:00:58 +03:00
2022-12-13 18:31:13 +03:00
PubGrub :: Incompatibility . new ( [ self_term , dep_term ] , :cause = > :dependency , :custom_explanation = > custom_explanation )
2022-11-12 00:00:58 +03:00
end
end
def all_versions_for ( package )
name = package . name
2023-01-31 03:35:54 +03:00
results = ( @base [ name ] + filter_prereleases ( @all_specs [ name ] , package ) ) . uniq { | spec | [ spec . version . hash , spec . platform ] }
2022-11-12 00:00:58 +03:00
locked_requirement = base_requirements [ name ]
2022-12-09 08:45:51 +03:00
results = filter_matching_specs ( results , locked_requirement ) if locked_requirement
2022-11-12 00:00:58 +03:00
versions = results . group_by ( & :version ) . reduce ( [ ] ) do | groups , ( version , specs ) |
platform_specs = package . platforms . flat_map { | platform | select_best_platform_match ( specs , platform ) }
next groups if platform_specs . empty?
ruby_specs = select_best_platform_match ( specs , Gem :: Platform :: RUBY )
groups << Resolver :: Candidate . new ( version , :specs = > ruby_specs ) if ruby_specs . any?
next groups if platform_specs == ruby_specs
groups << Resolver :: Candidate . new ( version , :specs = > platform_specs )
groups
2021-02-01 18:17:16 +03:00
end
2022-11-12 00:00:58 +03:00
sort_versions ( package , versions )
2018-11-03 02:07:56 +03:00
end
2021-05-28 13:47:49 +03:00
def source_for ( name )
@source_requirements [ name ] || @source_requirements [ :default ]
2018-11-03 02:07:56 +03:00
end
def name_for_explicit_dependency_source
Bundler . default_gemfile . basename . to_s
2019-04-14 09:01:35 +03:00
rescue StandardError
2018-11-03 02:07:56 +03:00
" Gemfile "
end
2022-12-09 08:45:51 +03:00
def raise_not_found! ( package )
name = package . name
source = source_for ( name )
specs = @all_specs [ name ]
matching_part = name
requirement_label = SharedHelpers . pretty_dependency ( package . dependency )
cache_message = begin
" or in gems cached in #{ Bundler . settings . app_cache_path } " if Bundler . app_cache . exist?
rescue GemfileNotFound
nil
end
specs_matching_requirement = filter_matching_specs ( specs , package . dependency . requirement )
if specs_matching_requirement . any?
specs = specs_matching_requirement
matching_part = requirement_label
platforms = package . platforms
platform_label = platforms . size == 1 ? " platform ' #{ platforms . first } " : " platforms ' #{ platforms . join ( " ', ' " ) } "
requirement_label = " #{ requirement_label } ' with #{ platform_label } "
end
message = String . new ( " Could not find gem ' #{ requirement_label } ' in #{ source } #{ cache_message } . \n " )
if specs . any?
message << " \n #{ other_specs_matching_message ( specs , matching_part ) } "
end
raise GemNotFound , message
2018-11-03 02:07:56 +03:00
end
2022-11-12 00:00:58 +03:00
private
2022-12-17 03:20:14 +03:00
def filter_matching_specs ( specs , requirements )
Array ( requirements ) . flat_map do | requirement |
specs . select { | spec | requirement_satisfied_by? ( requirement , spec ) }
end
2022-12-09 08:45:51 +03:00
end
2023-01-31 03:35:54 +03:00
def filter_prereleases ( specs , package )
return specs unless package . ignores_prereleases?
specs . reject { | s | s . version . prerelease? }
end
2022-12-09 08:45:51 +03:00
def requirement_satisfied_by? ( requirement , spec )
requirement . satisfied_by? ( spec . version ) || spec . source . is_a? ( Source :: Gemspec )
end
2022-11-12 00:00:58 +03:00
def sort_versions ( package , versions )
if versions . size > 1
@gem_version_promoter . sort_versions ( package , versions ) . reverse
else
versions
2018-11-03 02:07:56 +03:00
end
end
2022-11-12 00:00:58 +03:00
def repository_for ( package )
source_for ( package . name )
end
2018-11-03 02:07:56 +03:00
2022-09-05 03:15:30 +03:00
def base_requirements
@base . base_requirements
end
2022-11-12 00:00:58 +03:00
def prepare_dependencies ( requirements , packages )
to_dependency_hash ( requirements , packages ) . map do | dep_package , dep_constraint |
name = dep_package . name
2022-12-24 22:42:50 +03:00
2023-01-31 03:35:54 +03:00
next [ dep_package , dep_constraint ] if name == " bundler "
versions = versions_for ( dep_package , dep_constraint . range )
if versions . empty? && dep_package . ignores_prereleases?
@sorted_versions . delete ( dep_package )
dep_package . consider_prereleases!
versions = versions_for ( dep_package , dep_constraint . range )
2022-12-24 22:42:50 +03:00
end
2023-01-31 03:35:54 +03:00
next [ dep_package , dep_constraint ] unless versions . empty?
2022-12-24 22:42:50 +03:00
2022-11-12 00:00:58 +03:00
next unless dep_package . current_platform?
2018-11-03 02:07:56 +03:00
2022-12-09 08:45:51 +03:00
raise_not_found! ( dep_package )
2022-11-12 00:00:58 +03:00
end . compact . to_h
2018-11-03 02:07:56 +03:00
end
2022-12-09 08:45:51 +03:00
def other_specs_matching_message ( specs , requirement )
message = String . new ( " The source contains the following gems matching ' #{ requirement } ': \n " )
message << specs . map { | s | " * #{ s . full_name } " } . join ( " \n " )
2021-12-15 14:41:28 +03:00
message
end
2022-11-12 00:00:58 +03:00
def requirement_to_range ( requirement )
ranges = requirement . requirements . map do | ( op , version ) |
2023-01-10 07:53:41 +03:00
ver = Resolver :: Candidate . new ( version ) . generic!
platform_ver = Resolver :: Candidate . new ( version ) . platform_specific!
2022-11-12 00:00:58 +03:00
case op
when " ~> "
name = " ~> #{ ver } "
bump = Resolver :: Candidate . new ( version . bump . to_s + " .A " )
PubGrub :: VersionRange . new ( :name = > name , :min = > ver , :max = > bump , :include_min = > true )
when " > "
2023-01-10 07:53:41 +03:00
PubGrub :: VersionRange . new ( :min = > platform_ver )
2022-11-12 00:00:58 +03:00
when " >= "
PubGrub :: VersionRange . new ( :min = > ver , :include_min = > true )
when " < "
PubGrub :: VersionRange . new ( :max = > ver )
when " <= "
2023-01-10 07:53:41 +03:00
PubGrub :: VersionRange . new ( :max = > platform_ver , :include_max = > true )
2022-11-12 00:00:58 +03:00
when " = "
2023-01-10 07:53:41 +03:00
PubGrub :: VersionRange . new ( :min = > ver , :max = > platform_ver , :include_min = > true , :include_max = > true )
2022-11-12 00:00:58 +03:00
when " != "
2023-01-10 07:53:41 +03:00
PubGrub :: VersionRange . new ( :min = > ver , :max = > platform_ver , :include_min = > true , :include_max = > true ) . invert
2022-11-12 00:00:58 +03:00
else
raise " bad version specifier: #{ op } "
2021-02-01 18:17:16 +03:00
end
2019-04-14 09:01:35 +03:00
end
2021-02-01 18:17:16 +03:00
2022-11-12 00:00:58 +03:00
ranges . inject ( & :intersect )
end
2022-04-28 11:15:43 +03:00
2022-11-12 00:00:58 +03:00
def to_dependency_hash ( dependencies , packages )
dependencies . inject ( { } ) do | deps , dep |
package = packages [ dep . name ]
2022-04-28 11:15:43 +03:00
2022-11-12 00:00:58 +03:00
current_req = deps [ package ]
new_req = parse_dependency ( package , dep . requirement )
2022-04-28 11:15:43 +03:00
2022-11-12 00:00:58 +03:00
deps [ package ] = if current_req
current_req . intersect ( new_req )
else
new_req
end
2022-04-28 11:15:43 +03:00
2022-11-12 00:00:58 +03:00
deps
end
end
2022-04-28 11:15:43 +03:00
2022-12-17 03:20:14 +03:00
def bundler_not_found_message ( conflict_dependencies )
candidate_specs = filter_matching_specs ( source_for ( :default_bundler ) . specs . search ( " bundler " ) , conflict_dependencies )
2022-11-12 00:00:58 +03:00
if candidate_specs . any?
target_version = candidate_specs . last . version
new_command = [ File . basename ( $PROGRAM_NAME ) , " _ #{ target_version } _ " , * ARGV ] . join ( " " )
" Your bundle requires a different version of Bundler than the one you're running. \n " \
" Install the necessary version with `gem install bundler: #{ target_version } ` and rerun bundler using ` #{ new_command } ` \n "
else
" Your bundle requires a different version of Bundler than the one you're running, and that version could not be found. \n "
end
2018-11-03 02:07:56 +03:00
end
end
end