Merge pull request #2769 from mitchellh/f-bundlerize

Plugin dependency management revamp

This is a huge revamp of how plugin dependency management is done. To understand the changes here, a brief history lesson is in order:

Since Vagrant 1.1, plugins have been loaded as RubyGems. Once Vagrant was loaded, it would iterate through a list of installed plugins, and `require` that plugin. This mostly worked okay. But the devil is in the details, and the edge cases were _really_ bad. In addition to the edge cases (mentioned below), building things like updaters, version constraints (">= 1.0", "< 1.1"), etc. all had to be done manually. This seemed silly, since RubyGems itself (and Bundler) do these sort of things for you. Why reinvent the wheel?

As for edge cases: the primary edge case is that since the dependencies of Vagrant and its respective plugins weren't resolved as a whole, you can run into cases where plugin installation succeeded, but plugin loading failed because Vagrant already loaded a common dependency with the wrong version. An example explains this best:

* Vagrant depends on "A >= 1.0, < 1.2"
* vagrant-plugin depends on "A = 1.1"
* When you run Vagrant, it loads the latest possible matching dependencies, so it would load A 1.2
* When Vagrant loads vagrant-plugin, it can't load, because A 1.2 is active, so A 1.1 can't be loaded.

The error above should never happen: the versions available for A should satisfy both Vagrant and vagrant-plugin (by loading v1.1 for both). 

With this new branch, all plugin installation, dependency resolution, updating, etc. is managed by [Bundler](http://gembundler.com). This has yielded numerous benefits:

* Vagrant now resolves dependencies before Vagrant is even loaded. This ensures that all plugins will be able to load. No more conflicts at run-time.

* Conflicts are detected at `vagrant plugin install` time. This means that if there would be a crash if that plugin were to load, the plugin won't even install and a human-friendly error is shown to the end user.

* `vagrant plugin install` now accepts complex version constraints such as "~> 1.0.0" or ">= 1.0, < 1.1". Vagrant stores these constraints for updating, which leads to the next point.

* `vagrant plugin update` without arguments now updates all installed plugins, respecting the constraints specified by `vagrant plugin install`.

* `vagrant plugin update NAME` will only update that gem (still respecting constraints). 

* Internally, there are a lot more unit tests. /cc @phinze :)

The goal of this branch was to replace the _existing_ system and functionality with Bundler-ized management. It did not introduce any new features except where they naturally fell into place (version constraints). However, with this new system, many new possibilities are also available:

* Vagrant environment local plugins (i.e. a Gemfile but for a specific Vagrant environment). 

* Plugin installation from git

I'm sure those will be pursued at some point in the future.

This fixes: #2612, #2406, #2428
This commit is contained in:
Mitchell Hashimoto 2014-01-07 10:53:41 -08:00
commit ba85627c21
36 changed files with 1159 additions and 547 deletions

View File

@ -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

View File

@ -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 : "<unknown>"
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?

266
lib/vagrant/bundler.rb Normal file
View File

@ -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<Gem::Specification>]
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<String>] 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<Gem::Specification>]
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<Gem::Specification>]
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

View File

@ -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<String>]
def sources
@data["sources"] || []
end
# This saves the state back into the state file.
def save!
@path.open("w+") do |f|

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <name> [-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

View File

@ -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,

View File

@ -10,26 +10,18 @@ module VagrantPlugins
include MixinInstallOpts
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin update <name> [-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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,5 @@
ENV["VAGRANT_FORCE_PLUGINS"] = "1"
require_relative "test/acceptance/base"
Vagrant::Spec::Acceptance.configure do |c|

View File

@ -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

View File

@ -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 <name> <license-file>`**
@ -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 <name>`**
**Command: `vagrant plugin update [<name>]`**
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.