bundler-multilock 2.0.0.beta4 → 2.0.0.beta6
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/ext/dsl.rb +7 -2
- data/lib/bundler/multilock/ext/runtime.rb +128 -0
- data/lib/bundler/multilock/version.rb +1 -1
- data/lib/bundler/multilock.rb +649 -67
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bdc44af5344eed4edb2782c151e3ab714450bcd81003130fba1385247c227da
|
|
4
|
+
data.tar.gz: 77391ab3ec993404d7363736accb18921c8d1c5190c2609a3bf45e4e950c6f3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38436b8ee5d0096b5387da472d11a543158dcb884143cbbd9cf8acf9f9dd76e91a644ffb4b3fcdadeb7768b7b0f87f5597531060633fdc1dca92c8893adc31eb
|
|
7
|
+
data.tar.gz: aad7cd30a4f7ef60aff473d0ac763dab63f33f7dbfddceb2b18748563294d9d49f414f44f067ec2bfe482edee9925a4242f0a34f5dbc0de0d4053564a1661fa7
|
|
@@ -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
|
|
@@ -99,10 +99,15 @@ module Bundler
|
|
|
99
99
|
@allowed_duplicate_plugins = Set.new
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
# Significant changes:
|
|
103
|
+
# * a lockfile with a block or options is managed by Multilock
|
|
104
|
+
# * a bare lockfile name is just Bundler's own `lockfile`, for
|
|
105
|
+
# Gemfiles that only want to name their lockfile
|
|
106
|
+
def lockfile(*args, **kwargs, &block)
|
|
103
107
|
return true if Multilock.loaded?
|
|
108
|
+
return super(args.first) if args.length == 1 && args.first.is_a?(String) && kwargs.empty? && !block
|
|
104
109
|
|
|
105
|
-
Multilock.add_lockfile(
|
|
110
|
+
Multilock.add_lockfile(*args, builder: self, **kwargs, &block)
|
|
106
111
|
end
|
|
107
112
|
end
|
|
108
113
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Multilock
|
|
5
|
+
module Ext
|
|
6
|
+
# Makes `bundle cache` (and `bundle install` when vendor/cache exists) package
|
|
7
|
+
# the gems from every lockfile, and only prune gems that none of them use.
|
|
8
|
+
# Likewise, `bundle clean` only removes gems that none of the lockfiles use, and the
|
|
9
|
+
# `prune` setting covers the git gems from all of them.
|
|
10
|
+
module Runtime
|
|
11
|
+
::Bundler::Runtime.prepend(self)
|
|
12
|
+
|
|
13
|
+
def cache(custom_path = nil, *)
|
|
14
|
+
cache_alternate_lockfiles(custom_path)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def prune_cache(cache_path)
|
|
19
|
+
return super if multilock_alternate_definitions.empty?
|
|
20
|
+
|
|
21
|
+
ensure_multilock_cache_path(cache_path)
|
|
22
|
+
|
|
23
|
+
resolve = [@definition, *multilock_alternate_definitions].flat_map { |definition| definition.resolve.to_a }
|
|
24
|
+
prune_gem_cache(resolve, cache_path)
|
|
25
|
+
prune_git_and_path_cache(resolve, cache_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Bundler's `clean` only keeps what the one definition needs, so keep what all of them need.
|
|
29
|
+
# It also keys the specs it keeps by name, which would drop all but one version of a gem
|
|
30
|
+
# that the lockfiles disagree on, so that's replaced for the duration. (Keyed by where they
|
|
31
|
+
# are installed, since different lockfiles can lock different revisions of the same git gem.)
|
|
32
|
+
def clean(*)
|
|
33
|
+
alternate_definitions = multilock_alternate_definitions
|
|
34
|
+
return super if alternate_definitions.empty?
|
|
35
|
+
|
|
36
|
+
combined = [specs, *alternate_definitions.map(&:specs)].flat_map(&:to_a)
|
|
37
|
+
rubygems = Bundler.rubygems
|
|
38
|
+
original_add_default_gems_to = rubygems.method(:add_default_gems_to)
|
|
39
|
+
rubygems.define_singleton_method(:add_default_gems_to) do |_specs|
|
|
40
|
+
(original_add_default_gems_to.call([]).values + combined).to_h { |spec| [spec.full_gem_path, spec] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
super
|
|
45
|
+
ensure
|
|
46
|
+
rubygems.singleton_class.send(:remove_method, :add_default_gems_to)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Same as Bundler's, except that it also covers the git gems that only other lockfiles use
|
|
53
|
+
def prune_git_metadata
|
|
54
|
+
return super if multilock_alternate_definitions.empty?
|
|
55
|
+
|
|
56
|
+
owned = "#{Bundler.install_path}#{File::SEPARATOR}"
|
|
57
|
+
git_sources = [@definition, *multilock_alternate_definitions].flat_map do |definition|
|
|
58
|
+
definition.sources.git_sources
|
|
59
|
+
end
|
|
60
|
+
git_dirs = git_sources.reject(&:local?).filter_map do |source|
|
|
61
|
+
install_path = source.install_path.to_s
|
|
62
|
+
next unless install_path.start_with?(owned)
|
|
63
|
+
|
|
64
|
+
git_dir = File.join(install_path, ".git")
|
|
65
|
+
git_dir if File.exist?(git_dir)
|
|
66
|
+
end.uniq
|
|
67
|
+
return if git_dirs.empty?
|
|
68
|
+
|
|
69
|
+
Bundler.ui.info "Removing git metadata from checked out git gems"
|
|
70
|
+
git_dirs.each do |git_dir|
|
|
71
|
+
::Bundler::SharedHelpers.filesystem_access(git_dir) do |p|
|
|
72
|
+
FileUtils.rm_rf(p)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ensure_multilock_cache_path(cache_path)
|
|
78
|
+
return if File.exist?(cache_path)
|
|
79
|
+
|
|
80
|
+
::Bundler::SharedHelpers.filesystem_access(cache_path) do |p|
|
|
81
|
+
FileUtils.mkdir_p(p)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Array<Definition>] the definitions of the lockfiles other than the one being cached
|
|
86
|
+
def multilock_alternate_definitions
|
|
87
|
+
@multilock_alternate_definitions ||= Multilock.alternate_definitions
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Caches the gems that only the other lockfiles use. The ones in common are cached by the
|
|
91
|
+
# usual process, which also cleans up git sources and prunes afterwards.
|
|
92
|
+
def cache_alternate_lockfiles(custom_path)
|
|
93
|
+
return if multilock_alternate_definitions.empty?
|
|
94
|
+
|
|
95
|
+
cache_path = Bundler.app_cache(custom_path)
|
|
96
|
+
ensure_multilock_cache_path(cache_path)
|
|
97
|
+
|
|
98
|
+
cached = Set.new
|
|
99
|
+
multilock_alternate_definitions.each do |definition|
|
|
100
|
+
specs_to_cache = if Bundler.settings[:cache_all_platforms]
|
|
101
|
+
definition.resolve.materialized_for_all_platforms
|
|
102
|
+
else
|
|
103
|
+
definition.specs
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
specs_to_cache.each do |spec|
|
|
107
|
+
next if spec.name == "bundler"
|
|
108
|
+
|
|
109
|
+
source = spec.source
|
|
110
|
+
next if source.is_a?(::Bundler::Source::Gemspec)
|
|
111
|
+
|
|
112
|
+
# git sources are equal regardless of which revision is locked, but each one
|
|
113
|
+
# is cached separately
|
|
114
|
+
cache_key = source.respond_to?(:app_cache_dirname) ? source.app_cache_dirname : source
|
|
115
|
+
next unless cached.add?([spec.full_name, cache_key])
|
|
116
|
+
|
|
117
|
+
if source.respond_to?(:migrate_cache)
|
|
118
|
+
source.migrate_cache(custom_path, local: false)
|
|
119
|
+
elsif source.respond_to?(:cache)
|
|
120
|
+
source.cache(spec, custom_path)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/bundler/multilock.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "multilock/ext/bundler"
|
|
|
4
4
|
require_relative "multilock/ext/definition"
|
|
5
5
|
require_relative "multilock/ext/dsl"
|
|
6
6
|
require_relative "multilock/ext/plugin/dsl"
|
|
7
|
+
require_relative "multilock/ext/runtime"
|
|
7
8
|
require_relative "multilock/ext/shared_helpers"
|
|
8
9
|
require_relative "multilock/ext/source"
|
|
9
10
|
require_relative "multilock/ext/source_list"
|
|
@@ -40,11 +41,15 @@ module Bundler
|
|
|
40
41
|
parent: nil,
|
|
41
42
|
enforce_pinned_additional_dependencies: false,
|
|
42
43
|
&block)
|
|
44
|
+
# remembered for gems that need to be kept for the primary lockfile, even if it was never
|
|
45
|
+
# declared and an alternate lockfile is active (which may use its own Gemfile)
|
|
46
|
+
@primary_gemfile ||= Bundler.default_gemfile
|
|
47
|
+
@primary_lockfile ||= Bundler.default_lockfile(force_original: true)
|
|
48
|
+
|
|
43
49
|
active = true if active.nil? && lockfile_definitions.empty? && lockfile.nil? && gemfile.nil?
|
|
44
50
|
|
|
45
51
|
# if a gemfile was provided, but not a lockfile, infer the default lockfile for that gemfile
|
|
46
52
|
lockfile ||= "#{gemfile}.lock" if gemfile
|
|
47
|
-
# allow short-form lockfile names
|
|
48
53
|
lockfile = expand_lockfile(lockfile)
|
|
49
54
|
|
|
50
55
|
raise ArgumentError, "Lockfile #{lockfile} is already defined" if lockfile_definitions.key?(lockfile)
|
|
@@ -139,9 +144,6 @@ module Bundler
|
|
|
139
144
|
|
|
140
145
|
Bundler.ui.debug("Syncing to alternate lockfiles")
|
|
141
146
|
|
|
142
|
-
attempts = 1
|
|
143
|
-
previous_contents = Set.new
|
|
144
|
-
|
|
145
147
|
default_root = Bundler.root
|
|
146
148
|
|
|
147
149
|
cache = Cache.new
|
|
@@ -158,17 +160,6 @@ module Bundler
|
|
|
158
160
|
|
|
159
161
|
relative_lockfile = lockfile_name.relative_path_from(Dir.pwd)
|
|
160
162
|
|
|
161
|
-
# prevent infinite loops of tick-tocking back and forth between two versions
|
|
162
|
-
current_contents = cache.contents(lockfile_name)
|
|
163
|
-
if previous_contents.include?(current_contents)
|
|
164
|
-
Bundler.ui.debug("Unable to converge on a single solution for #{lockfile_name}; " \
|
|
165
|
-
"perhaps there are conflicting requirements?")
|
|
166
|
-
attempts = 1
|
|
167
|
-
previous_contents.clear
|
|
168
|
-
next
|
|
169
|
-
end
|
|
170
|
-
previous_contents << current_contents
|
|
171
|
-
|
|
172
163
|
# already up to date?
|
|
173
164
|
up_to_date = false
|
|
174
165
|
conflicts = Set.new
|
|
@@ -178,11 +169,7 @@ module Bundler
|
|
|
178
169
|
checker.deep_check(lockfile_definition, conflicts:)
|
|
179
170
|
end
|
|
180
171
|
end
|
|
181
|
-
if up_to_date
|
|
182
|
-
attempts = 1
|
|
183
|
-
previous_contents.clear
|
|
184
|
-
next
|
|
185
|
-
end
|
|
172
|
+
next if up_to_date
|
|
186
173
|
|
|
187
174
|
# a missing lockfile that enforces pinned additional dependencies has nothing
|
|
188
175
|
# to be out of sync with, so even a frozen bundle just generates it
|
|
@@ -201,7 +188,7 @@ module Bundler
|
|
|
201
188
|
Bundler.ui.info("Installing gems for #{relative_lockfile}...")
|
|
202
189
|
write_lockfile(lockfile_definition, lockfile_name, cache, install:)
|
|
203
190
|
else
|
|
204
|
-
Bundler.ui.info("Syncing to #{relative_lockfile}...")
|
|
191
|
+
Bundler.ui.info("Syncing to #{relative_lockfile}...")
|
|
205
192
|
synced_any = true
|
|
206
193
|
|
|
207
194
|
parent_lockfile_name = lockfile_definition[:parent]
|
|
@@ -230,6 +217,9 @@ module Bundler
|
|
|
230
217
|
TEXT
|
|
231
218
|
end
|
|
232
219
|
|
|
220
|
+
added_platforms = []
|
|
221
|
+
removed_platforms = []
|
|
222
|
+
unlock_gems = Set.new
|
|
233
223
|
if lockfile_name.exist?
|
|
234
224
|
# if the lockfile already exists, "merge" it together
|
|
235
225
|
parent_lockfile = if adjusted_parent_lockfile_contents == cache.contents(lockfile_name)
|
|
@@ -242,24 +232,13 @@ module Bundler
|
|
|
242
232
|
|
|
243
233
|
dependency_changes = false
|
|
244
234
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
precedence = if !(cache.reverse_dependencies(lockfile_name)[spec.name] & conflicts).empty? # rubocop:disable Style/ArrayIntersect -- not an array
|
|
253
|
-
:parent
|
|
254
|
-
elsif cache.conflicting_requirements?(lockfile_name,
|
|
255
|
-
parent_lockfile_name,
|
|
256
|
-
spec,
|
|
257
|
-
parent_spec)
|
|
258
|
-
:self
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
spec_precedences[spec.name] = precedence || :parent
|
|
262
|
-
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
|
+
)
|
|
263
242
|
|
|
264
243
|
lockfile.sources.map! do |source|
|
|
265
244
|
parent_lockfile.sources.find { |s| s == source } || source
|
|
@@ -268,7 +247,7 @@ module Bundler
|
|
|
268
247
|
lockfile.specs.map! do |spec|
|
|
269
248
|
parent_spec = cache.find_matching_spec(parent_specs, spec)
|
|
270
249
|
next spec unless parent_spec
|
|
271
|
-
next spec if
|
|
250
|
+
next spec if precedences[spec.name] == :self
|
|
272
251
|
|
|
273
252
|
dependency_changes ||= spec != parent_spec
|
|
274
253
|
|
|
@@ -279,8 +258,63 @@ module Bundler
|
|
|
279
258
|
parent_spec
|
|
280
259
|
end
|
|
281
260
|
|
|
282
|
-
lockfile.
|
|
283
|
-
lockfile
|
|
261
|
+
# the parent's versions might depend on gems this lockfile didn't need before.
|
|
262
|
+
# without them, Bundler considers the lockfile broken, and ignores what's
|
|
263
|
+
# locked in it, which can update everything
|
|
264
|
+
names = lockfile.specs.to_set(&:name)
|
|
265
|
+
pending = lockfile.specs.dup
|
|
266
|
+
until pending.empty?
|
|
267
|
+
pending.shift.dependencies.each do |dependency|
|
|
268
|
+
next if names.include?(dependency.name) || !(dependency_specs = parent_specs[dependency.name])
|
|
269
|
+
|
|
270
|
+
names << dependency.name
|
|
271
|
+
dependency_changes = true
|
|
272
|
+
dependency_specs.each_value do |dependency_spec|
|
|
273
|
+
source = dependency_spec.source
|
|
274
|
+
lockfile.sources << source unless lockfile.sources.include?(source)
|
|
275
|
+
lockfile.specs << dependency_spec
|
|
276
|
+
pending << dependency_spec
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# leave the lockfile's platforms alone, and let Bundler add and remove them itself
|
|
282
|
+
# (in write_lockfile), like `bundle lock --add-platform` and `--remove-platform`,
|
|
283
|
+
# so that it also adds or removes the gems needed for them
|
|
284
|
+
removed_platforms = lockfile.platforms - parent_lockfile.platforms
|
|
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
|
|
317
|
+
lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version)
|
|
284
318
|
unless lockfile.bundler_version == parent_lockfile.bundler_version
|
|
285
319
|
unlocking_bundler = parent_lockfile.bundler_version
|
|
286
320
|
lockfile.instance_variable_set(:@bundler_version, parent_lockfile.bundler_version)
|
|
@@ -307,24 +341,13 @@ module Bundler
|
|
|
307
341
|
cache,
|
|
308
342
|
install:,
|
|
309
343
|
dependency_changes:,
|
|
310
|
-
unlocking_bundler
|
|
344
|
+
unlocking_bundler:,
|
|
345
|
+
added_platforms:,
|
|
346
|
+
removed_platforms:,
|
|
347
|
+
unlock_gems:)
|
|
311
348
|
end
|
|
312
349
|
end
|
|
313
350
|
cache.invalidate_lockfile(lockfile_name) if had_changes
|
|
314
|
-
|
|
315
|
-
# if we had changes, bundler may have updated some common
|
|
316
|
-
# dependencies beyond the default lockfile, so re-run it
|
|
317
|
-
# once to reset them back to the default lockfile's version.
|
|
318
|
-
# if it's already good, the `check` check at the beginning of
|
|
319
|
-
# the loop will skip the second sync anyway.
|
|
320
|
-
if had_changes
|
|
321
|
-
attempts += 1
|
|
322
|
-
Bundler.ui.debug("Re-running sync to #{relative_lockfile} to reset common dependencies")
|
|
323
|
-
redo
|
|
324
|
-
else
|
|
325
|
-
attempts = 1
|
|
326
|
-
previous_contents.clear
|
|
327
|
-
end
|
|
328
351
|
end
|
|
329
352
|
end
|
|
330
353
|
ensure
|
|
@@ -386,10 +409,509 @@ module Bundler
|
|
|
386
409
|
return [] unless lockfile
|
|
387
410
|
return [lockfile] if recovered
|
|
388
411
|
|
|
389
|
-
# we can't tell if Bundler expanded
|
|
390
|
-
#
|
|
412
|
+
# we can't tell if Bundler expanded "active" to an absolute path,
|
|
413
|
+
# or if it was written that way, so allow either
|
|
391
414
|
relative = Pathname.new(lockfile).relative_path_from(Pathname.pwd).to_s
|
|
392
|
-
relative
|
|
415
|
+
(relative == "active") ? [lockfile, relative] : [lockfile]
|
|
416
|
+
end
|
|
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
|
|
393
915
|
end
|
|
394
916
|
|
|
395
917
|
# @!visibility private
|
|
@@ -411,6 +933,8 @@ module Bundler
|
|
|
411
933
|
def reset!
|
|
412
934
|
@lockfile_definitions = {}
|
|
413
935
|
@loaded = false
|
|
936
|
+
@primary_gemfile = nil
|
|
937
|
+
@primary_lockfile = nil
|
|
414
938
|
end
|
|
415
939
|
|
|
416
940
|
# @!visibility private
|
|
@@ -418,6 +942,40 @@ module Bundler
|
|
|
418
942
|
lockfile_definitions[Bundler.default_lockfile(force_original: true)]
|
|
419
943
|
end
|
|
420
944
|
|
|
945
|
+
# @!visibility private
|
|
946
|
+
# The definitions of the other lockfiles, when working with all of them (so not with
|
|
947
|
+
# BUNDLE_LOCKFILE), for packaging gems from all of them. Their gems need to have been
|
|
948
|
+
# installed already, by a regular `bundle install`.
|
|
949
|
+
#
|
|
950
|
+
# @return [Array<Definition>]
|
|
951
|
+
def alternate_definitions
|
|
952
|
+
return [] if lockfile_definitions.empty? || env_lockfile
|
|
953
|
+
|
|
954
|
+
# the primary lockfile doesn't have to be declared
|
|
955
|
+
candidates = lockfile_definitions.values
|
|
956
|
+
if @primary_lockfile && !lockfile_definitions.key?(@primary_lockfile)
|
|
957
|
+
candidates += [{ gemfile: @primary_gemfile, lockfile: @primary_lockfile, prepare: nil }]
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
default_root = Bundler.root
|
|
961
|
+
candidates.filter_map do |lockfile_definition|
|
|
962
|
+
lockfile = lockfile_definition[:lockfile]
|
|
963
|
+
# the one Bundler is already working with, which may be an alternate lockfile flagged as active
|
|
964
|
+
next if lockfile == Bundler.default_lockfile
|
|
965
|
+
next unless lockfile.exist?
|
|
966
|
+
|
|
967
|
+
Bundler.root = lockfile_definition[:gemfile].dirname
|
|
968
|
+
builder = Dsl.new
|
|
969
|
+
prepare_block = lockfile_definition[:prepare]
|
|
970
|
+
builder.eval_gemfile(lockfile_definition[:gemfile], &prepare_block) if prepare_block
|
|
971
|
+
builder.eval_gemfile(lockfile_definition[:gemfile])
|
|
972
|
+
# (as the default lockfile, so that it isn't swapped for the active one)
|
|
973
|
+
Bundler.with_default_lockfile(lockfile) { builder.to_definition(lockfile, {}) }
|
|
974
|
+
end
|
|
975
|
+
ensure
|
|
976
|
+
Bundler.root = default_root if default_root
|
|
977
|
+
end
|
|
978
|
+
|
|
421
979
|
private
|
|
422
980
|
|
|
423
981
|
# @return [Array(String, true or false), nil]
|
|
@@ -445,9 +1003,6 @@ module Bundler
|
|
|
445
1003
|
end
|
|
446
1004
|
|
|
447
1005
|
def expand_lockfile(lockfile)
|
|
448
|
-
if lockfile.is_a?(String) && !(lockfile.include?("/") || lockfile.end_with?(".lock"))
|
|
449
|
-
lockfile = "Gemfile.#{lockfile}.lock"
|
|
450
|
-
end
|
|
451
1006
|
# use absolute paths
|
|
452
1007
|
lockfile = Bundler.root.join(lockfile).expand_path if lockfile
|
|
453
1008
|
# use the default lockfile (Gemfile.lock) if none was given
|
|
@@ -504,7 +1059,10 @@ module Bundler
|
|
|
504
1059
|
cache,
|
|
505
1060
|
install:,
|
|
506
1061
|
dependency_changes: false,
|
|
507
|
-
unlocking_bundler: false
|
|
1062
|
+
unlocking_bundler: false,
|
|
1063
|
+
added_platforms: [],
|
|
1064
|
+
removed_platforms: [],
|
|
1065
|
+
unlock_gems: Set.new)
|
|
508
1066
|
prepare_block = lockfile_definition[:prepare]
|
|
509
1067
|
|
|
510
1068
|
gemfile = lockfile_definition[:gemfile]
|
|
@@ -520,8 +1078,16 @@ module Bundler
|
|
|
520
1078
|
builder.instance_variable_set(:@ruby_version, parent_ruby_version_requirement)
|
|
521
1079
|
end
|
|
522
1080
|
|
|
523
|
-
|
|
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)
|
|
524
1088
|
definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
|
|
1089
|
+
added_platforms.each { |platform| definition.add_platform(platform) }
|
|
1090
|
+
removed_platforms.each { |platform| definition.remove_platform(platform) }
|
|
525
1091
|
|
|
526
1092
|
# match the parent lockfile in whether there are checksums
|
|
527
1093
|
unless Bundler.frozen_bundle?
|
|
@@ -537,6 +1103,10 @@ module Bundler
|
|
|
537
1103
|
definition.instance_variable_set(:@lockfile_contents, current_lockfile.read) if current_lockfile.exist?
|
|
538
1104
|
|
|
539
1105
|
orig_definition = definition.dup # we might need it twice
|
|
1106
|
+
# `dup` shares the platforms array, and resolving locally changes it (like dropping the ruby platform,
|
|
1107
|
+
# and adding the local platform) before it might fail, which would leave a remote resolution
|
|
1108
|
+
# starting from the wrong platforms
|
|
1109
|
+
orig_definition.instance_variable_set(:@platforms, definition.instance_variable_get(:@platforms).dup)
|
|
540
1110
|
|
|
541
1111
|
# install gems for the exact current version of the lockfile
|
|
542
1112
|
# this ensures it doesn't re-resolve with only (different)
|
|
@@ -572,8 +1142,10 @@ module Bundler
|
|
|
572
1142
|
previous_ui_level = Bundler.ui.level
|
|
573
1143
|
Bundler.ui.level = "warn"
|
|
574
1144
|
begin
|
|
575
|
-
# force a remote resolution if intermediate gems are missing
|
|
576
|
-
|
|
1145
|
+
# force a remote resolution if intermediate gems are missing, or if platforms were added
|
|
1146
|
+
# (a gem's source variant would satisfy them locally, without its platform specific variants)
|
|
1147
|
+
if (added_platforms.any? && !local_only?) ||
|
|
1148
|
+
definition.instance_variable_get(:@locked_spec_with_missing_deps) ||
|
|
577
1149
|
definition.instance_variable_get(:@locked_spec_with_invalid_deps) ||
|
|
578
1150
|
definition.instance_variable_get(:@missing_lockfile_dep) ||
|
|
579
1151
|
definition.instance_variable_get(:@invalid_lockfile_dep)
|
|
@@ -593,6 +1165,11 @@ module Bundler
|
|
|
593
1165
|
spec.source.specs << spec
|
|
594
1166
|
end
|
|
595
1167
|
resolve_locally(definition)
|
|
1168
|
+
# a local resolution can only see the gems that are installed, so it can drop platforms
|
|
1169
|
+
# (like ruby) that remote gem variants would keep. the platforms should match the parent's
|
|
1170
|
+
parent_platforms = cache.parser(lockfile_definition[:parent]).platforms.map(&:to_s).sort
|
|
1171
|
+
raise SolveFailure unless local_only? || definition.platforms.map(&:to_s).sort == parent_platforms
|
|
1172
|
+
|
|
596
1173
|
missing_checksums = fill_checksums(definition, lockfile_definition[:parent], cache)
|
|
597
1174
|
# fetch checksums that aren't available locally
|
|
598
1175
|
raise SolveFailure unless missing_checksums.empty? || local_only?
|
|
@@ -612,6 +1189,11 @@ module Bundler
|
|
|
612
1189
|
"Run without `--local` to fetch them."
|
|
613
1190
|
end
|
|
614
1191
|
SharedHelpers.capture_filesystem_access do
|
|
1192
|
+
# a frozen bundle can't change its lockfile, and trying to just fails with a misleading
|
|
1193
|
+
# error from Bundler (like about updating Bundler itself). install what it locks, and
|
|
1194
|
+
# let the check afterwards report what's actually wrong with it
|
|
1195
|
+
next if Bundler.frozen_bundle?
|
|
1196
|
+
|
|
615
1197
|
definition.instance_variable_set(:@resolved_bundler_version, unlocking_bundler) if unlocking_bundler
|
|
616
1198
|
|
|
617
1199
|
# need to force it to _not_ preserve unknown sections, so that it
|
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.beta6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Instructure
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
version: 4.1.0.beta2
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: '
|
|
21
|
+
version: '4.2'
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -28,7 +28,7 @@ dependencies:
|
|
|
28
28
|
version: 4.1.0.beta2
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '
|
|
31
|
+
version: '4.2'
|
|
32
32
|
executables: []
|
|
33
33
|
extensions: []
|
|
34
34
|
extra_rdoc_files: []
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- lib/bundler/multilock/ext/definition.rb
|
|
42
42
|
- lib/bundler/multilock/ext/dsl.rb
|
|
43
43
|
- lib/bundler/multilock/ext/plugin/dsl.rb
|
|
44
|
+
- lib/bundler/multilock/ext/runtime.rb
|
|
44
45
|
- lib/bundler/multilock/ext/shared_helpers.rb
|
|
45
46
|
- lib/bundler/multilock/ext/source.rb
|
|
46
47
|
- lib/bundler/multilock/ext/source_list.rb
|