bundler-multilock 1.2.3 → 1.3.0

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: cc3afea590bb51cf3bb3ea6e5e1b5fc6da4ba400276db5b99cefbce26ac00a40
4
- data.tar.gz: dc917e664709ab17ffdcd6b8d067c4e54c71377de945ecb9975ad8b8975271fd
3
+ metadata.gz: 4143c13ccc6c8b7ef05ce4372cd1e77b293b3b55c8c1eafbb478e87ce195ae4f
4
+ data.tar.gz: 4e5ea6d45852f02823c578b3ad78eb95109f16dbbf4d2652cc2dc3f9addb1330
5
5
  SHA512:
6
- metadata.gz: 4938a9f3a520ae864bc0f56b8f153d2f2bef00a3c0bacc6ec59647f3cb47db4b53062e213ab368f90c7901d777c911d0245eba9d59239eb1f02c5ee7d406f47c
7
- data.tar.gz: 6055d06c51beec2d7e0be5f96f72edb19fcd70553fd07846350d0df1f8fa01f2addfa34cd75551889ee2af48247a9b1ad5ab45e4d2ac19ed756291ddc08c3dee
6
+ metadata.gz: 8c45ae78248dcee0436eadde12ed749fc81112a514d43454998b1c4c238164060998713477f2689db433a753d652a6d419d45fa19650ec612f4ff0933ce82242
7
+ data.tar.gz: ff5e788cb22be790791b2f99d9abcd2fb7d1da717954fd7fd5644fd06274d12b0e7d98e01b917a3247e47ab08856ae9f994b99ed74f9d257972adc636c4ac3f4
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ui/capture"
4
+
5
+ module Bundler
6
+ module Multilock
7
+ # caches lockfiles across multiple lockfile checks or sync runs
8
+ class Cache
9
+ def initialize
10
+ @contents = {}
11
+ @parsers = {}
12
+ @specs = {}
13
+ @reverse_dependencies = {}
14
+ @reverse_requirements = {}
15
+ @base_checks = {}
16
+ @deep_checks = {}
17
+ @base_check_messages = {}
18
+ @deep_check_messages = {}
19
+ @missing_specs = Set.new
20
+ @logged_missing = false
21
+ end
22
+
23
+ # Removes a given lockfile's associated cached data
24
+ #
25
+ # Should be called if the lockfile is modified
26
+ # @param lockfile_name [Pathname]
27
+ # @return [void]
28
+ def invalidate_lockfile(lockfile_name)
29
+ @contents.delete(lockfile_name)
30
+ @parsers.delete(lockfile_name)
31
+ @specs.delete(lockfile_name)
32
+ @reverse_dependencies.delete(lockfile_name)
33
+ @reverse_requirements.delete(lockfile_name)
34
+ invalidate_checks(lockfile_name)
35
+ end
36
+
37
+ def invalidate_checks(lockfile_name)
38
+ @base_checks.delete(lockfile_name)
39
+ @base_check_messages.delete(lockfile_name)
40
+ # must clear them all; downstream lockfiles may depend on the state of this lockfile
41
+ @deep_checks.clear
42
+ @deep_check_messages.clear
43
+ end
44
+
45
+ # @param lockfile_name [Pathname]
46
+ # @return [String] the raw contents of the lockfile
47
+ def contents(lockfile_name)
48
+ @contents.fetch(lockfile_name) do
49
+ @contents[lockfile_name] = lockfile_name.file? && lockfile_name.read.freeze
50
+ end
51
+ end
52
+
53
+ # @param lockfile_name [Pathname]
54
+ # @return [LockfileParser]
55
+ def parser(lockfile_name)
56
+ @parsers[lockfile_name] ||= LockfileParser.new(contents(lockfile_name))
57
+ end
58
+
59
+ def specs(lockfile_name)
60
+ @specs[lockfile_name] ||= parser(lockfile_name).specs.to_h do |spec|
61
+ [[spec.name, spec.platform], spec]
62
+ end
63
+ end
64
+
65
+ # @param lockfile_name [Pathname]
66
+ # @return [Hash<String, Set<String>>] hash of gem name to set of gem names that depend on it
67
+ def reverse_dependencies(lockfile_name)
68
+ ensure_reverse_data(lockfile_name)
69
+ @reverse_dependencies[lockfile_name]
70
+ end
71
+
72
+ # @param lockfile_name [Pathname]
73
+ # @return [Hash<String, Gem::Requirement>] hash of gem name to requirement for that gem
74
+ def reverse_requirements(lockfile_name)
75
+ ensure_reverse_data(lockfile_name)
76
+ @reverse_requirements[lockfile_name]
77
+ end
78
+
79
+ def conflicting_requirements?(lockfile1_name, lockfile2_name, spec1, spec2)
80
+ reverse_requirements1 = reverse_requirements(lockfile1_name)[spec1.name]
81
+ reverse_requirements2 = reverse_requirements(lockfile2_name)[spec1.name]
82
+
83
+ !reverse_requirements1.satisfied_by?(spec2.version) &&
84
+ !reverse_requirements2.satisfied_by?(spec1.version)
85
+ end
86
+
87
+ def log_missing_spec(spec)
88
+ return if @missing_specs.include?(spec)
89
+
90
+ Bundler.ui.error "The following gems are missing" if @missing_specs.empty?
91
+ @missing_specs << spec
92
+ Bundler.ui.error(" * #{spec.name} (#{spec.version})")
93
+ end
94
+
95
+ %i[base deep].each do |type|
96
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
97
+ def #{type}_check(lockfile_name)
98
+ if @#{type}_checks.key?(lockfile_name)
99
+ @#{type}_check_messages[lockfile_name].replay
100
+ @#{type}_checks[lockfile_name]
101
+ else
102
+ result = nil
103
+ messages = Bundler::Multilock::UI::Capture.capture do
104
+ result = @#{type}_checks[lockfile_name] = yield
105
+ end
106
+ @#{type}_check_messages[lockfile_name] = messages.tap(&:replay)
107
+ result
108
+ end
109
+ end
110
+ RUBY
111
+ end
112
+
113
+ private
114
+
115
+ def ensure_reverse_data(lockfile_name)
116
+ return if @reverse_requirements.key?(lockfile_name)
117
+
118
+ # can use Gem::Requirement.default_prelease when Ruby 2.6 support is dropped
119
+ reverse_requirements = Hash.new { |h, k| h[k] = Gem::Requirement.new(">= 0.a") }
120
+ reverse_dependencies = Hash.new { |h, k| h[k] = Set.new }
121
+
122
+ lockfile = parser(lockfile_name)
123
+
124
+ lockfile.dependencies.each_value do |dep|
125
+ reverse_requirements[dep.name].requirements.concat(dep.requirement.requirements)
126
+ end
127
+ lockfile.specs.each do |spec|
128
+ spec.dependencies.each do |dep|
129
+ reverse_requirements[dep.name].requirements.concat(dep.requirement.requirements)
130
+ reverse_dependencies[dep.name] << spec.name
131
+ end
132
+ end
133
+
134
+ @reverse_requirements[lockfile_name] = reverse_requirements
135
+ @reverse_dependencies[lockfile_name] = reverse_dependencies
136
+ end
137
+ end
138
+ end
139
+ end
@@ -2,31 +2,19 @@
2
2
 
