gel 0.3.0 → 0.8.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -3
  3. data/RELEASING.md +12 -0
  4. data/exe/gel +4 -2
  5. data/gemlib/gel/stub.rb +20 -0
  6. data/lib/gel/catalog/common.rb +4 -2
  7. data/lib/gel/catalog/compact_index.rb +6 -10
  8. data/lib/gel/catalog/dependency_index.rb +10 -10
  9. data/lib/gel/catalog/legacy_index.rb +4 -6
  10. data/lib/gel/catalog/marshal_hacks.rb +2 -0
  11. data/lib/gel/catalog.rb +33 -52
  12. data/lib/gel/catalog_set.rb +100 -0
  13. data/lib/gel/command/help.rb +13 -2
  14. data/lib/gel/command/lock.rb +3 -3
  15. data/lib/gel/command/open.rb +24 -0
  16. data/lib/gel/command/shell_setup.rb +11 -8
  17. data/lib/gel/command/stub.rb +45 -2
  18. data/lib/gel/command/version.rb +7 -0
  19. data/lib/gel/command.rb +43 -6
  20. data/lib/gel/compatibility/rubygems.rb +10 -197
  21. data/lib/gel/compatibility.rb +2 -2
  22. data/lib/gel/config.rb +41 -7
  23. data/lib/gel/db.rb +93 -83
  24. data/lib/gel/direct_gem.rb +16 -4
  25. data/lib/gel/environment.rb +542 -249
  26. data/lib/gel/error.rb +156 -24
  27. data/lib/gel/gemfile_parser.rb +74 -12
  28. data/lib/gel/gemspec_parser.rb +26 -7
  29. data/lib/gel/git_catalog.rb +15 -3
  30. data/lib/gel/git_depot.rb +62 -28
  31. data/lib/gel/httpool.rb +5 -2
  32. data/lib/gel/installer.rb +61 -23
  33. data/lib/gel/lock_loader.rb +87 -112
  34. data/lib/gel/lock_parser.rb +23 -31
  35. data/lib/gel/locked_store.rb +30 -21
  36. data/lib/gel/multi_store.rb +13 -4
  37. data/lib/gel/null_solver.rb +67 -0
  38. data/lib/gel/package/abortable.rb +18 -0
  39. data/lib/gel/package/installer.rb +124 -49
  40. data/lib/gel/package.rb +21 -4
  41. data/lib/gel/path_catalog.rb +1 -1
  42. data/lib/gel/pinboard.rb +4 -2
  43. data/lib/gel/platform.rb +38 -0
  44. data/lib/gel/pub_grub/package.rb +67 -0
  45. data/lib/gel/pub_grub/preference_strategy.rb +10 -6
  46. data/lib/gel/pub_grub/solver.rb +37 -0
  47. data/lib/gel/pub_grub/source.rb +64 -92
  48. data/lib/gel/resolved_gem_set.rb +234 -0
  49. data/lib/gel/runtime.rb +3 -3
  50. data/lib/gel/set.rb +62 -0
  51. data/lib/gel/stdlib.rb +83 -0
  52. data/lib/gel/store.rb +94 -25
  53. data/lib/gel/store_catalog.rb +2 -2
  54. data/lib/gel/store_gem.rb +54 -6
  55. data/lib/gel/stub_set.rb +32 -2
  56. data/lib/gel/support/cgi_escape.rb +34 -0
  57. data/lib/gel/support/gem_platform.rb +0 -2
  58. data/lib/gel/support/sha512.rb +142 -0
  59. data/lib/gel/support/tar/tar_writer.rb +2 -2
  60. data/lib/gel/tail_file.rb +2 -1
  61. data/lib/gel/util.rb +108 -0
  62. data/lib/gel/vendor/pstore.rb +3 -0
  63. data/lib/gel/vendor/pub_grub.rb +3 -0
  64. data/lib/gel/vendor/ruby_digest.rb +3 -0
  65. data/lib/gel/vendor_catalog.rb +38 -0
  66. data/lib/gel/version.rb +1 -1
  67. data/lib/gel.rb +15 -0
  68. data/man/man1/gel-exec.1 +1 -1
  69. data/man/man1/gel-install.1 +1 -1
  70. data/man/man1/gel.1 +14 -1
  71. data/{lib/gel/compatibility → slib}/bundler/cli.rb +0 -0
  72. data/{lib/gel/compatibility → slib}/bundler/friendly_errors.rb +0 -0
  73. data/{lib/gel/compatibility/rubygems/dependency_installer.rb → slib/bundler/gem_helper.rb} +0 -0
  74. data/slib/bundler/gem_tasks.rb +0 -0
  75. data/{lib/gel/compatibility → slib}/bundler/setup.rb +0 -0
  76. data/{lib/gel/compatibility → slib}/bundler.rb +39 -3
  77. data/{lib/gel/compatibility → slib}/rubygems/command.rb +0 -0
  78. data/slib/rubygems/dependency_installer.rb +12 -0
  79. data/{lib/gel/compatibility → slib}/rubygems/gem_runner.rb +0 -0
  80. data/slib/rubygems/package.rb +6 -0
  81. data/slib/rubygems/package_task.rb +7 -0
  82. data/slib/rubygems/specification.rb +0 -0
  83. data/slib/rubygems/version.rb +0 -0
  84. data/slib/rubygems.rb +297 -0
  85. data/vendor/pstore/LICENSE.txt +22 -0
  86. data/vendor/pstore/lib/pstore.rb +488 -0
  87. data/vendor/pub_grub/LICENSE.txt +21 -0
  88. data/vendor/pub_grub/lib/pub_grub/assignment.rb +20 -0
  89. data/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +183 -0
  90. data/vendor/pub_grub/lib/pub_grub/failure_writer.rb +182 -0
  91. data/vendor/pub_grub/lib/pub_grub/incompatibility.rb +143 -0
  92. data/vendor/pub_grub/lib/pub_grub/package.rb +35 -0
  93. data/vendor/pub_grub/lib/pub_grub/partial_solution.rb +121 -0
  94. data/vendor/pub_grub/lib/pub_grub/rubygems.rb +45 -0
  95. data/vendor/pub_grub/lib/pub_grub/solve_failure.rb +17 -0
  96. data/vendor/pub_grub/lib/pub_grub/static_package_source.rb +53 -0
  97. data/vendor/pub_grub/lib/pub_grub/term.rb +105 -0
  98. data/vendor/pub_grub/lib/pub_grub/version.rb +3 -0
  99. data/vendor/pub_grub/lib/pub_grub/version_constraint.rb +124 -0
  100. data/vendor/pub_grub/lib/pub_grub/version_range.rb +399 -0
  101. data/vendor/pub_grub/lib/pub_grub/version_solver.rb +247 -0
  102. data/vendor/pub_grub/lib/pub_grub/version_union.rb +174 -0
  103. data/vendor/pub_grub/lib/pub_grub.rb +31 -0
  104. data/vendor/ruby-digest/UNLICENSE +24 -0
  105. data/vendor/ruby-digest/lib/ruby_digest.rb +812 -0
  106. metadata +95 -19
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rbconfig"
4
+ require_relative "util"
5
+ require_relative "stdlib"
6
+ require_relative "support/gem_platform"
4
7
 
