Skip to content

Commit d5a55e1

Browse files
ccutrerdfop02
andcommitted
Fix plugin installation from gemfile
Several things are fixed: * Don't re-install a plugin referenced from the gemfile with every call to `bundle install` * If the version of a plugin referenced in the gemfile conflicts with what's in the plugin index, _do_ re-install it * If plugins aren't installed yet, don't throw cryptic errors from commands that don't implicitly install gems, such as `bundle check` and `bundle info`. This also applies if the plugin index references a system gem, and that gem is removed. This is all accomplished by actuallying including plugins as regular dependencies in the Gemfile, so that they end up in the lockfile, and then just using the regular lockfile with the plugins-only pass of the gemfile in the Plugin infrastructure. This also means that non-specific version constraints can be used for plugins, and you can update them with `bundle update <plugin>` just like any other gem. Co-authored-by: Diogo Fernandes <diogofernandesop@gmail.com>
1 parent 45d2a9a commit d5a55e1

File tree

22 files changed

+467
-66
lines changed

22 files changed

+467
-66
lines changed

Manifest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ bundler/lib/bundler/plugin.rb
161161
bundler/lib/bundler/plugin/api.rb
162162
bundler/lib/bundler/plugin/api/source.rb
163163
bundler/lib/bundler/plugin/dsl.rb
164+
bundler/lib/bundler/plugin/dummy_source.rb
164165
bundler/lib/bundler/plugin/events.rb
165166
bundler/lib/bundler/plugin/index.rb
166167
bundler/lib/bundler/plugin/installer.rb

bundler/lib/bundler/cli/install.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def run
5959
removed_message: "The --binstubs option have been removed in favor of `bundle binstubs --all`"
6060
end
6161

62-
Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins?
62+
Plugin.gemfile_install(Bundler.default_gemfile, Bundler.default_lockfile) if Bundler.feature_flag.plugins?
6363

6464
definition = Bundler.definition
6565
definition.validate_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.feature_flag.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
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.feature_flag.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/definition.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
105105
@locked_specs = SpecSet.new([])
106106
@locked_sources = []
107107
end
108+
109+
remove_plugin_dependencies_if_necessary
108110
else
109111
@unlock = {}
110112
@platforms = []
@@ -228,6 +230,10 @@ def requested_dependencies
228230
dependencies_for(requested_groups)
229231
end
230232

233+
def plugin_dependencies
234+
requested_dependencies.select {|dep| dep.type == :plugin }
235+
end
236+
231237
def current_dependencies
232238
filter_relevant(dependencies)
233239
end
@@ -421,6 +427,7 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
421427
def validate_runtime!
422428
validate_ruby!
423429
validate_platforms!
430+
validate_plugins!
424431
end
425432

426433
def validate_ruby!
@@ -456,6 +463,19 @@ def validate_platforms!
456463
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
457464
end
458465

466+
def validate_plugins!
467+
missing_plugins_list = []
468+
plugin_dependencies.each do |plugin|
469+
missing_plugins_list << plugin unless Plugin.installed?(plugin.name)
470+
end
471+
missing_plugins_list.map! {|p| "#{p.name} (#{p.requirement})" }
472+
if missing_plugins_list.size > 1
473+
raise GemNotFound, "Plugins #{missing_plugins_list.join(", ")} are not installed"
474+
elsif missing_plugins_list.any?
475+
raise GemNotFound, "Plugin #{missing_plugins_list.join(", ")} is not installed"
476+
end
477+
end
478+
459479
def add_platform(platform)
460480
return if @platforms.include?(platform)
461481

@@ -1083,5 +1103,14 @@ def spec_set_incomplete_for_platform?(spec_set, platform)
10831103
def source_map
10841104
@source_map ||= SourceMap.new(sources, dependencies, @locked_specs)
10851105
end
1106+
1107+
def remove_plugin_dependencies_if_necessary
1108+
return if Bundler.feature_flag.plugins_in_lockfile?
1109+
# we already have plugin dependencies in the lockfile; continue to do so regardless
1110+
# of the current setting
1111+
return if @dependencies.any? {|d| d.type == :plugin && @locked_deps.key?(d.name) }
1112+
1113+
@dependencies.reject! {|d| d.type == :plugin }
1114+
end
10861115
end
10871116
end

bundler/lib/bundler/dependency.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ class Dependency < Gem::Dependency
2929

3030
def initialize(name, version, options = {}, &blk)
3131
type = options["type"] || :runtime
32-
super(name, version, type)
32+
if type == :plugin
33+
# RubyGems doesn't support plugin type, which only
34+
# makes sense in the context of Bundler, so bypass
35+
# the RubyGems validation
36+
super(name, version, :runtime)
37+
@type = type
38+
else
39+
super(name, version, type)
40+
end
3341

3442
@autorequire = nil
3543
@groups = Array(options["group"] || :default).map(&:to_sym)

