bundler-multilock 1.4.0 → 2.0.0.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96d2ea242c1ed2649402ecdaef79a271a80e406158aed36ace8e1bcd54c499e2
4
- data.tar.gz: cf18464bb04949db98e4c18cff115a80fa287b1013befd2032c03cad6c171e59
3
+ metadata.gz: a2008728954b70bf6b491a20e480903c0f0fb827e05259a00b185b5cbfd8c262
4
+ data.tar.gz: fb1083e142229ddb68c753d1380a9d0be289f84221be72f2b7cc240920c9a1d7
5
5
  SHA512:
6
- metadata.gz: b2a454d5a6ce63f688233c70712d3dfec217c296b3a36c40081ae74f76bbf20fe6c347be91168d575dfe11df2ebb521b2e48d925464ddd02a18c7ce5e33095c6
7
- data.tar.gz: edd63eaf7fdaf1b1c3809a8adeed6022ab954cacfc8feee0d44a855fe11ce39a83f3aa8aed6779291dfc009a77608381551100436b73bfedb2bdd92606e47dd9
6
+ metadata.gz: 13bc99524329cc8364b84ca27b54e8424472475123534109f92b97847fa0a47b54bed7b10f46b20d85ebc138133b7739cf16322041662eae9f719a1bbb8c9b18
7
+ data.tar.gz: 39ecee3398e36805e27654c403c0dbe685992bb70deff5394e91436cf7c453bf398836a0b71b10a0240563f73da7dccb9fff4d9f1bc97763cd095fe7435fcaef
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  require_relative "cache"
6
4
 
7
5
  module Bundler
@@ -13,8 +11,12 @@ module Bundler
13
11
  end
14
12
  end
15
13
 
16
- def initialize(cache = Cache.new)
14
+ # @param check_installed [true, false]
15
+ # If gems must be installed (not just locked) for a lockfile to be valid.
16
+ # `bundle lock` doesn't install anything, so it shouldn't care.
17
+ def initialize(cache = Cache.new, check_installed: true)
17
18
  @cache = cache
19
+ @check_installed = check_installed
18
20
  end
19
21
 
20
22
  def run(skip_base_checks: false)
@@ -54,14 +56,28 @@ module Bundler
54
56
  # root needs to be set so that paths are output relative to the correct root in the lockfile
55
57
  Bundler.root = lockfile_definition[:gemfile].dirname
56
58
 
57
- definition = Definition.build(lockfile_definition[:gemfile], lockfile_name, false)
58
- next false unless definition.send(:current_platform_locked?)
59
+ lockfile_path = lockfile_name.relative_path_from(Dir.pwd)
60
+ gemfile_path = lockfile_definition[:gemfile].relative_path_from(Dir.pwd)
61
+
62
+ # (as the default lockfile, so that it isn't swapped for the active one)
63
+ definition = Bundler.with_default_lockfile(lockfile_name) do
64
+ Definition.build(lockfile_definition[:gemfile], lockfile_name, false)
65
+ end
66
+ unless definition.send(:current_platform_locked?)
67
+ next base_check_failed("#{lockfile_path} does not include the current platform " \
68
+ "(#{Bundler.local_platform}).")
69
+ end
70
+ # the lockfile's sources don't match the Gemfile's (like a different
71
+ # git remote); also, resolving would try to fetch the new source
72
+ if definition.instance_variable_get(:@source_changes)
73
+ next base_check_failed(source_changes_message(definition, lockfile_path, gemfile_path))
74
+ end
59
75
 
60
76
  begin
61
77
  definition.validate_runtime!
62
- not_installed = Bundler.ui.silence { definition.missing_specs }
63
- rescue RubyVersionMismatch, GemNotFound, SolveFailure
64
- next false
78
+ not_installed = @check_installed ? Bundler.ui.silence { definition.missing_specs } : []
79
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure, GitError => e
80
+ next base_check_failed("Could not check #{lockfile_path}: #{e.message}")
65
81
  end
66
82
 
67
83
  if Bundler.ui.error?
@@ -71,6 +87,19 @@ module Bundler
71
87
  end
72
88
 
73
89
  next false unless not_installed.empty?
90
+ # without checking installed gems, nothing above notices a dependency
91
+ # that was added to the Gemfile but isn't locked yet
92
+ unless @check_installed || definition.no_resolve_needed?
93
+ next base_check_failed("#{lockfile_path} is out of date, because #{definition.send(:change_reason)}.")
94
+ end
95
+
96
+ # (and even when checking installed gems, a dependency like a path gem
97
+ # or plugin can be "installed" without being locked)
98
+ unlocked = definition.dependencies.map(&:name) - definition.locked_gems.dependencies.keys
99
+ unless unlocked.empty?
100
+ next base_check_failed("#{lockfile_path} is missing dependencies from #{gemfile_path}: " \
101
+ "#{unlocked.join(", ")}.")
102
+ end
74
103
 
75
104
  # cache a sentinel so that we can share a cache regardless of the check_missing_deps argument
76
105
  next :missing_deps unless (definition.locked_gems.dependencies.values - definition.dependencies).empty?
@@ -86,12 +115,56 @@ module Bundler
86
115
  Bundler.root = default_root
87
116
  end
88
117
 
118
+ # @return [false]
119
+ def base_check_failed(message) # rubocop:disable Naming/PredicateMethod -- not a predicate
120
+ Bundler.ui.error(message)
121
+ false
122
+ end
123
+
124
+ def source_changes_message(definition, lockfile_path, gemfile_path)
125
+ locked = definition.locked_gems.sources
126
+ current = definition.send(:sources).lock_sources
127
+ only_locked = locked.reject { |source| current.include?(source) }
128
+ only_current = current.reject { |source| locked.include?(source) }
129
+
130
+ message = "The sources in #{lockfile_path} do not match #{gemfile_path}."
131
+ message << "\n Only in #{lockfile_path}: #{only_locked.join(", ")}" unless only_locked.empty?
132
+ message << "\n Only in #{gemfile_path}: #{only_current.join(", ")}" unless only_current.empty?
133
+ message
134
+ end
135
+
136
+ # checks that every gem has a checksum, and that gems in common (same version
137
+ # and source) with the parent lockfile have the same checksums
138
+ def checksums_valid?(parser, parent_parser, lockfile_path)
139
+ parent_specs = parent_parser.specs.to_h { |spec| [spec.full_name, spec] }
140
+ invalid = parser.specs.select do |spec|
141
+ next false unless spec.source.is_a?(Source::Rubygems)
142
+
143
+ checksums = spec.source.checksum_store.checksums_to_lock(spec.full_name)
144
+ unless checksums
145
+ Bundler.ui.error("#{lockfile_path} is missing a checksum for #{spec.lock_name}.")
146
+ next true
147
+ end
148
+
149
+ parent_spec = parent_specs[spec.full_name]
150
+ next false unless parent_spec && parent_spec.source == spec.source
151
+
152
+ parent_checksums = parent_spec.source.checksum_store.checksums_to_lock(spec.full_name)
153
+ next false if parent_checksums.nil? || checksums == parent_checksums
154
+
155
+ Bundler.ui.error("The checksum for #{spec.lock_name} in #{lockfile_path} " \
156
+ "does not match the parent lockfile's (#{parent_checksums}).")
157
+ true
158
+ end
159
+ invalid.empty?
160
+ end
161
+
89
162
  # this checks for mismatches between the parent lockfile and the given lockfile,