3
3
  require "set"
4
4
 
5
+ require_relative "cache"
6
+
5
7
  module Bundler
6
8
  module Multilock
7
9
  class Check
8
- attr_reader :lockfiles, :lockfile_contents, :lockfile_specs
9
-
10
10
  class << self
11
11
  def run
12
12
  new.run
13
13
  end
14
14
  end
15
15
 
16
- def initialize
17
- @lockfiles = {}
18
- @lockfile_contents = {}
19
- @lockfile_specs = {}
20
- end
21
-
22
- def load_lockfile(lockfile)
23
- return if lockfile_contents.key?(lockfile)
24
-
25
- contents = lockfile_contents[lockfile] = lockfile.read.freeze
26
- parser = lockfiles[lockfile] = LockfileParser.new(contents)
27
- lockfile_specs[lockfile] = parser.specs.to_h do |spec|
28
- [[spec.name, spec.platform], spec]
29
- end
16
+ def initialize(cache = Cache.new)
17
+ @cache = cache
30
18
  end
31
19
 
32
20
  def run(skip_base_checks: false)
@@ -34,177 +22,171 @@ module Bundler
34
22
 
35
23
  success = true
36
24
  unless skip_base_checks
37
- missing_specs = base_check({ gemfile: Bundler.default_gemfile,
38
- lockfile: Bundler.default_lockfile(force_original: true) },
39
- return_missing: true).to_set
25
+ default_lockfile_definition = Multilock.default_lockfile_definition
26
+ default_lockfile_definition ||= { gemfile: Bundler.default_gemfile,
27
+ lockfile: Bundler.default_lockfile(force_original: true) }
28
+ base_check(default_lockfile_definition)
40
29
  end
41
- Multilock.lockfile_definitions.each do |lockfile_definition|
42
- next if lockfile_definition[:lockfile] == Bundler.default_lockfile(force_original: true)
30
+ Multilock.lockfile_definitions.each do |lockfile_name, lockfile_definition|
31
+ next if lockfile_name == Bundler.default_lockfile(force_original: true)
43
32
 
44
- unless lockfile_definition[:lockfile].exist?
45
- Bundler.ui.error("Lockfile #{lockfile_definition[:lockfile]} does not exist.")
33
+ unless lockfile_name.exist?
34
+ Bundler.ui.error("Lockfile #{lockfile_name} does not exist.")
46
35
  success = false
36
+ next
47
37
  end
48
38
 
49
- unless skip_base_checks
50
- new_missing = base_check(lockfile_definition, log_missing: missing_specs, return_missing: true)
51
- success = false unless new_missing.empty?
52
- missing_specs.merge(new_missing)
53
- end
54
- success = false unless check(lockfile_definition)
39
+ success &&= base_check(lockfile_definition) && deep_check(lockfile_definition)
55
40
  end
56
41
  success
57
42
  end
58
43
 
59
44
  # this is mostly equivalent to the built in checks in `bundle check`, but even
60
45
  # more conservative, and returns false instead of exiting on failure
