diff --git a/Manifest.txt b/Manifest.txt index 83328c03670e..fed1f772c476 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -168,6 +168,7 @@ bundler/lib/bundler/plugin/installer/git.rb bundler/lib/bundler/plugin/installer/path.rb bundler/lib/bundler/plugin/installer/rubygems.rb bundler/lib/bundler/plugin/source_list.rb +bundler/lib/bundler/plugin/unloaded_source.rb bundler/lib/bundler/process_lock.rb bundler/lib/bundler/remote_specification.rb bundler/lib/bundler/resolver.rb diff --git a/bundler/lib/bundler/cli/install.rb b/bundler/lib/bundler/cli/install.rb index 69affd1a109a..cf21afbb06e0 100644 --- a/bundler/lib/bundler/cli/install.rb +++ b/bundler/lib/bundler/cli/install.rb @@ -38,7 +38,7 @@ def run Bundler::Fetcher.disable_endpoint = options["full-index"] - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] + Plugin.gemfile_install(Bundler.default_gemfile, Bundler.default_lockfile) if Bundler.settings[:plugins] # For install we want to enable strict validation # (rather than some optimizations we perform at app runtime). diff --git a/bundler/lib/bundler/cli/update.rb b/bundler/lib/bundler/cli/update.rb index d92ffd995f36..33f9ac87406a 100644 --- a/bundler/lib/bundler/cli/update.rb +++ b/bundler/lib/bundler/cli/update.rb @@ -15,8 +15,6 @@ def run Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] - sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) @@ -33,29 +31,39 @@ def run conservative = options[:conservative] - if full_update + unlock = if full_update if conservative - Bundler.definition(conservative: conservative) + { conservative: conservative } else - Bundler.definition(true) + true end else unless Bundler.default_lockfile.exist? raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \ "Run `bundle install` to update and install the bundled gems." end - Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems) + explicit_gems = gems.dup if groups.any? deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? } gems.concat(deps.map(&:name)) end - Bundler.definition(gems: gems, sources: sources, ruby: options[:ruby], - conservative: conservative, - bundler: update_bundler) + { + gems: gems, + sources: sources, + ruby: options[:ruby], + conservative: conservative, + bundler: update_bundler, + } end + Plugin.gemfile_install(Bundler.default_gemfile, Bundler.default_lockfile, unlock.dup) if Bundler.settings[:plugins] + + Bundler::CLI::Common.ensure_all_gems_in_lockfile!(explicit_gems) if explicit_gems + + Bundler.definition(unlock) + Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) Bundler::Fetcher.disable_endpoint = options["full-index"] diff --git a/bundler/lib/bundler/dependency.rb b/bundler/lib/bundler/dependency.rb index cb9c7a76eab8..7a7f7944c849 100644 --- a/bundler/lib/bundler/dependency.rb +++ b/bundler/lib/bundler/dependency.rb @@ -118,6 +118,10 @@ def gemfile_dep? !gemspec_dev_dep? end + def plugin? + @plugin ||= @options.fetch("plugin", false) + end + def current_env? return true unless env if env.is_a?(Hash) diff --git a/bundler/lib/bundler/dsl.rb b/bundler/lib/bundler/dsl.rb index 6e2638a8be4c..e7504d0455d5 100644 --- a/bundler/lib/bundler/dsl.rb +++ b/bundler/lib/bundler/dsl.rb @@ -124,9 +124,15 @@ def source(source, *args, &blk) if options.key?("type") options["type"] = options["type"].to_s - unless Plugin.source?(options["type"]) + unless (source_plugin = Plugin.source_plugin(options["type"])) raise InvalidOption, "No plugin sources available for #{options["type"]}" end + # Implicitly add a dependency on source plugins who are named bundler-source-, + # and aren't already mentioned in the Gemfile. + # See also Plugin::DSL#source + if source_plugin.start_with?("bundler-source-") && !@dependencies.any? {|d| d.name == source_plugin } + plugin(source_plugin) + end unless block_given? raise InvalidOption, "You need to pass a block to #source with :type option" @@ -256,8 +262,15 @@ def env(name) @env = old end - def plugin(*args) - # Pass on + def plugin(name, *args) + options = args.last.is_a?(Hash) ? args.pop.dup : {} + version = args || [">= 0"] + + normalize_options(name, version, options) + options["plugin"] = true + options["require"] = false + + add_dependency(name, version, options) end def method_missing(name, *args) diff --git a/bundler/lib/bundler/plugin.rb b/bundler/lib/bundler/plugin.rb index faca6bea5306..081ec8e18031 100644 --- a/bundler/lib/bundler/plugin.rb +++ b/bundler/lib/bundler/plugin.rb @@ -4,11 +4,12 @@ module Bundler module Plugin - autoload :DSL, File.expand_path("plugin/dsl", __dir__) - autoload :Events, File.expand_path("plugin/events", __dir__) - autoload :Index, File.expand_path("plugin/index", __dir__) - autoload :Installer, File.expand_path("plugin/installer", __dir__) - autoload :SourceList, File.expand_path("plugin/source_list", __dir__) + autoload :DSL, File.expand_path("plugin/dsl", __dir__) + autoload :Events, File.expand_path("plugin/events", __dir__) + autoload :Index, File.expand_path("plugin/index", __dir__) + autoload :Installer, File.expand_path("plugin/installer", __dir__) + autoload :SourceList, File.expand_path("plugin/source_list", __dir__) + autoload :UnloadedSource, File.expand_path("plugin/unloaded_source", __dir__) class MalformattedPlugin < PluginError; end class UndefinedCommandError < PluginError; end @@ -17,6 +18,14 @@ class PluginInstallError < PluginError; end PLUGIN_FILE_NAME = "plugins.rb" + # Module-level flag set while .gemfile_install parses the Gemfile and + # consulted by .from_lock to substitute plugin sources with + # UnloadedSource. It relies on definitions being built one at a time in + # a single thread; if they are ever built concurrently or reentrantly, + # this needs to be replaced by explicit state passed down to the + # lockfile parser. + @gemfile_parse = false + module_function def reset! @@ -26,6 +35,7 @@ def reset! @commands = {} @hooks_by_event = Hash.new {|h, k| h[k] = [] } @loaded_plugin_names = [] + @index = nil end reset! @@ -40,7 +50,7 @@ def install(names, options) specs = Installer.new.install(names, options) - save_plugins names, specs + save_plugins specs.slice(*names) rescue PluginError specs_to_delete = specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) } specs_to_delete.each_value {|spec| Bundler.rm_rf(spec.full_gem_path) } @@ -100,29 +110,44 @@ def list # # @param [Pathname] gemfile path # @param [Proc] block that can be evaluated for (inline) Gemfile - def gemfile_install(gemfile = nil, &inline) - Bundler.settings.temporary(frozen: false, deployment: false) do - builder = DSL.new - if block_given? - builder.instance_eval(&inline) - else - builder.eval_gemfile(gemfile) - end - builder.check_primary_source_safety - definition = builder.to_definition(nil, true) - - return if definition.dependencies.empty? + def gemfile_install(gemfile = nil, lockfile = nil, unlock = {}, &inline) + @gemfile_parse = true + Bundler.configure + builder = DSL.new + if block_given? + builder.instance_eval(&inline) + else + builder.eval_gemfile(gemfile) + end + builder.check_primary_source_safety - plugins = definition.dependencies.map(&:name) - installed_specs = Installer.new.install_definition(definition) + plugins = builder.dependencies.map(&:name) + return if plugins.empty? - save_plugins plugins, installed_specs, builder.inferred_plugins + # skip the update if unlocking specific gems, but none of them are plugins + # declared in the Gemfile + if unlock.is_a?(Hash) && unlock[:gems] && !unlock[:gems].empty? && + (unlock[:gems] & plugins).empty? + unlock = {} end + + # resolve remotely when unlocking, so that plugins can be updated. + # Definition#initialize consumes the unlock hash, so this must be decided + # before building the definition. + updating = unlock == true || (unlock.is_a?(Hash) && !unlock.empty?) + + definition = builder.to_definition(lockfile, unlock) + + installed_specs = Installer.new.install_definition(definition, updating) + + save_plugins installed_specs.slice(*plugins), builder.inferred_plugins rescue RuntimeError => e unless e.is_a?(GemfileError) Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}" end raise + ensure + @gemfile_parse = false end # The index object used to store the details about the plugin @@ -183,12 +208,17 @@ def add_source(source, cls) # Checks if any plugin declares the source def source?(name) - !index.source_plugin(name.to_s).nil? + !!source_plugin(name) + end + + # Returns the plugin that handles the source +name+ if any + def source_plugin(name) + index.source_plugin(name.to_s) end # @return [Class] that handles the source. The class includes API::Source def source(name) - raise UnknownSourceError, "Source #{name} not found" unless source? name + raise UnknownSourceError, "Source #{name} not found" unless source_plugin(name) load_plugin(index.source_plugin(name)) unless @sources.key? name @@ -199,9 +229,14 @@ def source(name) # @return [API::Source] the instance of the class that handles the source # type passed in locked_opts def from_lock(locked_opts) + opts = locked_opts.merge("uri" => locked_opts["remote"]) + # when reading the lockfile while doing the plugin-install-from-gemfile phase, + # we need to ignore any plugin sources + return UnloadedSource.new(opts) if @gemfile_parse + src = source(locked_opts["type"]) - src.new(locked_opts.merge("uri" => locked_opts["remote"])) + src.new(opts) end # To be called via the API to register a hooks and corresponding block that @@ -237,7 +272,9 @@ def hook(event, *args, &arg_blk) # # @return [String, nil] installed path def installed?(plugin) - Index.new.installed?(plugin) + (path = index.installed?(plugin)) && + index.plugin_path(plugin).join(PLUGIN_FILE_NAME).file? && + path end # @return [true, false] whether the plugin is loaded @@ -247,19 +284,11 @@ def loaded?(plugin) # Post installation processing and registering with index # - # @param [Array] plugins list to be installed # @param [Hash] specs of plugins mapped to installation path (currently they # contain all the installed specs, including plugins) # @param [Array] names of inferred source plugins that can be ignored - def save_plugins(plugins, specs, optional_plugins = []) - plugins.each do |name| - spec = specs[name] - - # It's possible that the `plugin` found in the Gemfile don't appear in the specs. For instance when - # calling `BUNDLE_WITHOUT=default bundle install`, the plugins will not get installed. - next if spec.nil? - next if index.up_to_date?(spec) - + def save_plugins(specs, optional_plugins = []) + specs.each do |name, spec| save_plugin(name, spec, optional_plugins.include?(name)) end end @@ -284,6 +313,8 @@ def validate_plugin!(plugin_path) # # @raise [PluginInstallError] if validation or registration raises any error def save_plugin(name, spec, optional_plugin = false) + return if index.up_to_date?(spec) + validate_plugin! Pathname.new(spec.full_gem_path) installed = register_plugin(name, spec, optional_plugin) Bundler.ui.info "Installed plugin #{name}" if installed @@ -319,7 +350,7 @@ def register_plugin(name, spec, optional_plugin = false) raise MalformattedPlugin, "#{e.class}: #{e.message}" end - if optional_plugin && @sources.keys.any? {|s| source? s } + if optional_plugin && @sources.keys.any? {|s| source_plugin(s) } Bundler.rm_rf(path) false else diff --git a/bundler/lib/bundler/plugin/dsl.rb b/bundler/lib/bundler/plugin/dsl.rb index da751d1774ec..b2140934ff3e 100644 --- a/bundler/lib/bundler/plugin/dsl.rb +++ b/bundler/lib/bundler/plugin/dsl.rb @@ -5,12 +5,11 @@ module Plugin # Dsl to parse the Gemfile looking for plugins to install class DSL < Bundler::Dsl class PluginGemfileError < PluginError; end - alias_method :_gem, :gem # To use for plugin installation as gem # So that we don't have to override all there methods to dummy ones # explicitly. # They will be handled by method_missing - [:gemspec, :gem, :install_if, :platforms, :env].each {|m| undef_method m } + [:gemspec, :install_if, :platforms, :env].each {|m| undef_method m } # This lists the plugins that was added automatically and not specified by # the user. @@ -24,12 +23,11 @@ class PluginGemfileError < PluginError; end def initialize super - @sources = Plugin::SourceList.new @inferred_plugins = [] # The source plugins inferred from :type end - def plugin(name, *args) - _gem(name, *args) + def gem(*args) + # Ignore regular dependencies when doing the plugins-only pre-parse end def method_missing(name, *args) diff --git a/bundler/lib/bundler/plugin/installer.rb b/bundler/lib/bundler/plugin/installer.rb index 9be8b36843bb..c2a94177379b 100644 --- a/bundler/lib/bundler/plugin/installer.rb +++ b/bundler/lib/bundler/plugin/installer.rb @@ -32,12 +32,18 @@ def install(names, options) # # @param [Definition] definition object # @return [Hash] map of names to their specs they are installed with - def install_definition(definition) + def install_definition(definition, latest = false) def definition.lock(*); end - definition.remotely! + + if latest || definition.missing_specs? + definition.remotely! + else + definition.with_cache! + end + specs = definition.specs - install_from_specs specs + install_from_specs(specs) end private @@ -89,14 +95,14 @@ def install_rubygems(names, version, sources) end def install_all_sources(names, version, source_list, source = nil) - deps = names.map {|name| Dependency.new(name, version, { "source" => source }) } + deps = names.map {|name| Dependency.new(name, version, { "source" => source, "plugin" => true }) } Bundler.configure_gem_home_and_path(Plugin.root) Bundler.settings.temporary(deployment: false, frozen: false) do definition = Definition.new(nil, deps, source_list, true) - install_definition(definition) + install_definition(definition, true) end end @@ -110,6 +116,8 @@ def install_from_specs(specs) paths = {} specs.each do |spec| + next if spec.name == "bundler" + spec.source.download(spec) spec.source.install(spec) diff --git a/bundler/lib/bundler/plugin/installer/path.rb b/bundler/lib/bundler/plugin/installer/path.rb index 58c4924eb0cb..02ec62d41749 100644 --- a/bundler/lib/bundler/plugin/installer/path.rb +++ b/bundler/lib/bundler/plugin/installer/path.rb @@ -5,7 +5,7 @@ module Plugin class Installer class Path < Bundler::Source::Path def root - SharedHelpers.in_bundle? ? Bundler.root : Plugin.root + Plugin.root end def eql?(other) diff --git a/bundler/lib/bundler/plugin/unloaded_source.rb b/bundler/lib/bundler/plugin/unloaded_source.rb new file mode 100644 index 000000000000..bec91582e7e4 --- /dev/null +++ b/bundler/lib/bundler/plugin/unloaded_source.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + # Stands in for a source handled by a plugin that is not loaded yet, so + # that the lockfile can still be parsed during the plugin install pass. + class UnloadedSource + include API::Source + end + end +end diff --git a/spec/bundler/plugin_spec.rb b/spec/bundler/plugin_spec.rb index b379594c6f9a..c0cfdf6bf49f 100644 --- a/spec/bundler/plugin_spec.rb +++ b/spec/bundler/plugin_spec.rb @@ -115,6 +115,7 @@ let(:gemfile) { bundled_app_gemfile } before do + allow(Bundler).to receive(:configure) allow(Plugin::DSL).to receive(:new) { builder } allow(builder).to receive(:eval_gemfile).with(gemfile) allow(builder).to receive(:check_primary_source_safety) @@ -123,7 +124,7 @@ end it "doesn't calls installer without any plugins" do - allow(definition).to receive(:dependencies) { [] } + allow(builder).to receive(:dependencies) { [] } allow(installer).to receive(:install_definition).never subject.gemfile_install(gemfile) @@ -139,7 +140,7 @@ before do allow(index).to receive(:up_to_date?) { nil } - allow(definition).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0"), Bundler::Dependency.new("another-plugin", ">=0")] } + allow(builder).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0", "plugin" => true), Bundler::Dependency.new("another-plugin", ">=0", "plugin" => true)] } allow(installer).to receive(:install_definition) { plugin_specs } end @@ -187,18 +188,16 @@ end end - describe "#source?" do - it "returns true value for sources in index" do + describe "#source_plugin" do + it "returns the plugin for sources in index" do allow(index). - to receive(:command_plugin).with("foo-source") { "my-plugin" } - result = subject.command? "foo-source" - expect(result).to be_truthy + to receive(:source_plugin).with("foo-source") { "my-plugin" } + expect(subject.source_plugin("foo-source")).to eql "my-plugin" end - it "returns false value for source not in index" do - allow(index).to receive(:command_plugin).with("foo-source") { nil } - result = subject.command? "foo-source" - expect(result).to be_falsy + it "returns nil value for source not in index" do + allow(index).to receive(:source_plugin).with("foo-source") { nil } + expect(subject.source_plugin("foo-source")).to be_nil end end diff --git a/spec/bundler/source_list_spec.rb b/spec/bundler/source_list_spec.rb index 61bd99b063b4..7a8e885b2e1b 100644 --- a/spec/bundler/source_list_spec.rb +++ b/spec/bundler/source_list_spec.rb @@ -6,7 +6,7 @@ stub_const "ASourcePlugin", Class.new(Bundler::Plugin::API) ASourcePlugin.source "new_source" - allow(Bundler::Plugin).to receive(:source?).with("new_source").and_return(true) + allow(Bundler::Plugin).to receive(:source_plugin).with("new_source").and_return("new_source") end subject(:source_list) { Bundler::SourceList.new } diff --git a/spec/commands/update_spec.rb b/spec/commands/update_spec.rb index 03a3786d80a2..f100bdfb9edb 100644 --- a/spec/commands/update_spec.rb +++ b/spec/commands/update_spec.rb @@ -744,6 +744,23 @@ expect(the_bundle).not_to include_gems "myrack 1.2" end + it "doesn't fail when a gem was added to the group but is not in the lockfile yet" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", :group => :development + G + + gemfile <<-G + source "https://gem.repo2" + gem "activesupport", :group => :development + gem "myrack", :group => :development + G + + bundle "update --group development" + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + context "when conservatively updating a group with non-group sub-deps" do it "should update only specified group gems" do install_gemfile <<-G diff --git a/spec/other/major_deprecation_spec.rb b/spec/other/major_deprecation_spec.rb index ab7589d698d2..55cb1996a066 100644 --- a/spec/other/major_deprecation_spec.rb +++ b/spec/other/major_deprecation_spec.rb @@ -88,9 +88,9 @@ end end - describe "bundle update --quiet" do + describe "bundle update --all --quiet" do it "does not print any deprecations" do - bundle :update, quiet: true, raise_on_error: false + bundle :update, all: true, quiet: true, raise_on_error: false expect(deprecations).to be_empty end end diff --git a/spec/plugins/install_spec.rb b/spec/plugins/install_spec.rb index dcacf764bef6..42ef686fb032 100644 --- a/spec/plugins/install_spec.rb +++ b/spec/plugins/install_spec.rb @@ -64,6 +64,21 @@ plugin_should_be_installed("foo", "kung-foo") end + it "installs a plugin with dependencies, without registering them as plugins" do + update_repo2 do + build_gem "foo-dep" + build_plugin "foo-with-dep" do |s| + s.add_dependency "foo-dep" + end + end + + bundle "plugin install foo-with-dep --source https://gem.repo2" + + expect(out).to include("Installed plugin foo-with-dep") + plugin_should_be_installed("foo-with-dep") + plugin_should_not_be_installed("foo-dep") + end + it "uses the same version for multiple plugins" do update_repo2 do build_plugin "foo", "1.1" @@ -265,6 +280,61 @@ def exec(command, args) plugin_should_be_installed("foo") end + it "installs plugins with dependencies, without registering them as plugins" do + update_repo2 do + build_gem "foo-dep" + build_plugin "foo-with-dep" do |s| + s.add_dependency "foo-dep" + end + end + + install_gemfile <<-G + source 'https://gem.repo2' + plugin 'foo-with-dep' + G + + expect(out).to include("Installed plugin foo-with-dep") + plugin_should_be_installed("foo-with-dep") + plugin_should_not_be_installed("foo-dep") + end + + it "updates a plugin with bundle update" do + install_gemfile <<-G + source 'https://gem.repo2' + plugin 'foo' + G + + plugin_should_be_installed_with_version("foo", "1.0") + + update_repo2 do + build_plugin "foo", "1.1" + end + + bundle "update foo" + + plugin_should_be_installed_with_version("foo", "1.1") + end + + it "updates a plugin that is not installed locally" do + install_gemfile <<-G + source 'https://gem.repo2' + plugin 'foo' + G + + plugin_should_be_installed_with_version("foo", "1.0") + + update_repo2 do + build_plugin "foo", "1.1" + end + + # simulate a machine where the plugin has not been installed yet + FileUtils.rm_rf bundled_app(".bundle/plugin") + + bundle "update foo" + + plugin_should_be_installed_with_version("foo", "1.1") + end + it "overrides the index with the new plugin version" do gemfile <<-G source 'https://gem.repo2' @@ -286,7 +356,7 @@ def exec(command, args) bundle "install" - expected = local_plugin_gem("foo-2.0.0", "lib").to_s + expected = system_gem_path("gems", "foo-2.0.0", "lib").to_s expect(Bundler::Plugin.index.load_paths("foo")).to eq([expected]) end @@ -299,7 +369,7 @@ def exec(command, args) bundle "install", env: { "BUNDLE_WITHOUT" => "default" } - expect(out).to include("Bundle complete! 1 Gemfile dependency, 0 gems now installed.") + expect(out).to include("Bundle complete! 2 Gemfile dependencies, 0 gems now installed.") end it "accepts plugin version" do @@ -321,6 +391,157 @@ def exec(command, args) expect(out).to include("Bundle complete!") end + it "installs plugins in included groups" do + gemfile <<-G + source 'https://gem.repo2' + group :development do + plugin 'foo' + end + gem 'myrack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installed plugin foo") + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed("foo") + end + + it "does not install plugins in excluded groups" do + gemfile <<-G + source 'https://gem.repo2' + group :development do + plugin 'foo' + end + gem 'myrack', "1.0.0" + G + + bundle "config set --local without development" + bundle "install" + + expect(out).not_to include("Installed plugin foo") + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_not_be_installed("foo") + end + + it "upgrade plugins version listed in gemfile" do + update_repo2 do + build_plugin "foo", "1.4.0" + build_plugin "foo", "1.5.0" + end + + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo', "1.4.0" + gem 'myrack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installing foo 1.4.0") + expect(out).to include("Installed plugin foo") + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed_with_version("foo", "1.4.0") + + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo', "1.5.0" + gem 'myrack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installing foo 1.5.0") + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed_with_version("foo", "1.5.0") + end + + it "downgrade plugins version listed in gemfile" do + update_repo2 do + build_plugin "foo", "1.4.0" + build_plugin "foo", "1.5.0" + end + + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo', "1.5.0" + gem 'myrack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installing foo 1.5.0") + expect(out).to include("Installed plugin foo") + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed_with_version("foo", "1.5.0") + + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo', "1.4.0" + gem 'myrack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installing foo 1.4.0") + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed_with_version("foo", "1.4.0") + end + + it "install only plugins not installed yet listed in gemfile" do + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo' + gem 'myrack', "1.0.0" + G + + 2.times { bundle "install" } + + expect(out).to_not include("Fetching gem metadata") + expect(out).to_not include("Fetching foo") + expect(out).to_not include("Installed plugin foo") + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed("foo") + + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo' + plugin 'kung-foo' + gem 'myrack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installing kung-foo") + expect(out).to include("Installed plugin kung-foo") + + expect(out).to_not include("Fetching foo") + expect(out).to_not include("Installed plugin foo") + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed("foo") + plugin_should_be_installed("kung-foo") + end + it "accepts git sources" do build_git "ga-plugin" do |s| s.write "plugins.rb" @@ -368,22 +589,48 @@ def exec(command, args) it "installs plugins" do install_gemfile <<-G source 'https://gem.repo2' + plugin 'foo' gem 'myrack', "1.0.0" G + expect(out).to include("Installed plugin foo") + bundle_config "deployment true" + bundle "install" + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("myrack 1.0.0") + plugin_should_be_installed("foo") + end + end + + context "plugins in the lockfile" do + it "includes plugins as dependencies for a new lockfile" do install_gemfile <<-G source 'https://gem.repo2' plugin 'foo' gem 'myrack', "1.0.0" G - expect(out).to include("Installed plugin foo") + expect(the_bundle).to include_gems("foo 1.0.0") + end - expect(out).to include("Bundle complete!") + it "includes plugins as dependencies for an existing lockfile" do + install_gemfile <<-G + source 'https://gem.repo2' + gem 'myrack', "1.0.0" + G - expect(the_bundle).to include_gems("myrack 1.0.0") - plugin_should_be_installed("foo") + expect(the_bundle).not_to include_gems("foo 1.0.0") + + install_gemfile <<-G + source 'https://gem.repo2' + plugin 'foo' + gem 'myrack', "1.0.0" + G + + expect(the_bundle).to include_gems("foo 1.0.0") end end end @@ -400,7 +647,9 @@ def exec(command, args) RUBY ruby code, artifice: "compact_index", env: { "BUNDLER_VERSION" => Bundler::VERSION } - expect(local_plugin_gem("foo-1.0", "plugins.rb")).to exist + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + plugin_should_be_installed("foo") end end diff --git a/spec/plugins/source/example_spec.rb b/spec/plugins/source/example_spec.rb index 4cd4a1a9318f..4332f2179870 100644 --- a/spec/plugins/source/example_spec.rb +++ b/spec/plugins/source/example_spec.rb @@ -72,6 +72,7 @@ def install(spec, opts) checksums = checksums_section_when_enabled do |c| c.no_checksum "a-path-gem", "1.0" + c.checksum gem_repo2, "bundler-source-mpath", "1.0" end expect(lockfile).to eq <<~G @@ -84,12 +85,14 @@ def install(spec, opts) GEM remote: https://gem.repo2/ specs: + bundler-source-mpath (1.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES a-path-gem! + bundler-source-mpath #{checksums} BUNDLED WITH #{Bundler::VERSION} @@ -338,6 +341,7 @@ def installed? bundle "install" checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "bundler-source-gitp", "1.0" c.no_checksum "ma-gitp-gem", "1.0" end @@ -352,11 +356,13 @@ def installed? GEM remote: https://gem.repo2/ specs: + bundler-source-gitp (1.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES + bundler-source-gitp ma-gitp-gem! #{checksums} BUNDLED WITH diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 5a3c38a4db36..efd56b44f68f 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -211,6 +211,7 @@ def indent(string, padding = 4, indent_character = " ") RSpec::Matchers.alias_matcher :include_gem, :include_gems def plugin_should_be_installed(*names) + Bundler::Plugin.instance_variable_set(:@index, nil) names.each do |name| expect(Bundler::Plugin).to be_installed(name) path = Pathname.new(Bundler::Plugin.installed?(name)) @@ -218,7 +219,17 @@ def plugin_should_be_installed(*names) end end + def plugin_should_be_installed_with_version(name, version) + Bundler::Plugin.instance_variable_set(:@index, nil) + expect(Bundler::Plugin).to be_installed(name) + path = Pathname.new(Bundler::Plugin.installed?(name)) + + expect(File.basename(path)).to eq("#{name}-#{version}") + expect(path + "plugins.rb").to exist + end + def plugin_should_not_be_installed(*names) + Bundler::Plugin.instance_variable_set(:@index, nil) names.each do |name| expect(Bundler::Plugin).not_to be_installed(name) end