bundler-multilock 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 648cdaff608ff51cfe15ab2c098cff90218068d650f9c0fa4825dca90d2cc573
4
+ data.tar.gz: 12be8f53d490c149f2e2691b4e694c8bc395673a2c30e622b5071b1b4771ddb4
5
+ SHA512:
6
+ metadata.gz: 5e42ff819d3a1640e6c51dd326e01c9d3400a8193807cb1ed3400b72b9dc2b9fb0dc71289471545c74034c61f2c53884ef1f8c53618360d9490ff6193f72a19e
7
+ data.tar.gz: 916d273cb5dbe217d8b5d77be760f9db732e92b39177bfc5487f8868f93e0a90bca11afbeb4c09b3e508f99098f81275973b941e243e7668e0443cd61bcc888a
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Bundler
6
+ module Multilock
7
+ class Check
8
+ class << self
9
+ def run
10
+ new.run
11
+ end
12
+ end
13
+
14
+ def initialize
15
+ default_lockfile_contents = Bundler.default_lockfile.read
16
+ @default_lockfile = LockfileParser.new(default_lockfile_contents)
17
+ @default_specs = @default_lockfile.specs.to_h do |spec|
18
+ [[spec.name, spec.platform], spec]
19
+ end
20
+ end
21
+
22
+ def run
23
+ return true unless Bundler.default_lockfile.exist?
24
+
25
+ success = true
26
+ Multilock.lockfile_definitions.each do |lockfile_definition|
27
+ next unless lockfile_definition[:lockfile].exist?
28
+
29
+ success = false unless check(lockfile_definition)
30
+ end
31
+ success
32
+ end
33
+
34
+ # this is mostly equivalent to the built in checks in `bundle check`, but even
35
+ # more conservative, and returns false instead of exiting on failure
36
+ def base_check(lockfile_definition)
37
+ return false unless lockfile_definition[:lockfile].file?
38
+
39
+ Multilock.prepare_block = lockfile_definition[:prepare]
40
+ definition = Definition.build(lockfile_definition[:gemfile], lockfile_definition[:lockfile], false)
41
+ return false unless definition.send(:current_platform_locked?)
42
+
43
+ begin
44
+ definition.validate_runtime!
45
+ definition.resolve_only_locally!
46
+ not_installed = definition.missing_specs
47
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure
48
+ return false
49
+ end
50
+
51
+ not_installed.empty? && definition.no_resolve_needed?
52
+ ensure
53
+ Multilock.prepare_block = nil
54
+ end
55
+
56
+ # this checks for mismatches between the default lockfile and the given lockfile,
57
+ # and for pinned dependencies in lockfiles requiring them
58
+ def check(lockfile_definition, allow_mismatched_dependencies: true)
59
+ success = true
60
+ proven_pinned = Set.new
61
+ needs_pin_check = []
62
+ lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
63
+ lockfile_path = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
64
+ unless lockfile.platforms == @default_lockfile.platforms
65
+ Bundler.ui.error("The platforms in #{lockfile_path} do not match the default lockfile.")
66
+ success = false
67
+ end
68
+ unless lockfile.bundler_version == @default_lockfile.bundler_version
69
+ Bundler.ui.error("bundler (#{lockfile.bundler_version}) in #{lockfile_path} " \
70
+ "does not match the default lockfile's version (@#{@default_lockfile.bundler_version}).")
71
+ success = false
72
+ end
73
+
74
+ specs = lockfile.specs.group_by(&:name)
75
+ if allow_mismatched_dependencies
76
+ allow_mismatched_dependencies = lockfile_definition[:allow_mismatched_dependencies]
77
+ end
78
+
79
+ # build list of top-level dependencies that differ from the default lockfile,
80
+ # and all _their_ transitive dependencies
81
+ if allow_mismatched_dependencies
82
+ transitive_dependencies = Set.new
83
+ # only dependencies that differ from the default lockfile
84
+ pending_transitive_dependencies = lockfile.dependencies.reject do |name, dep|
85
+ @default_lockfile.dependencies[name] == dep
86
+ end.map(&:first)
87
+
88
+ until pending_transitive_dependencies.empty?
89
+ dep = pending_transitive_dependencies.shift
90
+ next if transitive_dependencies.include?(dep)
91
+
92
+ transitive_dependencies << dep
93
+ platform_specs = specs[dep]
94
+ unless platform_specs
95
+ # should only be bundler that's missing a spec
96
+ raise "Could not find spec for dependency #{dep}" unless dep == "bundler"
97
+
98
+ next
99
+ end
100
+
101
+ pending_transitive_dependencies.concat(platform_specs.flat_map(&:dependencies).map(&:name).uniq)
102
+ end
103
+ end
104
+
105
+ # look through top-level explicit dependencies for pinned requirements
106
+ if lockfile_definition[:enforce_pinned_additional_dependencies]
107
+ find_pinned_dependencies(proven_pinned, lockfile.dependencies.each_value)
108
+ end
109
+
110
+ # check for conflicting requirements (and build list of pins, in the same loop)
111
+ specs.values.flatten.each do |spec|
112
+ default_spec = @default_specs[[spec.name, spec.platform]]
113
+
114
+ if lockfile_definition[:enforce_pinned_additional_dependencies]
115
+ # look through what this spec depends on, and keep track of all pinned requirements
116
+ find_pinned_dependencies(proven_pinned, spec.dependencies)
117
+
118
+ needs_pin_check << spec unless default_spec
119
+ end
120
+
121
+ next unless default_spec
122
+
123
+ # have to ensure Path sources are relative to their lockfile before comparing
124
+ same_source = if [default_spec.source, spec.source].grep(Source::Path).length == 2
125
+ lockfile_definition[:lockfile]
126
+ .dirname
127
+ .join(spec.source.path)
128
+ .ascend
129
+ .any?(Bundler.default_lockfile.dirname.join(default_spec.source.path))
130
+ else
131
+ default_spec.source == spec.source
132
+ end
133
+
134
+ next if default_spec.version == spec.version && same_source
135
+ next if allow_mismatched_dependencies && transitive_dependencies.include?(spec.name)
136
+
137
+ Bundler.ui.error("#{spec}#{spec.git_version} in #{lockfile_path} " \
138
+ "does not match the default lockfile's version " \
139
+ "(@#{default_spec.version}#{default_spec.git_version}); " \
140
+ "this may be due to a conflicting requirement, which would require manual resolution.")
141
+ success = false
142
+ end
143
+
144
+ # now that we have built a list of every gem that is pinned, go through
145
+ # the gems that were in this lockfile, but not the default lockfile, and
146
+ # ensure it's pinned _somehow_
147
+ needs_pin_check.each do |spec|
148
+ pinned = case spec.source
149
+ when Source::Git
150
+ spec.source.ref == spec.source.revision
151
+ when Source::Path
152
+ true
153
+ when Source::Rubygems
154
+ proven_pinned.include?(spec.name)
155
+ else
156
+ false
157
+ end
158
+
159
+ next if pinned
160
+
161
+ Bundler.ui.error("#{spec} in #{lockfile_path} has not been pinned to a specific version, " \
162
+ "which is required since it is not part of the default lockfile.")
163
+ success = false
164
+ end
165
+
166
+ success
167
+ end
168
+
169
+ private
170
+
171
+ def find_pinned_dependencies(proven_pinned, dependencies)
172
+ dependencies.each do |dependency|
173
+ dependency.requirement.requirements.each do |requirement|
174
+ proven_pinned << dependency.name if requirement.first == "="
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ module Ext
6
+ module BundlerClassMethods
7
+ def self.prepended(klass)
8
+ super
9
+
10
+ klass.attr_writer :cache_root, :default_lockfile, :root
11
+ end
12
+
13
+ ::Bundler.singleton_class.prepend(self)
14
+
15
+ def app_cache(custom_path = nil)
16
+ super(custom_path || @cache_root)
17
+ end
18
+
19
+ def default_lockfile(force_original: false)
20
+ return @default_lockfile if @default_lockfile && !force_original
21
+
22
+ super()
23
+ end
24
+
25
+ def with_default_lockfile(lockfile)
26
+ previous_default_lockfile, @default_lockfile = @default_lockfile, lockfile
27
+ yield
28
+ ensure
29
+ @default_lockfile = previous_default_lockfile
30
+ end
31
+
32
+ def reset!
33
+ super
34
+ Multilock.reset!
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ module Ext
6
+ module Definition
7
+ ::Bundler::Definition.prepend(self)
8
+
9
+ def initialize(lockfile, *args)
10
+ # we changed the default lockfile in Bundler::Multilock.add_lockfile
11
+ # since DSL.evaluate was called (re-entrantly); sub the proper value in
12
+ if !lockfile.equal?(Bundler.default_lockfile) &&
13
+ Bundler.default_lockfile(force_original: true) == lockfile
14
+ lockfile = Bundler.default_lockfile
15
+ end
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Bundler
6
+ module Multilock
7
+ module Ext
8
+ module Dsl
9
+ module ClassMethods
10
+ ::Bundler::Dsl.singleton_class.prepend(self)
11
+
12
+ # Significant changes:
13
+ # * evaluate the prepare block as part of the gemfile
14
+ # * mark Multilock as loaded once the main gemfile is evaluated
15
+ # so that they're not loaded multiple times
16
+ def evaluate(gemfile, lockfile, unlock)
17
+ builder = new
18
+ builder.eval_gemfile(gemfile, &Multilock.prepare_block) if Multilock.prepare_block
19
+ builder.eval_gemfile(gemfile)
20
+ Multilock.loaded!
21
+ builder.to_definition(lockfile, unlock)
22
+ end
23
+ end
24
+
25
+ ::Bundler::Dsl.prepend(self)
26
+
27
+ def initialize
28
+ super
29
+ @gemfiles = Set.new
30
+ end
31
+
32
+ # Significant changes:
33
+ # * allow a block
34
+ def eval_gemfile(gemfile, contents = nil, &block)
35
+ expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent)
36
+ original_gemfile = @gemfile
37
+ @gemfile = expanded_gemfile_path
38
+ @gemfiles << expanded_gemfile_path
39
+ contents ||= Bundler.read_file(@gemfile.to_s)
40
+ if block
41
+ instance_eval(&block)
42
+ else
43
+ instance_eval(contents.dup.tap { |x| x.untaint if RUBY_VERSION < "2.7" }, gemfile.to_s, 1)
44
+ end
45
+ rescue Exception => e # rubocop:disable Lint/RescueException
46
+ message = "There was an error " \
47
+ "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \
48
+ "`#{File.basename gemfile.to_s}`: #{e.message}"
49
+
50
+ raise Bundler::Dsl::DSLError.new(message, gemfile, e.backtrace, contents)
51
+ ensure
52
+ @gemfile = original_gemfile
53
+ end
54
+
55
+ def lockfile(*args, **kwargs, &block)
56
+ return if Multilock.loaded?
57
+
58
+ Multilock.add_lockfile(*args, builder: self, **kwargs, &block)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ module Ext
6
+ module PluginExt
7
+ module DSL
8
+ ::Bundler::Plugin::DSL.include(self)
9
+
10
+ def lockfile(...)
11
+ # pass
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ module Ext
6
+ module PluginExt
7
+ module ClassMethods
8
+ ::Bundler::Plugin.singleton_class.prepend(self)
9
+
10
+ def load_plugin(name)
11
+ return if @loaded_plugin_names.include?(name)
12
+
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ module Ext
6
+ module SourceList
7
+ ::Bundler::SourceList.prepend(self)
8
+
9
+ # consider them equivalent if the replacements just have a bunch of dups
10
+ def equivalent_sources?(lock_sources, replacement_sources)
11
+ super(lock_sources, replacement_sources.uniq)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/lockfile_generator"
4
+
5
+ module Bundler
6
+ module Multilock
7
+ # generates a lockfile based on another LockfileParser
8
+ class LockfileGenerator < Bundler::LockfileGenerator
9
+ def self.generate(lockfile)
10
+ new(LockfileAdapter.new(lockfile)).generate!
11
+ end
12
+
13
+ private
14
+
15
+ class LockfileAdapter < SimpleDelegator
16
+ def sources
17
+ self
18
+ end
19
+
20
+ def lock_sources
21
+ __getobj__.sources
22
+ end
23
+
24
+ def resolve
25
+ specs
26
+ end
27
+
28
+ def dependencies
29
+ super.values
30
+ end
31
+
32
+ def locked_ruby_version
33
+ ruby_version
34
+ end
35
+ end
36
+
37
+ private_constant :LockfileAdapter
38
+
39
+ def add_bundled_with
40
+ add_section("BUNDLED WITH", definition.bundler_version.to_s)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "multilock/ext/bundler"
4
+ require_relative "multilock/ext/definition"
5
+ require_relative "multilock/ext/dsl"
6
+ require_relative "multilock/ext/plugin"
7
+ require_relative "multilock/ext/plugin/dsl"
8
+ require_relative "multilock/ext/source_list"
9
+ require_relative "multilock/version"
10
+
11
+ module Bundler
12
+ module Multilock
13
+ class << self
14
+ # @!visibility private
15
+ attr_reader :lockfile_definitions
16
+ # @!visibility private
17
+ attr_accessor :prepare_block
18
+
19
+ # @param lockfile [String] The lockfile path (defaults to Gemfile.lock)
20
+ # @param builder [Dsl] The Bundler DSL
21
+ # @param gemfile [String, nil]
22
+ # The Gemfile for this lockfile (defaults to Gemfile)
23
+ # @param default [Boolean]
24
+ # If this lockfile should be the default (instead of Gemfile.lock)
25
+ # @param allow_mismatched_dependencies [true, false]
26
+ # Allows version differences in dependencies between this lockfile and
27
+ # the default lockfile. Note that even with this option, only top-level
28
+ # dependencies that differ from the default lockfile, and their transitive
29
+ # depedencies, are allowed to mismatch.
30
+ # @param enforce_pinned_additional_dependencies [true, false]
31
+ # If dependencies are present in this lockfile that are not present in the
32
+ # default lockfile, enforce that they are pinned.
33
+ # @yield
34
+ # Block executed only when this lockfile is active.
35
+ # @return [true, false] if the lockfile is the current lockfile
36
+ def add_lockfile(lockfile = nil,
37
+ builder:,
38
+ gemfile: nil,
39
+ default: nil,
40
+ allow_mismatched_dependencies: true,
41
+ enforce_pinned_additional_dependencies: false,
42
+ &block)
43
+ # terminology gets confusing here. The "default" param means
44
+ # "use this lockfile when not overridden by BUNDLE_LOCKFILE"
45
+ # but Bundler.defaul_lockfile (usually) means "Gemfile.lock"
46
+ # so refer to the former as "current" internally
47
+ current = default
48
+ current = true if current.nil? && lockfile_definitions.empty? && lockfile.nil? && gemfile.nil?
49
+
50
+ # allow short-form lockfile names
51
+ lockfile = "Gemfile.#{lockfile}.lock" if lockfile && !(lockfile.include?("/") || lockfile.end_with?(".lock"))
52
+ # if a gemfile was provided, but not a lockfile, infer the default lockfile for that gemfile
53
+ lockfile ||= "#{gemfile}.lock" if gemfile
54
+ # use absolute paths
55
+ lockfile = Bundler.root.join(lockfile).expand_path if lockfile
56
+ # use the default lockfile (Gemfile.lock) if none was given
57
+ lockfile ||= Bundler.default_lockfile(force_original: true)
58
+ if current && (old_current = lockfile_definitions.find { |definition| definition[:current] })
59
+ raise ArgumentError, "Only one lockfile (#{old_current[:lockfile]}) can be flagged as the default"
60
+ end
61
+
62
+ raise ArgumentError, "Lockfile #{lockfile} is already defined" if lockfile_definitions.any? do |definition|
63
+ definition[:lockfile] == lockfile
64
+ end
65
+
66
+ env_lockfile = ENV["BUNDLE_LOCKFILE"]
67
+ if env_lockfile
68
+ unless env_lockfile.include?("/") || env_lockfile.end_with?(".lock")
69
+ env_lockfile = "Gemfile.#{env_lockfile}.lock"
70
+ end
71
+ env_lockfile = Bundler.root.join(env_lockfile).expand_path
72
+ current = env_lockfile == lockfile
73
+ end
74
+
75
+ lockfile_definitions << (lockfile_def = {
76
+ gemfile: (gemfile && Bundler.root.join(gemfile).expand_path) || Bundler.default_gemfile,
77
+ lockfile: lockfile,
78
+ current: current,
79
+ prepare: block,
80
+ allow_mismatched_dependencies: allow_mismatched_dependencies,
81
+ enforce_pinned_additional_dependencies: enforce_pinned_additional_dependencies
82
+ })
83
+
84
+ if (defined?(CLI::Check) ||
85
+ defined?(CLI::Install) ||
86
+ defined?(CLI::Lock) ||
87
+ defined?(CLI::Update)) &&
88
+ !defined?(CLI::Cache)
89
+ # always use Gemfile.lock for `bundle check`, `bundle install`,
90
+ # `bundle lock`, and `bundle update`. `bundle cache` delegates to
91
+ # `bundle install`, but we want that to run as normal.
92
+ current = lockfile == Bundler.default_lockfile(force_original: true)
93
+ end
94
+
95
+ if current
96
+ block&.call
97
+ Bundler.default_lockfile = lockfile
98
+
99
+ # we started evaluating the project's primary gemfile, but got told to use a lockfile
100
+ # associated with a different Gemfile. so we need to evaluate that Gemfile instead
101
+ if lockfile_def[:gemfile] != Bundler.default_gemfile
102
+ # share a cache between all lockfiles
103
+ Bundler.cache_root = Bundler.root
104
+ ENV["BUNDLE_GEMFILE"] = lockfile_def[:gemfile].to_s
105
+ Bundler.root = Bundler.default_gemfile.dirname
106
+ Bundler.default_lockfile = lockfile
107
+
108
+ builder.eval_gemfile(Bundler.default_gemfile)
109
+
110
+ return false
111
+ end
112
+ end
113
+ true
114
+ end
115
+
116
+ # @!visibility private
117
+ def after_install_all(install: true)
118
+ loaded!
119
+ previous_recursive = @recursive
120
+
121
+ return if lockfile_definitions.empty?
122
+ return if ENV["BUNDLE_LOCKFILE"] # explicitly working against a single lockfile
123
+
124
+ # must be running `bundle cache`
125
+ return unless Bundler.default_lockfile == Bundler.default_lockfile(force_original: true)
126
+
127
+ require_relative "multilock/check"
128
+
129
+ if Bundler.frozen_bundle? && !install
130
+ # only do the checks if we're frozen
131
+ exit 1 unless Check.run
132
+ return
133
+ end
134
+
135
+ # this hook will be called recursively when it has to install gems
136
+ # for a secondary lockfile. defend against that
137
+ return if @recursive
138
+
139
+ @recursive = true
140
+
141
+ require "tempfile"
142
+ require_relative "multilock/lockfile_generator"
143
+
144
+ Bundler.ui.info ""
145
+
146
+ default_lockfile_contents = Bundler.default_lockfile.read.freeze
147
+ default_specs = LockfileParser.new(default_lockfile_contents).specs.to_h do |spec|
148
+ [[spec.name, spec.platform], spec]
149
+ end
150
+ default_root = Bundler.root
151
+
152
+ attempts = 1
153
+
154
+ checker = Check.new
155
+ Bundler.settings.temporary(cache_all_platforms: true, suppress_install_using_messages: true) do
156
+ lockfile_definitions.each do |lockfile_definition|
157
+ # we already wrote the default lockfile
158
+ next if lockfile_definition[:lockfile] == Bundler.default_lockfile(force_original: true)
159
+
160
+ # root needs to be set so that paths are output relative to the correct root in the lockfile
161
+ Bundler.root = lockfile_definition[:gemfile].dirname
162
+
163
+ relative_lockfile = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
164
+
165
+ # already up to date?
166
+ up_to_date = false
167
+ Bundler.settings.temporary(frozen: true) do
168
+ Bundler.ui.silence do
169
+ up_to_date = checker.base_check(lockfile_definition) &&
170
+ checker.check(lockfile_definition, allow_mismatched_dependencies: false)
171
+ end
172
+ end
173
+ if up_to_date
174
+ attempts = 1
175
+ next
176
+ end
177
+
178
+ if Bundler.frozen_bundle?
179
+ # if we're frozen, you have to use the pre-existing lockfile
180
+ unless lockfile_definition[:lockfile].exist?
181
+ Bundler.ui.error("The bundle is locked, but #{relative_lockfile} is missing. " \
182
+ "Please make sure you have checked #{relative_lockfile} " \
183
+ "into version control before deploying.")
184
+ exit 1
185
+ end
186
+
187
+ Bundler.ui.info("Installing gems for #{relative_lockfile}...")
188
+ write_lockfile(lockfile_definition, lockfile_definition[:lockfile], install: install)
189
+ else
190
+ Bundler.ui.info("Syncing to #{relative_lockfile}...") if attempts == 1
191
+
192
+ # adjust locked paths from the default lockfile to be relative to _this_ gemfile
193
+ adjusted_default_lockfile_contents =
194
+ default_lockfile_contents.gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
195
+ remote_path = Pathname.new($1)
196
+ next remote if remote_path.absolute?
197
+
198
+ relative_remote_path = remote_path.expand_path(default_root).relative_path_from(Bundler.root).to_s
199
+ remote.sub($1, relative_remote_path)
200
+ end
201
+
202
+ # add a source for the current gem
203
+ gem_spec = default_specs[[File.basename(Bundler.root), "ruby"]]
204
+
205
+ if gem_spec
206
+ adjusted_default_lockfile_contents += <<~TEXT
207
+ PATH
208
+ remote: .
209
+ specs:
210
+ #{gem_spec.to_lock}
211
+ TEXT
212
+ end
213
+
214
+ if lockfile_definition[:lockfile].exist?
215
+ # if the lockfile already exists, "merge" it together
216
+ default_lockfile = LockfileParser.new(adjusted_default_lockfile_contents)
217
+ lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
218
+
219
+ dependency_changes = false
220
+ # replace any duplicate specs with what's in the default lockfile
221
+ lockfile.specs.map! do |spec|
222
+ default_spec = default_specs[[spec.name, spec.platform]]
223
+ next spec unless default_spec
224
+
225
+ dependency_changes ||= spec != default_spec
226
+ default_spec
227
+ end
228
+
229
+ lockfile.specs.replace(default_lockfile.specs + lockfile.specs).uniq!
230
+ lockfile.sources.replace(default_lockfile.sources + lockfile.sources).uniq!
231
+ lockfile.platforms.replace(default_lockfile.platforms).uniq!
232
+ # prune more specific platforms
233
+ lockfile.platforms.delete_if do |p1|
234
+ lockfile.platforms.any? do |p2|
235
+ p2 != "ruby" && p1 != p2 && MatchPlatform.platforms_match?(p2, p1)
236
+ end
237
+ end
238
+ lockfile.instance_variable_set(:@ruby_version, default_lockfile.ruby_version)
239
+ lockfile.instance_variable_set(:@bundler_version, default_lockfile.bundler_version)
240
+
241
+ new_contents = LockfileGenerator.generate(lockfile)
242
+ else
243
+ # no lockfile? just start out with the default lockfile's contents to inherit its
244
+ # locked gems
245
+ new_contents = adjusted_default_lockfile_contents
246
+ end
247
+
248
+ had_changes = false
249
+ # Now build a definition based on the given Gemfile, with the combined lockfile
250
+ Tempfile.create do |temp_lockfile|
251
+ temp_lockfile.write(new_contents)
252
+ temp_lockfile.flush
253
+
254
+ had_changes = write_lockfile(lockfile_definition,
255
+ temp_lockfile.path,
256
+ install: install,
257
+ dependency_changes: dependency_changes)
258
+ end
259
+
260
+ # if we had changes, bundler may have updated some common
261
+ # dependencies beyond the default lockfile, so re-run it
262
+ # once to reset them back to the default lockfile's version.
263
+ # if it's already good, the `check` check at the beginning of
264
+ # the loop will skip the second sync anyway.
265
+ if had_changes && attempts < 3
266
+ attempts += 1
267
+ redo
268
+ else
269
+ attempts = 1
270
+ end
271
+ end
272
+ end
273
+ end
274
+
275
+ exit 1 unless checker.run
276
+ ensure
277
+ @recursive = previous_recursive
278
+ end
279
+
280
+ # @!visibility private
281
+ def loaded!
282
+ return if loaded?
283
+
284
+ @loaded = true
285
+ return if lockfile_definitions.empty?
286
+ return unless lockfile_definitions.none? { |definition| definition[:current] }
287
+ # Gemfile.lock isn't explicitly specified, otherwise it would be current
288
+ return if lockfile_definitions.none? do |definition|
289
+ definition[:lockfile] == Bundler.default_lockfile(force_original: true)
290
+ end
291
+
292
+ raise GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}" if ENV["BUNDLE_LOCKFILE"]
293
+
294
+ raise GemfileEvalError, "No lockfiles marked as default"
295
+ end
296
+
297
+ # @!visibility private
298
+ def loaded?
299
+ @loaded
300
+ end
301
+
302
+ # @!visibility private
303
+ def inject_preamble
304
+ minor_version = Gem::Version.new(::Bundler::Multilock::VERSION).segments[0..1].join(".")
305
+ bundle_preamble1_match = %(plugin "bundler-multilock")
306
+ bundle_preamble1 = <<~RUBY
307
+ plugin "bundler-multilock", "~> #{minor_version}"
308
+ RUBY
309
+ bundle_preamble2 = <<~RUBY
310
+ return unless Plugin.installed?("bundler-multilock")
311
+
312
+ Plugin.send(:load_plugin, "bundler-multilock")
313
+ RUBY
314
+
315
+ gemfile = Bundler.default_gemfile.read
316
+
317
+ injection_point = 0
318
+ while gemfile.match?(/^(?:#|\n|source)/, injection_point)
319
+ if gemfile[injection_point] == "\n"
320
+ injection_point += 1
321
+ else
322
+ injection_point = gemfile.index("\n", injection_point)
323
+ injection_point += 1 if injection_point
324
+ injection_point ||= -1
325
+ end
326
+ end
327
+
328
+ modified = inject_specific_preamble(gemfile, injection_point, bundle_preamble2, add_newline: true)
329
+ modified = true if inject_specific_preamble(gemfile,
330
+ injection_point,
331
+ bundle_preamble1,
332
+ match: bundle_preamble1_match,
333
+ add_newline: false)
334
+
335
+ Bundler.default_gemfile.write(gemfile) if modified
336
+ end
337
+
338
+ # @!visibility private
339
+ def reset!
340
+ @lockfile_definitions = []
341
+ @loaded = false
342
+ end
343
+
344
+ private
345
+
346
+ def inject_specific_preamble(gemfile, injection_point, preamble, add_newline:, match: preamble)
347
+ return false if gemfile.include?(match)
348
+
349
+ add_newline = false unless gemfile[injection_point - 1] == "\n"
350
+
351
+ gemfile.insert(injection_point, "\n") if add_newline
352
+ gemfile.insert(injection_point, preamble)
353
+
354
+ true
355
+ end
356
+
357
+ def write_lockfile(lockfile_definition, lockfile, install:, dependency_changes: false)
358
+ self.prepare_block = lockfile_definition[:prepare]
359
+ definition = Definition.build(lockfile_definition[:gemfile], lockfile, false)
360
+ definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
361
+ if lockfile_definition[:lockfile].exist?
362
+ definition.instance_variable_set(:@lockfile_contents,
363
+ lockfile_definition[:lockfile].read)
364
+ end
365
+
366
+ resolved_remotely = false
367
+ begin
368
+ previous_ui_level = Bundler.ui.level
369
+ Bundler.ui.level = "warn"
370
+ begin
371
+ definition.resolve_with_cache!
372
+ rescue GemNotFound, SolveFailure
373
+ definition = Definition.build(lockfile_definition[:gemfile], lockfile, false)
374
+ definition.resolve_remotely!
375
+ resolved_remotely = true
376
+ end
377
+ definition.lock(lockfile_definition[:lockfile], true)
378
+ ensure
379
+ Bundler.ui.level = previous_ui_level
380
+ end
381
+
382
+ # if we're running `bundle install` or `bundle update`, and something is missing from
383
+ # the secondary lockfile, install it.
384
+ if install && (definition.missing_specs.any? || resolved_remotely)
385
+ Bundler.with_default_lockfile(lockfile_definition[:lockfile]) do
386
+ Installer.install(lockfile_definition[:gemfile].dirname, definition, {})
387
+ end
388
+ end
389
+
390
+ !definition.nothing_changed?
391
+ ensure
392
+ self.prepare_block = nil
393
+ end
394
+ end
395
+
396
+ reset!
397
+
398
+ @recursive = false
399
+ @prepare_block = nil
400
+ end
401
+ end
data/plugins.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (C) 2023 - present Instructure, Inc.
5
+ #
6
+ # This file is part of Canvas.
7
+ #
8
+ # Canvas is free software: you can redistribute it and/or modify it under
9
+ # the terms of the GNU Affero General Public License as published by the Free
10
+ # Software Foundation, version 3 of the License.
11
+ #
12
+ # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
13
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
+ # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
15
+ # details.
16
+ #
17
+ # You should have received a copy of the GNU Affero General Public License along
18
+ # with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require_relative "lib/bundler/multilock"
22
+
23
+ # this is terrible, but we can't prepend into these modules because we only load
24
+ # _inside_ of the CLI commands already running
25
+ if defined?(Bundler::CLI::Check)
26
+ require_relative "lib/bundler/multilock/check"
27
+ at_exit do
28
+ next unless $!.nil?
29
+ next if $!.is_a?(SystemExit) && !$!.success?
30
+
31
+ next if Bundler::Multilock::Check.run
32
+
33
+ Bundler.ui.warn("You can attempt to fix by running `bundle install`")
34
+ exit 1
35
+ end
36
+ end
37
+ if defined?(Bundler::CLI::Lock)
38
+ at_exit do
39
+ next unless $!.nil?
40
+ next if $!.is_a?(SystemExit) && !$!.success?
41
+
42
+ Bundler::Multilock.after_install_all(install: false)
43
+ end
44
+ end
45
+
46
+ Bundler::Plugin.add_hook(Bundler::Plugin::Events::GEM_AFTER_INSTALL_ALL) do |_|
47
+ Bundler::Multilock.after_install_all
48
+ end
49
+
50
+ Bundler::Multilock.inject_preamble unless Bundler::Multilock.loaded?
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bundler-multilock
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Instructure
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.4.19
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.4.19
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-inst
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.24'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.24'
111
+ description:
112
+ email:
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - lib/bundler/multilock.rb
118
+ - lib/bundler/multilock/check.rb
119
+ - lib/bundler/multilock/ext/bundler.rb
120
+ - lib/bundler/multilock/ext/definition.rb
121
+ - lib/bundler/multilock/ext/dsl.rb
122
+ - lib/bundler/multilock/ext/plugin.rb
123
+ - lib/bundler/multilock/ext/plugin/dsl.rb
124
+ - lib/bundler/multilock/ext/source_list.rb
125
+ - lib/bundler/multilock/lockfile_generator.rb
126
+ - lib/bundler/multilock/version.rb
127
+ - plugins.rb
128
+ homepage: https://github.com/instructure/bundler-multilock
129
+ licenses:
130
+ - MIT
131
+ metadata:
132
+ rubygems_mfa_required: 'true'
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '2.7'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.1.6
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: Support Multiple Lockfiles
152
+ test_files: []