Skip to content

Commit 4994b6a

Browse files
committed
Fix: install must validate selected packages against lock file
Introduces Versions.matches? to compare a version against a given requirement, and refactors Versions.resolve to avoid duplication.
1 parent 72e1897 commit 4994b6a

File tree

3 files changed

+112
-29
lines changed

3 files changed

+112
-29
lines changed

src/commands/install.cr

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ module Shards
1717
solver.prepare(development: !Shards.production?)
1818

1919
if packages = solver.solve
20+
if lockfile?
21+
validate(packages)
22+
end
23+
2024
install(packages)
2125

2226
if generate_lockfile?(packages)
@@ -30,6 +34,33 @@ module Shards
3034
end
3135
end
3236

37+
private def validate(packages)
38+
packages.each do |package|
39+
if lock = locks.find { |d| d.name == package.name }
40+
if version = lock.version?
41+
validate_locked_version(package, version)
42+
elsif commit = lock["commit"]?
43+
validate_locked_commit(package, commit)
44+
else
45+
raise InvalidLock.new # unknown lock resolver
46+
end
47+
elsif Shards.production?
48+
raise LockConflict.new("can't install new dependency #{package.name} in production")
49+
end
50+
end
51+
end
52+
53+
private def validate_locked_version(package, version)
54+
return if Shards.production? && package.version == version
55+
return if Versions.matches?(version, package.spec.version)
56+
raise LockConflict.new("#{package.name} requirements changed")
57+
end
58+
59+
private def validate_locked_commit(package, commit)
60+
return if commit == package.commit
61+
raise LockConflict.new("#{package.name} requirements changed")
62+
end
63+
3364
private def install(packages : Array(Package))
3465
# first install all dependencies:
3566
installed = packages.compact_map { |package| install(package) }

src/versions.cr

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -177,39 +177,56 @@ module Shards
177177
case requirement
178178
when "*", ""
179179
versions
180+
when /~>\s*([^\s]+)/
181+
ver = if idx = $1.rindex('.')
182+
$1[0...idx]
183+
else
184+
$1
185+
end
186+
versions.select { |version| matches_approximate?(version, $1, ver) }
187+
when /\s*(~>|>=|<=|>|<|=)\s*([^~<>=\s]+)\s*/
188+
versions.select { |version| matches_operator?(version, $1, $2) }
189+
else
190+
versions.select { |version| matches_operator?(version, "=", requirement) }
191+
end
192+
end
180193

181-
when /~>(.+)/
182-
ver = $1.strip
183-
vver = if idx = ver.rindex('.')
184-
ver[0...idx]
185-
else
186-
ver
187-
end
188-
versions.select do |v|
189-
v.starts_with?(vver) &&
190-
!v[vver.size]?.try(&.ascii_alphanumeric?) &&
191-
(compare(v, ver) <= 0)
192-
end
193-
194-
when />=(.+)/
195-
ver = $1.strip
196-
versions.select { |v| compare(v, ver) <= 0 }
197-
198-
when /<=(.+)/
199-
ver = $1.strip
200-
versions.select { |v| compare(v, ver) >= 0 }
201-
202-
when />(.+)/
203-
ver = $1.strip
204-
versions.select { |v| compare(v, ver) < 0 }
194+
def self.matches?(version : String, requirement : String)
195+
case requirement
196+
when "*", ""
197+
true
198+
when /~>\s*([^\s]+)\d*/
199+
ver = if idx = $1.rindex('.')
200+
$1[0...idx]
201+
else
202+
$1
203+
end
204+
matches_approximate?(version, $1, ver)
205+
when /\s*(~>|>=|<=|>|<|=)\s*([^~<>=\s]+)\s*/
206+
matches_operator?(version, $1, $2)
207+
else
208+
matches_operator?(version, "=", requirement)
209+
end
210+
end
205211

206-
when /<(.+)/
207-
ver = $1.strip
208-
versions.select { |v| compare(v, ver) > 0 }
212+
private def self.matches_approximate?(version, requirement, ver)
213+
version.starts_with?(ver) &&
214+
!version[ver.size]?.try(&.ascii_alphanumeric?) &&
215+
(compare(version, requirement) <= 0)
216+
end
209217

218+
private def self.matches_operator?(version, operator, requirement)
219+
case operator
220+
when ">="
221+
compare(version, requirement) <= 0
222+
when "<="
223+
compare(version, requirement) >= 0
224+
when ">"
225+
compare(version, requirement) < 0
226+
when "<"
227+
compare(version, requirement) > 0
210228
else
211-
ver = requirement.strip
212-
versions.select { |v| v == ver }
229+
compare(version, requirement) == 0
213230
end
214231
end
215232
end

test/versions_test.cr

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,40 @@ module Shards
165165
assert_equal ["0.1"], Versions.resolve(["0.1"], "~> 0.1")
166166
assert_equal ["0.1"], Versions.resolve(["0.1"], "~> 0.1.0")
167167
end
168+
169+
def test_matches?
170+
assert Versions.matches?("0.1.0", "*")
171+
assert Versions.matches?("1.0.0", "*")
172+
173+
assert Versions.matches?("1.0.0", "1.0.0")
174+
assert Versions.matches?("1.0.0", "1.0")
175+
refute Versions.matches?("1.0.0", "1.0.1")
176+
177+
assert Versions.matches?("1.0.0", ">= 1.0.0")
178+
assert Versions.matches?("1.0.0", ">= 1.0")
179+
assert Versions.matches?("1.0.1", ">= 1.0.0")
180+
refute Versions.matches?("1.0.0", ">= 1.0.1")
181+
182+
refute Versions.matches?("1.0.0", "> 1.0.0")
183+
refute Versions.matches?("1.0.0", "> 1.0")
184+
assert Versions.matches?("1.0.1", "> 1.0.0")
185+
refute Versions.matches?("1.0.0", "> 1.0.1")
186+
187+
assert Versions.matches?("1.0.0", "<= 1.0.0")
188+
assert Versions.matches?("1.0.0", "<= 1.0")
189+
refute Versions.matches?("1.0.1", "<= 1.0.0")
190+
assert Versions.matches?("1.0.0", "<= 1.0.1")
191+
192+
refute Versions.matches?("1.0.0", "< 1.0.0")
193+
refute Versions.matches?("1.0.0", "< 1.0")
194+
refute Versions.matches?("1.0.1", "< 1.0.0")
195+
assert Versions.matches?("1.0.0", "< 1.0.1")
196+
197+
assert Versions.matches?("1.0.0", "~> 1.0.0")
198+
assert Versions.matches?("1.0.0", "~> 1.0")
199+
refute Versions.matches?("1.0.0", "~> 1.1")
200+
assert Versions.matches?("1.0.1", "~> 1.0.0")
201+
refute Versions.matches?("1.0.0", "~> 1.0.1")
202+
end
168203
end
169204
end

0 commit comments

Comments
 (0)