bundler-multilock 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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 +11 -1
- data/lib/bundler/multilock/ui/capture.rb +53 -0
- data/lib/bundler/multilock/version.rb +1 -1
- data/lib/bundler/multilock.rb +122 -41
- metadata +24 -8
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
|
@@ -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
|
data/lib/bundler/multilock.rb
CHANGED
@@ -56,25 +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) &&
|
72
|
+
!parent.exist?
|
74
73
|
raise ArgumentError, "Parent lockfile #{parent} is not defined"
|
75
74
|
end
|
76
75
|
|
77
|
-
lockfile_definitions
|
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
|
-
|
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
|
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 =
|
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.
|
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
|
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,
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
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)
|
199
213
|
|
200
214
|
# adjust locked paths from the parent lockfile to be relative to _this_ gemfile
|
201
215
|
adjusted_parent_lockfile_contents =
|
202
|
-
|
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
|
236
|
+
if lockfile_name.exist?
|
223
237
|
# if the lockfile already exists, "merge" it together
|
224
|
-
parent_lockfile =
|
225
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
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 =
|
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,
|
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.
|
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.
|
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-
|
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.
|
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: []
|