forked from mint-lang/mint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinstaller.cr
221 lines (180 loc) · 6.43 KB
/
installer.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
module Mint
class Installer
install_error InstallerFailedToInstall
alias Package = NamedTuple(name: String, version: String)
alias Constraint = FixedConstraint | SimpleConstraint
class Retry < Exception; end
@dependencies =
{} of Package => Hash(String, Constraint)
# This holds the elimiated packages, which package elminiated it
# and with which constraint
@eliminated =
[] of Tuple(Package, Package, Constraint)
@repositories =
{} of String => Repository
@root_dependencies =
[] of Dependency
@resolved =
{} of String => Semver
@root =
{name: "root", version: "0.0.0"}
getter root, root_dependencies
def initialize
@root_dependencies = MintJson.parse_current.dependencies
if @root_dependencies.any?
terminal.puts "#{COG} Constructing dependency tree..."
resolve_dependencies
terminal.puts "\n#{COG} Resolving dependency tree..."
solve
print_resolved
terminal.puts "\n#{COG} Copying packages..."
populate
else
terminal.puts "There are no dependencies!\n\nThere is nothing to do!"
end
end
# Prints the resolved packages adn their verions
def print_resolved
@resolved.each do |name, version|
name =
name
.colorize(:light_green)
.mode(:bold)
version =
version
.colorize
.mode(:bold)
terminal.puts " #{DIAMOND} #{name} #{ARROW} #{version}"
end
end
# Populates the resolved package into ".mint/packages" directory
def populate
@resolved.each do |name, version|
# Determine resolved packages path
destination =
File.join(Dir.current, ".mint", "packages", name)
# Checkout the version we want
@repositories[name].checkout(version)
# Remove destination folder
FileUtils.rm_rf(destination)
# Create destination folder
FileUtils.mkdir_p(File.dirname(destination))
# Copy contents
FileUtils.cp_r(@repositories[name].directory, destination)
end
end
# Solves the dependency graph
def solve(base = root)
return unless @dependencies[base]?
# We itarate over the dependencies of the given package (base)
@dependencies[base].each do |dependency, constraint|
# Clone or update the repository and save it
repository =
@repositories[dependency]
# Check if this was resolved aready
resolved =
@resolved[dependency]?
upper =
constraint.upper
lower =
constraint.lower
# If it's already resolved
if resolved
# And matches the constraint
if resolved < upper && resolved >= lower
# We are skipping
next
else
package =
{name: dependency, version: resolved.to_s}
# If it did not match, eliminate and retry
@eliminated << {package, base, constraint}
raise Retry.new
end
else
# If it's not resolved yet, we go through all versions
repository.versions.each do |version|
package = {name: dependency, version: version.to_s}
# Skip if eliminated
next if @eliminated.map(&.[0]).includes?(package)
# If matches the constraint constraint
if version < upper && version >= lower
# Set this version as resolve and try to resolve the
# packages dependencies
@resolved[dependency] = version
solve(package)
break
else
# If it did not match eliminate and retry
@eliminated << {package, base, constraint}
raise Retry.new
end
end
end
# If every version was eliminated there no possible solution
unless @resolved[dependency]?
eliminated =
@eliminated
.select { |item| item[0][:name] == dependency }
.map { |item| "#{item[0][:version]} by #{item[2]} from #{item[1][:name]}:#{item[1][:version]}" }
raise InstallerFailedToInstall, {
"package" => "#{base[:name]}:#{base[:version]}",
"constraint" => constraint.to_s,
"eliminated" => eliminated,
"name" => dependency,
}
end
end
rescue error : Retry
# Clear the resolved cache
@resolved = {} of String => Semver
# Try to solve it again
solve
end
def resolve_dependencies(dependencies = root_dependencies, package = root)
dependencies.each do |dependency|
resolve_dependency(dependency, package)
end
end
def resolve_dependency(dependency, package)
# Don't resolved this dependency multiple times
return if @dependencies[package]? &&
@dependencies[package][dependency.name]?
@dependencies[package] ||= {} of String => Constraint
@dependencies[package][dependency.name] = dependency.constraint
# Don't resolve versions of the dependency multiple times
if @repositories[dependency.name]?
# TODO: Check and warn here if we are using different repository than
# the resolved one.
return
else
root_dependency =
root_dependencies.find(&.name.==(dependency.name))
repository =
@repositories[dependency.name] ||=
if root_dependency &&
root_dependency.constraint.is_a?(FixedConstraint)
# This is where fixed constraints happen, instead of simple
# repository, we create a fixed repository.
Repository.open(
version: root_dependency.constraint.as(FixedConstraint).version,
target: root_dependency.constraint.as(FixedConstraint).target,
url: root_dependency.repository,
name: root_dependency.name)
else
Repository.open(dependency.name, dependency.repository)
end
# Go through all of the dependencies and resolve them
repository.versions.each do |version|
json = repository.json(version)
resolve_dependencies(
json.dependencies,
{name: dependency.name, version: version.to_s})
end
end
end
def terminal
Render::Terminal::STDOUT
end
end
end