diff options
author | Homu <homu@barosl.com> | 2016-07-05 23:44:57 +0900 |
---|---|---|
committer | Homu <homu@barosl.com> | 2016-07-05 23:44:57 +0900 |
commit | cc4df62a4707281fc657101a93710c63ed957a70 (patch) | |
tree | 51fc5cfce170b2f0743402f304f723e6aa429434 | |
parent | 2439ac84cfd731aaee9ebaca284cb077cf601969 (diff) | |
parent | 57e817290335570abc1aacdf778e255477403302 (diff) | |
download | bundler-cc4df62a4707281fc657101a93710c63ed957a70.tar.gz |
Auto merge of #4674 - asutoshpalai:plugin, r=segiddins
[Plugin] Source plugins
Adds source plugin. This is in continuation of #4608.
32 files changed, 1751 insertions, 189 deletions
diff --git a/lib/bundler.rb b/lib/bundler.rb index a2260e01..0ba58e51 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 33b0557f..bef62f3b 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 358784a9..04ef641b 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 a57dbcac..d5b28440 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 9aabd73a..f5366d2a 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 9631446d..9ff4b738 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 00000000..78514563 --- /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 f65054f0..4bfc8437 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 1e39eb00..4abf85fb 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 2c10bb24..a50d0cee 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 6b1f1aee..33f5e5af 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 53db29a9..88c446e1 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 76955ee4..5344ab69 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 bbdd30b1..3c4d914f 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 0aa27bd5..abc46d5a 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 0595cbdc..fdc77cb2 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 327baa4e..dede8fd5 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 d9bb32b4..98d7b68c 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 00000000..d62127a6 --- /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 23134dd8..a227d315 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 876564f2..9a694833 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 f969aad1..15db1789 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 33a0ddea..a914aabb 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 517f539b..0c81df22 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 d7567d6e..56f17f9a 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 53dbbc67..0b326133 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 71e87a5b..71e87a5b 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 c9c0776f..070a234a 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 00000000..ead24e0d --- /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 00000000..ea164e98 --- /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 8e19f9ae..d091ff69 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 76b0c32e..1a05c766 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 |