|
1 | 1 | require "./command"
|
2 |
| -require "../sat" |
3 |
| -require "../dependency_graph" |
| 2 | +require "../solver" |
4 | 3 |
|
5 | 4 | module Shards
|
6 | 5 | module Commands
|
7 | 6 | class Lock < Command
|
8 |
| - private def graph |
9 |
| - @graph ||= DependencyGraph.new |
10 |
| - end |
11 |
| - |
12 |
| - private def sat |
13 |
| - @sat ||= SAT.new |
14 |
| - end |
15 |
| - |
16 | 7 | def run
|
17 |
| - # 1. build dependency graph: |
18 |
| - Shards.logger.info { "Building dependency graph" } |
19 |
| - |
20 |
| - spent = Time.measure do |
21 |
| - graph.add(spec, development: !Shards.production?) |
22 |
| - end |
23 |
| - |
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})" |
27 |
| - end |
28 |
| - |
29 |
| - # 2. translate dependency graph as CNF clauses (conjunctive normal form): |
30 |
| - spent = Time.measure do |
31 |
| - # the package to install dependencies for: |
32 |
| - sat.add_clause [spec.name] |
33 |
| - |
34 |
| - # main dependencies: |
35 |
| - negation = "~#{spec.name}" |
36 |
| - add_dependencies(negation, spec.dependencies) |
37 |
| - add_dependencies(negation, spec.development_dependencies) unless Shards.production? |
38 |
| - |
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}"] |
45 |
| - end |
46 |
| - end |
47 |
| - |
48 |
| - # nested dependencies: |
49 |
| - graph.each do |pkg| |
50 |
| - pkg.each do |version, s| |
51 |
| - add_dependencies("~#{pkg.name}:#{version}", s.dependencies) |
52 |
| - end |
53 |
| - end |
54 |
| - end |
55 |
| - Shards.logger.debug { "Built #{sat.@clauses.size} clauses (duration: #{spent})" } |
56 |
| - |
57 |
| - # STDERR.puts "VARIABLES:" |
58 |
| - |
59 |
| - # STDERR.puts variable |
60 |
| - # end |
61 |
| - |
62 |
| - # STDERR.puts "\nCLAUSES:" |
63 |
| - |
64 |
| - # STDERR.puts sat.clause_to_s(clause) |
65 |
| - # end |
66 |
| - |
67 |
| - # 3. distances (for decision making) |
68 |
| - # compute distance for each version from a reference version: |
69 |
| - distances = {} of String => Int32 |
70 |
| - distances[spec.name] = 0 |
71 |
| - |
72 |
| - # TODO: should be the distance from a given version or a range of |
73 |
| - # versions, not necessarily from the latest one (e.g. for |
74 |
| - # conservative updates). |
75 |
| - # TODO: consider adding some weight (e.g. to update some dependencies). |
76 |
| - graph.each do |pkg| |
77 |
| - pkg.each_version do |version, index| |
78 |
| - distances["#{pkg.name}:#{version}"] = index |
79 |
| - end |
80 |
| - end |
81 |
| - |
82 |
| - # 4. solving + decision |
83 |
| - # FIXME: some nested dependencies seem to be selected despite being extraneous (missing clauses?) |
84 |
| - Shards.logger.info { "Solving dependencies" } |
85 |
| - count = 0 |
86 |
| - |
87 |
| - solution = nil |
88 |
| - solution_distance = Int32::MAX |
89 |
| - |
90 |
| - spent = Time.measure do |
91 |
| - sat.solve do |proposal| |
92 |
| - count += 1 |
93 |
| - |
94 |
| - # 4 (bis). decision making |
95 |
| - # decide the proposal quality (most up-to-date): |
96 |
| - distance = proposal.reduce(0) { |a, e| a + distances[e] } |
97 |
| - |
98 |
| - # better solution? |
99 |
| - if distance < solution_distance |
100 |
| - solution = proposal.dup |
101 |
| - solution_distance = distance |
| 8 | + solver = Solver.new(spec) |
| 9 | + solver.prepare(development: !Shards.production?) |
102 | 10 |
|
103 |
| - Shards.logger.debug do |
104 |
| - "Select proposal (distance=#{solution_distance}): #{solution.sort.join(' ')}" |
105 |
| - end |
106 |
| - |
107 |
| - # fewer dependencies? |
108 |
| - elsif distance == solution_distance && proposal.size < solution.not_nil!.size |
109 |
| - solution = proposal.dup |
110 |
| - solution_distance = distance |
111 |
| - |
112 |
| - Shards.logger.debug do |
113 |
| - "Select smaller proposal (distance=#{solution_distance}): #{solution.sort.join(' ')}" |
114 |
| - end |
115 |
| - end |
116 |
| - end |
117 |
| - end |
118 |
| - |
119 |
| - # 5. |
120 |
| - if solution |
121 |
| - Shards.logger.debug { "Analyzed #{count} solutions (duration: #{spent}" } |
| 11 | + if solution = solver.solve |
122 | 12 | Shards.logger.info { "Found solution:" }
|
123 | 13 |
|
124 |
| - solution |
125 |
| - .compact_map { |variable| variable.split(':') if variable.index(':') } |
126 |
| - .sort { |a, b| a[0] <=> b[0] } |
127 |
| - .each { |p| puts " #{p[0]}: #{p[1]}" } |
128 |
| - else |
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 |
136 |
| - end |
137 |
| - end |
| 14 | + puts "version: 1.1" |
| 15 | + puts "shards:" |
138 | 16 |
|
139 |
| - private def add_dependencies(negation, dependencies) |
140 |
| - dependencies.each do |d| |
141 |
| - versions = graph.resolve(d) |
| 17 | + solution.sort_by!(&.name).each do |rs| |
| 18 | + key = rs.resolver.class.key |
142 | 19 |
|
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 |
| 20 | + puts " #{rs.name}:" |
| 21 | + puts " #{key}: #{rs.resolver.dependency[key]}" |
148 | 22 |
|
149 |
| - clause = [negation] |
150 |
| - versions.each { |v| clause << "#{d.name}:#{v}" } |
151 |
| - sat.add_clause(clause) |
152 |
| - end |
153 |
| - end |
154 |
| - |
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 |
164 |
| - |
165 |
| - clause[1] =~ /:(.+)$/ |
166 |
| - b_version = $1 |
167 |
| - |
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 |
| 23 | + if rs.commit |
| 24 | + puts " commit: #{rs.commit}" |
178 | 25 | 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}:" |
| 26 | + puts " version: #{rs.version}" unless rs.commit |
184 | 27 | end
|
185 | 28 |
|
186 |
| - clause[1] =~ /^(.+):(.+)$/ |
187 |
| - b_name, b_version = $1, $2 |
188 |
| - |
189 |
| - dependency = spec.dependencies.find(&.name.==(b_name)).not_nil! |
190 |
| - |
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 |
| 29 | + puts |
| 30 | + end |
| 31 | + else |
| 32 | + solver.each_conflict do |message| |
| 33 | + Shards.logger.warn { "Conflict #{message}" } |
202 | 34 | end
|
| 35 | + Shards.logger.error { "Failed to find a solution" } |
203 | 36 | end
|
204 | 37 | end
|
205 | 38 | end
|
|
0 commit comments