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,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,13 @@
1
+ require 'pub_grub/failure_writer'
2
+
3
+ module PubGrub
4
+ class SolveFailure < StandardError
5
+ def initialize(incompatibility)
6
+ @incompatibility = incompatibility
7
+ end
8
+
9
+ def to_s
10
+ "\n" + FailureWriter.new(@incompatibility).write
11
+ end
12
+ end
13
+ 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