class Vagrant::Bundler
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.
Constants
- DEFAULT_GEM_SOURCES
Default gem repositories
- HASHICORP_GEMSTORE
Location of HashiCorp gem repository
Attributes
@return [Array<Gem::Specification>, nil] List of builtin specs
@return [Pathname] Vagrant
environment specific plugin path
@return [Pathname] Vagrant
environment data path
@return [Pathname] Global plugin path
@return [Pathname] Global plugin solution set path
Public Class Methods
# File lib/vagrant/bundler.rb, line 180 def self.instance @bundler ||= self.new end
# File lib/vagrant/bundler.rb, line 195 def initialize @builtin_specs = [] @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze @logger = Log4r::Logger.new("vagrant::bundler") end
Public Instance Methods
Clean removes any unused gems.
# File lib/vagrant/bundler.rb, line 388 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| gem_version = info['installed_gem_version'] gem_version = info['gem_version'] if gem_version.to_s.empty? gem_version = "> 0" if gem_version.to_s.empty? Gem::Dependency.new(name, gem_version) end @logger.debug("Current plugin dependency list: #{plugin_deps}") # 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 = generate_vagrant_set # 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 # 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 solution = request_set.resolve(current_set) solution_specs = solution.map(&:full_spec) solution_full_names = solution_specs.map(&:full_name) # Find all specs installed to plugins directory that are not # 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[:env_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 plugin_specs.each do |spec| @logger.debug("Uninstalling gem - #{spec.full_name}") Gem::Uninstaller.new(spec.name, version: spec.version, install_dir: plugin_gem_path, all: true, executables: true, force: true, ignore: true, ).uninstall_gem(spec) end solution.find_all do |spec| plugins.keys.include?(spec.name) end end
Removes any temporary files created by init
# File lib/vagrant/bundler.rb, line 345 def deinit # no-op 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
# File lib/vagrant/bundler.rb, line 205 def environment_path=(env_data_path) if !env_data_path.is_a?(Pathname) raise TypeError, "Expected `Pathname` but received `#{env_data_path.class}`" end @env_plugin_gem_path = env_data_path.join("plugins", "gems", RUBY_VERSION).freeze @environment_data_path = env_data_path end
Initializes Bundler
and the various gem paths so that we can begin loading gems.
# File lib/vagrant/bundler.rb, line 238 def init!(plugins, repair=false, **opts) if !@initial_specifications @initial_specifications = Gem::Specification.find_all{true} else Gem::Specification.all = @initial_specifications Gem::Specification.reset end solution_file = load_solution_file(opts) @logger.debug("solution file in use for init: #{solution_file}") solution = nil composed_set = generate_vagrant_set # Force the composed set to allow prereleases if Vagrant.allow_prerelease_dependencies? @logger.debug("enabling prerelease dependency matching due to user request") composed_set.prerelease = true end if solution_file&.valid? @logger.debug("loading cached solution set") solution = solution_file.dependency_list.map do |dep| spec = composed_set.find_all(dep).select do |dep_spec| next(true) unless Gem.loaded_specs.has_key?(dep_spec.name) Gem.loaded_specs[dep_spec.name].version.eql?(dep_spec.version) end.first if !spec @logger.warn("failed to locate specification for dependency - #{dep}") @logger.warn("invalidating solution file - #{solution_file}") solution_file.invalidate! break end dep_r = Gem::Resolver::DependencyRequest.new(dep, nil) Gem::Resolver::ActivationRequest.new(spec, dep_r) end end if !solution_file&.valid? @logger.debug("generating solution set for configured plugins") # Add HashiCorp RubyGems source if !Gem.sources.include?(HASHICORP_GEMSTORE) sources = [HASHICORP_GEMSTORE] + Gem.sources.sources Gem.sources.replace(sources) end # Generate dependencies for all registered plugins plugin_deps = plugins.map do |name, info| 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}") # 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 begin @logger.debug("resolving solution from available specification set") # Resolve the request set to ensure proper activation order solution = request_set.resolve(composed_set) @logger.debug("solution set for configured plugins has been resolved") rescue Gem::UnsatisfiableDependencyError => failure if repair raise failure if @init_retried @logger.debug("Resolution failed but attempting to repair. Failure: #{failure}") install(plugins) @init_retried = true retry else raise end end end # Activate the gems @logger.debug("activating solution set") activate_solution(solution) if solution_file && !solution_file.valid? solution_file.dependency_list = solution.map do |activation| activation.request.dependency end solution_file.store! @logger.debug("solution set stored to - #{solution_file}") end full_vagrant_spec_list = @initial_specifications + solution.map(&:full_spec) if(defined?(::Bundler)) @logger.debug("Updating Bundler with full specification list") ::Bundler.rubygems.replace_entrypoints(full_vagrant_spec_list) end Gem.post_reset do Gem::Specification.all = full_vagrant_spec_list end Gem::Specification.reset nil end
Installs the list of plugins.
@param [Hash] plugins @param [Boolean] env_local Environment
local plugin install @return [Array<Gem::Specification>]
# File lib/vagrant/bundler.rb, line 354 def install(plugins, env_local=false) internal_install(plugins, nil, env_local: env_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]
# File lib/vagrant/bundler.rb, line 362 def install_local(path, opts={}) plugin_source = Gem::Source::SpecificFile.new(path) plugin_info = { plugin_source.spec.name => { "gem_version" => plugin_source.spec.version.to_s, "local_source" => plugin_source, "sources" => opts.fetch(:sources, []) } } @logger.debug("Installing local plugin - #{plugin_info}") internal_install(plugin_info, nil, env_local: opts[:env_local]) plugin_source.spec end
Use the given options to create a solution file instance for use during initialization. When a Vagrant
environment is in use, solution files will be stored within the environment’s data directory. This is because the solution for loading global plugins is dependent on any solution generated for local plugins. When no Vagrant
environment is in use (running Vagrant
without a Vagrantfile
), the Vagrant
user data path will be used for solution storage since only the global plugins will be used.
@param [Hash] opts Options passed to init!
@return [SolutionFile]
# File lib/vagrant/bundler.rb, line 224 def load_solution_file(opts={}) return if !opts[:local] && !opts[:global] return if opts[:local] && opts[:global] return if opts[:local] && environment_data_path.nil? solution_path = (environment_data_path || Vagrant.user_data_path) + "bundler" solution_path += opts[:local] ? "local.sol" : "global.sol" SolutionFile.new( plugin_file: opts[:local] || opts[:global], solution_file: solution_path ) 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.
# File lib/vagrant/bundler.rb, line 381 def update(plugins, specific, **opts) specific ||= [] update = opts.merge({gems: specific.empty? ? true : specific}) internal_install(plugins, update) end
During the duration of the yielded block, Bundler
loud output is enabled.
# File lib/vagrant/bundler.rb, line 473 def verbose if block_given? initial_state = @verbose @verbose = true yield @verbose = initial_state else @verbose = true end end
Protected Instance Methods
Activate a given solution
# File lib/vagrant/bundler.rb, line 749 def activate_solution(solution) retried = false begin @logger.debug("Activating solution set: #{solution.map(&:full_name)}") solution.each do |activation_request| unless activation_request.full_spec.activated? @logger.debug("Activating gem #{activation_request.full_spec.full_name}") activation_request.full_spec.activate if(defined?(::Bundler)) @logger.debug("Marking gem #{activation_request.full_spec.full_name} loaded within Bundler.") ::Bundler.rubygems.mark_loaded activation_request.full_spec end end end rescue Gem::LoadError => e # 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 @logger.error("Failed to load solution set - #{e.class}: #{e}") matcher = e.message.match(/Could not find '(?<gem_name>[^']+)'/) if matcher && !matcher["gem_name"].empty? desired_activation_request = solution.detect do |request| request.name == matcher["gem_name"] end if desired_activation_request && !desired_activation_request.full_spec.activated? @logger.warn("Found misordered activation request for #{desired_activation_request.full_name}. Moving to solution HEAD.") solution.delete(desired_activation_request) solution.unshift(desired_activation_request) retry end end raise else @logger.debug("Failed to load solution set. Retrying with reverse order.") retried = true solution.reverse! retry end end end
Generate the builtin resolver set
# File lib/vagrant/bundler.rb, line 712 def generate_builtin_set(system_plugins=[]) builtin_set = BuiltinSet.new @logger.debug("Generating new builtin set instance.") vagrant_internal_specs.each do |spec| if !system_plugins.include?(spec.name) builtin_set.add_builtin_spec(spec) end end builtin_set end
Generate the plugin resolver set. Optionally provide specification names (short or full) that should be ignored
@param [Pathname] path to plugins @param [Array<String>] gems to skip @return [PluginSet]
# File lib/vagrant/bundler.rb, line 729 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_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 next if skip.include?(spec.name) || skip.include?(spec.full_name) plugin_set.add_vendor_gem(spec.name, spec.gem_dir) end plugin_set end
Generate the composite resolver set totally all of vagrant (builtin + plugin set)
# File lib/vagrant/bundler.rb, line 637 def generate_vagrant_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
# File lib/vagrant/bundler.rb, line 486 def internal_install(plugins, update, **extra) update = {} if !update.is_a?(Hash) skips = [] source_list = {} system_plugins = plugins.map do |plugin_name, plugin_info| plugin_name if plugin_info["system"] end.compact installer_set = VagrantSet.new(:both) installer_set.system_plugins = system_plugins # Generate all required plugin deps plugin_deps = plugins.map do |name, info| gem_version = info['gem_version'].to_s.empty? ? '> 0' : info['gem_version'] if update[:gems] == true || (update[:gems].respond_to?(:include?) && update[:gems].include?(name)) if Gem::Requirement.new(gem_version).exact? gem_version = "> 0" @logger.debug("Detected exact version match for `#{name}` plugin update. Reset to loosen constraint #{gem_version.inspect}.") end skips << name end source_list[name] ||= [] if plugin_source = info.delete("local_source") installer_set.add_local(plugin_source.spec.name, plugin_source.spec, plugin_source) source_list[name] << plugin_source.path end Array(info["sources"]).each do |source| if !source.end_with?("/") source = source + "/" end source_list[name] << source end Gem::Dependency.new(name, *gem_version.split(",")) end if Vagrant.strict_dependency_enforcement @logger.debug("Enabling strict dependency enforcement") plugin_deps += vagrant_internal_specs.map do |spec| next if system_plugins.include?(spec.name) # If this spec is for a default plugin included in # the ruby stdlib, ignore it next if spec.default_gem? # If we are not running within the installer and # we are not within a bundler environment then we # only want activated specs if !Vagrant.in_installer? && !Vagrant.in_bundler? next if !spec.activated? end Gem::Dependency.new(spec.name, spec.version) end.compact else @logger.debug("Disabling strict dependency enforcement") end @logger.debug("Dependency list for installation:\n - " \ "#{plugin_deps.map{|d| "#{d.name} #{d.requirement}"}.join("\n - ")}") all_sources = source_list.values.flatten.uniq default_sources = DEFAULT_GEM_SOURCES & all_sources all_sources -= DEFAULT_GEM_SOURCES # Only allow defined Gem sources Gem.sources.clear @logger.debug("Enabling user defined remote RubyGems sources") all_sources.each do |src| begin next if File.file?(src) || URI.parse(src).scheme.nil? rescue URI::InvalidURIError next end @logger.debug("Adding RubyGems source #{src}") Gem.sources << src end @logger.debug("Enabling default remote RubyGems sources") default_sources.each do |src| @logger.debug("Adding source - #{src}") Gem.sources << src end validate_configured_sources! source_list.values.each{|srcs| srcs.delete_if{|src| default_sources.include?(src)}} installer_set.prefer_sources = source_list @logger.debug("Current source list for install: #{Gem.sources.to_a}") # Create the request set for the new plugins request_set = Gem::RequestSet.new(*plugin_deps) installer_set = Gem::Resolver.compose_sets( installer_set, generate_builtin_set(system_plugins), generate_plugin_set(skips) ) if Vagrant.allow_prerelease_dependencies? @logger.debug("enabling prerelease dependency matching based on user request") request_set.prerelease = true installer_set.prerelease = true end @logger.debug("Generating solution set for installation.") # Generate the required solution set for new plugins solution = request_set.resolve(installer_set) activate_solution(solution) # Remove gems which are already installed request_set.sorted_requests.delete_if do |act_req| rs = act_req.spec if vagrant_internal_specs.detect{ |i| i.name == rs.name && i.version == rs.version } @logger.debug("Removing activation request from install. Already installed. (#{rs.spec.full_name})") true end end @logger.debug("Installing required gems.") # 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 satisfied by gems in the install directory (which will likely not # be true) install_path = extra[:env_local] ? env_plugin_gem_path : plugin_gem_path result = request_set.install_into(install_path.to_s, true, ignore_dependencies: true, prerelease: Vagrant.prerelease? || Vagrant.allow_prerelease_dependencies?, wrappers: true, document: [] ) 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
@return [Array<>] spec list
# File lib/vagrant/bundler.rb, line 646 def vagrant_internal_specs # activate any dependencies up front so we can always # pin them when resolving self_spec = Gem::Specification.find { |s| s.name == "vagrant" && s.activated? } if !self_spec @logger.warn("Failed to locate activated vagrant specification. Activating...") self_spec = Gem::Specification.find { |s| s.name == "vagrant" } if !self_spec @logger.error("Failed to locate Vagrant RubyGem specification") raise Vagrant::Errors::SourceSpecNotFound end self_spec.activate @logger.info("Activated vagrant specification version - #{self_spec.version}") end # discover all the gems we have available list = {} if Gem.respond_to?(:default_specifications_dir) spec_dir = Gem.default_specifications_dir else spec_dir = Gem::Specification.default_specifications_dir end directories = [spec_dir] if Vagrant.in_bundler? Gem::Specification.find_all{true}.each do |spec| list[spec.full_name] = spec end else builtin_specs.each do |spec| list[spec.full_name] = spec end end if Vagrant.in_installer? directories += Gem::Specification.dirs.find_all do |path| !path.start_with?(Gem.user_dir) end end Gem::Specification.each_spec(directories) do |spec| if !list[spec.full_name] list[spec.full_name] = spec end end list.values end
Iterates each configured RubyGem source to validate that it is properly available. If source is unavailable an exception is raised.
# File lib/vagrant/bundler.rb, line 692 def validate_configured_sources! Gem.sources.each_source do |src| begin src.load_specs(:released) rescue Gem::Exception => source_error if ENV["VAGRANT_ALLOW_PLUGIN_SOURCE_ERRORS"] @logger.warn("Failed to load configured plugin source: #{src}!") @logger.warn("Error received attempting to load source (#{src}): #{source_error}") @logger.warn("Ignoring plugin source load failure due user request via env variable") else @logger.error("Failed to load configured plugin source `#{src}`: #{source_error}") raise Vagrant::Errors::PluginSourceError, source: src.uri.to_s, error_msg: source_error.message end end end end