Skip to content

Commit 6f25b03

Browse files
committed
Ensure plugins are installed before allowing other operations
Keep track of plugins during the main Gemfile pass, and then validate that they're installed everywhere else we validate the runtime.
1 parent 31cffe2 commit 6f25b03

File tree

5 files changed

+119
-13
lines changed

5 files changed

+119
-13
lines changed

bundler/lib/bundler/definition.rb

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class << self
1414
attr_reader(
1515
:dependencies,
1616
:locked_deps,
17+
:plugins,
1718
:locked_gems,
1819
:platforms,
1920
:ruby_version,
@@ -57,7 +58,16 @@ def self.build(gemfile, lockfile, unlock)
5758
# @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version
5859
# @param optional_groups [Array(String)] A list of optional groups
5960
# @param lockfile_contents [String, nil] The contents of the lockfile
60-
def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [], lockfile_contents = nil)
61+
# @param plugins [Array(Bundler::Dependency)] array of plugin dependencies from Gemfile
62+
def initialize(lockfile,
63+
dependencies,
64+
sources,
65+
unlock,
66+
ruby_version = nil,
67+
optional_groups = [],
68+
gemfiles = [],
69+
lockfile_contents = nil,
70+
plugins = [])
6171
if [true, false].include?(unlock)
6272
@unlocking_bundler = false
6373
@unlocking = unlock
@@ -67,6 +77,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
6777
end
6878

6979
@dependencies = dependencies
80+
@plugins = plugins
7081
@sources = sources
7182
@unlock = unlock
7283
@optional_groups = optional_groups
@@ -240,10 +251,18 @@ def requested_dependencies
240251
dependencies_for(requested_groups)
241252
end
242253

254+
def requested_plugins
255+
plugins_for(requested_groups)
256+
end
257+
243258
def current_dependencies
244259
filter_relevant(dependencies)
245260
end
246261

262+
def current_plugins
263+
filter_relevant(plugins)
264+
end
265+
247266
def current_locked_dependencies
248267
filter_relevant(locked_dependencies)
249268
end
@@ -286,6 +305,13 @@ def dependencies_for(groups)
286305
deps
287306
end
288307

308+
def plugins_for(groups)
309+
groups.map!(&:to_sym)
310+
current_plugins.reject do |d|
311+
(d.groups & groups).empty?
312+
end
313+
end
314+
289315
# Resolve all the dependencies specified in Gemfile. It ensures that
290316
# dependencies that have been already resolved via locked file and are fresh
291317
# are reused when resolving dependencies
@@ -318,7 +344,7 @@ def spec_git_paths
318344
end
319345

320346
def groups
321-
dependencies.map(&:groups).flatten.uniq
347+
(dependencies + plugins).map(&:groups).flatten.uniq
322348
end
323349

324350
def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or_unused = false)
@@ -424,6 +450,7 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
424450
def validate_runtime!
425451
validate_ruby!
426452
validate_platforms!
453+
validate_plugins!
427454
end
428455

429456
def validate_ruby!
@@ -459,6 +486,18 @@ def validate_platforms!
459486
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
460487
end
461488

489+
def validate_plugins!
490+
missing_plugins_list = []
491+
requested_plugins.each do |plugin|
492+
missing_plugins_list << plugin unless Plugin.installed?(plugin.name)
493+
end
494+
if missing_plugins_list.size > 1
495+
raise GemNotFound, "Plugins #{missing_plugins_list.join(", ")} are not installed"
496+
elsif missing_plugins_list.any?
497+
raise GemNotFound, "Plugin #{missing_plugins_list.join(", ")} is not installed"
498+
end
499+
end
500+
462501
def add_platform(platform)
463502
@new_platform ||= !@platforms.include?(platform)
464503
@platforms |= [platform]

bundler/lib/bundler/dsl.rb

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def initialize
2929
@sources = SourceList.new
3030
@git_sources = {}
3131
@dependencies = []
32+
@plugins = []
3233
@groups = []
3334
@install_conditionals = []
3435
@optional_groups = []
@@ -97,7 +98,7 @@ def gem(name, *args)
9798
options["gemfile"] = @gemfile
9899
version = args || [">= 0"]
99100

100-
normalize_options(name, version, options)
101+
normalize_options(name, version, true, options)
101102

102103
dep = Dependency.new(name, version, options)
103104

@@ -228,7 +229,7 @@ def github(repo, options = {})
228229

229230
def to_definition(lockfile, unlock, lockfile_contents: nil)
230231
check_primary_source_safety
231-
Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, lockfile_contents)
232+
Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, lockfile_contents, @plugins)
232233
end
233234

234235
def group(*args, &blk)
@@ -270,8 +271,29 @@ def env(name)
270271
@env = old
271272
end
272273

