bundler-multilock 2.0.0.beta3 → 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: ff691ee3da8c3993a9fff31d22a2b9332b6014a97cec78e9982598d0c5838c60
4
- data.tar.gz: 4023c64461e543e844a993521a3535da270a43aeefce332c4b0a451f8fc16b2a
3
+ metadata.gz: 730bc7b4e1a107a6b3fd9527014b630b97918ed317e703b8cf045c67d6f6fae9
4
+ data.tar.gz: 11943fc5875e3a07ba89f66b38e6b4ab6d100fe0d47168e24d5d3ae023609ebb
5
5
  SHA512:
6
- metadata.gz: 4255bf0a1327afa83559cd0e5bed0a71be94799de7d73a48b1c39d46c41642190ca0b80ba728dc0982f78d764b5cbf8349dc3429665ff449e5f04ca3409083c9
7
- data.tar.gz: 730480d9e67879996505d1a452d14a702abafe5291a719f056bad904b6eda6c5b80f5080f38663475f2e0ca119f9d4ecdf4d992e8b43b36dba97424b83541bce
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.beta3"
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,13 +169,14 @@ 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
173
+
174
+ # a missing lockfile that enforces pinned additional dependencies has nothing
175
+ # to be out of sync with, so even a frozen bundle just generates it
176
+ generate_missing = Bundler.frozen_bundle? && !lockfile_name.exist? &&
177
+ lockfile_definition[:enforce_pinned_additional_dependencies]
186
178
 
187
- if Bundler.frozen_bundle?
179
+ if Bundler.frozen_bundle? && !generate_missing
188
180
  # if we're frozen, you have to use the pre-existing lockfile
189
181
  unless lockfile_name.exist?
190
182
  Bundler.ui.error("The bundle is locked, but #{relative_lockfile} is missing. " \
@@ -196,7 +188,7 @@ module Bundler
196
188
  Bundler.ui.info("Installing gems for #{relative_lockfile}...")
197
189
  write_lockfile(lockfile_definition, lockfile_name, cache, install:)
198
190
  else
199
- Bundler.ui.info("Syncing to #{relative_lockfile}...") if attempts == 1
191
+ Bundler.ui.info("Syncing to #{relative_lockfile}...")
200
192
  synced_any = true
201
193
 
202
194
  parent_lockfile_name = lockfile_definition[:parent]
@@ -225,6 +217,8 @@ module Bundler
225
217
  TEXT
226
218
  end
227
219
 
220
+ added_platforms = []
221
+ removed_platforms = []
228
222
  if lockfile_name.exist?
229
223
  # if the lockfile already exists, "merge" it together
230
224
  parent_lockfile = if adjusted_parent_lockfile_contents == cache.contents(lockfile_name)
@@ -274,8 +268,32 @@ module Bundler
274
268
  parent_spec
275
269
  end
276
270
 
277
- lockfile.platforms.replace(parent_lockfile.platforms).uniq!
278
- 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)
279
297
  unless lockfile.bundler_version == parent_lockfile.bundler_version
280
298
  unlocking_bundler = parent_lockfile.bundler_version
281
299
  lockfile.instance_variable_set(:@bundler_version, parent_lockfile.bundler_version)
@@ -294,28 +312,20 @@ module Bundler
294
312
  temp_lockfile.write(new_contents)
295
313
  temp_lockfile.flush
296
314
 
297
- had_changes ||= write_lockfile(lockfile_definition,
298
- temp_lockfile.path,
299
- cache,
300
- install:,
301
- dependency_changes:,
302
- unlocking_bundler:)
315
+ # Bundler refuses to write a lockfile when frozen, which we need to allow in the case
316
+ # of a missing lockfile that enforces pinned dependencies.
317
+ Bundler.settings.temporary(frozen: generate_missing ? false : Bundler.frozen_bundle?) do
318
+ had_changes ||= write_lockfile(lockfile_definition,
319
+ temp_lockfile.path,
320
+ cache,
321
+ install:,
322
+ dependency_changes:,
323
+ unlocking_bundler:,
324
+ added_platforms:,
325
+ removed_platforms:)
326
+ end
303
327
  end
304
328
  cache.invalidate_lockfile(lockfile_name) if had_changes
