bundler-multilock 1.2.2 → 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: aa4ad7559633d79e55eea6f48aa41ea9c99d15449b7f10b702c109e066705dec
4
- data.tar.gz: 4b99ab0309f3853dee89cf3571cee02e2d662ca97cda59e7945547fc5f5039bb
3
+ metadata.gz: 4143c13ccc6c8b7ef05ce4372cd1e77b293b3b55c8c1eafbb478e87ce195ae4f
4
+ data.tar.gz: 4e5ea6d45852f02823c578b3ad78eb95109f16dbbf4d2652cc2dc3f9addb1330
5
5
  SHA512:
6
- metadata.gz: ba7be8c9ed956ef0235497812ab415b91541ac7e290c2a01e8418df10262bb3f7c9d19b0380ee69c44a62bed7c448fbf82ebeda6f282304b9a7966b51aacd23c
7
- data.tar.gz: 46a9d5e61d28a8c0a1b70264a5dbfcf4d865bbc3ced72606287de22ddeb8ec76d40a65d2ca43efa0d70f93625a760a1c95d4879e75c4adf54a9e5efbe0c290e3
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
@@ -41,7 +51,7 @@ module Bundler
41
51
  if block
42
52
  instance_eval(&block)
43
53
  else
44
- instance_eval(contents.dup.tap { |x| x.untaint if RUBY_VERSION < "2.7" }, gemfile.to_s, 1)
54
+ instance_eval(contents.dup.tap { |x| x.untaint if RUBY_VERSION < "2.7" }, @gemfile.to_s, 1)
45
55
  end
46
56
  rescue Exception => e # rubocop:disable Lint/RescueException
47
57
  message = "There was an error " \
@@ -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.2"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -56,25 +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) &&
72
+ !parent.exist?
74
73
  raise ArgumentError, "Parent lockfile #{parent} is not defined"
75
74
  end
76
75
 
