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 +4 -4
- data/lib/bundler/multilock/check.rb +96 -13
- data/lib/bundler/multilock/ext/bundler.rb +7 -1
- data/lib/bundler/multilock/ext/cli.rb +33 -1
- data/lib/bundler/multilock/ext/definition.rb +7 -0
- data/lib/bundler/multilock/ext/dsl.rb +41 -6
- data/lib/bundler/multilock/preamble.rb +260 -0
- data/lib/bundler/multilock/version.rb +1 -1
- data/lib/bundler/multilock.rb +157 -97
- data/plugins.rb +6 -0
- metadata +9 -14
- data/lib/bundler/multilock/ext/plugin.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2008728954b70bf6b491a20e480903c0f0fb827e05259a00b185b5cbfd8c262
|
|
4
|
+
data.tar.gz: fb1083e142229ddb68c753d1380a9d0be289f84221be72f2b7cc240920c9a1d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
data/lib/bundler/multilock.rb
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
89
|
-
%i[check install lock update].include?(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 #{
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
582
|
+
next if spec.installable_on_platform?(Bundler.local_platform)
|
|
536
583
|
|
|
537
584
|
spec.source.specs << spec
|
|
538
585
|
end
|
|
539
|
-
definition
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
18
|
+
version: 4.1.0.beta2
|
|
20
19
|
- - "<"
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: '
|
|
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:
|
|
28
|
+
version: 4.1.0.beta2
|
|
30
29
|
- - "<"
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
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.
|
|
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:
|
|
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
|