305
-
306
- # if we had changes, bundler may have updated some common
307
- # dependencies beyond the default lockfile, so re-run it
308
- # once to reset them back to the default lockfile's version.
309
- # if it's already good, the `check` check at the beginning of
310
- # the loop will skip the second sync anyway.
311
- if had_changes
312
- attempts += 1
313
- Bundler.ui.debug("Re-running sync to #{relative_lockfile} to reset common dependencies")
314
- redo
315
- else
316
- attempts = 1
317
- previous_contents.clear
318
- end
319
329
  end
320
330
  end
321
331
  ensure
@@ -377,10 +387,10 @@ module Bundler
377
387
  return [] unless lockfile
378
388
  return [lockfile] if recovered
379
389
 
380
- # we can't tell if Bundler expanded a short name (or "active") to an
381
- # 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
382
392
  relative = Pathname.new(lockfile).relative_path_from(Pathname.pwd).to_s
383
- relative.include?("/") ? [lockfile] : [lockfile, relative]
393
+ (relative == "active") ? [lockfile, relative] : [lockfile]
384
394
  end
385
395
 
386
396
  # @!visibility private
@@ -402,6 +412,8 @@ module Bundler
402
412
  def reset!
403
413
  @lockfile_definitions = {}
404
414
  @loaded = false
415
+ @primary_gemfile = nil
416
+ @primary_lockfile = nil
405
417
  end
406
418
 
407
419
  # @!visibility private
@@ -409,6 +421,40 @@ module Bundler
409
421
  lockfile_definitions[Bundler.default_lockfile(force_original: true)]
410
422
  end
411
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
+
412
458
  private
413
459
 
414
460
  # @return [Array(String, true or false), nil]
@@ -436,9 +482,6 @@ module Bundler
436
482
  end
437
483
 
438
484
  def expand_lockfile(lockfile)
439
- if lockfile.is_a?(String) && !(lockfile.include?("/") || lockfile.end_with?(".lock"))
440
- lockfile = "Gemfile.#{lockfile}.lock"
441
- end
442
485
  # use absolute paths
443
486
  lockfile = Bundler.root.join(lockfile).expand_path if lockfile
444
487
  # use the default lockfile (Gemfile.lock) if none was given
@@ -495,7 +538,9 @@ module Bundler
495
538
  cache,
496
539
  install:,
497
540
  dependency_changes: false,
498
- unlocking_bundler: false)
541
+ unlocking_bundler: false,
542
+ added_platforms: [],
543
+ removed_platforms: [])
499
544
  prepare_block = lockfile_definition[:prepare]
500
545
 
501
546
  gemfile = lockfile_definition[:gemfile]
@@ -513,6 +558,8 @@ module Bundler
513
558
 
514
559
  definition = builder.to_definition(lockfile, { bundler: unlocking_bundler })
515
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) }
516
563
 
517
564
  # match the parent lockfile in whether there are checksums
518
565
  unless Bundler.frozen_bundle?
@@ -528,6 +575,10 @@ module Bundler
528
575
  definition.instance_variable_set(:@lockfile_contents, current_lockfile.read) if current_lockfile.exist?
529
576
 
530
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)
531
582
 
532
583
  # install gems for the exact current version of the lockfile
533
584
  # this ensures it doesn't re-resolve with only (different)
@@ -563,8 +614,10 @@ module Bundler
563
614
  previous_ui_level = Bundler.ui.level
564
615
  Bundler.ui.level = "warn"
565
616
  begin
566
- # force a remote resolution if intermediate gems are missing
567
- 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) ||
568
621
  definition.instance_variable_get(:@locked_spec_with_invalid_deps) ||
569
622
  definition.instance_variable_get(:@missing_lockfile_dep) ||
570
623
  definition.instance_variable_get(:@invalid_lockfile_dep)
@@ -584,6 +637,11 @@ module Bundler
584
637
  spec.source.specs << spec
585
638
  end
586
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
+
587
645
  missing_checksums = fill_checksums(definition, lockfile_definition[:parent], cache)
588
646
  # fetch checksums that aren't available locally
589
647
  raise SolveFailure unless missing_checksums.empty? || local_only?
@@ -603,6 +661,11 @@ module Bundler
603
661
  "Run without `--local` to fetch them."
604
662
  end
605
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
+
606
669
  definition.instance_variable_set(:@resolved_bundler_version, unlocking_bundler) if unlocking_bundler
607
670
 
608
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.beta3
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