77
- lockfile_definitions << (lockfile_def = {
76
+ lockfile_definitions[lockfile] = (lockfile_def = {
78
77
  gemfile: (gemfile && Bundler.root.join(gemfile).expand_path) || Bundler.default_gemfile,
79
78
  lockfile: lockfile,
80
79
  active: active,
@@ -149,37 +148,52 @@ module Bundler
149
148
  Bundler.ui.debug("Syncing to alternate lockfiles")
150
149
 
151
150
  attempts = 1
151
+ previous_contents = Set.new
152
152
 
153
153
  default_root = Bundler.root
154
154
 
155
- checker = Check.new
155
+ cache = Cache.new
156
+ checker = Check.new(cache)
156
157
  synced_any = false
158
+ local_parser_cache = {}
157
159
  Bundler.settings.temporary(cache_all_platforms: true, suppress_install_using_messages: true) do
158
- lockfile_definitions.each do |lockfile_definition|
160
+ lockfile_definitions.each do |lockfile_name, lockfile_definition|
159
161
  # we already wrote the default lockfile
160
- next if lockfile_definition[:lockfile] == Bundler.default_lockfile(force_original: true)
162
+ next if lockfile_name == Bundler.default_lockfile(force_original: true)
161
163
 
162
164
  # root needs to be set so that paths are output relative to the correct root in the lockfile
163
165
  Bundler.root = lockfile_definition[:gemfile].dirname
164
166
 
165
- 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
166
179
 
167
180
  # already up to date?
168
181
  up_to_date = false
169
182
  Bundler.settings.temporary(frozen: true) do
170
183
  Bundler.ui.silence do
171
184
  up_to_date = checker.base_check(lockfile_definition, check_missing_deps: true) &&
172
- checker.check(lockfile_definition)
185
+ checker.deep_check(lockfile_definition)
173
186
  end
174
187
  end
175
188
  if up_to_date
176
189
  attempts = 1
190
+ previous_contents.clear
177
191
  next
178
192
  end
179
193
 
180
194
  if Bundler.frozen_bundle?
181
195
  # if we're frozen, you have to use the pre-existing lockfile
182
- unless lockfile_definition[:lockfile].exist?
196
+ unless lockfile_name.exist?
183
197
  Bundler.ui.error("The bundle is locked, but #{relative_lockfile} is missing. " \
184
198
  "Please make sure you have checked #{relative_lockfile} " \
185
199
  "into version control before deploying.")
@@ -187,19 +201,19 @@ module Bundler
187
201
  end
188
202
 
189
203
  Bundler.ui.info("Installing gems for #{relative_lockfile}...")
190
- write_lockfile(lockfile_definition, lockfile_definition[:lockfile], install: install)
204
+ write_lockfile(lockfile_definition, lockfile_name, cache, install: install)
191
205
  else
192
206
  Bundler.ui.info("Syncing to #{relative_lockfile}...") if attempts == 1
193
207
  synced_any = true
194
208
 
195
- parent = lockfile_definition[:parent]
196
- parent_root = parent.dirname
197
- checker.load_lockfile(parent)
198
- 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)
199
213
 
200
214
  # adjust locked paths from the parent lockfile to be relative to _this_ gemfile
201
215
  adjusted_parent_lockfile_contents =
202
- 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|
203
217
  remote_path = Pathname.new($1)
204
218
  next remote if remote_path.absolute?
205
219
 
@@ -219,23 +233,60 @@ module Bundler
219
233
  TEXT
220
234
  end
221
235
 
222
- if lockfile_definition[:lockfile].exist?
236
+ if lockfile_name.exist?
223
237
  # if the lockfile already exists, "merge" it together
224
- parent_lockfile = LockfileParser.new(adjusted_parent_lockfile_contents)
225
- 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)
226
245
 
227
246
  dependency_changes = false
228
- # 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
229
277
  lockfile.specs.map! do |spec|
230
278
  parent_spec = parent_specs[[spec.name, spec.platform]]
231
279
  next spec unless parent_spec
232
280
 
281
+ next spec if check_precedence.call(spec, parent_spec) == :self
282
+
233
283
  dependency_changes ||= spec != parent_spec
234
- parent_spec
284
+
285
+ new_spec = parent_spec.dup
286
+ new_spec.source = spec.source
287
+ new_spec
235
288
  end
236
289
 
237
- lockfile.specs.replace(parent_lockfile.specs + lockfile.specs).uniq!
238
- lockfile.sources.replace(parent_lockfile.sources + lockfile.sources).uniq!
239
290
  lockfile.platforms.replace(parent_lockfile.platforms).uniq!
240
291
  # prune more specific platforms
241
292
  lockfile.platforms.delete_if do |p1|
@@ -243,9 +294,9 @@ module Bundler
243
294
  p2 != "ruby" && p1 != p2 && MatchPlatform.platforms_match?(p2, p1)
244
295
  end
245
296
  end
246
- 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
247
298
  unless lockfile.bundler_version == parent_lockfile.bundler_version
248
- unlocking_bundler = true
299
+ unlocking_bundler = parent_lockfile.bundler_version
249
300
  lockfile.instance_variable_set(:@bundler_version, parent_lockfile.bundler_version)
250
301
  end
251
302
 
@@ -262,24 +313,27 @@ module Bundler
262
313
  temp_lockfile.write(new_contents)
263
314
  temp_lockfile.flush
264
315
 
265
- had_changes = write_lockfile(lockfile_definition,
266
- temp_lockfile.path,
267
- install: install,
268
- dependency_changes: dependency_changes,
269
- 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)
270
322
  end
323
+ cache.invalidate_lockfile(lockfile_name) if had_changes
271
324
 
272
325
  # if we had changes, bundler may have updated some common
273
326
  # dependencies beyond the default lockfile, so re-run it
274
327
  # once to reset them back to the default lockfile's version.
275
328
  # if it's already good, the `check` check at the beginning of
276
329
  # the loop will skip the second sync anyway.
277
- if had_changes && attempts < 2
330
+ if had_changes
278
331
  attempts += 1
279
332
  Bundler.ui.debug("Re-running sync to #{relative_lockfile} to reset common dependencies")
280
333
  redo
281
334
  else
282
335
  attempts = 1
336
+ previous_contents.clear
283
337
  end
284
338
  end
285
339
  end
@@ -299,7 +353,7 @@ module Bundler
299
353
  @loaded = true
300
354
  return if lockfile_definitions.empty?
301
355
 
302
- return unless lockfile_definitions.none? { |definition| definition[:active] }
356
+ return unless lockfile_definitions.each_value.none? { |definition| definition[:active] }
303
357
 
304
358
  if ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) } ==
305
359
  Bundler.default_lockfile(force_original: true)
@@ -309,9 +363,7 @@ module Bundler
309
363
  raise GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}" if ENV["BUNDLE_LOCKFILE"]
310
364
 
311
365
  # Gemfile.lock isn't explicitly specified, otherwise it would be active
312
- default_lockfile_definition = lockfile_definitions.find do |definition|
313
- definition[:lockfile] == Bundler.default_lockfile(force_original: true)
314
- end
366
+ default_lockfile_definition = self.default_lockfile_definition
315
367
  return unless default_lockfile_definition && default_lockfile_definition[:active] == false
316
368
 
