Skip to content

Commit a89e1cb

Browse files
authored
Merge pull request #6957 from ccutrer/plugin-install
Fix plugin installation from gemfile
2 parents 6bcc9e2 + f868e59 commit a89e1cb

17 files changed

Lines changed: 437 additions & 81 deletions

File tree

Manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ bundler/lib/bundler/plugin/installer/git.rb
168168
bundler/lib/bundler/plugin/installer/path.rb
169169
bundler/lib/bundler/plugin/installer/rubygems.rb
170170
bundler/lib/bundler/plugin/source_list.rb
171+
bundler/lib/bundler/plugin/unloaded_source.rb
171172
bundler/lib/bundler/process_lock.rb
172173
bundler/lib/bundler/remote_specification.rb
173174
bundler/lib/bundler/resolver.rb

bundler/lib/bundler/cli/install.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def run
3838

3939
Bundler::Fetcher.disable_endpoint = options["full-index"]
4040

41-
Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
41+
Plugin.gemfile_install(Bundler.default_gemfile, Bundler.default_lockfile) if Bundler.settings[:plugins]
4242

4343
# For install we want to enable strict validation
4444
# (rather than some optimizations we perform at app runtime).

bundler/lib/bundler/cli/update.rb

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ def run
1515

1616
Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler
1717

18-
Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
19-
2018
sources = Array(options[:source])
2119
groups = Array(options[:group]).map(&:to_sym)
2220

@@ -33,29 +31,39 @@ def run
3331

3432
conservative = options[:conservative]
3533

36-
if full_update
34+
unlock = if full_update
3735
if conservative
38-
Bundler.definition(conservative: conservative)
36+
{ conservative: conservative }
3937
else
40-
Bundler.definition(true)
38+
true
4139
end
4240
else
4341
unless Bundler.default_lockfile.exist?
4442
raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \
4543
"Run `bundle install` to update and install the bundled gems."
4644
end
47-
Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems)
45+
explicit_gems = gems.dup
4846

4947
if groups.any?
5048
deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? }
5149
gems.concat(deps.map(&:name))
5250
end
5351

54-
Bundler.definition(gems: gems, sources: sources, ruby: options[:ruby],
55-
conservative: conservative,
56-
bundler: update_bundler)
52+
{
53+
gems: gems,
54+
sources: sources,
55+
ruby: options[:ruby],
56+
conservative: conservative,
57+
bundler: update_bundler,
58+
}
5759
end
5860

61+
Plugin.gemfile_install(Bundler.default_gemfile, Bundler.default_lockfile, unlock.dup) if Bundler.settings[:plugins]
62+
63+
Bundler::CLI::Common.ensure_all_gems_in_lockfile!(explicit_gems) if explicit_gems
64+
65+
Bundler.definition(unlock)
66+
5967
Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options)
6068

6169
Bundler::Fetcher.disable_endpoint = options["full-index"]

bundler/lib/bundler/dependency.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ def gemfile_dep?
118118
!gemspec_dev_dep?
119119
end
120120

121+
def plugin?
122+
@plugin ||= @options.fetch("plugin", false)
123+
end
124+
121125
def current_env?
122126
return true unless env
123127
if env.is_a?(Hash)

bundler/lib/bundler/dsl.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,15 @@ def source(source, *args, &blk)
124124

125125
if options.key?("type")
126126
options["type"] = options["type"].to_s
127-
unless Plugin.source?(options["type"])
127+
unless (source_plugin = Plugin.source_plugin(options["type"]))
128128
raise InvalidOption, "No plugin sources available for #{options["type"]}"
129129
end
130+
# Implicitly add a dependency on source plugins who are named bundler-source-<type>,
131+
# and aren't already mentioned in the Gemfile.
132+
# See also Plugin::DSL#source
133+
if source_plugin.start_with?("bundler-source-") && !@dependencies.any? {|d| d.name == source_plugin }
134+
plugin(source_plugin)
135+
end
130136