90
163
  # and for pinned dependencies in lockfiles requiring them
91
164
  def deep_check(lockfile_definition, conflicts: nil)
92
165
  lockfile_name = lockfile_definition[:lockfile]
93
166
  @cache.deep_check(lockfile_name) do
94
- success = true # -- work around rubocop bug
167
+ success = true
95
168
  proven_pinned = Set.new
96
169
  needs_pin_check = []
97
170
  parser = @cache.parser(lockfile_name)
@@ -100,18 +173,26 @@ module Bundler
100
173
  parent_parser = @cache.parser(parent_lockfile_name)
101
174
  unless parser.platforms == parent_parser.platforms
102
175
  Bundler.ui.error("The platforms in #{lockfile_path} do not match the parent lockfile.")
103
- success = false # -- work around rubocop bug
176
+ success = false
104
177
  end
105
178
  unless parser.bundler_version == parent_parser.bundler_version
106
179
  Bundler.ui.error("bundler (#{parser.bundler_version}) in #{lockfile_path} " \
107
180
  "does not match the parent lockfile's version (@#{parent_parser.bundler_version}).")
108
- success = false # -- work around rubocop bug
181
+ success = false
109
182
  end
110
183
  unless parser.ruby_version == parent_parser.ruby_version
111
184
  Bundler.ui.error("ruby (#{parser.ruby_version || "<none>"}) in #{lockfile_path} " \
112
185
  "does not match the parent lockfile's version (#{parent_parser.ruby_version}).")
113
186
  success = false
114
187
  end
188
+ unless !parser.checksums == !parent_parser.checksums
189
+ Bundler.ui.error("The parent lockfile #{parent_parser.checksums ? "has" : "does not have"} checksums, " \
190
+ "but #{lockfile_path} #{parser.checksums ? "does" : "does not"}.")
191
+ success = false
192
+ end
193
+ if parser.checksums && parent_parser.checksums && !checksums_valid?(parser, parent_parser, lockfile_path)
194
+ success = false
195
+ end
115
196
 
116
197
  # look through top-level explicit dependencies for pinned requirements
117
198
  if lockfile_definition[:enforce_pinned_additional_dependencies]
@@ -177,8 +258,10 @@ module Bundler
177
258
 
178
259
  next if pinned
179
260
 
180
- Bundler.ui.error("#{spec} in #{lockfile_path} has not been pinned to a specific version, " \
181
- "which is required since it is not part of the parent lockfile.")
261
+ message = "#{spec} in #{lockfile_path} has not been pinned to a specific version, " \
262
+ "which is required since it is not part of the parent lockfile."
263
+ message << " Be sure to set the ref to a commit hash for git sources." if spec.source.is_a?(Source::Git)
264
+ Bundler.ui.error(message)
182
265
  success = false
183
266
  end
184
267
 
@@ -19,7 +19,13 @@ module Bundler
19
19
  def default_lockfile(force_original: false)
20
20
  return @default_lockfile if @default_lockfile && !force_original
21
21
 
22
- super()
22
+ # Multilock handles BUNDLE_LOCKFILE itself
23
+ env_lockfile = ENV.delete("BUNDLE_LOCKFILE")
24
+ begin
25
+ super()
26
+ ensure
27
+ ENV["BUNDLE_LOCKFILE"] = env_lockfile if env_lockfile
28
+ end
23
29
  end
24
30
 
25
31
  def with_default_lockfile(lockfile)
@@ -4,12 +4,44 @@ module Bundler
4
4
  module Multilock
5
5
  module Ext
6
6
  module CLI
7
+ # What we need to know from the CLI instance, when we can't find it
8
+ Invocation = Struct.new(:current_command_chain, :options)
9
+ private_constant :Invocation
10
+
7
11
  module ClassMethods
8
12
  def instance
9
13
  return @instance if instance_variable_defined?(:@instance)
10
14
 
11
15
  # this is a little icky, but there's no other way to determine which command was run
12
- @instance = ObjectSpace.each_object(::Bundler::CLI).first
16
+ @instance = begin
17
+ ObjectSpace.each_object(::Bundler::CLI).first
18
+ rescue RuntimeError
19
+ # JRuby only supports ObjectSpace for classes and modules (unless
20
+ # it's run with -X+O), so parse the command line the same way Thor
21
+ # does instead
22
+ invocation_from_argv
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def invocation_from_argv
29
+ # `bundle exec` loads Ruby executables (like bundler itself) in the
30
+ # same process, with their own ARGV. ObjectSpace would find the
31
+ # original `exec` command then, so be consistent with that. (Bundler
32
+ # only loads CLI::Exec when running `bundle exec`.)
33
+ return Invocation.new([:exec], {}) if ::Bundler::CLI.const_defined?(:Exec, false)
34
+
35
+ args = ARGV.dup
36
+ name = normalize_command_name(retrieve_command_name(args))
37
+ command = all_commands[name]
38
+ return unless command
39
+
40
+ _arguments, options = Bundler::Thor::Options.split(args)
41
+ options = Bundler::Thor::Options.new(class_options.merge(command.options)).parse(options)
42
+ Invocation.new([name.to_sym], options)
43
+ rescue Bundler::Thor::Error
44
+ nil
13
45
  end
14
46
  end
15
47
 
@@ -16,6 +16,13 @@ module Bundler
16
16
  super
17
17
  end
18
18
 
19
+ # `bundle install` sets the lockfile from BUNDLE_LOCKFILE, which Multilock handles itself
20
+ def lockfile=(lockfile)
21
+ lockfile = Bundler.default_lockfile if lockfile && lockfile.to_s == ENV.fetch("BUNDLE_LOCKFILE", nil)
22
+
23
+ super
24
+ end
25
+
19
26
  def validate_runtime!