317
369
  raise GemfileEvalError, "No lockfiles marked as active"
@@ -376,10 +428,15 @@ module Bundler
376
428
 
377
429
  # @!visibility private
378
430
  def reset!
379
- @lockfile_definitions = []
431
+ @lockfile_definitions = {}
380
432
  @loaded = false
381
433
  end
382
434
 
435
+ # @!visibility private
436
+ def default_lockfile_definition
437
+ lockfile_definitions[Bundler.default_lockfile(force_original: true)]
438
+ end
439
+
383
440
  private
384
441
 
385
442
  def expand_lockfile(lockfile)
@@ -405,7 +462,12 @@ module Bundler
405
462
  true
406
463
  end
407
464
 
408
- 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)
409
471
  prepare_block = lockfile_definition[:prepare]
410
472
 
411
473
  gemfile = lockfile_definition[:gemfile]
@@ -414,6 +476,12 @@ module Bundler
414
476
  builder = Dsl.new
415
477
  builder.eval_gemfile(gemfile, &prepare_block) if prepare_block
416
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
417
485
 
418
486
  definition = builder.to_definition(lockfile, { bundler: unlocking_bundler })
419
487
  definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
@@ -435,11 +503,12 @@ module Bundler
435
503
 
436
504
  current_definition.resolve_with_cache!
437
505
  if current_definition.missing_specs.any?
506
+ cache.invalidate_checks(current_lockfile)
438
507
  Bundler.with_default_lockfile(current_lockfile) do
439
508
  Installer.install(gemfile.dirname, current_definition, {})
440
509
  end
441
510
  end
442
- rescue RubyVersionMismatch, GemNotFound, SolveFailure
511
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure, InstallError, ProductionError
443
512
  # ignore
444
513
  end
445
514
  end
@@ -469,7 +538,17 @@ module Bundler
469
538
  resolved_remotely = true
470
539
  end
471
540
  SharedHelpers.capture_filesystem_access do
472
- definition.lock(lockfile_definition[:lockfile], true)
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)
546
+ if Bundler.gem_version >= Gem::Version.new("2.5.6")
547
+ definition.instance_variable_set(:@lockfile, lockfile_definition[:lockfile])
548
+ definition.lock
549
+ else
550
+ definition.lock(lockfile_definition[:lockfile])
551
+ end
473
552
  end
474
553
  ensure
475
554
  Bundler.ui.level = previous_ui_level
@@ -494,6 +573,8 @@ module Bundler
494
573
  end
495
574
  end
496
575
 
576
+ # see https://github.com/rubygems/rubygems/pull/7368
577
+ Bundler::LazySpecification.include(Bundler::MatchMetadata) if defined?(Bundler::MatchMetadata)
497
578
  Bundler::Multilock.inject_preamble unless Bundler::Multilock.loaded?
498
579
 
499
580
  # this is terrible, but we can't prepend into these modules because we only load
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.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-09 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
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2.6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: gem-release
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.2'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.2'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: rake
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -58,13 +72,14 @@ dependencies:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
74
  version: '3.12'
61
- description:
62
- email:
75
+ description:
76
+ email:
63
77
  executables: []
64
78
  extensions: []
65
79
  extra_rdoc_files: []
66
80
  files:
67
81
  - lib/bundler/multilock.rb
82
+ - lib/bundler/multilock/cache.rb
68
83
  - lib/bundler/multilock/check.rb
69
84
  - lib/bundler/multilock/ext/bundler.rb
70
85
  - lib/bundler/multilock/ext/definition.rb
@@ -75,6 +90,7 @@ files:
75
90
  - lib/bundler/multilock/ext/source.rb
76
91
  - lib/bundler/multilock/ext/source_list.rb
77
92
  - lib/bundler/multilock/lockfile_generator.rb
93
+ - lib/bundler/multilock/ui/capture.rb
78
94
  - lib/bundler/multilock/version.rb
79
95
  - plugins.rb
80
96
  homepage: https://github.com/instructure/bundler-multilock
@@ -82,7 +98,7 @@ licenses:
82
98
  - MIT
83
99
  metadata:
84
100
  rubygems_mfa_required: 'true'
85
- post_install_message:
101
+ post_install_message:
86
102
  rdoc_options: []
87
103
  require_paths:
88
104
  - lib
@@ -97,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
113
  - !ruby/object:Gem::Version
98
114
  version: '0'
99
115
  requirements: []
100
- rubygems_version: 3.4.19
101
- signing_key:
116
+ rubygems_version: 3.5.7
117
+ signing_key:
102
118
  specification_version: 4
103
119
  summary: Support Multiple Lockfiles
104
120
  test_files: []