5
8
  class Gel::Environment
6
9
  IGNORE_LIST = %w(bundler gel rubygems-update)
@@ -12,11 +15,30 @@ class Gel::Environment
12
15
  end
13
16
  self.gemfile = nil
14
17
  @active_lockfile = false
15
- @architectures = ["ruby"].freeze
18
+ @architectures =
19
+ begin
20
+ local = Gel::Support::GemPlatform.local
21
+
22
+ list = []
23
+ if local.cpu == "universal" && RUBY_PLATFORM =~ /^universal\.([^-]+)/
24
+ list << "#$1-#{local.os}"
25
+ end
26
+ list << "#{local.cpu}-#{local.os}"
27
+ list << "java" if defined?(org.jruby.Ruby)
28
+ list << "ruby"
29
+
30
+ list
31
+ end.compact.map(&:freeze).freeze
16
32
 
17
33
  GEMFILE_PLATFORMS = begin
18
34
  v = RbConfig::CONFIG["ruby_version"].split(".")[0..1].inject(:+)
19
- ["ruby", "ruby_#{v}", "mri", "mri_#{v}"]
35
+
36
+ # FIXME: This isn't the right condition
37
+ if defined?(org.jruby.Ruby)
38
+ ["jruby", "jruby_#{v}", "java", "java_#{v}"]
39
+ else
40
+ ["ruby", "ruby_#{v}", "mri", "mri_#{v}"]
41
+ end
20
42
  end
21
43
 
22
44
  def self.platform?(platform)
@@ -42,34 +64,27 @@ class Gel::Environment
42
64
 
43
65
  def self.open(store)
44
66
  @store = store
67
+
68
+ if store.respond_to?(:locked_versions) && store.locked_versions
69
+ gems = store.gems(store.locked_versions)
70
+ activate_gems gems.values
71
+ end
45
72
  end
46
73
 
47
74
  def self.original_rubylib
48
75
  lib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR)
49
- lib.delete File.expand_path("compatibility", __dir__)
50
- #lib.delete File.expand_path("..", __dir__)
76
+ lib.delete File.expand_path("../../slib", __dir__)
51
77
  return nil if lib.empty?
52
78
  lib.join(File::PATH_SEPARATOR)
53
79
  end
54
80
 
55
81
  def self.modified_rubylib
56
82
  lib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR)
57
- dir = File.expand_path("compatibility", __dir__)
83
+ dir = File.expand_path("../../slib", __dir__)
58
84
  lib.unshift dir unless lib.include?(dir)
59
- #dir = File.expand_path("..", __dir__)
60
- #lib.unshift dir unless lib.include?(dir)
61
85
  lib.join(File::PATH_SEPARATOR)
62
86
  end
63
87
 
64
- def self.search_upwards(name, dir = Dir.pwd)
65
- until (file = File.join(dir, name)) && File.exist?(file)
66
- next_dir = File.dirname(dir)
67
- return nil if next_dir == dir
68
- dir = next_dir
69
- end
70
- file
71
- end
72
-
73
88
  def self.find_gemfile(path = nil, error: true)
74
89
  if path && @gemfile && @gemfile.filename != File.expand_path(path)
75
90
  raise Gel::Error::CannotActivateError.new(path: path, gemfile: @gemfile.filename)
@@ -77,7 +92,7 @@ class Gel::Environment
77
92
  return @gemfile.filename if @gemfile
78
93
 
79
94
  path ||= ENV["GEL_GEMFILE"]
80
- path ||= search_upwards("Gemfile")
95
+ path ||= Gel::Util.search_upwards("Gemfile")
81
96
  path ||= "Gemfile"
82
97
 
83
98
  if File.exist?(path)
@@ -98,52 +113,55 @@ class Gel::Environment
98
113
  end
99
114
 
100
115
  def self.lockfile_name(gemfile = self.gemfile&.filename)
101
- ENV["GEL_LOCKFILE"] ||
102
- (gemfile && File.exist?(gemfile + ".lock") && gemfile + ".lock") ||
103
- search_upwards("Gemfile.lock") ||
104
- "Gemfile.lock"
116
+ ENV["GEL_LOCKFILE"] || (gemfile && gemfile + ".lock") || "Gemfile.lock"
105
117
  end
106
118
 
107
- def self.with_root_store
108
- app_store = Gel::Environment.store
109
-
110
- base_store = app_store
111
- base_store = base_store.inner if base_store.is_a?(Gel::LockedStore)
112
-
119
+ def self.with_store(store)
113
120
  # Work around the fact Gel::Environment is a singleton: we really
114
- # want to treat the environment we're running in separately from
115
- # the application's environment we're working on. But for now, we
116
- # can just cheat and swap them.
117
- @store = base_store
121
+ # want to treat the environment we're running in separately from the
122
+ # application's environment we're working on. But for now, we can
123
+ # just cheat and swap them. (This is clearly not at all thread-safe;
124
+ # we're relying on this method only being called from the CLI and
125
+ # our test suite.)
126
+
127
+ original_store = @store
128
+ @store = store
118
129
 
119
- yield base_store
130
+ yield store
120
131
  ensure
121
- @store = app_store
132
+ @store = original_store
122
133
  end
123
134
 
