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