61
- def base_check(lockfile_definition, log_missing: false, return_missing: false, check_missing_deps: false)
62
- return return_missing ? [] : false unless lockfile_definition[:lockfile].file?
63
-
64
- Multilock.prepare_block = lockfile_definition[:prepare]
65
- definition = Definition.build(lockfile_definition[:gemfile], lockfile_definition[:lockfile], false)
66
- return return_missing ? [] : false unless definition.send(:current_platform_locked?)
67
-
68
- begin
69
- definition.validate_runtime!
70
- not_installed = Bundler.ui.silence { definition.missing_specs }
71
- rescue RubyVersionMismatch, GemNotFound, SolveFailure
72
- return return_missing ? [] : false
73
- end
46
+ def base_check(lockfile_definition, check_missing_deps: false)
47
+ lockfile_name = lockfile_definition[:lockfile]
48
+ default_root = Bundler.root
74
49
 
75
- if log_missing
76
- not_installed.each do |spec|
77
- next if log_missing.include?(spec)
50
+ result = @cache.base_check(lockfile_name) do
51
+ next false unless lockfile_name.file?
78
52
 
79
- Bundler.ui.error "The following gems are missing" if log_missing.empty?
80
- Bundler.ui.error(" * #{spec.name} (#{spec.version})")
53
+ Multilock.prepare_block = lockfile_definition[:prepare]
54
+ # root needs to be set so that paths are output relative to the correct root in the lockfile
55
+ Bundler.root = lockfile_definition[:gemfile].dirname
56
+
57
+ definition = Definition.build(lockfile_definition[:gemfile], lockfile_name, false)
58
+ next false unless definition.send(:current_platform_locked?)
59
+
60
+ begin
61
+ definition.validate_runtime!
62
+ not_installed = Bundler.ui.silence { definition.missing_specs }
63
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure
64
+ next false
81
65
  end
82
- end
83
66
 
84
- return not_installed if return_missing
67
+ if Bundler.ui.error?
68
+ not_installed.each do |spec|
69
+ @cache.log_missing_spec(spec)
70
+ end
71
+ end
72
+
73
+ next false unless not_installed.empty?
74
+
75
+ # cache a sentinel so that we can share a cache regardless of the check_missing_deps argument
76
+ next :missing_deps unless (definition.locked_gems.dependencies.values - definition.dependencies).empty?
85
77
 
86
- return false unless not_installed.empty? && definition.no_resolve_needed?
87
- return true unless check_missing_deps
78
+ true
79
+ end
80
+
81
+ return !check_missing_deps if result == :missing_deps
88
82
 
89
- (definition.locked_gems.dependencies.values - definition.dependencies).empty?
83
+ result
90
84
  ensure
91
85
  Multilock.prepare_block = nil
86
+ Bundler.root = default_root
92
87
  end
93
88
 
94
89
  # this checks for mismatches between the parent lockfile and the given lockfile,
95
90
  # and for pinned dependencies in lockfiles requiring them
96
- def check(lockfile_definition)
97
- success = true
98
- proven_pinned = Set.new
99
- needs_pin_check = []
100
- lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
101
- lockfile_path = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
102
- parent = lockfile_definition[:parent]
103
- load_lockfile(parent)
104
- parent_lockfile = lockfiles[parent]
105
- unless lockfile.platforms == parent_lockfile.platforms
106
- Bundler.ui.error("The platforms in #{lockfile_path} do not match the parent lockfile.")
107
- success = false
108
- end
109
- unless lockfile.bundler_version == parent_lockfile.bundler_version
110
- Bundler.ui.error("bundler (#{lockfile.bundler_version}) in #{lockfile_path} " \
111
- "does not match the parent lockfile's version (@#{parent_lockfile.bundler_version}).")
112
- success = false
113
- end
114
-
115
- reverse_dependencies = cache_reverse_dependencies(lockfile)
116
- parent_reverse_dependencies = cache_reverse_dependencies(parent_lockfile)
117
-
118
- # look through top-level explicit dependencies for pinned requirements
119
- if lockfile_definition[:enforce_pinned_additional_dependencies]
120
- find_pinned_dependencies(proven_pinned, lockfile.dependencies.each_value)
121
- end
122
-
123
- # check for conflicting requirements (and build list of pins, in the same loop)
124
- lockfile.specs.each do |spec|
125
- parent_spec = lockfile_specs[parent][[spec.name, spec.platform]]
91
+ def deep_check(lockfile_definition)
92
+ lockfile_name = lockfile_definition[:lockfile]
93
+ @cache.deep_check(lockfile_name) do
94
+ success = true
95
+ proven_pinned = Set.new
96
+ needs_pin_check = []
97
+ parser = @cache.parser(lockfile_name)
98
+ lockfile_path = lockfile_name.relative_path_from(Dir.pwd)
99
+ parent_lockfile_name = lockfile_definition[:parent]
100
+ parent_parser = @cache.parser(parent_lockfile_name)
101
+ unless parser.platforms == parent_parser.platforms
102
+ Bundler.ui.error("The platforms in #{lockfile_path} do not match the parent lockfile.")
103
+ success = false
104
+ end
105
+ unless parser.bundler_version == parent_parser.bundler_version
106
+ Bundler.ui.error("bundler (#{parser.bundler_version}) in #{lockfile_path} " \
107
+ "does not match the parent lockfile's version (@#{parent_parser.bundler_version}).")
108
+ success = false
109
+ end
110
+ unless parser.ruby_version == parent_parser.ruby_version
111
+ Bundler.ui.error("ruby (#{parser.ruby_version || "<none>"}) in #{lockfile_path} " \
112
+ "does not match the parent lockfile's version (#{parent_parser.ruby_version}).")
113
+ success = false
114
+ end
126
115
 
