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.



198
199
200
201
202
# File 'lib/vagrant/bundler.rb', line 198

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:



196
197
198
# File 'lib/vagrant/bundler.rb', line 196

def builtin_specs
  @builtin_specs
end

#env_plugin_gem_pathPathname (readonly)

Returns Vagrant environment specific plugin path.

Returns:

  • (Pathname)

    Vagrant environment specific plugin path



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

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



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

def environment_data_path
  @environment_data_path
end

#plugin_gem_pathPathname (readonly)

Returns Global plugin path.

Returns:

  • (Pathname)

    Global plugin path



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

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



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

def plugin_solution_path
  @plugin_solution_path
end

Class Method Details

.instanceObject



183
184
185
# File 'lib/vagrant/bundler.rb', line 183

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

Instance Method Details

#clean(plugins, **opts) ⇒ Object

Clean removes any unused gems.



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
465
466
467
468
469
470
471
472
# File 'lib/vagrant/bundler.rb', line 391

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



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

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



208
209
210
211
212
213
214
# File 'lib/vagrant/bundler.rb', line 208

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.



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
338
339
340
341
342
343
344
345
# File 'lib/vagrant/bundler.rb', line 241

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) ⇒ Array<Gem::Specification>

Installs the list of plugins.

Parameters:

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

    Environment local plugin install

Returns:



357
358
359
# File 'lib/vagrant/bundler.rb', line 357

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:



365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/vagrant/bundler.rb', line 365

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:



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/vagrant/bundler.rb', line 227

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.



384
385
386
387
388
# File 'lib/vagrant/bundler.rb', line 384

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.



476
477
478
479
480
481
482
483
484
485
# File 'lib/vagrant/bundler.rb', line 476

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