Skip to content

Commit 20b4c17

Browse files
committed
Extract DependencyGraph, save & report conflicts
1 parent 9ed445e commit 20b4c17

File tree

11 files changed

+297
-68
lines changed

11 files changed

+297
-68
lines changed

src/cli.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ module Shards
4141
build(path, args[1..-1])
4242
when "check"
4343
Commands::Check.run(path)
44+
when "graph"
45+
Commands::Graph.run(path)
4446
when "init"
4547
Commands::Init.run(path)
4648
when "install"

src/commands/graph.cr

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require "./command"
2+
require "../dependency_graph"
3+
4+
module Shards
5+
module Commands
6+
class Graph < Command
7+
def run
8+
graph = DependencyGraph.new
9+
graph.add(spec, development: !Shards.production?)
10+
11+
graph.each do |pkg|
12+
print " * "
13+
print pkg.name
14+
print ':'
15+
16+
pkg.each_version do |version|
17+
print ' '
18+
print version
19+
end
20+
21+
print '\n'
22+
end
23+
end
24+
end
25+
end
26+
end

src/commands/lock.cr

Lines changed: 97 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,32 @@
11
require "./command"
22
require "../sat"
3+
require "../dependency_graph"
34

45
module Shards
56
module Commands
67
class Lock < Command
7-
record Pkg,
8-
resolver : Resolver,
9-
versions : Hash(String, Spec)
8+
private def graph
9+
@graph ||= DependencyGraph.new
10+
end
1011

1112
private def sat
1213
@sat ||= SAT.new
1314
end
1415

15-
private def pkgs
16-
@pkgs ||= {} of String => Pkg
17-
end
18-
1916
def run
2017
# 1. build dependency graph:
21-
Shards.logger.info { "Collecting dependencies..." }
18+
Shards.logger.info { "Building dependency graph" }
2219

2320
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?)
2622
end
2723

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})"
3127
end
3228

3329
# 2. translate dependency graph as CNF clauses (conjunctive normal form):
34-
Shards.logger.debug { "Building clauses..." }
35-
3630
spent = Time.measure do
3731
# the package to install dependencies for:
3832
sat.add_clause [spec.name]
@@ -42,22 +36,30 @@ module Shards
4236
add_dependencies(negation, spec.dependencies)
4337
add_dependencies(negation, spec.development_dependencies) unless Shards.production?
4438

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}"]
4945
end
5046
end
5147

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)
5652
end
5753
end
5854
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+
# [email protected] do |variable|
59+
# STDERR.puts variable
60+
# end
6061

62+
# STDERR.puts "\nCLAUSES:"
6163
# [email protected] do |clause|
6264
# STDERR.puts sat.clause_to_s(clause)
6365
# end
@@ -71,15 +73,15 @@ module Shards
7173
# versions, not necessarily from the latest one (e.g. for
7274
# conservative updates).
7375
# 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
7779
end
7880
end
7981

8082
# 4. solving + decision
8183
# 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" }
8385
count = 0
8486

8587
solution = nil
@@ -97,64 +99,106 @@ module Shards
9799
if distance < solution_distance
98100
solution = proposal.dup
99101
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
101106

102107
# fewer dependencies?
103108
elsif distance == solution_distance && proposal.size < solution.not_nil!.size
104109
solution = proposal.dup
105110
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
107115
end
108116
end
109117
end
110118

111119
# 5.
112120
if solution
113-
Shards.logger.info { "Analyzed #{count} solutions (duration: #{spent}" }
121+
Shards.logger.debug { "Analyzed #{count} solutions (duration: #{spent}" }
114122
Shards.logger.info { "Found solution:" }
123+
115124
solution
116125
.compact_map { |variable| variable.split(':') if variable.index(':') }
117126
.sort { |a, b| a[0] <=> b[0] }
118-
.each { |p| puts "#{p[0]}: #{p[1]}" }
127+
.each { |p| puts " #{p[0]}: #{p[1]}" }
119128
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
121136
end
122137
end
123138

124139
private def add_dependencies(negation, dependencies)
125140
dependencies.each do |d|
126-
versions = Versions.resolve(pkgs[d.name].versions.keys, {d.version})
141+
versions = graph.resolve(d)
127142

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
131148

132149
clause = [negation]
133150
versions.each { |v| clause << "#{d.name}:#{v}" }
134151
sat.add_clause(clause)
135152
end
136153
end
137154

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
144164

145-
resolver = Shards.find_resolver(dependency)
146-
versions = resolver.available_versions
165+
clause[1] =~ /:(.+)$/
166+
b_version = $1
147167

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
151188

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!
154190

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
158202
end
159203
end
160204
end

src/config.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Shards
77

88
VERSION_REFERENCE = /^v?\d+[-.][-.a-zA-Z\d]+$/
99
VERSION_TAG = /^v(\d+[-.][-.a-zA-Z\d]+)$/
10+
VERSION_AT_GIT_COMMIT = /\+git\.commit\.([0-9a-f]+)$/
1011

1112
def self.cache_path
1213
@@cache_path ||= find_or_create_cache_path

src/dependency.cr

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@ module Shards
2222
end
2323

2424
def version
25+
version { "*" }
26+
end
27+
28+
def version?
29+
version { nil }
30+
end
31+
32+
private def version
2533
if version = self["version"]?
2634
version
2735
elsif self["tag"]? =~ VERSION_TAG
2836
$1
2937
else
30-
"*"
38+
yield
3139
end
3240
end
3341

@@ -39,6 +47,20 @@ module Shards
3947
self["path"]?
4048
end
4149

50+
def to_human_requirement
51+
if version = version?
52+
version
53+
elsif branch = self["branch"]?
54+
"branch #{branch}"
55+
elsif tag = self["tag"]?
56+
"tag #{tag}"
57+
elsif commit = self["commit"]?
58+
"commit #{commit}"
59+
else
60+
"*"
61+
end
62+
end
63+
4264
def inspect(io)
4365
io << "#<" << self.class.name << " {" << name << " => "
4466
super

0 commit comments

Comments
 (0)