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

builtin_specs[RW]

@return [Array<Gem::Specification>, nil] List of builtin specs

env_plugin_gem_path[R]

@return [Pathname] Vagrant environment specific plugin path

environment_data_path[R]

@return [Pathname] Vagrant environment data path

plugin_gem_path[R]

@return [Pathname] Global plugin path

plugin_solution_path[R]

@return [Pathname] Global plugin solution set path

Public Class Methods

instance() click to toggle source
# File lib/vagrant/bundler.rb, line 180
def self.instance
  @bundler ||= self.new
end
new() click to toggle source
# 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(plugins, **opts) click to toggle source

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
deinit() click to toggle source

Removes any temporary files created by init

# File lib/vagrant/bundler.rb, line 345
def deinit
  # no-op
end
environment_path=(env_data_path) click to toggle source

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
init!(plugins, repair=false, **opts) click to toggle source

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
install(plugins, env_local=false) click to toggle source

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
install_local(path, opts={}) click to toggle source

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
load_solution_file(opts={}) click to toggle source

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(plugins, specific, **opts) click to toggle source

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
verbose() { || ... } click to toggle source

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_solution(solution) click to toggle source

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_builtin_set(system_plugins=[]) click to toggle source

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_plugin_set(*args) click to toggle source

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_vagrant_set() click to toggle source

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
internal_install(plugins, update, **extra) click to toggle source
# 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
vagrant_internal_specs() click to toggle source

@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
validate_configured_sources!() click to toggle source

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