1
1
require " ./command"
2
2
require " ../sat"
3
+ require " ../dependency_graph"
3
4
4
5
module Shards
5
6
module Commands
6
7
class Lock < Command
7
- record Pkg ,
8
- resolver : Resolver ,
9
- versions : Hash ( String , Spec )
8
+ private def graph
9
+ @graph ||= DependencyGraph .new
10
+ end
10
11
11
12
private def sat
12
13
@sat ||= SAT .new
13
14
end
14
15
15
- private def pkgs
16
- @pkgs ||= {} of String => Pkg
17
- end
18
-
19
16
def run
20
17
# 1. build dependency graph:
21
- Shards .logger.info { " Collecting dependencies... " }
18
+ Shards .logger.info { " Building dependency graph " }
22
19
23
20
spent = Time .measure do
24
- dig(spec.dependencies, resolve: true )
25
- dig(spec.development_dependencies, resolve: true ) unless Shards .production?
21
+ graph.add(spec, development: ! Shards .production?)
26
22
end
27
23
28
- Shards .logger.info do
29
- total = pkgs .reduce(0 ) { |acc , (_ , pkg )| acc + pkg.versions.size }
30
- " Collected #{ pkgs. size} dependencies (total: #{ total } specs, duration: #{ spent } )"
24
+ Shards .logger.debug do
25
+ total = graph.packages .reduce(0 ) { |acc , (_ , pkg )| acc + pkg.versions.size }
26
+ " Collected #{ graph.packages. size} dependencies (#{ total } specs, duration: #{ spent } )"
31
27
end
32
28
33
29
# 2. translate dependency graph as CNF clauses (conjunctive normal form):
34
- Shards .logger.debug { " Building clauses..." }
35
-
36
30
spent = Time .measure do
37
31
# the package to install dependencies for:
38
32
sat.add_clause [spec.name]
@@ -42,22 +36,30 @@ module Shards
42
36
add_dependencies(negation, spec.dependencies)
43
37
add_dependencies(negation, spec.development_dependencies) unless Shards .production?
44
38
45
- # nested dependencies:
46
- pkgs.each do |name , pkg |
47
- pkg.versions.each do |version , s |
48
- add_dependencies(" ~#{ name } :#{ version } " , s.dependencies)
39
+ # version conflicts:
40
+ # - we want at most 1 version for each package
41
+ # - defined before dependencies, so any conflict will fail quickly
42
+ graph.each do |pkg |
43
+ pkg.each_combination do |a , b |
44
+ sat.add_clause [" ~#{ pkg.name } :#{ a } " , " ~#{ pkg.name } :#{ b } " ]
49
45
end
50
46
end
51
47
52
- # version conflicts (we want at most 1 version for each package) :
53
- pkgs .each do |name , pkg |
54
- pkg.versions.keys.each_combination( 2 ) do |( a , b ) |
55
- sat.add_clause [ " ~#{ name } :#{ a } " , " ~ #{ name } : #{ b } " ]
48
+ # nested dependencies :
49
+ graph .each do |pkg |
50
+ pkg.each do |version , s |
51
+ add_dependencies( " ~#{ pkg. name} :#{ version } " , s.dependencies)
56
52
end
57
53
end
58
54
end
59
- Shards .logger.info { " Built #{ sat.@clauses .size } clauses (duration: #{ spent } )" }
55
+ Shards .logger.debug { " Built #{ sat.@clauses .size } clauses (duration: #{ spent } )" }
56
+
57
+ # STDERR.puts "VARIABLES:"
58
+
59
+ # STDERR.puts variable
60
+ # end
60
61
62
+ # STDERR.puts "\nCLAUSES:"
61
63
62
64
# STDERR.puts sat.clause_to_s(clause)
63
65
# end
@@ -71,15 +73,15 @@ module Shards
71
73
# versions, not necessarily from the latest one (e.g. for
72
74
# conservative updates).
73
75
# TODO: consider adding some weight (e.g. to update some dependencies).
74
- pkgs .each do |name , pkg |
75
- pkg.versions.keys.each_with_index do |version , index |
76
- distances[" #{ name } :#{ version } " ] = index
76
+ graph .each do |pkg |
77
+ pkg.each_version do |version , index |
78
+ distances[" #{ pkg. name} :#{ version } " ] = index
77
79
end
78
80
end
79
81
80
82
# 4. solving + decision
81
83
# FIXME: some nested dependencies seem to be selected despite being extraneous (missing clauses?)
82
- Shards .logger.info { " Solving dependencies... " }
84
+ Shards .logger.info { " Solving dependencies" }
83
85
count = 0
84
86
85
87
solution = nil
@@ -97,64 +99,106 @@ module Shards
97
99
if distance < solution_distance
98
100
solution = proposal.dup
99
101
solution_distance = distance
100
- Shards .logger.debug { " Select proposal (distance=#{ solution_distance } ): #{ solution.sort.join(' ' ) } " }
102
+
103
+ Shards .logger.debug do
104
+ " Select proposal (distance=#{ solution_distance } ): #{ solution.sort.join(' ' ) } "
105
+ end
101
106
102
107
# fewer dependencies?
103
108
elsif distance == solution_distance && proposal.size < solution.not_nil!.size
104
109
solution = proposal.dup
105
110
solution_distance = distance
106
- Shards .logger.debug { " Select smaller proposal (distance=#{ solution_distance } ): #{ solution.sort.join(' ' ) } " }
111
+
112
+ Shards .logger.debug do
113
+ " Select smaller proposal (distance=#{ solution_distance } ): #{ solution.sort.join(' ' ) } "
114
+ end
107
115
end
108
116
end
109
117
end
110
118
111
119
# 5.
112
120
if solution
113
- Shards .logger.info { " Analyzed #{ count } solutions (duration: #{ spent } " }
121
+ Shards .logger.debug { " Analyzed #{ count } solutions (duration: #{ spent } " }
114
122
Shards .logger.info { " Found solution:" }
123
+
115
124
solution
116
125
.compact_map { |variable | variable.split(':' ) if variable.index(':' ) }
117
126
.sort { |a , b | a[0 ] <=> b[0 ] }
118
- .each { |p | puts " #{ p[0 ] } : #{ p[1 ] } " }
127
+ .each { |p | puts " #{ p[0 ] } : #{ p[1 ] } " }
119
128
else
120
- Shards .logger.error { " Failed to find a solution (duration: #{ spent } )" }
129
+ report_conflicts
130
+
131
+ if Shards .logger.debug?
132
+ Shards .logger.error { " Failed to find a solution (duration: #{ spent } )" }
133
+ else
134
+ Shards .logger.error { " Failed to find a solution" }
135
+ end
121
136
end
122
137
end
123
138
124
139
private def add_dependencies (negation , dependencies )
125
140
dependencies.each do |d |
126
- versions = Versions .resolve(pkgs[d.name].versions.keys, {d.version} )
141
+ versions = graph .resolve(d )
127
142
128
- # FIXME: looks like we couldn't resolve a constraint here; maybe it's
129
- # related to a git refs?
130
- next if versions.empty?
143
+ if versions.empty?
144
+ # FIXME: we couldn't resolve a constraint (likely a git refs)
145
+ Shards .logger.warn { " Failed to match versions for #{ d.inspect } " }
146
+ next
147
+ end
131
148
132
149
clause = [negation]
133
150
versions.each { |v | clause << " #{ d.name } :#{ v } " }
134
151
sat.add_clause(clause)
135
152
end
136
153
end
137
154
138
- # TODO: try and limit versions to what's actually reachable, in order to
139
- # reduce the dependency graph, which will reduce the number of
140
- # solutions, thus reduce the overall solving time.
141
- private def dig (dependencies , resolve = false )
142
- dependencies.each do |dependency |
143
- next if pkgs.has_key?(dependency.name)
155
+ private def report_conflicts
156
+ interest = ::Set (String ).new
157
+ negation = " ~#{ self .spec.name} "
158
+
159
+ sat.conflicts.reverse_each do |clause |
160
+ if clause.size == 2 && clause.all?(& .starts_with?('~' ))
161
+ # version conflict:
162
+ clause[0 ] =~ /^~(.+) :(.+) $/
163
+ a_name, a_version = $1 , $2
144
164
145
- resolver = Shards .find_resolver(dependency)
146
- versions = resolver.available_versions
165
+ clause[ 1 ] =~ /: (.+) $/
166
+ b_version = $1
147
167
148
- # resolve main spec constraints (avoids impossible branches in the
149
- # dependency graph):
150
- versions = Versions .resolve(versions, {dependency.version}) if resolve
168
+ Shards .logger.warn do
169
+ " Conflict can't install '#{ a_name } ' versions #{ a_version } and #{ b_version } at the same time."
170
+ end
171
+ interest << " #{ a_name } :"
172
+
173
+ elsif interest.any? { |x | clause.any?(& .starts_with?(x)) }
174
+ # dependency graph conflict:
175
+ if clause[0 ] == negation
176
+ spec = self .spec
177
+ a_name, a_version = spec.name, nil
178
+ else
179
+ clause[0 ] =~ /^~(.+) :(.+) $/
180
+ a_name, a_version = $1 , $2
181
+
182
+ spec = graph.packages[a_name].versions[a_version]
183
+ interest << " #{ a_name } :"
184
+ end
185
+
186
+ clause[1 ] =~ /^(.+) :(.+) $/
187
+ b_name, b_version = $1 , $2
151
188
152
- pkg = Pkg .new(resolver, resolver.specs(Versions .sort(versions)))
153
- pkgs[dependency.name] = pkg
189
+ dependency = spec.dependencies.find(& .name.== (b_name)).not_nil!
154
190
155
- pkg.versions.each do |version , spec |
156
- next unless version =~ VERSION_REFERENCE
157
- dig(spec.dependencies)
191
+ Shards .logger.warn do
192
+ human = dependency.to_human_requirement
193
+
194
+ String .build do |str |
195
+ str << " Conflict " << a_name
196
+ str << ' ' << a_version if a_version
197
+ str << " requires " << dependency.name << ' '
198
+ str << human
199
+ # str << " (selected " << b_version << ')' unless human == b_version
200
+ end
201
+ end
158
202
end
159
203
end
160
204
end
0 commit comments