bundler-multilock 1.2.3 → 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: 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