bundler/lib/bundler/dsl.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,15 @@ def source(source, *args, &blk)
160160

161161
if options.key?("type")
162162
options["type"] = options["type"].to_s
163-
unless Plugin.source?(options["type"])
163+
unless (source_plugin = Plugin.source_plugin(options["type"]))
164164
raise InvalidOption, "No plugin sources available for #{options["type"]}"
165165
end
166+
# Implicitly add a dependency on source plugins who are named bundler-source-<type>,
167+
# and aren't already mentioned in the Gemfile.
168+
# See also Plugin::DSL#source
169+
if source_plugin.start_with?("bundler-source-") && !@dependencies.any? {|d| d.name == source_plugin }
170+
plugin(source_plugin)
171+
end
166172

167173
unless block_given?
168174
raise InvalidOption, "You need to pass a block to #source with :type option"
@@ -271,7 +277,13 @@ def env(name)
271277
end
272278

273279
def plugin(*args)
274-
# Pass on
280+
options = args.last.is_a?(Hash) ? args.pop.dup : {}
281+
282+
normalize_hash(options)
283+
options["type"] = :plugin
284+
options["require"] = false
285+
286+
gem(*args, options)
275287
end
276288

277289
def method_missing(name, *args)

bundler/lib/bundler/feature_flag.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def self.settings_method(name, key, &default)
3535
settings_flag(:global_gem_cache) { bundler_3_mode? }
3636
settings_flag(:path_relative_to_cwd) { bundler_3_mode? }
3737
settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") }
38+
settings_flag(:plugins_in_lockfile) { bundler_3_mode? }
3839
settings_flag(:print_only_version_number) { bundler_3_mode? }
3940
settings_flag(:setup_makes_kernel_gem_public) { !bundler_3_mode? }
4041
settings_flag(:update_requires_all_flag) { bundler_4_mode? }

bundler/lib/bundler/man/bundle-config.1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ The following is a list of all configuration keys and their purpose\. You can le
163163
.IP "\(bu" 4
164164
\fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler's experimental plugin system\.
165165
.IP "\(bu" 4
166+
\fBplugins_in_lockfile\fR (\fBBUNDLE_PLUGINS_IN_LOCKFILE\fR): Include plugins as regular dependencies in the lockfile\.
167+
.IP "\(bu" 4
166168
\fBprefer_patch\fR (BUNDLE_PREFER_PATCH): Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\.
167169
.IP "\(bu" 4
168170
\fBprint_only_version_number\fR (\fBBUNDLE_PRINT_ONLY_VERSION_NUMBER\fR): Print only version number from \fBbundler \-\-version\fR\.

bundler/lib/bundler/man/bundle-config.1.ronn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html).
234234
Makes `--path` relative to the CWD instead of the `Gemfile`.
235235
* `plugins` (`BUNDLE_PLUGINS`):
236236
Enable Bundler's experimental plugin system.
237+
* `plugins_in_lockfile` (`BUNDLE_PLUGINS_IN_LOCKFILE`):
238+
Include plugins as regular dependencies in the lockfile.
237239
* `prefer_patch` (BUNDLE_PREFER_PATCH):
238240
Prefer updating only to next patch version during updates. Makes `bundle update` calls equivalent to `bundler update --patch`.
239241
* `print_only_version_number` (`BUNDLE_PRINT_ONLY_VERSION_NUMBER`):

bundler/lib/bundler/plugin.rb

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
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 :DummySource, File.expand_path("plugin/dummy_source", __dir__)
9+
autoload :Events, File.expand_path("plugin/events", __dir__)
10+
autoload :Index, File.expand_path("plugin/index", __dir__)
11+
autoload :Installer, File.expand_path("plugin/installer", __dir__)
12+
autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
1213

1314
class MalformattedPlugin < PluginError; end
1415
class UndefinedCommandError < PluginError; end
1516
class UnknownSourceError < PluginError; end
1617
class PluginInstallError < PluginError; end
1718

1819
PLUGIN_FILE_NAME = "plugins.rb"
20+
@gemfile_parse = false
1921

2022
module_function
2123

@@ -26,6 +28,7 @@ def reset!
2628
@commands = {}
2729
@hooks_by_event = Hash.new {|h, k| h[k] = [] }
2830
@loaded_plugin_names = []
31+
@index = nil
2932
end
3033

3134
reset!
@@ -40,7 +43,7 @@ def install(names, options)
4043

4144
specs = Installer.new.install(names, options)
4245

