Skip to content

Commit f223173

Browse files
committed
Use SAT solver in all commands
1 parent 36ef762 commit f223173

File tree

14 files changed

+186
-428
lines changed

14 files changed

+186
-428
lines changed

src/cli.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module Shards
1414
list [--tree] - Lists installed dependencies.
1515
outdated [--pre] - Lists dependencies that are outdated.
1616
prune - Removes unused dependencies from `lib` folder.
17-
update - Updates dependencies and `shards.lock`.
17+
update [<shard>, ...] - Updates dependencies and `shard.lock`.
1818
version [<path>] - Prints the current version of the shard.
1919
2020
Options:
@@ -59,7 +59,7 @@ module Shards
5959
when "prune"
6060
Commands::Prune.run(path)
6161
when "update"
62-
Commands::Update.run(path)
62+
Commands::Update.run(path, args.reject(&.starts_with?("--")))
6363
when "version"
6464
Commands::Version.run(args[1]? || path)
6565
else

src/commands/build.cr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Shards
1010
end
1111

1212
if targets.empty?
13-
targets = manager.spec.targets.map(&.name)
13+
targets = spec.targets.map(&.name)
1414
end
1515

1616
targets.each do |name|
@@ -23,15 +23,15 @@ module Shards
2323
end
2424

2525
private def build(target, options)
26-
Shards.logger.info "Building: #{target.name}"
26+
Shards.logger.info { "Building: #{target.name}" }
2727

2828
args = [
2929
"build",
3030
"-o", File.join(Shards.bin_path, target.name),
3131
target.main,
3232
]
3333
options.each { |option| args << option }
34-
Shards.logger.debug "crystal #{args.join(' ')}"
34+
Shards.logger.debug { "crystal #{args.join(' ')}" }
3535

3636
error = IO::Memory.new
3737
status = Process.run("crystal", args: args, output: Process::Redirect::Inherit, error: error)

src/commands/command.cr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
require "../lock"
2-
require "../manager"
32
require "../spec"
43

54
module Shards
@@ -40,10 +39,6 @@ module Shards
4039
File.basename(spec_path)
4140
end
4241

43-
def manager
44-
@manager ||= Manager.new(spec)
45-
end
46-
4742
def locks
4843
@locks ||= if lockfile?
4944
Shards::Lock.from_file(lockfile_path)
@@ -55,5 +50,10 @@ module Shards
5550
def lockfile?
5651
File.exists?(lockfile_path)
5752
end
53+
54+
def write_lockfile(packages)
55+
Shards.logger.info { "Writing #{LOCK_FILENAME}" }
56+
Shards::Lock.write(packages, LOCK_FILENAME)
57+
end
5858
end
5959
end

src/commands/install.cr

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,67 @@
11
require "./command"
2+
require "../solver"
23

34
module Shards
45
module Commands
5-
# OPTIMIZE: avoid updating GIT caches until required
66
class Install < Command
77
def run
8+
Shards.logger.info { "Resolving dependencies" }
9+
10+
solver = Solver.new(spec)
11+
812
if lockfile?
9-
manager.locks = locks
10-
manager.resolve
11-
install(manager.packages, locks)
12-
else
13-
manager.resolve
14-
install(manager.packages)
13+
# install must be as conservative as possible:
14+
solver.locks = locks
1515
end
1616

17-
if generate_lockfile?
18-
manager.to_lock(lockfile_path)
19-
end
20-
end
17+
solver.prepare(development: !Shards.production?)
2118

22-
private def install(packages : Set, locks : Array(Dependency))
23-
packages.each do |package|
24-
version = nil
19+
if packages = solver.solve
20+
install(packages)
2521

26-
if lock = locks.find { |dependency| dependency.name == package.name }
27-
if version = lock["version"]?
28-
unless package.matching_versions.includes?(version)
29-
raise LockConflict.new("#{package.name} requirements changed")
30-
end
31-
elsif version = lock["commit"]?
32-
unless package.matches?(version)
33-
raise LockConflict.new("#{package.name} requirements changed")
34-
end
35-
else
36-
raise InvalidLock.new # unknown lock resolver
37-
end
38-
elsif Shards.production?
39-
raise LockConflict.new("can't install new dependency #{package.name} in production")
22+
if generate_lockfile?(packages)
23+
write_lockfile(packages)
4024
end
41-
42-
install(package, version)
25+
else
26+
solver.each_conflict do |message|
27+
Shards.logger.warn { "Conflict #{message}" }
28+
end
29+
Shards.logger.error { "Failed to resolve dependencies" }
4330
end
4431
end
4532

46-
private def install(packages : Set)
47-
packages
48-
.compact_map { |package| install(package) }
49-
.each(&.postinstall)
33+
private def install(packages : Array(Package))
34+
# first install all dependencies:
35+
installed = packages.compact_map { |package| install(package) }
5036

51-
# always install executables because the path resolver never installs
52-
# dependencies, but uses them as-is:
37+
# then execute the postinstall script of installed dependencies (with
38+
# access to all transitive dependencies):
39+
installed.each(&.postinstall)
40+
41+
# always install executables because the path resolver never actually
42+
# installs dependencies:
5343
packages.each(&.install_executables)
5444
end
5545

56-
private def install(package : Package, version = nil)
57-
version ||= package.version
58-
59-
if package.installed?(version)
60-
Shards.logger.info "Using #{package.name} (#{package.report_version})"
46+
private def install(package : Package)
47+
if package.installed?
48+
Shards.logger.info { "Using #{package.name} (#{package.report_version})" }
6149
return
6250
end
6351

