Skip to content

Commit

Permalink
Ensure plugins are installed before allowing other operations
Browse files Browse the repository at this point in the history
Keep track of plugins during the main Gemfile pass, and then
validate that they're installed everywhere else we validate
the runtime.
  • Loading branch information
ccutrer committed Oct 11, 2023
1 parent 76251fc commit 043e255
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 13 deletions.
43 changes: 41 additions & 2 deletions bundler/lib/bundler/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class << self
attr_reader(
:dependencies,
:locked_deps,
:plugins,
:locked_gems,
:platforms,
:ruby_version,
Expand Down Expand Up @@ -56,7 +57,16 @@ def self.build(gemfile, lockfile, unlock)
# @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version
# @param optional_groups [Array(String)] A list of optional groups
# @param lockfile_contents [String, nil] The contents of the lockfile
def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [], lockfile_contents = nil)
# @param plugins [Array(Bundler::Dependency)] array of plugin dependencies from Gemfile
def initialize(lockfile,
dependencies,
sources,
unlock,
ruby_version = nil,
optional_groups = [],
gemfiles = [],
lockfile_contents = nil,
plugins = [])
if [true, false].include?(unlock)
@unlocking_bundler = false
@unlocking = unlock
Expand All @@ -66,6 +76,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
end

@dependencies = dependencies
@plugins = plugins
@sources = sources
@unlock = unlock
@optional_groups = optional_groups
Expand Down Expand Up @@ -237,10 +248,18 @@ def requested_dependencies
dependencies_for(requested_groups)
end

def requested_plugins
plugins_for(requested_groups)
end

def current_dependencies
filter_relevant(dependencies)
end

def current_plugins
filter_relevant(plugins)
end

def current_locked_dependencies
filter_relevant(locked_dependencies)
end
Expand Down Expand Up @@ -276,6 +295,13 @@ def dependencies_for(groups)
end
end

def plugins_for(groups)
groups.map!(&:to_sym)
current_plugins.reject do |d|
(d.groups & groups).empty?
end
end

# Resolve all the dependencies specified in Gemfile. It ensures that
# dependencies that have been already resolved via locked file and are fresh
# are reused when resolving dependencies
Expand Down Expand Up @@ -308,7 +334,7 @@ def spec_git_paths
end

def groups
dependencies.map(&:groups).flatten.uniq
(dependencies + plugins).map(&:groups).flatten.uniq
end

def lock(file, preserve_unknown_sections = false)
Expand Down Expand Up @@ -422,6 +448,7 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
def validate_runtime!
validate_ruby!
validate_platforms!
validate_plugins!
end

def validate_ruby!
Expand Down Expand Up @@ -457,6 +484,18 @@ def validate_platforms!
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
end

def validate_plugins!
missing_plugins_list = []
requested_plugins.each do |plugin|
missing_plugins_list << plugin unless Plugin.installed?(plugin.name)
end
if missing_plugins_list.size > 1
raise GemNotFound, "Plugins #{missing_plugins_list.join(", ")} are not installed"
elsif missing_plugins_list.any?
raise GemNotFound, "Plugin #{missing_plugins_list.join(", ")} is not installed"
end
end

def add_platform(platform)
@new_platform ||= !@platforms.include?(platform)
@platforms |= [platform]
Expand Down
40 changes: 32 additions & 8 deletions bundler/lib/bundler/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def initialize
@sources = SourceList.new
@git_sources = {}
@dependencies = []
@plugins = []
@groups = []
@install_conditionals = []
@optional_groups = []
Expand Down Expand Up @@ -96,7 +97,7 @@ def gem(name, *args)
options["gemfile"] = @gemfile
version = args || [">= 0"]

normalize_options(name, version, options)
normalize_options(name, version, true, options)

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

Expand Down Expand Up @@ -215,7 +216,7 @@ def github(repo, options = {})

def to_definition(lockfile, unlock, lockfile_contents: nil)
check_primary_source_safety
Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, lockfile_contents)
Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, lockfile_contents, @plugins)
end

def group(*args, &blk)
Expand Down Expand Up @@ -257,8 +258,29 @@ def env(name)
@env = old
end

def plugin(*args)
# Pass on
def plugin(name, *args)
options = args.last.is_a?(Hash) ? args.pop.dup : {}
options["gemfile"] = @gemfile
version = args || [">= 0"]

