bundler-multilock 1.0.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 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: []