bundler-multilock 2.0.0.beta5 → 2.0.0.beta7
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.
- checksums.yaml +4 -4
- data/lib/bundler/multilock/cache.rb +66 -0
- data/lib/bundler/multilock/check.rb +107 -13
- data/lib/bundler/multilock/lockfile_generator.rb +4 -0
- data/lib/bundler/multilock/version.rb +1 -1
- data/lib/bundler/multilock.rb +550 -22
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4be5320cfc5b4d3e13288c22fae8811f3be04f88ca74cc234f4959adcb918f48
|
|
4
|
+
data.tar.gz: 338049458fd3ac3c8b0878803c131824e392747cd30d4ca66abd90ac5bc5d6bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67360e0dab15f5bb6c5fe2fb65ce7b78bbe9db065a37b8d9a35e640ddeab456f94a13cad9ae1d6ccc84b79525f3341f94f494e849194840e1bc6a6ff7cdfa544
|
|
7
|
+
data.tar.gz: 72452dda2334ed58087d938b68961a779acfa42529265181c36209f494e91ac27f07033710f56881a22f90bf2ace8b14561929376251ff9d54625e6d5300f6be
|
|
@@ -12,8 +12,10 @@ module Bundler
|
|
|
12
12
|
@specs = {}
|
|
13
13
|
@reverse_dependencies = {}
|
|
14
14
|
@reverse_requirements = {}
|
|
15
|
+
@dependent_requirements = {}
|
|
15
16
|
@base_checks = {}
|
|
16
17
|
@deep_checks = {}
|
|
18
|
+
@dependency_options = {}
|
|
17
19
|
@base_check_messages = {}
|
|
18
20
|
@deep_check_messages = {}
|
|
19
21
|
@missing_specs = Set.new
|
|
@@ -31,6 +33,7 @@ module Bundler
|
|
|
31
33
|
@specs.delete(lockfile_name)
|
|
32
34
|
@reverse_dependencies.delete(lockfile_name)
|
|
33
35
|
@reverse_requirements.delete(lockfile_name)
|
|
36
|
+
@dependent_requirements.delete(lockfile_name)
|
|
34
37
|
invalidate_checks(lockfile_name)
|
|
35
38
|
end
|
|
36
39
|
|
|
@@ -42,6 +45,24 @@ module Bundler
|
|
|
42
45
|
@deep_check_messages.clear
|
|
43
46
|
end
|
|
44
47
|
|
|
48
|
+
# @param lockfile_name [Pathname]
|
|
49
|
+
# @param options [Hash<String, Array<Hash>>]
|
|
50
|
+
# how the lockfile's Gemfile limits the dependencies that have `platforms:` or `force_ruby_platform`, by
|
|
51
|
+
# gem name, with a hash for each time the gem is declared. `:force_ruby` is if only the source variant is
|
|
52
|
+
# used, and `:platforms` is the lockfile's platforms that it's for (or nil for all of them)
|
|
53
|
+
# @return [void]
|
|
54
|
+
def record_dependency_options(lockfile_name, options)
|
|
55
|
+
@dependency_options[lockfile_name] = options
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# (known once the lockfile's Gemfile has been evaluated by the base check)
|
|
59
|
+
#
|
|
60
|
+
# @param lockfile_name [Pathname]
|
|
61
|
+
# @return [Hash<String, Array<Hash>>] see {#record_dependency_options}
|
|
62
|
+
def dependency_options(lockfile_name)
|
|
63
|
+
@dependency_options.fetch(lockfile_name) { {} }
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
# @param lockfile_name [Pathname]
|
|
46
67
|
# @return [String] the raw contents of the lockfile
|
|
47
68
|
def contents(lockfile_name)
|
|
@@ -92,6 +113,46 @@ module Bundler
|
|
|
92
113
|
@reverse_requirements[lockfile_name]
|
|
93
114
|
end
|
|
94
115
|
|
|
116
|
+
# The gems in a lockfile whose requirements a version of a gem doesn't satisfy, other than
|
|
117
|
+
# the gems that are being ignored (like because they're going to change themselves).
|
|
118
|
+
#
|
|
119
|
+
# Requirements from the lockfile's own dependencies (the Gemfile) are never ignored.
|
|
120
|
+
#
|
|
121
|
+
# @param lockfile_name [Pathname]
|
|
122
|
+
# @param name [String] the gem's name
|
|
123
|
+
# @param version [Gem::Version]
|
|
124
|
+
# @param ignored_dependents [Set<String>] names of gems whose requirements shouldn't be considered
|
|
125
|
+
# @param retained [Set<LazySpecification>] variants of gems that are still considered, even if the gem is ignored
|
|
126
|
+
# @param gemfile_requirements [Hash<String, Gem::Requirement>, nil]
|
|
127
|
+
# the requirements of the lockfile's Gemfile, if they might be different from what's in the lockfile
|
|
128
|
+
# @return [Array<String, nil>] the names of the gems (or nil, for the Gemfile)
|
|
129
|
+
def blocking_dependents(lockfile_name,
|
|
130
|
+
name,
|
|
131
|
+
version,
|
|
132
|
+
ignored_dependents,
|
|
133
|
+
retained: nil,
|
|
134
|
+
gemfile_requirements: nil)
|
|
135
|
+
ensure_reverse_data(lockfile_name)
|
|
136
|
+
blocking = []
|
|
137
|
+
@dependent_requirements[lockfile_name][name].each do |dependent, requirement, spec|
|
|
138
|
+
next if ignored_dependents.include?(dependent) && !retained&.include?(spec)
|
|
139
|
+
# (the lockfile's version of the Gemfile's requirements is replaced below)
|
|
140
|
+
next if dependent.nil? && gemfile_requirements
|
|
141
|
+
|
|
142
|
+
blocking << dependent unless requirement.satisfied_by?(version)
|
|
143
|
+
end
|
|
144
|
+
blocking << nil if gemfile_requirements&.key?(name) && !gemfile_requirements[name].satisfied_by?(version)
|
|
145
|
+
blocking.uniq
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @param lockfile_name [Pathname]
|
|
149
|
+
# @param name [String] the gem's name
|
|
150
|
+
# @param version [Gem::Version]
|
|
151
|
+
# @return [true, false] if a version of a gem satisfies everything in a lockfile that requires it
|
|
152
|
+
def requirements_satisfied?(lockfile_name, name, version)
|
|
153
|
+
reverse_requirements(lockfile_name)[name].satisfied_by?(version)
|
|
154
|
+
end
|
|
155
|
+
|
|
95
156
|
def conflicting_requirements?(lockfile1_name, lockfile2_name, spec1, spec2)
|
|
96
157
|
reverse_requirements1 = reverse_requirements(lockfile1_name)[spec1.name]
|
|
97
158
|
reverse_requirements2 = reverse_requirements(lockfile2_name)[spec1.name]
|
|
@@ -134,21 +195,26 @@ module Bundler
|
|
|
134
195
|
# can use Gem::Requirement.default_prelease when Ruby 2.6 support is dropped
|
|
135
196
|
reverse_requirements = Hash.new { |h, k| h[k] = Gem::Requirement.new(">= 0.a") }
|
|
136
197
|
reverse_dependencies = Hash.new { |h, k| h[k] = Set.new }
|
|
198
|
+
# name => [[dependent name, Gem::Requirement, dependent spec]] (nil for the lockfile's own dependencies)
|
|
199
|
+
dependent_requirements = Hash.new { |h, k| h[k] = [] }
|
|
137
200
|
|
|
138
201
|
lockfile = parser(lockfile_name)
|
|
139
202
|
|
|
140
203
|
lockfile.dependencies.each_value do |dep|
|
|
141
204
|
reverse_requirements[dep.name].requirements.concat(dep.requirement.requirements)
|
|
205
|
+
dependent_requirements[dep.name] << [nil, dep.requirement, nil]
|
|
142
206
|
end
|
|
143
207
|
lockfile.specs.each do |spec|
|
|
144
208
|
spec.dependencies.each do |dep|
|
|
145
209
|
reverse_requirements[dep.name].requirements.concat(dep.requirement.requirements)
|
|
146
210
|
reverse_dependencies[dep.name] << spec.name
|
|
211
|
+
dependent_requirements[dep.name] << [spec.name, dep.requirement, spec]
|
|
147
212
|
end
|
|
148
213
|
end
|
|
149
214
|
|
|
150
215
|
@reverse_requirements[lockfile_name] = reverse_requirements
|
|
151
216
|
@reverse_dependencies[lockfile_name] = reverse_dependencies
|
|
217
|
+
@dependent_requirements[lockfile_name] = dependent_requirements
|
|
152
218
|
end
|
|
153
219
|
end
|
|
154
220
|
end
|
|
@@ -63,6 +63,7 @@ module Bundler
|
|
|
63
63
|
definition = Bundler.with_default_lockfile(lockfile_name) do
|
|
64
64
|
Definition.build(lockfile_definition[:gemfile], lockfile_name, false)
|
|
65
65
|
end
|
|
66
|
+
@cache.record_dependency_options(lockfile_name, dependency_options(definition))
|
|
66
67
|
unless definition.send(:current_platform_locked?)
|
|
67
68
|
next base_check_failed("#{lockfile_path} does not include the current platform " \
|
|
68
69
|
"(#{Bundler.local_platform}).")
|
|
@@ -121,6 +122,28 @@ module Bundler
|
|
|
121
122
|
false
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
# @param definition [Definition]
|
|
126
|
+
# @return [Hash<String, Array<Hash>>] how the Gemfile limits the variants of the gems it has dependencies
|
|
127
|
+
# for, by gem name (see {Cache#record_dependency_options})
|
|
128
|
+
def dependency_options(definition)
|
|
129
|
+
# (a gem can be declared more than once, like in blocks for different platforms)
|
|
130
|
+
definition.dependencies.group_by(&:name).filter_map do |name, dependencies|
|
|
131
|
+
declarations = dependencies.map do |dependency|
|
|
132
|
+
platforms = if dependency.platforms.any?
|
|
133
|
+
definition.platforms.select do |platform|
|
|
134
|
+
dependency.expanded_platforms.include?(Gem::Platform.generic(platform))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
{ force_ruby: dependency.force_ruby_platform ? true : false, platforms: }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# (a declaration that isn't limited at all is for every platform, so that's what the gem is)
|
|
141
|
+
next if declarations.any? { |declaration| !declaration[:force_ruby] && declaration[:platforms].nil? }
|
|
142
|
+
|
|
143
|
+
[name, declarations]
|
|
144
|
+
end.to_h
|
|
145
|
+
end
|
|
146
|
+
|
|
124
147
|
def source_changes_message(definition, lockfile_path, gemfile_path)
|
|
125
148
|
locked = definition.locked_gems.sources
|
|
126
149
|
current = definition.send(:sources).lock_sources
|
|
@@ -133,6 +156,36 @@ module Bundler
|
|
|
133
156
|
message
|
|
134
157
|
end
|
|
135
158
|
|
|
159
|
+
# @param unneeded_specs [Array<LazySpecification>]
|
|
160
|
+
# @param parser [LockfileParser]
|
|
161
|
+
# @return [Array<LazySpecification>] the specs that are only in the lockfile because of the given specs
|
|
162
|
+
# (which aren't included), because they're needed with them, and not without them
|
|
163
|
+
def specs_only_needed_by(unneeded_specs, parser)
|
|
164
|
+
specs_by_name = parser.specs.group_by(&:name)
|
|
165
|
+
reachable_without = lambda do |excluded|
|
|
166
|
+
reachable = Set.new
|
|
167
|
+
pending = parser.dependencies.keys
|
|
168
|
+
until pending.empty?
|
|
169
|
+
name = pending.shift
|
|
170
|
+
next unless reachable.add?(name)
|
|
171
|
+
|
|
172
|
+
specs_by_name[name]&.each do |spec|
|
|
173
|
+
next if excluded.any? { |unneeded| unneeded.equal?(spec) }
|
|
174
|
+
|
|
175
|
+
pending.concat(spec.dependencies.map(&:name))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
reachable
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
needed = reachable_without.call([])
|
|
182
|
+
still_needed = reachable_without.call(unneeded_specs)
|
|
183
|
+
parser.specs.select do |spec|
|
|
184
|
+
needed.include?(spec.name) && !still_needed.include?(spec.name) &&
|
|
185
|
+
unneeded_specs.none? { |unneeded| unneeded.equal?(spec) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
136
189
|
# checks that every gem has a checksum, and that gems in common (same version
|
|
137
190
|
# and source) with the parent lockfile have the same checksums
|
|
138
191
|
def checksums_valid?(parser, parent_parser, lockfile_path)
|
|
@@ -194,6 +247,56 @@ module Bundler
|
|
|
194
247
|
success = false
|
|
195
248
|
end
|
|
196
249
|
|
|
250
|
+
# the variants of gems don't depend on the platforms that are listed, so check them separately
|
|
251
|
+
same_source = lambda do |spec, parent_spec|
|
|
252
|
+
Multilock.same_source?(spec, parent_spec, lockfile_name, parent_lockfile_name)
|
|
253
|
+
end
|
|
254
|
+
stale_variants, missing_variants = Multilock.variant_drift(
|
|
255
|
+
parser.specs,
|
|
256
|
+
parent_parser.specs,
|
|
257
|
+
parser.platforms,
|
|
258
|
+
parent_parser.platforms,
|
|
259
|
+
dependency_names: parser.dependencies.keys,
|
|
260
|
+
dependency_options: @cache.dependency_options(lockfile_name),
|
|
261
|
+
same_source:
|
|
262
|
+
)
|
|
263
|
+
stale_variants += specs_only_needed_by(stale_variants, parser) unless stale_variants.empty?
|
|
264
|
+
|
|
265
|
+
# the source variants of gems that the parent only has other variants of don't have to be in the
|
|
266
|
+
# parent lockfile, and aren't additional dependencies (nor are the gems that only they need).
|
|
267
|
+
# (but the ones the parent does have are just like any other gem, with dependencies that are needed)
|
|
268
|
+
parent_versions = parent_parser.specs.group_by { |spec| [spec.name, spec.version] }
|
|
269
|
+
source_variants = parser.specs.select do |spec|
|
|
270
|
+
next false unless spec.platform == Gem::Platform::RUBY
|
|
271
|
+
next false unless (parent_variants = parent_versions[[spec.name, spec.version]])
|
|
272
|
+
|
|
273
|
+
parent_variants.none? { |parent_spec| parent_spec.platform == Gem::Platform::RUBY } &&
|
|
274
|
+
parent_variants.any? do |parent_spec|
|
|
275
|
+
Multilock.same_source?(spec, parent_spec, lockfile_name, parent_lockfile_name)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
exempt_from_pins = stale_variants + source_variants
|
|
279
|
+
exempt_from_pins += specs_only_needed_by(exempt_from_pins, parser) unless source_variants.empty?
|
|
280
|
+
unless stale_variants.empty? && missing_variants.empty?
|
|
281
|
+
parent_path = parent_lockfile_name.relative_path_from(Dir.pwd)
|
|
282
|
+
if stale_variants.any?
|
|
283
|
+
Bundler.ui.error("#{lockfile_path} has gem variants that #{parent_path} doesn't " \
|
|
284
|
+
"(#{stale_variants.map { |spec| spec.lock_name.strip }.join(", ")}), " \
|
|
285
|
+
"so it is out of sync with it.")
|
|
286
|
+
end
|
|
287
|
+
if missing_variants.any?
|
|
288
|
+
Bundler.ui.error("#{lockfile_path} is missing gem variants that #{parent_path} has " \
|
|
289
|
+
"(#{missing_variants.map { |spec| spec.lock_name.strip }.join(", ")}), " \
|
|
290
|
+
"so it is out of sync with it.")
|
|
291
|
+
end
|
|
292
|
+
if Bundler.frozen_bundle?
|
|
293
|
+
Bundler.ui.error("That can't be fixed while the bundle is frozen. Run `bundle install` " \
|
|
294
|
+
"without frozen mode, and commit the updated #{lockfile_path}.")
|
|
295
|
+
end
|
|
296
|
+
success = false
|
|
297
|
+
end
|
|
298
|
+
exempt_from_pins = exempt_from_pins.to_set { |spec| [spec.name, spec.version] }
|
|
299
|
+
|
|
197
300
|
# look through top-level explicit dependencies for pinned requirements
|
|
198
301
|
if lockfile_definition[:enforce_pinned_additional_dependencies]
|
|
199
302
|
find_pinned_dependencies(proven_pinned, parser.dependencies.each_value)
|
|
@@ -207,23 +310,14 @@ module Bundler
|
|
|
207
310
|
# look through what this spec depends on, and keep track of all pinned requirements
|
|
208
311
|
find_pinned_dependencies(proven_pinned, spec.dependencies)
|
|
209
312
|
|
|
210
|
-
|
|
313
|
+
# (already reported, and not actually additional)
|
|
314
|
+
needs_pin_check << spec unless parent_spec || exempt_from_pins.include?([spec.name, spec.version])
|
|
211
315
|
end
|
|
212
316
|
|
|
213
317
|
next unless parent_spec
|
|
214
318
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
lockfile_name
|
|
218
|
-
.dirname
|
|
219
|
-
.join(spec.source.path)
|
|
220
|
-
.ascend
|
|
221
|
-
.any?(parent_lockfile_name.dirname.join(parent_spec.source.path))
|
|
222
|
-
else
|
|
223
|
-
parent_spec.source == spec.source
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
next if parent_spec.version == spec.version && same_source
|
|
319
|
+
next if parent_spec.version == spec.version &&
|
|
320
|
+
Multilock.same_source?(spec, parent_spec, lockfile_name, parent_lockfile_name)
|
|
227
321
|
|
|
228
322
|
# the version in the parent lockfile cannot possibly satisfy the requirements
|
|
229
323
|
# in this lockfile, and vice versa, so we assume it's intentional and allow it
|
data/lib/bundler/multilock.rb
CHANGED
|
@@ -219,6 +219,7 @@ module Bundler
|
|
|
219
219
|
|
|
220
220
|
added_platforms = []
|
|
221
221
|
removed_platforms = []
|
|
222
|
+
unlock_gems = Set.new
|
|
222
223
|
if lockfile_name.exist?
|
|
223
224
|
# if the lockfile already exists, "merge" it together
|
|
224
225
|
parent_lockfile = if adjusted_parent_lockfile_contents == cache.contents(lockfile_name)
|
|
@@ -231,24 +232,13 @@ module Bundler
|
|
|
231
232
|
|
|
232
233
|
dependency_changes = false
|
|
233
234
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
precedence = if !(cache.reverse_dependencies(lockfile_name)[spec.name] & conflicts).empty? # rubocop:disable Style/ArrayIntersect -- not an array
|
|
242
|
-
:parent
|
|
243
|
-
elsif cache.conflicting_requirements?(lockfile_name,
|
|
244
|
-
parent_lockfile_name,
|
|
245
|
-
spec,
|
|
246
|
-
parent_spec)
|
|
247
|
-
:self
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
spec_precedences[spec.name] = precedence || :parent
|
|
251
|
-
end
|
|
235
|
+
precedences, unlock_gems = spec_precedences(
|
|
236
|
+
cache,
|
|
237
|
+
lockfile_name,
|
|
238
|
+
parent_lockfile_name,
|
|
239
|
+
conflicts,
|
|
240
|
+
gemfile_requirements: gemfile_requirements(lockfile_definition)
|
|
241
|
+
)
|
|
252
242
|
|
|
253
243
|
lockfile.sources.map! do |source|
|
|
254
244
|
parent_lockfile.sources.find { |s| s == source } || source
|
|
@@ -257,7 +247,7 @@ module Bundler
|
|
|
257
247
|
lockfile.specs.map! do |spec|
|
|
258
248
|
parent_spec = cache.find_matching_spec(parent_specs, spec)
|
|
259
249
|
next spec unless parent_spec
|
|
260
|
-
next spec if
|
|
250
|
+
next spec if precedences[spec.name] == :self
|
|
261
251
|
|
|
262
252
|
dependency_changes ||= spec != parent_spec
|
|
263
253
|
|
|
@@ -293,6 +283,37 @@ module Bundler
|
|
|
293
283
|
# so that it also adds or removes the gems needed for them
|
|
294
284
|
removed_platforms = lockfile.platforms - parent_lockfile.platforms
|
|
295
285
|
added_platforms = parent_lockfile.platforms - lockfile.platforms
|
|
286
|
+
|
|
287
|
+
# the lockfiles can agree on platforms, but still not on the variants of gems for them (perhaps
|
|
288
|
+
# due to merging of semantics conflicts), so clean up and/or add any missing variants.
|
|
289
|
+
# (the parent lockfile's specs have had their paths rebased to be relative to this lockfile)
|
|
290
|
+
same_source = lambda do |spec, parent_spec|
|
|
291
|
+
same_source?(spec, parent_spec, lockfile_name, lockfile_name)
|
|
292
|
+
end
|
|
293
|
+
stale_variants, missing_variants = variant_drift(
|
|
294
|
+
lockfile.specs,
|
|
295
|
+
parent_lockfile.specs,
|
|
296
|
+
lockfile.platforms,
|
|
297
|
+
parent_lockfile.platforms,
|
|
298
|
+
dependency_names: lockfile.dependencies.keys,
|
|
299
|
+
dependency_options: cache.dependency_options(lockfile_name),
|
|
300
|
+
same_source:
|
|
301
|
+
)
|
|
302
|
+
stale_variants.map(&:platform).uniq.each do |platform|
|
|
303
|
+
entries = matching_platforms(lockfile.platforms, platform)
|
|
304
|
+
# Bundler can only remove a platform that's listed (and gems can have variants for
|
|
305
|
+
# platforms, like ruby, that never are)
|
|
306
|
+
lockfile.platforms << platform if entries.empty?
|
|
307
|
+
removed_platforms |= entries.empty? ? [platform] : entries
|
|
308
|
+
end
|
|
309
|
+
missing_variants.map(&:platform).uniq.each do |platform|
|
|
310
|
+
# (only platforms that the parent still lists)
|
|
311
|
+
entries = matching_platforms(lockfile.platforms, platform) & parent_lockfile.platforms
|
|
312
|
+
# and it can only add a platform that isn't listed
|
|
313
|
+
lockfile.platforms.reject! { |entry| entries.include?(entry) }
|
|
314
|
+
added_platforms |= entries
|
|
315
|
+
end
|
|
316
|
+
removed_platforms -= added_platforms
|
|
296
317
|
lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version)
|
|
297
318
|
unless lockfile.bundler_version == parent_lockfile.bundler_version
|
|
298
319
|
unlocking_bundler = parent_lockfile.bundler_version
|
|
@@ -322,7 +343,8 @@ module Bundler
|
|
|
322
343
|
dependency_changes:,
|
|
323
344
|
unlocking_bundler:,
|
|
324
345
|
added_platforms:,
|
|
325
|
-
removed_platforms
|
|
346
|
+
removed_platforms:,
|
|
347
|
+
unlock_gems:)
|
|
326
348
|
end
|
|
327
349
|
end
|
|
328
350
|
cache.invalidate_lockfile(lockfile_name) if had_changes
|
|
@@ -393,6 +415,505 @@ module Bundler
|
|
|
393
415
|
(relative == "active") ? [lockfile, relative] : [lockfile]
|
|
394
416
|
end
|
|
395
417
|
|
|
418
|
+
# @!visibility private
|
|
419
|
+
# @param lockfile_definition [Hash]
|
|
420
|
+
# @return [Hash<String, Gem::Requirement>] the requirements that the lockfile's Gemfile has for its
|
|
421
|
+
# dependencies, which are only in the lockfile itself after it's synced
|
|
422
|
+
def gemfile_requirements(lockfile_definition)
|
|
423
|
+
gemfile = lockfile_definition[:gemfile]
|
|
424
|
+
prepare_block = lockfile_definition[:prepare]
|
|
425
|
+
|
|
426
|
+
builder = Dsl.new
|
|
427
|
+
builder.eval_gemfile(gemfile, &prepare_block) if prepare_block
|
|
428
|
+
builder.eval_gemfile(gemfile)
|
|
429
|
+
builder.dependencies.group_by(&:name).transform_values do |dependencies|
|
|
430
|
+
Gem::Requirement.new(dependencies.flat_map { |dependency| dependency.requirement.as_list })
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# @!visibility private
|
|
435
|
+
# Decides, for each gem in a lockfile that is also in its parent lockfile, which one's version wins when
|
|
436
|
+
# syncing the lockfile.
|
|
437
|
+
#
|
|
438
|
+
# A gem follows the parent, unless something that's staying as it is requires its version, and the
|
|
439
|
+
# parent can't use that version either (so it's different on purpose). Gems that follow the parent
|
|
440
|
+
# have different requirements from what they had, so they don't count. Neither do gems that nothing
|
|
441
|
+
# needs any more, once the others follow the parent. If the parent can use the version, the gems that
|
|
442
|
+
# are in the way have to be updated instead, so they're unlocked. And they also don't count, since
|
|
443
|
+
# they'll have different requirements, once they're updated.
|
|
444
|
+
#
|
|
445
|
+
# Each of those can change the others, so this starts out assuming that every gem that's different from
|
|
446
|
+
# the parent is going to follow it, and repeats, until that matches which ones actually do. And then
|
|
447
|
+
# repeats that, until the gems that are unlocked don't change.
|
|
448
|
+
#
|
|
449
|
+
# @param cache [Cache]
|
|
450
|
+
# @param lockfile_name [Pathname]
|
|
451
|
+
# @param parent_lockfile_name [Pathname]
|
|
452
|
+
# @param conflicts [Set<String>] names of gems that are out of sync, with no reason to be
|
|
453
|
+
# @param gemfile_requirements [Hash<String, Gem::Requirement>, nil]
|
|
454
|
+
# the requirements of the lockfile's Gemfile. The lockfile's own copy of them may be about to change
|
|
455
|
+
# @return [Array(Hash<String, Symbol>, Set<String>)]
|
|
456
|
+
# gem name to +:parent+ or +:self+, and the names of the gems that have to be unlocked for the ones
|
|
457
|
+
# that follow the parent to be able to
|
|
458
|
+
def spec_precedences(cache, lockfile_name, parent_lockfile_name, conflicts, gemfile_requirements: nil)
|
|
459
|
+
parent_specs = cache.specs(parent_lockfile_name)
|
|
460
|
+
own_specs = cache.specs(lockfile_name)
|
|
461
|
+
roots = gemfile_requirements&.keys || cache.parser(lockfile_name).dependencies.keys
|
|
462
|
+
|
|
463
|
+
# every platform variant of a gem gets replaced with its parent's, so any of them can change
|
|
464
|
+
variants = Hash.new { |hash, name| hash[name] = [] }
|
|
465
|
+
cache.parser(lockfile_name).specs.each do |spec|
|
|
466
|
+
next unless (parent_spec = cache.find_matching_spec(parent_specs, spec))
|
|
467
|
+
|
|
468
|
+
variants[spec.name] << [spec, parent_spec]
|
|
469
|
+
end
|
|
470
|
+
# (and the ones that the variants the lockfile has are replaced with)
|
|
471
|
+
matched = variants.transform_values { |gem_variants| gem_variants.map(&:last).uniq }
|
|
472
|
+
|
|
473
|
+
# other variants of a gem that follows the parent, that the parent doesn't have a counterpart for,
|
|
474
|
+
# are kept as they are
|
|
475
|
+
unmatched = cache.parser(lockfile_name).specs.select do |spec|
|
|
476
|
+
variants.key?(spec.name) && !cache.find_matching_spec(parent_specs, spec)
|
|
477
|
+
end.group_by(&:name)
|
|
478
|
+
|
|
479
|
+
# a gem can change what it depends on without changing its version (like a git gem)
|
|
480
|
+
differing = variants.select do |_name, gem_variants|
|
|
481
|
+
gem_variants.any? { |spec, parent_spec| spec_changed?(spec, parent_spec) }
|
|
482
|
+
end.keys
|
|
483
|
+
|
|
484
|
+
# the variants that stay, but that cleaning up the variants after replacing the gems will remove
|
|
485
|
+
counterparts = {}.compare_by_identity
|
|
486
|
+
variants.each_value do |gem_variants|
|
|
487
|
+
gem_variants.each { |spec, parent_spec| counterparts[spec] = parent_spec }
|
|
488
|
+
end
|
|
489
|
+
removed = lambda do |following_parent|
|
|
490
|
+
merged = cache.parser(lockfile_name).specs.map do |spec|
|
|
491
|
+
following_parent.include?(spec.name) ? counterparts.fetch(spec, spec) : spec
|
|
492
|
+
end
|
|
493
|
+
stale, = variant_drift(merged,
|
|
494
|
+
cache.parser(parent_lockfile_name).specs,
|
|
495
|
+
cache.parser(lockfile_name).platforms,
|
|
496
|
+
cache.parser(parent_lockfile_name).platforms,
|
|
497
|
+
dependency_names: roots.to_a,
|
|
498
|
+
dependency_options: cache.dependency_options(lockfile_name),
|
|
499
|
+
same_source: lambda do |spec, parent_spec|
|
|
500
|
+
same_source?(spec, parent_spec, lockfile_name, parent_lockfile_name)
|
|
501
|
+
end)
|
|
502
|
+
stale.to_set & unmatched.values.flatten
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# the specs of gems that only the parent has, that this lockfile could get (if the Gemfile can use them)
|
|
506
|
+
importable = lambda do |name|
|
|
507
|
+
parent_specs[name]&.values&.select do |spec|
|
|
508
|
+
!gemfile_requirements&.key?(name) || gemfile_requirements[name].satisfied_by?(spec.version)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
unneeded = lambda do |following_parent, removed_variants|
|
|
513
|
+
staying = unmatched.transform_values { |specs| specs.reject { |spec| removed_variants.include?(spec) } }
|
|
514
|
+
own_specs.keys.to_set - needed_gems(roots, own_specs, importable, following_parent, matched, staying)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
unlock = Set.new
|
|
518
|
+
loop do
|
|
519
|
+
precedences, blockers = converge_precedences(cache,
|
|
520
|
+
lockfile_name,
|
|
521
|
+
parent_lockfile_name,
|
|
522
|
+
variants,
|
|
523
|
+
conflicts.to_set | differing,
|
|
524
|
+
differing:,
|
|
525
|
+
unlock:,
|
|
526
|
+
removed:,
|
|
527
|
+
unneeded:,
|
|
528
|
+
roots:,
|
|
529
|
+
importable:,
|
|
530
|
+
retained: unmatched.values.flatten.to_set,
|
|
531
|
+
gemfile_requirements:)
|
|
532
|
+
|
|
533
|
+
return [precedences, unlock] if blockers.subset?(unlock)
|
|
534
|
+
|
|
535
|
+
unlock |= blockers
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# @!visibility private
|
|
540
|
+
# (see #spec_precedences)
|
|
541
|
+
# @param variants [Hash<String, Array<Array(LazySpecification, LazySpecification)>>]
|
|
542
|
+
# gem name to each of its specs, and its spec in the parent lockfile
|
|
543
|
+
# @param out_of_sync [Set<String>] the gems assumed to follow the parent to start with
|
|
544
|
+
# @param differing [Array<String>] the gems that follow the parent, if they can
|
|
545
|
+
# @param unlock [Set<String>] gems that are already going to be unlocked
|
|
546
|
+
# @param removed [#call] given the gems that follow the parent, the variants that stay as they are, but
|
|
547
|
+
# that are going to be removed
|
|
548
|
+
# @param unneeded [#call] given the gems that follow the parent, and the variants that are going to be
|
|
549
|
+
# removed, the gems that nothing needs any more
|
|
550
|
+
# @param roots [Enumerable<String>] the names of the gems that are in the Gemfile
|
|
551
|
+
# @param importable [#call] given the name of a gem that only the parent has, the specs of it that could be used
|
|
552
|
+
# @param retained [Set<LazySpecification>] variants of gems that stay as they are, even if the gem follows
|
|
553
|
+
# the parent
|
|
554
|
+
# @return [Array(Hash<String, Symbol>, Set<String>)] the precedences, and the gems that are in the way
|
|
555
|
+
def converge_precedences(cache,
|
|
556
|
+
lockfile_name,
|
|
557
|
+
parent_lockfile_name,
|
|
558
|
+
variants,
|
|
559
|
+
out_of_sync,
|
|
560
|
+
differing:,
|
|
561
|
+
unlock:,
|
|
562
|
+
removed:,
|
|
563
|
+
unneeded:,
|
|
564
|
+
roots:,
|
|
565
|
+
importable:,
|
|
566
|
+
retained:,
|
|
567
|
+
gemfile_requirements:)
|
|
568
|
+
seen = Set.new
|
|
569
|
+
own_specs = cache.specs(lockfile_name)
|
|
570
|
+
# what gems are replaced with, which is all of the variants of gems that this lockfile gets
|
|
571
|
+
matched = variants.transform_values { |gem_variants| gem_variants.map(&:last).uniq }
|
|
572
|
+
effective_specs = ->(name) { matched[name] || importable.call(name) }
|
|
573
|
+
|
|
574
|
+
loop do
|
|
575
|
+
removed_now = removed.call(out_of_sync)
|
|
576
|
+
unneeded_now = unneeded.call(out_of_sync, removed_now)
|
|
577
|
+
ignored = out_of_sync | unneeded_now | unlock
|
|
578
|
+
# (a variant that stays doesn't require anything, if nothing needs the gem any more, or it's updated,
|
|
579
|
+
# or it's going to be removed)
|
|
580
|
+
kept = retained.reject do |spec|
|
|
581
|
+
unneeded_now.include?(spec.name) || unlock.include?(spec.name) || removed_now.include?(spec)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# gems that only the parent has, that gems following the parent bring along, require things too
|
|
585
|
+
# (but not gems that won't be needed any more, and only for the variants that are used)
|
|
586
|
+
following = out_of_sync - unneeded_now
|
|
587
|
+
following |= imported_gems(effective_specs, own_specs, following, roots)
|
|
588
|
+
|
|
589
|
+
blockers = Set.new
|
|
590
|
+
precedences = variants.to_h do |name, gem_variants|
|
|
591
|
+
follow_parent = true
|
|
592
|
+
blocking_gems = Set.new
|
|
593
|
+
|
|
594
|
+
gem_variants.each do |spec, parent_spec|
|
|
595
|
+
next if spec.version == parent_spec.version
|
|
596
|
+
|
|
597
|
+
blocking = cache.blocking_dependents(lockfile_name,
|
|
598
|
+
name,
|
|
599
|
+
parent_spec.version,
|
|
600
|
+
ignored,
|
|
601
|
+
retained: kept,
|
|
602
|
+
gemfile_requirements:)
|
|
603
|
+
|
|
604
|
+
# something that's still needed, and not out of sync, requires this version. if that's the
|
|
605
|
+
# Gemfile, it can't be changed. if the parent lockfile is also unable to use this version, it's
|
|
606
|
+
# different on purpose, unless a gem following the parent requires it to change. otherwise it
|
|
607
|
+
# still follows the parent, and what's in the way has to be updated, to be able to
|
|
608
|
+
if blocking.include?(nil) ||
|
|
609
|
+
(blocking.any? &&
|
|
610
|
+
!cache.requirements_satisfied?(parent_lockfile_name, name, spec.version) &&
|
|
611
|
+
!required_to_change?(cache.reverse_dependencies(parent_lockfile_name)[name] & following,
|
|
612
|
+
effective_specs,
|
|
613
|
+
name,
|
|
614
|
+
spec.version))
|
|
615
|
+
follow_parent = false
|
|
616
|
+
else
|
|
617
|
+
blocking_gems.merge(blocking)
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
blockers.merge(blocking_gems) if follow_parent
|
|
622
|
+
[name, follow_parent ? :parent : :self]
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
replaced = differing.select { |name| precedences[name] == :parent }.to_set
|
|
626
|
+
# (what a gem depends on can change which gems are needed in either direction, so it's possible,
|
|
627
|
+
# but unlikely, to go in circles. Bundler will sort out what's left over, in that case)
|
|
628
|
+
return [precedences, blockers] if replaced == out_of_sync || !seen.add?(replaced)
|
|
629
|
+
|
|
630
|
+
out_of_sync = replaced
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# @!visibility private
|
|
635
|
+
# @param dependents [Enumerable<String>] gems that are following the parent, that depend on the gem in the
|
|
636
|
+
# parent lockfile
|
|
637
|
+
# @param effective_specs [#call] given a gem's name, the specs in the parent lockfile that it's replaced with
|
|
638
|
+
# @return [true, false] if what any of the gems require from the parent lockfile excludes +version+ of +name+
|
|
639
|
+
def required_to_change?(dependents, effective_specs, name, version)
|
|
640
|
+
dependents.any? do |dependent|
|
|
641
|
+
effective_specs.call(dependent)&.any? do |spec|
|
|
642
|
+
spec.dependencies.any? do |dependency|
|
|
643
|
+
dependency.name == name && !dependency.requirement.satisfied_by?(version)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# @!visibility private
|
|
650
|
+
# @return [true, false] if syncing +spec+ to +parent_spec+ changes what it is, or what it depends on
|
|
651
|
+
def spec_changed?(spec, parent_spec)
|
|
652
|
+
return true unless spec.version == parent_spec.version
|
|
653
|
+
|
|
654
|
+
dependencies = lambda do |other|
|
|
655
|
+
other.dependencies.map { |dependency| [dependency.name, dependency.requirement.as_list.sort] }.sort
|
|
656
|
+
end
|
|
657
|
+
dependencies.call(spec) != dependencies.call(parent_spec)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# @!visibility private
|
|
661
|
+
# @param roots [Enumerable<String>] the names of the gems that are in the Gemfile
|
|
662
|
+
# @param specs [Hash<String, Hash<Object, LazySpecification>>] a lockfile's specs
|
|
663
|
+
# @param importable [#call] given the name of a gem that only the parent has, the specs of it that could be used
|
|
664
|
+
# @param following_parent [Set<String>] gems that will have their dependencies from the parent lockfile
|
|
665
|
+
# @param matched [Hash<String, Array<LazySpecification>>]
|
|
666
|
+
# the specs in the parent lockfile that the variants of gems are replaced with
|
|
667
|
+
# @param unmatched [Hash<String, Array<LazySpecification>>]
|
|
668
|
+
# variants of gems that stay as they are, even for gems that are following the parent
|
|
669
|
+
# @return [Set<String>] the names of the gems that are needed by the Gemfile, once the gems that are
|
|
670
|
+
# following the parent do
|
|
671
|
+
def needed_gems(roots, specs, importable, following_parent, matched, unmatched)
|
|
672
|
+
needed = Set.new
|
|
673
|
+
pending = roots.to_a.dup
|
|
674
|
+
until pending.empty?
|
|
675
|
+
name = pending.shift
|
|
676
|
+
next unless needed.add?(name)
|
|
677
|
+
|
|
678
|
+
variants = if following_parent.include?(name) && matched.key?(name)
|
|
679
|
+
matched[name] + unmatched.fetch(name, [])
|
|
680
|
+
elsif specs[name]
|
|
681
|
+
specs[name].values
|
|
682
|
+
else
|
|
683
|
+
# (a gem that the parent has, that this lockfile will get)
|
|
684
|
+
importable.call(name)
|
|
685
|
+
end
|
|
686
|
+
variants&.each { |spec| pending.concat(spec.dependencies.map(&:name)) }
|
|
687
|
+
end
|
|
688
|
+
needed
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# @!visibility private
|
|
692
|
+
# @param effective_specs [#call] given a gem's name, the specs in the parent lockfile that it's replaced with
|
|
693
|
+
# @param specs [Hash<String, Hash<Object, LazySpecification>>] the lockfile's specs
|
|
694
|
+
# @param following_parent [Set<String>] gems that will have their dependencies from the parent lockfile
|
|
695
|
+
# @param roots [Enumerable<String>] the names of the gems that are in the Gemfile
|
|
696
|
+
# @return [Set<String>] the names of the gems that the parent lockfile has, that the lockfile doesn't, but
|
|
697
|
+
# will get, because the Gemfile or the gems following the parent need them
|
|
698
|
+
def imported_gems(effective_specs, specs, following_parent, roots)
|
|
699
|
+
imported = Set.new
|
|
700
|
+
pending = following_parent.to_a
|
|
701
|
+
# (a gem that was just added to the Gemfile)
|
|
702
|
+
roots.each do |name|
|
|
703
|
+
pending << name if !specs.key?(name) && effective_specs.call(name) && imported.add?(name)
|
|
704
|
+
end
|
|
705
|
+
until pending.empty?
|
|
706
|
+
effective_specs.call(pending.shift)&.each do |spec|
|
|
707
|
+
spec.dependencies.each do |dependency|
|
|
708
|
+
next if specs.key?(dependency.name) || !imported.add?(dependency.name)
|
|
709
|
+
|
|
710
|
+
pending << dependency.name
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
imported
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# @!visibility private
|
|
718
|
+
# Finds variants of gems (a gem version for a particular platform) that are in only one of two lockfiles,
|
|
719
|
+
# even though both of them have that version of the gem (or this one has a version left behind next to
|
|
720
|
+
# one the parent has), and the platform is one that only one of them is for. The source variant of a
|
|
721
|
+
# gem is never stale for a version of it that the parent lockfile has.
|
|
722
|
+
#
|
|
723
|
+
# The lockfiles can't be compared by their platforms alone, since gems can have variants for platforms
|
|
724
|
+
# (like ruby) that aren't listed, and what gems this lockfile needs on each platform depends on what its
|
|
725
|
+
# Gemfile declares (see {#gems_needed_on}). A variant is only missing if this lockfile needs the gem for
|
|
726
|
+
# the platform, and would use that variant.
|
|
727
|
+
#
|
|
728
|
+
# Gems that this lockfile has `platforms:` or `force_ruby_platform` for, and gems that it gets from a
|
|
729
|
+
# different source than the parent lockfile does, aren't expected to have the same variants.
|
|
730
|
+
#
|
|
731
|
+
# @param specs [Array<LazySpecification>]
|
|
732
|
+
# @param parent_specs [Array<LazySpecification>]
|
|
733
|
+
# @param platforms [Array<Gem::Platform, String>] the platforms listed in the lockfile for +specs+
|
|
734
|
+
# @param parent_platforms [Array<Gem::Platform, String>] the platforms listed in the parent lockfile
|
|
735
|
+
# @param dependency_names [Array<String>, nil]
|
|
736
|
+
# the gems that are dependencies in the Gemfile for +specs+ (nil if it's not known, so any gem is needed)
|
|
737
|
+
# @param dependency_options [Hash<String, Array<Hash>>] see {Cache#record_dependency_options}
|
|
738
|
+
# @param same_source [#call] given a spec and a parent lockfile spec, if they're from the same source
|
|
739
|
+
# @return [Array(Array<LazySpecification>, Array<LazySpecification>)]
|
|
740
|
+
# the variants only in +specs+ (that the parent lockfile's platforms don't include), and the ones only in
|
|
741
|
+
# +parent_specs+ (that +platforms+ do include, and +specs+ need)
|
|
742
|
+
def variant_drift(specs,
|
|
743
|
+
parent_specs,
|
|
744
|
+
platforms,
|
|
745
|
+
parent_platforms,
|
|
746
|
+
dependency_names: nil,
|
|
747
|
+
dependency_options: {},
|
|
748
|
+
same_source: ->(spec, parent_spec) { spec.source == parent_spec.source })
|
|
749
|
+
ours = specs.group_by { |spec| [spec.name, spec.version] }
|
|
750
|
+
theirs = parent_specs.group_by { |spec| [spec.name, spec.version] }
|
|
751
|
+
ours_by_name = specs.group_by(&:name)
|
|
752
|
+
|
|
753
|
+
# a version of a gem that the parent doesn't have is left behind (instead of intentionally different)
|
|
754
|
+
# if this lockfile also has a version of that gem that the parent lockfile does have
|
|
755
|
+
left_behind = lambda do |spec|
|
|
756
|
+
ours_by_name[spec.name].any? do |other|
|
|
757
|
+
other.version != spec.version &&
|
|
758
|
+
theirs[[other.name, other.version]]&.any? { |parent| same_platform?(parent.platform, other.platform) }
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# (other variants of the gem from a different source are expected to be different)
|
|
763
|
+
# (and the source of this lockfile's spec is always the first one to compare)
|
|
764
|
+
from_same_source = ->(spec, parents) { parents.any? { |parent| same_source.call(spec, parent) } }
|
|
765
|
+
|
|
766
|
+
stale = specs.select do |spec|
|
|
767
|
+
next false if matching_platforms(parent_platforms, spec.platform).any?
|
|
768
|
+
# Bundler keeps (or adds) the source variant of a gem next to the platform specific ones, whether
|
|
769
|
+
# or not the ruby platform is listed, so it's not stale for a version of the gem that the parent has
|
|
770
|
+
next false if spec.platform == Gem::Platform::RUBY && theirs.key?([spec.name, spec.version])
|
|
771
|
+
|
|
772
|
+
if (parent_variants = theirs[[spec.name, spec.version]])
|
|
773
|
+
from_same_source.call(spec, parent_variants) &&
|
|
774
|
+
parent_variants.none? { |parent| same_platform?(parent.platform, spec.platform) }
|
|
775
|
+
else
|
|
776
|
+
left_behind.call(spec)
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# the variants of the parent lockfile that this lockfile would use, for the platforms they both have,
|
|
781
|
+
# when it doesn't have a variant it can use instead on that platform
|
|
782
|
+
missing = Set.new
|
|
783
|
+
platforms.each do |platform|
|
|
784
|
+
next if parent_platforms.none? { |other| same_platform?(other, platform) }
|
|
785
|
+
|
|
786
|
+
gems_needed_on(platform, ours_by_name, dependency_names, dependency_options).each do |name, force_ruby|
|
|
787
|
+
# (a gem that's forced to the ruby platform only uses its source variant)
|
|
788
|
+
target = force_ruby ? Gem::Platform::RUBY : platform
|
|
789
|
+
|
|
790
|
+
# (not every gem that's needed is in the lockfile, like bundler itself)
|
|
791
|
+
ours_by_name.fetch(name, []).map(&:version).uniq.each do |version|
|
|
792
|
+
next unless (parent_variants = theirs[[name, version]])
|
|
793
|
+
|
|
794
|
+
variants = ours[[name, version]]
|
|
795
|
+
MatchPlatform.select_best_platform_match(parent_variants, target).each do |expected|
|
|
796
|
+
# (both lockfiles have to list the platform. if the parent doesn't, its variant is just a
|
|
797
|
+
# source variant that Bundler kept, and this lockfile having removed the platform is what
|
|
798
|
+
# needs to sync)
|
|
799
|
+
next unless matching_platforms(platforms, expected.platform).any? &&
|
|
800
|
+
matching_platforms(parent_platforms, expected.platform).any?
|
|
801
|
+
next unless variants.any? { |variant| same_source.call(variant, expected) }
|
|
802
|
+
|
|
803
|
+
missing << expected unless usable_instead?(variants, expected, target)
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
missing = missing.to_a
|
|
809
|
+
|
|
810
|
+
[stale, missing]
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# @!visibility private
|
|
814
|
+
# If a variant of a gem (that the lockfile has) could be used on a platform in place of a variant of the
|
|
815
|
+
# same gem from the parent lockfile, because it's at least as specific to the platform.
|
|
816
|
+
#
|
|
817
|
+
# @param variants [Array<LazySpecification>] the variants of the gem that the lockfile has
|
|
818
|
+
# @param expected [LazySpecification] the variant of the gem from the parent lockfile
|
|
819
|
+
# @param platform [Gem::Platform, String] the platform the variants would be used for
|
|
820
|
+
def usable_instead?(variants, expected, platform)
|
|
821
|
+
specificity = ->(spec) { Gem::Platform.platform_specificity_match(spec.platform, platform) }
|
|
822
|
+
|
|
823
|
+
MatchPlatform.select_best_platform_match(variants, platform).any? do |variant|
|
|
824
|
+
specificity.call(variant) <= specificity.call(expected)
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# @!visibility private
|
|
829
|
+
# The gems that a Gemfile needs on a platform, the same way Bundler works them out for a lockfile
|
|
830
|
+
# (see Bundler::Materialization): starting with the dependencies in the Gemfile that are for the
|
|
831
|
+
# platform, and then what the variant of each gem that would be used for the platform depends on.
|
|
832
|
+
# (A variant of a gem for a different platform may depend on different gems.)
|
|
833
|
+
#
|
|
834
|
+
# A gem can be declared in the Gemfile more than once, such as for different platforms, and only the
|
|
835
|
+
# declarations for the platform are considered.
|
|
836
|
+
#
|
|
837
|
+
# @param platform [Gem::Platform, String] one of the platforms listed in the lockfile
|
|
838
|
+
# @param specs_by_name [Hash<String, Array<LazySpecification>>] the specs of the lockfile
|
|
839
|
+
# @param dependency_names [Array<String>, nil] the gems that are dependencies in the Gemfile
|
|
840
|
+
# (nil if it's not known, so any gem is needed)
|
|
841
|
+
# @param dependency_options [Hash<String, Array<Hash>>] see {Cache#record_dependency_options}
|
|
842
|
+
# @return [Hash<String, true or false>] the gems that are needed, and if only the source variant is used
|
|
843
|
+
def gems_needed_on(platform, specs_by_name, dependency_names, dependency_options)
|
|
844
|
+
declarations_for = lambda do |name|
|
|
845
|
+
dependency_options.fetch(name, []).select do |declaration|
|
|
846
|
+
declaration[:platforms].nil? || declaration[:platforms].any? { |other| same_platform?(other, platform) }
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
pending = if dependency_names
|
|
851
|
+
dependency_names.select do |name|
|
|
852
|
+
!dependency_options.key?(name) || declarations_for.call(name).any?
|
|
853
|
+
end
|
|
854
|
+
else
|
|
855
|
+
specs_by_name.keys
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
needed = {}
|
|
859
|
+
until pending.empty?
|
|
860
|
+
name = pending.shift
|
|
861
|
+
next if needed.key?(name)
|
|
862
|
+
|
|
863
|
+
declarations = declarations_for.call(name)
|
|
864
|
+
force_ruby = declarations.any? && declarations.all? { |declaration| declaration[:force_ruby] }
|
|
865
|
+
needed[name] = force_ruby
|
|
866
|
+
next unless (variants = specs_by_name[name])
|
|
867
|
+
|
|
868
|
+
# (not `force_ruby:`, since that changes the specs)
|
|
869
|
+
selected = MatchPlatform.select_best_platform_match(variants, force_ruby ? Gem::Platform::RUBY : platform)
|
|
870
|
+
selected.each { |spec| pending.concat(spec.dependencies.map(&:name)) }
|
|
871
|
+
end
|
|
872
|
+
needed
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
# @!visibility private
|
|
876
|
+
# @return [true, false] if two specs, from different lockfiles, are from the same source
|
|
877
|
+
def same_source?(spec, parent_spec, lockfile_name, parent_lockfile_name)
|
|
878
|
+
# have to ensure Path sources are relative to their lockfile before comparing
|
|
879
|
+
if [parent_spec.source, spec.source].grep(Source::Path).length == 2
|
|
880
|
+
lockfile_name
|
|
881
|
+
.dirname
|
|
882
|
+
.join(spec.source.path)
|
|
883
|
+
.ascend
|
|
884
|
+
.any?(parent_lockfile_name.dirname.join(parent_spec.source.path))
|
|
885
|
+
else
|
|
886
|
+
parent_spec.source == spec.source
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# @!visibility private
|
|
891
|
+
# @return [true, false] if gems for each platform could be used for the other (since a gem
|
|
892
|
+
# for Linux can be used for musl, but a musl gem can't be used for Linux), and both are for the same
|
|
893
|
+
# CPU (since a gem for a universal CPU can be used for any, but isn't the same as one for a specific one)
|
|
894
|
+
def same_platform?(platform, other)
|
|
895
|
+
return platform == other unless platform.is_a?(Gem::Platform) && other.is_a?(Gem::Platform)
|
|
896
|
+
|
|
897
|
+
platform.cpu == other.cpu &&
|
|
898
|
+
platform === other && other === platform # rubocop:disable Style/CaseEquality -- Gem::Platform's match
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# @!visibility private
|
|
902
|
+
# @param platforms [Array<Gem::Platform, String>] platforms listed in a lockfile
|
|
903
|
+
# @param platform [Gem::Platform, String] the platform of a gem variant
|
|
904
|
+
# @return [Array<Gem::Platform, String>] the listed platforms that the variant could be used for
|
|
905
|
+
def matching_platforms(platforms, platform)
|
|
906
|
+
# (a gem for Linux can be used on musl, but not the other way around, and only the ruby platform
|
|
907
|
+
# uses source variants)
|
|
908
|
+
platforms.select do |other|
|
|
909
|
+
if platform.is_a?(Gem::Platform) && other.is_a?(Gem::Platform)
|
|
910
|
+
platform === other # rubocop:disable Style/CaseEquality -- Gem::Platform's match
|
|
911
|
+
else
|
|
912
|
+
platform == other
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
|
|
396
917
|
# @!visibility private
|
|
397
918
|
# @return [Bundler::CLI, nil] the bundler command that's running, if any
|
|
398
919
|
def cli
|
|
@@ -540,7 +1061,8 @@ module Bundler
|
|
|
540
1061
|
dependency_changes: false,
|
|
541
1062
|
unlocking_bundler: false,
|
|
542
1063
|
added_platforms: [],
|
|
543
|
-
removed_platforms: []
|
|
1064
|
+
removed_platforms: [],
|
|
1065
|
+
unlock_gems: Set.new)
|
|
544
1066
|
prepare_block = lockfile_definition[:prepare]
|
|
545
1067
|
|
|
546
1068
|
gemfile = lockfile_definition[:gemfile]
|
|
@@ -556,7 +1078,13 @@ module Bundler
|
|
|
556
1078
|
builder.instance_variable_set(:@ruby_version, parent_ruby_version_requirement)
|
|
557
1079
|
end
|
|
558
1080
|
|
|
559
|
-
|
|
1081
|
+
unlock = { bundler: unlocking_bundler }
|
|
1082
|
+
# only the gems that are in the way of syncing with the parent
|
|
1083
|
+
unless unlock_gems.empty?
|
|
1084
|
+
unlock[:gems] = unlock_gems.to_a
|
|
1085
|
+
unlock[:conservative] = true
|
|
1086
|
+
end
|
|
1087
|
+
definition = builder.to_definition(lockfile, unlock)
|
|
560
1088
|
definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
|
|
561
1089
|
added_platforms.each { |platform| definition.add_platform(platform) }
|
|
562
1090
|
removed_platforms.each { |platform| definition.remove_platform(platform) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler-multilock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.
|
|
4
|
+
version: 2.0.0.beta7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Instructure
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 4.1.0.
|
|
18
|
+
version: 4.1.0.beta3
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '4.2'
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: 4.1.0.
|
|
28
|
+
version: 4.1.0.beta3
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '4.2'
|