124
- def self.auto_install_pub_grub!
125
- with_root_store do |base_store|
126
- base_store.monitor.synchronize do
127
- if base_store.each("pub_grub").none?
128
- require_relative "work_pool"
135
+ def self.with_root_store(&block)
136
+ app_store = Gel::Environment.store
129
137
 
130
- Gel::WorkPool.new(2) do |work_pool|
131
- catalog = Gel::Catalog.new("https://rubygems.org", work_pool: work_pool)
138
+ base_store = app_store
139
+ base_store = base_store.inner if base_store.is_a?(Gel::LockedStore)
132
140
 
133
- install_gem([catalog], "pub_grub", [">= 0.5.0"])
134
- end
135
- end
136
- end
137
- end
141
+ with_store(base_store, &block)
142
+ end
143
+
144
+ def self.git_depot
145
+ require_relative "git_depot"
146
+ @git_depot ||= Gel::GitDepot.new(store)
138
147
  end
139
148
 
140
- def self.lock(store: store(), output: nil, gemfile: Gel::Environment.load_gemfile, lockfile: Gel::Environment.lockfile_name, catalog_options: {}, preference_strategy: nil)
149
+ def self.solve_for_gemfile(store: store(), output: nil, gemfile: Gel::Environment.load_gemfile, lockfile: Gel::Environment.lockfile_name, catalog_options: {}, solve: true, preference_strategy: nil, platforms: nil)
141
150
  output = nil if $DEBUG
142
151
 
152
+ target_platforms = Array(platforms)
153
+
143
154
  if lockfile && File.exist?(lockfile)
144
- loader = Gel::LockLoader.new(lockfile, gemfile)
155
+ require_relative "resolved_gem_set"
156
+ gem_set = Gel::ResolvedGemSet.load(lockfile, git_depot: git_depot)
157
+ target_platforms |= gem_set.platforms if gem_set.platforms
158
+
159
+ strategy = preference_strategy&.call(gem_set)
145
160
  end
146
161
 
162
+ target_platforms |= architectures if target_platforms.empty?
163
+
164
+ require_relative "work_pool"
147
165
  require_relative "catalog"
148
166
  all_sources = (gemfile.sources | gemfile.gems.flat_map { |_, _, o| o[:source] }).compact
149
167
  local_source = all_sources.delete(:local)
@@ -152,6 +170,7 @@ class Gel::Environment
152
170
  server_catalogs = all_sources.map { |s| Gel::Catalog.new(s, initial_gems: server_gems, work_pool: catalog_pool, **catalog_options) }
153
171
 
154
172
  require_relative "store_catalog"
173
+ store = store.inner if store.is_a?(Gel::LockedStore)
155
174
  local_catalogs = local_source ? [Gel::StoreCatalog.new(store)] : []
156
175
 