131137
unless block_given?
132138
raise InvalidOption, "You need to pass a block to #source with :type option"
@@ -256,8 +262,15 @@ def env(name)
256262
@env = old
257263
end
258264

259-
def plugin(*args)
260-
# Pass on
265+
def plugin(name, *args)
266+
options = args.last.is_a?(Hash) ? args.pop.dup : {}
267+
version = args || [">= 0"]
268+
269+
normalize_options(name, version, options)
270+
options["plugin"] = true
271+
options["require"] = false
272+
273+
add_dependency(name, version, options)
261274
end
262275

263276
def method_missing(name, *args)

bundler/lib/bundler/plugin.rb

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
module Bundler
66
module Plugin
7-
autoload :DSL, File.expand_path("plugin/dsl", __dir__)
8-
autoload :Events, File.expand_path("plugin/events", __dir__)
9-
autoload :Index, File.expand_path("plugin/index", __dir__)
10-
autoload :Installer, File.expand_path("plugin/installer", __dir__)
11-
autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
7+
autoload :DSL, File.expand_path("plugin/dsl", __dir__)
8+
autoload :Events, File.expand_path("plugin/events", __dir__)
9+
autoload :Index, File.expand_path("plugin/index", __dir__)
10+
autoload :Installer, File.expand_path("plugin/installer", __dir__)
11+
autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
12+
autoload :UnloadedSource, File.expand_path("plugin/unloaded_source", __dir__)
1213

1314
class MalformattedPlugin < PluginError; end
1415
class UndefinedCommandError < PluginError; end
@@ -17,6 +18,14 @@ class PluginInstallError < PluginError; end
1718

1819
PLUGIN_FILE_NAME = "plugins.rb"
1920

21+
# Module-level flag set while .gemfile_install parses the Gemfile and
22+
# consulted by .from_lock to substitute plugin sources with
23+
# UnloadedSource. It relies on definitions being built one at a time in
24+
# a single thread; if they are ever built concurrently or reentrantly,
25+
# this needs to be replaced by explicit state passed down to the
26+
# lockfile parser.
27+
@gemfile_parse = false
28+
2029
module_function
2130

2231
def reset!
@@ -26,6 +35,7 @@ def reset!
2635
@commands = {}
2736
@hooks_by_event = Hash.new {|h, k| h[k] = [] }
2837
@loaded_plugin_names = []
38+
@index = nil
2939
end
3040

3141
reset!
@@ -40,7 +50,7 @@ def install(names, options)
4050

4151
specs = Installer.new.install(names, options)
4252

43-
save_plugins names, specs
53+
save_plugins specs.slice(*names)
4454
rescue PluginError
4555
specs_to_delete = specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) }
4656
specs_to_delete.each_value {|spec| Bundler.rm_rf(spec.full_gem_path) }
@@ -100,29 +110,44 @@ def list
100110
#
101111
# @param [Pathname] gemfile path
102112
# @param [Proc] block that can be evaluated for (inline) Gemfile
103-
def gemfile_install(gemfile = nil, &inline)
104-
Bundler.settings.temporary(frozen: false, deployment: false) do
105-
builder = DSL.new
106-
if block_given?
107-
builder.instance_eval(&inline)
108-
else
109-
builder.eval_gemfile(gemfile)
110-
end
111-
builder.check_primary_source_safety
112-
definition = builder.to_definition(nil, true)
113-
114-
return if definition.dependencies.empty?
113+
def gemfile_install(gemfile = nil, lockfile = nil, unlock = {}, &inline)
114+
@gemfile_parse = true
115+
Bundler.configure
116+
builder = DSL.new
117+
if block_given?
118+
builder.instance_eval(&inline)
119+
else
120+
builder.eval_gemfile(gemfile)
121+
end
122+
builder.check_primary_source_safety
115123

