summaryrefslogtreecommitdiff
path: root/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
blob: dce20d37ad9eae8df14b0f7d6b98ba9a2cc697d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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
require_relative 'version_constraint'
require_relative 'incompatibility'

module Bundler::PubGrub
  # Types:
  #
  # Where possible, Bundler::PubGrub will accept user-defined types, so long as they quack.
  #
  # ## "Package":
  #
  # This class will be used to represent the various packages being solved for.
  # .to_s will be called when displaying errors and debugging info, it should
  # probably return the package's name.
  # It must also have a reasonable definition of #== and #hash
  #
  # Example classes: String ("rails")
  #
  #
  # ## "Version":
  #
  # This class will be used to represent a single version number.
  #
  # Versions don't need to store their associated package, however they will
  # only be compared against other versions of the same package.
  #
  # It must be Comparible (and implement <=> reasonably)
  #
  # Example classes: Gem::Version, Integer
  #
  #
  # ## "Dependency"
  #
  # This class represents the requirement one package has on another. It is
  # returned by dependencies_for(package, version) and will be passed to
  # parse_dependency to convert it to a format Bundler::PubGrub understands.
  #
  # It must also have a reasonable definition of #==
  #
  # Example classes: String ("~> 1.0"), Gem::Requirement
  #
  class BasicPackageSource
    # Override me!
    #
    # This is called per package to find all possible versions of a package.
    #
    # It is called at most once per-package
    #
    # Returns: Array of versions for a package, in preferred order of selection
    def all_versions_for(package)
      raise NotImplementedError
    end

    # Override me!
    #
    # Returns: Hash in the form of { package => requirement, ... }
    def dependencies_for(package, version)
      raise NotImplementedError
    end

    # Override me!
    #
    # Convert a (user-defined) dependency into a format Bundler::PubGrub understands.
    #
    # Package is passed to this method but for many implementations is not
    # needed.
    #
    # Returns: either a Bundler::PubGrub::VersionRange, Bundler::PubGrub::VersionUnion, or a
    #   Bundler::PubGrub::VersionConstraint
    def parse_dependency(package, dependency)
      raise NotImplementedError
    end

    # Override me!
    #
    # If not overridden, this will call dependencies_for with the root package.
    #
    # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for)
    def root_dependencies
      dependencies_for(@root_package, @root_version)
    end

    # Override me (maybe)
    #
    # If not overridden, the order returned by all_versions_for will be used
    #
    # Returns: Array of versions in preferred order
    def sort_versions_by_preferred(package, sorted_versions)
      indexes = @version_indexes[package]
      sorted_versions.sort_by { |version| indexes[version] }
    end

    def initialize
      @root_package = Package.root
      @root_version = Package.root_version

      @cached_versions = Hash.new do |h,k|
        if k == @root_package
          h[k] = [@root_version]
        else
          h[k] = all_versions_for(k)
        end
      end
      @sorted_versions = Hash.new { |h,k| h[k] = @cached_versions[k].sort }
      @version_indexes = Hash.new { |h,k| h[k] = @cached_versions[k].each.with_index.to_h }

      @cached_dependencies = Hash.new do |packages, package|
        if package == @root_package
          packages[package] = {
            @root_version => root_dependencies
          }
        else
          packages[package] = Hash.new do |versions, version|
            versions[version] = dependencies_for(package, version)
          end
        end
      end
    end

    def versions_for(package, range=VersionRange.any)
      versions = range.select_versions(@sorted_versions[package])

      # Conditional avoids (among other things) calling
      # sort_versions_by_preferred with the root package
      if versions.size > 1
        sort_versions_by_preferred(package, versions)
      else
        versions
      end
    end

    def no_versions_incompatibility_for(_package, unsatisfied_term)
      cause = Incompatibility::NoVersions.new(unsatisfied_term)

      Incompatibility.new([unsatisfied_term], cause: cause)
    end

    def incompatibilities_for(package, version)
      package_deps = @cached_dependencies[package]
      sorted_versions = @sorted_versions[package]
      package_deps[version].map do |dep_package, dep_constraint_name|
        low = high = sorted_versions.index(version)

        # find version low such that all >= low share the same dep
        while low > 0 &&
            package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name
          low -= 1
        end
        low =
          if low == 0
            nil
          else
            sorted_versions[low]
          end

        # 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_name
          high += 1
        end
        high =
          if high == sorted_versions.length
            nil
          else
            sorted_versions[high]
          end

        range = VersionRange.new(min: low, max: high, include_min: true)

        self_constraint = VersionConstraint.new(package, range: range)

        if !@packages.include?(dep_package)
          # no such package -> this version is invalid
        end

        dep_constraint = parse_dependency(dep_package, dep_constraint_name)
        if !dep_constraint
          # falsey indicates this dependency was invalid
          cause = Bundler::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name)
          return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)]
        elsif !dep_constraint.is_a?(VersionConstraint)
          # Upgrade range/union to VersionConstraint
          dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint)
        end

        Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency)
      end
    end
  end
end