diff --git a/lib/vagrant.rb b/lib/vagrant.rb index c6c0fe904..2e9739fdf 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -18,7 +18,7 @@ require_relative "vagrant/shared_helpers" if Vagrant.plugins_enabled? # Initialize Bundler before we load _any_ RubyGems. require_relative "vagrant/bundler" - require_relative "vagrant/plugin_manager" + require_relative "vagrant/plugin/manager" Vagrant::Bundler.instance.init!(Vagrant::Plugin::Manager.instance.installed_plugins) end diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb index 81c445860..e4afd9fbe 100644 --- a/lib/vagrant/bundler.rb +++ b/lib/vagrant/bundler.rb @@ -1,3 +1,4 @@ +require "pathname" require "tempfile" require_relative "shared_helpers" @@ -12,6 +13,11 @@ module Vagrant @bundler ||= self.new end + def initialize + @gem_home = ENV["GEM_HOME"] + @gem_path = ENV["GEM_PATH"] + end + # Initializes Bundler and the various gem paths so that we can begin # loading gems. This must only be called once. def init!(plugins) @@ -24,23 +30,117 @@ module Vagrant # 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 = Tempfile.new("vagrant-gemfile") - @gemfile.puts(%Q[gem "vagrant", "= #{Vagrant::VERSION}"]) - plugins.each do |plugin| - @gemfile.puts(%Q[gem "#{plugin}"]) - end - @gemfile.close + @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}#{ENV["GEM_PATH"]}" + "#{Vagrant.user_data_path.join("gems")}#{::File::PATH_SEPARATOR}#{@gem_path}" Gem.clear_paths # Load Bundler and setup our paths require "bundler" ::Bundler.setup + + # Do some additional Bundler initialization + ::Bundler.ui = ::Bundler::UI.new + if !::Bundler.ui.respond_to?(:silence) + ui = ::Bundler.ui + def ui.silence(*args) + yield + end + end + end + + # Installs the list of plugins. + # + # @return [Array] + def install(plugins) + gemfile = build_gemfile(plugins) + lockfile = "#{gemfile.path}.lock" + definition = ::Bundler::Definition.build(gemfile, lockfile, nil) + root = File.dirname(gemfile.path) + opts = {} + opts["update"] = true + + with_isolated_gem do + ::Bundler::Installer.install(root, definition, opts) + end + + # Clean up any unused/old gems + runtime = ::Bundler::Runtime.new(root, definition) + runtime.clean + + definition.specs + end + + # Builds a valid Gemfile for use with Bundler given the list of + # plugins. + # + # @return [Tempfile] + def build_gemfile(plugins) + Tempfile.new("vagrant-gemfile").tap do |gemfile| + gemfile.puts(%Q[source "https://rubygems.org"]) + gemfile.puts(%Q[source "http://gems.hashicorp.com"]) + gemfile.puts(%Q[gem "vagrant", "= #{Vagrant::VERSION}"]) + plugins.each do |plugin| + gemfile.puts(%Q[gem "#{plugin}"]) + end + gemfile.close + end + end + + protected + + 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"] = nil + 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 + + # Set a custom configuration to avoid loading ~/.gemrc loads and + # /etc/gemrc and so on. + old_config = Gem.configuration + 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 + 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/init.rb b/lib/vagrant/init.rb index 32ef6e4e5..49c2668cb 100644 --- a/lib/vagrant/init.rb +++ b/lib/vagrant/init.rb @@ -66,6 +66,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 diff --git a/lib/vagrant/plugin/manager.rb b/lib/vagrant/plugin/manager.rb index da5704c7f..20ff87d25 100644 --- a/lib/vagrant/plugin/manager.rb +++ b/lib/vagrant/plugin/manager.rb @@ -1,4 +1,6 @@ +require_relative "../bundler" require_relative "../shared_helpers" +require_relative "state_file" module Vagrant module Plugin @@ -20,6 +22,20 @@ module Vagrant @global_file = StateFile.new(global_file) end + # Installs another plugin into our gem directory. + # + # @param [String] name Name of the plugin (gem) + def install_plugin(name) + result = nil + Vagrant::Bundler.instance.install(installed_plugins.push(name)).each do |spec| + next if spec.name != name + next if result && result.version >= spec.version + result = spec + end + + result + end + # This returns the list of plugins that should be enabled. # # @return [Array] diff --git a/plugins/commands/plugin/action.rb b/plugins/commands/plugin/action.rb index 0e21edd64..4c171256a 100644 --- a/plugins/commands/plugin/action.rb +++ b/plugins/commands/plugin/action.rb @@ -9,7 +9,6 @@ module VagrantPlugins def self.action_install Vagrant::Action::Builder.new.tap do |b| b.use InstallGem - b.use PruneGems end end @@ -31,7 +30,6 @@ module VagrantPlugins def self.action_uninstall Vagrant::Action::Builder.new.tap do |b| b.use UninstallPlugin - b.use PruneGems end end @@ -40,7 +38,6 @@ module VagrantPlugins Vagrant::Action::Builder.new.tap do |b| b.use PluginExistsCheck b.use InstallGem - b.use PruneGems end end diff --git a/plugins/commands/plugin/action/install_gem.rb b/plugins/commands/plugin/action/install_gem.rb index 07a5000fb..ac91754e3 100644 --- a/plugins/commands/plugin/action/install_gem.rb +++ b/plugins/commands/plugin/action/install_gem.rb @@ -8,6 +8,7 @@ rescue LoadError end require "log4r" +require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin @@ -49,40 +50,10 @@ module VagrantPlugins 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) - - # 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. - @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) + # TODO: support version, pre-release, custom sources + manager = Vagrant::Plugin::Manager.instance + plugin_spec = manager.install_plugin(plugin_name) # Tell the user env[:ui].success(I18n.t("vagrant.commands.plugin.installed",