Skip to content

Commit

Permalink
Merge pull request #2769 from mitchellh/f-bundlerize
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mitchellh committed Jan 7, 2014
2 parents f51b6d0 + 4f623f6 commit ba85627
Show file tree
Hide file tree
Showing 36 changed files with 1,159 additions and 547 deletions.
78 changes: 43 additions & 35 deletions bin/vagrant
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,55 @@ 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")
argv.delete("--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'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
93 changes: 21 additions & 72 deletions lib/vagrant.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -66,6 +78,7 @@

# We need these components always so instead of an autoload we
# just require them explicitly here.
require "vagrant/plugin"
require "vagrant/registry"

module Vagrant
Expand Down Expand Up @@ -118,12 +131,6 @@ def self.in_installer?
!!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.
Expand Down Expand Up @@ -178,72 +185,11 @@ def self.plugin(version, component=nil)
"#{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
Expand Down Expand Up @@ -312,3 +258,6 @@ def self.require_version(*requirements)
# 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?
Loading

0 comments on commit ba85627

Please sign in to comment.