Skip to content

Commit 20b7818

Browse files
committed
Extract generic Solver from Commands::Lock
1 parent 20b4c17 commit 20b7818

File tree

8 files changed

+837
-814
lines changed

8 files changed

+837
-814
lines changed

src/commands/graph.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
require "./command"
2-
require "../dependency_graph"
2+
require "../solver/graph"
33

44
module Shards
55
module Commands
66
class Graph < Command
77
def run
8-
graph = DependencyGraph.new
8+
graph = Solver::Graph.new
99
graph.add(spec, development: !Shards.production?)
1010

1111
graph.each do |pkg|

src/commands/lock.cr

Lines changed: 19 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,38 @@
11
require "./command"
2-
require "../sat"
3-
require "../dependency_graph"
2+
require "../solver"
43

54
module Shards
65
module Commands
76
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-
167
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-
# [email protected] do |variable|
59-
# STDERR.puts variable
60-
# end
61-
62-
# STDERR.puts "\nCLAUSES:"
63-
# [email protected] do |clause|
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?)
10210

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
12212
Shards.logger.info { "Found solution:" }
12313

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:"
13816

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
14219

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]}"
14822

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}"
17825
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
18427
end
18528

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}" }
20234
end
35+
Shards.logger.error { "Failed to find a solution" }
20336
end
20437
end
20538
end

src/dependency_graph.cr

Lines changed: 0 additions & 94 deletions
This file was deleted.

0 commit comments

Comments
 (0)