273-
def plugin(*args)
274-
# Pass on
274+
def plugin(name, *args)
275+
options = args.last.is_a?(Hash) ? args.pop.dup : {}
276+
options["gemfile"] = @gemfile
277+
version = args || [">= 0"]
278+
279+
# We don't care to add sources for plugins in this pass over the gemfile
280+
# since we're not actually installing plugins here (they should already
281+
# be installed), just keeping track of them so that we can verify they
282+
# are actually installed. This is important because otherwise sources
283+
# unique to the plugin (like a git source) would end up in the lockfile,
284+
# which we don't want.
285+
normalize_options(name, version, false, options)
286+
287+
dep = Dependency.new(name, version, options)
288+
289+
# if there's already a dependency with this name we try to prefer one
290+
if current = @plugins.find {|d| d.name == dep.name }
291+
Bundler.ui.warn "Your Gemfile lists the plugin #{current.name} (#{current.requirement}) more than once.\n" \
292+
"You should keep only one of them.\n" \
293+
"Remove any duplicate entries and specify the plugin only once."
294+
end
295+
296+
@plugins << dep
275297
end
276298

277299
def method_missing(name, *args)
@@ -347,7 +369,7 @@ def valid_keys
347369
@valid_keys ||= VALID_KEYS
348370
end
349371

350-
def normalize_options(name, version, opts)
372+
def normalize_options(name, version, add_to_sources, opts)
351373
if name.is_a?(Symbol)
352374
raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead)
353375
end
@@ -382,7 +404,7 @@ def normalize_options(name, version, opts)
382404
end
383405

384406
# Save sources passed in a key
385-
if opts.key?("source")
407+
if opts.key?("source") && add_to_sources
386408
source = normalize_source(opts["source"])
387409
opts["source"] = @sources.add_rubygems_source("remotes" => source)
388410
end
@@ -403,8 +425,10 @@ def normalize_options(name, version, opts)
403425
else
404426
options = opts.dup
405427
end
406-
source = send(type, param, options) {}
407-
opts["source"] = source
428+
if add_to_sources
429+
source = send(type, param, options) {}
430+
opts["source"] = source
431+
end
408432
end
409433

410434
opts["source"] ||= @source

bundler/lib/bundler/plugin.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def reset!
2727
@commands = {}
2828
@hooks_by_event = Hash.new {|h, k| h[k] = [] }
2929
@loaded_plugin_names = []
30+
@index = nil
3031
end
3132

3233
reset!
@@ -238,11 +239,9 @@ def hook(event, *args, &arg_blk)
238239
@hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) }
239240
end
240241

241-
# currently only intended for specs
242-
#
243242
# @return [String, nil] installed path
244243
def installed?(plugin)
245-
Index.new.installed?(plugin)
244+
index.installed?(plugin)
246245
end
247246

248247
# @return [true, false] whether the plugin is loaded

bundler/spec/plugins/install_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,48 @@ def exec(command, args)
463463
plugin_should_be_installed("foo")
464464
end
465465
end
466+
467+
it "fails bundle commands if plugins are not yet installed" do
468+
gemfile <<-G
469+
source '#{file_uri_for(gem_repo2)}'
470+
group :development do
471+
plugin 'foo'
472+
end
473+
474+
source '#{file_uri_for(gem_repo1)}' do
475+
gem 'rake'
476+
end
477+
G
478+
479+
plugin_should_not_be_installed("foo")
480+
481+
bundle "check", raise_on_error: false
482+
expect(err).to include("Plugin foo (>= 0) is not installed")
483+
484+
bundle "exec rake", raise_on_error: false
485+
expect(err).to include("Plugin foo (>= 0) is not installed")
486+
487+
bundle "config set --local without development"
488+
bundle "install"
489+
bundle "config unset --local without"
490+
491+
plugin_should_not_be_installed("foo")
492+
493+
bundle "check", raise_on_error: false
494+
expect(err).to include("Plugin foo (>= 0) is not installed")
495+
496+
bundle "exec rake", raise_on_error: false
497+
expect(err).to include("Plugin foo (>= 0) is not installed")
498+
499+
plugin_should_not_be_installed("foo")
500+
501+
bundle "install"
502+
plugin_should_be_installed("foo")
503+
504+
bundle "check"
505+
bundle "exec rake -T", raise_on_error: false
506+
expect(err).not_to include("Plugin foo (>= 0) is not installed")
507+
end
466508
end
467509

468510
context "inline gemfiles" do

bundler/spec/support/matchers.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def indent(string, padding = 4, indent_character = " ")
209209
RSpec::Matchers.alias_matcher :include_gem, :include_gems
210210

211211
def plugin_should_be_installed(*names)
212+
Bundler::Plugin.remove_instance_variable(:@index)
212213
names.each do |name|
213214
expect(Bundler::Plugin).to be_installed(name)
214215
path = Pathname.new(Bundler::Plugin.installed?(name))
@@ -217,6 +218,7 @@ def plugin_should_be_installed(*names)
217218
end
218219

219220
def plugin_should_be_installed_with_version(name, version)
221+
Bundler::Plugin.remove_instance_variable(:@index)
220222
expect(Bundler::Plugin).to be_installed(name)
221223
path = Pathname.new(Bundler::Plugin.installed?(name))
222224

0 commit comments

Comments
 (0)