116
+ # look through top-level explicit dependencies for pinned requirements
127
117
  if lockfile_definition[:enforce_pinned_additional_dependencies]
128
- # look through what this spec depends on, and keep track of all pinned requirements
129
- find_pinned_dependencies(proven_pinned, spec.dependencies)
130
-
131
- needs_pin_check << spec unless parent_spec
118
+ find_pinned_dependencies(proven_pinned, parser.dependencies.each_value)
132
119
  end
133
120
 
134
- next unless parent_spec
135
-
136
- # have to ensure Path sources are relative to their lockfile before comparing
137
- same_source = if [parent_spec.source, spec.source].grep(Source::Path).length == 2
138
- lockfile_definition[:lockfile]
139
- .dirname
140
- .join(spec.source.path)
141
- .ascend
142
- .any?(parent.dirname.join(parent_spec.source.path))
143
- else
144
- parent_spec.source == spec.source
145
- end
146
-
147
- next if parent_spec.version == spec.version && same_source
148
-
149
- # the version in the parent lockfile cannot possibly satisfy the requirements
150
- # in this lockfile, and vice versa, so we assume it's intentional and allow it
151
- unless reverse_dependencies[spec.name].satisfied_by?(parent_spec.version) ||
152
- parent_reverse_dependencies[spec.name].satisfied_by?(spec.version)
153
- # we're allowing it to differ from the parent, so pin check requirement comes into play
154
- needs_pin_check << spec if lockfile_definition[:enforce_pinned_additional_dependencies]
155
- next
121
+ # check for conflicting requirements (and build list of pins, in the same loop)
122
+ parser.specs.each do |spec|
123
+ parent_spec = @cache.specs(parent_lockfile_name)[[spec.name, spec.platform]]
124
+
125
+ if lockfile_definition[:enforce_pinned_additional_dependencies]
126
+ # look through what this spec depends on, and keep track of all pinned requirements
127
+ find_pinned_dependencies(proven_pinned, spec.dependencies)
128
+
129
+ needs_pin_check << spec unless parent_spec
130
+ end
131
+
132
+ next unless parent_spec
133
+
134
+ # have to ensure Path sources are relative to their lockfile before comparing
135
+ same_source = if [parent_spec.source, spec.source].grep(Source::Path).length == 2
136
+ lockfile_name
137
+ .dirname
138
+ .join(spec.source.path)
139
+ .ascend
140
+ .any?(parent_lockfile_name.dirname.join(parent_spec.source.path))
141
+ else
142
+ parent_spec.source == spec.source
143
+ end
144
+
145
+ next if parent_spec.version == spec.version && same_source
146
+
147
+ # the version in the parent lockfile cannot possibly satisfy the requirements
148
+ # in this lockfile, and vice versa, so we assume it's intentional and allow it
149
+ if @cache.conflicting_requirements?(lockfile_name, parent_lockfile_name, spec, parent_spec)
150
+ # we're allowing it to differ from the parent, so pin check requirement comes into play
151
+ needs_pin_check << spec if lockfile_definition[:enforce_pinned_additional_dependencies]
152
+ next
153
+ end
154
+
155
+ Bundler.ui.error("#{spec}#{spec.git_version} in #{lockfile_path} " \
156
+ "does not match the parent lockfile's version " \
157
+ "(@#{parent_spec.version}#{parent_spec.git_version}); " \
158
+ "this may be due to a conflicting requirement, which would require manual resolution.")
159
+ success = false
156
160
  end
157
161
 
158
- Bundler.ui.error("#{spec}#{spec.git_version} in #{lockfile_path} " \
159
- "does not match the parent lockfile's version " \
160
- "(@#{parent_spec.version}#{parent_spec.git_version}); " \
161
- "this may be due to a conflicting requirement, which would require manual resolution.")
162
- success = false
163
- end
162
+ # now that we have built a list of every gem that is pinned, go through
163
+ # the gems that were in this lockfile, but not the parent lockfile, and
164
+ # ensure it's pinned _somehow_
165
+ needs_pin_check.each do |spec|
166
+ pinned = case spec.source
167
+ when Source::Git
168
+ spec.source.ref == spec.source.revision
169
+ when Source::Path
170
+ true
171
+ when Source::Rubygems
172
+ proven_pinned.include?(spec.name)
173
+ else
174
+ false
175
+ end
176
+
177
+ next if pinned
178
+
179
+ Bundler.ui.error("#{spec} in #{lockfile_path} has not been pinned to a specific version, " \
180
+ "which is required since it is not part of the parent lockfile.")
181
+ success = false
182
+ end
164
183
 
