Skip to content

Commit 98fd7d7

Browse files
committed
Add a 'lock' command that uses the SAT solver [WIP]
1 parent 6698da3 commit 98fd7d7

File tree

6 files changed

+141
-3
lines changed

6 files changed

+141
-3
lines changed

src/cli.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ module Shards
4747
Commands::Install.run(path)
4848
when "list"
4949
Commands::List.run(path, tree: args.includes?("--tree"))
50+
when "lock"
51+
Commands::Lock.run(path)
5052
when "outdated"
5153
Commands::Outdated.run(path, prereleases: args.includes?("--pre"))
5254
when "prune"

src/commands/command.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module Shards
4646

4747
def locks
4848
@locks ||= if lockfile?
49-
Lock.from_file(lockfile_path)
49+
Shards::Lock.from_file(lockfile_path)
5050
else
5151
raise Error.new("Missing #{LOCK_FILENAME}. Please run 'shards install'")
5252
end

src/commands/lock.cr

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
require "./command"
2+
require "../sat"
3+
4+
module Shards
5+
module Commands
6+
class Lock < Command
7+
record Pkg,
8+
resolver : Resolver,
9+
versions : Hash(String, Spec)
10+
11+
def run
12+
Shards.logger.info { "Collecting dependencies..." }
13+
14+
spent = Time.measure do
15+
dig(spec.dependencies, resolve: true)
16+
dig(spec.development_dependencies, resolve: true) unless Shards.production?
17+
end
18+
19+
Shards.logger.info do
20+
total = pkgs.reduce(0) { |acc, (name, pkg)| acc + pkg.versions.size }
21+
"Collected #{pkgs.size} dependencies (total: #{total} specs, duration: #{spent})"
22+
end
23+
24+
sat = Shards::SAT.new
25+
26+
Shards.logger.info { "Building SAT clauses..." }
27+
28+
spent = Time.measure do
29+
negation = "~#{spec.name}"
30+
31+
# the package to install dependencies for:
32+
sat.add_clause [spec.name]
33+
34+
# main dependencies:
35+
spec.dependencies.each do |d|
36+
clause = [negation]
37+
Versions
38+
.resolve(pkgs[d.name].versions.keys, {d.version})
39+
.each { |v| clause << "#{d.name}:#{v}" }
40+
sat.add_clause(clause)
41+
end
42+
43+
# nested dependencies:
44+
pkgs.each do |name, pkg|
45+
pkg.versions.each do |version, s|
46+
s.dependencies.each do |d|
47+
clause = ["~#{name}:#{version}"]
48+
Versions
49+
.resolve(pkgs[d.name].versions.keys, {d.version})
50+
.map { |v| clause << "#{d.name}:#{v}" }
51+
sat.add_clause(clause) unless clause.size == 1
52+
end
53+
end
54+
end
55+
56+
# version conflicts (only 1 version per package)
57+
pkgs.each do |name, pkg|
58+
pkg.versions.keys.each_combination(2) do |(a, b)|
59+
sat.add_clause ["~#{name}:#{a}", "~#{name}:#{b}"]
60+
end
61+
end
62+
end
63+
Shards.logger.info { "Built #{sat.@clauses.size} clauses (duration: #{spent})" }
64+
65+
sat.@clauses.each do |clause|
66+
STDERR.puts sat.clause_to_s(clause)
67+
end
68+
69+
Shards.logger.info { "Solving dependencies..." }
70+
count = 0
71+
72+
spent = Time.measure do
73+
sat.solve do |solution|
74+
# p solution
75+
count += 1
76+
print "found: #{count} solutions\r" if count == 100
77+
end
78+
end
79+
print "found: #{count} solutions\n"
80+
81+
if count == 0
82+
Shards.logger.error { "Failed to find a solution (duration: #{spent})" }
83+
else
84+
Shards.logger.info { "Found #{count} solutions (duration: #{spent}" }
85+
end
86+
end
87+
88+
protected def sat
89+
@sat ||= SAT.new
90+
end
91+
92+
protected def pkgs
93+
@pkgs ||= {} of String => Pkg
94+
end
95+
96+
protected def dig(dependencies, resolve = false)
97+
dependencies.each do |dependency|
98+
next if pkgs.has_key?(dependency.name)
99+
100+
resolver = Shards.find_resolver(dependency)
101+
versions = resolver.available_versions
102+
103+
# resolve main spec constraints (avoids useless branches in the dependency graph):
104+
versions = Versions.resolve(versions, {dependency.version}) if resolve
105+
106+
pkg = Pkg.new(resolver, resolver.specs(Versions.sort(versions)))
107+
pkgs[dependency.name] = pkg
108+
109+
pkg.versions.each do |version, spec|
110+
next unless version =~ VERSION_REFERENCE
111+
dig(spec.dependencies)
112+
dig(spec.development_dependencies) unless Shards.production?
113+
end
114+
end
115+
end
116+
end
117+
end
118+
end

src/commands/prune.cr

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ module Shards
77
class Prune < Command
88
def run(*args)
99
return unless lockfile?
10-
locks = Lock.from_file(lockfile_path)
1110

1211
Dir[File.join(Shards.install_path, "*")].each do |path|
1312
next unless Dir.exists?(path)

src/resolvers/git.cr

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,25 @@ module Shards
3535
refs = git_refs(version)
3636

3737
if file_exists?(refs, SPEC_FILENAME)
38-
capture("git show #{refs}:#{SPEC_FILENAME}")
38+
capture("git show #{refs}:#{SPEC_FILENAME}")
3939
else
4040
raise Error.new("Missing \"#{refs}:#{SPEC_FILENAME}\" for #{dependency.name.inspect}")
4141
end
4242
end
4343

44+
def specs(versions)
45+
specs = {} of String => Spec
46+
47+
versions.each do |version|
48+
refs = git_refs(version)
49+
yaml = capture("git show #{refs}:#{SPEC_FILENAME}")
50+
specs[version] = Spec.from_yaml(yaml)
51+
rescue Error
52+
end
53+
54+
specs
55+
end
56+
4457
def available_versions
4558
update_local_cache
4659

src/resolvers/resolver.cr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ module Shards
1515
Spec.from_yaml(read_spec(version))
1616
end
1717

18+
def specs(versions)
19+
specs = {} of String => Spec
20+
versions.each { |version| specs[version] = spec(version) }
21+
specs
22+
end
23+
1824
def installed_spec
1925
return unless installed?
2026

0 commit comments

Comments
 (0)