20
27
  Multilock.loaded! unless Multilock.lockfile_definitions.empty?
21
28
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module Bundler
6
4
  module Multilock
7
5
  module Ext
@@ -16,9 +14,21 @@ module Bundler
16
14
  # themselves
17
15
  # * mark Multilock as loaded once the main gemfile is evaluated
18
16
  # so that they're not loaded multiple times
17
+ # * ignore a lockfile that Bundler derived from BUNDLE_LOCKFILE
18
+ # before Multilock was loaded
19
+ # * when evaluating again after Multilock is loaded (`lockfile` no
20
+ # longer does anything then), still include the lockfile's block
19
21
  def evaluate(gemfile, lockfile, unlock)
22
+ if !Multilock.loaded? &&
23
+ (env_lockfile = ENV.fetch("BUNDLE_LOCKFILE", nil)) &&
24
+ lockfile.to_s == env_lockfile
25
+ lockfile = Bundler.default_lockfile(force_original: true)
26
+ end
27
+
20
28
  builder = new
21
- builder.eval_gemfile(gemfile, &Multilock.prepare_block) if Multilock.prepare_block
29
+ prepare_block = Multilock.prepare_block
30
+ prepare_block ||= Multilock.lockfile_definitions.dig(lockfile, :prepare) if Multilock.loaded?
31
+ builder.eval_gemfile(gemfile, &prepare_block) if prepare_block
22
32
  builder.eval_gemfile(gemfile)
23
33
  if (ruby_version_requirement = builder.instance_variable_get(:@ruby_version)) &&
24
34
  Multilock.lockfile_definitions[lockfile]
@@ -52,7 +62,7 @@ module Bundler
52
62
  if block
53
63
  instance_eval(&block)
54
64
  else
55
- instance_eval(contents.dup.tap { |x| x.untaint if RUBY_VERSION < "2.7" }, @gemfile.to_s, 1)
65
+ instance_eval(contents.dup, @gemfile.to_s, 1)
56
66
  end
57
67
  rescue Exception => e # rubocop:disable Lint/RescueException
58
68
  message = "There was an error " \
@@ -64,10 +74,35 @@ module Bundler
64
74
  @gemfile = original_gemfile
65
75
  end
66
76
 
67
- def lockfile(*args, **kwargs, &)
77
+ # Significant changes:
78
+ # * in a Gemfile we switched to (see #allow_duplicate_plugins_in), don't
79
+ # warn the first time a plugin that's already declared is declared again
80
+ def plugin(name, *args)
81
+ unless @duplicate_plugins_gemfile &&
82
+ @gemfile == @duplicate_plugins_gemfile &&
83
+ !@allowed_duplicate_plugins.include?(name) &&
84
+ @dependencies.any? { |dependency| dependency.name == name }
85
+ return super
86
+ end
87
+
88
+ @allowed_duplicate_plugins << name
89
+ # Bundler only warns about it (instead of raising) if the version
90
+ # requirements and the source match exactly
91
+ Bundler.ui.silence { super }
92
+ end
93
+
94
+ # @!visibility private
95
+ # Allows plugins to be declared again, once each, in the given Gemfile,
96
+ # without Bundler warning that they're listed more than once
97
+ def allow_duplicate_plugins_in(gemfile)
98
+ @duplicate_plugins_gemfile = Pathname.new(gemfile).expand_path
99
+ @allowed_duplicate_plugins = Set.new
100
+ end
101
+
102
+ def lockfile(*, **, &)
68
103
  return true if Multilock.loaded?
69
104
 
70
- Multilock.add_lockfile(*args, builder: self, **kwargs, &)
105
+ Multilock.add_lockfile(*, builder: self, **, &)
71
106
  end
72
107
  end
73
108
  end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Multilock