165
- # now that we have built a list of every gem that is pinned, go through
166
- # the gems that were in this lockfile, but not the parent lockfile, and
167
- # ensure it's pinned _somehow_
168
- needs_pin_check.each do |spec|
169
- pinned = case spec.source
170
- when Source::Git
171
- spec.source.ref == spec.source.revision
172
- when Source::Path
173
- true
174
- when Source::Rubygems
175
- proven_pinned.include?(spec.name)
176
- else
177
- false
178
- end
179
-
180
- next if pinned
181
-
182
- Bundler.ui.error("#{spec} in #{lockfile_path} has not been pinned to a specific version, " \
183
- "which is required since it is not part of the parent lockfile.")
184
- success = false
184
+ success
185
185
  end
186
-
187
- success
188
186
  end
189
187
 
190
188
  private
191
189
 
192
- def cache_reverse_dependencies(lockfile)
193
- # can use Gem::Requirement.default_prelease when Ruby 2.6 support is dropped
194
- reverse_dependencies = Hash.new { |h, k| h[k] = Gem::Requirement.new(">= 0.a") }
195
-
196
- lockfile.dependencies.each_value do |spec|
197
- reverse_dependencies[spec.name].requirements.concat(spec.requirement.requirements)
198
- end
199
- lockfile.specs.each do |spec|
200
- spec.dependencies.each do |dependency|
201
- reverse_dependencies[dependency.name].requirements.concat(dependency.requirement.requirements)
202
- end
203
- end
204
-
205
- reverse_dependencies
206
- end
207
-
208
190
  def find_pinned_dependencies(proven_pinned, dependencies)
209
191
  dependencies.each do |dependency|
210
192
  dependency.requirement.requirements.each do |requirement|
@@ -11,12 +11,22 @@ module Bundler
11
11
 
12
12
  # Significant changes:
13
13
  # * evaluate the prepare block as part of the gemfile
14
+ # * keep track of the ruby version set in the default gemfile
15
+ # * apply that ruby version to alternate lockfiles if they didn't set one
16
+ # themselves
14
17
  # * mark Multilock as loaded once the main gemfile is evaluated
15
18
  # so that they're not loaded multiple times
16
19
  def evaluate(gemfile, lockfile, unlock)
17
20
  builder = new
18
21
  builder.eval_gemfile(gemfile, &Multilock.prepare_block) if Multilock.prepare_block
19
22
  builder.eval_gemfile(gemfile)
23
+ if (ruby_version_requirement = builder.instance_variable_get(:@ruby_version))
24
+ Multilock.lockfile_definitions[lockfile][:ruby_version_requirement] = ruby_version_requirement
25
+ elsif (parent_lockfile = Multilock.lockfile_definitions.dig(lockfile, :parent)) &&
26
+ (parent_lockfile_definition = Multilock.lockfile_definitions[parent_lockfile]) &&
27
+ (parent_ruby_version_requirement = parent_lockfile_definition[:ruby_version_requirement])
28
+ builder.instance_variable_set(:@ruby_version, parent_ruby_version_requirement)
29
+ end
20
30
  Multilock.loaded!
21
31
  builder.to_definition(lockfile, unlock)
22
32
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ module UI
6
+ class Capture < Bundler::UI::Silent
7
+ class << self
8
+ def capture
9
+ original_ui = Bundler.ui
10
+ Bundler.ui = new
11
+ yield
12
+ Bundler.ui
13
+ ensure
14
+ Bundler.ui = original_ui
15
+ end
16
+ end
17
+
18
+ def initialize
19
+ @messages = []
20
+
21
+ super
22
+ end
23
+
24
+ def replay
25
+ @messages.each do |(level, args)|
26
+ Bundler.ui.send(level, *args)
27
+ end
28
+ nil
29
+ end
30
+
31
+ def add_color(string, _color)
32
+ string
33
+ end
34
+
35
+ %i[info confirm warn error debug].each do |level|
36
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
37
+ def #{level}(message = nil, newline = nil) # def info(message = nil, newline = nil)
38
+ @messages << [:#{level}, [message, newline]] # @messages << [:info, [message, newline]]
39
+ end # end
40
+ #
41
+ def #{level}? # def info?
42
+ true # true
43
+ end # end
44
+ RUBY
45
+
46
+ def trace(message, newline = nil, force = false) # rubocop:disable Style/OptionalBooleanParameter
47
+ @messages << [:trace, [message, newline, force]]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bundler
4
4
  module Multilock
5
- VERSION = "1.2.3"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -56,26 +56,24 @@ module Bundler
56
56
  # allow short-form lockfile names
57
57
  lockfile = expand_lockfile(lockfile)
58
58
 
59
- if lockfile_definitions.find { |definition| definition[:lockfile] == lockfile }
60
- raise ArgumentError, "Lockfile #{lockfile} is already defined"
61
- end
59
+ raise ArgumentError, "Lockfile #{lockfile} is already defined" if lockfile_definitions.key?(lockfile)
62
60
 
63
61
  env_lockfile = lockfile if active && ENV["BUNDLE_LOCKFILE"] == "active"
64
62
  env_lockfile ||= ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) }
65
63
  active = env_lockfile == lockfile if env_lockfile
66
64
 
67
- if active && (old_active = lockfile_definitions.find { |definition| definition[:active] })
65
+ if active && (old_active = lockfile_definitions.each_value.find { |definition| definition[:active] })
68
66
  raise ArgumentError, "Only one lockfile (#{old_active[:lockfile]}) can be flagged as active"
