diff --git a/bin/vagrant b/bin/vagrant index cd7fbc9d3..448803bd7 100755 --- a/bin/vagrant +++ b/bin/vagrant @@ -13,6 +13,40 @@ if idx = argv.index("--") argv = argv.slice(0, idx) end +# Fast path the version of Vagrant +if argv.include?("-v") || argv.include?("--version") + require "vagrant/version" + puts "Vagrant #{Vagrant::VERSION}" + exit 0 +end + +# This is kind of hacky, and I'd love to find a better way to do this, but +# if we're accessing the plugin interface, we want to NOT load plugins +# for this run, because they can actually interfere with the function +# of the plugin interface. +argv.each do |arg| + if !arg.start_with?("-") + if arg == "plugin" + ENV["VAGRANT_NO_PLUGINS"] = "1" + ENV["VAGRANT_VAGRANTFILE"] = "plugin_command_#{Time.now.to_i}" + end + + break + end +end + +# First, make sure that we're executing using the proper Bundler context +# with our plugins. If we're not, then load that and reload Vagrant. +if !ENV["VAGRANT_INTERNAL_BUNDLERIZED"] + require "rbconfig" + ruby_path = File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"]) + Kernel.exec( + ruby_path, + File.expand_path("../../lib/vagrant/pre-rubygems.rb", __FILE__), + *ARGV) + raise "Fatal error: this line should never be reached" +end + # Set logging level to `debug`. This is done before loading 'vagrant', as it # sets up the logging system. if argv.include?("--debug") @@ -20,6 +54,14 @@ if argv.include?("--debug") ENV["VAGRANT_LOG"] = "debug" end +# Require some stuff that is NOT dependent on RubyGems +require "vagrant/shared_helpers" + +# Setup our dependencies by initializing Bundler. If we're using plugins, +# then also initialize the paths to the plugins. +require "bundler" +Bundler.setup + require 'log4r' require 'vagrant' require 'vagrant/cli' @@ -72,27 +114,6 @@ end # Default to colored output opts[:ui_class] ||= Vagrant::UI::Colored -# This is kind of hacky, and I'd love to find a better way to do this, but -# if we're accessing the plugin interface, we want to NOT load plugins -# for this run, because they can actually interfere with the function -# of the plugin interface. -argv.each do |arg| - if !arg.start_with?("-") - if arg == "plugin" - ENV["VAGRANT_NO_PLUGINS"] = "1" - ENV["VAGRANT_VAGRANTFILE"] = "plugin_command_#{Time.now.to_i}" - end - - break - end -end - -# Fast path the version of Vagrant -if argv.include?("-v") || argv.include?("--version") - puts "Vagrant #{Vagrant::VERSION}" - exit 0 -end - # Recombine the arguments argv << "--" argv += argv_extra @@ -105,21 +126,8 @@ begin env = Vagrant::Environment.new(opts) if !Vagrant.in_installer? - warned = false - - # If we're in a bundler environment, we assume it is for plugin - # development and will let the user know that. - if defined?(Bundler) - require 'bundler/shared_helpers' - if Bundler::SharedHelpers.in_bundle? - env.ui.warn(I18n.t("vagrant.general.in_bundler")) - env.ui.warn("") - warned = true - end - end - # If we're not in the installer, warn. - env.ui.warn(I18n.t("vagrant.general.not_in_installer")) if !warned + env.ui.warn(I18n.t("vagrant.general.not_in_installer")) end begin diff --git a/lib/vagrant.rb b/lib/vagrant.rb index e8d46327f..fcb102d5a 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -1,5 +1,17 @@ -require 'log4r' +# This file is load before RubyGems are loaded, and allow us to actually +# resolve plugin dependencies and load the proper versions of everything. + +require "vagrant/shared_helpers" + +if Vagrant.plugins_enabled? && !defined?(Bundler) + puts "It appears that Vagrant was not properly loaded. Specifically," + puts "the bundler context Vagrant requires was not setup. Please execute" + puts "vagrant using only the `vagrant` executable." + abort +end + require 'rubygems' +require 'log4r' # Enable logging if it is requested. We do this before # anything else so that we can setup the output before @@ -66,6 +78,7 @@ end # We need these components always so instead of an autoload we # just require them explicitly here. +require "vagrant/plugin" require "vagrant/registry" module Vagrant @@ -118,12 +131,6 @@ module Vagrant !!ENV["VAGRANT_INSTALLER_ENV"] end - # The source root is the path to the root directory of - # the Vagrant gem. - def self.source_root - @source_root ||= Pathname.new(File.expand_path('../../', __FILE__)) - end - # Configure a Vagrant environment. The version specifies the version # of the configuration that is expected by the block. The block, based # on that version, configures the environment. @@ -178,72 +185,11 @@ module Vagrant "#{version} #{component}" end - # This should be used instead of Ruby's built-in `require` in order to - # load a Vagrant plugin. This will load the given plugin by first doing - # a normal `require`, giving a nice error message if things go wrong, - # and second by verifying that a Vagrant plugin was actually defined in - # the process. - # - # @param [String] name Name of the plugin to load. + # @deprecated def self.require_plugin(name) - logger = Log4r::Logger.new("vagrant::root") - - if ENV["VAGRANT_NO_PLUGINS"] - logger.warn("VAGRANT_NO_PLUGINS is set, not loading 3rd party plugin: #{name}") - return - end - - # Redirect stdout/stderr so that we can output it in our own way. - previous_stderr = $stderr - previous_stdout = $stdout - $stderr = StringIO.new - $stdout = StringIO.new - - # Attempt the normal require - begin - require name - plugin("2").manager.plugin_required(name) - rescue Exception => e - # Since this is a rare case, we create a one-time logger here - # in order to output the error - logger.error("Failed to load plugin: #{name}") - logger.error(" -- Error: #{e.inspect}") - logger.error(" -- Backtrace:") - logger.error(e.backtrace.join("\n")) - - # If it is a LoadError we first try to see if it failed loading - # the top-level entrypoint. If so, then we report a different error. - if e.is_a?(LoadError) - # Parse the message in order to get what failed to load, and - # add some extra protection around if the message is different. - parts = e.to_s.split(" -- ", 2) - if parts.length == 2 && parts[1] == name - raise Errors::PluginLoadError, :plugin => name - end - end - - # Get the string data out from the stdout/stderr captures - stderr = $stderr.string - stdout = $stdout.string - if !stderr.empty? || !stdout.empty? - raise Errors::PluginLoadFailedWithOutput, - :plugin => name, - :stderr => stderr, - :stdout => stdout - end - - # And raise an error itself - raise Errors::PluginLoadFailed, - :plugin => name - end - - # Log plugin version - gem = Gem::Specification.find { |spec| spec.name == name } - version = gem ? gem.version : "" - logger.info("Loaded plugin #{name}, version #{version}") - ensure - $stderr = previous_stderr if previous_stderr - $stdout = previous_stdout if previous_stdout + puts "Vagrant.require_plugin is deprecated and has no effect any longer." + puts "Use `vagrant plugin` commands to manage plugins. This warning will" + puts "be removed in the next version of Vagrant." end # This allows a Vagrantfile to specify the version of Vagrant that is @@ -312,3 +258,6 @@ Vagrant.source_root.join("plugins").children(true).each do |directory| # Otherwise, attempt to load from sub-directories directory.children(true).each(&plugin_load_proc) end + +# If we have plugins enabled, then load those +Bundler.require(:plugins) if Vagrant.plugins_enabled? diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb new file mode 100644 index 000000000..92a06fa0c --- /dev/null +++ b/lib/vagrant/bundler.rb @@ -0,0 +1,266 @@ +require "monitor" +require "pathname" +require "set" +require "tempfile" + +require "bundler" + +require_relative "shared_helpers" +require_relative "version" + +module Vagrant + # This class manages Vagrant's interaction with Bundler. Vagrant uses + # Bundler as a way to properly resolve all dependencies of Vagrant and + # all Vagrant-installed plugins. + class Bundler + def self.instance + @bundler ||= self.new + end + + def initialize + @monitor = Monitor.new + + @gem_home = ENV["GEM_HOME"] + @gem_path = ENV["GEM_PATH"] + + # Set the Bundler UI to be a silent UI. We have to add the + # `silence` method to it because Bundler UI doesn't have it. + ::Bundler.ui = ::Bundler::UI.new + if !::Bundler.ui.respond_to?(:silence) + ui = ::Bundler.ui + def ui.silence(*args) + yield + end + end + end + + # Initializes Bundler and the various gem paths so that we can begin + # loading gems. This must only be called once. + def init!(plugins) + # Setup the Bundler configuration + @configfile = File.open(Tempfile.new("vagrant").path + "1", "w+") + @configfile.close + + # Build up the Gemfile for our Bundler context. We make sure to + # lock Vagrant to our current Vagrant version. In addition to that, + # we add all our plugin dependencies. + @gemfile = build_gemfile(plugins) + + # Set the environmental variables for Bundler + ENV["BUNDLE_CONFIG"] = @configfile.path + ENV["BUNDLE_GEMFILE"] = @gemfile.path + ENV["GEM_PATH"] = + "#{Vagrant.user_data_path.join("gems")}#{::File::PATH_SEPARATOR}#{@gem_path}" + Gem.clear_paths + end + + # Installs the list of plugins. + # + # @param [Hash] plugins + # @return [Array] + def install(plugins, local=false) + internal_install(plugins, nil, local: local) + end + + # Installs a local '*.gem' file so that Bundler can find it. + # + # @param [String] path Path to a local gem file. + # @return [Gem::Specification] + def install_local(path) + # We have to do this load here because this file can be loaded + # before RubyGems is actually loaded. + require "rubygems/dependency_installer" + begin + require "rubygems/format" + rescue LoadError + # rubygems 2.x + end + + # If we're installing from a gem file, determine the name + # based on the spec in the file. + pkg = if defined?(Gem::Format) + # RubyGems 1.x + Gem::Format.from_file_by_path(path) + else + # RubyGems 2.x + Gem::Package.new(path) + end + + # Install the gem manually. If the gem exists locally, then + # Bundler shouldn't attempt to get it remotely. + with_isolated_gem do + installer = Gem::DependencyInstaller.new( + :document => [], :prerelease => false) + installer.install(path, "= #{pkg.spec.version}") + end + + pkg.spec + end + + # Update updates the given plugins, or every plugin if none is given. + # + # @param [Hash] plugins + # @param [Array] specific Specific plugin names to update. If + # empty or nil, all plugins will be updated. + def update(plugins, specific) + specific ||= [] + update = true + update = { gems: specific } if !specific.empty? + internal_install(plugins, update) + end + + # Clean removes any unused gems. + def clean(plugins) + gemfile = build_gemfile(plugins) + lockfile = "#{gemfile.path}.lock" + definition = ::Bundler::Definition.build(gemfile, lockfile, nil) + root = File.dirname(gemfile.path) + + with_isolated_gem do + runtime = ::Bundler::Runtime.new(root, definition) + runtime.clean + end + end + + # During the duration of the yielded block, Bundler loud output + # is enabled. + def verbose + @monitor.synchronize do + begin + old_ui = ::Bundler.ui + require 'bundler/vendored_thor' + ::Bundler.ui = ::Bundler::UI::Shell.new + yield + ensure + ::Bundler.ui = old_ui + end + end + end + + protected + + # Builds a valid Gemfile for use with Bundler given the list of + # plugins. + # + # @return [Tempfile] + def build_gemfile(plugins) + f = File.open(Tempfile.new("vagrant").path + "2", "w+") + f.tap do |gemfile| + gemfile.puts(%Q[source "https://rubygems.org"]) + gemfile.puts(%Q[source "http://gems.hashicorp.com"]) + sources = plugins.values.map { |p| p["sources"] }.flatten.compact.uniq + sources.each do |source| + next if source == "" + gemfile.puts(%Q[source "#{source}"]) + end + + gemfile.puts(%Q[gem "vagrant", "= #{Vagrant::VERSION}"]) + + gemfile.puts("group :plugins do") + plugins.each do |name, plugin| + version = plugin["gem_version"] + version = nil if version == "" + + opts = {} + if plugin["require"] && plugin["require"] != "" + opts[:require] = plugin["require"] + end + + gemfile.puts(%Q[gem "#{name}", #{version.inspect}, #{opts.inspect}]) + end + gemfile.puts("end") + + gemfile.close + end + end + + # This installs a set of plugins and optionally updates those gems. + # + # @param [Hash] plugins + # @param [Hash, Boolean] update If true, updates all plugins, otherwise + # can be a hash of options. See Bundler.definition. + # @return [Array] + def internal_install(plugins, update, **extra) + gemfile = build_gemfile(plugins) + lockfile = "#{gemfile.path}.lock" + definition = ::Bundler::Definition.build(gemfile, lockfile, update) + root = File.dirname(gemfile.path) + opts = {} + opts["local"] = true if extra[:local] + + with_isolated_gem do + ::Bundler::Installer.install(root, definition, opts) + end + + # TODO(mitchellh): clean gems here... for some reason when I put + # it in on install, we get a GemNotFound exception. Gotta investigate. + + definition.specs + rescue ::Bundler::VersionConflict => e + raise Errors::PluginInstallVersionConflict, + conflicts: e.to_s.gsub("Bundler", "Vagrant") + end + + def with_isolated_gem + # Remove bundler settings so that Bundler isn't loaded when building + # native extensions because it causes all sorts of problems. + old_rubyopt = ENV["RUBYOPT"] + old_gemfile = ENV["BUNDLE_GEMFILE"] + ENV["BUNDLE_GEMFILE"] = Tempfile.new("vagrant-gemfile").path + ENV["RUBYOPT"] = (ENV["RUBYOPT"] || "").gsub(/-rbundler\/setup\s*/, "") + + # Set the GEM_HOME so gems are installed only to our local gem dir + ENV["GEM_HOME"] = Vagrant.user_data_path.join("gems").to_s + + # Clear paths so that it reads the new GEM_HOME setting + Gem.paths = ENV + + # Reset the all specs override that Bundler does + old_all = Gem::Specification._all + Gem::Specification.all = nil + + # /etc/gemrc and so on. + old_config = nil + begin + old_config = Gem.configuration + rescue Psych::SyntaxError + # Just ignore this. This means that the ".gemrc" file has + # an invalid syntax and can't be loaded. We don't care, because + # when we set Gem.configuration to nil later, it'll force a reload + # if it is needed. + end + Gem.configuration = NilGemConfig.new + + # Use a silent UI so that we have no output + Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do + return yield + end + ensure + ENV["BUNDLE_GEMFILE"] = old_gemfile + ENV["GEM_HOME"] = @gem_home + ENV["RUBYOPT"] = old_rubyopt + + Gem.configuration = old_config + Gem.paths = ENV + Gem::Specification.all = old_all + end + + # This is pretty hacky but it is a custom implementation of + # Gem::ConfigFile so that we don't load any gemrc files. + class NilGemConfig < Gem::ConfigFile + def initialize + # We _can not_ `super` here because that can really mess up + # some other configuration state. We need to just set everything + # directly. + + @api_keys = {} + @args = [] + @backtrace = false + @bulk_threshold = 1000 + @hash = {} + @update_sources = true + @verbose = true + end + end + end +end diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 62d059dd1..1365ebc83 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -128,9 +128,6 @@ module Vagrant @default_private_key_path = @home_path.join("insecure_private_key") copy_insecure_private_key - # Load the plugins - load_plugins - # Call the hooks that does not require configurations to be loaded # by using a "clean" action runner hook(:environment_plugins_loaded, runner: Action::Runner.new(env: self)) @@ -591,7 +588,7 @@ module Vagrant def setup_home_path @home_path = Pathname.new(File.expand_path(@home_path || ENV["VAGRANT_HOME"] || - default_home_path)) + Vagrant.user_data_path)) @logger.info("Home path: #{@home_path}") # Setup the list of child directories that need to be created if they @@ -691,23 +688,6 @@ module Vagrant end end - # This returns the default home directory path for Vagrant, which - # can differ depending on the system. - # - # @return [Pathname] - def default_home_path - path = "~/.vagrant.d" - - # On Windows, we default ot the USERPROFILE directory if it - # is available. This is more compatible with Cygwin and sharing - # the home directory across shells. - if Util::Platform.windows? && ENV["USERPROFILE"] - path = "#{ENV["USERPROFILE"]}/.vagrant.d" - end - - Pathname.new(path) - end - # Finds the Vagrantfile in the given directory. # # @param [Pathname] path Path to search in. @@ -722,55 +702,6 @@ module Vagrant nil end - # Loads the Vagrant plugins by properly setting up RubyGems so that - # our private gem repository is on the path. - def load_plugins - # Add our private gem path to the gem path and reset the paths - # that Rubygems knows about. - ENV["GEM_PATH"] = "#{@gems_path}#{::File::PATH_SEPARATOR}#{ENV["GEM_PATH"]}" - ::Gem.clear_paths - - # If we're in a Bundler environment, don't load plugins. This only - # happens in plugin development environments. - if defined?(Bundler) - require 'bundler/shared_helpers' - if Bundler::SharedHelpers.in_bundle? - @logger.warn("In a bundler environment, not loading environment plugins!") - return - end - end - - # This keeps track of the old plugins that need to be reinstalled - # because they were installed with an old version of Ruby. - reinstall = [] - - # Load the plugins - plugins_json_file = @home_path.join("plugins.json") - @logger.debug("Loading plugins from: #{plugins_json_file}") - state = VagrantPlugins::CommandPlugin::StateFile.new(plugins_json_file) - state.installed_plugins.each do |name, extra| - # If the Ruby version changed, then they need to reinstall the plugin - if extra["ruby_version"] != RUBY_VERSION - reinstall << name - next - end - - @logger.info("Loading plugin from JSON: #{name}") - begin - Vagrant.require_plugin(name) - rescue Errors::PluginLoadError => e - @ui.error(e.message + "\n") - rescue Errors::PluginLoadFailed => e - @ui.error(e.message + "\n") - end - end - - if !reinstall.empty? - @ui.warn(I18n.t("vagrant.plugin_needs_reinstall", - names: reinstall.join(", "))) - end - end - # This upgrades a Vagrant 1.0.x "dotfile" to the new V2 format. # # This is a destructive process. Once the upgrade is complete, the diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index a4fd6090b..ce21aef7b 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -168,6 +168,10 @@ module Vagrant error_key(:failed, "vagrant.actions.box.verify") end + class BundlerError < VagrantError + error_key(:bundler_error) + end + class CFEngineBootstrapFailed < VagrantError error_key(:cfengine_bootstrap_failed) end @@ -428,6 +432,10 @@ module Vagrant error_key(:plugin_gem_error) end + class PluginGemNotFound < VagrantError + error_key(:plugin_gem_not_found) + end + class PluginInstallBadEntryPoint < VagrantError error_key(:plugin_install_bad_entry_point) end @@ -440,6 +448,10 @@ module Vagrant error_key(:plugin_install_not_found) end + class PluginInstallVersionConflict < VagrantError + error_key(:plugin_install_version_conflict) + end + class PluginLoadError < VagrantError error_key(:plugin_load_error) end diff --git a/lib/vagrant/plugin.rb b/lib/vagrant/plugin.rb index 5538746a5..66dcbdbec 100644 --- a/lib/vagrant/plugin.rb +++ b/lib/vagrant/plugin.rb @@ -1,6 +1,8 @@ module Vagrant module Plugin - autoload :V1, "vagrant/plugin/v1" - autoload :V2, "vagrant/plugin/v2" + autoload :V1, "vagrant/plugin/v1" + autoload :V2, "vagrant/plugin/v2" + autoload :Manager, "vagrant/plugin/manager" + autoload :StateFile, "vagrant/plugin/state_file" end end diff --git a/lib/vagrant/plugin/manager.rb b/lib/vagrant/plugin/manager.rb new file mode 100644 index 000000000..b36f0b264 --- /dev/null +++ b/lib/vagrant/plugin/manager.rb @@ -0,0 +1,134 @@ +require "set" + +require_relative "../bundler" +require_relative "../shared_helpers" +require_relative "state_file" + +module Vagrant + module Plugin + # The Manager helps with installing, listing, and initializing plugins. + class Manager + # Returns the path to the [StateFile] for global plugins. + # + # @return [Pathname] + def self.global_plugins_file + Vagrant.user_data_path.join("plugins.json") + end + + def self.instance + @instance ||= self.new(global_plugins_file) + end + + # @param [Pathname] global_file + def initialize(global_file) + @global_file = StateFile.new(global_file) + end + + # Installs another plugin into our gem directory. + # + # @param [String] name Name of the plugin (gem) + # @return [Gem::Specification] + def install_plugin(name, **opts) + local = false + if name =~ /\.gem$/ + # If this is a gem file, then we install that gem locally. + local_spec = Vagrant::Bundler.instance.install_local(name) + name = local_spec.name + opts[:version] = "= #{local_spec.version}" + local = true + end + + plugins = installed_plugins + plugins[name] = { + "require" => opts[:require], + "gem_version" => opts[:version], + "sources" => opts[:sources], + } + + result = nil + install_lambda = lambda do + Vagrant::Bundler.instance.install(plugins, local).each do |spec| + next if spec.name != name + next if result && result.version >= spec.version + result = spec + end + end + + if opts[:verbose] + Vagrant::Bundler.instance.verbose(&install_lambda) + else + install_lambda.call + end + + # If the version constraint is just a specific version, don't + # store the constraint. + opts.delete(:version) if opts[:version] && opts[:version] =~ /^\d/ + + # Add the plugin to the state file + @global_file.add_plugin( + result.name, + version: opts[:version], + require: opts[:require], + sources: opts[:sources], + ) + + result + rescue ::Bundler::GemNotFound + raise Errors::PluginGemNotFound, name: name + rescue ::Bundler::BundlerError => e + raise Errors::BundlerError, message: e.to_s + end + + # Uninstalls the plugin with the given name. + # + # @param [String] name + def uninstall_plugin(name) + @global_file.remove_plugin(name) + + # Clean the environment, removing any old plugins + Vagrant::Bundler.instance.clean(installed_plugins) + rescue ::Bundler::BundlerError => e + raise Errors::BundlerError, message: e.to_s + end + + # Updates all or a specific set of plugins. + def update_plugins(specific) + Vagrant::Bundler.instance.update(installed_plugins, specific) + rescue ::Bundler::BundlerError => e + raise Errors::BundlerError, message: e.to_s + end + + # This returns the list of plugins that should be enabled. + # + # @return [Hash] + def installed_plugins + @global_file.installed_plugins + end + + # This returns the list of plugins that are installed as + # Gem::Specifications. + # + # @return [Array] + def installed_specs + installed = Set.new(installed_plugins.keys) + + # Go through the plugins installed in this environment and + # get the latest version of each. + installed_map = {} + Gem::Specification.find_all.each do |spec| + # Ignore specs that aren't in our installed list + next if !installed.include?(spec.name) + + # If we already have a newer version in our list of installed, + # then ignore it + next if installed_map.has_key?(spec.name) && + installed_map[spec.name].version >= spec.version + + installed_map[spec.name] = spec + end + + installed_map.values + end + end + end +end diff --git a/plugins/commands/plugin/state_file.rb b/lib/vagrant/plugin/state_file.rb similarity index 64% rename from plugins/commands/plugin/state_file.rb rename to lib/vagrant/plugin/state_file.rb index 55603ffb8..b9161672f 100644 --- a/plugins/commands/plugin/state_file.rb +++ b/lib/vagrant/plugin/state_file.rb @@ -1,7 +1,7 @@ require "json" -module VagrantPlugins - module CommandPlugin +module Vagrant + module Plugin # This is a helper to deal with the plugin state file that Vagrant # uses to track what plugins are installed and activated and such. class StateFile @@ -27,17 +27,27 @@ module VagrantPlugins # Add a plugin that is installed to the state file. # # @param [String] name The name of the plugin - def add_plugin(name) - if !@data["installed"].has_key?(name) - @data["installed"][name] = { - "ruby_version" => RUBY_VERSION, - "vagrant_version" => Vagrant::VERSION, - } - end + def add_plugin(name, **opts) + @data["installed"][name] = { + "ruby_version" => RUBY_VERSION, + "vagrant_version" => Vagrant::VERSION, + "gem_version" => opts[:version] || "", + "require" => opts[:require] || "", + "sources" => opts[:sources] || [], + } save! end + # Adds a RubyGems index source to look up gems. + # + # @param [String] url URL of the source. + def add_source(url) + @data["sources"] ||= [] + @data["sources"] << url if !@data["sources"].include?(url) + save! + end + # This returns a hash of installed plugins according to the state # file. Note that this may _not_ directly match over to actually # installed gems. @@ -55,6 +65,23 @@ module VagrantPlugins save! end + # Remove a source for RubyGems. + # + # @param [String] url URL of the source + def remove_source(url) + @data["sources"] ||= [] + @data["sources"].delete(url) + save! + end + + # Returns the list of RubyGems sources that will be searched for + # plugins. + # + # @return [Array] + def sources + @data["sources"] || [] + end + # This saves the state back into the state file. def save! @path.open("w+") do |f| diff --git a/lib/vagrant/pre-rubygems.rb b/lib/vagrant/pre-rubygems.rb new file mode 100644 index 000000000..9cea06038 --- /dev/null +++ b/lib/vagrant/pre-rubygems.rb @@ -0,0 +1,30 @@ +# This file is to be loaded _before_ any RubyGems are loaded. This file +# initializes the Bundler context so that Vagrant and its associated plugins +# can load properly, and then execs out into Vagrant again. + +if defined?(Bundler) + require "bundler/shared_helpers" + if Bundler::SharedHelpers.in_bundle? + if ENV["VAGRANT_FORCE_PLUGINS"] + puts "Vagrant appears to be running in a Bundler environment. Normally," + puts "plugins would not be loaded, but VAGRANT_FORCE_PLUGINS is enabled," + puts "so they will be." + puts + else + puts "Vagrant appears to be running in a Bundler environment. Plugins" + puts "will not be loaded and plugin commands are disabled." + puts + ENV["VAGRANT_NO_PLUGINS"] = "1" + end + end +end + +require_relative "bundler" +require_relative "plugin/manager" +require_relative "shared_helpers" + +plugins = Vagrant::Plugin::Manager.instance.installed_plugins +Vagrant::Bundler.instance.init!(plugins) + +ENV["VAGRANT_INTERNAL_BUNDLERIZED"] = "1" +Kernel.exec("vagrant", *ARGV) diff --git a/lib/vagrant/shared_helpers.rb b/lib/vagrant/shared_helpers.rb new file mode 100644 index 000000000..78cdb4a92 --- /dev/null +++ b/lib/vagrant/shared_helpers.rb @@ -0,0 +1,34 @@ +require "pathname" + +module Vagrant + # This returns whether or not 3rd party plugins should be loaded. + # + # @return [Boolean] + def self.plugins_enabled? + !ENV["VAGRANT_NO_PLUGINS"] + end + + # The source root is the path to the root directory of the Vagrant source. + # + # @return [Pathname] + def self.source_root + @source_root ||= Pathname.new(File.expand_path('../../../', __FILE__)) + end + + # This returns the path to the ~/.vagrant.d folder where Vagrant's + # per-user state is stored. + # + # @return [Pathname] + def self.user_data_path + path = "~/.vagrant.d" + + # On Windows, we default ot the USERPROFILE directory if it + # is available. This is more compatible with Cygwin and sharing + # the home directory across shells. + if ENV["USERPROFILE"] + path = "#{ENV["USERPROFILE"]}/.vagrant.d" + end + + return Pathname.new(path).expand_path + end +end diff --git a/plugins/commands/plugin/action.rb b/plugins/commands/plugin/action.rb index a132aec13..832bc3508 100644 --- a/plugins/commands/plugin/action.rb +++ b/plugins/commands/plugin/action.rb @@ -8,16 +8,14 @@ module VagrantPlugins # This middleware sequence will install a plugin. def self.action_install Vagrant::Action::Builder.new.tap do |b| - b.use BundlerCheck b.use InstallGem - b.use PruneGems end end # This middleware sequence licenses paid addons. def self.action_license Vagrant::Action::Builder.new.tap do |b| - b.use BundlerCheck + b.use PluginExistsCheck b.use LicensePlugin end end @@ -25,7 +23,6 @@ module VagrantPlugins # This middleware sequence will list all installed plugins. def self.action_list Vagrant::Action::Builder.new.tap do |b| - b.use BundlerCheck b.use ListPlugins end end @@ -33,31 +30,26 @@ module VagrantPlugins # This middleware sequence will uninstall a plugin. def self.action_uninstall Vagrant::Action::Builder.new.tap do |b| - b.use BundlerCheck + b.use PluginExistsCheck b.use UninstallPlugin - b.use PruneGems end end # This middleware sequence will update a plugin. def self.action_update Vagrant::Action::Builder.new.tap do |b| - b.use BundlerCheck - b.use PluginExistsCheck - b.use InstallGem - b.use PruneGems + b.use UpdateGems end end # The autoload farm action_root = Pathname.new(File.expand_path("../action", __FILE__)) - autoload :BundlerCheck, action_root.join("bundler_check") autoload :InstallGem, action_root.join("install_gem") autoload :LicensePlugin, action_root.join("license_plugin") autoload :ListPlugins, action_root.join("list_plugins") autoload :PluginExistsCheck, action_root.join("plugin_exists_check") - autoload :PruneGems, action_root.join("prune_gems") autoload :UninstallPlugin, action_root.join("uninstall_plugin") + autoload :UpdateGems, action_root.join("update_gems") end end end diff --git a/plugins/commands/plugin/action/bundler_check.rb b/plugins/commands/plugin/action/bundler_check.rb deleted file mode 100644 index b53a83c5d..000000000 --- a/plugins/commands/plugin/action/bundler_check.rb +++ /dev/null @@ -1,25 +0,0 @@ -module VagrantPlugins - module CommandPlugin - module Action - class BundlerCheck - def initialize(app, env) - @app = app - end - - def call(env) - # Bundler sets up its own custom gem load paths such that our - # own gems are never loaded. Therefore, give an error if a user - # tries to install gems while within a Bundler-managed environment. - if defined?(Bundler) - require 'bundler/shared_helpers' - if Bundler::SharedHelpers.in_bundle? - raise Vagrant::Errors::GemCommandInBundler - end - end - - @app.call(env) - end - end - end - end -end diff --git a/plugins/commands/plugin/action/install_gem.rb b/plugins/commands/plugin/action/install_gem.rb index 07a5000fb..9f553a0ac 100644 --- a/plugins/commands/plugin/action/install_gem.rb +++ b/plugins/commands/plugin/action/install_gem.rb @@ -1,13 +1,5 @@ -require "rubygems" -require "rubygems/dependency_installer" - -begin - require "rubygems/format" -rescue LoadError - # rubygems 2.x -end - require "log4r" +require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin @@ -21,69 +13,29 @@ module VagrantPlugins end def call(env) + entrypoint = env[:plugin_entry_point] plugin_name = env[:plugin_name] - prerelease = env[:plugin_prerelease] + sources = env[:plugin_sources] version = env[:plugin_version] - # Determine the plugin name we'll look for in the installed set - # in order to determine the version and all that. - find_plugin_name = plugin_name - if plugin_name =~ /\.gem$/ - # If we're installing from a gem file, determine the name - # based on the spec in the file. - pkg = if defined?(Gem::Format) - # RubyGems 1.x - Gem::Format.from_file_by_path(plugin_name) - else - # RubyGems 2.x - Gem::Package.new(plugin_name) - end - - find_plugin_name = pkg.spec.name - version = pkg.spec.version - end - # Install the gem plugin_name_label = plugin_name - plugin_name_label += ' --prerelease' if prerelease plugin_name_label += " --version '#{version}'" if version env[:ui].info(I18n.t("vagrant.commands.plugin.installing", :name => plugin_name_label)) - installed_gems = env[:gem_helper].with_environment do - # Override the list of sources by the ones set as a parameter if given - if env[:plugin_sources] - @logger.info("Custom plugin sources: #{env[:plugin_sources]}") - Gem.sources = env[:plugin_sources] - end - installer = Gem::DependencyInstaller.new(:document => [], :prerelease => prerelease) + manager = Vagrant::Plugin::Manager.instance + plugin_spec = manager.install_plugin( + plugin_name, + version: version, + require: entrypoint, + sources: sources, + verbose: !!env[:plugin_verbose], + ) - # If we don't have a version, use the default version - version ||= Gem::Requirement.default - - begin - installer.install(plugin_name, version) - rescue Gem::GemNotFoundException - raise Vagrant::Errors::PluginInstallNotFound, - :name => plugin_name - end - end - - # The plugin spec is the last installed gem since RubyGems - # currently always installed the requested gem last. - @logger.debug("Installed #{installed_gems.length} gems.") - plugin_spec = installed_gems.find do |gem| - gem.name.downcase == find_plugin_name.downcase - end - - # Store the installed name so we can uninstall it if things go - # wrong. + # Record it so we can uninstall if something goes wrong @installed_plugin_name = plugin_spec.name - # Mark that we installed the gem - @logger.info("Adding the plugin to the state file...") - env[:plugin_state_file].add_plugin(plugin_spec.name) - # Tell the user env[:ui].success(I18n.t("vagrant.commands.plugin.installed", :name => plugin_spec.name, diff --git a/plugins/commands/plugin/action/license_plugin.rb b/plugins/commands/plugin/action/license_plugin.rb index 518c1824c..f4566c94c 100644 --- a/plugins/commands/plugin/action/license_plugin.rb +++ b/plugins/commands/plugin/action/license_plugin.rb @@ -17,15 +17,6 @@ module VagrantPlugins end def call(env) - # Get the list of installed plugins according to the state file - installed = env[:plugin_state_file].installed_plugins.keys - - # If the plugin we're trying to license doesn't exist in the - # state file, then it is an error. - if !installed.include?(env[:plugin_name]) - raise Vagrant::Errors::PluginNotFound, :name => env[:plugin_name] - end - # Verify the license file exists license_file = Pathname.new(env[:plugin_license_path]) if !license_file.file? diff --git a/plugins/commands/plugin/action/list_plugins.rb b/plugins/commands/plugin/action/list_plugins.rb index f5dd0574b..8a8ecd312 100644 --- a/plugins/commands/plugin/action/list_plugins.rb +++ b/plugins/commands/plugin/action/list_plugins.rb @@ -1,5 +1,4 @@ -require "rubygems" -require "set" +require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin @@ -17,32 +16,35 @@ module VagrantPlugins end def call(env) - # Get the list of installed plugins according to the state file - installed = env[:plugin_state_file].installed_plugins.keys - - # Go through the plugins installed in this environment and - # get the latest version of each. - installed_map = {} - env[:gem_helper].with_environment do - Gem::Specification.find_all.each do |spec| - # Ignore specs that aren't in our installed list - next if !installed.include?(spec.name) - - # If we already have a newer version in our list of installed, - # then ignore it - next if installed_map.has_key?(spec.name) && - installed_map[spec.name].version >= spec.version - - installed_map[spec.name] = spec - end - end + manager = Vagrant::Plugin::Manager.instance + plugins = manager.installed_plugins + specs = manager.installed_specs # Output! - if installed_map.empty? + if specs.empty? env[:ui].info(I18n.t("vagrant.commands.plugin.no_plugins")) - else - installed_map.values.each do |spec| - env[:ui].info "#{spec.name} (#{spec.version})" + return @app.call(env) + end + + specs.each do |spec| + env[:ui].info "#{spec.name} (#{spec.version})" + + # Grab the plugin. Note that the check for whether it exists + # shouldn't be necessary since installed_specs checks that but + # its nice to be certain. + plugin = plugins[spec.name] + next if !plugin + + if plugin["gem_version"] && plugin["gem_version"] != "" + env[:ui].info(I18n.t( + "vagrant.commands.plugin.plugin_version", + version: plugin["gem_version"])) + end + + if plugin["require"] && plugin["require"] != "" + env[:ui].info(I18n.t( + "vagrant.commands.plugin.plugin_require", + require: plugin["require"])) end end diff --git a/plugins/commands/plugin/action/plugin_exists_check.rb b/plugins/commands/plugin/action/plugin_exists_check.rb index abe8d43cf..7a369976e 100644 --- a/plugins/commands/plugin/action/plugin_exists_check.rb +++ b/plugins/commands/plugin/action/plugin_exists_check.rb @@ -1,4 +1,4 @@ -require "set" +require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin @@ -11,9 +11,8 @@ module VagrantPlugins end def call(env) - # Get the list of installed plugins according to the state file - installed = env[:plugin_state_file].installed_plugins.keys - if !installed.include?(env[:plugin_name]) + installed = Vagrant::Plugin::Manager.instance.installed_plugins + if !installed.has_key?(env[:plugin_name]) raise Vagrant::Errors::PluginNotInstalled, name: env[:plugin_name] end diff --git a/plugins/commands/plugin/action/prune_gems.rb b/plugins/commands/plugin/action/prune_gems.rb deleted file mode 100644 index d5e754994..000000000 --- a/plugins/commands/plugin/action/prune_gems.rb +++ /dev/null @@ -1,158 +0,0 @@ -require "rubygems" -require "rubygems/user_interaction" -require "rubygems/uninstaller" -require "set" - -require "log4r" - -module VagrantPlugins - module CommandPlugin - module Action - # This class prunes any unnecessary gems from the Vagrant-managed - # gem folder. This keeps the gem folder to the absolute minimum set - # of required gems and doesn't let it blow up out of control. - # - # A high-level description of how this works: - # - # 1. Get the list of installed plugins. Vagrant maintains this - # list on its own. - # 2. Get the list of installed RubyGems. - # 3. Find the latest version of each RubyGem that matches an installed - # plugin. These are our root RubyGems that must be installed. - # 4. Go through each root and mark all dependencies recursively as - # necessary. - # 5. Set subtraction between all gems and necessary gems yields a - # list of gems that aren't needed. Uninstall them. - # - class PruneGems - def initialize(app, env) - @app = app - @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::prune") - end - - def call(env) - @logger.info("Pruning gems...") - - # Get the list of installed plugins according to the state file - installed = env[:plugin_state_file].installed_plugins.keys - - # Get the actual specifications of installed gems - all_specs = env[:gem_helper].with_environment do - [].tap do |result| - Gem::Specification.find_all do |s| - # Ignore default gems since they can't be uninstalled - next if s.respond_to?(:default_gem?) && s.default_gem? - - result << s - end - end - end - - # The list of specs to prune initially starts out as all of them - all_specs = Set.new(all_specs) - - # Go through each spec and find the latest version of the installed - # gems, since we want to keep those. - installed_specs = {} - - @logger.debug("Collecting installed plugin gems...") - all_specs.each do |spec| - # If this isn't a spec that we claim is installed, skip it - next if !installed.include?(spec.name) - - # If it is already in the specs, then we need to make sure we - # have the latest version. - if installed_specs.has_key?(spec.name) - if installed_specs[spec.name].version > spec.version - next - end - end - - @logger.debug(" -- #{spec.name} (#{spec.version})") - installed_specs[spec.name] = spec - end - - # Recursive dependency checker to keep all dependencies and remove - # all non-crucial gems from the prune list. - good_specs = Set.new - to_check = installed_specs.values - - while true - # If we're out of gems to check then we break out - break if to_check.empty? - - # Get a random (first) element to check - spec = to_check.shift - - # If we already checked this, then do the next one - next if good_specs.include?(spec) - - # Find all the dependencies and add the latest compliant gem - # to the `to_check` list. - if spec.dependencies.length > 0 - @logger.debug("Finding dependencies for '#{spec.name}' to mark as good...") - spec.dependencies.each do |dep| - # Ignore non-runtime dependencies - next if dep.type != :runtime - @logger.debug("Searching for: '#{dep.name}'") - - latest_matching = nil - - all_specs.each do |prune_spec| - if dep =~ prune_spec - # If we have a matching one already and this one isn't newer - # then we ditch it. - next if latest_matching && - prune_spec.version <= latest_matching.version - - latest_matching = prune_spec - end - end - - if latest_matching.nil? - @logger.error("Missing dependency for '#{spec.name}': #{dep.name}") - next - end - - @logger.debug("Latest matching dep: '#{latest_matching.name}' (#{latest_matching.version})") - to_check << latest_matching - end - end - - # Add ito the list of checked things so we don't accidentally - # re-check it - good_specs.add(spec) - end - - # Figure out the gems we need to prune - prune_specs = all_specs - good_specs - @logger.debug("Gems to prune: #{prune_specs.inspect}") - @logger.info("Pruning #{prune_specs.length} gems.") - - if prune_specs.length > 0 - env[:gem_helper].with_environment do - # Due to a bug in rubygems 2.0, we need to load the - # specifications before removing any. This achieves that. - Gem::Specification.to_a - - prune_specs.each do |prune_spec| - uninstaller = Gem::Uninstaller.new(prune_spec.name, { - :all => true, - :executables => true, - :force => true, - :ignore => true, - :version => prune_spec.version.version - }) - - @logger.info("Uninstalling: #{prune_spec.name} (#{prune_spec.version})") - uninstaller.uninstall - end - end - end - - @app.call(env) - end - end - end - end -end diff --git a/plugins/commands/plugin/action/uninstall_plugin.rb b/plugins/commands/plugin/action/uninstall_plugin.rb index f86675523..acf1ef2a3 100644 --- a/plugins/commands/plugin/action/uninstall_plugin.rb +++ b/plugins/commands/plugin/action/uninstall_plugin.rb @@ -13,7 +13,9 @@ module VagrantPlugins # Remove it! env[:ui].info(I18n.t("vagrant.commands.plugin.uninstalling", :name => env[:plugin_name])) - env[:plugin_state_file].remove_plugin(env[:plugin_name]) + + manager = Vagrant::Plugin::Manager.instance + manager.uninstall_plugin(env[:plugin_name]) @app.call(env) end diff --git a/plugins/commands/plugin/action/update_gems.rb b/plugins/commands/plugin/action/update_gems.rb new file mode 100644 index 000000000..57414273f --- /dev/null +++ b/plugins/commands/plugin/action/update_gems.rb @@ -0,0 +1,51 @@ +require "vagrant/plugin/manager" + +module VagrantPlugins + module CommandPlugin + module Action + class UpdateGems + def initialize(app, env) + @app = app + end + + def call(env) + names = env[:plugin_name] || [] + + if names.empty? + env[:ui].info(I18n.t("vagrant.commands.plugin.updating")) + else + env[:ui].info(I18n.t("vagrant.commands.plugin.updating_specific", + names: names.join(", "))) + end + + manager = Vagrant::Plugin::Manager.instance + installed_specs = manager.installed_specs + new_specs = manager.update_plugins(names) + + updated = {} + installed_specs.each do |ispec| + new_specs.each do |uspec| + next if uspec.name != ispec.name + next if ispec.version >= uspec.version + next if updated[uspec.name] && updated[uspec.name].version >= uspec.version + + updated[uspec.name] = uspec + end + end + + if updated.empty? + env[:ui].success(I18n.t("vagrant.commands.plugin.up_to_date")) + end + + updated.values.each do |spec| + env[:ui].success(I18n.t("vagrant.commands.plugin.updated", + name: spec.name, version: spec.version.to_s)) + end + + # Continue + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/plugin/command/base.rb b/plugins/commands/plugin/command/base.rb index 5180dfd5d..5cf522fd2 100644 --- a/plugins/commands/plugin/command/base.rb +++ b/plugins/commands/plugin/command/base.rb @@ -1,3 +1,5 @@ +require "vagrant/plugin/state_file" + module VagrantPlugins module CommandPlugin module Command @@ -9,11 +11,6 @@ module VagrantPlugins # @param [Object] callable the Middleware callable # @param [Hash] env Extra environment hash that is merged in. def action(callable, env=nil) - env = { - :gem_helper => GemHelper.new(@env.gems_path), - :plugin_state_file => StateFile.new(@env.home_path.join("plugins.json")) - }.merge(env || {}) - @env.action_runner.run(callable, env) end end diff --git a/plugins/commands/plugin/command/install.rb b/plugins/commands/plugin/command/install.rb index 6667fd062..b38a08e25 100644 --- a/plugins/commands/plugin/command/install.rb +++ b/plugins/commands/plugin/command/install.rb @@ -10,12 +10,16 @@ module VagrantPlugins include MixinInstallOpts def execute - options = {} + options = { verbose: false } opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin install [-h]" o.separator "" build_install_opts(o, options) + + o.on("--verbose", "Enable verbose output for plugin installation") do |v| + options[:verbose] = v + end end # Parse the options @@ -26,10 +30,10 @@ module VagrantPlugins # Install the gem action(Action.action_install, { :plugin_entry_point => options[:entry_point], - :plugin_prerelease => options[:plugin_prerelease], :plugin_version => options[:plugin_version], :plugin_sources => options[:plugin_sources], - :plugin_name => argv[0] + :plugin_name => argv[0], + :plugin_verbose => options[:verbose] }) # Success, exit status 0 diff --git a/plugins/commands/plugin/command/mixin_install_opts.rb b/plugins/commands/plugin/command/mixin_install_opts.rb index 96c879968..0b1b0973a 100644 --- a/plugins/commands/plugin/command/mixin_install_opts.rb +++ b/plugins/commands/plugin/command/mixin_install_opts.rb @@ -8,9 +8,13 @@ module VagrantPlugins options[:entry_point] = entry_point end + # @deprecated o.on("--plugin-prerelease", "Allow prerelease versions of this plugin.") do |plugin_prerelease| - options[:plugin_prerelease] = plugin_prerelease + puts "--plugin-prelease is deprecated and will be removed in the next" + puts "version of Vagrant. It has no effect now. Use the '--plugin-version'" + puts "flag to get a specific pre-release version." + puts end o.on("--plugin-source PLUGIN_SOURCE", String, diff --git a/plugins/commands/plugin/command/update.rb b/plugins/commands/plugin/command/update.rb index ca4bbb746..a0b832238 100644 --- a/plugins/commands/plugin/command/update.rb +++ b/plugins/commands/plugin/command/update.rb @@ -10,26 +10,18 @@ module VagrantPlugins include MixinInstallOpts def execute - options = {} - opts = OptionParser.new do |o| - o.banner = "Usage: vagrant plugin update [-h]" + o.banner = "Usage: vagrant plugin update [names...] [-h]" o.separator "" - build_install_opts(o, options) end # Parse the options argv = parse_options(opts) return if !argv - raise Vagrant::Errors::CLIInvalidUsage, :help => opts.help.chomp if argv.length < 1 # Update the gem action(Action.action_update, { - :plugin_entry_point => options[:entry_point], - :plugin_prerelease => options[:plugin_prerelease], - :plugin_version => options[:plugin_version], - :plugin_sources => options[:plugin_sources], - :plugin_name => argv[0] + :plugin_name => argv, }) # Success, exit status 0 diff --git a/plugins/commands/plugin/gem_helper.rb b/plugins/commands/plugin/gem_helper.rb index b937570b8..e1a1fff91 100644 --- a/plugins/commands/plugin/gem_helper.rb +++ b/plugins/commands/plugin/gem_helper.rb @@ -29,7 +29,15 @@ module VagrantPlugins # Set a custom configuration to avoid loading ~/.gemrc loads and # /etc/gemrc and so on. - old_config = Gem.configuration + old_config = nil + begin + old_config = Gem.configuration + rescue Psych::SyntaxError + # Just ignore this. This means that the ".gemrc" file has + # an invalid syntax and can't be loaded. We don't care, because + # when we set Gem.configuration to nil later, it'll force a reload + # if it is needed. + end Gem.configuration = NilGemConfig.new # Clear the sources so that installation uses custom sources diff --git a/plugins/commands/plugin/plugin.rb b/plugins/commands/plugin/plugin.rb index db9763319..0fd05973b 100644 --- a/plugins/commands/plugin/plugin.rb +++ b/plugins/commands/plugin/plugin.rb @@ -16,7 +16,5 @@ DESC end autoload :Action, File.expand_path("../action", __FILE__) - autoload :GemHelper, File.expand_path("../gem_helper", __FILE__) - autoload :StateFile, File.expand_path("../state_file", __FILE__) end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 154693760..384d1220d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -139,11 +139,6 @@ en: Old: %{old} New: %{new} - in_bundler: |- - You appear to be running Vagrant in a Bundler environment. Because - Vagrant should be run within installers (outside of Bundler), Vagrant - will assume that you're developing plugins and will change its behavior - in certain ways to better assist plugin development. not_in_installer: |- You appear to be running Vagrant outside of the official installers. Note that the installers are what ensure that Vagrant has all required @@ -259,6 +254,13 @@ en: The box '%{name}' is still stored on disk in the Vagrant 1.0.x format. This box must be upgraded in order to work properly with this version of Vagrant. + bundler_error: |- + Bundler, the underlying system Vagrant uses to install plugins, + reported an error. The error is shown below. These errors are usually + caused by misconfigured plugin installations or transient network + issues. The error from Bundler is: + + %{message} cfengine_bootstrap_failed: |- Failed to bootstrap CFEngine. Please see the output above to see what went wrong and address the issue. @@ -479,6 +481,9 @@ en: manage Vagrant plugins. The output of the errors are shown below: %{output} + plugin_gem_not_found: |- + The plugin '%{name}' could not be installed because it could not + be found. Please double check the name and try again. plugin_install_bad_entry_point: |- Attempting to load the plugin '%{name}' failed, because the entry point doesn't exist. The entry point attempted was @@ -491,6 +496,16 @@ en: plugin_install_not_found: |- The plugin '%{name}' could not be found in local or remote repositories. Please check the name of the plugin and try again. + plugin_install_version_conflict: |- + The plugin(s) can't be installed due to the version conflicts below. + This means that the plugins depend on a library version that conflicts + with other plugins or Vagrant itself, creating an impossible situation + where Vagrant wouldn't be able to load the plugins. + + You can fix the issue by either removing a conflicting plugin or + by contacting a plugin author to see if they can address the conflict. + + %{conflicts} plugin_load_error: |- The plugin "%{plugin}" could not be found. Please make sure that it is properly installed via `vagrant plugin`. Note that plugins made for @@ -911,12 +926,22 @@ en: Installing license for '%{name}'... no_plugins: |- No plugins installed. + plugin_require: " - Custom entrypoint: %{require}" + plugin_version: " - Version Constraint: %{version}" installed: |- Installed the plugin '%{name} (%{version})'! installing: |- Installing the '%{name}' plugin. This can take a few minutes... uninstalling: |- Uninstalling the '%{name}' plugin... + up_to_date: |- + All plugins are up to date. + updated: |- + Updated '%{name}' to version '%{version}'! + updating: |- + Updating installed plugins... + updating_specific: |- + Updating plugins: %{names}. This may take a few minutes... post_install: |- Post install message from the '%{name}' plugin: diff --git a/test/unit/plugins/commands/plugin/action/install_gem_test.rb b/test/unit/plugins/commands/plugin/action/install_gem_test.rb new file mode 100644 index 000000000..df5362555 --- /dev/null +++ b/test/unit/plugins/commands/plugin/action/install_gem_test.rb @@ -0,0 +1,95 @@ +require File.expand_path("../../../../../base", __FILE__) + +describe VagrantPlugins::CommandPlugin::Action::InstallGem do + let(:app) { lambda { |env| } } + let(:env) {{ + ui: Vagrant::UI::Silent.new + }} + + let(:manager) { double("manager") } + + subject { described_class.new(app, env) } + + before do + Vagrant::Plugin::Manager.stub(instance: manager) + end + + describe "#call" do + it "should install the plugin" do + spec = Gem::Specification.new + manager.should_receive(:install_plugin).with( + "foo", version: nil, require: nil, sources: nil, verbose: false).once.and_return(spec) + + app.should_receive(:call).with(env).once + + env[:plugin_name] = "foo" + subject.call(env) + end + + it "should specify the version if given" do + spec = Gem::Specification.new + manager.should_receive(:install_plugin).with( + "foo", version: "bar", require: nil, sources: nil, verbose: false).once.and_return(spec) + + app.should_receive(:call).with(env).once + + env[:plugin_name] = "foo" + env[:plugin_version] = "bar" + subject.call(env) + end + + it "should specify the entrypoint if given" do + spec = Gem::Specification.new + manager.should_receive(:install_plugin).with( + "foo", version: "bar", require: "baz", sources: nil, verbose: false).once.and_return(spec) + + app.should_receive(:call).with(env).once + + env[:plugin_entry_point] = "baz" + env[:plugin_name] = "foo" + env[:plugin_version] = "bar" + subject.call(env) + end + + it "should specify the sources if given" do + spec = Gem::Specification.new + manager.should_receive(:install_plugin).with( + "foo", version: nil, require: nil, sources: ["foo"], verbose: false).once.and_return(spec) + + app.should_receive(:call).with(env).once + + env[:plugin_name] = "foo" + env[:plugin_sources] = ["foo"] + subject.call(env) + end + end + + describe "#recover" do + it "should do nothing by default" do + subject.recover(env) + end + + context "with a successful plugin install" do + let(:action_runner) { double("action_runner") } + + before do + spec = Gem::Specification.new + spec.name = "foo" + manager.stub(install_plugin: spec) + + env[:plugin_name] = "foo" + subject.call(env) + + env[:action_runner] = action_runner + end + + it "should uninstall the plugin" do + action_runner.should_receive(:run).with do |action, newenv| + expect(newenv[:plugin_name]).to eql("foo") + end + + subject.recover(env) + end + end + end +end diff --git a/test/unit/plugins/commands/plugin/action/plugin_exists_check_test.rb b/test/unit/plugins/commands/plugin/action/plugin_exists_check_test.rb new file mode 100644 index 000000000..3a442e9ae --- /dev/null +++ b/test/unit/plugins/commands/plugin/action/plugin_exists_check_test.rb @@ -0,0 +1,31 @@ +require File.expand_path("../../../../../base", __FILE__) + +describe VagrantPlugins::CommandPlugin::Action::PluginExistsCheck do + let(:app) { lambda {} } + let(:env) { {} } + + let(:manager) { double("manager") } + + subject { described_class.new(app, env) } + + before do + Vagrant::Plugin::Manager.stub(instance: manager) + end + + it "should raise an exception if the plugin doesn't exist" do + manager.stub(installed_plugins: { "foo" => {} }) + app.should_not_receive(:call) + + env[:plugin_name] = "bar" + expect { subject.call(env) }. + to raise_error(Vagrant::Errors::PluginNotInstalled) + end + + it "should call the app if the plugin is installed" do + manager.stub(installed_plugins: { "bar" => {} }) + app.should_receive(:call).once.with(env) + + env[:plugin_name] = "bar" + subject.call(env) + end +end diff --git a/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb b/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb new file mode 100644 index 000000000..ffe17122e --- /dev/null +++ b/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb @@ -0,0 +1,24 @@ +require File.expand_path("../../../../../base", __FILE__) + +describe VagrantPlugins::CommandPlugin::Action::UninstallPlugin do + let(:app) { lambda { |env| } } + let(:env) {{ + ui: Vagrant::UI::Silent.new, + }} + + let(:manager) { double("manager") } + + subject { described_class.new(app, env) } + + before do + Vagrant::Plugin::Manager.stub(instance: manager) + end + + it "uninstalls the specified plugin" do + manager.should_receive(:uninstall_plugin).with("bar").ordered + app.should_receive(:call).ordered + + env[:plugin_name] = "bar" + subject.call(env) + end +end diff --git a/test/unit/plugins/commands/plugin/action/update_gems_test.rb b/test/unit/plugins/commands/plugin/action/update_gems_test.rb new file mode 100644 index 000000000..f92b26990 --- /dev/null +++ b/test/unit/plugins/commands/plugin/action/update_gems_test.rb @@ -0,0 +1,33 @@ +require File.expand_path("../../../../../base", __FILE__) + +describe VagrantPlugins::CommandPlugin::Action::UpdateGems do + let(:app) { lambda { |env| } } + let(:env) {{ + ui: Vagrant::UI::Silent.new + }} + + let(:manager) { double("manager") } + + subject { described_class.new(app, env) } + + before do + Vagrant::Plugin::Manager.stub(instance: manager) + manager.stub(installed_specs: []) + end + + describe "#call" do + it "should update all plugins if none are specified" do + manager.should_receive(:update_plugins).with([]).once.and_return([]) + app.should_receive(:call).with(env).once + subject.call(env) + end + + it "should update specified plugins" do + manager.should_receive(:update_plugins).with(["foo"]).once.and_return([]) + app.should_receive(:call).with(env).once + + env[:plugin_name] = ["foo"] + subject.call(env) + end + end +end diff --git a/test/unit/vagrant/plugin/manager_test.rb b/test/unit/vagrant/plugin/manager_test.rb new file mode 100644 index 000000000..70d6dbb44 --- /dev/null +++ b/test/unit/vagrant/plugin/manager_test.rb @@ -0,0 +1,172 @@ +require "json" +require "pathname" + +require "vagrant/plugin" +require "vagrant/plugin/manager" +require "vagrant/plugin/state_file" + +require File.expand_path("../../../base", __FILE__) + +describe Vagrant::Plugin::Manager do + let(:path) do + f = Tempfile.new("vagrant") + p = f.path + f.close + f.unlink + Pathname.new(p) + end + + let(:bundler) { double("bundler") } + + after do + path.unlink if path.file? + end + + before do + Vagrant::Bundler.stub(instance: bundler) + end + + subject { described_class.new(path) } + + describe "#install_plugin" do + it "installs the plugin and adds it to the state file" do + specs = Array.new(5) { Gem::Specification.new } + specs[3].name = "foo" + bundler.should_receive(:install).once.with do |plugins, local| + expect(plugins).to have_key("foo") + expect(local).to be_false + end.and_return(specs) + + result = subject.install_plugin("foo") + + # It should return the spec of the installed plugin + expect(result).to eql(specs[3]) + + # It should've added the plugin to the state + expect(subject.installed_plugins).to have_key("foo") + end + + it "masks GemNotFound with our error" do + bundler.should_receive(:install).and_raise(Bundler::GemNotFound) + + expect { subject.install_plugin("foo") }. + to raise_error(Vagrant::Errors::PluginGemNotFound) + end + + it "masks bundler errors with our own error" do + bundler.should_receive(:install).and_raise(Bundler::InstallError) + + expect { subject.install_plugin("foo") }. + to raise_error(Vagrant::Errors::BundlerError) + end + + describe "installation options" do + let(:specs) do + specs = Array.new(5) { Gem::Specification.new } + specs[3].name = "foo" + specs + end + + before do + bundler.stub(:install).and_return(specs) + end + + it "installs a version with constraints" do + bundler.should_receive(:install).once.with do |plugins, local| + expect(plugins).to have_key("foo") + expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0") + expect(local).to be_false + end.and_return(specs) + + subject.install_plugin("foo", version: ">= 0.1.0") + + plugins = subject.installed_plugins + expect(plugins).to have_key("foo") + expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0") + end + + it "installs with an exact version but doesn't constrain" do + bundler.should_receive(:install).once.with do |plugins, local| + expect(plugins).to have_key("foo") + expect(plugins["foo"]["gem_version"]).to eql("0.1.0") + expect(local).to be_false + end.and_return(specs) + + subject.install_plugin("foo", version: "0.1.0") + + plugins = subject.installed_plugins + expect(plugins).to have_key("foo") + expect(plugins["foo"]["gem_version"]).to eql("") + end + end + end + + describe "#uninstall_plugin" do + it "removes the plugin from the state" do + sf = Vagrant::Plugin::StateFile.new(path) + sf.add_plugin("foo") + + # Sanity + expect(subject.installed_plugins).to have_key("foo") + + # Test + bundler.should_receive(:clean).once.with({}) + + # Remove it + subject.uninstall_plugin("foo") + expect(subject.installed_plugins).to_not have_key("foo") + end + + it "masks bundler errors with our own error" do + bundler.should_receive(:clean).and_raise(Bundler::InstallError) + + expect { subject.uninstall_plugin("foo") }. + to raise_error(Vagrant::Errors::BundlerError) + end + end + + describe "#update_plugins" do + it "masks bundler errors with our own error" do + bundler.should_receive(:update).and_raise(Bundler::InstallError) + + expect { subject.update_plugins([]) }. + to raise_error(Vagrant::Errors::BundlerError) + end + end + + context "without state" do + describe "#installed_plugins" do + it "is empty initially" do + expect(subject.installed_plugins).to be_empty + end + end + end + + context "with state" do + before do + sf = Vagrant::Plugin::StateFile.new(path) + sf.add_plugin("foo") + end + + describe "#installed_plugins" do + it "has the plugins" do + plugins = subject.installed_plugins + expect(plugins.length).to eql(1) + expect(plugins).to have_key("foo") + end + end + + describe "#installed_specs" do + it "has the plugins" do + # We just add "i18n" because it is a dependency of Vagrant and + # we know it will be there. + sf = Vagrant::Plugin::StateFile.new(path) + sf.add_plugin("i18n") + + specs = subject.installed_specs + expect(specs.length).to eql(1) + expect(specs.first.name).to eql("i18n") + end + end + end +end diff --git a/test/unit/plugins/commands/plugin/state_file_test.rb b/test/unit/vagrant/plugin/state_file_test.rb similarity index 67% rename from test/unit/plugins/commands/plugin/state_file_test.rb rename to test/unit/vagrant/plugin/state_file_test.rb index 888c650b5..6b37198ef 100644 --- a/test/unit/plugins/commands/plugin/state_file_test.rb +++ b/test/unit/vagrant/plugin/state_file_test.rb @@ -1,9 +1,9 @@ require "json" require "pathname" -require File.expand_path("../../../../base", __FILE__) +require File.expand_path("../../../base", __FILE__) -describe VagrantPlugins::CommandPlugin::StateFile do +describe Vagrant::Plugin::StateFile do let(:path) do f = Tempfile.new("vagrant") p = f.path @@ -32,6 +32,9 @@ describe VagrantPlugins::CommandPlugin::StateFile do expect(plugins["foo"]).to eql({ "ruby_version" => RUBY_VERSION, "vagrant_version" => Vagrant::VERSION, + "gem_version" => "", + "require" => "", + "sources" => [], }) end @@ -50,6 +53,34 @@ describe VagrantPlugins::CommandPlugin::StateFile do instance = described_class.new(path) expect(instance.installed_plugins.keys).to eql(["foo"]) end + + it "should store metadata" do + subject.add_plugin("foo", version: "1.2.3") + expect(subject.installed_plugins["foo"]["gem_version"]).to eql("1.2.3") + end + + describe "sources" do + it "should have no sources" do + expect(subject.sources).to be_empty + end + + it "should add sources" do + subject.add_source("foo") + expect(subject.sources).to eql(["foo"]) + end + + it "should de-dup sources" do + subject.add_source("foo") + subject.add_source("foo") + expect(subject.sources).to eql(["foo"]) + end + + it "can remove sources" do + subject.add_source("foo") + subject.remove_source("foo") + expect(subject.sources).to be_empty + end + end end context "with an old-style file" do diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index 0d143d42b..cb8c0f105 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -47,36 +47,6 @@ describe Vagrant do end end - describe "requiring plugins" do - it "should require the plugin given" do - # For now, just require a stdlib - expect { described_class.require_plugin "set" }. - to_not raise_error - end - - it "should add the gem name to plugin manager" do - expect(described_class.plugin("2").manager). - to receive(:plugin_required).with("set") - described_class.require_plugin "set" - end - - it "should raise an error if the file doesn't exist" do - expect { described_class.require_plugin("i_dont_exist") }. - to raise_error(Vagrant::Errors::PluginLoadError) - end - - it "should raise an error if the loading failed in some other way" do - plugin_dir = temporary_dir - plugin_path = plugin_dir.join("test.rb") - plugin_path.open("w") do |f| - f.write(%Q[require 'I_dont_exist']) - end - - expect { described_class.require_plugin(plugin_path.to_s) }. - to raise_error(Vagrant::Errors::PluginLoadFailed) - end - end - describe "has_plugin?" do before(:each) do Class.new(described_class.plugin("2")) do diff --git a/vagrant-spec.config.example.rb b/vagrant-spec.config.example.rb index 59a39520d..dd0dbfe36 100644 --- a/vagrant-spec.config.example.rb +++ b/vagrant-spec.config.example.rb @@ -1,3 +1,5 @@ +ENV["VAGRANT_FORCE_PLUGINS"] = "1" + require_relative "test/acceptance/base" Vagrant::Spec::Acceptance.configure do |c| diff --git a/vagrant.gemspec b/vagrant.gemspec index f720fb50b..1b1bdc318 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -14,6 +14,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.rubyforge_project = "vagrant" + s.add_dependency "bundler", "~> 1.5.1" s.add_dependency "childprocess", "~> 0.3.7" s.add_dependency "erubis", "~> 2.7.0" s.add_dependency "i18n", "~> 0.6.0" @@ -25,8 +26,6 @@ Gem::Specification.new do |s| s.add_development_dependency "contest", ">= 0.1.2" s.add_development_dependency "minitest", "~> 2.5.1" s.add_development_dependency "mocha" - # This has problems on Windows, we need to find a better way: - # s.add_development_dependency "sys-proctable", "~> 0.9.0" s.add_development_dependency "rspec", "~> 2.14.0" # The following block of code determines the files that should be included diff --git a/website/docs/source/v2/cli/plugin.html.md b/website/docs/source/v2/cli/plugin.html.md index 6a6d159ac..ee5651a5c 100644 --- a/website/docs/source/v2/cli/plugin.html.md +++ b/website/docs/source/v2/cli/plugin.html.md @@ -28,6 +28,25 @@ repositories, usually [RubyGems](http://rubygems.org). This command will also update a plugin if it is already installed, but you can also use `vagrant plugin update` for that. +This command accepts optional command-line flags: + +* `--entry-point ENTRYPOINT` - By default, installed plugins are loaded + internally by loading an initialization file of the same name as the plugin. + Most of the time, this is correct. If the plugin you're installing has + another entrypoint, this flag can be used to specify it. + +* `--plugin-source SOURCE` - Adds a source from which to fetch a plugin. Note + that this doesn't only affect the single plugin being installed, by all future + plugin as well. This is a limitation of the underlying plugin installer + Vagrant uses. + +* `--plugin-version VERSION` - The version of the plugin to install. By default, + this command will install the latest version. You can constrain the version + using this flag. You can set it to a specific version, such as "1.2.3" or + you can set it to a version contraint, such as "> 1.0.2". You can set it + to a more complex constraint by comma-separating multiple constraints: + "> 1.0.2, < 1.1.0" (don't forget to quote these on the command-line). + # Plugin License **Command: `vagrant plugin license `** @@ -39,7 +58,10 @@ such as the [VMware Fusion provider](/v2/vmware/index.html). **Command: `vagrant plugin list`** -This lists all installed plugins and their respective versions. +This lists all installed plugins and their respective installed versions. +If a version constraint was specified for a plugin when installing it, the +constraint will be listed as well. Other plugin-specific information may +be shown, too. # Plugin Uninstall @@ -50,7 +72,13 @@ plugin will also be uninstalled assuming no other plugin needs them. # Plugin Update -**Command: `vagrant plugin update `** +**Command: `vagrant plugin update []`** -This updates the plugin with the given name. If the plugin isn't already -installed, this will not install it. +This updates the plugins that are installed within Vagrant. If you specified +version constraints when installing the plugin, this command will respect +those constraints. If you want to change a version constraint, re-install +the plugin using `vagrant plugin install`. + +If a name is specified, only that single plugin will be updated. If a +name is specified of a plugin that is not installed, this command will not +install it.