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

env_plugin_gem_path[R]

@return [Pathname] Vagrant environment specific plugin path

plugin_gem_path[R]

@return [Pathname] Global plugin path

Public Class Methods

instance() click to toggle source
# File lib/vagrant/bundler.rb, line 31
def self.instance
  @bundler ||= self.new
end
new() click to toggle source
# File lib/vagrant/bundler.rb, line 40
def initialize
  @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze
  @logger = Log4r::Logger.new("vagrant::bundler")

  # TODO: Remove fix when https://github.com/rubygems/rubygems/pull/2735
  # gets merged and released
  #
  # Because of a rubygems bug, we need to set the gemrc file path
  # through this method rather than relying on the environment varible
  # GEMRC. On windows, that path gets split on `:`: and `;`, which means
  # the drive letter gets treated as its own path. If that path exists locally,
  # (like having a random folder called `c` where the library was invoked),
  # it fails thinking the folder `c` is a gemrc file.
  gemrc_val = ENV["GEMRC"]
  ENV["GEMRC"] = ""
  Gem.configuration = Gem::ConfigFile.new(["--config-file", gemrc_val])
  ENV["GEMRC"] = gemrc_val
end

Public Instance Methods

clean(plugins, **opts) click to toggle source

Clean removes any unused gems.

# File lib/vagrant/bundler.rb, line 176
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 133
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 63
def environment_path=(env_data_path)
  @env_plugin_gem_path = env_data_path.join("plugins", "gems", RUBY_VERSION).freeze
end
init!(plugins, repair=false) click to toggle source

Initializes Bundler and the various gem paths so that we can begin loading gems.

# File lib/vagrant/bundler.rb, line 69
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
  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

  repair_result = nil
  begin
    # Compose set for resolution
    composed_set = generate_vagrant_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
      @logger.debug("Resolution failed but attempting to repair. Failure: #{failure}")
      install(plugins)
      @init_retried = true
      retry
    else
      raise
    end
  end

  # Activate the gems
  activate_solution(solution)

  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 142
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 150
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
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 169
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 261
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 505
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?
          activation_request = desired_activation_request
          @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 468
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 485
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 412
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 274
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 loose 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 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)
  )
  @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 |activation_req|
    rs_spec = activation_req.spec
    if vagrant_internal_specs.detect{|ispec| ispec.name == rs_spec.name && ispec.version == rs_spec.version }
      @logger.debug("Removing activation request from install. Already installed. (#{rs_spec.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?,
    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
vagrant_internal_specs() click to toggle source

@return [Array<>] spec list

# File lib/vagrant/bundler.rb, line 421
def vagrant_internal_specs
  # activate any dependencies up front so we can always
  # pin them when resolving
  if (vs = Gem::Specification.find { |s| s.name == "vagrant" && s.activated? })
    vs.runtime_dependencies.each { |d| gem d.name, *d.requirement.as_list }
  end
  # discover all the gems we have available
  list = {}
  directories = [Gem::Specification.default_specifications_dir]
  Gem::Specification.find_all{true}.each do |spec|
    list[spec.full_name] = spec
  end
  if(!defined?(::Bundler))
    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 448
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