64-
Shards.logger.info "Installing #{package.name} (#{package.report_version})"
65-
package.install(version)
52+
Shards.logger.info { "Installing #{package.name} (#{package.report_version})" }
53+
package.install
6654
package
6755
end
6856

69-
private def generate_lockfile?
70-
!Shards.production? && manager.packages.any? && (!lockfile? || outdated_lockfile?)
57+
private def generate_lockfile?(packages)
58+
!Shards.production? && !packages.empty? && (!lockfile? || outdated_lockfile?(packages))
7159
end
7260

73-
private def outdated_lockfile?
74-
locks.size != manager.packages.size
61+
private def outdated_lockfile?(packages)
62+
a = packages.map { |x| {x.name, x.version, x.commit} }
63+
b = locks.map { |x| {x.name, x["version"], x["commit"]?} }
64+
a != b
7565
end
7666
end
7767
end

src/commands/lock.cr

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ require "../solver"
44
module Shards
55
module Commands
66
class Lock < Command
7-
def run(shards, print = false, update = false)
7+
def run(shards : Array(String), print = false, update = false)
88
Shards.logger.info { "Resolving dependencies" }
99

1010
solver = Solver.new(spec)
@@ -24,35 +24,34 @@ module Shards
2424

2525
solver.prepare(development: !Shards.production?)
2626

27-
if solution = solver.solve
27+
if packages = solver.solve
2828
if print
29-
to_lockfile(solution, STDOUT)
29+
Shards::Lock.write(packages, STDOUT)
3030
else
31-
Shards.logger.info { "Writing #{LOCK_FILENAME}" }
32-
File.open(LOCK_FILENAME, "w") { |file| to_lockfile(solution, file) }
31+
write_lockfile(packages)
3332
end
3433
else
3534
solver.each_conflict do |message|
3635
Shards.logger.warn { "Conflict #{message}" }
3736
end
38-
Shards.logger.error { "Failed to find a solution" }
37+
Shards.logger.error { "Failed to resolve dependencies" }
3938
end
4039
end
4140

42-
private def to_lockfile(solution, io)
41+
private def to_lockfile(packages, io)
4342
io << "version: 1.0\n"
4443
io << "shards:\n"
4544

46-
solution.sort_by!(&.name).each do |rs|
47-
key = rs.resolver.class.key
45+
packages.sort_by!(&.name).each do |package|
46+
key = package.resolver.class.key
4847

49-
io << " " << rs.name << ":\n"
50-
io << " " << key << ": " << rs.resolver.dependency[key] << '\n'
48+
io << " " << package.name << ":\n"
49+
io << " " << key << ": " << package.resolver.dependency[key] << '\n'
5150

52-
if rs.commit
53-
io << " commit: " << rs.commit << '\n'
51+
if package.commit
52+
io << " commit: " << package.commit << '\n'
5453
else
55-
io << " version: " << rs.version << '\n'
54+
io << " version: " << package.version << '\n'
5655
end
5756

5857
io << '\n'

src/commands/outdated.cr

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,53 +11,59 @@ module Shards
1111
def run(@prereleases = false)
1212
return unless has_dependencies?
1313

14-
if lockfile?
15-
manager.locks = locks
16-
manager.resolve
17-
else
18-
manager.resolve
19-
end
14+
Shards.logger.info { "Resolving dependencies" }
2015

21-
manager.packages.each do |package|
22-
analyze(package)
23-
end
16+
solver = Solver.new(spec)
17+
solver.prepare(development: !Shards.production?)
2418

25-
if @up_to_date
26-
Shards.logger.info "Dependencies are up to date!"
19+
if packages = solver.solve
20+
packages.each { |package| analyze(package) }
21+
22+
if @up_to_date
23+
Shards.logger.info "Dependencies are up to date!"
24+
else
25+
@output.rewind
26+
Shards.logger.warn "Outdated dependencies:"
27+
puts @output.to_s
28+
end
2729
else
28-
@output.rewind
29-
Shards.logger.warn "Outdated dependencies:"
30-
puts @output.to_s
30+
solver.each_conflict do |message|
31+
Shards.logger.warn { "Conflict #{message}" }
32+
end
33+
Shards.logger.error { "Failed to resolve dependencies" }
3134
end
3235
end
3336

3437
private def analyze(package)
35-
_spec = package.resolver.installed_spec
38+
resolver = package.resolver
39+
installed = resolver.installed_spec.try(&.version)
3640

37-
unless _spec
41+
unless installed
3842
Shards.logger.warn { "#{package.name}: not installed" }
3943
return
4044
end
4145

42-
installed = _spec.version
43-
4446
# already the latest version?
45-
latest = Versions.sort(package.available_versions(@prereleases)).first
47+
available_versions =
48+
if @prereleases
49+
resolver.available_versions
50+
else
51+
Versions.without_prereleases(resolver.available_versions)
52+
end
53+
latest = Versions.sort(available_versions).first
4654
return if latest == installed
4755

4856
@up_to_date = false
4957

5058
@output << " * " << package.name
5159
@output << " (installed: " << installed
5260

53-
# is new version matching constraints available?
54-
available = package.matching_versions(@prereleases).first
55-
unless available == installed
56-
@output << ", available: " << available
61+
unless installed == package.version
62+
@output << ", available: " << package.version
5763
end
5864

5965
# also report latest version:
60-
if Versions.compare(latest, available) < 0
66+
if Versions.compare(latest, package.version) < 0
6167
@output << ", latest: " << latest
6268
end
6369

0 commit comments

Comments
 (0)