157
176
  git_sources = gemfile.gems.map { |_, _, o|
@@ -168,15 +187,36 @@ class Gel::Environment
168
187
 
169
188
  path_sources = gemfile.gems.map { |_, _, o| o[:path] }.compact
170
189
 
171
- require_relative "git_depot"
172
- git_depot = Gel::GitDepot.new(store)
190
+ vendor_dir = File.expand_path("../vendor/cache", gemfile.filename)
191
+ if Dir.exist?(vendor_dir)
192
+ require_relative "vendor_catalog"
193
+ vendor_catalogs = [Gel::VendorCatalog.new(vendor_dir)]
194
+ else
195
+ vendor_catalogs = []
196
+ end
173
197
 
174
198
  require_relative "path_catalog"
175
199
  require_relative "git_catalog"
176
200
 
201
+ previous_git_catalogs = {}
202
+ if gem_set
203
+ gem_set.gems.each do |gem_name, gem_resolutions|
204
+ next if strategy&.refresh_git?(gem_name)
205
+
206
+ gem_resolutions.map(&:catalog).grep(Gel::GitCatalog).uniq.each do |catalog|
207
+ previous_git_catalogs[[catalog.remote, catalog.ref_type, catalog.ref]] = catalog
208
+ end
209
+ end
210
+ end
211
+
212
+ git_catalogs = git_sources.map do |remote, ref_type, ref|
213
+ previous_git_catalogs[[remote, ref_type, ref]] || Gel::GitCatalog.new(git_depot, remote, ref_type, ref)
214
+ end
215
+
177
216
  catalogs =
217
+ vendor_catalogs +
178
218
  path_sources.map { |path| Gel::PathCatalog.new(path) } +
179
- git_sources.map { |remote, ref_type, ref| Gel::GitCatalog.new(git_depot, remote, ref_type, ref) } +
219
+ git_catalogs +
180
220
  [nil] +
181
221
  local_catalogs +
182
222
  server_catalogs
@@ -198,26 +238,26 @@ class Gel::Environment
198
238
  end
199
239
  end
200
240
 
201
- auto_install_pub_grub!
202
- with_root_store do
203
- gem "pub_grub"
204
- end
205
- require_relative "pub_grub/source"
241
+ require_relative "catalog_set"
242
+ catalog_set = Gel::CatalogSet.new(catalogs)
206
243
 
207
- strategy = loader && preference_strategy && preference_strategy.call(loader)
208
- source = Gel::PubGrub::Source.new(gemfile, catalogs, ["ruby"], strategy)
209
- solver = PubGrub::VersionSolver.new(source: source)
210
- solver.define_singleton_method(:next_package_to_try) do
211
- self.solution.unsatisfied.min_by do |term|
212
- package = term.package
213
- versions = self.source.versions_for(package, term.constraint.range)
244
+ if solve
245
+ require_relative "pub_grub/solver"
214
246
 
215
- if strategy
216
- strategy.package_priority(package, versions) + @package_depth[package]
217
- else
218
- @package_depth[package]
219
- end * 1000 + versions.count
220
- end.package
247
+ if gem_set
248
+ # If we have any existing resolution, and no strategy has been
249
+ # provided (i.e. we're doing an auto-resolve for 'gel install'
250
+ # or similar), then default to "anything is permitted, but
251
+ # change the least necessary to satisfy our constraints"
252
+
253
+ require_relative "pub_grub/preference_strategy"
254
+ strategy ||= Gel::PubGrub::PreferenceStrategy.new(gem_set, {}, bump: :hold, strict: false)
255
+ end
256
+
257
+ solver = Gel::PubGrub::Solver.new(gemfile: gemfile, catalog_set: catalog_set, platforms: target_platforms, strategy: strategy)
258
+ else
259
+ require_relative "null_solver"
260
+ solver = Gel::NullSolver.new(gemfile: gemfile, catalog_set: catalog_set, platforms: target_platforms)
221
261
  end
222
262
 
223
263
  if output
@@ -232,239 +272,273 @@ class Gel::Environment
232
272
  end
233
273
  output.puts
234
274
  else
235
- PubGrub.logger.info "Resolving dependencies..."
275
+ if solver.respond_to?(:logger)
276
+ solver.logger.info "Resolving dependencies..."
277
+ end
278
+
236
279
  solver.work until solver.solved?
237
280
  end
238
281
 
239
- solution = solver.result
240
- solution.delete(source.root)
241
-
242
282
  catalog_pool.stop
243
283
 
244
- lock_content = []
284
+ new_resolution = Gel::ResolvedGemSet.new(lockfile)
245
285
 
246
- output_specs_for = lambda do |results|
247
- lock_content << " specs:"
248
- results.each do |(package, version)|
249
- next if package.name == "bundler" || package.name == "ruby" || package.name =~ /^~/
286
+ packages_by_name = {}
287
+ versions_by_name = {}
288
+ solver.each_resolved_package do |package, version|
289
+ next if package.platform.nil?
250
290
 
251
- lock_content << " #{package} (#{version})"
291
+ ((packages_by_name[package.name] ||= {})[catalog_set.platform_for(package, version)] ||= []) << package
252
292
 
253
- deps = source.dependencies_for(package, version)
254
- next unless deps && deps.first
293
+ if versions_by_name[package.name]
294
+ raise "Conflicting version resolution #{versions_by_name[package.name].inspect} != #{version.inspect}" if versions_by_name[package.name] != version
295
+ else
296
+ versions_by_name[package.name] = version
297
+ end
298
+ end
255
299
 
256
- dep_lines = deps.map do |(dep_name, dep_requirements)|
257
- next dep_name if dep_requirements == [">= 0"] || dep_requirements == []
300
+ active_platforms = []
258
301
 
259
- req = Gel::Support::GemRequirement.new(dep_requirements)
260
- req_strings = req.requirements.sort_by { |(_op, ver)| ver }.map { |(op, ver)| "#{op} #{ver}" }
302
+ packages_by_name.each do |package_name, platformed_packages|
303
+ version = versions_by_name[package_name]
261
304
 
262
- "#{dep_name} (#{req_strings.join(", ")})"
263
- end
305
+ new_resolution.gems[package_name] =
306
+ platformed_packages.map do |resolved_platform, packages|
307
+ package = packages.first
264
308
 
265
- dep_lines.sort.each do |line|
266
- lock_content << " #{line}"
267
- end
268
- end
269
- end
309
+ active_platforms << resolved_platform
270
310
 
271
- grouped_graph = solution.sort_by { |package,_| package.name }.group_by { |(package, version)|
272
- spec = source.spec_for_version(package, version)
273
- catalog = spec.catalog
274
- catalog.is_a?(Gel::Catalog) || catalog.is_a?(Gel::StoreCatalog) ? nil : catalog
275
- }
276
- server_gems = grouped_graph.delete(nil)
311
+ catalog = catalog_set.catalog_for_version(package, version)
277
312
 
278
- grouped_graph.keys.sort_by do |catalog|
279
- case catalog
280
- when Gel::GitCatalog
281
- [1, catalog.remote, catalog.revision]
282
- when Gel::PathCatalog
283
- [2, catalog.path]
284
- end
285
- end.each do |catalog|
286
- case catalog
287
- when Gel::GitCatalog
288
- lock_content << "GIT"
289
- lock_content << " remote: #{catalog.remote}"
290
- lock_content << " revision: #{catalog.revision}"
291
- lock_content << " #{catalog.ref_type}: #{catalog.ref}" if catalog.ref
292
- when Gel::PathCatalog
293
- lock_content << "PATH"
294
- lock_content << " remote: #{catalog.path}"
295
- end
313
+ deps = catalog_set.dependencies_for(package, version)
296
314
 
297
- output_specs_for.call(grouped_graph[catalog])
298
- lock_content << ""
299
- end
315
+ resolved_platform = nil if resolved_platform == "ruby"
300
316
 
301
- if server_gems
302
- lock_content << "GEM"
303
- server_catalogs.each do |catalog|
304
- lock_content << " remote: #{catalog}"
305
- end
306
- output_specs_for.call(server_gems)
307
- lock_content << ""
308
- end
317
+ Gel::ResolvedGemSet::ResolvedGem.new(
318
+ package.name, version, resolved_platform,
319
+ deps.map do |(dep_name, dep_requirements)|
320
+ next [dep_name] if dep_requirements == [">= 0"] || dep_requirements == []
309
321
 
310
- lock_content << "PLATFORMS"
311
- lock_content << " ruby"
312
- lock_content << ""
322
+ req = Gel::Support::GemRequirement.new(dep_requirements)
323
+ req_strings = req.requirements.map { |(op, ver)| "#{op} #{ver}" }.sort.reverse
313
324
 
314
- lock_content << "DEPENDENCIES"
325
+ [dep_name, req_strings.join(", ")]
326
+ end,
327
+ set: new_resolution,
328
+ catalog: catalog
329
+ )
330
+ end
331
+ end
332
+ new_resolution.dependencies = gemfile_dependencies(gemfile: gemfile)
315
333
 
316
- bang_deps = gemfile.gems.select { |_, _, options|
317
- options[:path] || options[:git] || options[:source]
318
- }.map { |name, _, _| name }
334
+ new_resolution.platforms = target_platforms & active_platforms
335
+ new_resolution.server_catalogs = server_catalogs
336
+ new_resolution.bundler_version = gem_set&.bundler_version
337
+ new_resolution.ruby_version = RUBY_DESCRIPTION.split.first(2).join(" ") if gem_set&.ruby_version
338
+ new_resolution
339
+ end
340
+
341
+ def self.gemfile_dependencies(gemfile:)
342
+ gemfile.gems.
343
+ group_by { |name, _constraints, _options| name }.
344
+ map do |name, list|
319
345
 
320
- root_deps = source.root_dependencies
321
- root_deps.sort_by { |name,_| name }.each do |name, constraints|
322
- next if name =~ /^~/
346
+ constraints = list.flat_map { |_, c, _| c }.compact
323
347
 
324
- bang = "!" if bang_deps.include?(name)
325
348
  if constraints == []
326
- lock_content << " #{name}#{bang}"
349
+ name
327
350
  else
328
351
  r = Gel::Support::GemRequirement.new(constraints)
329
- req_strings = r.requirements.sort_by { |(_op, ver)| ver }.map { |(op, ver)| "#{op} #{ver}" }
352
+ req_strings = r.requirements.sort_by { |(_op, ver)| [ver, ver.segments] }.map { |(op, ver)| "#{op} #{ver}" }
330
353
 
331
- lock_content << " #{name} (#{req_strings.join(", ")})#{bang}"
354
+ "#{name} (#{req_strings.join(", ")})"
332
355
  end
333
- end
334
- lock_content << ""
335
-
336
- unless gemfile.ruby.empty?
337
- lock_content << "RUBY VERSION"
338
- lock_content << " #{RUBY_DESCRIPTION.split.first(2).join(" ")}"
339
- lock_content << ""
340
- end
341
-
342
- if loader&.bundler_version
343
- lock_content << "BUNDLED WITH"
344
- lock_content << " #{loader.bundler_version}"
345
- lock_content << ""
346
- end
356
+ end.sort
357
+ end
347
358
 
348
- lock_body = lock_content.join("\n")
359
+ def self.write_lock(output: nil, lockfile: lockfile_name, **args)
360
+ gem_set = solve_for_gemfile(output: output, lockfile: lockfile, **args)
349
361
 
350
362
  if lockfile
351
363
  output.puts "Writing lockfile to #{File.expand_path(lockfile)}" if output
352
- File.write(lockfile, lock_body)
364
+ File.write(lockfile, gem_set.dump)
353
365
  end
354
- lock_body
366
+
367
+ gem_set
355
368
  end
356
369
 
357
- def self.install_gem(catalogs, gem_name, requirements = nil, output: nil)
370
+ def self.install_gem(catalogs, gem_name, requirements = nil, output: nil, solve: true)
358
371
  base_store = Gel::Environment.store
359
372
  base_store = base_store.inner if base_store.is_a?(Gel::LockedStore)
360
373
 
361
- req = Gel::Support::GemRequirement.new(requirements)
362
- #base_store.each(gem_name) do |g|
363
- # return false if g.satisfies?(req)
364
- #end
365
-
366
- require_relative "installer"
367
- installer = Gel::Installer.new(base_store)
368
-
369
- found_any = false
370
- catalogs.each do |catalog|
371
- # TODO: Hand this over to resolution so we pick up dependencies
372
- # too
373
-
374
- info = catalog.gem_info(gem_name)
375
- next if info.nil?
376
-
377
- found_any = true
378
- version = info.keys.
379
- map { |v| Gel::Support::GemVersion.new(v.split("-", 2).first) }.
380
- sort_by { |v| [v.prerelease? ? 0 : 1, v] }.
381
- reverse.find { |v| req.satisfied_by?(v) }
382
- next if version.nil?
383
-
384
- return false if base_store.gem?(gem_name, version.to_s)
374
+ gemfile = Gel::GemfileParser.inline do
375
+ source "https://rubygems.org"
385
376
 
386
- installer.install_gem([catalog], gem_name, version.to_s)
387
-
388
- installer.wait(output)
389
-
390
- return true
377
+ gem gem_name, *requirements
391
378
  end
392
379
 
393
- if found_any
394
- raise Gel::Error::NoVersionSatisfyError.new(
395
- gem_name: gem_name,
396
- requirements: requirements,
397
- )
398
- else
399
- raise Gel::Error::UnknownGemError.new(gem_name: gem_name)
400
- end
380
+ gem_set = solve_for_gemfile(output: output, solve: solve, gemfile: gemfile)
381
+
382
+ loader = Gel::LockLoader.new(gem_set)
383
+ locked_store = loader.activate(self, base_store, install: true, output: output)
384
+ open(locked_store)
401
385
  end
402
386
 
403
- def self.activate(install: false, output: nil, error: true)
387
+ def self.activate(fast: false, install: false, output: nil, error: true)
404
388
  loaded = Gel::Environment.load_gemfile
405
389
  return if loaded.nil?
406
390
  return if @active_lockfile
407
391
 
408
392
  lockfile = Gel::Environment.lockfile_name
409
- unless File.exist?(lockfile)
410
- lock(output: $stderr, lockfile: lockfile)
393
+ if File.exist?(lockfile)
394
+ resolved_gem_set = Gel::ResolvedGemSet.load(lockfile, git_depot: git_depot)
395
+
396
+ resolved_gem_set = nil if !fast && lock_outdated?(loaded, resolved_gem_set)
411
397
  end
412
398
 
399
+ return if fast && !resolved_gem_set
400
+
401
+ resolved_gem_set ||= write_lock(output: output, lockfile: lockfile)
402
+
413
403
  @active_lockfile = true
414
- loader = Gel::LockLoader.new(lockfile, gemfile)
404
+ loader = Gel::LockLoader.new(resolved_gem_set, gemfile)
415
405
 
416
406
  base_store = Gel::Environment.store
417
407
  base_store = base_store.inner if base_store.is_a?(Gel::LockedStore)
418
408
 
419
- loader.activate(Gel::Environment, base_store, install: install, output: output)
409
+ require_relative "../../slib/bundler"
410
+
411
+ locked_store = loader.activate(Gel::Environment, base_store, install: install, output: output)
412
+ open(locked_store)
413
+ end
414
+
415
+ def self.lock_outdated?(gemfile, resolved_gem_set)
416
+ gemfile_dependencies(gemfile: gemfile) != resolved_gem_set.dependencies
420
417
  end
421
418
 
422
419
  def self.activate_for_executable(exes, install: false, output: nil)
423
- loader = nil
424
- if Gel::Environment.load_gemfile(error: false)
420
+ loaded_gemfile = nil
421
+ resolved_gem_set = nil
422
+ outdated_gem_set = nil
423
+ load_error = nil
424
+
425
+ if loaded_gemfile = Gel::Environment.load_gemfile(error: false)
425
426
  lockfile = Gel::Environment.lockfile_name
426
- unless File.exist?(lockfile)
427
- lock(output: $stderr, lockfile: lockfile)
427
+ if File.exist?(lockfile)
428
+ resolved_gem_set = Gel::ResolvedGemSet.load(lockfile, git_depot: git_depot)
429
+
430
+ if lock_outdated?(loaded_gemfile, resolved_gem_set)
431
+ outdated_gem_set = resolved_gem_set
432
+ resolved_gem_set = nil
433
+ end
428
434
  end
429
435
 
430
- loader = Gel::LockLoader.new(lockfile, gemfile)
436
+ if resolved_gem_set
437
+ loader = Gel::LockLoader.new(resolved_gem_set, gemfile)
431
438
 
432
- base_store = Gel::Environment.store
433
- base_store = base_store.inner if base_store.is_a?(Gel::LockedStore)
439
+ base_store = Gel::Environment.store
440
+ base_store = base_store.inner if base_store.is_a?(Gel::LockedStore)
434
441
 
435
- locked_store = loader.activate(nil, base_store, install: install, output: output)
442
+ begin
443
+ locked_store = loader.activate(self, base_store, install: install, output: output)
436
444
 
437
- exes.each do |exe|
438
- if locked_store.each.any? { |g| g.executables.include?(exe) }
439
- activate(install: install, output: output)
440
- return :lock
445
+ exes.each do |exe|
446
+ if locked_store.each.any? { |g| g.executables.include?(exe) }
447
+ open(locked_store)
448
+ return :lock
449
+ end
450
+ end
451
+ rescue Gel::Error::MissingGemError => ex
452
+ load_error = ex
441
453
  end
442
454
  end
443
455
  end
444
456
 
445
- locked_gems = loader ? loader.gem_names : []
457
+ locked_gems =
458
+ if resolved_gem_set
459
+ resolved_gem_set.gem_names
460
+ elsif loaded_gemfile
461
+ loaded_gemfile.gem_names | (outdated_gem_set&.gem_names || [])
462
+ else
463
+ []
464
+ end
446
465
 
447
466
  @gemfile = nil
448
467
  exes.each do |exe|
449
- candidates = @store.each.select do |g|
450
- !locked_gems.include?(g.name) && g.executables.include?(exe)
451
- end.group_by(&:name)
468
+ candidates = @store.each.select { |g| g.executables.include?(exe) }
469
+
470
+ locked_candidates, unlocked_candidates =
471
+ candidates.partition { |g| locked_gems.include?(g.name) }
472
+
473
+ # If we failed to load the lockfile, but we've now found a candidate
474
+ # supplied by a locked gem, it's time to fail: we have to run locked
475
+ # gems in a locked environment, and we can't do that right now.
476
+ # The user probably needs to run `gel install`, which is what this
477
+ # error will tell them to do.
478
+ if locked_candidates.any?
479
+ if load_error
480
+ raise load_error
481
+ elsif outdated_gem_set
482
+ raise Gel::Error::OutdatedLockfileError
483
+ elsif resolved_gem_set.nil?
484
+ raise Gel::Error::NoLockfileError
485
+ else
486
+ # The lockfile was up-to-date and fully processed; we can
487
+ # continue and ignore the locked candidates. This could happen
488
+ # if non-locked versions of locked gems supply the executable.
489
+ # We could still succeed if an unlocked_candidate can fill in.
490
+ end
491
+ end
492
+
493
+ # Specific situation, but plausible enough to warrant a more
494
+ # helpful error: there's no ambiguity about who owns the
495
+ # executable name, but the one gem that supplies it is locked to a
496
+ # version that doesn't have it.
497
+ if unlocked_candidates.empty? && locked_candidates.map(&:name).uniq.size == 1
498
+ # We're going to describe the set of versions (that we know
499
+ # about) that would have supplied the executable.
500
+ valid_versions = locked_candidates.map(&:version).uniq.map { |v| Gel::Support::Version.new(v) }.sort
501
+ locked_version = Gel::Support::Version.new(resolved_gem_set.gems[locked_candidates.first.name].version)
502
+
503
+ # Most likely, our not-executable-having version is outside some
504
+ # contiguous range of executable-having versions, so let's check
505
+ # for that, because it'll give us a shorter error message.
506
+ #
507
+ # (Note we assume but don't prove that every version we know
508
+ # about within the range does have the executable. If that
509
+ # assumption is wrong, the user will get the full list after
510
+ # they retry with a bad in-range version.)
511
+ if valid_versions.first > locked_version || valid_versions.last < locked_version
512
+ valid_versions = valid_versions.first.to_s..valid_versions.last.to_s
513
+ elsif valid_versions.size == 1
514
+ valid_versions = valid_versions.first.to_s
515
+ else
516
+ valid_versions = valid_versions.map(&:to_s)
517
+ end
518
+
519
+ raise Gel::Error::MissingExecutableError.new(
520
+ executable: exe,
521
+ gem_name: locked_candidates.first.name,
522
+ gem_versions: valid_versions,
523
+ locked_gem_version: locked_version.to_s,
524
+ )
525
+ end
452
526
 
453
527
  case candidates.size
454
528
  when 0
455
529
  nil
456
530
  when 1
457
- gem(candidates.keys.first)
531
+ gem(candidates.first.name)
458
532
  return :gem
459
533
  else
460
534
  # Multiple gems can supply this executable; do we have any
461
535
  # useful way of deciding which one should win? One obvious
462
536
  # tie-breaker: if a gem's name matches the executable, it wins.
463
537
 
464
- if candidates.keys.include?(exe)
538
+ if candidates.map(&:name).include?(exe)
465
539
  gem(exe)
466
540
  else
467
- gem(candidates.keys.first)
541
+ gem(candidates.first.name)
468
542
  end
469
543
 
470
544
  return :gem
@@ -484,7 +558,12 @@ class Gel::Environment
484
558
 
485
559
  def self.filtered_gems(gems = self.gemfile.gems)
486
560
  platforms = GEMFILE_PLATFORMS.map(&:to_s)
487
- gems = gems.reject { |g| g[2][:platforms] && (Array(g[2][:platforms]).map(&:to_s) & platforms).empty? }
561
+ gems = gems.reject do |_, _, options|
562
+ platform_options = Array(options[:platforms]).map(&:to_s)
563
+
564
+ next true if platform_options.any? && (platform_options & platforms).empty?
565
+ next true unless options.fetch(:install_if, true)
566
+ end
488
567
  gems
489
568
  end
490
569
 
@@ -513,52 +592,170 @@ class Gel::Environment
513
592
  if existing.satisfies?(requirements)
514
593
  return
515
594
  else
516
- why = " (#{why.join("; ")})" if why && why.first
517
- raise LoadError, "already loaded gem #{name} #{existing.version}, which is incompatible with: #{requirements}#{why}"
595
+ raise Gel::Error::AlreadyActivatedError.new(
596
+ name: name,
597
+ existing: existing.version,
598
+ requirements: requirements,
599
+ why: why,
600
+ )
518
601
  end
519
602
  end
520
603
 
604
+ found_any = false
521
605
  gem = @store.each(name).find do |g|
606
+ found_any = true
522
607
  g.satisfies?(requirements)
523
608
  end
524
609
 
525
610
  if gem
526
611
  activate_gem gem, why: why
527
612
  else
528
- why = " (#{why.join("; ")})" if why && why.first
529
- raise LoadError, "unable to satisfy requirements for gem #{name}: #{requirements}#{why}"
613
+ raise Gel::Error::UnsatisfiedDependencyError.new(
614
+ name: name,
615
+ was_locked: @store.is_a?(Gel::LockedStore),
616
+ found_any: found_any,
617
+ requirements: requirements,
618
+ why: why,
619
+ )
530
620
  end
531
621
  end
532
622
 
533
- def self.gems_from_lock(name_version_pairs)
534
- gems = @store.gems(name_version_pairs)
623
+ def self.activate_gem(gem, why: nil)
624
+ raise gem.version.class.name unless gem.version.class == String
625
+ if activated_gems[gem.name]
626
+ raise activated_gems[gem.name].version.class.name unless activated_gems[gem.name].version.class == String
627
+ return if activated_gems[gem.name].version == gem.version
628
+
629
+ raise Gel::Error::AlreadyActivatedError.new(
630
+ name: gem.name,
631
+ existing: activated_gems[gem.name].version,
632
+ requested: gem.version,
633
+ why: why,
634
+ )
635
+ end
535
636
 
536
- dirs = []
537
- gems.each do |name, g|
538
- dirs += g.require_paths
637
+ gem.dependencies.each do |dep, reqs|
638
+ self.gem(dep, *reqs.map { |(qual, ver)| "#{qual} #{ver}" }, why: ["required by #{gem.name} #{gem.version}", *why])
539
639
  end
540
640
 
541
- activated_gems.update gems
542
- $:.concat dirs
641
+ activate_gems [gem]
543
642
  end
544
643
 
545
- def self.activate_gem(gem, why: nil)
546
- return if activated_gems[gem.name] && activated_gems[gem.name].version == gem.version
547
- raise LoadError, "already activated #{gem.name} #{activated_gems[gem.name].version}" if activated_gems[gem.name]
644
+ def self.activate_gems(gems)
645
+ lib_dirs = gems.flat_map(&:require_paths)
646
+ preparation = {}
647
+ activation = {}
548
648
 
549
- gem.dependencies.each do |dep, reqs|
550
- self.gem(dep, *reqs.map { |(qual, ver)| "#{qual} #{ver}" }, why: ["required by #{gem.name} #{gem.version}", *why])
649
+ gems.each do |g|
650
+ preparation[g.name] = g.version
651
+ activation[g.name] = g
551
652
  end
552
653
 
553
- lib_dirs = gem.require_paths
554
- @store.prepare gem.name => gem.version
654
+ @store.prepare(preparation)
555
655
 
556
- activated_gems[gem.name] = gem
656
+ activated_gems.update(activation)
557
657
  $:.concat lib_dirs
558
658
  end
559
659
 
660
+ # Returns either an array of compatible gems that must all be activated
661
+ # (in the specified order) to activate the given +gem+, or a LoadError
662
+ # describing a dependency conflict that prevents it.
663
+ #
664
+ ##
665
+ #
666
+ # Recurses using internal +context+ as a hash of additional gems to
667
+ # consider already activated. This is used to identify internal conflicts
668
+ # between pending dependencies.
669
+ def self.gems_for_activation(gem, why: nil, context: {})
670
+ if active_gem = activated_gems[gem.name] || context[gem.name]
671
+ # This gem name is already active. Either it's the right version, and
672
+ # we have nothing to do, or it's the wrong version, and we're unable
673
+ # to proceed.
674
+ if active_gem == gem
675
+ return []
676
+ else
677
+ return Gel::Error::AlreadyActivatedError.new(
678
+ name: gem.name,
679
+ existing: active_gem.version,
680
+ requested: gem.version,
681
+ why: why,
682
+ )
683
+ end
684
+ end
685
+
686
+ context = context.dup
687
+ new_gems = [gem]
688
+ context[gem.name] = gem
689
+
690
+ gem.dependencies.each do |dep, reqs|
691
+ next if IGNORE_LIST.include?(dep)
692
+
693
+ inner_why = ["required by #{gem.name} #{gem.version}", *why]
694
+
695
+ requirements = Gel::Support::GemRequirement.new(
696
+ reqs.map { |(qual, ver)| "#{qual} #{ver}" }
697
+ )
698
+
699
+ if existing = activated_gems[dep] || context[dep]
700
+ if existing.satisfies?(requirements)
701
+ next
702
+ else
703
+ return Gel::Error::AlreadyActivatedError.new(
704
+ name: dep,
705
+ existing: existing.version,
706
+ requirements: requirements,
707
+ why: inner_why,
708
+ )
709
+ end
710
+ end
711
+
712
+ resolved = nil
713
+ first_failure = nil
714
+
715
+ found_any = false
716
+ candidates = @store.each(dep).select do |g|
717
+ found_any = true
718
+ g.satisfies?(requirements)
719
+ end
720
+
721
+ candidates.each do |g|
722
+ result = gems_for_activation(g, why: inner_why, context: context)
723
+ if result.is_a?(Exception)
724
+ first_failure ||= result
725
+ else
726
+ resolved = result
727
+ break
728
+ end
729
+ end
730
+
731
+ if resolved
732
+ new_gems += resolved
733
+ resolved.each do |r|
734
+ context[r.name] = r
735
+ end
736
+ elsif first_failure
737
+ return first_failure
738
+ else
739
+
740
+ return Gel::Error::UnsatisfiedDependencyError.new(
741
+ name: dep,
742
+ was_locked: @store.is_a?(Gel::LockedStore),
743
+ found_any: found_any,
744
+ requirements: requirements,
745
+ why: inner_why,
746
+ )
747
+ end
748
+ end
749
+
750
+ new_gems
751
+ end
752
+
560
753
  def self.gem_has_file?(gem_name, path)
561
- @store.gems_for_lib(path) do |gem, subdir|
754
+ search_name, search_ext = Gel::Util.split_filename_for_require(path)
755
+
756
+ @store.gems_for_lib(search_name) do |gem, subdir, ext|
757
+ next unless Gel::Util.ext_matches_requested?(ext, search_ext)
758
+
562
759
  if gem.name == gem_name && gem == activated_gems[gem_name]
563
760
  return gem.path(path, subdir)
564
761
  end
@@ -571,23 +768,119 @@ class Gel::Environment
571
768
  if full_path = gem_has_file?(gem_name, path)
572
769
  require full_path
573
770
  else
574
- raise LoadError, "No file #{path.inspect} found in gem #{gem_name.inspect}"
771
+ raise ::LoadError, "No file #{path.inspect} found in gem #{gem_name.inspect}"
575
772
  end
576
773
  end
577
774
 
578
- def self.resolve_gem_path(path)
775
+ # Search gems and stdlib for how we should load the given +path+
776
+ #
777
+ # Returns nil when the path is unrecognised (caller should fall back to
778
+ # scanning $LOAD_PATH). Otherwise, returns an array tuple:
779
+ #
780
+ # [
781
+ # gem, # nil == stdlib
782
+ # file, # full path to require, or nil if gem is conflicted
783
+ # resolved, # if gem: array of gems to activate, or nil if empty
784
+ # # if conflicted: string describing conflict
785
+ # # if stdlib: boolean whether the file is known to already
786
+ # # be loaded (may return false negative)
787
+ # ]
788
+ def self.scan_for_path(path)
579
789
  if @store && !path.start_with?("/")
790
+ search_name, search_ext = Gel::Util.split_filename_for_require(path)
791
+
792
+ # Fast scan first: find all the gems that supply a file matching
793
+ # +search_name+ (ignoring ext for now)
794
+ hits = []
795
+ @store.gems_for_lib(search_name) do |gem, subdir, ext|
796
+ hits << [gem, subdir, ext]
797
+ end
798
+
799
+ # Now we get a bit more detailed: 1) skip any results that don't
800
+ # match the +search_ext+; 2) immediately return if we've matched a
801
+ # gem that's already loaded.
580
802
  results = []
581
- @store.gems_for_lib(path) do |gem, subdir|
582
- results << [gem, subdir]
583
- break if activated_gems[gem.name] == gem
803
+ hits.each do |gem, subdir, ext|
804
+ next unless Gel::Util.ext_matches_requested?(ext, search_ext)
805
+
806
+ if activated_gems[gem.name] == gem
807
+ return [gem, gem.path(path, subdir), nil]
808
+ else
809
+ results << [gem, subdir, ext]
810
+ end
584
811
  end
585
- result = results.find { |g, _| activated_gems[g.name] == g } || results.first
586
812
 
587
- if result
588
- activate_gem result[0], why: ["provides #{path.inspect}"]
589
- return result[0].path(path, result[1])
813
+ # Okay, no already-loaded gems supply the file we're looking for.
814
+ # +results+ contains a list of gems that we could load.
815
+
816
+ # Before we start gaming out dependency trees for gems we could load,
817
+ # it's time to check whether we've already loaded this file from
818
+ # stdlib.
819
+ stdlib = Gel::Stdlib.instance
820
+
821
+ stdlib_path = stdlib.resolve(search_name, search_ext)
822
+ stdlib_path += search_ext if stdlib_path && search_ext
823
+
824
+ if stdlib_path && stdlib.active?(path)
825
+ # Yep, we don't need to do anything
826
+ return [nil, stdlib_path, true]
827
+ end
828
+
829
+ # We're going to have to activate a gem if we can. Recursively plan
830
+ # out the set of dependencies we need to activate... or alternatively,
831
+ # identify the conflict that prevents it.
832
+ first_activation_error = nil
833
+ results.each do |gem, subdir, ext|
834
+ a = gems_for_activation(gem, why: ["provides #{path.inspect}"])
835
+ if a.is_a?(Array)
836
+ # This is a valid dependency set; activate +a+, and require the
837
+ # file.
838
+ return [gem, gem.path(path, subdir), a]
839
+ else
840
+ # If we don't find a better answer later in this loop (or in
841
+ # +stdlib_path+), then this will be the failure we report.
842
+ first_activation_error ||= [gem, nil, a]
843
+ end
844
+ end
845
+
846
+ # We didn't find any viable gems to activate, so now we consider
847
+ # whether we previously found a not-yet-loaded stdlib file.
848
+ if stdlib_path
849
+ return [nil, stdlib_path, false]
850
+ end
851
+
852
+ # Still no luck: this file cannot be resolved. If we found a gem that
853
+ # was blocked by a conflict, we'll return the explanation as a string.
854
+ # Otherwise (no installed gems have any knowledge of this file) we
855
+ # return nil.
856
+ first_activation_error
857
+ end
858
+ end
859
+
860
+ def self.gem_for_path(path)
861
+ gem, _file, _resolved = scan_for_path(path)
862
+ gem
863
+ end
864
+
865
+ def self.resolve_gem_path(path)
866
+ path = path.to_s # might be e.g. a Pathname
867
+
868
+ gem, file, resolved = scan_for_path(path)
869
+
870
+ if file
871
+ if gem && resolved
872
+ activate_gems resolved
873
+ else
874
+ unless resolved
875
+ # This is a cheat: we're assuming the caller is about to require
876
+ # the file
877
+ Gel::Stdlib.instance.activate(path)
878
+ end
590
879
  end
880
+
881
+ return file
882
+ elsif resolved
883
+ raise resolved
591
884
  end
592
885
 
593
886
  path