69
67
  end
70
68
 
71
69
  parent = expand_lockfile(parent)
72
70
  if parent != Bundler.default_lockfile(force_original: true) &&
73
- !lockfile_definitions.find { |definition| definition[:lockfile] == parent } &&
71
+ !lockfile_definitions.key?(parent) &&
74
72
  !parent.exist?
75
73
  raise ArgumentError, "Parent lockfile #{parent} is not defined"
76
74
  end
77
75
 
78
- lockfile_definitions << (lockfile_def = {
76
+ lockfile_definitions[lockfile] = (lockfile_def = {
79
77
  gemfile: (gemfile && Bundler.root.join(gemfile).expand_path) || Bundler.default_gemfile,
80
78
  lockfile: lockfile,
81
79
  active: active,
@@ -150,37 +148,52 @@ module Bundler
150
148
  Bundler.ui.debug("Syncing to alternate lockfiles")
151
149
 
152
150
  attempts = 1
151
+ previous_contents = Set.new
153
152
 
154
153
  default_root = Bundler.root
155
154
 
156
- checker = Check.new
155
+ cache = Cache.new
156
+ checker = Check.new(cache)
157
157
  synced_any = false
158
+ local_parser_cache = {}
158
159
  Bundler.settings.temporary(cache_all_platforms: true, suppress_install_using_messages: true) do
159
- lockfile_definitions.each do |lockfile_definition|
160
+ lockfile_definitions.each do |lockfile_name, lockfile_definition|
160
161
  # we already wrote the default lockfile
161
- next if lockfile_definition[:lockfile] == Bundler.default_lockfile(force_original: true)
162
+ next if lockfile_name == Bundler.default_lockfile(force_original: true)
162
163
 
163
164
  # root needs to be set so that paths are output relative to the correct root in the lockfile
164
165
  Bundler.root = lockfile_definition[:gemfile].dirname
165
166
 
166
- relative_lockfile = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
167
+ relative_lockfile = lockfile_name.relative_path_from(Dir.pwd)
168
+
169
+ # prevent infinite loops of tick-tocking back and forth between two versions
170
+ current_contents = cache.contents(lockfile_name)
171
+ if previous_contents.include?(current_contents)
172
+ Bundler.ui.debug("Unable to converge on a single solution for #{lockfile_name}; " \
173
+ "perhaps there are conflicting requirements?")
174
+ attempts = 1
175
+ previous_contents.clear
176
+ next
177
+ end
178
+ previous_contents << current_contents
167
179
 
168
180
  # already up to date?
169
181
  up_to_date = false
170
182
  Bundler.settings.temporary(frozen: true) do
171
183
  Bundler.ui.silence do
172
184
  up_to_date = checker.base_check(lockfile_definition, check_missing_deps: true) &&
173
- checker.check(lockfile_definition)
185
+ checker.deep_check(lockfile_definition)
174
186
  end
175
187
  end
176
188
  if up_to_date
177
189
  attempts = 1
190
+ previous_contents.clear
178
191
  next
179
192
  end
180
193
 
181
194
  if Bundler.frozen_bundle?
182
195
  # if we're frozen, you have to use the pre-existing lockfile
183
- unless lockfile_definition[:lockfile].exist?
196
+ unless lockfile_name.exist?
184
197
  Bundler.ui.error("The bundle is locked, but #{relative_lockfile} is missing. " \
185
198
  "Please make sure you have checked #{relative_lockfile} " \
186
199
  "into version control before deploying.")
@@ -188,19 +201,19 @@ module Bundler
188
201
  end
189
202
 
190
203
  Bundler.ui.info("Installing gems for #{relative_lockfile}...")
191
- write_lockfile(lockfile_definition, lockfile_definition[:lockfile], install: install)
204
+ write_lockfile(lockfile_definition, lockfile_name, cache, install: install)
192
205
  else
193
206
  Bundler.ui.info("Syncing to #{relative_lockfile}...") if attempts == 1
194
207
  synced_any = true
195
208
 
196
- parent = lockfile_definition[:parent]
197
- parent_root = parent.dirname
198
- checker.load_lockfile(parent)
199
- parent_specs = checker.lockfile_specs[parent]
209
+ specs = lockfile_name.exist? ? cache.specs(lockfile_name) : {}
210
+ parent_lockfile_name = lockfile_definition[:parent]
211
+ parent_root = parent_lockfile_name.dirname
212
+ parent_specs = cache.specs(parent_lockfile_name)
200
213
 
201
214
  # adjust locked paths from the parent lockfile to be relative to _this_ gemfile
202
215
  adjusted_parent_lockfile_contents =
203
- checker.lockfile_contents[parent].gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
216
+ cache.contents(parent_lockfile_name).gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
204
217
  remote_path = Pathname.new($1)
205
218
  next remote if remote_path.absolute?
206
219
 
@@ -220,23 +233,60 @@ module Bundler
220
233
  TEXT
221
234
  end
222
235
 
223
- if lockfile_definition[:lockfile].exist?
236
+ if lockfile_name.exist?
224
237
  # if the lockfile already exists, "merge" it together
225
- parent_lockfile = LockfileParser.new(adjusted_parent_lockfile_contents)
226
- lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
238
+ parent_lockfile = if adjusted_parent_lockfile_contents == cache.contents(lockfile_name)
239
+ cache.parser(parent_lockfile_name)
240
+ else
241
+ local_parser_cache[adjusted_parent_lockfile_contents] ||=
242
+ LockfileParser.new(adjusted_parent_lockfile_contents)
243
+ end
244
+ lockfile = cache.parser(lockfile_name)
227
245
 
228
246
  dependency_changes = false
229
- # replace any duplicate specs with what's in the default lockfile
247
+
248
+ spec_precedences = {}
249
+
250
+ check_precedence = lambda do |spec, parent_spec|
251
+ next :parent if spec.nil?
252
+ next :self if parent_spec.nil?
253
+ next spec_precedences[spec.name] if spec_precedences.key?(spec.name)
254
+
255
+ precedence = :self if cache.conflicting_requirements?(lockfile_name,
256
+ parent_lockfile_name,
257
+ spec,
258
+ parent_spec)
259
+
260
+ # look through all reverse dependencies; if any of them say it
261
+ # has to come from self, due to conflicts, then this gem has
262
+ # to come from self as well
263
+ [cache.reverse_dependencies(lockfile_name),
264
+ cache.reverse_dependencies(parent_lockfile_name)].each do |reverse_dependencies|
265
+ break if precedence == :self
266
+
267
+ reverse_dependencies[spec.name].each do |dep_name|
268
+ precedence = check_precedence.call(specs[dep_name], parent_specs[dep_name])
269
+ break if precedence == :self
270
+ end
271
+ end
272
+
273
+ spec_precedences[spec.name] = precedence || :parent
274
+ end
275
+
276
+ # replace any duplicate specs with what's in the parent lockfile
230
277
  lockfile.specs.map! do |spec|
231
278
  parent_spec = parent_specs[[spec.name, spec.platform]]
232
279
  next spec unless parent_spec
233
280
 
281
+ next spec if check_precedence.call(spec, parent_spec) == :self
282
+
234
283
  dependency_changes ||= spec != parent_spec
235
- parent_spec
284
+
285
+ new_spec = parent_spec.dup
286
+ new_spec.source = spec.source
287
+ new_spec
236
288
  end
237
289
 
238
- lockfile.specs.replace(parent_lockfile.specs + lockfile.specs).uniq!
239
- lockfile.sources.replace(parent_lockfile.sources + lockfile.sources).uniq!
240
290
  lockfile.platforms.replace(parent_lockfile.platforms).uniq!
241
291
  # prune more specific platforms
242
292
  lockfile.platforms.delete_if do |p1|
@@ -244,9 +294,9 @@ module Bundler
244
294
  p2 != "ruby" && p1 != p2 && MatchPlatform.platforms_match?(p2, p1)
245
295
  end
246
296
  end
247
- lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version)
297
+ lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version) if lockfile.ruby_version
248
298
  unless lockfile.bundler_version == parent_lockfile.bundler_version
249
- unlocking_bundler = true
299
+ unlocking_bundler = parent_lockfile.bundler_version
250
300
  lockfile.instance_variable_set(:@bundler_version, parent_lockfile.bundler_version)
251
301
  end
252
302
 
@@ -263,24 +313,27 @@ module Bundler
263
313
  temp_lockfile.write(new_contents)
264
314
  temp_lockfile.flush
265
315
 
266
- had_changes = write_lockfile(lockfile_definition,
267
- temp_lockfile.path,
268
- install: install,
269
- dependency_changes: dependency_changes,
270
- unlocking_bundler: unlocking_bundler)
316
+ had_changes ||= write_lockfile(lockfile_definition,
317
+ temp_lockfile.path,
318
+ cache,
319
+ install: install,
320
+ dependency_changes: dependency_changes,
321
+ unlocking_bundler: unlocking_bundler)
271
322
  end
323
+ cache.invalidate_lockfile(lockfile_name) if had_changes
272
324
 
273
325
  # if we had changes, bundler may have updated some common
274
326
  # dependencies beyond the default lockfile, so re-run it
275
327
  # once to reset them back to the default lockfile's version.
276
328
  # if it's already good, the `check` check at the beginning of
277
329
  # the loop will skip the second sync anyway.
278
- if had_changes && attempts < 2
330
+ if had_changes
279
331
  attempts += 1
280
332
  Bundler.ui.debug("Re-running sync to #{relative_lockfile} to reset common dependencies")
281
333
  redo
282
334
  else
283
335
  attempts = 1
336
+ previous_contents.clear
284
337
  end
285
338
  end
286
339
  end
@@ -300,7 +353,7 @@ module Bundler
300
353
  @loaded = true
301
354
  return if lockfile_definitions.empty?
302
355
 
303
- return unless lockfile_definitions.none? { |definition| definition[:active] }
356
+ return unless lockfile_definitions.each_value.none? { |definition| definition[:active] }
304
357
 
305
358
  if ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) } ==
