diff --git a/bin/vagrant b/bin/vagrant index 43ec25ba9..f0d638074 100755 --- a/bin/vagrant +++ b/bin/vagrant @@ -20,43 +20,23 @@ if argv.include?("-v") || argv.include?("--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. +# Disable plugin loading for commands where plugins are not required argv.each_index do |i| arg = argv[i] if !arg.start_with?("-") - if arg == "plugin" - ENV["VAGRANT_NO_PLUGINS"] = "1" - ENV["VAGRANT_VAGRANTFILE"] = "plugin_command_#{Time.now.to_i}" + if ["plugin", "help"].include?(arg) || (arg == "box" && argv[i+1] == "list") + ENV['VAGRANT_NO_PLUGINS'] = "1" end - if arg == "help" - ENV["VAGRANT_VAGRANTFILE"] = "plugin_command_#{Time.now.to_i}" - end - - if arg == "box" && argv[i+1] == "list" - ENV["VAGRANT_VAGRANTFILE"] = "plugin_command_#{Time.now.to_i}" + if arg == "plugin" && ["repair", "expunge"].include?(argv[i+1]) + ENV['VAGRANT_DISABLE_PLUGIN_INIT'] = "1" 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") @@ -64,52 +44,6 @@ if argv.include?("--debug") ENV["VAGRANT_LOG"] = "debug" end -# Setup our dependencies by initializing Bundler. If we're using plugins, -# then also initialize the paths to the plugins. -begin - require "bundler" -rescue Errno::EINVAL - # Bundler can generated the EINVAL error during initial require, which means - # nothing has yet been setup (so no access to I18n). Note that vagrant has - # failed early and copy information related to problem and possible solution. - $stderr.puts "Vagrant failed to initialize at a very early stage:\n\n" - $stderr.puts "Vagrant received an \"EINVAL\" error while attempting to set some" - $stderr.puts "environment variables. This is usually caused by the total size of your" - $stderr.puts "environment variables being too large. Vagrant sets a handful of" - $stderr.puts "environment variables to function and requires this to work. Please" - $stderr.puts "delete some environment variables prior to executing Vagrant to" - $stderr.puts "fix this." - exit 255 -end - -begin - $vagrant_bundler_runtime = Bundler.setup(:default, :plugins) -rescue Bundler::GemNotFound - $stderr.puts "Bundler, the underlying system used to manage Vagrant plugins," - $stderr.puts "is reporting that a plugin or its dependency can't be found." - $stderr.puts "This is usually caused by manual tampering with the 'plugins.json'" - $stderr.puts "file in the Vagrant home directory. To fix this error, please" - $stderr.puts "remove that file and reinstall all your plugins using `vagrant" - $stderr.puts "plugin install`." -rescue Bundler::VersionConflict => e - $stderr.puts "Vagrant experienced a version conflict with some installed plugins!" - $stderr.puts "This usually happens if you recently upgraded Vagrant. As part of the" - $stderr.puts "upgrade process, some existing plugins are no longer compatible with" - $stderr.puts "this version of Vagrant. The recommended way to fix this is to remove" - $stderr.puts "your existing plugins and reinstall them one-by-one. To remove all" - $stderr.puts "plugins:" - $stderr.puts "" - $stderr.puts " rm -r ~/.vagrant.d/plugins.json ~/.vagrant.d/gems" - $stderr.puts "" - $stderr.puts "Note if you have an alternate VAGRANT_HOME environmental variable" - $stderr.puts "set, the folders above will be in that directory rather than your" - $stderr.puts "user's home directory." - $stderr.puts "" - $stderr.puts "The error message is shown below:\n\n" - $stderr.puts e.message - exit 1 -end - # Stdout/stderr should not buffer output $stdout.sync = true $stderr.sync = true diff --git a/lib/vagrant.rb b/lib/vagrant.rb index a84bb1b05..4fabcac96 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -1,12 +1,5 @@ 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' @@ -72,11 +65,6 @@ global_logger.info("RubyGems version: #{Gem::VERSION}") ENV.each do |k, v| global_logger.info("#{k}=#{v.inspect}") if k =~ /^VAGRANT_/ end -global_logger.info("Plugins:") -Bundler.definition.specs_for([:plugins]).each do |spec| - global_logger.info(" - #{spec.name} = #{spec.version}") -end - # We need these components always so instead of an autoload we # just require them explicitly here. @@ -254,6 +242,38 @@ if I18n.config.respond_to?(:enforce_available_locales=) I18n.config.enforce_available_locales = true end +# Setup the plugin manager and load any defined plugins +require_relative "vagrant/plugin/manager" +plugins = Vagrant::Plugin::Manager.instance.installed_plugins + +global_logger.info("Plugins:") +plugins.each do |plugin_name, plugin_info| + global_logger.info(" - #{plugin_name} = #{plugin_info["installed_gem_version"]}") +end + +if Vagrant.plugins_init? + begin + Vagrant::Bundler.instance.init!(plugins) + rescue Gem::ConflictError, Gem::DependencyError => e + $stderr.puts "Vagrant experienced a version conflict with some installed plugins!" + $stderr.puts "This usually happens if you recently upgraded Vagrant. As part of the" + $stderr.puts "upgrade process, some existing plugins are no longer compatible with" + $stderr.puts "this version of Vagrant. The recommended way to fix this is to remove" + $stderr.puts "your existing plugins and reinstall them one-by-one. To remove all" + $stderr.puts "plugins:" + $stderr.puts "" + $stderr.puts " vagrant expunge" + $stderr.puts "" + $stderr.puts "Note if you have an alternate VAGRANT_HOME environmental variable" + $stderr.puts "set, the folders above will be in that directory rather than your" + $stderr.puts "user's home directory." + $stderr.puts "" + $stderr.puts "The error message is shown below:\n\n" + $stderr.puts e.message + exit 1 + end +end + # A lambda that knows how to load plugins from a single directory. plugin_load_proc = lambda do |directory| # We only care about directories diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb index ff152f7b7..032522bb0 100644 --- a/lib/vagrant/bundler.rb +++ b/lib/vagrant/bundler.rb @@ -4,7 +4,8 @@ require "set" require "tempfile" require "fileutils" -require "bundler" +require "rubygems/package" +require "rubygems/uninstaller" require_relative "shared_helpers" require_relative "version" @@ -15,83 +16,115 @@ module Vagrant # Bundler as a way to properly resolve all dependencies of Vagrant and # all Vagrant-installed plugins. class Bundler + + HASHICORP_GEMSTORE = 'https://gems.hashicorp.com'.freeze + def self.instance @bundler ||= self.new end + attr_reader :plugin_gem_path + def initialize - @enabled = true if ENV["VAGRANT_INSTALLER_ENV"] || - ENV["VAGRANT_FORCE_BUNDLER"] - @enabled = !::Bundler::SharedHelpers.in_bundle? if !@enabled - @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 = - if ::Bundler::UI.const_defined? :Silent - # bundler >= 1.6.0, we use our custom UI - BundlerUI.new - else - # bundler < 1.6.0 - ::Bundler::UI.new - end - if !::Bundler.ui.respond_to?(:silence) - ui = ::Bundler.ui - def ui.silence(*args) - yield - end - end + @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze end # Initializes Bundler and the various gem paths so that we can begin # loading gems. This must only be called once. - def init!(plugins) - # If we're not enabled, then we don't do anything. - return if !@enabled + def init!(plugins, repair=false) + # Add HashiCorp RubyGems source + Gem.sources << HASHICORP_GEMSTORE - bundle_path = Vagrant.user_data_path.join("gems") - - # Setup the "local" Bundler configuration. We need to set BUNDLE_PATH - # because the existence of this actually suppresses `sudo`. - @appconfigpath = Dir.mktmpdir("vagrant-bundle-app-config") - File.open(File.join(@appconfigpath, "config"), "w+") do |f| - f.write("BUNDLE_PATH: \"#{bundle_path}\"") + # Generate dependencies for all registered plugins + plugin_deps = plugins.map do |name, info| + Gem::Dependency.new(name, info['gem_version'].to_s.empty? ? '> 0' : info['gem_version']) end - # Setup the Bundler configuration - @configfile = tempfile("vagrant-configfile") - @configfile.close + # Load dependencies into a request set for resolution + request_set = Gem::RequestSet.new(*plugin_deps) + # Never allow dependencies to be remotely satisfied during init + request_set.remote = false - # 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) + # Sets that we can resolve our dependencies from + current_set = Gem::Resolver::CurrentSet.new + plugin_set = Gem::Resolver::VendorSet.new + repair_result = nil + begin + # Register all known plugin specifications to the plugin set + Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).each do |spec_path| + spec = Gem::Specification.load(spec_path) + desired_spec_path = File.join(spec.gem_dir, "#{spec.name}.gemspec") + # Vendor set requires the spec to be within the gem directory. Some gems will package their + # spec file, and that's not what we want to load. + if !File.exist?(desired_spec_path) || !FileUtils.cmp(spec.spec_file, desired_spec_path) + File.write(desired_spec_path, spec.to_ruby) + end + plugin_set.add_vendor_gem(spec.name, spec.gem_dir) + end - Util::SafeEnv.change_env do |env| - # Set the environmental variables for Bundler - env["BUNDLE_APP_CONFIG"] = @appconfigpath - env["BUNDLE_CONFIG"] = @configfile.path - env["BUNDLE_GEMFILE"] = @gemfile.path - env["BUNDLE_RETRY"] = "3" - env["GEM_PATH"] = - "#{bundle_path}#{::File::PATH_SEPARATOR}#{@gem_path}" + # Compose set for resolution + composed_set = Gem::Resolver.compose_sets(current_set, plugin_set) + + # Resolve the request set to ensure proper activation order + solution = request_set.resolve(composed_set) + rescue Gem::UnsatisfiableDependencyError => failure + if repair + raise failure if @init_retried + install(plugins) + @init_retried = true + retry + else + $stderr.puts "Vagrant failed to properly initialize due to an error while" + $stderr.puts "while attempting to load configured plugins. This can be caused" + $stderr.puts "by manually tampering with the 'plugins.json' file, or by a" + $stderr.puts "recent Vagrant upgrade. To fix this problem, please run:\n\n" + $stderr.puts " vagrant plugin repair\n\n" + $stderr.puts "The error message is shown below:\n\n" + $stderr.puts failure.message + exit 1 + end end - Gem.clear_paths + # Activate the gems + begin + retried = false + solution.each do |activation_request| + unless activation_request.full_spec.activated? + activation_request.full_spec.activate + if(defined?(::Bundler)) + ::Bundler.rubygems.mark_loaded activation_request.full_spec + end + end + end + rescue Gem::LoadError + # Depending on the version of Ruby, the ordering of the solution set + # will be either 0..n (molinillo) or n..0 (pre-molinillo). Instead of + # attempting to determine what's in use, or if it has some how changed + # again, just reverse order on failure and attempt again. + if retried + raise + else + retried = true + solution.reverse! + retry + end + end + + full_vagrant_spec_list = Gem::Specification.find_all{true} + + solution.map(&:full_spec) + + if(defined?(::Bundler)) + ::Bundler.rubygems.replace_entrypoints(full_vagrant_spec_list) + end + + Gem.post_reset do + Gem::Specification.all = full_vagrant_spec_list + end end # Removes any temporary files created by init def deinit - # If we weren't enabled, then we don't do anything. - return if !@enabled - - FileUtils.rm_rf(ENV["BUNDLE_APP_CONFIG"]) rescue nil - FileUtils.rm_f(ENV["BUNDLE_CONFIG"]) rescue nil - FileUtils.rm_f(ENV["BUNDLE_GEMFILE"]) rescue nil - FileUtils.rm_f(ENV["BUNDLE_GEMFILE"]+".lock") rescue nil + # no-op end # Installs the list of plugins. @@ -107,34 +140,19 @@ module Vagrant # @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 + installer = Gem::Installer.at(path, + ignore_dependencies: true, + install_dir: plugin_gem_path.to_s + ) + installer.install + new_spec = installer.spec + plugin_info = { + new_spec.name => { + 'gem_version' => new_spec.version.to_s + } + } + internal_install(plugin_info, {}) + new_spec end # Update updates the given plugins, or every plugin if none is given. @@ -144,278 +162,114 @@ module Vagrant # 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) + # Generate dependencies for all registered plugins + plugin_deps = plugins.map do |name, info| + Gem::Dependency.new(name, info['gem_version'].to_s.empty? ? '> 0' : info['gem_version']) + end - with_isolated_gem do - runtime = ::Bundler::Runtime.new(root, definition) - runtime.clean + # Load dependencies into a request set for resolution + request_set = Gem::RequestSet.new(*plugin_deps) + # Never allow dependencies to be remotely satisfied during cleaning + request_set.remote = false + + # Sets that we can resolve our dependencies from. Note that we only + # resolve from the current set as all required deps are activated during + # init. + current_set = Gem::Resolver::CurrentSet.new + + # Collect all plugin specifications + plugin_specs = Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path| + Gem::Specification.load(spec_path) + end + + # Resolve the request set to ensure proper activation order + solution = request_set.resolve(current_set) + solution_specs = solution.map(&:full_spec) + + # Find all specs installed to plugins directory that are not + # found within the solution set + plugin_specs.delete_if do |spec| + solution.include?(spec) + end + + # Now delete all unused specs + plugin_specs.each do |spec| + Gem::Uninstaller.new(spec.name, + version: spec.version, + install_dir: plugin_gem_path, + ignore: true + ).uninstall_gem(spec) + end + + solution.find_all do |spec| + plugins.keys.include?(spec.name) 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 + if block_given? + initial_state = @verbose + @verbose = true + yield + @verbose = initial_state + else + @verbose = true end end protected - # Builds a valid Gemfile for use with Bundler given the list of - # plugins. - # - # @return [Tempfile] - def build_gemfile(plugins) - sources = plugins.values.map { |p| p["sources"] }.flatten.compact.uniq - - f = tempfile("vagrant-gemfile") - f.tap do |gemfile| - sources.each do |source| - next if source == "" - gemfile.puts(%Q[source "#{source}"]) - end - - gemfile.puts(%Q[gem "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) + update = {} unless update.is_a?(Hash) + + # Generate all required plugin deps + plugin_deps = plugins.map do |name, info| + if update == true || (update[:gems].respond_to?(:include?) && update[:gems].include?(name)) + gem_version = '> 0' + else + gem_version = info['gem_version'].to_s.empty? ? '> 0' : info['gem_version'] + end + Gem::Dependency.new(name, gem_version) end - # TODO(mitchellh): clean gems here... for some reason when I put - # it in on install, we get a GemNotFound exception. Gotta investigate. + # Create the request set for the new plugins + request_set = Gem::RequestSet.new(*plugin_deps) - definition.specs - rescue ::Bundler::VersionConflict => e - raise Errors::PluginInstallVersionConflict, - conflicts: e.to_s.gsub("Bundler", "Vagrant") - rescue ::Bundler::BundlerError => e - if !::Bundler.ui.is_a?(BundlerUI) - raise + # Generate all existing deps within the "vagrant system" + existing_deps = Gem::Specification.find_all{true}.map do |item| + Gem::Dependency.new(item.name, item.version) end - # Add the warn/error level output from Bundler if we have any - message = "#{e.message}" - if ::Bundler.ui.output != "" - message += "\n\n#{::Bundler.ui.output}" + # Import constraints into the request set to prevent installing + # gems that are incompatible with the core system + request_set.import(existing_deps) + + # Generate the required solution set for new plugins + solution = request_set.resolve(Gem::Resolver::InstallerSet.new(:both)) + + # If any items in the solution set are local but not activated, turn them on + solution.each do |activation_request| + if activation_request.installed? && !activation_request.full_spec.activated? + activation_request.full_spec.activate + end end - raise ::Bundler::BundlerError, message + # Install all remote gems into plugin path. Set the installer to ignore dependencies + # as we know the dependencies are satisfied and it will attempt to validate a gem's + # dependencies are satisified by gems in the install directory (which will likely not + # be true) + result = request_set.install_into(plugin_gem_path.to_s, true, ignore_dependencies: true) + result.map(&:full_spec) end - def with_isolated_gem - raise Errors::BundlerDisabled if !@enabled - - tmp_gemfile = tempfile("vagrant-gemfile") - tmp_gemfile.close - - # 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"] = tmp_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 - - # WARNING: Seriously don't touch this without reading the comment attached - # to the monkey-patch at the bottom of this file. - Gem::Specification.vagrant_reset! - - # /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 - tmp_gemfile.unlink rescue nil - - 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 method returns a proper "tempfile" on disk. Ruby's Tempfile class - # would work really great for this, except GC can come along and remove - # the file before we are done with it. This is because we "close" the file, - # but we might be shelling out to a subprocess. - # - # @return [File] - def tempfile(name) - path = Dir::Tmpname.create(name) {} - return File.open(path, "w+") - 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 - - # This monkey patches Gem::Specification from RubyGems to add a new method, - # `vagrant_reset!`. For some background, Vagrant needs to set the value - # of these variables to nil to force new specs to be loaded. Previously, - # this was accomplished by setting Gem::Specification.specs = nil. However, - # newer versions of Rubygems try to map across that nil using a group_by - # clause, breaking things. - # - # This generally never affected Vagrant users who were using the official - # Vagrant installers because we lock to an older version of Rubygems that - # does not have this issue. The users of the official debian packages, - # however, experienced this issue because they float on Rubygems. - # - # In GH-7073, a number of Debian users reported this issue, but it was not - # reproducible in the official installer for reasons described above. Commit - # ba77d4b switched to using Gem::Specification.reset, but this actually - # broke the ability to install gems locally (GH-7493) because it resets - # the complete local cache, which is already built. - # - # The only solution that works with both new and old versions of Rubygems - # is to provide our own function for JUST resetting all the stubs. Both - # @@all and @@stubs must be set to a falsey value, so some of the - # originally-suggested solutions of using an empty array do not work. Only - # setting these values to nil (without clearing the cache), allows Vagrant - # to install and manage plugins. - class Gem::Specification < Gem::BasicSpecification - def self.vagrant_reset! - @@all = @@stubs = nil - end - end - - if ::Bundler::UI.const_defined? :Silent - class BundlerUI < ::Bundler::UI::Silent - attr_reader :output - - def initialize - @output = "" - end - - def info(message, newline = nil) - end - - def confirm(message, newline = nil) - end - - def warn(message, newline = nil) - @output += message - @output += "\n" if newline - end - - def error(message, newline = nil) - @output += message - @output += "\n" if newline - end - - def debug(message, newline = nil) - end - - def debug? - false - end - - def quiet? - false - end - - def ask(message) - end - - def level=(name) - end - - def level(name = nil) - "info" - end - - def trace(message, newline = nil) - end - - def silence - yield - end - end - end end end diff --git a/lib/vagrant/plugin/manager.rb b/lib/vagrant/plugin/manager.rb index dfaab2ecf..a61da93fb 100644 --- a/lib/vagrant/plugin/manager.rb +++ b/lib/vagrant/plugin/manager.rb @@ -78,12 +78,13 @@ module Vagrant version: opts[:version], require: opts[:require], sources: opts[:sources], + installed_gem_version: result.version ) result - rescue ::Bundler::GemNotFound + rescue Gem::GemNotFoundException raise Errors::PluginGemNotFound, name: name - rescue ::Bundler::BundlerError => e + rescue Gem::Exception => e raise Errors::BundlerError, message: e.to_s end @@ -102,14 +103,14 @@ module Vagrant # Clean the environment, removing any old plugins Vagrant::Bundler.instance.clean(installed_plugins) - rescue ::Bundler::BundlerError => e + rescue Gem::Exception => 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 + rescue Gem::Exception => e raise Errors::BundlerError, message: e.to_s end diff --git a/lib/vagrant/pre-rubygems.rb b/lib/vagrant/pre-rubygems.rb deleted file mode 100644 index 63b46f594..000000000 --- a/lib/vagrant/pre-rubygems.rb +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -require_relative "shared_helpers" - -if defined?(Bundler) - require "bundler/shared_helpers" - if Bundler::SharedHelpers.in_bundle? && !Vagrant.very_quiet? - puts "Vagrant appears to be running in a Bundler environment. Your " - puts "existing Gemfile will be used. Vagrant will not auto-load any plugins" - puts "installed with `vagrant plugin`. Vagrant will autoload any plugins in" - puts "the 'plugins' group in your Gemfile. You can force Vagrant to take over" - puts "with VAGRANT_FORCE_BUNDLER." - puts - end -end - -require_relative "bundler" -require_relative "plugin/manager" - -plugins = Vagrant::Plugin::Manager.instance.installed_plugins -Vagrant::Bundler.instance.init!(plugins) - -ENV["VAGRANT_INTERNAL_BUNDLERIZED"] = "1" - -# If the VAGRANT_EXECUTABLE env is set, then we use that to point to a -# Ruby file to directly execute. Otherwise, we just depend on PATH lookup. -# This minor optimization can save hundreds of milliseconds on Windows. -if ENV["VAGRANT_EXECUTABLE"] - Kernel.exec("ruby", ENV["VAGRANT_EXECUTABLE"], *ARGV) -else - Kernel.exec("vagrant", *ARGV) -end diff --git a/lib/vagrant/shared_helpers.rb b/lib/vagrant/shared_helpers.rb index fe114013e..ed9bf9595 100644 --- a/lib/vagrant/shared_helpers.rb +++ b/lib/vagrant/shared_helpers.rb @@ -38,6 +38,13 @@ module Vagrant ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"] end + # Should the plugin system be initialized + # + # @return [Boolean] + def self.plugins_init? + !ENV['VAGRANT_DISABLE_PLUGIN_INIT'] + end + # This returns whether or not 3rd party plugins should and can be loaded. # # @return [Boolean] diff --git a/lib/vagrant/util/env.rb b/lib/vagrant/util/env.rb index 689c9e5ac..6a8714e54 100644 --- a/lib/vagrant/util/env.rb +++ b/lib/vagrant/util/env.rb @@ -5,7 +5,9 @@ module Vagrant class Env def self.with_original_env original_env = ENV.to_hash - ENV.replace(::Bundler::ORIGINAL_ENV) if defined?(::Bundler::ORIGINAL_ENV) + if defined?(::Bundler) && defined?(::Bundler::ORIGINAL_ENV) + ENV.replace(::Bundler::ORIGINAL_ENV) + end ENV.update(Vagrant.original_env) yield ensure diff --git a/lib/vagrant/util/subprocess.rb b/lib/vagrant/util/subprocess.rb index 75bc101cc..3737d3897 100644 --- a/lib/vagrant/util/subprocess.rb +++ b/lib/vagrant/util/subprocess.rb @@ -297,7 +297,9 @@ module Vagrant def jailbreak(env = {}) return if ENV.key?("VAGRANT_SKIP_SUBPROCESS_JAILBREAK") - env.replace(::Bundler::ORIGINAL_ENV) if defined?(::Bundler::ORIGINAL_ENV) + if defined?(::Bundler) && defined?(::Bundler::ORIGINAL_ENV) + env.replace(::Bundler::ORIGINAL_ENV) + end env.merge!(Vagrant.original_env) # Bundler does this, so I guess we should as well, since I think it diff --git a/plugins/commands/plugin/action.rb b/plugins/commands/plugin/action.rb index 832bc3508..e1ed6dbd7 100644 --- a/plugins/commands/plugin/action.rb +++ b/plugins/commands/plugin/action.rb @@ -6,6 +6,12 @@ module VagrantPlugins module CommandPlugin module Action # This middleware sequence will install a plugin. + def self.action_expunge + Vagrant::Action::Builder.new.tap do |b| + b.use ExpungePlugins + end + end + def self.action_install Vagrant::Action::Builder.new.tap do |b| b.use InstallGem @@ -27,6 +33,13 @@ module VagrantPlugins end end + # This middleware sequence will repair installed plugins. + def self.action_repair + Vagrant::Action::Builder.new.tap do |b| + b.use RepairPlugins + end + end + # This middleware sequence will uninstall a plugin. def self.action_uninstall Vagrant::Action::Builder.new.tap do |b| @@ -44,10 +57,12 @@ module VagrantPlugins # The autoload farm action_root = Pathname.new(File.expand_path("../action", __FILE__)) + autoload :ExpungePlugins, action_root.join("expunge_plugins") 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 :RepairPlugins, action_root.join("repair_plugins") autoload :UninstallPlugin, action_root.join("uninstall_plugin") autoload :UpdateGems, action_root.join("update_gems") end diff --git a/plugins/commands/plugin/action/expunge_plugins.rb b/plugins/commands/plugin/action/expunge_plugins.rb new file mode 100644 index 000000000..0debc5803 --- /dev/null +++ b/plugins/commands/plugin/action/expunge_plugins.rb @@ -0,0 +1,51 @@ +require "vagrant/plugin/manager" + +module VagrantPlugins + module CommandPlugin + module Action + # This middleware removes user installed plugins by + # removing: + # * ~/.vagrant.d/plugins.json + # * ~/.vagrant.d/gems + # Usage should be restricted to when a repair is + # unsuccessful and the only reasonable option remaining + # is to re-install all plugins + class ExpungePlugins + def initialize(app, env) + @app = app + end + + def call(env) + if !env[:force] + result = env[:ui].ask( + I18n.t("vagrant.commands.plugin.expunge_confirm") + + " [Y/N]:" + ) + if result.to_s.downcase.strip != 'y' + abort_action = true + end + end + + if !abort_action + plugins_json = File.join(env[:home_path], "plugins.json") + plugins_gems = env[:gems_path] + + if File.exist?(plugins_json) + FileUtils.rm(plugins_json) + end + + if File.directory?(plugins_gems) + FileUtils.rm_rf(plugins_gems) + end + + env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_complete")) + + @app.call(env) + else + env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_aborted")) + end + end + end + end + end +end diff --git a/plugins/commands/plugin/action/repair_plugins.rb b/plugins/commands/plugin/action/repair_plugins.rb new file mode 100644 index 000000000..3ce886b3f --- /dev/null +++ b/plugins/commands/plugin/action/repair_plugins.rb @@ -0,0 +1,31 @@ +require "vagrant/plugin/manager" + +module VagrantPlugins + module CommandPlugin + module Action + # This middleware attempts to repair installed plugins. + # + # In general, if plugins are failing to properly load the + # core issue will likely be one of two issues: + # 1. manual modifications within ~/.vagrant.d/ + # 2. vagrant upgrade + # Running an install on configured plugin set will most + # likely fix these issues, which is all this action does. + class RepairPlugins + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t("vagrant.commands.plugin.repairing")) + plugins = Vagrant::Plugin::Manager.instance.installed_plugins + Vagrant::Bundler.instance.init!(plugins, :repair) + env[:ui].info(I18n.t("vagrant.commands.plugin.repair_complete")) + + # Continue + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/plugin/command/expunge.rb b/plugins/commands/plugin/command/expunge.rb new file mode 100644 index 000000000..9e5db4bd3 --- /dev/null +++ b/plugins/commands/plugin/command/expunge.rb @@ -0,0 +1,65 @@ +require 'optparse' + +require_relative "base" + +module VagrantPlugins + module CommandPlugin + module Command + class Expunge < Base + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant plugin expunge [-h]" + + o.on("--force", "Do not prompt for confirmation") do |force| + options[:force] = force + end + + o.on("--reinstall", "Reinstall current plugins after expunge") do |reinstall| + options[:reinstall] = reinstall + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 + + plugins = Vagrant::Plugin::Manager.instance.installed_plugins + + if !options[:reinstall] && !options[:force] && !plugins.empty? + result = @env.ui.ask( + I18n.t("vagrant.commands.plugin.expunge_request_reinstall") + + " [Y/N]:" + ) + options[:reinstall] = result.to_s.downcase.strip == "y" + end + + # Remove all installed user plugins + action(Action.action_expunge, options) + + if options[:reinstall] + @env.ui.info(I18n.t("vagrant.commands.plugin.expunge_reinstall")) + plugins.each do |plugin_name, plugin_info| + plugin_info = Hash[ + plugin_info.map do |key, value| + [key.to_sym, value] + end + ] + action( + Action.action_install, + plugin_info.merge( + plugin_name: plugin_name + ) + ) + end + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/plugin/command/repair.rb b/plugins/commands/plugin/command/repair.rb new file mode 100644 index 000000000..6deff6ae2 --- /dev/null +++ b/plugins/commands/plugin/command/repair.rb @@ -0,0 +1,28 @@ +require 'optparse' + +require_relative "base" + +module VagrantPlugins + module CommandPlugin + module Command + class Repair < Base + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant plugin repair [-h]" + end + + # Parse the options + argv = parse_options(opts) + return if !argv + raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 + + # Attempt to repair installed plugins + action(Action.action_repair) + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/plugin/command/root.rb b/plugins/commands/plugin/command/root.rb index 8c4d1ad1b..6953eb495 100644 --- a/plugins/commands/plugin/command/root.rb +++ b/plugins/commands/plugin/command/root.rb @@ -14,6 +14,11 @@ module VagrantPlugins @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new + @subcommands.register(:expunge) do + require_relative "expunge" + Expunge + end + @subcommands.register(:install) do require_relative "install" Install @@ -29,6 +34,11 @@ module VagrantPlugins List end + @subcommands.register(:repair) do + require_relative "repair" + Repair + end + @subcommands.register(:update) do require_relative "update" Update diff --git a/templates/locales/en.yml b/templates/locales/en.yml index d3114d845..88099cedb 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1576,6 +1576,24 @@ en: the comments in the Vagrantfile as well as documentation on `vagrantup.com` for more information on using Vagrant. plugin: + expunge_confirm: |- + + This command permanently deletes all currently installed user plugins. It + should only be used when a repair command is unable to properly fix the + system. + + Continue? + expunge_request_reinstall: |- + Would you like Vagrant to attempt to reinstall current plugins? + expunge_complete: |- + + All user installed plugins have been removed from this Vagrant environment! + expunge_reinstall: |- + + Vagrant will now attempt to reinstall user plugins that were removed. + expunge_aborted: |- + + Vagrant expunge has been declined. Skipping removal of plugins. installed_license: |- The license for '%{name}' was successfully installed! installing_license: |- @@ -1601,6 +1619,14 @@ en: post_install: |- Post install message from the '%{name}' plugin: + %{message} + repairing: |- + Repairing currently installed plugins. This may take a few minutes... + repair_complete: |- + Installed plugins successfully repaired! + repair_failed: |- + Failed to automatically repair installed Vagrant plugins. Failure message: + %{message} snapshot: not_supported: |- diff --git a/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb b/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb new file mode 100644 index 000000000..de0d976de --- /dev/null +++ b/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb @@ -0,0 +1,64 @@ +require File.expand_path("../../../../../base", __FILE__) + +describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do + let(:app) { lambda { |env| } } + let(:home_path){ '/fake/file/path/.vagrant.d' } + let(:gems_path){ "#{home_path}/gems" } + let(:force){ true } + let(:env) {{ + ui: Vagrant::UI::Silent.new, + home_path: home_path, + gems_path: gems_path, + force: force + }} + + let(:manager) { double("manager") } + + let(:expect_to_receive) do + lambda do + allow(File).to receive(:exist?).with(File.join(home_path, 'plugins.json')).and_return(true) + allow(File).to receive(:directory?).with(gems_path).and_return(true) + expect(FileUtils).to receive(:rm).with(File.join(home_path, 'plugins.json')) + expect(FileUtils).to receive(:rm_rf).with(gems_path) + expect(app).to receive(:call).with(env).once + end + end + + subject { described_class.new(app, env) } + + before do + Vagrant::Plugin::Manager.stub(instance: manager) + end + + describe "#call" do + before do + instance_exec(&expect_to_receive) + end + + it "should delete all plugins" do + subject.call(env) + end + + describe "when force is false" do + let(:force){ false } + + it "should prompt user before deleting all plugins" do + expect(env[:ui]).to receive(:ask).and_return("Y\n") + subject.call(env) + end + + describe "when user declines prompt" do + let(:expect_to_receive) do + lambda do + expect(app).not_to receive(:call) + end + end + + it "should not delete all plugins" do + expect(env[:ui]).to receive(:ask).and_return("N\n") + subject.call(env) + end + end + end + end +end diff --git a/test/unit/vagrant/plugin/manager_test.rb b/test/unit/vagrant/plugin/manager_test.rb index ea8764b06..328c0b9da 100644 --- a/test/unit/vagrant/plugin/manager_test.rb +++ b/test/unit/vagrant/plugin/manager_test.rb @@ -45,14 +45,14 @@ describe Vagrant::Plugin::Manager do end it "masks GemNotFound with our error" do - expect(bundler).to receive(:install).and_raise(Bundler::GemNotFound) + expect(bundler).to receive(:install).and_raise(Gem::GemNotFoundException) expect { subject.install_plugin("foo") }. to raise_error(Vagrant::Errors::PluginGemNotFound) end it "masks bundler errors with our own error" do - expect(bundler).to receive(:install).and_raise(Bundler::InstallError) + expect(bundler).to receive(:install).and_raise(Gem::InstallError) expect { subject.install_plugin("foo") }. to raise_error(Vagrant::Errors::BundlerError) @@ -140,7 +140,7 @@ describe Vagrant::Plugin::Manager do end it "masks bundler errors with our own error" do - expect(bundler).to receive(:clean).and_raise(Bundler::InstallError) + expect(bundler).to receive(:clean).and_raise(Gem::InstallError) expect { subject.uninstall_plugin("foo") }. to raise_error(Vagrant::Errors::BundlerError) @@ -182,7 +182,7 @@ describe Vagrant::Plugin::Manager do describe "#update_plugins" do it "masks bundler errors with our own error" do - expect(bundler).to receive(:update).and_raise(Bundler::InstallError) + expect(bundler).to receive(:update).and_raise(Gem::InstallError) expect { subject.update_plugins([]) }. to raise_error(Vagrant::Errors::BundlerError) diff --git a/vagrant.gemspec b/vagrant.gemspec index 92d0a0bb7..a40848466 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -16,11 +16,6 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.rubyforge_project = "vagrant" - # Do not update the Bundler constraint. Vagrant relies on internal Bundler - # APIs, so even point releases can introduce breaking changes. These changes - # are *untestable* until after a release is made because there is no way for - # Bundler to exec into itself. Please do not update the Bundler constraint. - s.add_dependency "bundler", "= 1.12.5" s.add_dependency "childprocess", "~> 0.5.0" s.add_dependency "erubis", "~> 2.7.0" s.add_dependency "i18n", ">= 0.6.0", "<= 0.8.0"