# We don't care to add sources for plugins in this pass over the gemfile
# since we're not actually installing plugins here (they should already
# be installed), just keeping track of them so that we can verify they
# are actually installed. This is important because otherwise sources
# unique to the plugin (like a git source) would end up in the lockfile,
# which we don't want.
normalize_options(name, version, false, options)

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

# if there's already a dependency with this name we try to prefer one
if current = @plugins.find {|d| d.name == dep.name }
Bundler.ui.warn "Your Gemfile lists the plugin #{current.name} (#{current.requirement}) more than once.\n" \
"You should keep only one of them.\n" \
"Remove any duplicate entries and specify the plugin only once."
end

@plugins << dep
end

def method_missing(name, *args)
Expand Down Expand Up @@ -320,7 +342,7 @@ def valid_keys
@valid_keys ||= VALID_KEYS
end

def normalize_options(name, version, opts)
def normalize_options(name, version, add_to_sources, opts)
if name.is_a?(Symbol)
raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead)
end
Expand Down Expand Up @@ -355,7 +377,7 @@ def normalize_options(name, version, opts)
end

# Save sources passed in a key
if opts.key?("source")
if opts.key?("source") && add_to_sources
source = normalize_source(opts["source"])
opts["source"] = @sources.add_rubygems_source("remotes" => source)
end
Expand All @@ -376,8 +398,10 @@ def normalize_options(name, version, opts)
else
options = opts.dup
end
source = send(type, param, options) {}
opts["source"] = source
if add_to_sources
source = send(type, param, options) {}
opts["source"] = source
end
end

opts["source"] ||= @source
Expand Down
5 changes: 2 additions & 3 deletions bundler/lib/bundler/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def reset!
@commands = {}
@hooks_by_event = Hash.new {|h, k| h[k] = [] }
@loaded_plugin_names = []
@index = nil
end

reset!
Expand Down Expand Up @@ -237,11 +238,9 @@ def hook(event, *args, &arg_blk)
@hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) }
end

# currently only intended for specs
#
# @return [String, nil] installed path
def installed?(plugin)
Index.new.installed?(plugin)
index.installed?(plugin)
end

# Post installation processing and registering with index
Expand Down
42 changes: 42 additions & 0 deletions bundler/spec/plugins/install_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,48 @@ def exec(command, args)
plugin_should_be_installed("foo")
end
end

it "fails bundle commands if plugins are not yet installed" do
gemfile <<-G
source '#{file_uri_for(gem_repo2)}'
group :development do
plugin 'foo'
end
source '#{file_uri_for(gem_repo1)}' do
gem 'rake'
end
G

plugin_should_not_be_installed("foo")

bundle "check", :raise_on_error => false
expect(err).to include("Plugin foo (>= 0) is not installed")

bundle "exec rake", :raise_on_error => false
expect(err).to include("Plugin foo (>= 0) is not installed")

bundle "config set --local without development"
bundle "install"
bundle "config unset --local without"

plugin_should_not_be_installed("foo")

bundle "check", :raise_on_error => false
expect(err).to include("Plugin foo (>= 0) is not installed")

bundle "exec rake", :raise_on_error => false
expect(err).to include("Plugin foo (>= 0) is not installed")

plugin_should_not_be_installed("foo")

bundle "install"
plugin_should_be_installed("foo")

bundle "check"
bundle "exec rake -T", :raise_on_error => false
expect(err).not_to include("Plugin foo (>= 0) is not installed")
end
end

context "inline gemfiles" do
Expand Down
2 changes: 2 additions & 0 deletions bundler/spec/support/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ def indent(string, padding = 4, indent_character = " ")
RSpec::Matchers.alias_matcher :include_gem, :include_gems

def plugin_should_be_installed(*names)
Bundler::Plugin.remove_instance_variable(:@index)
names.each do |name|
expect(Bundler::Plugin).to be_installed(name)
path = Pathname.new(Bundler::Plugin.installed?(name))
Expand All @@ -229,6 +230,7 @@ def plugin_should_be_installed(*names)
end

def plugin_should_be_installed_with_version(name, version)
Bundler::Plugin.remove_instance_variable(:@index)
expect(Bundler::Plugin).to be_installed(name)
path = Pathname.new(Bundler::Plugin.installed?(name))

Expand Down

0 comments on commit 043e255

Please sign in to comment.