306
359
  Bundler.default_lockfile(force_original: true)
@@ -310,9 +363,7 @@ module Bundler
310
363
  raise GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}" if ENV["BUNDLE_LOCKFILE"]
311
364
 
312
365
  # Gemfile.lock isn't explicitly specified, otherwise it would be active
313
- default_lockfile_definition = lockfile_definitions.find do |definition|
314
- definition[:lockfile] == Bundler.default_lockfile(force_original: true)
315
- end
366
+ default_lockfile_definition = self.default_lockfile_definition
316
367
  return unless default_lockfile_definition && default_lockfile_definition[:active] == false
317
368
 
318
369
  raise GemfileEvalError, "No lockfiles marked as active"
@@ -377,10 +428,15 @@ module Bundler
377
428
 
378
429
  # @!visibility private
379
430
  def reset!
380
- @lockfile_definitions = []
431
+ @lockfile_definitions = {}
381
432
  @loaded = false
382
433
  end
383
434
 
435
+ # @!visibility private
436
+ def default_lockfile_definition
437
+ lockfile_definitions[Bundler.default_lockfile(force_original: true)]
438
+ end
439
+
384
440
  private
385
441
 
386
442
  def expand_lockfile(lockfile)
@@ -406,7 +462,12 @@ module Bundler
406
462
  true