5
+ # @!visibility private
6
+ # Keeps the preamble that loads the plugin in the Gemfile up to date:
7
+ #
8
+ # plugin "bundler-multilock", "~> 2.0"
9
+ # return unless Plugin.loaded?("bundler-multilock")
10
+ module Preamble
11
+ class << self
12
+ # @return [String] the version requirement for the plugin in the preamble
13
+ def requirement
14
+ version = Gem::Version.new(VERSION)
15
+ # a prerelease doesn't satisfy `~> 2.0`, so allow later prereleases
16
+ # (and releases) of this minor version instead
17
+ return "~> #{version}" if version.prerelease?
18
+
19
+ "~> #{version.segments.first}.0"
20
+ end
21
+
22
+ # Adds the preamble to the Gemfile, or updates an existing one
23
+ def inject
24
+ Bundler.ui.debug("Injecting multilock preamble")
25
+
26
+ requirement = self.requirement
27
+ bundle_preamble1_match = /plugin\(?\s*["']bundler-multilock["']/
28
+ bundle_preamble1 = <<~RUBY
29
+ plugin "bundler-multilock", "#{requirement}"
30
+ RUBY
31
+ bundle_preamble2 = <<~RUBY
32
+ return unless Plugin.loaded?("bundler-multilock")
33
+ RUBY
34
+
35
+ builder = Bundler::Plugin::DSL.new
36
+ # this method is called as part of the plugin loading, but @loaded_plugin_names
37
+ # hasn't been set yet, so avoid re-entrancy issues
38
+ plugins = Bundler::Plugin.instance_variable_get(:@loaded_plugin_names)
39
+ original_plugins = plugins.dup
40
+ plugins << "bundler-multilock"
41
+ begin
42
+ builder.eval_gemfile(Bundler.default_gemfile)
43
+ ensure
44
+ plugins.replace(original_plugins)
45
+ end
46
+ gemfile_paths = builder.instance_variable_get(:@gemfiles).to_a
47
+ gemfiles = gemfile_paths.to_h { |path| [path, path.read] }
48
+ originals = gemfiles.transform_values(&:dup)
49
+
50
+ gemfiles.each_value { |contents| upgrade(contents, requirement) }
51
+
52
+ gemfile = gemfiles[Bundler.default_gemfile.expand_path] || Bundler.default_gemfile.read
53
+
54
+ # skip past leading comments, blank lines, and sources.
55
+ # (\G anchors at injection_point; ^ would match any later line)
56
+ injection_point = 0
57
+ while injection_point < gemfile.length && gemfile.match?(/\G(?:#|\n|source)/, injection_point)
58
+ if gemfile[injection_point] == "\n"
59
+ injection_point += 1
60
+ else
61
+ injection_point = (gemfile.index("\n", injection_point) || (gemfile.length - 1)) + 1
62
+ end
63
+ end
64
+ guard_match = Regexp.new(Regexp.escape(bundle_preamble2).gsub('"', %(["'])))
65
+ guard_present = gemfiles.each_value.any? { |contents| match_in_code?(contents, guard_match) }
66
+ # if the plugin is already declared, the guard has to go after it (in
67
+ # whichever Gemfile it's in), so that Bundler can still find the plugin
68
+ # while it's not loaded
69
+ guarded_declaration = !guard_present && gemfiles.each_value.any? do |contents|
70
+ insert_after_statement(contents, bundle_preamble1_match, bundle_preamble2)
71
+ end
72
+ unless guarded_declaration
73
+ # don't append to the end of the last line. (if we got to the end, the
74
+ # Gemfile is only comments and sources, so the preamble will be injected.)
75
+ if injection_point == gemfile.length && !gemfile.empty? && !gemfile.end_with?("\n")
76
+ gemfile << "\n"
77
+ injection_point += 1
78
+ end
79
+
80
+ inject_specific(gemfile, gemfiles.values, injection_point, bundle_preamble2, add_newline: true)
81
+ inject_specific(gemfile,
82
+ gemfiles.values,
83
+ injection_point,
84
+ bundle_preamble1,
85
+ match: bundle_preamble1_match,
86
+ add_newline: false)
87
+ end
88
+ gemfiles[Bundler.default_gemfile.expand_path] = gemfile
89
+
90
+ gemfiles.each do |path, contents|
91
+ path.write(contents) unless contents == originals[path]
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Bundler loads the plugin before evaluating the Gemfile, so manually
98
+ # loading it is no longer necessary. But Bundler also pre-parses the
99
+ # Gemfile for plugins _without_ loading them, so the rest of the Gemfile
100
+ # needs to be skipped when the plugin isn't loaded (not just installed).
101
+ # Also update the version requirement if it doesn't allow this version,
102
+ # or if it still references a prerelease after a release.
103
+ def upgrade(gemfile, requirement)
104
+ load_plugin = /^\n?[ \t]*Plugin\.send\(:load_plugin, (["'])bundler-multilock\1\)[ \t]*\n/
105
+ gsub_code!(gemfile, load_plugin, "Plugin") { "" }
106
+ # normalize the guard (with or without parentheses), so that it's
107
+ # recognized as already present
108
+ guard = /return\ unless\ Plugin\.(?:installed|loaded)\?
109
+ (?:\(\s*(["'])bundler-multilock\1\s*\) # parenthesized
110
+ |\s+(["'])bundler-multilock\2) # or not
111
+ /x
112
+ gsub_code!(gemfile, guard, "return") do |m|
113
+ quote = m[1] || m[2]
114
+ "return unless Plugin.loaded?(#{quote}bundler-multilock#{quote})"
115
+ end
116
+ # every string argument after the name is a version constraint, until
117
+ # options (like `source:`, or `"source" =>`) start. there may be
118
+ # newlines and comments between them.
119
+ constraint = /\s*,(?:\s|\#[^\n]*)*(["'])[^"'\n]*\k<-1>(?!\s*=>)/
120
+ gsub_code!(gemfile, /^([ \t]*plugin\(?\s*(["'])bundler-multilock\2)((?:#{constraint})+)/, "plugin") do |m|
121
+ match = m[0]
122
+ prefix, constraints = m[1], m[3].gsub(/\#[^\n]*/, "")
123
+ quote = constraints[/["']/]
124
+ existing_requirement = Gem::Requirement.new(*constraints.scan(/(["'])([^"'\n]*)\1/).map(&:last))
125
+ version = Gem::Version.new(VERSION)
126
+ if existing_requirement.satisfied_by?(version) && (version.prerelease? || !existing_requirement.prerelease?)
127
+ next match
128
+ end
129
+
130
+ "#{prefix}, #{quote}#{requirement}#{quote}"
131
+ rescue Gem::Requirement::BadRequirementError
132
+ match
133
+ end
134
+ end
135
+
136
+ # Like String#gsub!, but skips matches whose `keyword` is inside a string,
137
+ # heredoc, or comment, instead of actual Ruby code
138
+ #
139
+ # @yieldparam match [MatchData]
140
+ def gsub_code!(source, pattern, keyword)
141
+ non_code = non_code_ranges(source)
142
+ result = +""
143
+ copied_until = 0
144
+ replaced = false
145
+ each_match(source, pattern) do |match|
146
+ offset = match.pre_match.bytesize + match[0].b.index(keyword)
147
+ next if non_code.any? { |range| range.cover?(offset) }
148
+
149
+ result << source[copied_until...match.begin(0)] << yield(match)
150
+ copied_until = match.end(0)
151
+ replaced = true
152
+ end
153
+ return unless replaced
154
+
155
+ source.replace(result << source[copied_until..])
156
+ end
157
+
158
+ # Yields each match of pattern in source
159
+ #
160
+ # @yieldparam match [MatchData]
161
+ def each_match(source, pattern)
162
+ position = 0
163
+ while (match = pattern.match(source, position))
164
+ yield match
165
+ # (always move forward, even after an empty match)
166
+ position = [match.end(0), match.begin(0) + 1].max
167
+ end
168
+ end
169
+
170
+ # @return [true, false] if pattern matches somewhere other than in a string, heredoc, or comment
171
+ def match_in_code?(source, pattern)
172
+ !code_match_offsets(source, pattern).empty?
173
+ end
174
+
175
+ # @return [Array<Integer>] the byte offsets of matches that aren't in a string, heredoc, or comment
176
+ def code_match_offsets(source, pattern)
177
+ non_code = non_code_ranges(source)
178
+ offsets = []
179
+ each_match(source, pattern) do |match|
180
+ offset = match.pre_match.bytesize
181
+ offsets << offset if non_code.none? { |range| range.cover?(offset) }
182
+ end
183
+ offsets
184
+ end
185
+
186
+ # Inserts text on the line after the first statement (in code) matching pattern
187
+ #
188
+ # @return [true, false] if there was such a statement
189
+ def insert_after_statement(source, pattern, text) # rubocop:disable Naming/PredicateMethod -- not a predicate
190
+ offset = code_match_offsets(source, pattern).first
191
+ return false unless offset
192
+
193
+ insertion = statement_end(source, offset)
194
+ text = "\n#{text}" if insertion == source.bytesize && !source.end_with?("\n")
195
+ source.insert(source.byteslice(0, insertion).length, text)
196
+ true
197
+ end
198
+
199
+ # @return [Integer] the byte offset of the start of the line after the
200
+ # statement at offset
201
+ def statement_end(source, offset)
202
+ require "ripper"
203
+
204
+ line_offsets = [0]
205
+ source.each_line { |line| line_offsets << (line_offsets.last + line.bytesize) }
206
+
207
+ depth = 0
208
+ Ripper.lex(source).each do |(line, column), type, _token, state|
209
+ start = line_offsets[line - 1] + column
210
+ next if start < offset
211
+
212
+ case type
213
+ when :on_lparen, :on_lbracket, :on_lbrace then depth += 1
214
+ when :on_rparen, :on_rbracket, :on_rbrace then depth -= 1
215
+ when :on_nl, :on_semicolon
216
+ return line_end(source, start) if depth.zero?
217
+ when :on_comment
218
+ # a comment after a complete expression ends the statement
219
+ return line_end(source, start) if depth.zero? && state.anybits?(Ripper::EXPR_END_ANY)
220
+ end
221
+ end
222
+ source.bytesize
223
+ end
224
+
225
+ def line_end(source, offset)
226
+ (source.byteindex("\n", offset) || (source.bytesize - 1)) + 1
227
+ end
228
+
229
+ # @return [Array<Range>] the byte ranges of strings, heredocs, and comments in Ruby source
230
+ def non_code_ranges(source)
231
+ require "ripper"
232
+
233
+ line_offsets = [0]
234
+ source.each_line { |line| line_offsets << (line_offsets.last + line.bytesize) }
235
+
236
+ Ripper.lex(source).filter_map do |(line, column), type, token|
237
+ start = line_offsets[line - 1] + column
238
+ case type
239
+ when :on_tstring_content, :on_comment, :on_embdoc then start...(start + token.bytesize)
240
+ when :on___end__ then start...source.bytesize
241
+ end
242
+ end
243
+ end
244
+
245
+ def inject_specific(gemfile, gemfiles, injection_point, preamble, add_newline:, match: nil) # rubocop:disable Naming/PredicateMethod -- not a predicate
246
+ # allow either type of quotes
247
+ match ||= Regexp.new(Regexp.escape(preamble).gsub('"', %(["'])))
248
+ return false if gemfiles.any? { |g| match_in_code?(g, match) }
249
+
250
+ add_newline = false unless gemfile[injection_point - 1] == "\n"
251
+
252
+ gemfile.insert(injection_point, "\n") if add_newline
253
+ gemfile.insert(injection_point, preamble)
254
+
255
+ true
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bundler
4
4
  module Multilock
5
- VERSION = "1.4.0"
5
+ VERSION = "2.0.0.beta1"
6
6
  end
7
7
  end
@@ -3,11 +3,11 @@
3
3
  require_relative "multilock/ext/bundler"
4
4
  require_relative "multilock/ext/definition"
5
5
  require_relative "multilock/ext/dsl"
6
- require_relative "multilock/ext/plugin"
7
6
  require_relative "multilock/ext/plugin/dsl"
8
7
  require_relative "multilock/ext/shared_helpers"
9
8
  require_relative "multilock/ext/source"
10
9
  require_relative "multilock/ext/source_list"
10
+ require_relative "multilock/preamble"
11
11
  require_relative "multilock/version"
12
12
 
13
13
  module Bundler
@@ -33,22 +33,13 @@ module Bundler
33
33
  # @yield
34
34
  # Block executed only when this lockfile is active.
35
35
  # @return [true, false] if the lockfile is the active lockfile
36
- def add_lockfile(lockfile = nil,
36
+ def add_lockfile(lockfile = nil, # rubocop:disable Naming/PredicateMethod -- not a predicate
37
37
  builder:,
38
38
  gemfile: nil,
39
39
  active: nil,
40
- default: nil,
41
40
  parent: nil,
42
- allow_mismatched_dependencies: nil,
43
41
  enforce_pinned_additional_dependencies: false,
44
42
  &block)
45
- # backcompat
46
- active = default if active.nil?
47
- Bundler.ui.warn("lockfile(default:) is deprecated. Use lockfile(active:) instead.") if default
48
- unless allow_mismatched_dependencies.nil?
49
- Bundler.ui.warn("lockfile(allow_mismatched_dependencies:) is deprecated.")
50
- end
51
-
52
43
  active = true if active.nil? && lockfile_definitions.empty? && lockfile.nil? && gemfile.nil?
53
44
 
54
45
  # if a gemfile was provided, but not a lockfile, infer the default lockfile for that gemfile
@@ -58,9 +49,11 @@ module Bundler
58
49
 
59
50
  raise ArgumentError, "Lockfile #{lockfile} is already defined" if lockfile_definitions.key?(lockfile)
60
51
 
61
- env_lockfile = lockfile if active && ENV["BUNDLE_LOCKFILE"] == "active"
62
- env_lockfile ||= ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) }
63
- active = env_lockfile == lockfile if env_lockfile
52
+ env_lockfile_names = self.env_lockfile_names
53
+ unless env_lockfile_names.empty?
54
+ active = (active && env_lockfile_names.include?("active")) ||
55
+ env_lockfile_names.any? { |name| expand_lockfile(name) == lockfile }
56
+ end
64
57
 
65
58
  if active && (old_active = lockfile_definitions.each_value.find { |definition| definition[:active] })
66
59
  raise ArgumentError, "Only one lockfile (#{old_active[:lockfile]}) can be flagged as active"
@@ -85,8 +78,8 @@ module Bundler
85
78
  # If they're using BUNDLE_LOCKFILE, then they really do want to
86
79
  # use a particular lockfile, and it overrides whatever they
87
80
  # dynamically set in their gemfile
88
- if !env_lockfile && defined?(CLI) &&
89
- %i[check install lock update].include?(CLI.instance&.current_command_chain&.first)
81
+ if env_lockfile_names.empty? &&
82
+ %i[check install lock update].include?(cli&.current_command_chain&.first)
90
83
  # always use Gemfile.lock for `bundle check`, `bundle install`,
91
84
  # `bundle lock`, and `bundle update`.
92
85
  active = lockfile == Bundler.default_lockfile(force_original: true)
@@ -105,6 +98,8 @@ module Bundler
105
98
  Bundler.root = Bundler.default_gemfile.dirname
106
99
  Bundler.default_lockfile = lockfile
107
100
 
101
+ # that Gemfile should declare its plugins too, but they may already be declared
102
+ builder.allow_duplicate_plugins_in(Bundler.default_gemfile)
108
103
  builder.eval_gemfile(Bundler.default_gemfile)
109
104
 
110
105
  return false
@@ -119,7 +114,7 @@ module Bundler
119
114
  previous_recursive = @recursive
120
115
 
121
116
  return if lockfile_definitions.empty?
122
- return if ENV["BUNDLE_LOCKFILE"] # explicitly working against a single lockfile
117
+ return if env_lockfile # explicitly working against a single lockfile
123
118
 
124
119
  # must be running `bundle cache`
125
120
  return unless Bundler.default_lockfile == Bundler.default_lockfile(force_original: true)
@@ -128,7 +123,8 @@ module Bundler
128
123
 
129
124
  if Bundler.frozen_bundle? && !install
130
125
  # only do the checks if we're frozen
131
- exit 1 unless Check.run
126
+ # `bundle lock` doesn't install anything
127
+ exit 1 unless Check.new(check_installed: false).run
132
128
  return
133
129
  end
134
130
 
@@ -149,7 +145,7 @@ module Bundler
149
145
  default_root = Bundler.root
150
146
 
151
147
  cache = Cache.new
152
- checker = Check.new(cache)
148
+ checker = Check.new(cache, check_installed: install)
153
149
  synced_any = false
154
150
  local_parser_cache = {}
155
151
  Bundler.settings.temporary(cache_all_platforms: true, suppress_install_using_messages: true) do
@@ -279,12 +275,6 @@ module Bundler
279
275
  end
280
276
 
281
277
  lockfile.platforms.replace(parent_lockfile.platforms).uniq!
282
- # prune more specific platforms
283
- lockfile.platforms.delete_if do |p1|
284
- lockfile.platforms.any? do |p2|
285
- p2 != "ruby" && p1 != p2 && MatchPlatform.platforms_match?(p2, p1)
286
- end
287
- end
288
278
  lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version) if lockfile.ruby_version
289
279
  unless lockfile.bundler_version == parent_lockfile.bundler_version
290
280
  unlocking_bundler = parent_lockfile.bundler_version
@@ -342,16 +332,21 @@ module Bundler
342
332
  return if loaded?
343
333
 
344
334
  @loaded = true
345
- return if lockfile_definitions.empty?
335
+ if lockfile_definitions.empty?
336
+ # nothing for Multilock to manage; use BUNDLE_LOCKFILE as a plain path, like Bundler would
337
+ if env_lockfile && !env_lockfile_names.include?("active")
338
+ Bundler.default_lockfile = Pathname.new(File.expand_path(env_lockfile))
339
+ end
340
+ return
341
+ end
346
342
 
347
343
  return unless lockfile_definitions.each_value.none? { |definition| definition[:active] }
348
344
 
349
- if ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) } ==
350
- Bundler.default_lockfile(force_original: true)
345
+ if env_lockfile_names.any? { |name| expand_lockfile(name) == Bundler.default_lockfile(force_original: true) }
351
346
  return
352
347
  end
353
348
 
354
- raise GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}" if ENV["BUNDLE_LOCKFILE"]
349
+ raise GemfileNotFound, "Could not locate lockfile #{env_lockfile.inspect}" if env_lockfile
355
350
 
356
351
  # Gemfile.lock isn't explicitly specified, otherwise it would be active
357
352
  default_lockfile_definition = self.default_lockfile_definition
@@ -366,55 +361,41 @@ module Bundler
366
361
  end
367
362
 
368
363
  # @!visibility private
369
- def inject_preamble
370
- Bundler.ui.debug("Injecting multilock preamble")
371
-
372
- minor_version = Gem::Version.new(::Bundler::Multilock::VERSION).segments[0..1].join(".")
373
- bundle_preamble1_match = /plugin ["']bundler-multilock["']/
374
- bundle_preamble1 = <<~RUBY
375
- plugin "bundler-multilock", "~> #{minor_version}"
376
- RUBY
377
- bundle_preamble2 = <<~RUBY
378
- return unless Plugin.installed?("bundler-multilock")
379
-
380
- Plugin.send(:load_plugin, "bundler-multilock")
381
- RUBY
382
-
383
- gemfile = Bundler.default_gemfile.read
384
-
385
- injection_point = 0
386
- while gemfile.match?(/^(?:#|\n|source)/, injection_point)
387
- if gemfile[injection_point] == "\n"
388
- injection_point += 1
389
- else
390
- injection_point = gemfile.index("\n", injection_point)
391
- injection_point += 1 if injection_point
392
- injection_point ||= -1
393
- end
394
- end
364
+ # The BUNDLE_LOCKFILE the user asked for, if any, as close as possible to
365
+ # how they wrote it
366
+ def env_lockfile
367
+ env_lockfile_and_recovered&.first
368
+ end
395
369
 
396
- builder = Bundler::Plugin::DSL.new
397
- # this method is called as part of the plugin loading, but @loaded_plugin_names
398
- # hasn't been set yet, so avoid re-entrancy issues
399
- plugins = Bundler::Plugin.instance_variable_get(:@loaded_plugin_names)
400
- original_plugins = plugins.dup
401
- plugins << "bundler-multilock"
402
- begin
403
- builder.eval_gemfile(Bundler.default_gemfile)
404
- ensure
405
- plugins.replace(original_plugins)
406
- end
407
- gemfiles = builder.instance_variable_get(:@gemfiles).map(&:read)
370
+ # @!visibility private
371
+ # What BUNDLE_LOCKFILE could have been written as; usually just one
372
+ # thing (see #env_lockfile_and_recovered)
373
+ #
374
+ # @return [Array<String>]
375
+ def env_lockfile_names
376
+ lockfile, recovered = env_lockfile_and_recovered
377
+ return [] unless lockfile
378
+ return [lockfile] if recovered
379
+
380
+ # we can't tell if Bundler expanded a short name (or "active") to an
381
+ # absolute path, or if it was written that way, so allow either
382
+ relative = Pathname.new(lockfile).relative_path_from(Pathname.pwd).to_s
383
+ relative.include?("/") ? [lockfile] : [lockfile, relative]
384
+ end
408
385
 
409
- modified = inject_specific_preamble(gemfile, gemfiles, injection_point, bundle_preamble2, add_newline: true)
410
- modified = true if inject_specific_preamble(gemfile,
411
- gemfiles,
412
- injection_point,
413
- bundle_preamble1,
414
- match: bundle_preamble1_match,
415
- add_newline: false)
386
+ # @!visibility private
387
+ # @return [Bundler::CLI, nil] the bundler command that's running, if any
388
+ def cli
389
+ # Bundler autoloads its CLI, so it's only running if it's already loaded
390
+ # (and checking that the constant is defined doesn't tell us that)
391
+ return unless cli_loaded? && CLI.respond_to?(:instance)
416
392
 
417
- Bundler.default_gemfile.write(gemfile) if modified
393
+ CLI.instance
394
+ end
395
+
396
+ # @!visibility private
397
+ def cli_loaded?
398
+ Bundler.autoload?(:CLI).nil? && !defined?(Bundler::CLI).nil?
418
399
  end
419
400
 
420
401
  # @!visibility private
@@ -430,6 +411,30 @@ module Bundler
430
411
 
431
412
  private
432
413
 
414
+ # @return [Array(String, true or false), nil]
415
+ # BUNDLE_LOCKFILE, and if it's known to be exactly how the user wrote it
416
+ def env_lockfile_and_recovered
417
+ lockfile = ENV.fetch("BUNDLE_LOCKFILE", nil)
418
+ return if lockfile.nil? || lockfile.empty?
419
+ # not expanded by Bundler (yet)
420
+ return [lockfile, true] unless Pathname.new(lockfile).absolute?
421
+
422
+ # Bundler expands BUNDLE_LOCKFILE to an absolute path, relative to the
423
+ # current directory, but remembers the original value. That original
424
+ # is inherited by nested bundler commands though, so make sure it's
425
+ # actually the one that was expanded.
426
+ original = ENV.fetch("#{EnvironmentPreserver::BUNDLER_PREFIX}BUNDLE_LOCKFILE", nil)
427
+ if original == EnvironmentPreserver::INTENTIONALLY_NIL
428
+ # Bundler sets BUNDLE_LOCKFILE to the default lockfile for subprocesses
429
+ # when nobody asked for a particular lockfile
430
+ return if lockfile == Bundler.default_lockfile(force_original: true).to_s
431
+ elsif original && File.expand_path(original) == lockfile
432
+ return [original, true]
433
+ end
434
+
435
+ [lockfile, false]
436
+ end
437
+
433
438
  def expand_lockfile(lockfile)
434
439
  if lockfile.is_a?(String) && !(lockfile.include?("/") || lockfile.end_with?(".lock"))
435
440
  lockfile = "Gemfile.#{lockfile}.lock"
@@ -440,17 +445,49 @@ module Bundler
440
445
  lockfile || Bundler.default_lockfile(force_original: true)
441
446
  end
442
447
 
443
- def inject_specific_preamble(gemfile, gemfiles, injection_point, preamble, add_newline:, match: nil)
444
- # allow either type of quotes
445
- match ||= Regexp.new(Regexp.escape(preamble).gsub('"', %(["'])))
446
- return false if gemfiles.any? { |g| match.match?(g) }
448
+ # Gems resolved from what's installed locally don't have checksums. Copy
449
+ # them from the parent lockfile for gems in common (so that they match),
450
+ # or else compute them from cached gems (without fetching anything).
451
+ #
452
+ # @return [Array] specs that still don't have checksums
453
+ def fill_checksums(definition, parent_lockfile, cache)
454
+ return [] if Bundler.frozen_bundle? || !definition.locked_checksums
455
+
456
+ require "rubygems/package"
457
+
458
+ parent_specs = cache.parser(parent_lockfile).specs.to_h { |spec| [spec.full_name, spec] }
459
+ definition.resolve.select do |spec|
460
+ next false unless spec.source.is_a?(Source::Rubygems)
461
+
462
+ store = spec.source.checksum_store
463
+ if (parent_spec = parent_specs[spec.full_name]) &&
464
+ parent_spec.source == spec.source &&
465
+ (checksums = parent_spec.source.checksum_store.checksums_to_lock(spec.full_name))
466
+ checksums.split(",").each do |checksum|
467
+ store.replace(spec, Checksum.from_lock(checksum, parent_lockfile.to_s))
468
+ end
469
+ end
470
+ next false unless store.missing?(spec) || store.empty?(spec)
447
471
 
448
- add_newline = false unless gemfile[injection_point - 1] == "\n"
472
+ cached_gem = spec.source.cached_built_in_gem(spec, local: true)
473
+ store.register(spec, Checksum.from_gem_package(Gem::Package.new(cached_gem))) if cached_gem
474
+ store.missing?(spec) || store.empty?(spec)
475
+ end
476
+ end
449
477
 
450
- gemfile.insert(injection_point, "\n") if add_newline
451
- gemfile.insert(injection_point, preamble)
478
+ # Resolves with only local gems. Bundler warns that scoped sources don't
479
+ # implement a dependency API when they're restricted to local gems, but
480
+ # that's expected here; if this fails, we resolve remotely instead.
481
+ def resolve_locally(definition)
482
+ definition.define_singleton_method(:non_dependency_api_warning) { nil }
483
+ definition.resolve_with_cache!
484
+ ensure
485
+ definition.singleton_class.remove_method(:non_dependency_api_warning)
486
+ end
452
487
 
453
- true
488
+ # @return [true, false] if running with `--local`, so nothing should be fetched
489
+ def local_only?
490
+ cli&.options&.[]("local") ? true : false
454
491
  end
455
492
 
456
493
  def write_lockfile(lockfile_definition,
@@ -477,6 +514,16 @@ module Bundler
477
514
  definition = builder.to_definition(lockfile, { bundler: unlocking_bundler })
478
515
  definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
479
516
 
517
+ # match the parent lockfile in whether there are checksums
518
+ unless Bundler.frozen_bundle?
519
+ parent_checksums = cache.parser(lockfile_definition[:parent]).checksums
520
+ if parent_checksums && !definition.locked_checksums
521
+ definition.add_checksums
522
+ elsif !parent_checksums && definition.locked_checksums
523
+ definition.instance_variable_set(:@locked_checksums, false)
524
+ end
525
+ end
526
+
480
527
  current_lockfile = lockfile_definition[:lockfile]
481
528
  definition.instance_variable_set(:@lockfile_contents, current_lockfile.read) if current_lockfile.exist?
482
529
 
@@ -499,7 +546,7 @@ module Bundler
499
546
  # if something has changed, we skip this step; it's unlocking anyway
500
547
  next unless current_definition.no_resolve_needed?
501
548
 
502
- current_definition.resolve_with_cache!
549
+ resolve_locally(current_definition)
503
550
  if current_definition.missing_specs.any?
504
551
  cache.invalidate_checks(current_lockfile)
505
552
  Bundler.with_default_lockfile(current_lockfile) do
@@ -532,16 +579,28 @@ module Bundler
532
579
  # in the lockfile (that we know are there from a prior remote
533
580
  # resolution), and add them to the locally installed spec list.
534
581
  definition.send(:source_map).locked_specs.each do |spec|
535
- next if spec.match_platform(Bundler.local_platform)
582
+ next if spec.installable_on_platform?(Bundler.local_platform)
536
583
 
537
584
  spec.source.specs << spec
538
585
  end
539
- definition.resolve_with_cache!
586
+ resolve_locally(definition)
587
+ missing_checksums = fill_checksums(definition, lockfile_definition[:parent], cache)
588
+ # fetch checksums that aren't available locally
589
+ raise SolveFailure unless missing_checksums.empty? || local_only?
540
590
  rescue GemNotFound, SolveFailure
591
+ raise if local_only?
592
+
541
593
  definition = orig_definition
542
594
 
543
595
  definition.resolve_remotely!
544
596
  resolved_remotely = true
597
+ missing_checksums = fill_checksums(definition, lockfile_definition[:parent], cache)
598
+ end
599
+ if local_only? && !missing_checksums.empty?
600
+ raise GemNotFound,
601
+ "Could not find checksums for #{missing_checksums.map(&:full_name).join(", ")} " \
602
+ "(for #{lockfile_definition[:lockfile].relative_path_from(Dir.pwd)}) locally. " \
603
+ "Run without `--local` to fetch them."
545
604
  end
546
605
  SharedHelpers.capture_filesystem_access do
547
606
  definition.instance_variable_set(:@resolved_bundler_version, unlocking_bundler) if unlocking_bundler
@@ -549,12 +608,9 @@ module Bundler
549
608
  # need to force it to _not_ preserve unknown sections, so that it
550
609
  # will overwrite the ruby version
551
610
  definition.instance_variable_set(:@unlocking_bundler, true)
552
- if Bundler.gem_version >= Gem::Version.new("2.5.6")
553
- definition.instance_variable_set(:@lockfile, lockfile_definition[:lockfile])
554
- definition.lock
555
- else
556
- definition.lock(lockfile_definition[:lockfile])
557
- end
611
+ # not `lockfile=`, which may substitute the lockfile from BUNDLE_LOCKFILE
612
+ definition.instance_variable_set(:@lockfile, lockfile_definition[:lockfile])
613
+ definition.lock
558
614
  end
559
615
  ensure
560
616
  Bundler.ui.level = previous_ui_level
@@ -579,11 +635,9 @@ module Bundler
579
635
  end
580
636
  end
581
637
 
582
- # see https://github.com/rubygems/rubygems/pull/7368
583
- Bundler::LazySpecification.include(Bundler::MatchMetadata) if defined?(Bundler::MatchMetadata)
584
- Bundler::Multilock.inject_preamble unless Bundler::Multilock.loaded?
638
+ Bundler::Multilock::Preamble.inject unless Bundler::Multilock.loaded?
585
639
 
586
- if defined?(Bundler::CLI)
640
+ if Bundler::Multilock.cli_loaded?
587
641
  require_relative "multilock/ext/cli"
588
642
 
589
643
  # this is terrible, but we can't prepend into these modules because we only load
@@ -605,7 +659,13 @@ if defined?(Bundler::CLI)
605
659
  next unless $!.nil?
606
660
  next if $!.is_a?(SystemExit) && !$!.success?
607
661
 
608
- Bundler::Multilock.after_install_all(install: false)
662
+ begin
663
+ Bundler::Multilock.after_install_all(install: false)
664
+ rescue Bundler::BundlerError => e
665
+ # this is outside of Bundler's usual error handling
666
+ Bundler.ui.error(e.message)
667
+ exit e.status_code
668
+ end
609
669
  end
610
670
  end
611
671
  end
data/plugins.rb CHANGED
@@ -20,6 +20,12 @@
20
20
 
21
21
  require_relative "lib/bundler/multilock"
22
22
 
23
+ # Registering for this event makes Bundler load the plugin before it evaluates
24
+ # the Gemfile, so `lockfile` is available without loading the plugin manually.
25
+ Bundler::Plugin.add_hook(Bundler::Plugin::Events::GEM_BEFORE_EVAL) do |_gemfile, _lockfile|
26
+ # nothing to do; loading the plugin is all that's needed
27
+ end
28
+
23
29
  Bundler::Plugin.add_hook(Bundler::Plugin::Events::GEM_AFTER_INSTALL_ALL) do |_|
24
30
  Bundler::Multilock.after_install_all
25
31
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-multilock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 2.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-04-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -16,22 +15,20 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 2.4.19
18
+ version: 4.1.0.beta2
20
19
  - - "<"
21
20
  - !ruby/object:Gem::Version
22
- version: '2.7'
21
+ version: '5'
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- version: 2.4.19
28
+ version: 4.1.0.beta2
30
29
  - - "<"
31
30
  - !ruby/object:Gem::Version
32
- version: '2.7'
33
- description:
34
- email:
31
+ version: '5'
35
32
  executables: []
36
33
  extensions: []
37
34
  extra_rdoc_files: []
@@ -43,12 +40,12 @@ files:
43
40
  - lib/bundler/multilock/ext/cli.rb
44
41
  - lib/bundler/multilock/ext/definition.rb
45
42
  - lib/bundler/multilock/ext/dsl.rb
46
- - lib/bundler/multilock/ext/plugin.rb
47
43
  - lib/bundler/multilock/ext/plugin/dsl.rb
48
44
  - lib/bundler/multilock/ext/shared_helpers.rb
49
45
  - lib/bundler/multilock/ext/source.rb
50
46
  - lib/bundler/multilock/ext/source_list.rb
51
47
  - lib/bundler/multilock/lockfile_generator.rb
48
+ - lib/bundler/multilock/preamble.rb
52
49
  - lib/bundler/multilock/ui/capture.rb
53
50
  - lib/bundler/multilock/version.rb
54
51
  - plugins.rb
@@ -57,7 +54,6 @@ licenses:
57
54
  - MIT
58
55
  metadata:
59
56
  rubygems_mfa_required: 'true'
60
- post_install_message:
61
57
  rdoc_options: []
62
58
  require_paths:
63
59
  - lib
@@ -65,15 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
61
  requirements:
66
62
  - - ">="
67
63
  - !ruby/object:Gem::Version
68
- version: '3.1'
64
+ version: '3.2'
69
65
  required_rubygems_version: !ruby/object:Gem::Requirement
70
66
  requirements:
71
67
  - - ">="
72
68
  - !ruby/object:Gem::Version
73
69
  version: '0'
74
70
  requirements: []
75
- rubygems_version: 3.5.8
76
- signing_key:
71
+ rubygems_version: 4.0.22
77
72
  specification_version: 4
78
73
  summary: Support Multiple Lockfiles
79
74
  test_files: []
@@ -1,19 +0,0 @@
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