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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +28 -0
- data/Rakefile +10 -0
- data/bin/console +24 -0
- data/bin/setup +8 -0
- data/lib/pub_grub.rb +18 -0
- data/lib/pub_grub/assignment.rb +20 -0
- data/lib/pub_grub/failure_writer.rb +179 -0
- data/lib/pub_grub/incompatibility.rb +109 -0
- data/lib/pub_grub/package.rb +79 -0
- data/lib/pub_grub/partial_solution.rb +125 -0
- data/lib/pub_grub/solve_failure.rb +13 -0
- data/lib/pub_grub/static_package_source.rb +83 -0
- data/lib/pub_grub/term.rb +104 -0
- data/lib/pub_grub/version_constraint.rb +162 -0
- data/lib/pub_grub/version_solver.rb +209 -0
- data/pub_grub.gemspec +26 -0
- metadata +108 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module PubGrub
|
|
2
|
+
class Incompatibility
|
|
3
|
+
ConflictCause = Struct.new(:incompatibility, :satisfier) do
|
|
4
|
+
alias_method :conflict, :incompatibility
|
|
5
|
+
alias_method :other, :satisfier
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_reader :terms, :cause
|
|
9
|
+
|
|
10
|
+
def initialize(terms, cause:)
|
|
11
|
+
@cause = cause
|
|
12
|
+
@terms = cleanup_terms(terms)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def failure?
|
|
16
|
+
terms.empty? || (terms.length == 1 && terms[0].package == Package.root && terms[0].positive?)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def conflict?
|
|
20
|
+
ConflictCause === cause
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns all external incompatibilities in this incompatibility's
|
|
24
|
+
# derivation graph
|
|
25
|
+
def external_incompatibilities
|
|
26
|
+
if conflict?
|
|
27
|
+
[
|
|
28
|
+
cause.conflict,
|
|
29
|
+
cause.other
|
|
30
|
+
].flat_map(&:external_incompatibilities)
|
|
31
|
+
else
|
|
32
|
+
[this]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
case cause
|
|
38
|
+
when :dependency
|
|
39
|
+
raise unless terms.length == 2
|
|
40
|
+
"#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}"
|
|
41
|
+
else
|
|
42
|
+
if failure?
|
|
43
|
+
"version solving has failed"
|
|
44
|
+
elsif terms.length == 1
|
|
45
|
+
term = terms[0]
|
|
46
|
+
if term.positive?
|
|
47
|
+
"#{terms[0].to_s(allow_every: true)} is forbidden"
|
|
48
|
+
else
|
|
49
|
+
"#{terms[0].invert} is required"
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
if terms.all?(&:positive?)
|
|
53
|
+
if terms.length == 2
|
|
54
|
+
"#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}"
|
|
55
|
+
else
|
|
56
|
+
"one of #{terms.map(&:to_s).join(" or ")} must be false"
|
|
57
|
+
end
|
|
58
|
+
elsif terms.all?(&:negative?)
|
|
59
|
+
if terms.length == 2
|
|
60
|
+
"either #{terms[0].invert} or #{terms[1].invert}"
|
|
61
|
+
else
|
|
62
|
+
"one of #{terms.map(&:invert).join(" or ")} must be true";
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
positive = terms.select(&:positive?)
|
|
66
|
+
negative = terms.select(&:negative?).map(&:invert)
|
|
67
|
+
|
|
68
|
+
if positive.length == 1
|
|
69
|
+
"#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}"
|
|
70
|
+
else
|
|
71
|
+
"if #{positive.join(" and ")} then #{negative.join(" or ")}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def inspect
|
|
79
|
+
"#<#{self.class} #{to_s}>"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def cleanup_terms(terms)
|
|
85
|
+
terms.each do |term|
|
|
86
|
+
raise "#{term.inspect} must be a term" unless term.is_a?(Term)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if terms.length != 1 && ConflictCause === cause
|
|
90
|
+
terms = terms.reject do |term|
|
|
91
|
+
term.positive? && term.package == Package.root
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Optimized simple cases
|
|
96
|
+
return terms if terms.length <= 1
|
|
97
|
+
return terms if terms.length == 2 && terms[0].package != terms[1].package
|
|
98
|
+
|
|
99
|
+
terms.group_by(&:package).map do |package, common_terms|
|
|
100
|
+
term =
|
|
101
|
+
common_terms.inject do |acc, term|
|
|
102
|
+
acc.intersect(term)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
term
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PubGrub
|
|
4
|
+
class Package
|
|
5
|
+
class Version
|
|
6
|
+
attr_reader :package, :id, :name
|
|
7
|
+
|
|
8
|
+
def initialize(package, id, name)
|
|
9
|
+
@package = package
|
|
10
|
+
@id = id
|
|
11
|
+
@name = name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inspect
|
|
19
|
+
"#<#{self.class} #{package.name} #{name} (#{id})>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def <=>(other)
|
|
23
|
+
[package, id] <=> [other.package, other.id]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :name, :versions
|
|
28
|
+
|
|
29
|
+
def initialize(name)
|
|
30
|
+
@name = name
|
|
31
|
+
@versions = []
|
|
32
|
+
yield self if block_given?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def version(version)
|
|
36
|
+
@versions.detect { |v| v.name == version } ||
|
|
37
|
+
raise("No such version of #{name.inspect}: #{version.inspect}")
|
|
38
|
+
end
|
|
39
|
+
alias_method :[], :version
|
|
40
|
+
|
|
41
|
+
def add_version(name)
|
|
42
|
+
Version.new(self, @versions.length, name).tap do |version|
|
|
43
|
+
@versions << version
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inspect
|
|
48
|
+
"#<#{self.class} #{name.inspect} (#{versions.count} versions)>"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def <=>(other)
|
|
52
|
+
name <=> other.name
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class RootPackage < Package
|
|
56
|
+
class Version < Package::Version
|
|
57
|
+
def to_s
|
|
58
|
+
"(root)"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_reader :version
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
super(:root)
|
|
66
|
+
@version = Version.new(self, 0, "1.0.0")
|
|
67
|
+
@versions = [@version].freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.root
|
|
72
|
+
@root ||= RootPackage.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.root_version
|
|
76
|
+
root.version
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require 'pub_grub/assignment'
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
4
|
+
module PubGrub
|
|
5
|
+
class PartialSolution
|
|
6
|
+
attr_reader :assignments, :decisions
|
|
7
|
+
attr_reader :attempted_solutions
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
reset!
|
|
11
|
+
|
|
12
|
+
@attempted_solutions = 1
|
|
13
|
+
@backtracking = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def decision_level
|
|
17
|
+
@decisions.length
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def relation(term)
|
|
21
|
+
package = term.package
|
|
22
|
+
return :overlap if !@terms.key?(package)
|
|
23
|
+
|
|
24
|
+
@terms[package].relation(term)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def satisfies?(term)
|
|
28
|
+
relation(term) == :subset
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def derive(term, cause)
|
|
32
|
+
add_assignment(Assignment.new(term, cause, decision_level, assignments.length))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def satisfier(term)
|
|
36
|
+
assigned_term = nil
|
|
37
|
+
|
|
38
|
+
@assignments_by[term.package].each do |assignment|
|
|
39
|
+
if assigned_term
|
|
40
|
+
assigned_term = assigned_term.intersect(assignment.term)
|
|
41
|
+
else
|
|
42
|
+
assigned_term = assignment.term
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if assigned_term.satisfies?(term)
|
|
46
|
+
return assignment
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
raise "#{term} unsatisfied"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A list of unsatisfied terms
|
|
54
|
+
def unsatisfied
|
|
55
|
+
@required.reject do |package|
|
|
56
|
+
@decisions.key?(package)
|
|
57
|
+
end.map do |package|
|
|
58
|
+
@terms[package]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def decide(version)
|
|
63
|
+
@attempted_solutions += 1 if @backtracking
|
|
64
|
+
@backtracking = false;
|
|
65
|
+
|
|
66
|
+
decisions[version.package] = version
|
|
67
|
+
assignment = Assignment.decision(version, decision_level, assignments.length)
|
|
68
|
+
add_assignment(assignment)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def backtrack(previous_level)
|
|
72
|
+
@backtracking = true
|
|
73
|
+
|
|
74
|
+
new_assignments = assignments.select do |assignment|
|
|
75
|
+
assignment.decision_level <= previous_level
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
new_decisions = Hash[decisions.first(previous_level)]
|
|
79
|
+
|
|
80
|
+
reset!
|
|
81
|
+
|
|
82
|
+
@decisions = new_decisions
|
|
83
|
+
|
|
84
|
+
new_assignments.each do |assignment|
|
|
85
|
+
add_assignment(assignment)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def reset!
|
|
92
|
+
# { Array<Assignment> }
|
|
93
|
+
@assignments = []
|
|
94
|
+
|
|
95
|
+
# { Package => Array<Assignment> }
|
|
96
|
+
@assignments_by = Hash.new { |h,k| h[k] = [] }
|
|
97
|
+
|
|
98
|
+
# { Package => Package::Version }
|
|
99
|
+
@decisions = {}
|
|
100
|
+
|
|
101
|
+
# { Package => Term }
|
|
102
|
+
@terms = {}
|
|
103
|
+
|
|
104
|
+
# { Package => Boolean }
|
|
105
|
+
@required = Set.new
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_assignment(assignment)
|
|
109
|
+
@assignments << assignment
|
|
110
|
+
@assignments_by[assignment.term.package] << assignment
|
|
111
|
+
|
|
112
|
+
term = assignment.term
|
|
113
|
+
package = term.package
|
|
114
|
+
|
|
115
|
+
@required.add(package) if term.positive?
|
|
116
|
+
|
|
117
|
+
if @terms.key?(package)
|
|
118
|
+
old_term = @terms[package]
|
|
119
|
+
@terms[package] = old_term.intersect(term)
|
|
120
|
+
else
|
|
121
|
+
@terms[term.package] = term
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'pub_grub/package'
|
|
2
|
+
require 'pub_grub/version_constraint'
|
|
3
|
+
require 'pub_grub/incompatibility'
|
|
4
|
+
|
|
5
|
+
module PubGrub
|
|
6
|
+
class StaticPackageSource
|
|
7
|
+
class DSL
|
|
8
|
+
def initialize(packages, root_deps)
|
|
9
|
+
@packages = packages
|
|
10
|
+
@root_deps = root_deps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def root(deps:)
|
|
14
|
+
@root_deps.update(deps)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add(name, version, deps: {})
|
|
18
|
+
@packages << [name, version, deps]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@root_deps = {}
|
|
24
|
+
@package_list = []
|
|
25
|
+
yield DSL.new(@package_list, @root_deps)
|
|
26
|
+
|
|
27
|
+
@packages = {
|
|
28
|
+
root: Package.root
|
|
29
|
+
}
|
|
30
|
+
@deps_by_version = {
|
|
31
|
+
Package.root_version => @root_deps
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@package_list.each do |name, version, deps|
|
|
35
|
+
@packages[name] ||= Package.new(name)
|
|
36
|
+
version = @packages[name].add_version(version)
|
|
37
|
+
@deps_by_version[version] = deps
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_package(name)
|
|
42
|
+
@packages[name] ||
|
|
43
|
+
raise("No such package in source: #{name.inspect}")
|
|
44
|
+
end
|
|
45
|
+
alias_method :package, :get_package
|
|
46
|
+
|
|
47
|
+
def version(package_name, version_name)
|
|
48
|
+
package(package_name).version(version_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def incompatibilities_for(version)
|
|
52
|
+
package = version.package
|
|
53
|
+
@deps_by_version[version].map do |dep_package_name, dep_constraint_name|
|
|
54
|
+
bitmap = VersionConstraint.bitmap_matching(package) do |requesting_version|
|
|
55
|
+
deps = @deps_by_version[requesting_version]
|
|
56
|
+
deps && deps[dep_package_name] && deps[dep_package_name] == dep_constraint_name
|
|
57
|
+
end
|
|
58
|
+
description =
|
|
59
|
+
if (bitmap == (1 << package.versions.length) - 1)
|
|
60
|
+
"any"
|
|
61
|
+
elsif (bitmap == 1 << version.id)
|
|
62
|
+
version.name
|
|
63
|
+
else
|
|
64
|
+
"requiring #{dep_package_name} #{dep_constraint_name}"
|
|
65
|
+
end
|
|
66
|
+
self_constraint = VersionConstraint.new(package, description, bitmap: bitmap)
|
|
67
|
+
|
|
68
|
+
dep_package = @packages[dep_package_name]
|
|
69
|
+
|
|
70
|
+
if !dep_package
|
|
71
|
+
# no such package -> this version is invalid
|
|
72
|
+
description = "referencing invalid package #{dep_package_name.inspect}"
|
|
73
|
+
constraint = VersionConstraint.new(package, description, bitmap: bitmap)
|
|
74
|
+
return [Incompatibility.new([Term.new(constraint, true)], cause: :invalid_dependency)]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
dep_constraint = VersionConstraint.new(dep_package, dep_constraint_name)
|
|
78
|
+
|
|
79
|
+
Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module PubGrub
|
|
4
|
+
class Term
|
|
5
|
+
attr_reader :package, :constraint, :positive
|
|
6
|
+
|
|
7
|
+
def initialize(constraint, positive)
|
|
8
|
+
@constraint = constraint
|
|
9
|
+
@package = @constraint.package
|
|
10
|
+
@positive = positive
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s(allow_every: false)
|
|
14
|
+
if positive
|
|
15
|
+
@constraint.to_s(allow_every: allow_every)
|
|
16
|
+
else
|
|
17
|
+
"not #{@constraint}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def invert
|
|
22
|
+
self.class.new(@constraint, !@positive)
|
|
23
|
+
end
|
|
24
|
+
alias_method :inverse, :invert
|
|
25
|
+
|
|
26
|
+
def intersect(other)
|
|
27
|
+
raise ArgumentError, "packages must match" if package != other.package
|
|
28
|
+
|
|
29
|
+
return self if relation(other) == :subset
|
|
30
|
+
return other if other.relation(self) == :subset
|
|
31
|
+
|
|
32
|
+
if positive? && other.positive?
|
|
33
|
+
self.class.new(constraint.intersect(other.constraint), true)
|
|
34
|
+
elsif negative? && other.negative?
|
|
35
|
+
self.class.new(constraint.union(other.constraint), false)
|
|
36
|
+
else
|
|
37
|
+
positive = positive? ? self : other
|
|
38
|
+
negative = negative? ? self : other
|
|
39
|
+
self.class.new(positive.constraint.intersect(negative.constraint.invert), true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def difference(other)
|
|
44
|
+
intersect(other.invert)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def relation(other)
|
|
48
|
+
if positive? && other.positive?
|
|
49
|
+
constraint.relation(other.constraint)
|
|
50
|
+
elsif negative? && other.positive?
|
|
51
|
+
if constraint.allows_all?(other.constraint)
|
|
52
|
+
:disjoint
|
|
53
|
+
else
|
|
54
|
+
:overlap
|
|
55
|
+
end
|
|
56
|
+
elsif positive? && other.negative?
|
|
57
|
+
if !other.constraint.allows_any?(constraint)
|
|
58
|
+
:subset
|
|
59
|
+
elsif other.constraint.allows_all?(constraint)
|
|
60
|
+
:disjoint
|
|
61
|
+
else
|
|
62
|
+
:overlap
|
|
63
|
+
end
|
|
64
|
+
elsif negative? && other.negative?
|
|
65
|
+
if constraint.allows_all?(other.constraint)
|
|
66
|
+
:subset
|
|
67
|
+
else
|
|
68
|
+
:overlap
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalized_constraint
|
|
76
|
+
@normalized_constraint ||= positive ? constraint : constraint.invert
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def satisfies?(other)
|
|
80
|
+
raise ArgumentError, "packages must match" unless package == other.package
|
|
81
|
+
|
|
82
|
+
relation(other) == :subset
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
extend Forwardable
|
|
86
|
+
def_delegators :normalized_constraint, :versions
|
|
87
|
+
|
|
88
|
+
def positive?
|
|
89
|
+
@positive
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def negative?
|
|
93
|
+
!positive?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def empty?
|
|
97
|
+
@empty ||= normalized_constraint.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def inspect
|
|
101
|
+
"#<#{self.class} #{self}>"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|