bundler-multilock 2.0.0.beta4 → 2.0.0.beta5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45718b8a0410e25b206212808dd21eb5530561297393c3f6fca54a14acd26e40
4
- data.tar.gz: 35245c2fc0f936995c0cbc5be482bf20dc6767de70cc23b41fe1b89b63e179cd
3
+ metadata.gz: 730bc7b4e1a107a6b3fd9527014b630b97918ed317e703b8cf045c67d6f6fae9
4
+ data.tar.gz: 11943fc5875e3a07ba89f66b38e6b4ab6d100fe0d47168e24d5d3ae023609ebb
5
5
  SHA512:
6
- metadata.gz: 2e069c0c770748ebdfe957fc1c795553ecaa7afdd7655a180e5c6ced84f0e31840bd7f9233dcd6d07b70ac765fbde244624b1976a15f2d5dc31adb1e1accadaf
7
- data.tar.gz: c70adec856e83bb4520d3705e714bd2d1e99672d8c866faf1718928d65c1b3062b004c242e353037f7d7a0b35ea7bfb0675b2af20afd64416c1f8a14d03c7421
6
+ metadata.gz: 998f0024a84771dc5d61ada77c069bd267c9d0da5c1d09acd11c098a02bdcd6c537a32e5e8bfd3005ebb9050d1fea9afed8a6f4cc32877e5af8224089431cd00
7
+ data.tar.gz: dcf0c9c3e0089f48f796857eec57f555381ff215327ba904a11ef294c1b8bd45611613be3548b8def4c0dc7a4abd6f2fa65f7082251bfe83d216c092891a822e
@@ -99,10 +99,15 @@ module Bundler
99
99
  @allowed_duplicate_plugins = Set.new
100
100
  end
101
101
 
102
- def lockfile(*, **, &)
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(*, builder: self, **, &)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bundler
4
4
  module Multilock
5
- VERSION = "2.0.0.beta4"
5
+ VERSION = "2.0.0.beta5"
6
6
  end
7
7
  end
@@ -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}...") if attempts == 1
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,8 @@ module Bundler
230
217
  TEXT
231
218
  end
232
219
 
220
+ added_platforms = []
221
+ removed_platforms = []
233
222
  if lockfile_name.exist?
234
223
  # if the lockfile already exists, "merge" it together
235
224
  parent_lockfile = if adjusted_parent_lockfile_contents == cache.contents(lockfile_name)
@@ -279,8 +268,32 @@ module Bundler
279
268
  parent_spec
280
269
  end
281
270
 
282
- lockfile.platforms.replace(parent_lockfile.platforms).uniq!
283
- lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version) if lockfile.ruby_version
271
+ # the parent's versions might depend on gems this lockfile didn't need before.
272
+ # without them, Bundler considers the lockfile broken, and ignores what's
273
+ # locked in it, which can update everything
274
+ names = lockfile.specs.to_set(&:name)
275
+ pending = lockfile.specs.dup
276
+ until pending.empty?
277
+ pending.shift.dependencies.each do |dependency|
278
+ next if names.include?(dependency.name) || !(dependency_specs = parent_specs[dependency.name])
279
+
280
+ names << dependency.name
281
+ dependency_changes = true
282
+ dependency_specs.each_value do |dependency_spec|
283
+ source = dependency_spec.source
284
+ lockfile.sources << source unless lockfile.sources.include?(source)
285
+ lockfile.specs << dependency_spec
286
+ pending << dependency_spec
287
+ end
288
+ end
289
+ end
290
+
291
+ # leave the lockfile's platforms alone, and let Bundler add and remove them itself
292
+ # (in write_lockfile), like `bundle lock --add-platform` and `--remove-platform`,
293
+ # so that it also adds or removes the gems needed for them
294
+ removed_platforms = lockfile.platforms - parent_lockfile.platforms
295
+ added_platforms = parent_lockfile.platforms - lockfile.platforms
296
+ lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version)
284
297
  unless lockfile.bundler_version == parent_lockfile.bundler_version
