Class: Vagrant::Bundler

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant/bundler.rb

Overview

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.

Defined Under Namespace

Classes: BuiltinSet, PluginSet, SolutionFile, VagrantSet

Constant Summary collapse

HASHICORP_GEMSTORE =

Location of HashiCorp gem repository

"https://gems.hashicorp.com/".freeze
DEFAULT_GEM_SOURCES =

Default gem repositories

[
  HASHICORP_GEMSTORE,
  "https://rubygems.org/".freeze
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeBundler

Returns a new instance of Bundler.


195
196
197
198
199
# 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

Instance Attribute Details

#builtin_specsArray<Gem::Specification>?

Returns List of builtin specs.

Returns:


193
194
195
# File 'lib/vagrant/bundler.rb', line 193

def builtin_specs
  @builtin_specs
end

#env_plugin_gem_pathPathname (readonly)

Returns Vagrant environment specific plugin path.

Returns:

  • (Pathname)

    Vagrant environment specific plugin path


189
190
191
# File 'lib/vagrant/bundler.rb', line 189

def env_plugin_gem_path
  @env_plugin_gem_path
end

#environment_data_pathPathname (readonly)

Returns Vagrant environment data path.

Returns:

  • (Pathname)

    Vagrant environment data path


191
192
193
# File 'lib/vagrant/bundler.rb', line 191

def environment_data_path
  @environment_data_path
end

#plugin_gem_pathPathname (readonly)

Returns Global plugin path.

Returns:

  • (Pathname)

    Global plugin path


185
186
187
# File 'lib/vagrant/bundler.rb', line 185

def plugin_gem_path
  @plugin_gem_path
end

#plugin_solution_pathPathname (readonly)

Returns Global plugin solution set path.

Returns:

  • (Pathname)

    Global plugin solution set path


187
188
189
# File 'lib/vagrant/bundler.rb', line 187

def plugin_solution_path
  @plugin_solution_path
end

Class Method Details

.instanceObject


180
181
182
# File 'lib/vagrant/bundler.rb', line 180

def self.instance
  @bundler ||= self.new
end

Instance Method Details

#clean(plugins, **opts) ⇒ Object

Clean removes any unused gems.


383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/vagrant/bundler.rb', line 383

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

#deinitObject

Removes any temporary files created by init


340
341
342
# File 'lib/vagrant/bundler.rb', line 340

def deinit
  # no-op
end

#environment_path=(env_data_path) ⇒ Pathname

Enable Vagrant environment specific plugins at given data path

Parameters:

  • Path (Pathname)

    to Vagrant::Environment data directory

Returns:

  • (Pathname)

    Path to environment specific gem directory


205
206
207
208
209
210
211
# 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) ⇒ Object

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


238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# 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).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) ⇒ Array<Gem::Specification>

Installs the list of plugins.

Parameters:

  • plugins (Hash)
  • env_local (Boolean) (defaults to: false)

    Environment local plugin install

Returns:


349
350
351
# File 'lib/vagrant/bundler.rb', line 349

def install(plugins, env_local=false)
  internal_install(plugins, nil, env_local: env_local)
end

#install_local(path, opts = {}) ⇒ Gem::Specification

Installs a local '*.gem' file so that Bundler can find it.

Parameters:

  • path (String)

    Path to a local gem file.

Returns:


357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/vagrant/bundler.rb', line 357

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 = {}) ⇒ SolutionFile

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.

Parameters:

  • opts (Hash) (defaults to: {})

    Options passed to #init!

Returns:


224
225
226
227
228
229
230
231
232
233
234
# 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) ⇒ Object

Update updates the given plugins, or every plugin if none is given.

Parameters:

  • plugins (Hash)
  • specific (Array<String>)

    Specific plugin names to update. If empty or nil, all plugins will be updated.


376
377
378
379
380
# File 'lib/vagrant/bundler.rb', line 376

def update(plugins, specific, **opts)
  specific ||= []
  update = opts.merge({gems: specific.empty? ? true : specific})
  internal_install(plugins, update)
end

#verboseObject

During the duration of the yielded block, Bundler loud output is enabled.


468
469
470
471
472
473
474
475
476
477
# File 'lib/vagrant/bundler.rb', line 468

def verbose
  if block_given?
    initial_state = @verbose
    @verbose = true
    yield
    @verbose = initial_state
  else
    @verbose = true
  end
end