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 +4 -4
- data/lib/bundler/multilock/cache.rb +139 -0
- data/lib/bundler/multilock/check.rb +134 -152
- data/lib/bundler/multilock/ext/dsl.rb +10 -0
- data/lib/bundler/multilock/ui/capture.rb +53 -0
- data/lib/bundler/multilock/version.rb +1 -1
- data/lib/bundler/multilock.rb +115 -42
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4143c13ccc6c8b7ef05ce4372cd1e77b293b3b55c8c1eafbb478e87ce195ae4f
|
4
|
+
data.tar.gz: 4e5ea6d45852f02823c578b3ad78eb95109f16dbbf4d2652cc2dc3f9addb1330
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
@
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
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
|
45
|
-
Bundler.ui.error("Lockfile #{
|
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
|
-
|
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,
|
62
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
return !check_missing_deps if result == :missing_deps
|
88
82
|
|
89
|
-
|
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
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
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
|
data/lib/bundler/multilock.rb
CHANGED
@@ -56,26 +56,24 @@ module Bundler
|
|
56
56
|
# allow short-form lockfile names
|
57
57
|
lockfile = expand_lockfile(lockfile)
|
58
58
|
|
59
|
-
|
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.
|
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
|
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
|
-
|
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
|
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 =
|
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.
|
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
|
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,
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
parent_specs =
|
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
|
-
|
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
|
236
|
+
if lockfile_name.exist?
|
224
237
|
# if the lockfile already exists, "merge" it together
|
225
|
-
parent_lockfile =
|
226
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
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 =
|
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,
|
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
|
548
|
+
definition.lock
|
476
549
|
else
|
477
|
-
definition.lock(lockfile_definition[:lockfile]
|
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.
|
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-
|
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.
|
116
|
+
rubygems_version: 3.5.7
|
115
117
|
signing_key:
|
116
118
|
specification_version: 4
|
117
119
|
summary: Support Multiple Lockfiles
|