gel 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +74 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +39 -0
  5. data/exe/gel +13 -0
  6. data/lib/gel.rb +22 -0
  7. data/lib/gel/catalog.rb +153 -0
  8. data/lib/gel/catalog/common.rb +82 -0
  9. data/lib/gel/catalog/compact_index.rb +152 -0
  10. data/lib/gel/catalog/dependency_index.rb +125 -0
  11. data/lib/gel/catalog/legacy_index.rb +157 -0
  12. data/lib/gel/catalog/marshal_hacks.rb +16 -0
  13. data/lib/gel/command.rb +86 -0
  14. data/lib/gel/command/config.rb +11 -0
  15. data/lib/gel/command/env.rb +7 -0
  16. data/lib/gel/command/exec.rb +66 -0
  17. data/lib/gel/command/help.rb +7 -0
  18. data/lib/gel/command/install.rb +7 -0
  19. data/lib/gel/command/install_gem.rb +16 -0
  20. data/lib/gel/command/lock.rb +34 -0
  21. data/lib/gel/command/ruby.rb +10 -0
  22. data/lib/gel/command/shell_setup.rb +25 -0
  23. data/lib/gel/command/stub.rb +12 -0
  24. data/lib/gel/command/update.rb +11 -0
  25. data/lib/gel/compatibility.rb +4 -0
  26. data/lib/gel/compatibility/bundler.rb +54 -0
  27. data/lib/gel/compatibility/bundler/cli.rb +6 -0
  28. data/lib/gel/compatibility/bundler/friendly_errors.rb +3 -0
  29. data/lib/gel/compatibility/bundler/setup.rb +4 -0
  30. data/lib/gel/compatibility/rubygems.rb +192 -0
  31. data/lib/gel/compatibility/rubygems/command.rb +4 -0
  32. data/lib/gel/compatibility/rubygems/dependency_installer.rb +0 -0
  33. data/lib/gel/compatibility/rubygems/gem_runner.rb +6 -0
  34. data/lib/gel/config.rb +80 -0
  35. data/lib/gel/db.rb +294 -0
  36. data/lib/gel/direct_gem.rb +29 -0
  37. data/lib/gel/environment.rb +592 -0
  38. data/lib/gel/error.rb +104 -0
  39. data/lib/gel/gemfile_parser.rb +144 -0
  40. data/lib/gel/gemspec_parser.rb +95 -0
  41. data/lib/gel/git_catalog.rb +38 -0
  42. data/lib/gel/git_depot.rb +119 -0
  43. data/lib/gel/httpool.rb +148 -0
  44. data/lib/gel/installer.rb +251 -0
  45. data/lib/gel/lock_loader.rb +164 -0
  46. data/lib/gel/lock_parser.rb +64 -0
  47. data/lib/gel/locked_store.rb +126 -0
  48. data/lib/gel/multi_store.rb +96 -0
  49. data/lib/gel/package.rb +156 -0
  50. data/lib/gel/package/inspector.rb +23 -0
  51. data/lib/gel/package/installer.rb +267 -0
  52. data/lib/gel/path_catalog.rb +44 -0
  53. data/lib/gel/pinboard.rb +140 -0
  54. data/lib/gel/pub_grub/preference_strategy.rb +82 -0
  55. data/lib/gel/pub_grub/source.rb +153 -0
  56. data/lib/gel/runtime.rb +27 -0
  57. data/lib/gel/store.rb +205 -0
  58. data/lib/gel/store_catalog.rb +31 -0
  59. data/lib/gel/store_gem.rb +80 -0
  60. data/lib/gel/stub_set.rb +51 -0
  61. data/lib/gel/support/gem_platform.rb +225 -0
  62. data/lib/gel/support/gem_requirement.rb +264 -0
  63. data/lib/gel/support/gem_version.rb +398 -0
  64. data/lib/gel/support/tar.rb +13 -0
  65. data/lib/gel/support/tar/tar_header.rb +229 -0
  66. data/lib/gel/support/tar/tar_reader.rb +123 -0
  67. data/lib/gel/support/tar/tar_reader/entry.rb +154 -0
  68. data/lib/gel/support/tar/tar_writer.rb +339 -0
  69. data/lib/gel/tail_file.rb +205 -0
  70. data/lib/gel/version.rb +5 -0
  71. data/lib/gel/work_pool.rb +143 -0
  72. data/man/man1/gel-exec.1 +16 -0
  73. data/man/man1/gel-install.1 +16 -0
  74. data/man/man1/gel.1 +30 -0
  75. metadata +131 -0
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require "net/http"
5
+
6
+ class Gel::Httpool
7
+ include MonitorMixin
8
+
9
+ require "logger"
10
+ Logger = ::Logger.new($stderr)
11
+ Logger.level = $DEBUG ? ::Logger::DEBUG : ::Logger::WARN
12
+
13
+ def initialize
14
+ super()
15
+
16
+ @pool = {}
17
+ @cond = new_cond
18
+
19
+ if block_given?
20
+ begin
21
+ yield self
22
+ ensure
23
+ close
24
+ end
25
+ end
26
+ end
27
+
28
+ def request(uri, request = Net::HTTP::Get.new(uri))
29
+ with_connection(uri) do |http|
30
+ logger.debug { "GET #{uri}" }
31
+
32
+ if uri.user
33
+ request.basic_auth(uri.user, uri.password || "")
34
+ end
35
+
36
+ t = Time.now
37
+ response = http.request(request)
38
+ logger.debug { "HTTP #{response.code} (#{response.message}) #{uri} [#{Time.now - t}s]" }
39
+ response
40
+ end
41
+ end
42
+
43
+ def close
44
+ https = nil
45
+
46
+ synchronize do
47
+ https = @pool.values.flatten
48
+ @pool = nil
49
+ @cond.broadcast
50
+ end
51
+
52
+ https.each(&:finish)
53
+ end
54
+
55
+ private
56
+
57
+ def ident_for(uri)
58
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
59
+ end
60
+
61
+ def logger
62
+ Logger
63
+ end
64
+
65
+ def connect(ident, uri)
66
+ logger.debug { "Connect #{ident}" }
67
+ t = Time.now
68
+ http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https")
69
+ logger.debug { " Connected #{ident} [#{Time.now - t}s]" }
70
+
71
+ http
72
+ end
73
+
74
+ def queue_new_connection(ident, uri)
75
+ Thread.new do
76
+ begin
77
+ http = connect(ident, uri)
78
+
79
+ synchronize do
80
+ unless Thread.current[:discard]
81
+ Thread.current[:result] = http
82
+ http = nil
83
+ @cond.broadcast
84
+ end
85
+ end
86
+
87
+ if http
88
+ checkin ident, http
89
+ end
90
+ rescue StandardError => exception
91
+ synchronize do
92
+ unless Thread.current[:discard]
93
+ Thread.current[:error] = exception
94
+ http = nil
95
+ @cond.broadcast
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def checkin(ident, http)
103
+ synchronize do
104
+ if @pool
105
+ @pool[ident].push http
106
+ http = nil
107
+ @cond.broadcast
108
+ end
109
+ end
110
+
111
+ if http
112
+ http.finish
113
+ end
114
+ end
115
+
116
+ def checkout(ident, uri)
117
+ synchronize do
118
+ @pool[ident] ||= []
119
+
120
+ return @pool[ident].pop unless @pool[ident].empty?
121
+
122
+ thread = queue_new_connection(ident, uri)
123
+
124
+ @cond.wait_while { !thread[:result] && @pool[ident].empty? }
125
+
126
+ if thread[:result]
127
+ thread[:result]
128
+ elsif thread[:error]
129
+ raise thread[:error]
130
+ else
131
+ thread[:discard] = true
132
+ @pool[ident].pop
133
+ end
134
+ end
135
+ end
136
+
137
+ def with_connection(uri)
138
+ ident = ident_for(uri)
139
+ http = checkout(ident, uri)
140
+
141
+ yield http
142
+
143
+ ensure
144
+ if http
145
+ checkin ident, http
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require "net/http"
5
+
6
+ require_relative "work_pool"
7
+ require_relative "git_depot"
8
+ require_relative "package"
9
+ require_relative "package/installer"
10
+
11
+ class Gel::Installer
12
+ DOWNLOAD_CONCURRENCY = 6
13
+ COMPILE_CONCURRENCY = 4
14
+
15
+ include MonitorMixin
16
+
17
+ attr_reader :store
18
+
19
+ def initialize(store)
20
+ super()
21
+
22
+ @trace = nil
23
+
24
+ @messages = Queue.new
25
+
26
+ @store = store
27
+ @dependencies = Hash.new { |h, k| h[k] = [] }
28
+ @weights = Hash.new(1)
29
+ @pending = Hash.new(0)
30
+
31
+ @download_pool = Gel::WorkPool.new(DOWNLOAD_CONCURRENCY, monitor: self, name: "gel-download", collect_errors: true)
32
+ @compile_pool = Gel::WorkPool.new(COMPILE_CONCURRENCY, monitor: self, name: "gel-compile", collect_errors: true)
33
+
34
+ @download_pool.queue_order = -> ((_, name)) { -@weights[name] }
35
+ @compile_pool.queue_order = -> ((_, name)) { -@weights[name] }
36
+
37
+ @git_depot = Gel::GitDepot.new(store)
38
+
39
+ @compile_waiting = []
40
+ end
41
+
42
+ def known_dependencies(deps)
43
+ deps = deps.dup
44
+
45
+ synchronize do
46
+ @dependencies.update(deps) { |k, l, r| deps[k] = r - l; l | r }
47
+ return if deps.values.all?(&:empty?)
48
+
49
+ deps.each do |dependent, dependencies|
50
+ dependencies.each do |dependency|
51
+ add_weight dependency, @weights[dependent]
52
+ end
53
+ end
54
+
55
+ # Every time we learn about a new dependency, we reorder the
56
+ # queues to ensure the most depended-on gems are processed first.
57
+ # This ensures we can start compiling extension gems as soon as
58
+ # possible.
59
+ @download_pool.reorder_queue!
60
+ @compile_pool.reorder_queue!
61
+ end
62
+ end
63
+
64
+ def install_gem(catalogs, name, version)
65
+ synchronize do
66
+ raise "catalogs is nil" if catalogs.nil?
67
+ @pending[name] += 1
68
+ @download_pool.queue(name) do
69
+ work_download([catalogs, name, version])
70
+ end
71
+ end
72
+ end
73
+
74
+ def load_git_gem(remote, revision, name)
75
+ synchronize do
76
+ @pending[name] += 1
77
+ @download_pool.queue(name) do
78
+ work_git(remote, revision, name)
79
+ end
80
+ end
81
+ end
82
+
83
+ def work_git(remote, revision, name)
84
+ @git_depot.checkout(remote, revision)
85
+
86
+ @messages << "Using #{name} (git)\n"
87
+ @pending[name] -= 1
88
+ end
89
+
90
+ def download_gem(catalogs, name, version)
91
+ catalogs.each do |catalog|
92
+ if fpath = catalog.cached_gem(name, version)
93
+ return fpath
94
+ end
95
+ end
96
+
97
+ catalogs.each do |catalog|
98
+ begin
99
+ return catalog.download_gem(name, version)
100
+ rescue Net::HTTPExceptions
101
+ end
102
+ end
103
+
104
+ raise "Unable to locate #{name} #{version} in: #{catalogs.join ", "}"
105
+ end
106
+
107
+ def work_download((catalogs, name, version))
108
+ fpath = download_gem(catalogs, name, version)
109
+
110
+ installer = Gel::Package::Installer.new(store)
111
+ g = Gel::Package.extract(fpath, installer)
112
+ known_dependencies g.spec.name => g.spec.runtime_dependencies.keys
113
+ if g.needs_compile?
114
+ synchronize do
115
+ add_weight name, 1000
116
+
117
+ @compile_pool.queue(g.spec.name) do
118
+ work_compile(g)
119
+ end
120
+ end
121
+ else
122
+ work_install(g)
123
+ end
124
+ end
125
+
126
+ def work_compile(g)
127
+ synchronize do
128
+ unless compile_ready?(g.spec.name)
129
+ @compile_waiting << g
130
+ return
131
+ end
132
+ end
133
+
134
+ g.compile
135
+ work_install(g)
136
+ end
137
+
138
+ def work_install(g)
139
+ @messages << "Installing #{g.spec.name} (#{g.spec.version})\n"
140
+ g.install
141
+ @pending[g.spec.name] -= 1
142
+
143
+ synchronize do
144
+ compile_recheck, @compile_waiting = @compile_waiting, []
145
+
146
+ compile_recheck.each do |g|
147
+ @compile_pool.queue(g.spec.name) do
148
+ work_compile(g)
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def wait(output = nil)
155
+ clear = ""
156
+ tty = output && output.isatty
157
+
158
+ pools = { "Downloading" => @download_pool, "Compiling" => @compile_pool }
159
+
160
+ return if pools.values.all?(&:idle?)
161
+
162
+ update_status = lambda do
163
+ synchronize do
164
+ if output
165
+ output.write clear
166
+ output.write @messages.pop until @messages.empty?
167
+
168
+ if tty
169
+ messages = pools.map { |label, pool| pool_status(label, pool, label == "Compiling" ? @compile_waiting.size : 0) }.compact
170
+ if messages.empty?
171
+ msgline = ""
172
+ else
173
+ msgline = "[" + messages.join("; ") + "]"
174
+ end
175
+ clear = "\r" + " " * msgline.size + "\r"
176
+ output.write msgline
177
+ end
178
+ else
179
+ @messages.pop until @messages.empty?
180
+ end
181
+ pools.values.all?(&:idle?) && @compile_waiting.empty?
182
+ end
183
+ end
184
+
185
+ pools.values.map do |pool|
186
+ Thread.new do
187
+ Thread.current.abort_on_exception = true
188
+
189
+ pool.wait(&update_status)
190
+ pools.values.each(&:tick!)
191
+ pool.stop
192
+ end
193
+ end.each(&:join)
194
+
195
+ errors = @download_pool.errors + @compile_pool.errors
196
+
197
+ if errors.empty?
198
+ if output
199
+ output.write "Installed #{@download_pool.count} gems\n"
200
+ end
201
+ else
202
+ if output
203
+ output.write "Installed #{@download_pool.count - errors.size} of #{@download_pool.count} gems\n\nErrors encountered with #{errors.size} gems:\n\n"
204
+ errors.each do |(_, name), exception|
205
+ output.write "#{name}\n #{exception}\n\n"
206
+ end
207
+ end
208
+
209
+ if errors.first
210
+ raise errors.first.last
211
+ else
212
+ raise "Errors encountered while installing gems"
213
+ end
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ def compile_ready?(name)
220
+ @dependencies[name].all? do |dep|
221
+ if @pending[dep] == 0
222
+ compile_ready?(dep)
223
+ elsif @download_pool.errors.any? { |(_, failed_name), ex| failed_name == dep }
224
+ raise "Depends on #{dep.inspect}, which failed to download"
225
+ elsif @compile_pool.errors.any? { |(_, failed_name), ex| failed_name == dep }
226
+ raise "Depends on #{dep.inspect}, which failed to compile"
227
+ else
228
+ false
229
+ end
230
+ end
231
+ end
232
+
233
+ def pool_status(label, pool, extra_queue = 0)
234
+ st = pool.status
235
+ queue = st[:queued] + extra_queue
236
+
237
+ return if st[:active].empty? && queue.zero?
238
+
239
+ msg = +"#{label}:"
240
+ msg << " #{st[:active].join(" ")}" unless st[:active].empty?
241
+ msg << " +#{queue}" unless queue.zero?
242
+ msg
243
+ end
244
+
245
+ def add_weight(name, weight)
246
+ @weights[name] += weight
247
+ @dependencies[name].each do |dependency|
248
+ add_weight dependency, weight
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Gel::LockLoader
4
+ attr_reader :filename
5
+ attr_reader :gemfile
6
+
7
+ def initialize(filename, gemfile = nil)
8
+ @filename = filename
9
+ @gemfile = gemfile
10
+ end
11
+
12
+ def lock_content
13
+ @lock_content ||= Gel::LockParser.new.parse(File.read(filename))
14
+ end
15
+
16
+ def each_gem
17
+ lock_content.each do |(section, body)|
18
+ case section
19
+ when "GEM", "PATH", "GIT"
20
+ specs = body["specs"]
21
+ specs.each do |gem_spec, dep_specs|
22
+ gem_spec =~ /\A(.+) \(([^-]+)(?:-(.+))?\)\z/
23
+ name, version, platform = $1, $2, $3
24
+
25
+ if dep_specs
26
+ deps = dep_specs.map do |spec|
27
+ spec =~ /\A(.+?)(?: \((.+)\))?\z/
28
+ [$1, $2 ? $2.split(", ") : []]
29
+ end
30
+ else
31
+ deps = []
32
+ end
33
+
34
+ sym =
35
+ case section
36
+ when "GEM"; :gem
37
+ when "PATH"; :path
38
+ when "GIT"; :git
39
+ end
40
+ yield sym, body, name, version, platform, deps
41
+ end
42
+ when "PLATFORMS", "DEPENDENCIES"
43
+ when "BUNDLED WITH"
44
+ else
45
+ warn "Unknown lockfile section #{section.inspect}"
46
+ end
47
+ end
48
+ end
49
+
50
+ def bundler_version
51
+ _, (version,) = lock_content.assoc("BUNDLED WITH")
52
+ version
53
+ end
54
+
55
+ def gem_names
56
+ names = []
57
+ each_gem do |section, body, name, version, platform, deps|
58
+ names << name
59
+ end
60
+ names
61
+ end
62
+
63
+ def activate(env, base_store, install: false, output: nil)
64
+ locked_store = Gel::LockedStore.new(base_store)
65
+
66
+ locks = {}
67
+
68
+ if install
69
+ require_relative "installer"
70
+ installer = Gel::Installer.new(base_store)
71
+ end
72
+
73
+ filtered_gems = Hash.new(nil)
74
+ top_gems = []
75
+ if gemfile && env
76
+ gemfile.gems.each do |name, *|
77
+ filtered_gems[name] = false
78
+ end
79
+ env.filtered_gems(gemfile.gems).each do |name, *|
80
+ top_gems << name
81
+ filtered_gems[name] = true
82
+ end
83
+ elsif pair = lock_content.assoc("DEPENDENCIES")
84
+ _, list = pair
85
+ top_gems = list.map { |name| name.split(" ", 2)[0].chomp("!") }
86
+ top_gems.each do |name|
87
+ filtered_gems[name] = true
88
+ end
89
+ end
90
+
91
+ gems = {}
92
+ each_gem do |section, body, name, version, platform, deps|
93
+ next if env && !env.platform?(platform)
94
+
95
+ gems[name] = [section, body, version, platform, deps]
96
+
97
+ installer.known_dependencies name => deps.map(&:first) if installer
98
+ end
99
+
100
+ walk = lambda do |name|
101
+ filtered_gems[name] = true
102
+ next unless gems[name]
103
+ gems[name].last.map(&:first).each do |dep_name|
104
+ walk[dep_name] unless filtered_gems[dep_name]
105
+ end
106
+ end
107
+
108
+ top_gems.each(&walk)
109
+
110
+ require_relative "git_depot"
111
+ require_relative "work_pool"
112
+
113
+ Gel::WorkPool.new(8) do |work_pool|
114
+ git_depot = Gel::GitDepot.new(base_store)
115
+
116
+ gems.each do |name, (section, body, version, platform, _deps)|
117
+ next unless filtered_gems[name]
118
+
119
+ if section == :gem
120
+ if installer && !base_store.gem?(name, version, platform)
121
+ require_relative "catalog"
122
+ catalogs = body["remote"].map { |r| Gel::Catalog.new(r, work_pool: work_pool) }
123
+ installer.install_gem(catalogs, name, platform ? "#{version}-#{platform}" : version)
124
+ end
125
+
126
+ locks[name] = version
127
+ else
128
+ if section == :git
129
+ remote = body["remote"].first
130
+ revision = body["revision"].first
131
+
132
+ dir = git_depot.git_path(remote, revision)
133
+ if installer && !Dir.exist?(dir)
134
+ installer.load_git_gem(remote, revision, name)
135
+
136
+ locks[name] = -> { Gel::DirectGem.new(dir, name, version) }
137
+ next
138
+ end
139
+ else
140
+ dir = File.expand_path(body["remote"].first, File.dirname(filename))
141
+ end
142
+
143
+ locks[name] = Gel::DirectGem.new(dir, name, version)
144
+ end
145
+ end
146
+
147
+ installer.wait(output) if installer
148
+
149
+ locks.each do |name, locked|
150
+ locks[name] = locked.call if locked.is_a?(Proc)
151
+ end
152
+ end
153
+
154
+ locked_store.lock(locks)
155
+
156
+ if env
157
+ env.open(locked_store)
158
+
159
+ env.gems_from_lock(locks)
160
+ end
161
+
162
+ locked_store
163
+ end
164
+ end