116-
plugins = definition.dependencies.map(&:name)
117-
installed_specs = Installer.new.install_definition(definition)
124+
plugins = builder.dependencies.map(&:name)
125+
return if plugins.empty?
118126

119-
save_plugins plugins, installed_specs, builder.inferred_plugins
127+
# skip the update if unlocking specific gems, but none of them are plugins
128+
# declared in the Gemfile
129+
if unlock.is_a?(Hash) && unlock[:gems] && !unlock[:gems].empty? &&
130+
(unlock[:gems] & plugins).empty?
131+
unlock = {}
120132
end
133+
134+
# resolve remotely when unlocking, so that plugins can be updated.
135+
# Definition#initialize consumes the unlock hash, so this must be decided
136+
# before building the definition.
137+
updating = unlock == true || (unlock.is_a?(Hash) && !unlock.empty?)
138+
139+
definition = builder.to_definition(lockfile, unlock)
140+
141+
installed_specs = Installer.new.install_definition(definition, updating)
142+
143+
save_plugins installed_specs.slice(*plugins), builder.inferred_plugins
121144
rescue RuntimeError => e
122145
unless e.is_a?(GemfileError)
123146
Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}"
124147
end
125148
raise
149+
ensure
150+
@gemfile_parse = false
126151
end
127152

128153
# The index object used to store the details about the plugin
@@ -183,12 +208,17 @@ def add_source(source, cls)
183208

184209
# Checks if any plugin declares the source
185210
def source?(name)
186-
!index.source_plugin(name.to_s).nil?
211+
!!source_plugin(name)
212+
end
213+
214+
# Returns the plugin that handles the source +name+ if any
215+
def source_plugin(name)
216+
index.source_plugin(name.to_s)
187217
end
188218

189219
# @return [Class] that handles the source. The class includes API::Source
190220
def source(name)
191-
raise UnknownSourceError, "Source #{name} not found" unless source? name
221+
raise UnknownSourceError, "Source #{name} not found" unless source_plugin(name)
192222

193223
load_plugin(index.source_plugin(name)) unless @sources.key? name
194224

@@ -199,9 +229,14 @@ def source(name)
199229
# @return [API::Source] the instance of the class that handles the source
200230
# type passed in locked_opts
201231
def from_lock(locked_opts)
232+
opts = locked_opts.merge("uri" => locked_opts["remote"])
233+
# when reading the lockfile while doing the plugin-install-from-gemfile phase,
234+
# we need to ignore any plugin sources
235+
return UnloadedSource.new(opts) if @gemfile_parse
236+
202237
src = source(locked_opts["type"])
203238

204-
src.new(locked_opts.merge("uri" => locked_opts["remote"]))
239+
src.new(opts)
205240
end
206241

207242
# To be called via the API to register a hooks and corresponding block that
@@ -237,7 +272,9 @@ def hook(event, *args, &arg_blk)
237272
#
238273
# @return [String, nil] installed path
239274
def installed?(plugin)
240-
Index.new.installed?(plugin)
275+
(path = index.installed?(plugin)) &&
276+
index.plugin_path(plugin).join(PLUGIN_FILE_NAME).file? &&
277+
path
241278
end
242279

243280
# @return [true, false] whether the plugin is loaded
@@ -247,19 +284,11 @@ def loaded?(plugin)
247284

