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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
|
require "rubygems"
require "bundler"
require "shellwords"
require "set"
module GemfileUtil
#
# Adds `override: true`, which allows your statement to override any other
# gem statement about the same gem in the Gemfile.
#
def gem(name, *args)
options = args[-1].is_a?(Hash) ? args[-1] : {}
# Unless we're finished with everything, ignore gems that are being overridden
unless overridden_gems == :finished
# If it's a path or override gem, it overrides whatever else is there.
if options[:path] || options[:override]
options.delete(:override)
warn_if_replacing(name, overridden_gems[name], args)
overridden_gems[name] = args
return
# If there's an override gem, and we're *not* an override gem, don't do anything
elsif overridden_gems[name]
warn_if_replacing(name, args, overridden_gems[name])
return
end
end
# Otherwise, add the gem normally
super
rescue
puts $!.backtrace
raise
end
def overridden_gems
@overridden_gems ||= {}
end
#
# Just before we finish the Gemfile, finish up the override gems
#
def to_definition(*args)
complete_overrides
super
end
def complete_overrides
to_override = overridden_gems
unless to_override == :finished
@overridden_gems = :finished
to_override.each do |name, args|
gem name, *args
end
end
end
#
# Include all gems in the locked gemfile.
#
# @param gemfile_path Path to the Gemfile to load (relative to your Gemfile)
# @param lockfile_path Path to the Gemfile to load (relative to your Gemfile).
# Defaults to <gemfile_path>.lock.
# @param groups A list of groups to include (whitelist). If not passed (or set
# to nil), all gems will be selected.
# @param without_groups A list of groups to ignore. Gems will be excluded from
# the results if all groups they belong to are ignored. This matches
# bundler's `without` behavior.
# @param gems A list of gems to include above and beyond the given groups.
# Gems in this list must be explicitly included in the Gemfile
# with a `gem "gem_name", ...` line or they will be silently
# ignored.
# @param copy_groups Whether to copy the groups over from the old lockfile to
# the new. Use this when the new lockfile has the same convention for
# groups as the old. Defaults to `false`.
#
def include_locked_gemfile(gemfile_path, lockfile_path = "#{gemfile_path}.lock", groups: nil, without_groups: nil, gems: [], copy_groups: false)
# Parse the desired lockfile
gemfile_path = Pathname.new(gemfile_path).expand_path(Bundler.default_gemfile.dirname).realpath
lockfile_path = Pathname.new(lockfile_path).expand_path(Bundler.default_gemfile.dirname).realpath
# Calculate relative_to
relative_to = Bundler.default_gemfile.dirname.realpath
# Call out to create-override-gemfile to read the Gemfile+Gemfile.lock (bundler does not work well if you do two things in one process)
create_override_gemfile_bin = File.expand_path("../bin/create-override-gemfile", __FILE__)
arguments = [
"--gemfile", gemfile_path,
"--lockfile", lockfile_path,
"--override"
]
arguments += [ "--relative-to", relative_to ] if relative_to != "."
arguments += Array(groups).flat_map { |group| [ "--group", group ] }
arguments += Array(without_groups).flat_map { |without| [ "--without", without ] }
arguments += Array(gems).flat_map { |name| [ "--gem", name ] }
arguments << "--copy-groups" if copy_groups
cmd = Shellwords.join([ Gem.ruby, "-S", create_override_gemfile_bin, *arguments ])
output = nil
Bundler.ui.info("> #{cmd}")
Bundler.with_clean_env do
output = `#{cmd}`
end
instance_eval(output, cmd, 1)
end
#
# Include all gems in the locked gemfile.
#
# @param current_gemfile The Gemfile you are currently loading (`self`).
# @param gemfile_path Path to the Gemfile to load (relative to your Gemfile)
# @param lockfile_path Path to the Gemfile to load (relative to your Gemfile).
# Defaults to <gemfile_path>.lock.
# @param groups A list of groups to include (whitelist). If not passed (or set
# to nil), all gems will be selected.
# @param without_groups A list of groups to ignore. Gems will be excluded from
# the results if all groups they belong to are ignored. This matches
# bundler's `without` behavior.
# @param gems A list of gems to include above and beyond the given groups.
# Gems in this list must be explicitly included in the Gemfile
# with a `gem "gem_name", ...` line or they will be silently
# ignored.
# @param copy_groups Whether to copy the groups over from the old lockfile to
# the new. Use this when the new lockfile has the same convention for
# groups as the old. Defaults to `false`.
#
def self.include_locked_gemfile(current_gemfile, gemfile_path, lockfile_path = "#{gemfile_path}.lock", groups: nil, without_groups: nil, gems: [], copy_groups: false)
current_gemfile.instance_eval do
extend GemfileUtil
include_locked_gemfile(gemfile_path, lockfile_path, groups: groups, without_groups: without_groups, gems: gems, copy_groups: copy_groups)
end
end
def warn_if_replacing(name, old_args, new_args)
return if !old_args || !new_args
if args_to_dep(name, *old_args) =~ args_to_dep(name, *new_args)
Bundler.ui.debug "Replaced Gemfile dependency #{name} (#{old_args}) with (#{new_args})"
else
Bundler.ui.warn "Replaced Gemfile dependency #{name} (#{old_args}) with (#{new_args})"
end
end
def args_to_dep(name, *version, **options)
version = [">= 0"] if version.empty?
Bundler::Dependency.new(name, version, options)
end
#
# Reads a bundle, including a gemfile and lockfile.
#
# Does no validation, does not update the lockfile or its gems in any way.
#
class Bundle
#
# Parse the given gemfile/lockfile pair.
#
# @return [Bundle] The parsed bundle.
#
def self.parse(gemfile_path, lockfile_path = "#{gemfile_path}.lock")
result = new(gemfile_path, lockfile_path)
result.gems
result
end
#
# Create a new Bundle to parse the given gemfile/lockfile pair.
#
def initialize(gemfile_path, lockfile_path = "#{gemfile_path}.lock")
@gemfile_path = gemfile_path
@lockfile_path = lockfile_path
end
#
# The path to the Gemfile
#
attr_reader :gemfile_path
#
# The path to the Lockfile
#
attr_reader :lockfile_path
#
# The list of gems.
#
# @return [Hash<String, Hash>] The resulting gems, where key = gem_name, and the
# hash has:
# - version: version of the gem.
# - source info (:source/:git/:ref/:path) from the lockfile
# - dependencies: A list of gem names this gem has a runtime
# dependency on. Dependencies are transitive: if A depends on B,
# and B depends on C, then A has C in its :dependencies list.
# - development_dependencies: - A list of gem names this gem has a
# development dependency on. Dependencies are transitive: if A
# depends on B, and B depends on C, then A has C in its
# :development_dependencies list. development dependencies *include*
# runtime dependencies.
# - groups: The list of groups (symbols) this gem is in. Groups
# are transitive: if A has a runtime dependency on B, and A is
# in group X, then B is also in group X.
# - declared_groups: The list of groups (symbols) this gem was
# declared in the Gemfile.
#
def gems
@gems ||= begin
gems = locks.dup
gems.each do |name, g|
if gem_declarations.has_key?(name)
g[:declared_groups] = gem_declarations[name][:groups]
else
g[:declared_groups] = []
end
g[:groups] = g[:declared_groups].dup
end
# Transitivize groups (since dependencies are already transitive, this is easy)
gems.each do |name, g|
g[:dependencies].each do |dep|
gems[dep][:groups] |= gems[name][:declared_groups].dup
end
end
gems
end
end
#
# Get the gems (and their deps) in the given group.
#
# @param groups A list of groups to include (whitelist). If not passed (or set
# to nil), all gems will be selected.
# @param without_groups A list of groups to ignore. Gems will be excluded from
# the results if all groups they belong to are ignored.
# This matches bundler's `without` behavior.
# @param gems A list of gems to include regardless of what groups are included.
#
# @return Hash[String, Hash] The resulting gems, where key = gem_name, and the
# hash has:
# - version: version of the gem.
# - source info (:source/:git/:ref/:path) from the lockfile
# - dependencies: A list of gem names this gem has a runtime
# dependency on. Dependencies are transitive: if A depends on B,
# and B depends on C, then A has C in its :dependencies list.
# - development_dependencies: - A list of gem names this gem has a
# development dependency on. Dependencies are transitive: if A
# depends on B, and B depends on C, then A has C in its
# :development_dependencies list. development dependencies
# *include* runtime dependencies.
# - groups: The list of groups (symbols) this gem is in. Groups
# are transitive: if A has a runtime dependency on B, and A is
# in group X, then B is also in group X.
# - declared_groups: The list of groups (symbols) this gem was
# declared in the Gemfile.
#
def select_gems(groups: nil, without_groups: nil)
# First, select the gems that match
result = {}
gems.each do |name, g|
dep_groups = g[:declared_groups] - [ :only_a_runtime_dependency_of_other_gems ]
dep_groups &= groups if groups
dep_groups -= without_groups if without_groups
if dep_groups.any?
result[name] ||= g
g[:dependencies].each do |dep|
result[dep] ||= gems[dep]
end
end
end
result
end
#
# Get all locks from the given lockfile.
#
# @return Hash[String, Hash] The resulting gems, where key = gem_name, and the
# hash has:
# - version: version of the gem.
# - source info (:source/:git/:ref/:path)
# - dependencies: A list of gem names this gem has a runtime
# dependency on. Dependencies are transitive: if A depends on B,
# and B depends on C, then A has C in its :dependencies list.
# - development_dependencies: - A list of gem names this gem has a
# development dependency on. Dependencies are transitive: if A
# depends on B, and B depends on C, then A has C in its
# :development_dependencies list. development dependencies *include*
# runtime dependencies.
#
def locks
@locks ||= begin
# Grab all the specs from the lockfile
locks = {}
parsed_lockfile = Bundler::LockfileParser.new(IO.read(lockfile_path))
parsed_lockfile.specs.each do |spec|
# Never include bundler, it can't be bundled and doesn't put itself in
# the lockfile correctly anyway
next if spec.name == "bundler"
# Only the platform-specific locks for now (TODO make it possible to emit all locks)
next if spec.platform && spec.platform != Gem::Platform::RUBY
lock = lock_source_metadata(spec)
lock[:version] = spec.version.to_s
runtime = spec.dependencies.select { |dep| dep.type == :runtime }
lock[:dependencies] = Set.new(runtime.map { |dep| dep.name })
lock[:development_dependencies] = Set.new(spec.dependencies.map { |dep| dep.name })
lock[:dependencies].delete("bundler")
lock[:development_dependencies].delete("bundler")
locks[spec.name] = lock
end
# Transitivize the deps.
locks.each do |name, lock|
# Not all deps were brought over (platform-specific ones) so weed them out
lock[:dependencies] &= locks.keys
lock[:development_dependencies] &= locks.keys
lock[:dependencies] = transitive_dependencies(locks, name, :dependencies)
lock[:development_dependencies] = transitive_dependencies(locks, name, :development_dependencies)
end
locks
end
end
#
# Get all desired gems, sans dependencies, from the gemfile.
#
# @param gemfile Path to the Gemfile to load
#
# @return Hash<String, Hash> An array of hashes where key = gem name and value
# has :groups (an array of symbols representing the groups the gem
# is in). :groups are not transitive, since we don't know the
# dependency tree yet.
#
def gem_declarations
@gem_declarations ||= begin
Bundler.with_clean_env do
# Set BUNDLE_GEMFILE to the new gemfile temporarily so all bundler's things work
# This works around some issues in bundler 1.11.2.
ENV["BUNDLE_GEMFILE"] = gemfile_path
parsed_gemfile = Bundler::Dsl.new
parsed_gemfile.eval_gemfile(gemfile_path)
parsed_gemfile.complete_overrides if parsed_gemfile.respond_to?(:complete_overrides)
result = {}
parsed_gemfile.dependencies.each do |dep|
groups = dep.groups.empty? ? [:default] : dep.groups
result[dep.name] = { groups: groups, platforms: dep.platforms }
end
result
end
end
end
private
#
# Given a bunch of locks (name -> { dependencies: [name,name] }) and a
# dependency name, add its dependencies to the result transitively.
#
def transitive_dependencies(locks, name, dep_key, result = Set.new)
locks[name][dep_key].each do |dep|
# Only ever add a dep once, so we don't infinitely recurse
if result.add?(dep)
transitive_dependencies(locks, dep, dep_key, result)
end
end
result
end
#
# Get source and version metadata for the given Bundler spec (coming from a lockfile).
#
# @return Hash { version: <version>, git: <git>, path: <path>, source: <source>, ref: <ref> }
#
def lock_source_metadata(spec)
# Copy source information from included Gemfile
result = {}
case spec.source
when Bundler::Source::Rubygems
result[:source] = spec.source.remotes.first.to_s
when Bundler::Source::Git
result[:git] = spec.source.uri.to_s
result[:ref] = spec.source.revision
when Bundler::Source::Path
result[:path] = spec.source.path.to_s
else
raise "Unknown source #{spec.source} for gem #{spec.name}"
end
result
end
end
end
|