285
298
  unlocking_bundler = parent_lockfile.bundler_version
286
299
  lockfile.instance_variable_set(:@bundler_version, parent_lockfile.bundler_version)
@@ -307,24 +320,12 @@ module Bundler
307
320
  cache,
308
321
  install:,
309
322
  dependency_changes:,
310
- unlocking_bundler:)
323
+ unlocking_bundler:,
324
+ added_platforms:,
325
+ removed_platforms:)
311
326
  end
312
327
  end
313
328
  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
329
  end
329
330
  end
330
331
  ensure
@@ -386,10 +387,10 @@ module Bundler
386
387
  return [] unless lockfile
387
388
  return [lockfile] if recovered
388
389
 
389
- # we can't tell if Bundler expanded a short name (or "active") to an
390
- # absolute path, or if it was written that way, so allow either
390
+ # we can't tell if Bundler expanded "active" to an absolute path,
391
+ # or if it was written that way, so allow either
391
392
  relative = Pathname.new(lockfile).relative_path_from(Pathname.pwd).to_s
392
- relative.include?("/") ? [lockfile] : [lockfile, relative]
393
+ (relative == "active") ? [lockfile, relative] : [lockfile]
393
394
  end
394
395
 
395
396
  # @!visibility private
@@ -411,6 +412,8 @@ module Bundler
411
412
  def reset!
412
413
  @lockfile_definitions = {}
413
414
  @loaded = false
415
+ @primary_gemfile = nil
416
+ @primary_lockfile = nil
414
417
  end
415
418
 
416
419
  # @!visibility private
@@ -418,6 +421,40 @@ module Bundler
418
421
  lockfile_definitions[Bundler.default_lockfile(force_original: true)]
419
422
  end
420
423
 
424
+ # @!visibility private
425
+ # The definitions of the other lockfiles, when working with all of them (so not with
426
+ # BUNDLE_LOCKFILE), for packaging gems from all of them. Their gems need to have been
427
+ # installed already, by a regular `bundle install`.
428
+ #
429
+ # @return [Array<Definition>]
430
+ def alternate_definitions
431
+ return [] if lockfile_definitions.empty? || env_lockfile
432
+
433
+ # the primary lockfile doesn't have to be declared
434
+ candidates = lockfile_definitions.values
435
+ if @primary_lockfile && !lockfile_definitions.key?(@primary_lockfile)
436
+ candidates += [{ gemfile: @primary_gemfile, lockfile: @primary_lockfile, prepare: nil }]
437
+ end
438
+
439
+ default_root = Bundler.root
440
+ candidates.filter_map do |lockfile_definition|
441
+ lockfile = lockfile_definition[:lockfile]
442
+ # the one Bundler is already working with, which may be an alternate lockfile flagged as active
443
+ next if lockfile == Bundler.default_lockfile
444
+ next unless lockfile.exist?
445
+
446
+ Bundler.root = lockfile_definition[:gemfile].dirname
447
+ builder = Dsl.new
448
+ prepare_block = lockfile_definition[:prepare]
449
+ builder.eval_gemfile(lockfile_definition[:gemfile], &prepare_block) if prepare_block
450
+ builder.eval_gemfile(lockfile_definition[:gemfile])
451
+ # (as the default lockfile, so that it isn't swapped for the active one)
452
+ Bundler.with_default_lockfile(lockfile) { builder.to_definition(lockfile, {}) }
453
+ end
454
+ ensure
455
+ Bundler.root = default_root if default_root
456
+ end
457
+
421
458
  private
422
459
 
423
460
  # @return [Array(String, true or false), nil]
@@ -445,9 +482,6 @@ module Bundler
445
482
  end
446
483
 
447
484
  def expand_lockfile(lockfile)
448
- if lockfile.is_a?(String) && !(lockfile.include?("/") || lockfile.end_with?(".lock"))
449
- lockfile = "Gemfile.#{lockfile}.lock"
450
- end
451
485
  # use absolute paths
452
486
  lockfile = Bundler.root.join(lockfile).expand_path if lockfile