43-
save_plugins names, specs
46+
save_plugins specs
4447
rescue PluginError
4548
specs_to_delete = specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) }
4649
specs_to_delete.each_value {|spec| Bundler.rm_rf(spec.full_gem_path) }
@@ -100,29 +103,44 @@ def list
100103
#
101104
# @param [Pathname] gemfile path
102105
# @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
106+
def gemfile_install(gemfile = nil, lockfile = nil, unlock = {}, &inline)
107+
# skip the update if unlocking specific gems, but none of them are our plugins
108+
if unlock.is_a?(Hash) && unlock[:gems] && !unlock[:gems].empty? &&
109+
(unlock[:gems] & index.installed_plugins).empty?
110+
unlock = {}
111+
end
112+
113+
@gemfile_parse = true
114+
# plugins_in_lockfile is the user facing setting to force plugins to be
115+
# included in the lockfile as regular dependencies. But during this
116+
# first pass over the Gemfile where we're installing the plugins, we
117+
# need that setting to be set, so that we can find the plugins and
118+
# install them. We don't persist a lockfile during this pass, so it won't
119+
# have any user-facing impact.
120+
Bundler.settings.temporary(plugins_in_lockfile: true) do
121+
Bundler.configure
105122
builder = DSL.new
106123
if block_given?
107124
builder.instance_eval(&inline)
108125
else
109126
builder.eval_gemfile(gemfile)
110127
end
111128
builder.check_primary_source_safety
112-
definition = builder.to_definition(nil, true)
129+
definition = builder.to_definition(lockfile, unlock)
113130

114131
return if definition.dependencies.empty?
115132

116-
plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p }
117133
installed_specs = Installer.new.install_definition(definition)
118134

119-
save_plugins plugins, installed_specs, builder.inferred_plugins
135+
save_plugins installed_specs, builder.inferred_plugins
120136
end
121137
rescue RuntimeError => e
122138
unless e.is_a?(GemfileError)
123139
Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}"
124140
end
125141
raise
142+
ensure
143+
@gemfile_parse = false
126144
end
127145

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

184202
# Checks if any plugin declares the source
185203
def source?(name)
186-
!index.source_plugin(name.to_s).nil?
204+
!!source_plugin(name)
205+
end
206+
207+
# Returns the plugin that handles the source +name+ if any
208+
def source_plugin(name)
209+
index.source_plugin(name.to_s)
187210
end
188211

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

193216
load_plugin(index.source_plugin(name)) unless @sources.key? name
194217

@@ -199,9 +222,14 @@ def source(name)
199222
# @return [API::Source] the instance of the class that handles the source
200223
# type passed in locked_opts
201224
def from_lock(locked_opts)
225+
opts = locked_opts.merge("uri" => locked_opts["remote"])
226+
# when reading the lockfile while doing the plugin-install-from-gemfile phase,
227+
# we need to ignore any plugin sources
228+
return DummySource.new(opts) if @gemfile_parse
229+
202230
src = source(locked_opts["type"])
203231

204-
src.new(locked_opts.merge("uri" => locked_opts["remote"]))
232+
src.new(opts)
205233
end
206234

207235
# To be called via the API to register a hooks and corresponding block that
@@ -237,7 +265,9 @@ def hook(event, *args, &arg_blk)
237265
#
238266
# @return [String, nil] installed path
239267
def installed?(plugin)
240-
Index.new.installed?(plugin)
268+
(path = index.installed?(plugin)) &&
269+
index.plugin_path(plugin).join(PLUGIN_FILE_NAME).file? &&
270+
path
241271
end
242272

243273
# @return [true, false] whether the plugin is loaded
@@ -251,12 +281,8 @@ def loaded?(plugin)
251281
# @param [Hash] specs of plugins mapped to installation path (currently they
252282
# contain all the installed specs, including plugins)
253283
# @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-
next if index.installed?(name)
257-
258-
spec = specs[name]
259-
284+
def save_plugins(specs, optional_plugins = [])
285+
specs.each do |name, spec|
260286
save_plugin(name, spec, optional_plugins.include?(name))
261287
end
262288
end
@@ -281,7 +307,10 @@ def validate_plugin!(plugin_path)
281307
#
282308
# @raise [PluginInstallError] if validation or registration raises any error
283309
def save_plugin(name, spec, optional_plugin = false)
284-
validate_plugin! Pathname.new(spec.full_gem_path)
310+
path = Pathname.new(spec.full_gem_path)
311+
return if index.installed?(name) && index.plugin_path(name) == path
312+
313+
validate_plugin!(path)
285314
installed = register_plugin(name, spec, optional_plugin)
286315
Bundler.ui.info "Installed plugin #{name}" if installed
287316
rescue PluginError => e
@@ -316,7 +345,7 @@ def register_plugin(name, spec, optional_plugin = false)
316345
raise MalformattedPlugin, "#{e.class}: #{e.message}"
317346
end
318347

319-
if optional_plugin && @sources.keys.any? {|s| source? s }
348+
if optional_plugin && @sources.keys.any? {|s| source_plugin(s) }
320349
Bundler.rm_rf(path)
321350
false
322351
else

0 commit comments

Comments
 (0)