diff --git a/lib/vagrant.rb b/lib/vagrant.rb index c775b5f37..64183c092 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -3,6 +3,7 @@ require "vagrant/shared_helpers" require "rubygems" require "log4r" require "vagrant/util" +require "vagrant/plugin/manager" # Enable logging if it is requested. We do this before # anything else so that we can setup the output before @@ -262,35 +263,6 @@ else global_logger.warn("resolv replacement has not been enabled!") 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| - installed_version = plugin_info["installed_gem_version"] - version_constraint = plugin_info["gem_version"] - installed_version = 'undefined' if installed_version.to_s.empty? - version_constraint = '> 0' if version_constraint.to_s.empty? - global_logger.info( - " - #{plugin_name} = [installed: " \ - "#{installed_version} constraint: " \ - "#{version_constraint}]" - ) -end - -if Vagrant.plugins_init? - begin - Vagrant::Bundler.instance.init!(plugins) - rescue StandardError, ScriptError => e - global_logger.error("Plugin initialization error - #{e.class}: #{e}") - e.backtrace.each do |backtrace_line| - global_logger.debug(backtrace_line) - end - raise Vagrant::Errors::PluginInitError, message: e.to_s - 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 @@ -320,43 +292,3 @@ 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 -if Vagrant.plugins_enabled? - begin - global_logger.info("Loading plugins!") - plugins.each do |plugin_name, plugin_info| - if plugin_info["require"].to_s.empty? - begin - global_logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`") - require plugin_name - rescue LoadError => err - if plugin_name.include?("-") - plugin_slash = plugin_name.gsub("-", "/") - global_logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}") - global_logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`") - require plugin_slash - else - raise - end - end - else - global_logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`") - require plugin_info["require"] - end - global_logger.debug("Successfully loaded plugin `#{plugin_name}`.") - end - if defined?(::Bundler) - global_logger.debug("Bundler detected in use. Loading `:plugins` group.") - ::Bundler.require(:plugins) - end - rescue ScriptError, StandardError => err - global_logger.error("Plugin loading error: #{err.class} - #{err}") - err.backtrace.each do |backtrace_line| - global_logger.debug(backtrace_line) - end - raise Vagrant::Errors::PluginLoadError, message: err.to_s - end -else - global_logger.debug("Plugin loading is currently disabled.") -end diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb index 301e40e37..18b734064 100644 --- a/lib/vagrant/bundler.rb +++ b/lib/vagrant/bundler.rb @@ -32,22 +32,42 @@ module Vagrant @bundler ||= self.new end + # @return [Pathname] Global plugin path attr_reader :plugin_gem_path + # @return [Pathname] Vagrant environment specific plugin path + attr_reader :env_plugin_gem_path def initialize @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze @logger = Log4r::Logger.new("vagrant::bundler") end + # Enable Vagrant environment specific plugins at given data path + # + # @param [Pathname] Path to Vagrant::Environment data directory + # @return [Pathname] Path to environment specific gem directory + def environment_path=(env_data_path) + @env_plugin_gem_path = env_data_path.join("plugins", "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. + # loading gems. def init!(plugins, repair=false) + if !@initial_specifications + @initial_specifications = Gem::Specification.find_all{true} + else + Gem::Specification.all = @initial_specifications + Gem::Specification.reset + end + # Add HashiCorp RubyGems source - Gem.sources << HASHICORP_GEMSTORE + if !Gem.sources.include?(HASHICORP_GEMSTORE) + Gem.sources << HASHICORP_GEMSTORE + end # 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']) + Gem::Dependency.new(name, info['installed_gem_version'].to_s.empty? ? '> 0' : info['installed_gem_version']) end @logger.debug("Current generated plugin dependency list: #{plugin_deps}") @@ -78,7 +98,7 @@ module Vagrant # Activate the gems activate_solution(solution) - full_vagrant_spec_list = Gem::Specification.find_all{true} + + full_vagrant_spec_list = @initial_specifications + solution.map(&:full_spec) if(defined?(::Bundler)) @@ -120,7 +140,7 @@ module Vagrant } } @logger.debug("Installing local plugin - #{plugin_info}") - internal_install(plugin_info, {}) + internal_install(plugin_info, nil, local: opts[:local]) plugin_source.spec end @@ -129,14 +149,14 @@ module Vagrant # @param [Hash] plugins # @param [Array] specific Specific plugin names to update. If # empty or nil, all plugins will be updated. - def update(plugins, specific) + def update(plugins, specific, **opts) specific ||= [] - update = {gems: specific.empty? ? true : specific} + update = opts.merge({gems: specific.empty? ? true : specific}) internal_install(plugins, update) end # Clean removes any unused gems. - def clean(plugins) + def clean(plugins, **opts) @logger.debug("Cleaning Vagrant plugins of stale gems.") # Generate dependencies for all registered plugins plugin_deps = plugins.map do |name, info| @@ -163,6 +183,13 @@ module Vagrant Gem::Specification.load(spec_path) end + # Include environment specific specification if enabled + if env_plugin_gem_path + plugin_specs += Dir.glob(env_plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path| + Gem::Specification.load(spec_path) + end + end + @logger.debug("Generating current plugin state solution set.") # Resolve the request set to ensure proper activation order @@ -171,11 +198,27 @@ module Vagrant solution_full_names = solution_specs.map(&:full_name) # Find all specs installed to plugins directory that are not - # found within the solution set + # found within the solution set. plugin_specs.delete_if do |spec| solution_full_names.include?(spec.full_name) end + if env_plugin_gem_path + # If we are cleaning locally, remove any global specs. If + # not, remove any local specs + if opts[:local] + @logger.debug("Removing specifications that are not environment local") + plugin_specs.delete_if do |spec| + spec.full_gem_path.to_s.include?(plugin_gem_path.realpath.to_s) + end + else + @logger.debug("Removing specifications that are environment local") + plugin_specs.delete_if do |spec| + spec.full_gem_path.to_s.include?(env_plugin_gem_path.realpath.to_s) + end + end + end + @logger.debug("Specifications to be removed - #{plugin_specs.map(&:full_name)}") # Now delete all unused specs @@ -318,18 +361,37 @@ module Vagrant # as we know the dependencies are satisfied and it will attempt to validate a gem's # dependencies are satisfied by gems in the install directory (which will likely not # be true) - result = request_set.install_into(plugin_gem_path.to_s, true, + install_path = extra[:local] ? env_plugin_gem_path : plugin_gem_path + result = request_set.install_into(install_path.to_s, true, ignore_dependencies: true, prerelease: Vagrant.prerelease?, wrappers: true ) result = result.map(&:full_spec) + result.each do |spec| + existing_paths = $LOAD_PATH.find_all{|s| s.include?(spec.full_name) } + if !existing_paths.empty? + @logger.debug("Removing existing LOAD_PATHs for #{spec.full_name} - " + + existing_paths.join(", ")) + existing_paths.each{|s| $LOAD_PATH.delete(s) } + end + spec.full_require_paths.each do |r_path| + if !$LOAD_PATH.include?(r_path) + @logger.debug("Adding path to LOAD_PATH - #{r_path}") + $LOAD_PATH.unshift(r_path) + end + end + end result end # Generate the composite resolver set totally all of vagrant (builtin + plugin set) def generate_vagrant_set - Gem::Resolver.compose_sets(generate_builtin_set, generate_plugin_set) + sets = [generate_builtin_set, generate_plugin_set] + if env_plugin_gem_path && env_plugin_gem_path.exist? + sets << generate_plugin_set(env_plugin_gem_path) + end + Gem::Resolver.compose_sets(*sets) end # @return [Array<[Gem::Specification, String]>] spec and directory pairs @@ -387,10 +449,16 @@ module Vagrant # Generate the plugin resolver set. Optionally provide specification names (short or # full) that should be ignored - def generate_plugin_set(skip=[]) + # + # @param [Pathname] path to plugins + # @param [Array] gems to skip + # @return [PluginSet] + def generate_plugin_set(*args) + plugin_path = args.detect{|i| i.is_a?(Pathname) } || plugin_gem_path + skip = args.detect{|i| i.is_a?(Array) } || [] plugin_set = PluginSet.new @logger.debug("Generating new plugin set instance. Skip gems - #{skip}") - Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).each do |spec_path| + Dir.glob(plugin_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 diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 60eb4a685..946b5c1a8 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -146,6 +146,7 @@ module Vagrant if opts[:local_data_path] @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd)) end + @logger.debug("Effective local data path: #{@local_data_path}") # If we have a root path, load the ".vagrantplugins" file. @@ -163,6 +164,19 @@ module Vagrant @default_private_key_path = @home_path.join("insecure_private_key") copy_insecure_private_key + # Initialize localized plugins + plugins = Vagrant::Plugin::Manager.instance.localize!(self) + + if !vagrantfile.config.vagrant.plugins.empty? + plugins = process_configured_plugins + end + + # Load any local plugins + Vagrant::Plugin::Manager.instance.load_plugins(plugins) + + plugins = Vagrant::Plugin::Manager.instance.globalize! + Vagrant::Plugin::Manager.instance.load_plugins(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)) @@ -898,6 +912,55 @@ module Vagrant protected + # Check for any local plugins defined within the Vagrantfile. If + # found, validate they are available. If they are not available, + # request to install them, or raise an exception + # + # @return [Hash] plugin list for loading + def process_configured_plugins + return if !Vagrant.plugins_enabled? + errors = vagrantfile.config.vagrant.validate(nil) + if !errors["vagrant"].empty? + raise Errors::ConfigInvalid, + errors: Util::TemplateRenderer.render( + "config/validation_failed", + errors: errors) + end + # Check if defined plugins are installed + installed = Plugin::Manager.instance.installed_plugins + needs_install = [] + config_plugins = vagrantfile.config.vagrant.plugins + config_plugins.each do |name, info| + if !installed[name] + needs_install << name + end + end + if !needs_install.empty? + ui.warn(I18n.t("vagrant.plugins.local.uninstalled_plugins", + plugins: needs_install.sort.join(", "))) + answer = nil + until ["y", "n"].include?(answer) + answer = ui.ask(I18n.t("vagrant.plugins.local.request_plugin_install") + ": ") + answer.strip.downcase! + end + if answer == "n" + raise Errors::PluginMissingLocalError, + plugins: needs_install.sort.join(", ") + end + needs_install.each do |name| + ui.info(I18n.t("vagrant.commands.plugin.installing", name: name)) + spec = Plugin::Manager.instance.install_plugin(name, + {sources: Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup}.merge( + config_plugins[name]).merge(local: true)) + ui.info(I18n.t("vagrant.commands.plugin.installed", + name: spec.name, version: spec.version.to_s)) + end + ui.info("\n") + Vagrant::Plugin::Manager.instance.localize!(self) + end + Vagrant::Plugin::Manager.instance.local_file.installed_plugins + end + # This method copies the private key into the home directory if it # doesn't already exist. # diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 7eef2d6d9..8fe7d9e71 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -640,6 +640,14 @@ module Vagrant error_key(:plugin_source_error) end + class PluginNoLocalError < VagrantError + error_key(:plugin_no_local_error) + end + + class PluginMissingLocalError < VagrantError + error_key(:plugin_missing_local_error) + end + class PushesNotDefined < VagrantError error_key(:pushes_not_defined) end diff --git a/lib/vagrant/plugin/manager.rb b/lib/vagrant/plugin/manager.rb index 64891bd37..4487b143f 100644 --- a/lib/vagrant/plugin/manager.rb +++ b/lib/vagrant/plugin/manager.rb @@ -27,13 +27,68 @@ module Vagrant @instance ||= self.new(user_plugins_file) end + attr_reader :user_file + attr_reader :system_file + attr_reader :local_file + # @param [Pathname] user_file def initialize(user_file) + @logger = Log4r::Logger.new("vagrant::plugin::manager") @user_file = StateFile.new(user_file) system_path = self.class.system_plugins_file @system_file = nil @system_file = StateFile.new(system_path) if system_path && system_path.file? + + @local_file = nil + end + + def globalize! + @logger.debug("Enabling globalized plugins") + if !Vagrant.plugins_init? + @logger.warn("Plugin initialization is disabled") + return {} + end + + plugins = Vagrant::Plugin::Manager.instance.installed_plugins + bundler_init(plugins) + plugins + end + + # @param [Environment] env Vagrant environment + def localize!(env) + if env.local_data_path + @logger.debug("Enabling localized plugins") + @local_file = StateFile.new(env.local_data_path.join("plugins.json")) + Vagrant::Bundler.instance.environment_path = env.local_data_path + plugins = local_file.installed_plugins + bundler_init(plugins) + plugins + end + end + + def bundler_init(plugins) + @logger.info("Plugins:") + plugins.each do |plugin_name, plugin_info| + installed_version = plugin_info["installed_gem_version"] + version_constraint = plugin_info["gem_version"] + installed_version = 'undefined' if installed_version.to_s.empty? + version_constraint = '> 0' if version_constraint.to_s.empty? + @logger.info( + " - #{plugin_name} = [installed: " \ + "#{installed_version} constraint: " \ + "#{version_constraint}]" + ) + end + begin + Vagrant::Bundler.instance.init!(plugins) + rescue StandardError, ScriptError => err + @logger.error("Plugin initialization error - #{err.class}: #{err}") + err.backtrace.each do |backtrace_line| + @logger.debug(backtrace_line) + end + raise Vagrant::Errors::PluginInitError, message: err.to_s + end end # Installs another plugin into our gem directory. @@ -41,7 +96,10 @@ module Vagrant # @param [String] name Name of the plugin (gem) # @return [Gem::Specification] def install_plugin(name, **opts) - local = false + if opts[:local] && @local_file.nil? + raise Errors::PluginNoLocalError + end + if name =~ /\.gem$/ # If this is a gem file, then we install that gem locally. local_spec = Vagrant::Bundler.instance.install_local(name, opts) @@ -59,7 +117,7 @@ module Vagrant if local_spec.nil? result = nil install_lambda = lambda do - Vagrant::Bundler.instance.install(plugins, local).each do |spec| + Vagrant::Bundler.instance.install(plugins, opts[:local]).each do |spec| next if spec.name != name next if result && result.version >= spec.version result = spec @@ -75,18 +133,20 @@ module Vagrant result = local_spec end # Add the plugin to the state file - @user_file.add_plugin( + plugin_file = opts[:local] ? @local_file : @user_file + plugin_file.add_plugin( result.name, version: opts[:version], require: opts[:require], sources: opts[:sources], + local: !!opts[:local], installed_gem_version: result.version.to_s ) # After install clean plugin gems to remove any cruft. This is useful # for removing outdated dependencies or other versions of an installed # plugin if the plugin is upgraded/downgraded - Vagrant::Bundler.instance.clean(installed_plugins) + Vagrant::Bundler.instance.clean(installed_plugins, local: !!opts[:local]) result rescue Gem::GemNotFoundException raise Errors::PluginGemNotFound, name: name @@ -97,7 +157,7 @@ module Vagrant # Uninstalls the plugin with the given name. # # @param [String] name - def uninstall_plugin(name) + def uninstall_plugin(name, **opts) if @system_file if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name) raise Errors::PluginUninstallSystem, @@ -105,7 +165,18 @@ module Vagrant end end - @user_file.remove_plugin(name) + if opts[:local] && @local_file.nil? + raise Errors::PluginNoLocalError + end + + plugin_file = opts[:local] ? @local_file : @user_file + + if !plugin_file.has_plugin?(name) + raise Errors::PluginNotInstalled, + name: name + end + + plugin_file.remove_plugin(name) # Clean the environment, removing any old plugins Vagrant::Bundler.instance.clean(installed_plugins) @@ -114,9 +185,15 @@ module Vagrant end # Updates all or a specific set of plugins. - def update_plugins(specific) - result = Vagrant::Bundler.instance.update(installed_plugins, specific) - installed_plugins.each do |name, info| + def update_plugins(specific, **opts) + if opts[:local] && @local_file.nil? + raise Errors::PluginNoLocalError + end + + plugin_file = opts[:local] ? @local_file : @user_file + + result = Vagrant::Bundler.instance.update(plugin_list.installed_plugins, specific) + plugin_list.installed_plugins.each do |name, info| matching_spec = result.detect{|s| s.name == name} info = Hash[ info.map do |key, value| @@ -124,7 +201,7 @@ module Vagrant end ] if matching_spec - @user_file.add_plugin(name, **info.merge( + plugin_file.add_plugin(name, **info.merge( version: "> 0", installed_gem_version: matching_spec.version.to_s )) @@ -148,6 +225,11 @@ module Vagrant end plugin_list = Util::DeepMerge.deep_merge(system, @user_file.installed_plugins) + if @local_file + plugin_list = Util::DeepMerge.deep_merge(plugin_list, + @local_file.installed_plugins) + end + # Sort plugins by name Hash[ plugin_list.map{|plugin_name, plugin_info| @@ -191,6 +273,48 @@ module Vagrant installed_map.values end + + def load_plugins(plugins) + if !Vagrant.plugins_enabled? + @logger.warn("Plugin loading is disabled") + return + end + + begin + @logger.info("Loading plugins...") + plugins.each do |plugin_name, plugin_info| + if plugin_info["require"].to_s.empty? + begin + @logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`") + require plugin_name + rescue LoadError => err + if plugin_name.include?("-") + plugin_slash = plugin_name.gsub("-", "/") + @logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}") + @logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`") + require plugin_slash + else + raise + end + end + else + @logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`") + require plugin_info["require"] + end + @logger.debug("Successfully loaded plugin `#{plugin_name}`.") + end + if defined?(::Bundler) + @logger.debug("Bundler detected in use. Loading `:plugins` group.") + ::Bundler.require(:plugins) + end + rescue ScriptError, StandardError => err + @logger.error("Plugin loading error: #{err.class} - #{err}") + err.backtrace.each do |backtrace_line| + @logger.debug(backtrace_line) + end + raise Vagrant::Errors::PluginLoadError, message: err.to_s + end + end end end end diff --git a/lib/vagrant/plugin/state_file.rb b/lib/vagrant/plugin/state_file.rb index 85db50b92..c8e118d4b 100644 --- a/lib/vagrant/plugin/state_file.rb +++ b/lib/vagrant/plugin/state_file.rb @@ -7,6 +7,10 @@ module Vagrant # 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 + + # @return [Pathname] path to file + attr_reader :path + def initialize(path) @path = path @@ -36,7 +40,8 @@ module Vagrant "gem_version" => opts[:version] || "", "require" => opts[:require] || "", "sources" => opts[:sources] || [], - "installed_gem_version" => opts[:installed_gem_version] + "installed_gem_version" => opts[:installed_gem_version], + "local" => opts[:local] } save! diff --git a/plugins/commands/plugin/action/expunge_plugins.rb b/plugins/commands/plugin/action/expunge_plugins.rb index 072ef81a6..bb011c544 100644 --- a/plugins/commands/plugin/action/expunge_plugins.rb +++ b/plugins/commands/plugin/action/expunge_plugins.rb @@ -42,17 +42,25 @@ module VagrantPlugins end if !abort_action - plugins_json = File.join(env[:home_path], "plugins.json") - plugins_gems = env[:gems_path] + files = [] + dirs = [] - if File.exist?(plugins_json) - FileUtils.rm(plugins_json) + # Do not include global paths if local only + if !env[:local] + files << Vagrant::Plugin::Manager.instance.user_file.path + dirs << Vagrant::Bundler.instance.plugin_gem_path end - if File.directory?(plugins_gems) - FileUtils.rm_rf(plugins_gems) + # Add local paths if they exist + if Vagrant::Plugin::Manager.instance.local_file + files << Vagrant::Plugin::Manager.instance.local_file.path + dirs << Vagrant::Bundler.instance.env_plugin_gem_path end + # Expunge files and directories + files.find_all(&:exist?).map(&:delete) + dirs.find_all(&:exist?).map(&:rmtree) + env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_complete")) @app.call(env) diff --git a/plugins/commands/plugin/action/install_gem.rb b/plugins/commands/plugin/action/install_gem.rb index 320f95e83..2612a4db4 100644 --- a/plugins/commands/plugin/action/install_gem.rb +++ b/plugins/commands/plugin/action/install_gem.rb @@ -18,6 +18,7 @@ module VagrantPlugins plugin_name = env[:plugin_name] sources = env[:plugin_sources] version = env[:plugin_version] + local = env[:plugin_local] # Install the gem plugin_name_label = plugin_name @@ -32,6 +33,7 @@ module VagrantPlugins require: entrypoint, sources: sources, verbose: !!env[:plugin_verbose], + local: local ) # Record it so we can uninstall if something goes wrong diff --git a/plugins/commands/plugin/action/list_plugins.rb b/plugins/commands/plugin/action/list_plugins.rb index f5f8d41bf..3eb8aeb00 100644 --- a/plugins/commands/plugin/action/list_plugins.rb +++ b/plugins/commands/plugin/action/list_plugins.rb @@ -35,13 +35,16 @@ module VagrantPlugins spec = specs[plugin_name] next if spec.nil? - system = "" - system = ", system" if plugin && plugin["system"] - env[:ui].info "#{spec.name} (#{spec.version}#{system})" + meta = ", global" + if plugin + meta = ", system" if plugin["system"] + meta = ", local" if plugin["local"] + end + env[:ui].info "#{spec.name} (#{spec.version}#{meta})" env[:ui].machine("plugin-name", spec.name) env[:ui].machine( "plugin-version", - "#{spec.version}#{system}", + "#{spec.version}#{meta}", target: spec.name) if plugin["gem_version"] && plugin["gem_version"] != "" diff --git a/plugins/commands/plugin/action/uninstall_plugin.rb b/plugins/commands/plugin/action/uninstall_plugin.rb index 0aa9b4a9a..8e988d1aa 100644 --- a/plugins/commands/plugin/action/uninstall_plugin.rb +++ b/plugins/commands/plugin/action/uninstall_plugin.rb @@ -15,7 +15,7 @@ module VagrantPlugins name: env[:plugin_name])) manager = Vagrant::Plugin::Manager.instance - manager.uninstall_plugin(env[:plugin_name]) + manager.uninstall_plugin(env[:plugin_name], local: env[:local]) @app.call(env) end diff --git a/plugins/commands/plugin/command/expunge.rb b/plugins/commands/plugin/command/expunge.rb index 6ec16ea9d..a59b68a7f 100644 --- a/plugins/commands/plugin/command/expunge.rb +++ b/plugins/commands/plugin/command/expunge.rb @@ -16,6 +16,10 @@ module VagrantPlugins options[:force] = force end + o.on("--local", "Remove local project plugins only") do |l| + options[:local] = l + end + o.on("--reinstall", "Reinstall current plugins after expunge") do |reinstall| options[:reinstall] = reinstall end diff --git a/plugins/commands/plugin/command/install.rb b/plugins/commands/plugin/command/install.rb index 441500387..6e9ae9428 100644 --- a/plugins/commands/plugin/command/install.rb +++ b/plugins/commands/plugin/command/install.rb @@ -9,6 +9,8 @@ module VagrantPlugins class Install < Base include MixinInstallOpts + LOCAL_INSTALL_PAUSE = 3 + def execute options = { verbose: false } @@ -17,6 +19,10 @@ module VagrantPlugins o.separator "" build_install_opts(o, options) + o.on("--local", "Install plugin for local project only") do |l| + options[:local] = l + end + o.on("--verbose", "Enable verbose output for plugin installation") do |v| options[:verbose] = v end @@ -25,17 +31,51 @@ module VagrantPlugins # Parse the options argv = parse_options(opts) return if !argv - raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 1 - # Install the gem - argv.each do |name| - action(Action.action_install, { - plugin_entry_point: options[:entry_point], - plugin_version: options[:plugin_version], - plugin_sources: options[:plugin_sources], - plugin_name: name, - plugin_verbose: options[:verbose] - }) + if argv.length < 1 + raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if !options[:local] + + errors = @env.vagrantfile.config.vagrant.validate(nil) + if !errors["vagrant"].empty? + raise Errors::ConfigInvalid, + errors: Util::TemplateRenderer.render( + "config/validation_failed", + errors: errors) + end + + local_plugins = @env.vagrantfile.config.vagrant.plugins + plugin_list = local_plugins.map do |name, info| + "#{name} (#{info.fetch(:version, "> 0")})" + end.join("\n") + + + @env.ui.info(I18n.t("vagrant.plugins.local.install_all", + plugins: plugin_list) + "\n") + + # Pause to allow user to cancel + sleep(LOCAL_INSTALL_PAUSE) + + local_plugins.each do |name, info| + action(Action.action_install, + plugin_entry_point: info[:entry_point], + plugin_version: info[:version], + plugin_sources: info[:sources] || Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup, + plugin_name: name, + plugin_local: true + ) + end + else + # Install the gem + argv.each do |name| + action(Action.action_install, + plugin_entry_point: options[:entry_point], + plugin_version: options[:plugin_version], + plugin_sources: options[:plugin_sources], + plugin_name: name, + plugin_verbose: options[:verbose], + plugin_local: options[:local] + ) + end end # Success, exit status 0 diff --git a/plugins/commands/plugin/command/uninstall.rb b/plugins/commands/plugin/command/uninstall.rb index ec032a5d9..57534358f 100644 --- a/plugins/commands/plugin/command/uninstall.rb +++ b/plugins/commands/plugin/command/uninstall.rb @@ -7,8 +7,13 @@ module VagrantPlugins module Command class Uninstall < Base def execute + options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin uninstall [ ...] [-h]" + + o.on("--local", "Remove plugin from local project") do |l| + options[:local] = l + end end # Parse the options @@ -18,7 +23,7 @@ module VagrantPlugins # Uninstall the gems argv.each do |gem| - action(Action.action_uninstall, plugin_name: gem) + action(Action.action_uninstall, plugin_name: gem, local: options[:local]) end # Success, exit status 0 diff --git a/plugins/kernel_v2/config/vagrant.rb b/plugins/kernel_v2/config/vagrant.rb index 951383db7..2e097d61b 100644 --- a/plugins/kernel_v2/config/vagrant.rb +++ b/plugins/kernel_v2/config/vagrant.rb @@ -5,16 +5,25 @@ module VagrantPlugins class VagrantConfig < Vagrant.plugin("2", :config) attr_accessor :host attr_accessor :sensitive + attr_accessor :plugins + + VALID_PLUGIN_KEYS = [:sources, :version, :entry_point].freeze def initialize @host = UNSET_VALUE @sensitive = UNSET_VALUE + @plugins = UNSET_VALUE end def finalize! @host = :detect if @host == UNSET_VALUE @host = @host.to_sym if @host @sensitive = nil if @sensitive == UNSET_VALUE + if @plugins == UNSET_VALUE + @plugins = {} + else + @plugins = format_plugins(@plugins) + end if @sensitive.is_a?(Array) || @sensitive.is_a?(String) Array(@sensitive).each do |value| @@ -23,18 +32,48 @@ module VagrantPlugins end end + # Validate the configuration + # + # @param [Vagrant::Machine, NilClass] machine Machine instance or nil + # @return [Hash] def validate(machine) errors = _detected_errors if @sensitive && (!@sensitive.is_a?(Array) && !@sensitive.is_a?(String)) errors << I18n.t("vagrant.config.root.sensitive_bad_type") end + + @plugins.each do |plugin_name, plugin_info| + invalid_keys = plugin_info.keys - VALID_PLUGIN_KEYS + if !invalid_keys.empty? + errors << I18n.t("vagrant.config.root.plugins_bad_key", + plugin_name: plugin_name, + plugin_key: invalid_keys.join(", ") + ) + end + end + {"vagrant" => errors} end def to_s "Vagrant" end + + def format_plugins(val) + result = case val + when String + {val => {}} + when Array + Hash[val.map{|item| [item.to_s, {}]}] + else + val + end + result.keys.each do |key| + result[key] = Hash[result[key].map{|k,v| [k.to_sym, v]}] + end + result + end end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 0fd99ccc8..6c28b16d8 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -410,6 +410,23 @@ en: Backup: %{backup_path} + plugins: + local: + uninstalled_plugins: |- + Vagrant has detected project local plugins configured for this + project which are not installed. + + %{plugins} + request_plugin_install: |- + Install local plugins (Y/N) + + install_all: |- + Vagrant will now install the following plugins to the local project + which have been defined in current Vagrantfile: + + %{plugins} + + Press ctrl-c to cancel... #------------------------------------------------------------------------------- # Translations for exception classes #------------------------------------------------------------------------------- @@ -1092,7 +1109,7 @@ en: %{message} plugin_not_installed: |- - The plugin '%{name}' is not installed. Please install it first. + The plugin '%{name}' is not currently installed. plugin_state_file_not_parsable: |- Failed to parse the state file "%{path}": %{message} @@ -1117,6 +1134,20 @@ en: %{error_msg} Source: %{source} + plugin_no_local_error: |- + Vagrant is not currently working within a Vagrant project directory. Local + plugins are only supported within a Vagrant project directory. + plugin_missing_local_error: |- + Vagrant is missing plugins required by the currently loaded Vagrantfile. + Please install the configured plugins and run this command again. The + following plugins are currently missing: + + %{plugins} + + To install the plugins configured in the current Vagrantfile run the + following command: + + vagrant plugin install --local powershell_not_found: |- Failed to locate the powershell executable on the available PATH. Please ensure powershell is installed and available on the local PATH, then @@ -1711,6 +1742,10 @@ en: sensitive_bad_type: |- Invalid type provided for `sensitive`. The sensitive option expects a string or an array of strings. + plugins_bad_key: |- + Invalid plugin configuration detected for `%{plugin_name}` plugin. + + Unknown keys: %{plugin_key} bad_key: |- Unknown configuration section '%{key}'. ssh: