File: implementation.rb

package info (click to toggle)
ruby-librarian 1.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 624 kB
  • sloc: ruby: 6,109; makefile: 11
file content (245 lines) | stat: -rw-r--r-- 7,503 bytes parent folder | download | duplicates (6)
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
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
require 'set'

require 'librarian/algorithms'
require 'librarian/dependency'

module Librarian
  class Resolver
    class Implementation

      class MultiSource
        attr_accessor :sources
        def initialize(sources)
          self.sources = sources
        end
        def manifests(name)
          sources.reverse.map{|source| source.manifests(name)}.flatten(1).compact
        end
        def to_s
          "(no source specified)"
        end
      end

      class State
        attr_accessor :manifests, :dependencies, :queue
        private :manifests=, :dependencies=, :queue=
        def initialize(manifests, dependencies, queue)
          self.manifests = manifests
          self.dependencies = dependencies # resolved
          self.queue = queue # scheduled
        end
      end

      attr_accessor :resolver, :spec, :cyclic
      private :resolver=, :spec=, :cyclic=

      def initialize(resolver, spec, options = { })
        unrecognized_options = options.keys - [:cyclic]
        unrecognized_options.empty? or raise Error,
          "unrecognized options: #{unrecognized_options.join(", ")}"
        self.resolver = resolver
        self.spec = spec
        self.cyclic = !!options[:cyclic]
        @level = 0
      end

      def resolve(manifests)
        manifests = index_by(manifests, &:name) if manifests.kind_of?(Array)
        queue = spec.dependencies + sourced_dependencies_for_manifests(manifests)
        state = State.new(manifests.dup, [], queue)
        do_resolve(state)
      end

    private

      def do_resolve(state)
        stack = [state]
        while !stack.empty? do
          state = stack.pop
          shift_resolved_enqueued_dependencies(state) or return
          state.queue.empty? and return state.manifests

          state.dependencies << state.queue.shift
          dependency = state.dependencies.last

          resolving_dependency_map_find_manifests(dependency) do |manifest|
            check_manifest(state, manifest) or next
            manifest.exclude_dependencies!(spec.exclusions).each do |d|
              debug { "Excluding dependency #{d.name} from #{manifest.name}" }
            end
            check_manifest_for_cycles(state, manifest) or next unless cyclic

            m = state.manifests.merge(dependency.name => manifest)
            a = sourced_dependencies_for_manifest(manifest)
            s = State.new(m, state.dependencies.dup, state.queue + a)

            stack.push(s)
          end
        end
      end

      def find_inconsistency(state, dependency)
        m = state.manifests[dependency.name]
        dependency.satisfied_by?(m) or return m if m
        violation = lambda{|d| !dependency.consistent_with?(d)}
        state.dependencies.find(&violation) || state.queue.find(&violation)
      end

      # When using this method, you are required to check the return value.
      # Returns +true+ if the resolved enqueued dependencies at the front of the
      # queue could all be moved to the resolved dependencies list.
      # Returns +false+ if there was an inconsistency when trying to move one or
      # more of them.
      # This modifies +queue+ and +dependencies+.
      def shift_resolved_enqueued_dependencies(state)
        while (d = state.queue.first) && state.manifests[d.name]
          if q = find_inconsistency(state, d)
            debug_conflict d, q
            return false
          end
          state.dependencies << state.queue.shift
        end
        true
      end

      # When using this method, you are required to check the return value.
      # Returns +true+ if the manifest satisfies all of the dependencies.
      # Returns +false+ if there was a dependency that the manifest does not
      # satisfy.
      def check_manifest(state, manifest)
        violation = lambda{|d| d.name == manifest.name && !d.satisfied_by?(manifest)}
        if q = state.dependencies.find(&violation) || state.queue.find(&violation)
          debug_conflict manifest, q
          return false
        end
        true
      end

      # When using this method, you are required to check the return value.
      # Returns +true+ if the manifest does not introduce a cycle.
      # Returns +false+ if the manifest introduces a cycle.
      def check_manifest_for_cycles(state, manifest)
        manifests = state.manifests.merge(manifest.name => manifest)
        known = manifests.keys
        graph = Hash[manifests.map{|n, m| [n, m.dependencies.map(&:name) & known]}]
        if Algorithms::AdjacencyListDirectedGraph.cyclic?(graph)
          debug_cycle manifest
          return false
        end
        true
      end

      def default_source
        @default_source ||= MultiSource.new(spec.sources)
      end

      def dependency_source_map
        @dependency_source_map ||=
          Hash[spec.dependencies.map{|d| [d.name, d.source]}]
      end

      def sourced_dependency_for(dependency)
        return dependency if dependency.source

        source = dependency_source_map[dependency.name] || default_source
        dependency.class.new(dependency.name, dependency.requirement, source)
      end

      def sourced_dependencies_for_manifest(manifest)
        manifest.dependencies.map{|d| sourced_dependency_for(d)}
      end

      def sourced_dependencies_for_manifests(manifests)
        manifests = manifests.values if manifests.kind_of?(Hash)
        manifests.map{|m| sourced_dependencies_for_manifest(m)}.flatten(1)
      end

      def resolving_dependency_map_find_manifests(dependency)
        scope_resolving_dependency dependency do
          map_find(dependency.manifests) do |manifest|
            scope_checking_manifest dependency, manifest do
              yield manifest
            end
          end
        end
      end

      def scope_resolving_dependency(dependency)
        debug { "Resolving #{dependency}" }
        resolution = nil
        scope do
          scope_checking_manifests do
            resolution = yield
          end
          if resolution
            debug { "Resolved #{dependency}" }
          else
            debug { "Failed to resolve #{dependency}" }
          end
        end
        resolution
      end

      def scope_checking_manifests
        debug { "Checking manifests" }
        scope do
          yield
        end
      end

      def scope_checking_manifest(dependency, manifest)
        debug { "Checking #{manifest}" }
        resolution = nil
        scope do
          resolution = yield
          if resolution
            debug { "Resolved #{dependency} at #{manifest}" }
          else
            debug { "Backtracking from #{manifest}" }
          end
        end
        resolution
      end

      def debug_schedule(dependency)
        debug { "Scheduling #{dependency}" }
      end

      def debug_conflict(dependency, conflict)
        debug { "Conflict between #{dependency} and #{conflict}" }
      end

      def debug_cycle(manifest)
        debug { "Cycle with #{manifest}" }
      end

      def map_find(enum)
        enum.each do |obj|
          res = yield(obj)
          res.nil? or return res
        end
        nil
      end

      def index_by(enum)
        Hash[enum.map{|obj| [yield(obj), obj]}]
      end

      def scope
        @level += 1
        yield
      ensure
        @level -= 1
      end

      def debug
        environment.logger.debug { '  ' * @level + yield }
      end

      def environment
        resolver.environment
      end

    end
  end
end