bundler-multilock 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []