Skip to content

Commit 77c797a

Browse files
committed
Basic dependency resolver
The resolver applies requirements as best as it can, but won't try to fix conflicts —like selecting an older dependency version higher in the dependency graph.
1 parent dd3afd3 commit 77c797a

File tree

13 files changed

+423
-82
lines changed

13 files changed

+423
-82
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The idea is for Crystal applications and libraries to have a `shard.yml` file
66
at their root looking like this:
77

88
```yaml
9+
name: shards
10+
version: 0.1.0
11+
912
sources:
1013
- https://shards.crystal-lang.org/
1114
- https://shards.example.com/
@@ -28,16 +31,17 @@ folder, ready to be required.
2831

2932
## Development Plan
3033

31-
- [ ] step 1: install/update dependencies
32-
- [ ] clone from Git repositories (with github shortener)
34+
- [x] step 1: install/update dependencies
35+
- [x] clone from Git repositories (with github shortener)
3336
- [ ] clone from Mercurial repositories (optional)
34-
- [ ] copy/link from local path
37+
- [x] copy/link from local path
3538

3639
- [ ] step 2: resolve dependencies
3740
- [ ] recursively install dependencies
38-
- [ ] list versions using git tags (v0.0.0-{pre,rc}0)
41+
- [x] list versions using git tags (v0.0.0-{pre,rc}0)
3942
- [ ] checkout specified versions (defaults to the latest one)
40-
- [ ] resolve versions, applying requirements (`*`, `>=`, `<=`, `<`, `>`, `~>`), recursively
43+
- [x] resolve versions, applying requirements (`*`, `>=`, `<=`, `<`, `>`, `~>`), recursively
44+
- [ ] resolve conflicts (when possible)
4145

4246
- [ ] step 3: central registry (dumb)
4347
- [ ] resolve dependencies by name => repository URL

src/dependencies.cr

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

src/dependency.cr

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Shards
2+
class Dependency < Hash(String, String)
3+
getter :name, :config
4+
5+
def initialize(@name, config)
6+
super nil
7+
config.each { |k, v| self[k.to_s] = v.to_s }
8+
end
9+
10+
def version
11+
self["version"]? || "*"
12+
end
13+
14+
def inspect(io)
15+
io << "#<" << self.class.name << " {\"" << name << "\" => "
16+
super
17+
io << "}>"
18+
end
19+
end
20+
end

src/errors.cr

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module Shards
2+
class Error < ::Exception
3+
end
4+
5+
class Conflict < Error
6+
getter :package
7+
8+
def initialize(@package)
9+
super "can't resolve #{package.name} (#{package.requirements.join(", ")})"
10+
end
11+
end
12+
end

src/helpers/natural_sort.cr

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module Shards
2+
module Helpers
3+
module NaturalSort
4+
EXTRACT_ALPHA_OR_NUMBER_GROUP = /([A-Za-z]+|[\d]+)/
5+
IS_NUMBER_STRING = /\A\d+\Z/
6+
7+
def natural_sort(a, b)
8+
ia = ib = 0
9+
10+
loop do
11+
return 0 unless a[ia .. -1] =~ EXTRACT_ALPHA_OR_NUMBER_GROUP
12+
aaa = $1
13+
ia += $1.size + 1
14+
15+
return 0 unless b[ib .. -1] =~ EXTRACT_ALPHA_OR_NUMBER_GROUP
16+
bbb = $1
17+
ib += $1.size + 1
18+
19+
if aaa =~ IS_NUMBER_STRING && bbb =~ IS_NUMBER_STRING
20+
aaa, bbb = aaa.to_i, bbb.to_i
21+
ret = bbb <=> aaa
22+
else
23+
ret = bbb <=> aaa
24+
end
25+
26+
return ret unless ret == 0
27+
end
28+
end
29+
end
30+
end
31+
end

src/helpers/versions.cr

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require "./natural_sort"
2+
3+
module Shards
4+
module Helpers
5+
module Versions
6+
include NaturalSort
7+
8+
def resolve_versions(versions, requirements)
9+
matches = requirements
10+
.map { |requirement| resolve_requirement(versions, requirement) }
11+
.inject(versions) { |a, e| a & e }
12+
end
13+
14+
def resolve_requirement(versions, requirement)
15+
case requirement
16+
when "*", ""
17+
versions
18+
19+
when /~>(.+)/
20+
ver = $1.strip
21+
vver = ver[0 ... ver.rindex(".").to_i]
22+
versions.select { |v| v.starts_with?(vver) && (natural_sort(v, ver) <= 0) }
23+
24+
when />=(.+)/
25+
ver = $1.strip
26+
versions.select { |v| natural_sort(v, ver) <= 0 }
27+
28+
when /<=(.+)/
29+
ver = $1.strip
30+
versions.select { |v| natural_sort(v, ver) >= 0 }
31+
32+
when />(.+)/
33+
ver = $1.strip
34+
versions.select { |v| natural_sort(v, ver) < 0 }
35+
36+
when /<(.+)/
37+
ver = $1.strip
38+
versions.select { |v| natural_sort(v, ver) > 0 }
39+
40+
else
41+
ver = requirement.strip
42+
versions.select { |v| v == ver }
43+
44+
end
45+
end
46+
end
47+
end
48+
end

src/logger.cr

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
require "logger"
2+
3+
module Shards
4+
def self.logger
5+
@@logger ||= Logger.new(STDOUT).tap do |logger|
6+
logger.progname = "shards"
7+
logger.level = Logger::Severity::DEBUG
8+
9+
logger.formatter = Logger::Formatter.new do |severity, _datetime, _progname, message, io|
10+
io << severity[0] << ": " << message
11+
end
12+
end
13+
end
14+
end

src/manager.cr

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require "./dependency"
2+
require "./resolvers"
3+
require "./helpers/versions"
4+
5+
module Shards
6+
class Package
7+
include Helpers::Versions
8+
9+
getter :requirements
10+
11+
def initialize(@dependency)
12+
@requirements = [] of String
13+
end
14+
15+
def name
16+
@dependency.name
17+
end
18+
19+
def version
20+
versions = resolve_versions(resolver.available_versions, requirements)
21+
22+
if versions.any?
23+
versions.first
24+
else
25+
raise Conflict.new(self)
26+
end
27+
end
28+
29+
def spec
30+
resolver.spec(version)
31+
end
32+
33+
private def resolver
34+
@resolver ||= Shards.find_resolver(@dependency)
35+
end
36+
end
37+
38+
class Set < Array(Package)
39+
def add(dependency)
40+
package = find { |package| package.name == dependency.name }
41+
42+
unless package
43+
package = Package.new(dependency)
44+
self << package
45+
end
46+
47+
package.requirements << dependency.version
48+
package
49+
end
50+
end
51+
52+
class Manager
53+
getter :spec
54+
getter :packages
55+
56+
def initialize(@spec)
57+
@packages = Set.new
58+
end
59+
60+
def resolve
61+
resolve(spec)
62+
rescue ex : Conflict
63+
Shards.logger.error ex.message
64+
exit -1
65+
end
66+
67+
# TODO: handle conflicts
68+
def resolve(spec)
69+
spec.dependencies.each do |dependency|
70+
package = packages.add(dependency)
71+
resolve(package.spec)
72+
end
73+
end
74+
end
75+
end

src/resolvers.cr

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
1+
require "./spec"
2+
13
module Shards
2-
class Resolver
4+
abstract class Resolver
35
getter :dependency
46

57
def initialize(@dependency)
68
end
9+
10+
def spec(version = nil)
11+
Spec.new(read_spec(version))
12+
end
13+
14+
abstract def read_spec(version = nil)
15+
abstract def available_versions
716
end
817

9-
@@resolvers = {} of String => Resolver.class
18+
@@resolver_classes = {} of String => Resolver.class
19+
@@resolvers = {} of String => Resolver
1020

1121
def self.register_resolver(name, resolver)
12-
@@resolvers[name.to_s] = resolver
22+
@@resolver_classes[name.to_s] = resolver
23+
end
24+
25+
def self.find_resolver(dependency)
26+
@@resolvers[dependency.name] ||= begin
27+
klass = get_resolver_class(dependency.keys)
28+
raise Error.new("can't resolve dependency #{dependency.name} (unsupported resolver)") unless klass
29+
klass.new(dependency)
30+
end
1331
end
1432

15-
def self.find_resolver(names)
33+
private def self.get_resolver_class(names)
1634
names.each do |name|
17-
if resolver = @@resolvers[name.to_s]
35+
if resolver = @@resolver_classes[name.to_s]
1836
return resolver
1937
end
2038
end
@@ -26,3 +44,4 @@ end
2644
require "./resolvers/git"
2745
require "./resolvers/github"
2846
require "./resolvers/bitbucket"
47+
require "./resolvers/path"

0 commit comments

Comments
 (0)