453
487
  # use the default lockfile (Gemfile.lock) if none was given
@@ -504,7 +538,9 @@ module Bundler
504
538
  cache,
505
539
  install:,
506
540
  dependency_changes: false,
507
- unlocking_bundler: false)
541
+ unlocking_bundler: false,
542
+ added_platforms: [],
543
+ removed_platforms: [])
508
544
  prepare_block = lockfile_definition[:prepare]
509
545
 
510
546
  gemfile = lockfile_definition[:gemfile]
@@ -522,6 +558,8 @@ module Bundler
522
558
 
523
559
  definition = builder.to_definition(lockfile, { bundler: unlocking_bundler })
524
560
  definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
561
+ added_platforms.each { |platform| definition.add_platform(platform) }
562
+ removed_platforms.each { |platform| definition.remove_platform(platform) }
525
563
 
526
564
  # match the parent lockfile in whether there are checksums
527
565
  unless Bundler.frozen_bundle?
@@ -537,6 +575,10 @@ module Bundler
537
575
  definition.instance_variable_set(:@lockfile_contents, current_lockfile.read) if current_lockfile.exist?
538
576
 
539
577
  orig_definition = definition.dup # we might need it twice
578
+ # `dup` shares the platforms array, and resolving locally changes it (like dropping the ruby platform,
579
+ # and adding the local platform) before it might fail, which would leave a remote resolution
580
+ # starting from the wrong platforms
581
+ orig_definition.instance_variable_set(:@platforms, definition.instance_variable_get(:@platforms).dup)
540
582
 
541
583
  # install gems for the exact current version of the lockfile
542
584
  # this ensures it doesn't re-resolve with only (different)
@@ -572,8 +614,10 @@ module Bundler
572
614
  previous_ui_level = Bundler.ui.level
573
615
  Bundler.ui.level = "warn"
574
616
  begin
575
- # force a remote resolution if intermediate gems are missing
576
- if definition.instance_variable_get(:@locked_spec_with_missing_deps) ||
617
+ # force a remote resolution if intermediate gems are missing, or if platforms were added
618
+ # (a gem's source variant would satisfy them locally, without its platform specific variants)
619
+ if (added_platforms.any? && !local_only?) ||
620
+ definition.instance_variable_get(:@locked_spec_with_missing_deps) ||
577
621
  definition.instance_variable_get(:@locked_spec_with_invalid_deps) ||
578
622
  definition.instance_variable_get(:@missing_lockfile_dep) ||
579
623
  definition.instance_variable_get(:@invalid_lockfile_dep)
@@ -593,6 +637,11 @@ module Bundler
593
637
  spec.source.specs << spec
594
638
  end
595
639
  resolve_locally(definition)
640
+ # a local resolution can only see the gems that are installed, so it can drop platforms
641
+ # (like ruby) that remote gem variants would keep. the platforms should match the parent's
642
+ parent_platforms = cache.parser(lockfile_definition[:parent]).platforms.map(&:to_s).sort
643
+ raise SolveFailure unless local_only? || definition.platforms.map(&:to_s).sort == parent_platforms
644
+
596
645
  missing_checksums = fill_checksums(definition, lockfile_definition[:parent], cache)
597
646
  # fetch checksums that aren't available locally
598
647
  raise SolveFailure unless missing_checksums.empty? || local_only?
@@ -612,6 +661,11 @@ module Bundler
612
661
  "Run without `--local` to fetch them."
613
662
  end
614
663
  SharedHelpers.capture_filesystem_access do
664
+ # a frozen bundle can't change its lockfile, and trying to just fails with a misleading
665
+ # error from Bundler (like about updating Bundler itself). install what it locks, and
666
+ # let the check afterwards report what's actually wrong with it
667
+ next if Bundler.frozen_bundle?
668
+
615
669
  definition.instance_variable_set(:@resolved_bundler_version, unlocking_bundler) if unlocking_bundler
616
670
 
617
671
  # 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.beta4
4
+ version: 2.0.0.beta5
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: '5'
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: '5'
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