summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHomu <homu@barosl.com>2016-07-05 23:44:57 +0900
committerHomu <homu@barosl.com>2016-07-05 23:44:57 +0900
commitcc4df62a4707281fc657101a93710c63ed957a70 (patch)
tree51fc5cfce170b2f0743402f304f723e6aa429434
parent2439ac84cfd731aaee9ebaca284cb077cf601969 (diff)
parent57e817290335570abc1aacdf778e255477403302 (diff)
downloadbundler-cc4df62a4707281fc657101a93710c63ed957a70.tar.gz
Auto merge of #4674 - asutoshpalai:plugin, r=segiddins
[Plugin] Source plugins Adds source plugin. This is in continuation of #4608.
-rw-r--r--lib/bundler.rb4
-rw-r--r--lib/bundler/cli/update.rb2
-rw-r--r--lib/bundler/dsl.rb21
-rw-r--r--lib/bundler/lockfile_parser.rb22
-rw-r--r--lib/bundler/plugin.rb125
-rw-r--r--lib/bundler/plugin/api.rb30
-rw-r--r--lib/bundler/plugin/api/source.rb293
-rw-r--r--lib/bundler/plugin/dsl.rb26
-rw-r--r--lib/bundler/plugin/index.rb53
-rw-r--r--lib/bundler/plugin/installer.rb16
-rw-r--r--lib/bundler/plugin/source_list.rb4
-rw-r--r--lib/bundler/rubygems_ext.rb2
-rw-r--r--lib/bundler/source/git.rb6
-rw-r--r--lib/bundler/source/path.rb26
-rw-r--r--lib/bundler/source/path/installer.rb50
-rw-r--r--lib/bundler/source_list.rb21
-rw-r--r--lib/bundler/yaml_serializer.rb45
-rw-r--r--spec/bundler/lockfile_parser_spec.rb4
-rw-r--r--spec/bundler/plugin/api/source_spec.rb83
-rw-r--r--spec/bundler/plugin/api_spec.rb46
-rw-r--r--spec/bundler/plugin/dsl_spec.rb17
-rw-r--r--spec/bundler/plugin/index_spec.rb104
-rw-r--r--spec/bundler/plugin/installer_spec.rb72
-rw-r--r--spec/bundler/plugin_spec.rb121
-rw-r--r--spec/bundler/source_list_spec.rb69
-rw-r--r--spec/bundler/yaml_serializer_spec.rb57
-rw-r--r--spec/plugins/command_spec.rb (renamed from spec/plugins/command.rb)0
-rw-r--r--spec/plugins/install_spec.rb (renamed from spec/plugins/install.rb)32
-rw-r--r--spec/plugins/source/example_spec.rb446
-rw-r--r--spec/plugins/source_spec.rb129
-rw-r--r--spec/support/builders.rb5
-rw-r--r--spec/support/matchers.rb9
32 files changed, 1751 insertions, 189 deletions
diff --git a/lib/bundler.rb b/lib/bundler.rb
index a2260e0142..0ba58e518c 100644
--- a/lib/bundler.rb
+++ b/lib/bundler.rb
@@ -3,14 +3,15 @@ require "fileutils"
require "pathname"
require "rbconfig"
require "thread"
+require "bundler/errors"
require "bundler/environment_preserver"
require "bundler/gem_remote_fetcher"
+require "bundler/plugin"
require "bundler/rubygems_ext"
require "bundler/rubygems_integration"
require "bundler/version"
require "bundler/constants"
require "bundler/current_ruby"
-require "bundler/errors"
module Bundler
environment_preserver = EnvironmentPreserver.new(ENV, %w(PATH GEM_PATH))
@@ -38,7 +39,6 @@ module Bundler
autoload :MatchPlatform, "bundler/match_platform"
autoload :Mirror, "bundler/mirror"
autoload :Mirrors, "bundler/mirror"
- autoload :Plugin, "bundler/plugin"
autoload :RemoteSpecification, "bundler/remote_specification"
autoload :Resolver, "bundler/resolver"
autoload :Retry, "bundler/retry"
diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb
index 33b0557fec..bef62f3b78 100644
--- a/lib/bundler/cli/update.rb
+++ b/lib/bundler/cli/update.rb
@@ -10,6 +10,8 @@ module Bundler
def run
Bundler.ui.level = "error" if options[:quiet]
+ Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
+
sources = Array(options[:source])
groups = Array(options[:group]).map(&:to_sym)
diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb
index 358784a9de..04ef641b8e 100644
--- a/lib/bundler/dsl.rb
+++ b/lib/bundler/dsl.rb
@@ -125,11 +125,26 @@ module Bundler
@dependencies << dep
end
- def source(source, &blk)
- source = normalize_source(source)
- if block_given?
+ def source(source, *args, &blk)
+ options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ options = normalize_hash(options)
+ if options.key?("type")
+ options["type"] = options["type"].to_s
+ unless Plugin.source?(options["type"])
+ raise "No sources available for #{options["type"]}"
+ end
+
+ unless block_given?
+ raise InvalidOption, "You need to pass a block to #source with :type option"
+ end
+
+ source_opts = options.merge("uri" => source)
+ with_source(@sources.add_plugin_source(options["type"], source_opts), &blk)
+ elsif block_given?
+ source = normalize_source(source)
with_source(@sources.add_rubygems_source("remotes" => source), &blk)
else
+ source = normalize_source(source)
check_primary_source_safety(@sources)
@sources.add_rubygems_remote(source)
end
diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb
index a57dbcac1c..d5b2844079 100644
--- a/lib/bundler/lockfile_parser.rb
+++ b/lib/bundler/lockfile_parser.rb
@@ -22,9 +22,10 @@ module Bundler
GIT = "GIT".freeze
GEM = "GEM".freeze
PATH = "PATH".freeze
+ PLUGIN = "PLUGIN SOURCE".freeze
SPECS = " specs:".freeze
OPTIONS = /^ ([a-z]+): (.*)$/i
- SOURCE = [GIT, GEM, PATH].freeze
+ SOURCE = [GIT, GEM, PATH, PLUGIN].freeze
SECTIONS_BY_VERSION_INTRODUCED = {
# The strings have to be dup'ed for old RG on Ruby 2.3+
@@ -32,6 +33,7 @@ module Bundler
Gem::Version.create("1.0".dup) => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze,
Gem::Version.create("1.10".dup) => [BUNDLED].freeze,
Gem::Version.create("1.12".dup) => [RUBY].freeze,
+ Gem::Version.create("1.13".dup) => [PLUGIN].freeze,
}.freeze
KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten.freeze
@@ -118,17 +120,14 @@ module Bundler
private
TYPES = {
- GIT => Bundler::Source::Git,
- GEM => Bundler::Source::Rubygems,
- PATH => Bundler::Source::Path,
+ GIT => Bundler::Source::Git,
+ GEM => Bundler::Source::Rubygems,
+ PATH => Bundler::Source::Path,
+ PLUGIN => Bundler::Plugin,
}.freeze
def parse_source(line)
case line
- when GIT, GEM, PATH
- @current_source = nil
- @opts = {}
- @type = line
when SPECS
case @type
when PATH
@@ -147,6 +146,9 @@ module Bundler
@rubygems_aggregate.add_remote(url)
end
@current_source = @rubygems_aggregate
+ when PLUGIN
+ @current_source = Plugin.source_from_lock(@opts)
+ @sources << @current_source
end
when OPTIONS
value = $2
@@ -161,6 +163,10 @@ module Bundler
else
@opts[key] = value
end
+ when *SOURCE
+ @current_source = nil
+ @opts = {}
+ @type = line
else
parse_spec(line)
end
diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb
index 9aabd73a42..f5366d2a13 100644
--- a/lib/bundler/plugin.rb
+++ b/lib/bundler/plugin.rb
@@ -10,45 +10,52 @@ module Bundler
class MalformattedPlugin < PluginError; end
class UndefinedCommandError < PluginError; end
+ class UnknownSourceError < PluginError; end
PLUGIN_FILE_NAME = "plugins.rb".freeze
module_function
@commands = {}
+ @sources = {}
# Installs a new plugin by the given name
#
# @param [Array<String>] names the name of plugin to be installed
- # @param [Hash] options various parameters as described in description
- # @option options [String] :source rubygems source to fetch the plugin gem from
- # @option options [String] :version (optional) the version of the plugin to install
+ # @param [Hash] options various parameters as described in description.
+ # Refer to cli/plugin for available options
def install(names, options)
- paths = Installer.new.install(names, options)
+ specs = Installer.new.install(names, options)
- save_plugins paths
+ save_plugins names, specs
rescue PluginError => e
- paths.values.map {|path| Bundler.rm_rf(path) } if paths
- Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n #{e.backtrace.join("\n ")}"
+ specs.values.map {|spec| Bundler.rm_rf(spec.full_gem_path) } if specs
+ Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n #{e.backtrace[0]}"
end
# Evaluates the Gemfile with a limited DSL and installs the plugins
# specified by plugin method
#
# @param [Pathname] gemfile path
+ # @param [Proc] block that can be evaluated for (inline) Gemfile
def gemfile_install(gemfile = nil, &inline)
+ builder = DSL.new
if block_given?
- builder = DSL.new
builder.instance_eval(&inline)
- definition = builder.to_definition(nil, true)
else
- definition = DSL.evaluate(gemfile, nil, {})
+ builder.eval_gemfile(gemfile)
end
- return unless definition.dependencies.any?
+ definition = builder.to_definition(nil, true)
- plugins = Installer.new.install_definition(definition)
+ return if definition.dependencies.empty?
- save_plugins plugins
+ plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p }
+ installed_specs = Installer.new.install_definition(definition)
+
+ save_plugins plugins, installed_specs, builder.inferred_plugins
+ rescue => e
+ Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}"
+ raise
end
# The index object used to store the details about the plugin
@@ -71,7 +78,7 @@ module Bundler
@commands[command] = cls
end
- # Checks if any plugins handles the command
+ # Checks if any plugin handles the command
def command?(command)
!index.command_plugin(command).nil?
end
@@ -79,13 +86,41 @@ module Bundler
# To be called from Cli class to pass the command and argument to
# approriate plugin class
def exec_command(command, args)
- raise UndefinedCommandError, "Command #{command} not found" unless command? command
+ raise UndefinedCommandError, "Command `#{command}` not found" unless command? command
load_plugin index.command_plugin(command) unless @commands.key? command
@commands[command].new.exec(command, args)
end
+ # To be called via the API to register to handle a source plugin
+ def add_source(source, cls)
+ @sources[source] = cls
+ end
+
+ # Checks if any plugin declares the source
+ def source?(name)
+ !index.source_plugin(name.to_s).nil?
+ end
+
+ # @return [Class] that handles the source. The calss includes API::Source
+ def source(name)
+ raise UnknownSourceError, "Source #{name} not found" unless source? name
+
+ load_plugin(index.source_plugin(name)) unless @sources.key? name
+
+ @sources[name]
+ end
+
+ # @param [Hash] The options that are present in the lock file
+ # @return [API::Source] the instance of the class that handles the source
+ # type passed in locked_opts
+ def source_from_lock(locked_opts)
+ src = source(locked_opts["type"])
+
+ src.new(locked_opts.merge("uri" => locked_opts["remote"]))
+ end
+
# currently only intended for specs
#
# @return [String, nil] installed path
@@ -95,13 +130,16 @@ module Bundler
# Post installation processing and registering with index
#
- # @param [Hash] plugins mapped to their installtion path
- def save_plugins(plugins)
- plugins.each do |name, path|
- path = Pathname.new path
- validate_plugin! path
- register_plugin name, path
- Bundler.ui.info "Installed plugin #{name}"
+ # @param [Array<String>] 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<String>] names of inferred source plugins that can be ignored
+ def save_plugins(plugins, specs, optional_plugins = [])
+ plugins.each do |name|
+ spec = specs[name]
+ validate_plugin! Pathname.new(spec.full_gem_path)
+ installed = register_plugin name, spec, optional_plugins.include?(name)
+ Bundler.ui.info "Installed plugin #{name}" if installed
end
end
@@ -110,21 +148,31 @@ module Bundler
# At present it only checks whether it contains plugins.rb file
#
# @param [Pathname] plugin_path the path plugin is installed at
- # @raise [Error] if plugins.rb file is not found
+ # @raise [MalformattedPlugin] if plugins.rb file is not found
def validate_plugin!(plugin_path)
plugin_file = plugin_path.join(PLUGIN_FILE_NAME)
- raise MalformattedPlugin, "#{PLUGIN_FILE_NAME} was not found in the plugin!" unless plugin_file.file?
+ raise MalformattedPlugin, "#{PLUGIN_FILE_NAME} was not found in the plugin." unless plugin_file.file?
end
# Runs the plugins.rb file in an isolated namespace, records the plugin
# actions it registers for and then passes the data to index to be stored.
#
# @param [String] name the name of the plugin
- # @param [Pathname] path the path where the plugin is installed at
- def register_plugin(name, path)
+ # @param [Specification] spec of installed plugin
+ # @param [Boolean] optional_plugin, removed if there is conflict with any
+ # other plugin (used for default source plugins)
+ #
+ # @raise [MalformattedPlugin] if plugins.rb raises any error
+ def register_plugin(name, spec, optional_plugin = false)
commands = @commands
+ sources = @sources
@commands = {}
+ @sources = {}
+
+ load_paths = spec.load_paths
+ add_to_load_path(load_paths)
+ path = Pathname.new spec.full_gem_path
begin
load path.join(PLUGIN_FILE_NAME), true
@@ -132,9 +180,16 @@ module Bundler
raise MalformattedPlugin, "#{e.class}: #{e.message}"
end
- index.register_plugin name, path.to_s, @commands.keys
+ if optional_plugin && @sources.keys.any? {|s| source? s }
+ Bundler.rm_rf(path)
+ false
+ else
+ index.register_plugin name, path.to_s, load_paths, @commands.keys, @sources.keys
+ true
+ end
ensure
@commands = commands
+ @sources = sources
end
# Executes the plugins.rb file
@@ -146,11 +201,25 @@ module Bundler
# done to avoid conflicts
path = index.plugin_path(name)
+ add_to_load_path(index.load_paths(name))
+
load path.join(PLUGIN_FILE_NAME)
+ rescue => e
+ Bundler.ui.error "Failed loading plugin #{name}: #{e.message}"
+ raise
+ end
+
+ def add_to_load_path(load_paths)
+ if insert_index = Bundler.rubygems.load_path_insert_index
+ $LOAD_PATH.insert(insert_index, *load_paths)
+ else
+ $LOAD_PATH.unshift(*load_paths)
+ end
end
class << self
- private :load_plugin, :register_plugin, :save_plugins, :validate_plugin!
+ private :load_plugin, :register_plugin, :save_plugins, :validate_plugin!,
+ :add_to_load_path
end
end
end
diff --git a/lib/bundler/plugin/api.rb b/lib/bundler/plugin/api.rb
index 9631446d9b..9ff4b7385b 100644
--- a/lib/bundler/plugin/api.rb
+++ b/lib/bundler/plugin/api.rb
@@ -23,19 +23,31 @@ module Bundler
# and hooks).
module Plugin
class API
+ autoload :Source, "bundler/plugin/api/source"
# The plugins should declare that they handle a command through this helper.
#
# @param [String] command being handled by them
- # @param [Class] (optional) class that shall handle the command. If not
+ # @param [Class] (optional) class that handles the command. If not
# provided, the `self` class will be used.
def self.command(command, cls = self)
Plugin.add_command command, cls
end
- # The cache dir to be used by the plugins for persistance storage
+ # The plugins should declare that they provide a installation source
+ # through this helper.
+ #
+ # @param [String] the source type they provide
+ # @param [Class] (optional) class that handles the source. If not
+ # provided, the `self` class will be used.
+ def self.source(source, cls = self)
+ cls.send :include, Bundler::Plugin::API::Source
+ Plugin.add_source source, cls
+ end
+
+ # The cache dir to be used by the plugins for storage
#
# @return [Pathname] path of the cache dir
- def cache
+ def cache_dir
Plugin.cache.join("plugins")
end
@@ -48,8 +60,16 @@ module Bundler
end
def method_missing(name, *args, &blk)
- super unless Bundler.respond_to?(name)
- Bundler.send(name, *args, &blk)
+ return Bundler.send(name, *args, &blk) if Bundler.respond_to?(name)
+
+ return SharedHelpers.send(name, *args, &blk) if SharedHelpers.respond_to?(name)
+
+ super
+ end
+
+ def respond_to_missing?(name, include_private = false)
+ SharedHelpers.respond_to?(name, include_private) ||
+ Bundler.respond_to?(name, include_private) || super
end
end
end
diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb
new file mode 100644
index 0000000000..78514563f7
--- /dev/null
+++ b/lib/bundler/plugin/api/source.rb
@@ -0,0 +1,293 @@
+# frozen_string_literal: true
+require "uri"
+require "digest/sha1"
+
+module Bundler
+ module Plugin
+ class API
+ # This class provides the base to build source plugins
+ # All the method here are require to build a source plugin (except
+ # `uri_hash`, `gem_install_dir`; they are helpers).
+ #
+ # Defaults for methods, where ever possible are provided which is
+ # expected to work. But, all source plugins have to override
+ # `fetch_gemspec_files` and `install`. Defaults are also not provided for
+ # `remote!`, `cache!` and `unlock!`.
+ #
+ # The defaults shall work for most situations but nevertheless they can
+ # be (preferably should be) overridden as per the plugins' needs safely
+ # (as long as they behave as expected).
+ # On overriding `initialize` you should call super first.
+ #
+ # If required plugin should override `hash`, `==` and `eql?` methods to be
+ # able to match objects representing same sources, but may be created in
+ # different situation (like form gemfile and lockfile). The default ones
+ # checks only for class and uri, but elaborate source plugins may need
+ # more comparisons (e.g. git checking on branch or tag).
+ #
+ # @!attribute [r] uri
+ # @return [String] the remote specified with `source` block in Gemfile
+ #
+ # @!attribute [r] options
+ # @return [String] options passed during initialization (either from
+ # lockfile or Gemfile)
+ #
+ # @!attribute [r] name
+ # @return [String] name that can be used to uniquely identify a source
+ #
+ # @!attribute [rw] dependency_names
+ # @return [Array<String>] Names of dependencies that the source should
+ # try to resolve. It is not necessary to use this list intenally. This
+ # is present to be compatible with `Definition` and is used by
+ # rubygems source.
+ module Source
+ attr_reader :uri, :options, :name
+ attr_accessor :dependency_names
+
+ def initialize(opts)
+ @options = opts
+ @dependency_names = []
+ @uri = opts["uri"]
+ @type = opts["type"]
+ @name = opts["name"] || "#{@type} at #{@uri}"
+ end
+
+ # This is used by the default `spec` method to constructs the
+ # Specification objects for the gems and versions that can be installed
+ # by this source plugin.
+ #
+ # Note: If the spec method is overridden, this function is not necessary
+ #
+ # @return [Array<String>] paths of the gemspec files for gems that can
+ # be installed
+ def fetch_gemspec_files
+ []
+ end
+
+ # Options to be saved in the lockfile so that the source plugin is able
+ # to check out same version of gem later.
+ #
+ # There options are passed when the source plugin is created from the
+ # lock file.
+ #
+ # @return [Hash]
+ def options_to_lock
+ {}
+ end
+
+ # Install the gem specified by the spec at appropriate path.
+ # `install_path` provides a sufficient default, if the source can only
+ # satisfy one gem, but is not binding.
+ #
+ # @return [String] post installation message (if any)
+ def install(spec, opts)
+ raise MalformattedPlugin, "Source plugins need to override the install method."
+ end
+
+ # It builds extensions, generates bins and installs them for the spec
+ # provided.
+ #
+ # It depends on `spec.loaded_from` to get full_gem_path. The source
+ # plugins should set that.
+ #
+ # It should be called in `install` after the plugin is done placing the
+ # gem at correct install location.
+ #
+ # It also runs Gem hooks `post_install`, `post_build` and `post_install`
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def post_install(spec, disable_exts = false)
+ opts = { :env_shebang => false, :disable_extensions => disable_exts }
+ installer = Bundler::Source::Path::Installer.new(spec, opts)
+ installer.post_install
+ end
+
+ # A default installation path to install a single gem. If the source
+ # servers multiple gems, it's not of much use and the source should one
+ # of its own.
+ def install_path
+ @install_path ||=
+ begin
+ base_name = File.basename(URI.parse(uri).normalize.path)
+
+ gem_install_dir.join("#{base_name}-#{uri_hash[0..11]}")
+ end
+ end
+
+ # Parses the gemspec files to find the specs for the gems that can be
+ # satisfied by the source.
+ #
+ # Few important points to keep in mind:
+ # - If the gems are not installed then it shall return specs for all
+ # the gems it can satisfy
+ # - If gem is installed (that is to be detected by the plugin itself)
+ # then it shall return at least the specs that are installed.
+ # - The `loaded_from` for each of the specs shall be correct (it is
+ # used to find the load path)
+ #
+ # @return [Bundler::Index] index containing the specs
+ def specs
+ files = fetch_gemspec_files
+
+ Bundler::Index.build do |index|
+ files.each do |file|
+ next unless spec = Bundler.load_gemspec(file)
+ Bundler.rubygems.set_installed_by_version(spec)
+
+ spec.source = self
+ Bundler.rubygems.validate(spec)
+
+ index << spec
+ end
+ end
+ end
+
+ # Set internal representation to fetch the gems/specs from remote.
+ #
+ # When this is called, the source should try to fetch the specs and
+ # install from remote path.
+ def remote!
+ end
+
+ # Set internal representation to fetch the gems/specs from app cache.
+ #
+ # When this is called, the source should try to fetch the specs and
+ # install from the path provided by `app_cache_path`.
+ def cached!
+ end
+
+ # This is called to update the spec and installation.
+ #
+ # If the source plugin is loaded from lockfile or otherwise, it shall
+ # refresh the cache/specs (e.g. git sources can make a fresh clone).
+ def unlock!
+ end
+
+ # Name of directory where plugin the is expected to cache the gems when
+ # #cache is called.
+ #
+ # Also this name is matched against the directories in cache for pruning
+ #
+ # This is used by `app_cache_path`
+ def app_cache_dirname
+ base_name = File.basename(URI.parse(uri).normalize.path)
+ "#{base_name}-#{uri_hash}"
+ end
+
+ # This method is called while caching to save copy of the gems that the
+ # source can resolve to path provided by `app_cache_app`so that they can
+ # be reinstalled from the cache without querying the remote (i.e. an
+ # alternative to remote)
+ #
+ # This is stored with the app and source plugins should try to provide
+ # specs and install only from this cache when `cached!` is called.
+ #
+ # This cache is different from the internal caching that can be done
+ # at sub paths of `cache_path` (from API). This can be though as caching
+ # by bundler.
+ def cache(spec, custom_path = nil)
+ new_cache_path = app_cache_path(custom_path)
+
+ FileUtils.rm_rf(new_cache_path)
+ FileUtils.cp_r(install_path, new_cache_path)
+ FileUtils.touch(app_cache_path.join(".bundlecache"))
+ end
+
+ # This shall check if two source object represent the same source.
+ #
+ # The comparison shall take place only on the attribute that can be
+ # inferred from the options passed from Gemfile and not on attibutes
+ # that are used to pin down the gem to specific version (e.g. Git
+ # sources should compare on branch and tag but not on commit hash)
+ #
+ # The sources objects are constructed from Gemfile as well as from
+ # lockfile. To converge the sources, it is necessary that they match.
+ #
+ # The same applies for `eql?` and `hash`
+ def ==(other)
+ other.is_a?(self.class) && uri == other.uri
+ end
+
+ # When overriding `eql?` please preserve the behaviour as mentioned in
+ # docstring for `==` method.
+ alias_method :eql?, :==
+
+ # When overriding `hash` please preserve the behaviour as mentioned in
+ # docstring for `==` method, i.e. two methods equal by above comparison
+ # should have same hash.
+ def hash
+ [self.class, uri].hash
+ end
+
+ # A helper method, not necessary if not used internally.
+ def installed?
+ File.directory?(install_path)
+ end
+
+ # The full path where the plugin should cache the gem so that it can be
+ # installed latter.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def app_cache_path(custom_path = nil)
+ @app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname)
+ end
+
+ # Used by definition.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def unmet_deps
+ specs.unmet_dependency_names
+ end
+
+ # Note: Do not override if you don't know what you are doing.
+ def can_lock?(spec)
+ spec.source == self
+ end
+
+ # Generates the content to be entered into the lockfile.
+ # Saves type and remote and also calls to `options_to_lock`.
+ #
+ # Plugin should use `options_to_lock` to save information in lockfile
+ # and not override this.
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def to_lock
+ out = String.new("#{LockfileParser::PLUGIN}\n")
+ out << " remote: #{@uri}\n"
+ out << " type: #{@type}\n"
+ options_to_lock.each do |opt, value|
+ out << " #{opt}: #{value}\n"
+ end
+ out << " specs:\n"
+ end
+
+ def to_s
+ "plugin source for #{options[:type]} with uri #{uri}"
+ end
+
+ # Note: Do not override if you don't know what you are doing.
+ def include?(other)
+ other == self
+ end
+
+ def uri_hash
+ Digest::SHA1.hexdigest(uri)
+ end
+
+ # Note: Do not override if you don't know what you are doing.
+ def gem_install_dir
+ Bundler.install_path
+ end
+
+ # It is used to obtain the full_gem_path.
+ #
+ # spec's loaded_from path is expanded against this to get full_gem_path
+ #
+ # Note: Do not override if you don't know what you are doing.
+ def root
+ Bundler.root
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bundler/plugin/dsl.rb b/lib/bundler/plugin/dsl.rb
index f65054f014..4bfc8437e0 100644
--- a/lib/bundler/plugin/dsl.rb
+++ b/lib/bundler/plugin/dsl.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module Bundler
- # Dsl to parse the Gemfile looking for plugins to install
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
@@ -12,9 +12,20 @@ module Bundler
# They will be handled by method_missing
[:gemspec, :gem, :path, :install_if, :platforms, :env].each {|m| undef_method m }
+ # This lists the plugins that was added automatically and not specified by
+ # the user.
+ #
+ # When we encounter :type attribute with a source block, we add a plugin
+ # by name bundler-source-<type> to list of plugins to be installed.
+ #
+ # These plugins are optional and are not installed when there is conflict
+ # with any other plugin.
+ attr_reader :inferred_plugins
+
def initialize
super
@sources = Plugin::SourceList.new
+ @inferred_plugins = [] # The source plugins inferred from :type
end
def plugin(name, *args)
@@ -24,6 +35,19 @@ module Bundler
def method_missing(name, *args)
raise PluginGemfileError, "Undefined local variable or method `#{name}' for Gemfile" unless Bundler::Dsl.method_defined? name
end
+
+ def source(source, *args, &blk)
+ options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ options = normalize_hash(options)
+ return super unless options.key?("type")
+
+ plugin_name = "bundler-source-#{options["type"]}"
+
+ return if @dependencies.any? {|d| d.name == plugin_name }
+
+ plugin(plugin_name)
+ @inferred_plugins << plugin_name
+ end
end
end
end
diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb
index 1e39eb0042..4abf85fb02 100644
--- a/lib/bundler/plugin/index.rb
+++ b/lib/bundler/plugin/index.rb
@@ -13,27 +13,48 @@ module Bundler
end
end
+ class SourceConflict < PluginError
+ def initialize(plugin, sources)
+ msg = "Source(s) `#{sources.join("`, `")}` declared by #{plugin} are already registered."
+ super msg
+ end
+ end
+
def initialize
@plugin_paths = {}
@commands = {}
+ @sources = {}
+ @load_paths = {}
load_index
end
- # This function is to be called when a new plugin is installed. This function shall add
- # the functions of the plugin to existing maps and also the name to source location.
+ # This function is to be called when a new plugin is installed. This
+ # function shall add the functions of the plugin to existing maps and also
+ # the name to source location.
#
# @param [String] name of the plugin to be registered
# @param [String] path where the plugin is installed
+ # @param [Array<String>] load_paths for the plugin
# @param [Array<String>] commands that are handled by the plugin
- def register_plugin(name, path, commands)
- @plugin_paths[name] = path
+ # @param [Array<String>] sources that are handled by the plugin
+ def register_plugin(name, path, load_paths, commands, sources)
+ old_commands = @commands.dup
common = commands & @commands.keys
raise CommandConflict.new(name, common) unless common.empty?
commands.each {|c| @commands[c] = name }
+ common = sources & @sources.keys
+ raise SourceConflict.new(name, common) unless common.empty?
+ sources.each {|k| @sources[k] = name }
+
+ @plugin_paths[name] = path
+ @load_paths[name] = load_paths
save_index
+ rescue
+ @commands = old_commands
+ raise
end
# Path where the index file is stored
@@ -45,6 +66,10 @@ module Bundler
Pathname.new @plugin_paths[name]
end
+ def load_paths(name)
+ @load_paths[name]
+ end
+
# Fetch the name of plugin handling the command
def command_plugin(command)
@commands[command]
@@ -54,9 +79,18 @@ module Bundler
@plugin_paths[name]
end
+ def source?(source)
+ @sources.key? source
+ end
+
+ def source_plugin(name)
+ @sources[name]
+ end
+
private
- # Reads the index file from the directory and initializes the instance variables.
+ # Reads the index file from the directory and initializes the instance
+ # variables.
def load_index
SharedHelpers.filesystem_access(index_file, :read) do |index_f|
valid_file = index_f && index_f.exist? && !index_f.size.zero?
@@ -65,16 +99,21 @@ module Bundler
require "bundler/yaml_serializer"
index = YAMLSerializer.load(data)
@plugin_paths = index["plugin_paths"] || {}
+ @load_paths = index["load_paths"] || {}
@commands = index["commands"] || {}
+ @sources = index["sources"] || {}
end
end
- # Should be called when any of the instance variables change. Stores the instance
- # variables in YAML format. (The instance variables are supposed to be only String key value pairs)
+ # Should be called when any of the instance variables change. Stores the
+ # instance variables in YAML format. (The instance variables are supposed
+ # to be only String key value pairs)
def save_index
index = {
"plugin_paths" => @plugin_paths,
+ "load_paths" => @load_paths,
"commands" => @commands,
+ "sources" => @sources,
}
require "bundler/yaml_serializer"
diff --git a/lib/bundler/plugin/installer.rb b/lib/bundler/plugin/installer.rb
index 2c10bb24c8..a50d0ceedd 100644
--- a/lib/bundler/plugin/installer.rb
+++ b/lib/bundler/plugin/installer.rb
@@ -25,18 +25,14 @@ module Bundler
# Installs the plugin from Definition object created by limited parsing of
# Gemfile searching for plugins to be installed
#
- # @param [Definition] definiton object
- # @return [Hash] map of plugin names to thier paths
+ # @param [Definition] definition object
+ # @return [Hash] map of names to their specs they are installed with
def install_definition(definition)
- plugins = definition.dependencies.map(&:name)
-
def definition.lock(*); end
definition.resolve_remotely!
specs = definition.specs
- paths = install_from_specs specs
-
- Hash[paths.select {|name, _| plugins.include? name }]
+ install_from_specs specs
end
private
@@ -66,7 +62,7 @@ module Bundler
# @param [Array] version of the gem to install
# @param [String, Array<String>] source(s) to resolve the gem
#
- # @return [String] the path where the plugin was installed
+ # @return [Hash] map of names to the specs of plugins installed
def install_rubygems(names, version, sources)
deps = names.map {|name| Dependency.new name, version }
@@ -82,14 +78,14 @@ module Bundler
#
# @param specs to install
#
- # @return [Hash] map of names to path where the plugin was installed
+ # @return [Hash] map of names to the specs
def install_from_specs(specs)
paths = {}
specs.each do |spec|
spec.source.install spec
- paths[spec.name] = spec.full_gem_path
+ paths[spec.name] = spec
end
paths
diff --git a/lib/bundler/plugin/source_list.rb b/lib/bundler/plugin/source_list.rb
index 6b1f1aee36..33f5e5afbd 100644
--- a/lib/bundler/plugin/source_list.rb
+++ b/lib/bundler/plugin/source_list.rb
@@ -19,6 +19,10 @@ module Bundler
def add_rubygems_source(options = {})
add_source_to_list Plugin::Installer::Rubygems.new(options), @rubygems_sources
end
+
+ def all_sources
+ path_sources + git_sources + rubygems_sources
+ end
end
end
end
diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb
index 53db29a959..88c446e11a 100644
--- a/lib/bundler/rubygems_ext.rb
+++ b/lib/bundler/rubygems_ext.rb
@@ -25,7 +25,7 @@ module Gem
attr_writer :full_gem_path unless instance_methods.include?(:full_gem_path=)
def full_gem_path
- if source.respond_to?(:path)
+ if source.respond_to?(:path) || source.is_a?(Bundler::Plugin::API::Source)
Pathname.new(loaded_from).dirname.expand_path(source.root).to_s.untaint
else
rg_full_gem_path
diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb
index 76955ee49e..5344ab694f 100644
--- a/lib/bundler/source/git.rb
+++ b/lib/bundler/source/git.rb
@@ -170,7 +170,7 @@ module Bundler
serialize_gemspecs_in(install_path)
@copied = true
end
- generate_bin(spec)
+ generate_bin(spec, !Bundler.rubygems.spec_missing_extensions?(spec))
requires_checkout? ? spec.post_install_message : nil
end
@@ -223,10 +223,6 @@ module Bundler
private
- def build_extensions(installer)
- super if Bundler.rubygems.spec_missing_extensions?(installer.spec)
- end
-
def serialize_gemspecs_in(destination)
destination = destination.expand_path(Bundler.root) if destination.relative?
Dir["#{destination}/#{@glob}"].each do |spec_path|
diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb
index bbdd30b1e6..3c4d914fb3 100644
--- a/lib/bundler/source/path.rb
+++ b/lib/bundler/source/path.rb
@@ -203,13 +203,8 @@ module Bundler
end
end.compact
- SharedHelpers.chdir(gem_dir) do
- installer = Path::Installer.new(spec, :env_shebang => false)
- run_hooks(:pre_install, installer)
- build_extensions(installer) unless disable_extensions
- installer.generate_bin
- run_hooks(:post_install, installer)
- end
+ installer = Path::Installer.new(spec, :env_shebang => false, :disable_extensions => disable_extensions)
+ installer.post_install
rescue Gem::InvalidSpecificationException => e
Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
"This prevents bundler from installing bins or native extensions, but " \
@@ -223,23 +218,6 @@ module Bundler
Bundler.ui.warn "The validation message from Rubygems was:\n #{e.message}"
end
-
- def build_extensions(installer)
- installer.build_extensions
- run_hooks(:post_build, installer)
- end
-
- def run_hooks(type, installer)
- hooks_meth = "#{type}_hooks"
- return unless Gem.respond_to?(hooks_meth)
- Gem.send(hooks_meth).each do |hook|
- result = hook.call(installer)
- next unless result == false
- location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
- message = "#{type} hook#{location} failed for #{installer.spec.full_name}"
- raise InstallHookError, message
- end
- end
end
end
end
diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb
index 0aa27bd564..abc46d5a04 100644
--- a/lib/bundler/source/path/installer.rb
+++ b/lib/bundler/source/path/installer.rb
@@ -6,13 +6,14 @@ module Bundler
attr_reader :spec
def initialize(spec, options = {})
- @spec = spec
- @gem_dir = Bundler.rubygems.path(spec.full_gem_path)
- @wrappers = true
- @env_shebang = true
- @format_executable = options[:format_executable] || false
- @build_args = options[:build_args] || Bundler.rubygems.build_args
- @gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin"
+ @spec = spec
+ @gem_dir = Bundler.rubygems.path(spec.full_gem_path)
+ @wrappers = true
+ @env_shebang = true
+ @format_executable = options[:format_executable] || false
+ @build_args = options[:build_args] || Bundler.rubygems.build_args
+ @gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin"
+ @disable_extentions = options[:disable_extensions]
if Bundler.requires_sudo?
@tmp_dir = Bundler.tmp(spec.full_name).to_s
@@ -22,9 +23,26 @@ module Bundler
end
end
- def generate_bin
- return if spec.executables.nil? || spec.executables.empty?
+ def post_install
+ SharedHelpers.chdir(@gem_dir) do
+ run_hooks(:pre_install)
+
+ unless @disable_extentions
+ build_extensions
+ run_hooks(:post_build)
+ end
+
+ generate_bin unless spec.executables.nil? || spec.executables.empty?
+
+ run_hooks(:post_install)
+ end
+ ensure
+ Bundler.rm_rf(@tmp_dir) if Bundler.requires_sudo?
+ end
+
+ private
+ def generate_bin
super
if Bundler.requires_sudo?
@@ -35,8 +53,18 @@ module Bundler
Bundler.sudo "cp -R #{@bin_dir}/#{exe} #{@gem_bin_dir}"
end
end
- ensure
- Bundler.rm_rf(@tmp_dir) if Bundler.requires_sudo?
+ end
+
+ def run_hooks(type)
+ hooks_meth = "#{type}_hooks"
+ return unless Gem.respond_to?(hooks_meth)
+ Gem.send(hooks_meth).each do |hook|
+ result = hook.call(self)
+ next unless result == false
+ location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
+ message = "#{type} hook#{location} failed for #{spec.full_name}"
+ raise InstallHookError, message
+ end
end
end
end
diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb
index 0595cbdcf4..fdc77cb23d 100644
--- a/lib/bundler/source_list.rb
+++ b/lib/bundler/source_list.rb
@@ -2,11 +2,13 @@
module Bundler
class SourceList
attr_reader :path_sources,
- :git_sources
+ :git_sources,
+ :plugin_sources
def initialize
@path_sources = []
@git_sources = []
+ @plugin_sources = []
@rubygems_aggregate = Source::Rubygems.new
@rubygems_sources = []
end
@@ -27,6 +29,10 @@ module Bundler
add_source_to_list Source::Rubygems.new(options), @rubygems_sources
end
+ def add_plugin_source(source, options = {})
+ add_source_to_list Plugin.source(source).new(options), @plugin_sources
+ end
+
def add_rubygems_remote(uri)
@rubygems_aggregate.add_remote(uri)
@rubygems_aggregate
@@ -41,7 +47,7 @@ module Bundler
end
def all_sources
- path_sources + git_sources + rubygems_sources
+ path_sources + git_sources + plugin_sources + rubygems_sources
end
def get(source)
@@ -49,14 +55,14 @@ module Bundler
end
def lock_sources
- lock_sources = (path_sources + git_sources).sort_by(&:to_s)
+ lock_sources = (path_sources + git_sources + plugin_sources).sort_by(&:to_s)
lock_sources << combine_rubygems_sources
end
def replace_sources!(replacement_sources)
return true if replacement_sources.empty?
- [path_sources, git_sources].each do |source_list|
+ [path_sources, git_sources, plugin_sources].each do |source_list|
source_list.map! do |source|
replacement_sources.find {|s| s == source } || source
end
@@ -92,9 +98,10 @@ module Bundler
def source_list_for(source)
case source
- when Source::Git then git_sources
- when Source::Path then path_sources
- when Source::Rubygems then rubygems_sources
+ when Source::Git then git_sources
+ when Source::Path then path_sources
+ when Source::Rubygems then rubygems_sources
+ when Plugin::API::Source then plugin_sources
else raise ArgumentError, "Invalid source: #{source.inspect}"
end
end
diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb
index 327baa4ee7..dede8fd5fd 100644
--- a/lib/bundler/yaml_serializer.rb
+++ b/lib/bundler/yaml_serializer.rb
@@ -16,6 +16,8 @@ module Bundler
yaml << k << ":"
if v.is_a?(Hash)
yaml << dump_hash(v).gsub(/^(?!$)/, " ") # indent all non-empty lines
+ elsif v.is_a?(Array) # Expected to be array of strings
+ yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n"
else
yaml << " " << v.to_s.gsub(/\s+/, " ").inspect << "\n"
end
@@ -23,11 +25,20 @@ module Bundler
yaml
end
- SCAN_REGEX = /
+ ARRAY_REGEX = /
+ ^
+ (?:[ ]*-[ ]) # '- ' before array items
+ (['"]?) # optional opening quote
+ (.*) # value
+ \1 # matching closing quote
+ $
+ /xo
+
+ HASH_REGEX = /
^
([ ]*) # indentations
(.*) # key
- (?::(?=\s)) # : (without the lookahead the #key includes this when : is present in value)
+ (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value)
[ ]?
(?: !\s)? # optional exclamation mark found with ruby 1.9.3
(['"]?) # optional opening quote
@@ -39,15 +50,27 @@ module Bundler
def load(str)
res = {}
stack = [res]
- str.scan(SCAN_REGEX).each do |(indent, key, _, val)|
- key = convert_to_backward_compatible_key(key)
- depth = indent.scan(/ /).length
- if val.empty?
- new_hash = {}
- stack[depth][key] = new_hash
- stack[depth + 1] = new_hash
- else
- stack[depth][key] = val
+ last_hash = nil
+ last_empty_key = nil
+ str.split("\n").each do |line|
+ if match = HASH_REGEX.match(line)
+ indent, key, _, val = match.captures
+ key = convert_to_backward_compatible_key(key)
+ depth = indent.scan(/ /).length
+ if val.empty?
+ new_hash = {}
+ stack[depth][key] = new_hash
+ stack[depth + 1] = new_hash
+ last_empty_key = key
+ last_hash = stack[depth]
+ else
+ stack[depth][key] = val
+ end
+ elsif match = ARRAY_REGEX.match(line)
+ _, val = match.captures
+ last_hash[last_empty_key] = [] unless last_hash[last_empty_key].is_a?(Array)
+
+ last_hash[last_empty_key].push(val)
end
end
res
diff --git a/spec/bundler/lockfile_parser_spec.rb b/spec/bundler/lockfile_parser_spec.rb
index d9bb32b4de..98d7b68c6e 100644
--- a/spec/bundler/lockfile_parser_spec.rb
+++ b/spec/bundler/lockfile_parser_spec.rb
@@ -60,7 +60,7 @@ describe Bundler::LockfileParser do
it "returns the same as > 1.0" do
expect(subject).to contain_exactly(
- described_class::BUNDLED, described_class::RUBY
+ described_class::BUNDLED, described_class::RUBY, described_class::PLUGIN
)
end
end
@@ -70,7 +70,7 @@ describe Bundler::LockfileParser do
it "returns the same as for the release version" do
expect(subject).to contain_exactly(
- described_class::RUBY
+ described_class::RUBY, described_class::PLUGIN
)
end
end
diff --git a/spec/bundler/plugin/api/source_spec.rb b/spec/bundler/plugin/api/source_spec.rb
new file mode 100644
index 0000000000..d62127a604
--- /dev/null
+++ b/spec/bundler/plugin/api/source_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+describe Bundler::Plugin::API::Source do
+ let(:uri) { "uri://to/test" }
+ let(:type) { "spec_type" }
+
+ subject(:source) do
+ klass = Class.new
+ klass.send :include, Bundler::Plugin::API::Source
+ klass.new("uri" => uri, "type" => type)
+ end
+
+ describe "attributes" do
+ it "allows access to uri" do
+ expect(source.uri).to eq("uri://to/test")
+ end
+
+ it "allows access to name" do
+ expect(source.name).to eq("spec_type at uri://to/test")
+ end
+ end
+
+ context "post_install" do
+ let(:installer) { double(:installer) }
+
+ before do
+ allow(Bundler::Source::Path::Installer).to receive(:new) { installer }
+ end
+
+ it "calls Path::Installer's post_install" do
+ expect(installer).to receive(:post_install).once
+
+ source.post_install(double(:spec))
+ end
+ end
+
+ context "install_path" do
+ let(:uri) { "uri://to/a/repository-name" }
+ let(:hash) { Digest::SHA1.hexdigest(uri) }
+ let(:install_path) { Pathname.new "/bundler/install/path" }
+
+ before do
+ allow(Bundler).to receive(:install_path) { install_path }
+ end
+
+ it "returns basename with uri_hash" do
+ expected = Pathname.new "#{install_path}/repository-name-#{hash[0..11]}"
+ expect(source.install_path).to eq(expected)
+ end
+ end
+
+ context "to_lock" do
+ it "returns the string with remote and type" do
+ expected = strip_whitespace <<-L
+ PLUGIN SOURCE
+ remote: #{uri}
+ type: #{type}
+ specs:
+ L
+
+ expect(source.to_lock).to eq(expected)
+ end
+
+ context "with additional options to lock" do
+ before do
+ allow(source).to receive(:options_to_lock) { { "first" => "option" } }
+ end
+
+ it "includes them" do
+ expected = strip_whitespace <<-L
+ PLUGIN SOURCE
+ remote: #{uri}
+ type: #{type}
+ first: option
+ specs:
+ L
+
+ expect(source.to_lock).to eq(expected)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugin/api_spec.rb b/spec/bundler/plugin/api_spec.rb
index 23134dd853..a227d31591 100644
--- a/spec/bundler/plugin/api_spec.rb
+++ b/spec/bundler/plugin/api_spec.rb
@@ -7,18 +7,36 @@ describe Bundler::Plugin::API do
stub_const "UserPluginClass", Class.new(Bundler::Plugin::API)
end
- it "declares a command plugin with same class as handler" do
- allow(Bundler::Plugin).
- to receive(:add_command).with("meh", UserPluginClass).once
+ describe "#command" do
+ it "declares a command plugin with same class as handler" do
+ expect(Bundler::Plugin).
+ to receive(:add_command).with("meh", UserPluginClass).once
- UserPluginClass.command "meh"
+ UserPluginClass.command "meh"
+ end
+
+ it "accepts another class as argument that handles the command" do
+ stub_const "NewClass", Class.new
+ expect(Bundler::Plugin).to receive(:add_command).with("meh", NewClass).once
+
+ UserPluginClass.command "meh", NewClass
+ end
end
- it "accepts another class as argument that handles the command" do
- stub_const "NewClass", Class.new
- allow(Bundler::Plugin).to receive(:add_command).with("meh", NewClass).once
+ describe "#source" do
+ it "declares a source plugin with same class as handler" do
+ expect(Bundler::Plugin).
+ to receive(:add_source).with("a_source", UserPluginClass).once
+
+ UserPluginClass.source "a_source"
+ end
+
+ it "accepts another class as argument that handles the command" do
+ stub_const "NewClass", Class.new
+ expect(Bundler::Plugin).to receive(:add_source).with("a_source", NewClass).once
- UserPluginClass.command "meh", NewClass
+ UserPluginClass.source "a_source", NewClass
+ end
end
end
@@ -30,8 +48,16 @@ describe Bundler::Plugin::API do
subject(:api) { UserPluginClass.new }
# A test of delegation
- it "provides the bundler settings" do
- expect(api.settings).to eq(Bundler.settings)
+ it "provides the Bundler's functions" do
+ expect(Bundler).to receive(:an_unkown_function).once
+
+ api.an_unkown_function
+ end
+
+ it "includes Bundler::SharedHelpers' functions" do
+ expect(Bundler::SharedHelpers).to receive(:an_unkown_helper).once
+
+ api.an_unkown_helper
end
context "#tmp" do
diff --git a/spec/bundler/plugin/dsl_spec.rb b/spec/bundler/plugin/dsl_spec.rb
index 876564f22b..9a694833ff 100644
--- a/spec/bundler/plugin/dsl_spec.rb
+++ b/spec/bundler/plugin/dsl_spec.rb
@@ -19,4 +19,21 @@ describe Bundler::Plugin::DSL do
expect { dsl.no_method }.to raise_error(DSL::PluginGemfileError)
end
end
+
+ describe "source block" do
+ it "adds #source with :type to list and also inferred_plugins list" do
+ expect(dsl).to receive(:plugin).with("bundler-source-news").once
+
+ dsl.source("some_random_url", :type => "news") {}
+
+ expect(dsl.inferred_plugins).to eq(["bundler-source-news"])
+ end
+
+ it "registers a source type plugin only once for multiple declataions" do
+ expect(dsl).to receive(:plugin).with("bundler-source-news").and_call_original.once
+
+ dsl.source("some_random_url", :type => "news") {}
+ dsl.source("another_random_url", :type => "news") {}
+ end
+ end
end
diff --git a/spec/bundler/plugin/index_spec.rb b/spec/bundler/plugin/index_spec.rb
index f969aad12f..15db178966 100644
--- a/spec/bundler/plugin/index_spec.rb
+++ b/spec/bundler/plugin/index_spec.rb
@@ -6,30 +6,35 @@ describe Bundler::Plugin::Index do
subject(:index) { Index.new }
- before do
- build_lib "new-plugin", :path => lib_path("new-plugin") do |s|
- s.write "plugins.rb"
- end
- end
-
describe "#register plugin" do
before do
- index.register_plugin("new-plugin", lib_path("new-plugin").to_s, [])
+ path = lib_path("new-plugin")
+ index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], [], [])
end
it "is available for retrieval" do
expect(index.plugin_path("new-plugin")).to eq(lib_path("new-plugin"))
end
+ it "load_paths is available for retrival" do
+ expect(index.load_paths("new-plugin")).to eq([lib_path("new-plugin").join("lib").to_s])
+ end
+
it "is persistent" do
new_index = Index.new
expect(new_index.plugin_path("new-plugin")).to eq(lib_path("new-plugin"))
end
+
+ it "load_paths are persistant" do
+ new_index = Index.new
+ expect(new_index.load_paths("new-plugin")).to eq([lib_path("new-plugin").join("lib").to_s])
+ end
end
describe "commands" do
before do
- index.register_plugin("cplugin", lib_path("cplugin").to_s, ["newco"])
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["newco"], [])
end
it "returns the plugins name on query" do
@@ -38,8 +43,89 @@ describe Bundler::Plugin::Index do
it "raises error on conflict" do
expect do
- index.register_plugin("aplugin", lib_path("aplugin").to_s, ["newco"])
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [])
end.to raise_error(Index::CommandConflict)
end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.command_plugin("newco")).to eq("cplugin")
+ end
+ end
+
+ describe "source" do
+ before do
+ path = lib_path("splugin")
+ index.register_plugin("splugin", path.to_s, [path.join("lib").to_s], [], ["new_source"])
+ end
+
+ it "returns the plugins name on query" do
+ expect(index.source_plugin("new_source")).to eq("splugin")
+ end
+
+ it "raises error on conflict" do
+ expect do
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"])
+ end.to raise_error(Index::SourceConflict)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.source_plugin("new_source")).to eq("splugin")
+ end
+ end
+
+ describe "after conflict" do
+ before do
+ path = lib_path("aplugin")
+ index.register_plugin("aplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"])
+ end
+
+ shared_examples "it cleans up" do
+ it "the path" do
+ expect(index.installed?("cplugin")).to be_falsy
+ end
+
+ it "the command" do
+ expect(index.command_plugin("xfoo")).to be_falsy
+ end
+
+ it "the source" do
+ expect(index.source_plugin("xbar")).to be_falsy
+ end
+ end
+
+ context "on command conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+
+ context "on source conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"])
+ end.to raise_error(Index::SourceConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+
+ context "on command and source conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ include_examples "it cleans up"
+ end
end
end
diff --git a/spec/bundler/plugin/installer_spec.rb b/spec/bundler/plugin/installer_spec.rb
index 33a0ddea48..a914aabbf3 100644
--- a/spec/bundler/plugin/installer_spec.rb
+++ b/spec/bundler/plugin/installer_spec.rb
@@ -16,20 +16,21 @@ describe Bundler::Plugin::Installer do
end
describe "with mocked installers" do
- it "returns the installation path after installing git plugins" do
+ let(:spec) { double(:spec) }
+ it "returns the installed spec after installing git plugins" do
allow(installer).to receive(:install_git).
- and_return("new-plugin" => "/git/install/path")
+ and_return("new-plugin" => spec)
expect(installer.install(["new-plugin"], :git => "https://some.ran/dom")).
- to eq("new-plugin" => "/git/install/path")
+ to eq("new-plugin" => spec)
end
- it "returns the installation path after installing rubygems plugins" do
+ it "returns the installed spec after installing rubygems plugins" do
allow(installer).to receive(:install_rubygems).
- and_return("new-plugin" => "/rubygems/install/path")
+ and_return("new-plugin" => spec)
expect(installer.install(["new-plugin"], :source => "https://some.ran/dom")).
- to eq("new-plugin" => "/rubygems/install/path")
+ to eq("new-plugin" => spec)
end
end
@@ -41,30 +42,57 @@ describe Bundler::Plugin::Installer do
end
end
- it "returns the installation path after installing git plugins" do
- build_git "ga-plugin", :path => lib_path("ga-plugin") do |s|
- s.write "plugins.rb"
+ context "git plugins" do
+ before do
+ build_git "ga-plugin", :path => lib_path("ga-plugin") do |s|
+ s.write "plugins.rb"
+ end
end
- rev = revision_for(lib_path("ga-plugin"))
- expected = { "ga-plugin" => Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s }
+ let(:result) do
+ installer.install(["ga-plugin"], :git => "file://#{lib_path("ga-plugin")}")
+ end
+
+ it "returns the installed spec after installing" do
+ expect(result["ga-plugin"]).to be_kind_of(Gem::Specification)
+ end
- opts = { :git => "file://#{lib_path("ga-plugin")}" }
- expect(installer.install(["ga-plugin"], opts)).to eq(expected)
+ it "has expected full gem path" do
+ rev = revision_for(lib_path("ga-plugin"))
+ expect(result["ga-plugin"].full_gem_path).
+ to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s)
+ end
end
- it "returns the installation path after installing rubygems plugins" do
- opts = { :source => "file://#{gem_repo2}" }
- expect(installer.install(["re-plugin"], opts)).
- to eq("re-plugin" => plugin_gems("re-plugin-1.0").to_s)
+ context "rubygems plugins" do
+ let(:result) do
+ installer.install(["re-plugin"], :source => "file://#{gem_repo2}")
+ end
+
+ it "returns the installed spec after installing " do
+ expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ end
+
+ it "has expected full_gem)path" do
+ expect(result["re-plugin"].full_gem_path).
+ to eq(plugin_gems("re-plugin-1.0").to_s)
+ end
end
- it "accepts multiple plugins" do
- opts = { :source => "file://#{gem_repo2}" }
+ context "multiple plugins" do
+ let(:result) do
+ installer.install(["re-plugin", "ma-plugin"], :source => "file://#{gem_repo2}")
+ end
+
+ it "returns the installed spec after installing " do
+ expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ expect(result["ma-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ end
- expect(installer.install(["re-plugin", "ma-plugin"], opts)).
- to eq("re-plugin" => plugin_gems("re-plugin-1.0").to_s,
- "ma-plugin" => plugin_gems("ma-plugin-1.0").to_s)
+ it "has expected full_gem)path" do
+ expect(result["re-plugin"].full_gem_path).to eq(plugin_gems("re-plugin-1.0").to_s)
+ expect(result["ma-plugin"].full_gem_path).to eq(plugin_gems("ma-plugin-1.0").to_s)
+ end
end
end
end
diff --git a/spec/bundler/plugin_spec.rb b/spec/bundler/plugin_spec.rb
index 517f539bd8..0c81df2232 100644
--- a/spec/bundler/plugin_spec.rb
+++ b/spec/bundler/plugin_spec.rb
@@ -6,6 +6,8 @@ describe Bundler::Plugin do
let(:installer) { double(:installer) }
let(:index) { double(:index) }
+ let(:spec) { double(:spec) }
+ let(:spec2) { double(:spec2) }
before do
build_lib "new-plugin", :path => lib_path("new-plugin") do |s|
@@ -16,6 +18,16 @@ describe Bundler::Plugin do
s.write "plugins.rb"
end
+ allow(spec).to receive(:full_gem_path).
+ and_return(lib_path("new-plugin").to_s)
+ allow(spec).to receive(:load_paths).
+ and_return([lib_path("new-plugin").join("lib").to_s])
+
+ allow(spec2).to receive(:full_gem_path).
+ and_return(lib_path("another-plugin").to_s)
+ allow(spec2).to receive(:load_paths).
+ and_return([lib_path("another-plugin").join("lib").to_s])
+
allow(Plugin::Installer).to receive(:new) { installer }
allow(Plugin).to receive(:index) { index }
allow(index).to receive(:register_plugin)
@@ -26,13 +38,13 @@ describe Bundler::Plugin do
before do
allow(installer).to receive(:install).with(["new-plugin"], opts) do
- { "new_plugin" => lib_path("new-plugin") }
+ { "new-plugin" => spec }
end
end
it "passes the name and options to installer" do
allow(installer).to receive(:install).with(["new-plugin"], opts) do
- { "new-plugin" => lib_path("new-plugin").to_s }
+ { "new-plugin" => spec }
end.once
subject.install ["new-plugin"], opts
@@ -47,7 +59,7 @@ describe Bundler::Plugin do
it "registers the plugin with index" do
allow(index).to receive(:register_plugin).
- with("new-plugin", lib_path("new-plugin").to_s, []).once
+ with("new-plugin", lib_path("new-plugin").to_s, [lib_path("new-plugin").join("lib").to_s], []).once
subject.install ["new-plugin"], opts
end
@@ -56,8 +68,8 @@ describe Bundler::Plugin do
allow(installer).to receive(:install).
with(["new-plugin", "another-plugin"], opts) do
{
- "new_plugin" => lib_path("new-plugin"),
- "another-plugin" => lib_path("another-plugin"),
+ "new-plugin" => spec,
+ "another-plugin" => spec2,
}
end.once
@@ -70,10 +82,14 @@ describe Bundler::Plugin do
describe "evaluate gemfile for plugins" do
let(:definition) { double("definition") }
+ let(:builder) { double("builder") }
let(:gemfile) { bundled_app("Gemfile") }
before do
- allow(Plugin::DSL).to receive(:evaluate) { definition }
+ allow(Plugin::DSL).to receive(:new) { builder }
+ allow(builder).to receive(:eval_gemfile).with(gemfile)
+ allow(builder).to receive(:to_definition) { definition }
+ allow(builder).to receive(:inferred_plugins) { [] }
end
it "doesn't calls installer without any plugins" do
@@ -83,18 +99,38 @@ describe Bundler::Plugin do
subject.gemfile_install(gemfile)
end
- it "should validate and register the plugins" do
- allow(definition).to receive(:dependencies) { [1, 2] }
- plugin_paths = {
- "new-plugin" => lib_path("new-plugin"),
- "another-plugin" => lib_path("another-plugin"),
- }
- allow(installer).to receive(:install_definition) { plugin_paths }
+ context "with dependencies" do
+ let(:plugin_specs) do
+ {
+ "new-plugin" => spec,
+ "another-plugin" => spec2,
+ }
+ end
- expect(subject).to receive(:validate_plugin!).twice
- expect(subject).to receive(:register_plugin).twice
+ before do
+ allow(index).to receive(:installed?) { nil }
+ allow(definition).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0"), Bundler::Dependency.new("another-plugin", ">=0")] }
+ allow(installer).to receive(:install_definition) { plugin_specs }
+ end
- subject.gemfile_install(gemfile)
+ it "should validate and register the plugins" do
+ expect(subject).to receive(:validate_plugin!).twice
+ expect(subject).to receive(:register_plugin).twice
+
+ subject.gemfile_install(gemfile)
+ end
+
+ it "should pass the optional plugins to #register_plugin" do
+ allow(builder).to receive(:inferred_plugins) { ["another-plugin"] }
+
+ expect(subject).to receive(:register_plugin).
+ with("new-plugin", spec, false).once
+
+ expect(subject).to receive(:register_plugin).
+ with("another-plugin", spec2, true).once
+
+ subject.gemfile_install(gemfile)
+ end
end
end
@@ -120,4 +156,57 @@ describe Bundler::Plugin do
to raise_error(Plugin::UndefinedCommandError)
end
end
+
+ describe "#source?" do
+ it "returns true value 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
+ 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
+ end
+ end
+
+ describe "#source" do
+ it "raises UnknownSourceError when source is not found" do
+ allow(index).to receive(:source_plugin).with("bar") { nil }
+ expect { subject.source("bar") }.
+ to raise_error(Plugin::UnknownSourceError)
+ end
+
+ it "loads the plugin, if not loaded" do
+ allow(index).to receive(:source_plugin).with("foo-bar") { "plugin_name" }
+
+ expect(subject).to receive(:load_plugin).with("plugin_name")
+ subject.source("foo-bar")
+ end
+
+ it "returns the class registered with #add_source" do
+ allow(index).to receive(:source_plugin).with("foo") { "plugin_name" }
+ stub_const "NewClass", Class.new
+
+ subject.add_source("foo", NewClass)
+ expect(subject.source("foo")).to be(NewClass)
+ end
+ end
+
+ describe "#source_from_lock" do
+ it "returns instance of registered class initialized with locked opts" do
+ opts = { "type" => "l_source", "remote" => "xyz", "other" => "random" }
+ allow(index).to receive(:source_plugin).with("l_source") { "plugin_name" }
+
+ stub_const "SClass", Class.new
+ s_instance = double(:s_instance)
+ subject.add_source("l_source", SClass)
+
+ expect(SClass).to receive(:new).
+ with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance }
+ expect(subject.source_from_lock(opts)).to be(s_instance)
+ end
+ end
end
diff --git a/spec/bundler/source_list_spec.rb b/spec/bundler/source_list_spec.rb
index d7567d6e11..56f17f9af9 100644
--- a/spec/bundler/source_list_spec.rb
+++ b/spec/bundler/source_list_spec.rb
@@ -4,6 +4,10 @@ require "spec_helper"
describe Bundler::SourceList do
before do
allow(Bundler).to receive(:root) { Pathname.new "/" }
+
+ stub_const "ASourcePlugin", Class.new(Bundler::Plugin::API)
+ ASourcePlugin.source "new_source"
+ allow(Bundler::Plugin).to receive(:source?).with("new_source").and_return(true)
end
subject(:source_list) { Bundler::SourceList.new }
@@ -15,6 +19,7 @@ describe Bundler::SourceList do
source_list.add_path_source("path" => "/existing/path/to/gem")
source_list.add_git_source("uri" => "git://existing-git.org/path.git")
source_list.add_rubygems_source("remotes" => ["https://existing-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
end
describe "#add_path_source" do
@@ -100,6 +105,29 @@ describe Bundler::SourceList do
expect(@returned_source.remotes.first).to eq(URI("https://othersource.org/"))
end
end
+
+ describe "#add_plugin_source" do
+ before do
+ @duplicate = source_list.add_plugin_source("new_source", "uri" => "http://host/path.")
+ @new_source = source_list.add_plugin_source("new_source", "uri" => "http://host/path.")
+ end
+
+ it "returns the new plugin source" do
+ expect(@new_source).to be_a(Bundler::Plugin::API::Source)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("uri" => "http://host/path.")
+ end
+
+ it "adds the source to the beginning of git_sources" do
+ expect(source_list.plugin_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ expect(source_list.plugin_sources).not_to include equal(@duplicate)
+ end
+ end
end
describe "#all_sources" do
@@ -107,6 +135,7 @@ describe Bundler::SourceList do
source_list.add_git_source("uri" => "git://host/path.git")
source_list.add_rubygems_source("remotes" => ["https://rubygems.org"])
source_list.add_path_source("path" => "/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
expect(source_list.all_sources).to include rubygems_aggregate
end
@@ -114,6 +143,7 @@ describe Bundler::SourceList do
it "includes the aggregate rubygems source when no rubygems sources have been added" do
source_list.add_git_source("uri" => "git://host/path.git")
source_list.add_path_source("path" => "/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
expect(source_list.all_sources).to include rubygems_aggregate
end
@@ -122,12 +152,15 @@ describe Bundler::SourceList do
source_list.add_git_source("uri" => "git://third-git.org/path.git")
source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"])
source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/b")
source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"])
source_list.add_path_source("path" => "/second/path/to/gem")
source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://some.o.url/")
source_list.add_git_source("uri" => "git://second-git.org/path.git")
source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/c")
source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
source_list.add_git_source("uri" => "git://first-git.org/path.git")
@@ -138,6 +171,9 @@ describe Bundler::SourceList do
Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://some.url/c"),
+ ASourcePlugin.new("uri" => "https://some.o.url/"),
+ ASourcePlugin.new("uri" => "https://some.url/b"),
Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
@@ -205,6 +241,35 @@ describe Bundler::SourceList do
end
end
+ describe "#plugin_sources" do
+ it "returns an empty array when no plugin sources have been added" do
+ source_list.add_rubygems_remote("https://rubygems.org")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.plugin_sources).to be_empty
+ end
+
+ it "returns plugin sources in the reverse order that they were added" do
+ source_list.add_plugin_source("new_source", "uri" => "https://third-git.org/path.git")
+ source_list.add_git_source("https://new-git.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_remote("https://third-rubygems.org")
+ source_list.add_plugin_source("new_source", "uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_rubygems_remote("https://first-rubygems.org")
+ source_list.add_plugin_source("new_source", "uri" => "git://first-git.org/path.git")
+
+ expect(source_list.plugin_sources).to eq [
+ ASourcePlugin.new("uri" => "git://first-git.org/path.git"),
+ ASourcePlugin.new("uri" => "git://second-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://third-git.org/path.git"),
+ ]
+ end
+ end
+
describe "#rubygems_sources" do
it "includes the aggregate rubygems source when rubygems sources have been added" do
source_list.add_git_source("uri" => "git://host/path.git")
@@ -268,12 +333,14 @@ describe Bundler::SourceList do
it "combines the rubygems sources into a single instance, removing duplicate remotes from the end" do
source_list.add_git_source("uri" => "git://third-git.org/path.git")
source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://third-bar.org/foo")
source_list.add_path_source("path" => "/third/path/to/gem")
source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
source_list.add_path_source("path" => "/second/path/to/gem")
source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
source_list.add_git_source("uri" => "git://second-git.org/path.git")
source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://second-plugin.org/random")
source_list.add_path_source("path" => "/first/path/to/gem")
source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"])
source_list.add_git_source("uri" => "git://first-git.org/path.git")
@@ -282,6 +349,8 @@ describe Bundler::SourceList do
Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://second-plugin.org/random"),
+ ASourcePlugin.new("uri" => "https://third-bar.org/foo"),
Bundler::Source::Path.new("path" => "/first/path/to/gem"),
Bundler::Source::Path.new("path" => "/second/path/to/gem"),
Bundler::Source::Path.new("path" => "/third/path/to/gem"),
diff --git a/spec/bundler/yaml_serializer_spec.rb b/spec/bundler/yaml_serializer_spec.rb
index 53dbbc6766..0b3261336c 100644
--- a/spec/bundler/yaml_serializer_spec.rb
+++ b/spec/bundler/yaml_serializer_spec.rb
@@ -32,6 +32,31 @@ describe Bundler::YAMLSerializer do
expect(serializer.dump(hash)).to eq(expected)
end
+
+ it "array inside an hash" do
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Jack and Jill went up the hill",
+ "To fetch a pail of water.",
+ "Jack fell down and broke his crown,",
+ "And Jill came tumbling after.",
+ ],
+ },
+ }
+
+ expected = strip_whitespace <<-YAML
+ ---
+ nested_hash:
+ contains_array:
+ - "Jack and Jill went up the hill"
+ - "To fetch a pail of water."
+ - "Jack fell down and broke his crown,"
+ - "And Jill came tumbling after."
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
end
describe "#load" do
@@ -52,6 +77,7 @@ describe Bundler::YAMLSerializer do
it "works for nested hash" do
yaml = strip_whitespace <<-YAML
+ ---
baa:
baa: "black sheep"
have: "you any wool?"
@@ -78,6 +104,27 @@ describe Bundler::YAMLSerializer do
expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://rubygems-mirror.org")
end
+
+ it "handles arrays inside hashes" do
+ yaml = strip_whitespace <<-YAML
+ ---
+ nested_hash:
+ contains_array:
+ - "Why shouldn't you write with a broken pencil?"
+ - "Because it's pointless!"
+ YAML
+
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Why shouldn't you write with a broken pencil?",
+ "Because it's pointless!",
+ ],
+ },
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
end
describe "against yaml lib" do
@@ -87,6 +134,16 @@ describe Bundler::YAMLSerializer do
"my-stand" => "I can totally keep secrets",
"but" => "The people I tell them to can't :P",
},
+ "more" => {
+ "first" => [
+ "Can a kangaroo jump higher than a house?",
+ "Of course, a house doesn't jump at all.",
+ ],
+ "second" => [
+ "What did the sea say to the sand?",
+ "Nothing, it simply waved.",
+ ],
+ },
"sales" => {
"item" => "A Parachute",
"description" => "Only used once, never opened.",
diff --git a/spec/plugins/command.rb b/spec/plugins/command_spec.rb
index 71e87a5b01..71e87a5b01 100644
--- a/spec/plugins/command.rb
+++ b/spec/plugins/command_spec.rb
diff --git a/spec/plugins/install.rb b/spec/plugins/install_spec.rb
index c9c0776f34..070a234a4a 100644
--- a/spec/plugins/install.rb
+++ b/spec/plugins/install_spec.rb
@@ -13,6 +13,7 @@ describe "bundler plugin install" do
bundle "plugin install no-foo --source file://#{gem_repo1}"
expect(out).to include("Could not find")
+ plugin_should_not_be_installed("no-foo")
end
it "installs from rubygems source" do
@@ -44,6 +45,29 @@ describe "bundler plugin install" do
plugin_should_be_installed("foo", "kung-foo")
end
+ it "works with different load paths" do
+ build_repo2 do
+ build_plugin "testing" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "fubar"
+ class Test < Bundler::Plugin::API
+ command "check2"
+
+ def exec(command, args)
+ puts "mate"
+ end
+ end
+ RUBY
+ s.require_paths = %w(lib src)
+ s.write("src/fubar.rb")
+ end
+ end
+ bundle "plugin install testing --source file://#{gem_repo2}"
+
+ bundle "check2", "no-color" => false
+ expect(out).to eq("mate")
+ end
+
context "malformatted plugin" do
it "fails when plugins.rb is missing" do
build_repo2 do
@@ -54,9 +78,9 @@ describe "bundler plugin install" do
expect(out).to include("plugins.rb was not found")
- expect(out).not_to include("Installed plugin")
-
expect(plugin_gems("charlie-1.0")).not_to be_directory
+
+ plugin_should_not_be_installed("charlie")
end
it "fails when plugins.rb throws exception on load" do
@@ -70,9 +94,9 @@ describe "bundler plugin install" do
bundle "plugin install chaplin --source file://#{gem_repo2}"
- expect(out).not_to include("Installed plugin")
-
expect(plugin_gems("chaplin-1.0")).not_to be_directory
+
+ plugin_should_not_be_installed("chaplin")
end
end
diff --git a/spec/plugins/source/example_spec.rb b/spec/plugins/source/example_spec.rb
new file mode 100644
index 0000000000..ead24e0d37
--- /dev/null
+++ b/spec/plugins/source/example_spec.rb
@@ -0,0 +1,446 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+describe "real source plugins" do
+ context "with a minimal source plugin" do
+ before do
+ build_repo2 do
+ build_plugin "bundler-source-mpath" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "fileutils"
+ require "bundler-source-mpath"
+
+ class MPath < Bundler::Plugin::API
+ source "mpath"
+
+ attr_reader :path
+
+ def initialize(opts)
+ super
+
+ @path = Pathname.new options["uri"]
+ end
+
+ def fetch_gemspec_files
+ @spec_files ||= begin
+ glob = "{,*,*/*}.gemspec"
+ if installed?
+ search_path = install_path
+ else
+ search_path = path
+ end
+ Dir["\#{search_path.to_s}/\#{glob}"]
+ end
+ end
+
+ def install(spec, opts)
+ mkdir_p(install_path.parent)
+ FileUtils.cp_r(path, install_path)
+
+ post_install(spec)
+
+ nil
+ end
+ end
+ RUBY
+ end # build_plugin
+ end
+
+ build_lib "a-path-gem"
+
+ gemfile <<-G
+ source "file://#{gem_repo2}" # plugin source
+ source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do
+ gem "a-path-gem"
+ end
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+
+ expect(out).to include("Bundle complete!")
+
+ should_be_installed("a-path-gem 1.0")
+ end
+
+ it "writes to lock file" do
+ bundle "install"
+
+ lockfile_should_be <<-G
+ PLUGIN SOURCE
+ remote: #{lib_path("a-path-gem-1.0")}
+ type: mpath
+ specs:
+ a-path-gem (1.0)
+
+ GEM
+ remote: file:#{gem_repo2}/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ a-path-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "provides correct #full_gem_path" do
+ bundle "install"
+ run <<-RUBY
+ puts Bundler.rubygems.find_name('a-path-gem').first.full_gem_path
+ RUBY
+ expect(out).to eq(bundle("show a-path-gem"))
+ end
+
+ it "installs the gem executables" do
+ build_lib "gem-with-bin" do |s|
+ s.executables = ["foo"]
+ end
+
+ install_gemfile <<-G
+ source "file://#{gem_repo2}" # plugin source
+ source "#{lib_path("gem-with-bin-1.0")}", :type => :mpath do
+ gem "gem-with-bin"
+ end
+ G
+
+ bundle "exec foo"
+ expect(out).to eq("1.0")
+ end
+
+ describe "bundle cache/package" do
+ let(:uri_hash) { Digest::SHA1.hexdigest(lib_path("a-path-gem-1.0").to_s) }
+ it "copies repository to vendor cache and uses it" do
+ bundle "install"
+ bundle "cache --all"
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.bundlecache")).to be_file
+
+ FileUtils.rm_rf lib_path("a-path-gem-1.0")
+ should_be_installed("a-path-gem 1.0")
+ end
+
+ it "copies repository to vendor cache and uses it even when installed with bundle --path" do
+ bundle "install --path vendor/bundle"
+ bundle "cache --all"
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+
+ FileUtils.rm_rf lib_path("a-path-gem-1.0")
+ should_be_installed("a-path-gem 1.0")
+ end
+
+ it "bundler package copies repository to vendor cache" do
+ bundle "install --path vendor/bundle"
+ bundle "package --all"
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+
+ FileUtils.rm_rf lib_path("a-path-gem-1.0")
+ should_be_installed("a-path-gem 1.0")
+ end
+ end
+
+ context "with lockfile" do
+ before do
+ lockfile <<-G
+ PLUGIN SOURCE
+ remote: #{lib_path("a-path-gem-1.0")}
+ type: mpath
+ specs:
+ a-path-gem (1.0)
+
+ GEM
+ remote: file:#{gem_repo2}/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ a-path-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+
+ should_be_installed("a-path-gem 1.0")
+ end
+ end
+ end
+
+ context "with a more elaborate source plugin" do
+ before do
+ build_repo2 do
+ build_plugin "bundler-source-gitp" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class SPlugin < Bundler::Plugin::API
+ source "gitp"
+
+ attr_reader :ref
+
+ def initialize(opts)
+ super
+
+ @ref = options["ref"] || options["branch"] || options["tag"] || "master"
+ @unlocked = false
+ end
+
+ def eql?(other)
+ other.is_a?(self.class) && uri == other.uri && ref == other.ref
+ end
+
+ alias_method :==, :eql?
+
+ def fetch_gemspec_files
+ @spec_files ||= begin
+ glob = "{,*,*/*}.gemspec"
+ if !cached?
+ cache_repo
+ end
+
+ if installed? && !@unlocked
+ path = install_path
+ else
+ path = cache_path
+ end
+
+ Dir["\#{path}/\#{glob}"]
+ end
+ end
+
+ def install(spec, opts)
+ mkdir_p(install_path.dirname)
+ rm_rf(install_path)
+ `git clone --no-checkout --quiet "\#{cache_path}" "\#{install_path}"`
+ Dir.chdir install_path do
+ `git reset --hard \#{revision}`
+ end
+
+ post_install(spec)
+
+ nil
+ end
+
+ def options_to_lock
+ opts = {"revision" => revision}
+ opts["ref"] = ref if ref != "master"
+ opts
+ end
+
+ def unlock!
+ @unlocked = true
+ @revision = latest_revision
+ end
+
+ def app_cache_dirname
+ "\#{base_name}-\#{shortref_for_path(revision)}"
+ end
+
+ private
+
+ def cache_path
+ @cache_path ||= cache_dir.join("gitp", base_name)
+ end
+
+ def cache_repo
+ `git clone --quiet \#{@options["uri"]} \#{cache_path}`
+ end
+
+ def cached?
+ File.directory?(cache_path)
+ end
+
+ def locked_revision
+ options["revision"]
+ end
+
+ def revision
+ @revision ||= locked_revision || latest_revision
+ end
+
+ def latest_revision
+ if !cached? || @unlocked
+ rm_rf(cache_path)
+ cache_repo
+ end
+
+ Dir.chdir cache_path do
+ `git rev-parse --verify \#{@ref}`.strip
+ end
+ end
+
+ def base_name
+ File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git")
+ end
+
+ def shortref_for_path(ref)
+ ref[0..11]
+ end
+
+ def install_path
+ @install_path ||= begin
+ git_scope = "\#{base_name}-\#{shortref_for_path(revision)}"
+
+ path = gem_install_dir.join(git_scope)
+
+ if !path.exist? && requires_sudo?
+ user_bundle_path.join(ruby_scope).join(git_scope)
+ else
+ path
+ end
+ end
+ end
+
+ def installed?
+ File.directory?(install_path)
+ end
+ end
+ RUBY
+ end
+ end
+
+ build_git "ma-gitp-gem"
+
+ gemfile <<-G
+ source "file://#{gem_repo2}" # plugin source
+ source "file://#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
+ gem "ma-gitp-gem"
+ end
+ G
+ end
+
+ it "handles the source option" do
+ bundle "install"
+ expect(out).to include("Bundle complete!")
+ should_be_installed("ma-gitp-gem 1.0")
+ end
+
+ it "writes to lock file" do
+ revision = revision_for(lib_path("ma-gitp-gem-1.0"))
+ bundle "install"
+
+ lockfile_should_be <<-G
+ PLUGIN SOURCE
+ remote: file://#{lib_path("ma-gitp-gem-1.0")}
+ type: gitp
+ revision: #{revision}
+ specs:
+ ma-gitp-gem (1.0)
+
+ GEM
+ remote: file:#{gem_repo2}/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ ma-gitp-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ context "with lockfile" do
+ before do
+ revision = revision_for(lib_path("ma-gitp-gem-1.0"))
+ lockfile <<-G
+ PLUGIN SOURCE
+ remote: file://#{lib_path("ma-gitp-gem-1.0")}
+ type: gitp
+ revision: #{revision}
+ specs:
+ ma-gitp-gem (1.0)
+
+ GEM
+ remote: file:#{gem_repo2}/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ ma-gitp-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+ should_be_installed("ma-gitp-gem 1.0")
+ end
+
+ it "uses the locked ref" do
+ update_git "ma-gitp-gem"
+ bundle "install"
+
+ run <<-RUBY
+ require 'ma-gitp-gem'
+ puts "WIN" unless defined?(MAGITPGEM_PREV_REF)
+ RUBY
+ expect(out).to eq("WIN")
+ end
+
+ it "updates the deps on bundler update" do
+ update_git "ma-gitp-gem"
+ bundle "update ma-gitp-gem"
+
+ run <<-RUBY
+ require 'ma-gitp-gem'
+ puts "WIN" if defined?(MAGITPGEM_PREV_REF)
+ RUBY
+ expect(out).to eq("WIN")
+ end
+
+ it "updates the deps on change in gemfile" do
+ update_git "ma-gitp-gem", "1.1", :path => lib_path("ma-gitp-gem-1.0"), :gemspec => true
+ gemfile <<-G
+ source "file://#{gem_repo2}" # plugin source
+ source "file://#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
+ gem "ma-gitp-gem", "1.1"
+ end
+ G
+ bundle "install"
+
+ should_be_installed("ma-gitp-gem 1.1")
+ end
+ end
+
+ describe "bundle cache with gitp" do
+ it "copies repository to vendor cache and uses it" do
+ git = build_git "foo"
+ ref = git.ref_for("master", 11)
+
+ install_gemfile <<-G
+ source "file://#{gem_repo2}" # plugin source
+ source '#{lib_path("foo-1.0")}', :type => :gitp do
+ gem "foo"
+ end
+ G
+
+ bundle "cache --all"
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file
+
+ FileUtils.rm_rf lib_path("foo-1.0")
+ should_be_installed "foo 1.0"
+ end
+ end
+ end
+end
diff --git a/spec/plugins/source_spec.rb b/spec/plugins/source_spec.rb
new file mode 100644
index 0000000000..ea164e9811
--- /dev/null
+++ b/spec/plugins/source_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+describe "bundler source plugin" do
+ describe "plugins dsl eval for #source with :type option" do
+ before do
+ update_repo2 do
+ build_plugin "bundler-source-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class OPSource < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+ end
+
+ it "installs bundler-source-* gem when no handler for source is present" do
+ install_gemfile <<-G
+ source "file://#{gem_repo2}"
+ source "file://#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+
+ plugin_should_be_installed("bundler-source-psource")
+ end
+
+ it "enables the plugin to require a lib path" do
+ update_repo2 do
+ build_plugin "bundler-source-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "bundler-source-psource"
+ class PSource < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+
+ install_gemfile <<-G
+ source "file://#{gem_repo2}"
+ source "file://#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ context "with an explicit handler" do
+ before do
+ update_repo2 do
+ build_plugin "another-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Cheater < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+ end
+
+ context "installed though cli" do
+ before do
+ bundle "plugin install another-psource --source file://#{gem_repo2}"
+
+ install_gemfile <<-G
+ source "file://#{gem_repo2}"
+ source "file://#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "doesn't install the default one" do
+ plugin_should_not_be_installed("bundler-source-psource")
+ end
+ end
+
+ context "explicit presence in gemfile" do
+ before do
+ install_gemfile <<-G
+ source "file://#{gem_repo2}"
+
+ plugin "another-psource"
+
+ source "file://#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "installs the explicit one" do
+ plugin_should_be_installed("another-psource")
+ end
+
+ it "doesn't install the default one" do
+ plugin_should_not_be_installed("bundler-source-psource")
+ end
+ end
+
+ context "explicit default source" do
+ before do
+ install_gemfile <<-G
+ source "file://#{gem_repo2}"
+
+ plugin "bundler-source-psource"
+
+ source "file://#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "installs the default one" do
+ plugin_should_be_installed("bundler-source-psource")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/builders.rb b/spec/support/builders.rb
index 8e19f9ae94..d091ff69a9 100644
--- a/spec/support/builders.rb
+++ b/spec/support/builders.rb
@@ -532,7 +532,7 @@ module Spec
@spec.executables.each do |file|
executable = "#{@spec.bindir}/#{file}"
@spec.files << executable
- write executable, "#!/usr/bin/env ruby\nrequire '#{@name}' ; puts #{@name.upcase}"
+ write executable, "#!/usr/bin/env ruby\nrequire '#{@name}' ; puts #{Builders.constantize(@name)}"
end
end
@@ -629,6 +629,7 @@ module Spec
def _build(options)
libpath = options[:path] || _default_path
+ update_gemspec = options[:gemspec] || false
Dir.chdir(libpath) do
silently "git checkout master"
@@ -653,7 +654,7 @@ module Spec
_default_files.keys.each do |path|
_default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'"
end
- super(options.merge(:path => libpath, :gemspec => false))
+ super(options.merge(:path => libpath, :gemspec => update_gemspec))
`git add *`
`git commit -m "BUMP"`
end
diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb
index 76b0c32eef..1a05c76637 100644
--- a/spec/support/matchers.rb
+++ b/spec/support/matchers.rb
@@ -65,12 +65,19 @@ module Spec
def plugin_should_be_installed(*names)
names.each do |name|
- path = Plugin.installed?(name)
+ path = Bundler::Plugin.installed?(name)
expect(path).to be_truthy
expect(Pathname.new(path).join("plugins.rb")).to exist
end
end
+ def plugin_should_not_be_installed(*names)
+ names.each do |name|
+ path = Bundler::Plugin.installed?(name)
+ expect(path).to be_falsey
+ end
+ end
+
def should_be_locked
expect(bundled_app("Gemfile.lock")).to exist
end