248285
# Post installation processing and registering with index
249286
#
250-
# @param [Array<String>] plugins list to be installed
251287
# @param [Hash] specs of plugins mapped to installation path (currently they
252288
# contain all the installed specs, including plugins)
253289
# @param [Array<String>] names of inferred source plugins that can be ignored
254-
def save_plugins(plugins, specs, optional_plugins = [])
255-
plugins.each do |name|
256-
spec = specs[name]
257-
258-
# It's possible that the `plugin` found in the Gemfile don't appear in the specs. For instance when
259-
# calling `BUNDLE_WITHOUT=default bundle install`, the plugins will not get installed.
260-
next if spec.nil?
261-
next if index.up_to_date?(spec)
262-
290+
def save_plugins(specs, optional_plugins = [])
291+
specs.each do |name, spec|
263292
save_plugin(name, spec, optional_plugins.include?(name))
264293
end
265294
end
@@ -284,6 +313,8 @@ def validate_plugin!(plugin_path)
284313
#
285314
# @raise [PluginInstallError] if validation or registration raises any error
286315
def save_plugin(name, spec, optional_plugin = false)
316+
return if index.up_to_date?(spec)
317+
287318
validate_plugin! Pathname.new(spec.full_gem_path)
288319
installed = register_plugin(name, spec, optional_plugin)
289320
Bundler.ui.info "Installed plugin #{name}" if installed
@@ -319,7 +350,7 @@ def register_plugin(name, spec, optional_plugin = false)
319350
raise MalformattedPlugin, "#{e.class}: #{e.message}"
320351
end
321352

322-
if optional_plugin && @sources.keys.any? {|s| source? s }
353+
if optional_plugin && @sources.keys.any? {|s| source_plugin(s) }
323354
Bundler.rm_rf(path)
324355
false
325356
else

bundler/lib/bundler/plugin/dsl.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ module Plugin
55
# Dsl to parse the Gemfile looking for plugins to install
66
class DSL < Bundler::Dsl
77
class PluginGemfileError < PluginError; end
8-
alias_method :_gem, :gem # To use for plugin installation as gem
98

109
# So that we don't have to override all there methods to dummy ones
1110
# explicitly.
1211
# They will be handled by method_missing
13-
[:gemspec, :gem, :install_if, :platforms, :env].each {|m| undef_method m }
12+
[:gemspec, :install_if, :platforms, :env].each {|m| undef_method m }
1413

1514
# This lists the plugins that was added automatically and not specified by
1615
# the user.
@@ -24,12 +23,11 @@ class PluginGemfileError < PluginError; end
2423

2524
def initialize
2625
super
27-
@sources = Plugin::SourceList.new
2826
@inferred_plugins = [] # The source plugins inferred from :type
2927
end
3028

31-
def plugin(name, *args)
32-
_gem(name, *args)
29+
def gem(*args)
30+
# Ignore regular dependencies when doing the plugins-only pre-parse
3331
end
3432

3533
def method_missing(name, *args)

bundler/lib/bundler/plugin/installer.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ def install(names, options)
3232
#
3333
# @param [Definition] definition object
3434
# @return [Hash] map of names to their specs they are installed with
35-
def install_definition(definition)
35+
def install_definition(definition, latest = false)
3636
def definition.lock(*); end
37-
definition.remotely!
37+
38+
if latest || definition.missing_specs?
39+
definition.remotely!
40+
else
41+
definition.with_cache!
42+
end
43+
3844
specs = definition.specs
3945

40-
install_from_specs specs
46+
install_from_specs(specs)
4147
end
4248

4349
private
@@ -89,14 +95,14 @@ def install_rubygems(names, version, sources)
8995
end
9096

9197
def install_all_sources(names, version, source_list, source = nil)
92-
deps = names.map {|name| Dependency.new(name, version, { "source" => source }) }
98+
deps = names.map {|name| Dependency.new(name, version, { "source" => source, "plugin" => true }) }
9399

94100
Bundler.configure_gem_home_and_path(Plugin.root)
95101

96102
Bundler.settings.temporary(deployment: false, frozen: false) do
97103
definition = Definition.new(nil, deps, source_list, true)
98104

99-
install_definition(definition)
105+
install_definition(definition, true)
100106
end
101107
end
102108

@@ -110,6 +116,8 @@ def install_from_specs(specs)
110116
paths = {}
111117

112118
specs.each do |spec|
119+
next if spec.name == "bundler"
120+
113121
spec.source.download(spec)
114122
spec.source.install(spec)
115123

0 commit comments

Comments
 (0)