pub_grub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ require 'rubygems/requirement'
2
+
3
+ module PubGrub
4
+ class VersionConstraint
5
+ attr_reader :package, :constraint
6
+
7
+ # @param package [PubGrub::Package]
8
+ # @param constraint [String]
9
+ def initialize(package, constraint = nil, bitmap: nil)
10
+ @package = package
11
+ @constraint = Array(constraint)
12
+ @bitmap = bitmap # Calculated lazily
13
+ end
14
+
15
+ def self.exact(version)
16
+ package = version.package
17
+ new(package, version.name, bitmap: bitmap_matching(package) { |v| v == version })
18
+ end
19
+
20
+ def self.any(package)
21
+ new(package, nil, bitmap: (1 << package.versions.count) - 1)
22
+ end
23
+
24
+ def self.bitmap_matching(package)
25
+ package.versions.select do |version|
26
+ yield version
27
+ end.inject(0) do |acc, version|
28
+ acc | (1 << version.id)
29
+ end
30
+ end
31
+
32
+ def bitmap
33
+ return @bitmap if @bitmap
34
+
35
+ # TODO: Should not be hardcoded to rubygems semantics
36
+ requirement = Gem::Requirement.new(constraint)
37
+ @bitmap = self.class.bitmap_matching(package) do |version|
38
+ requirement.satisfied_by?(Gem::Version.new(version.name))
39
+ end
40
+ end
41
+
42
+ def intersect(other)
43
+ unless package == other.package
44
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
45
+ end
46
+ if bitmap == other.bitmap
47
+ self
48
+ else
49
+ self.class.new(package, constraint + other.constraint, bitmap: bitmap & other.bitmap)
50
+ end
51
+ end
52
+
53
+ def union(other)
54
+ unless package == other.package
55
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
56
+ end
57
+ if bitmap == other.bitmap
58
+ self
59
+ else
60
+ self.class.new(package, "#{constraint_string} OR #{other.constraint_string}", bitmap: bitmap | other.bitmap)
61
+ end
62
+ end
63
+
64
+ def invert
65
+ new_bitmap = bitmap ^ ((1 << package.versions.length) - 1)
66
+ new_constraint =
67
+ if constraint.length == 0
68
+ ["not >= 0"]
69
+ elsif constraint.length == 1
70
+ ["not #{constraint[0]}"]
71
+ else
72
+ ["not (#{constraint_string})"]
73
+ end
74
+ self.class.new(package, new_constraint, bitmap: new_bitmap)
75
+ end
76
+
77
+ def difference(other)
78
+ intersect(other.invert)
79
+ end
80
+
81
+ def versions
82
+ package.versions.select do |version|
83
+ bitmap[version.id] == 1
84
+ end
85
+ end
86
+
87
+ if RUBY_VERSION >= "2.5"
88
+ def allows_all?(other)
89
+ bitmap.allbits?(other.bitmap)
90
+ end
91
+
92
+ def allows_any?(other)
93
+ bitmap.anybits?(other.bitmap)
94
+ end
95
+ else
96
+ def allows_all?(other)
97
+ other_bitmap = other.bitmap
98
+ (bitmap & other_bitmap) == other_bitmap
99
+ end
100
+
101
+ def allows_any?(other)
102
+ (bitmap & other.bitmap) != 0
103
+ end
104
+ end
105
+
106
+ def subset?(other)
107
+ other.allows_all?(self)
108
+ end
109
+
110
+ def overlap?(other)
111
+ other.allows_any?(self)
112
+ end
113
+
114
+ def disjoint?(other)
115
+ !overlap?(other)
116
+ end
117
+
118
+ def relation(other)
119
+ if subset?(other)
120
+ :subset
121
+ elsif overlap?(other)
122
+ :overlap
123
+ else
124
+ :disjoint
125
+ end
126
+ end
127
+
128
+ def to_s(allow_every: false)
129
+ if package == Package.root
130
+ "root"
131
+ elsif allow_every && any?
132
+ "every version of #{package.name}"
133
+ else
134
+ "#{package.name} #{constraint_string}"
135
+ end
136
+ end
137
+
138
+ def constraint_string
139
+ case constraint.length
140
+ when 0
141
+ ">= 0"
142
+ when 1
143
+ "#{constraint[0]}"
144
+ else
145
+ "#{constraint.join(", ")}"
146
+ end
147
+ end
148
+
149
+ def empty?
150
+ bitmap == 0
151
+ end
152
+
153
+ # Does this match every version of the package
154
+ def any?
155
+ bitmap == self.class.any(package).bitmap
156
+ end
157
+
158
+ def inspect
159
+ "#<#{self.class} #{self} (#{bitmap.to_s(2).rjust(package.versions.count, "0")})>"
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,209 @@
1
+ require 'pub_grub/partial_solution'
2
+ require 'pub_grub/term'
3
+ require 'pub_grub/incompatibility'
4
+ require 'pub_grub/solve_failure'
5
+
6
+ module PubGrub
7
+ class VersionSolver
8
+ attr_reader :source
9
+ attr_reader :solution
10
+
11
+ def initialize(source:)
12
+ @source = source
13
+
14
+ # { package => [incompatibility, ...]}
15
+ @incompatibilities = Hash.new do |h, k|
16
+ h[k] = []
17
+ end
18
+
19
+ @solution = PartialSolution.new
20
+
21
+ add_incompatibility Incompatibility.new([
22
+ Term.new(VersionConstraint.any(Package.root), false)
23
+ ], cause: :root)
24
+ end
25
+
26
+ def solve
27
+ next_package = Package.root
28
+
29
+ while next_package
30
+ propagate(next_package)
31
+
32
+ next_package = choose_package_version
33
+ end
34
+
35
+ result = solution.decisions.values
36
+
37
+ logger.info "Solution found after #{solution.attempted_solutions} attempts:"
38
+ result.each do |version|
39
+ logger.info "* #{version.package.name} #{version}"
40
+ end
41
+
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ def propagate(initial_package)
48
+ changed = [initial_package]
49
+ while package = changed.shift
50
+ @incompatibilities[package].reverse_each do |incompatibility|
51
+ result = propagate_incompatibility(incompatibility)
52
+ if result == :conflict
53
+ root_cause = resolve_conflict(incompatibility)
54
+ changed.clear
55
+ changed << propagate_incompatibility(root_cause)
56
+ elsif result # should be a Package
57
+ changed << result
58
+ end
59
+ end
60
+ changed.uniq!
61
+ end
62
+ end
63
+
64
+ def propagate_incompatibility(incompatibility)
65
+ unsatisfied = nil
66
+ incompatibility.terms.each do |term|
67
+ relation = solution.relation(term)
68
+ if relation == :disjoint
69
+ return nil
70
+ elsif relation == :overlap
71
+ # If more than one term is inconclusive, we can't deduce anything
72
+ return nil if unsatisfied
73
+ unsatisfied = term
74
+ end
75
+ end
76
+
77
+ if !unsatisfied
78
+ return :conflict
79
+ end
80
+
81
+ logger.debug("derived: #{unsatisfied.invert}")
82
+
83
+ solution.derive(unsatisfied.invert, incompatibility)
84
+
85
+ unsatisfied.package
86
+ end
87
+
88
+ def choose_package_version
89
+ unsatisfied = solution.unsatisfied
90
+
91
+ if unsatisfied.empty?
92
+ logger.info "No packages unsatisfied. Solving complete!"
93
+ return nil
94
+ end
95
+
96
+ unsatisfied_term = unsatisfied.min_by do |term|
97
+ term.versions.count
98
+ end
99
+ version = unsatisfied_term.versions.first
100
+
101
+ if version.nil?
102
+ raise "required term with no versions: #{unsatisfied_term}"
103
+ end
104
+
105
+ conflict = false
106
+
107
+ source.incompatibilities_for(version).each do |incompatibility|
108
+ add_incompatibility incompatibility
109
+
110
+ conflict ||= incompatibility.terms.all? do |term|
111
+ term.package == version.package || solution.satisfies?(term)
112
+ end
113
+ end
114
+
115
+ unless conflict
116
+ logger.info("selecting #{version.package.name} #{version}")
117
+
118
+ solution.decide(version)
119
+ end
120
+
121
+ version.package
122
+ end
123
+
124
+ def resolve_conflict(incompatibility)
125
+ logger.info "conflict: #{incompatibility}"
126
+
127
+ new_incompatibility = false
128
+
129
+ while !incompatibility.failure?
130
+ most_recent_term = nil
131
+ most_recent_satisfier = nil
132
+ difference = nil
133
+
134
+ previous_level = 1
135
+
136
+ incompatibility.terms.each do |term|
137
+ satisfier = solution.satisfier(term)
138
+
139
+ if most_recent_satisfier.nil?
140
+ most_recent_term = term
141
+ most_recent_satisfier = satisfier
142
+ elsif most_recent_satisfier.index < satisfier.index
143
+ previous_level = [previous_level, most_recent_satisfier.decision_level].max
144
+ most_recent_term = term
145
+ most_recent_satisfier = satisfier
146
+ difference = nil
147
+ else
148
+ previous_level = [previous_level, satisfier.decision_level].max
149
+ end
150
+
151
+ if most_recent_term == term
152
+ difference = most_recent_satisfier.term.difference(most_recent_term)
153
+ if difference.empty?
154
+ difference = nil
155
+ else
156
+ difference_satisfier = solution.satisfier(difference.inverse)
157
+ previous_level = [previous_level, difference_satisfier.decision_level].max
158
+ end
159
+ end
160
+ end
161
+
162
+ if previous_level < most_recent_satisfier.decision_level ||
163
+ most_recent_satisfier.decision?
164
+
165
+ logger.info "backtracking to #{previous_level}"
166
+ solution.backtrack(previous_level)
167
+
168
+ if new_incompatibility
169
+ add_incompatibility(incompatibility)
170
+ end
171
+
172
+ return incompatibility
173
+ end
174
+
175
+ new_terms = []
176
+ new_terms += incompatibility.terms - [most_recent_term]
177
+ new_terms += most_recent_satisfier.cause.terms.reject { |term|
178
+ term.package == most_recent_satisfier.term.package
179
+ }
180
+ if difference
181
+ new_terms << difference.invert
182
+ end
183
+
184
+ incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))
185
+
186
+ new_incompatibility = true
187
+
188
+ partially = difference ? " partially" : ""
189
+ logger.info "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}"
190
+ logger.info "! which is caused by #{most_recent_satisfier.cause}"
191
+ logger.info "! thus #{incompatibility}"
192
+ end
193
+
194
+ raise SolveFailure.new(incompatibility)
195
+ end
196
+
197
+ def add_incompatibility(incompatibility)
198
+ logger.debug("fact: #{incompatibility}");
199
+ incompatibility.terms.each do |term|
200
+ package = term.package
201
+ @incompatibilities[package] << incompatibility
202
+ end
203
+ end
204
+
205
+ def logger
206
+ PubGrub.logger
207
+ end
208
+ end
209
+ end
data/pub_grub.gemspec ADDED
@@ -0,0 +1,26 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pub_grub"
7
+ spec.version = "0.1.0"
8
+ spec.authors = ["John Hawthorn"]
9
+ spec.email = ["john@hawthorn.email"]
10
+
11
+ spec.summary = %q{A version solver based on dart's PubGrub}
12
+ spec.description = %q{A version solver based on dart's PubGrub}
13
+ spec.homepage = "https://github.com/jhawthorn/pubgrub"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.16"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "minitest", "~> 5.0"
26
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pub_grub
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - John Hawthorn
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description: A version solver based on dart's PubGrub
56
+ email:
57
+ - john@hawthorn.email
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - CODE_OF_CONDUCT.md
65
+ - Gemfile
66
+ - Gemfile.lock
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - lib/pub_grub.rb
73
+ - lib/pub_grub/assignment.rb
74
+ - lib/pub_grub/failure_writer.rb
75
+ - lib/pub_grub/incompatibility.rb
76
+ - lib/pub_grub/package.rb
77
+ - lib/pub_grub/partial_solution.rb
78
+ - lib/pub_grub/solve_failure.rb
79
+ - lib/pub_grub/static_package_source.rb
80
+ - lib/pub_grub/term.rb
81
+ - lib/pub_grub/version_constraint.rb
82
+ - lib/pub_grub/version_solver.rb
83
+ - pub_grub.gemspec
84
+ homepage: https://github.com/jhawthorn/pubgrub
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.7.3
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: A version solver based on dart's PubGrub
108
+ test_files: []