407
463
  end
408
464
 
409
- def write_lockfile(lockfile_definition, lockfile, install:, dependency_changes: false, unlocking_bundler: false)
465
+ def write_lockfile(lockfile_definition,
466
+ lockfile,
467
+ cache,
468
+ install:,
469
+ dependency_changes: false,
470
+ unlocking_bundler: false)
410
471
  prepare_block = lockfile_definition[:prepare]
411
472
 
412
473
  gemfile = lockfile_definition[:gemfile]
@@ -415,6 +476,12 @@ module Bundler
415
476
  builder = Dsl.new
416
477
  builder.eval_gemfile(gemfile, &prepare_block) if prepare_block
417
478
  builder.eval_gemfile(gemfile)
479
+ if !builder.instance_variable_get(:@ruby_version) &&
480
+ (parent_lockfile = lockfile_definition[:parent]) &&
481
+ (parent_lockfile_definition = lockfile_definitions[parent_lockfile]) &&
482
+ (parent_ruby_version_requirement = parent_lockfile_definition[:ruby_version_requirement])
483
+ builder.instance_variable_set(:@ruby_version, parent_ruby_version_requirement)
484
+ end
418
485
 
419
486
  definition = builder.to_definition(lockfile, { bundler: unlocking_bundler })
420
487
  definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
@@ -436,11 +503,12 @@ module Bundler
436
503
 
437
504
  current_definition.resolve_with_cache!
438
505
  if current_definition.missing_specs.any?
506
+ cache.invalidate_checks(current_lockfile)
439
507
  Bundler.with_default_lockfile(current_lockfile) do
440
508
  Installer.install(gemfile.dirname, current_definition, {})
441
509
  end
442
510
  end
443
- rescue RubyVersionMismatch, GemNotFound, SolveFailure
511
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure, InstallError, ProductionError
444
512
  # ignore
445
513
  end
446
514
  end
@@ -470,11 +538,16 @@ module Bundler
470
538
  resolved_remotely = true
471
539
  end
472
540
  SharedHelpers.capture_filesystem_access do
541
+ definition.instance_variable_set(:@resolved_bundler_version, unlocking_bundler) if unlocking_bundler
542
+
543
+ # need to force it to _not_ preserve unknown sections, so that it
544
+ # will overwrite the ruby version
545
+ definition.instance_variable_set(:@unlocking_bundler, true)
473
546
  if Bundler.gem_version >= Gem::Version.new("2.5.6")
474
547
  definition.instance_variable_set(:@lockfile, lockfile_definition[:lockfile])
475
- definition.lock(true)
548
+ definition.lock
476
549
  else
477
- definition.lock(lockfile_definition[:lockfile], true)
550
+ definition.lock(lockfile_definition[:lockfile])
478
551
  end
479
552
  end
480
553
  ensure
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-multilock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-12 00:00:00.000000000 Z
11
+ date: 2024-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -79,6 +79,7 @@ extensions: []
79
79
  extra_rdoc_files: []
80
80
  files:
81
81
  - lib/bundler/multilock.rb
82
+ - lib/bundler/multilock/cache.rb
82
83
  - lib/bundler/multilock/check.rb
83
84
  - lib/bundler/multilock/ext/bundler.rb
84
85
  - lib/bundler/multilock/ext/definition.rb
@@ -89,6 +90,7 @@ files:
89
90
  - lib/bundler/multilock/ext/source.rb
90
91
  - lib/bundler/multilock/ext/source_list.rb
91
92
  - lib/bundler/multilock/lockfile_generator.rb
93
+ - lib/bundler/multilock/ui/capture.rb
92
94
  - lib/bundler/multilock/version.rb
93
95
  - plugins.rb
94
96
  homepage: https://github.com/instructure/bundler-multilock
@@ -111,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
113
  - !ruby/object:Gem::Version
112
114
  version: '0'
113
115
  requirements: []
114
- rubygems_version: 3.5.6
116
+ rubygems_version: 3.5.7
115
117
  signing_key:
116
118
  specification_version: 4
117
119
  summary: Support Multiple Lockfiles