"Fossies" - the Fresh Open Source Software Archive 
Member "vagrant-2.2.14/lib/vagrant/bundler.rb" (20 Nov 2020, 33318 Bytes) of package /linux/misc/vagrant-2.2.14.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
See also the latest
Fossies "Diffs" side-by-side code changes report for "bundler.rb":
2.2.13_vs_2.2.14.
1 require "monitor"
2 require "pathname"
3 require "set"
4 require "tempfile"
5 require "fileutils"
6 require "uri"
7
8 require "rubygems/package"
9 require "rubygems/uninstaller"
10 require "rubygems/name_tuple"
11
12 require_relative "shared_helpers"
13 require_relative "version"
14 require_relative "util/safe_env"
15
16 module Vagrant
17 # This class manages Vagrant's interaction with Bundler. Vagrant uses
18 # Bundler as a way to properly resolve all dependencies of Vagrant and
19 # all Vagrant-installed plugins.
20 class Bundler
21 class SolutionFile
22 # @return [Pathname] path to plugin file
23 attr_reader :plugin_file
24 # @return [Pathname] path to solution file
25 attr_reader :solution_file
26 # @return [Array<Gem::Resolver::DependencyRequest>] list of required dependencies
27 attr_reader :dependency_list
28
29 # @param [Pathname] plugin_file Path to plugin file
30 # @param [Pathname] solution_file Custom path to solution file
31 def initialize(plugin_file:, solution_file: nil)
32 @logger = Log4r::Logger.new("vagrant::bundler::solution_file")
33 @plugin_file = Pathname.new(plugin_file.to_s)
34 if solution_file
35 @solution_file = Pathname.new(solution_file.to_s)
36 else
37 @solution_file = Pathname.new(@plugin_file.to_s + ".sol")
38 end
39 @valid = false
40 @dependency_list = [].freeze
41 @logger.debug("new solution file instance plugin_file=#{plugin_file} " \
42 "solution_file=#{solution_file}")
43 load
44 end
45
46 # Set the list of dependencies for this solution
47 #
48 # @param [Array<Gem::Dependency>] dependency_list List of dependencies for the solution
49 # @return [Array<Gem::Resolver::DependencyRequest>]
50 def dependency_list=(dependency_list)
51 Array(dependency_list).each do |d|
52 if !d.is_a?(Gem::Dependency)
53 raise TypeError, "Expected `Gem::Dependency` but received `#{d.class}`"
54 end
55 end
56 @dependency_list = dependency_list.map do |d|
57 Gem::Resolver::DependencyRequest.new(d, nil).freeze
58 end.freeze
59 end
60
61 # @return [Boolean] contained solution is valid
62 def valid?
63 @valid
64 end
65
66 # @return [FalseClass] invalidate this solution file
67 def invalidate!
68 @valid = false
69 @logger.debug("manually invalidating solution file #{self}")
70 @valid
71 end
72
73 # Delete the solution file
74 #
75 # @return [Boolean] true if file was deleted
76 def delete!
77 if !solution_file.exist?
78 @logger.debug("solution file does not exist. nothing to delete.")
79 return false
80 end
81 @logger.debug("deleting solution file - #{solution_file}")
82 solution_file.delete
83 true
84 end
85
86 # Store the solution file
87 def store!
88 if !plugin_file.exist?
89 @logger.debug("plugin file does not exist, not storing solution")
90 return
91 end
92 if !solution_file.dirname.exist?
93 @logger.debug("creating directory for solution file: #{solution_file.dirname}")
94 solution_file.dirname.mkpath
95 end
96 @logger.debug("writing solution file contents to disk")
97 solution_file.write({
98 dependencies: dependency_list.map { |d|
99 [d.dependency.name, d.dependency.requirements_list]
100 },
101 checksum: plugin_file_checksum,
102 vagrant_version: Vagrant::VERSION
103 }.to_json)
104 @valid = true
105 end
106
107 def to_s # :nodoc:
108 "<Vagrant::Bundler::SolutionFile:#{plugin_file}:" \
109 "#{solution_file}:#{valid? ? "valid" : "invalid"}>"
110 end
111
112 protected
113
114 # Load the solution file for the plugin path provided
115 # if it exists. Validate solution is still applicable
116 # before injecting dependencies.
117 def load
118 if !plugin_file.exist? || !solution_file.exist?
119 @logger.debug("missing file so skipping loading")
120 return
121 end
122 solution = read_solution || return
123 return if !valid_solution?(
124 checksum: solution[:checksum],
125 version: solution[:vagrant_version]
126 )
127 @logger.debug("loading solution dependency list")
128 @dependency_list = Array(solution[:dependencies]).map do |name, requirements|
129 gd = Gem::Dependency.new(name, requirements)
130 Gem::Resolver::DependencyRequest.new(gd, nil).freeze
131 end.freeze
132 @logger.debug("solution dependency list: #{dependency_list}")
133 @valid = true
134 end
135
136 # Validate the given checksum matches the plugin file
137 # checksum
138 #
139 # @param [String] checksum Checksum value to validate
140 # @return [Boolean]
141 def valid_solution?(checksum:, version:)
142 file_checksum = plugin_file_checksum
143 @logger.debug("solution validation check CHECKSUM #{file_checksum} <-> #{checksum}" \
144 " VERSION #{Vagrant::VERSION} <-> #{version}")
145 plugin_file_checksum == checksum &&
146 Vagrant::VERSION == version
147 end
148
149 # @return [String] checksum of plugin file
150 def plugin_file_checksum
151 digest = Digest::SHA256.new
152 digest.file(plugin_file.to_s)
153 digest.hexdigest
154 end
155
156 # Read contents of solution file and parse
157 #
158 # @return [Hash]
159 def read_solution
160 @logger.debug("reading solution file - #{solution_file}")
161 begin
162 hash = JSON.load(solution_file.read)
163 Vagrant::Util::HashWithIndifferentAccess.new(hash)
164 rescue => err
165 @logger.warn("failed to load solution file, ignoring (error: #{err})")
166 nil
167 end
168 end
169 end
170
171 # Location of HashiCorp gem repository
172 HASHICORP_GEMSTORE = "https://gems.hashicorp.com/".freeze
173
174 # Default gem repositories
175 DEFAULT_GEM_SOURCES = [
176 HASHICORP_GEMSTORE,
177 "https://rubygems.org/".freeze
178 ].freeze
179
180 def self.instance
181 @bundler ||= self.new
182 end
183
184 # @return [Pathname] Global plugin path
185 attr_reader :plugin_gem_path
186 # @return [Pathname] Global plugin solution set path
187 attr_reader :plugin_solution_path
188 # @return [Pathname] Vagrant environment specific plugin path
189 attr_reader :env_plugin_gem_path
190 # @return [Pathname] Vagrant environment data path
191 attr_reader :environment_data_path
192
193 def initialize
194 @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze
195 @logger = Log4r::Logger.new("vagrant::bundler")
196 end
197
198 # Enable Vagrant environment specific plugins at given data path
199 #
200 # @param [Pathname] Path to Vagrant::Environment data directory
201 # @return [Pathname] Path to environment specific gem directory
202 def environment_path=(env_data_path)
203 if !env_data_path.is_a?(Pathname)
204 raise TypeError, "Expected `Pathname` but received `#{env_data_path.class}`"
205 end
206 @env_plugin_gem_path = env_data_path.join("plugins", "gems", RUBY_VERSION).freeze
207 @environment_data_path = env_data_path
208 end
209
210 # Use the given options to create a solution file instance
211 # for use during initialization. When a Vagrant environment
212 # is in use, solution files will be stored within the environment's
213 # data directory. This is because the solution for loading global
214 # plugins is dependent on any solution generated for local plugins.
215 # When no Vagrant environment is in use (running Vagrant without a
216 # Vagrantfile), the Vagrant user data path will be used for solution
217 # storage since only the global plugins will be used.
218 #
219 # @param [Hash] opts Options passed to #init!
220 # @return [SolutionFile]
221 def load_solution_file(opts={})
222 return if !opts[:local] && !opts[:global]
223 return if opts[:local] && opts[:global]
224 return if opts[:local] && environment_data_path.nil?
225 solution_path = (environment_data_path || Vagrant.user_data_path) + "bundler"
226 solution_path += opts[:local] ? "local.sol" : "global.sol"
227 SolutionFile.new(
228 plugin_file: opts[:local] || opts[:global],
229 solution_file: solution_path
230 )
231 end
232
233 # Initializes Bundler and the various gem paths so that we can begin
234 # loading gems.
235 def init!(plugins, repair=false, **opts)
236 if !@initial_specifications
237 @initial_specifications = Gem::Specification.find_all{true}
238 else
239 Gem::Specification.all = @initial_specifications
240 Gem::Specification.reset
241 end
242
243 solution_file = load_solution_file(opts)
244 @logger.debug("solution file in use for init: #{solution_file}")
245
246 solution = nil
247 composed_set = generate_vagrant_set
248
249 # Force the composed set to allow prereleases
250 if Vagrant.allow_prerelease_dependencies?
251 @logger.debug("enabling prerelease dependency matching due to user request")
252 composed_set.prerelease = true
253 end
254
255 if solution_file&.valid?
256 @logger.debug("loading cached solution set")
257 solution = solution_file.dependency_list.map do |dep|
258 spec = composed_set.find_all(dep).first
259 if !spec
260 @logger.warn("failed to locate specification for dependency - #{dep}")
261 @logger.warn("invalidating solution file - #{solution_file}")
262 solution_file.invalidate!
263 break
264 end
265 dep_r = Gem::Resolver::DependencyRequest.new(dep, nil)
266 Gem::Resolver::ActivationRequest.new(spec, dep_r)
267 end
268 end
269
270 if !solution_file&.valid?
271 @logger.debug("generating solution set for configured plugins")
272 # Add HashiCorp RubyGems source
273 if !Gem.sources.include?(HASHICORP_GEMSTORE)
274 sources = [HASHICORP_GEMSTORE] + Gem.sources.sources
275 Gem.sources.replace(sources)
276 end
277
278 # Generate dependencies for all registered plugins
279 plugin_deps = plugins.map do |name, info|
280 Gem::Dependency.new(name, info['installed_gem_version'].to_s.empty? ? '> 0' : info['installed_gem_version'])
281 end
282
283 @logger.debug("Current generated plugin dependency list: #{plugin_deps}")
284
285 # Load dependencies into a request set for resolution
286 request_set = Gem::RequestSet.new(*plugin_deps)
287 # Never allow dependencies to be remotely satisfied during init
288 request_set.remote = false
289
290 repair_result = nil
291 begin
292 @logger.debug("resolving solution from available specification set")
293 # Resolve the request set to ensure proper activation order
294 solution = request_set.resolve(composed_set)
295 @logger.debug("solution set for configured plugins has been resolved")
296 rescue Gem::UnsatisfiableDependencyError => failure
297 if repair
298 raise failure if @init_retried
299 @logger.debug("Resolution failed but attempting to repair. Failure: #{failure}")
300 install(plugins)
301 @init_retried = true
302 retry
303 else
304 raise
305 end
306 end
307 end
308
309 # Activate the gems
310 @logger.debug("activating solution set")
311 activate_solution(solution)
312
313 if solution_file && !solution_file.valid?
314 solution_file.dependency_list = solution.map do |activation|
315 activation.request.dependency
316 end
317 solution_file.store!
318 @logger.debug("solution set stored to - #{solution_file}")
319 end
320
321 full_vagrant_spec_list = @initial_specifications +
322 solution.map(&:full_spec)
323
324 if(defined?(::Bundler))
325 @logger.debug("Updating Bundler with full specification list")
326 ::Bundler.rubygems.replace_entrypoints(full_vagrant_spec_list)
327 end
328
329 Gem.post_reset do
330 Gem::Specification.all = full_vagrant_spec_list
331 end
332
333 Gem::Specification.reset
334 nil
335 end
336
337 # Removes any temporary files created by init
338 def deinit
339 # no-op
340 end
341
342 # Installs the list of plugins.
343 #
344 # @param [Hash] plugins
345 # @param [Boolean] env_local Environment local plugin install
346 # @return [Array<Gem::Specification>]
347 def install(plugins, env_local=false)
348 internal_install(plugins, nil, env_local: env_local)
349 end
350
351 # Installs a local '*.gem' file so that Bundler can find it.
352 #
353 # @param [String] path Path to a local gem file.
354 # @return [Gem::Specification]
355 def install_local(path, opts={})
356 plugin_source = Gem::Source::SpecificFile.new(path)
357 plugin_info = {
358 plugin_source.spec.name => {
359 "gem_version" => plugin_source.spec.version.to_s,
360 "local_source" => plugin_source,
361 "sources" => opts.fetch(:sources, [])
362 }
363 }
364 @logger.debug("Installing local plugin - #{plugin_info}")
365 internal_install(plugin_info, nil, env_local: opts[:env_local])
366 plugin_source.spec
367 end
368
369 # Update updates the given plugins, or every plugin if none is given.
370 #
371 # @param [Hash] plugins
372 # @param [Array<String>] specific Specific plugin names to update. If
373 # empty or nil, all plugins will be updated.
374 def update(plugins, specific, **opts)
375 specific ||= []
376 update = opts.merge({gems: specific.empty? ? true : specific})
377 internal_install(plugins, update)
378 end
379
380 # Clean removes any unused gems.
381 def clean(plugins, **opts)
382 @logger.debug("Cleaning Vagrant plugins of stale gems.")
383 # Generate dependencies for all registered plugins
384 plugin_deps = plugins.map do |name, info|
385 gem_version = info['installed_gem_version']
386 gem_version = info['gem_version'] if gem_version.to_s.empty?
387 gem_version = "> 0" if gem_version.to_s.empty?
388 Gem::Dependency.new(name, gem_version)
389 end
390
391 @logger.debug("Current plugin dependency list: #{plugin_deps}")
392
393 # Load dependencies into a request set for resolution
394 request_set = Gem::RequestSet.new(*plugin_deps)
395 # Never allow dependencies to be remotely satisfied during cleaning
396 request_set.remote = false
397
398 # Sets that we can resolve our dependencies from. Note that we only
399 # resolve from the current set as all required deps are activated during
400 # init.
401 current_set = generate_vagrant_set
402
403 # Collect all plugin specifications
404 plugin_specs = Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path|
405 Gem::Specification.load(spec_path)
406 end
407
408 # Include environment specific specification if enabled
409 if env_plugin_gem_path
410 plugin_specs += Dir.glob(env_plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path|
411 Gem::Specification.load(spec_path)
412 end
413 end
414
415 @logger.debug("Generating current plugin state solution set.")
416
417 # Resolve the request set to ensure proper activation order
418 solution = request_set.resolve(current_set)
419 solution_specs = solution.map(&:full_spec)
420 solution_full_names = solution_specs.map(&:full_name)
421
422 # Find all specs installed to plugins directory that are not
423 # found within the solution set.
424 plugin_specs.delete_if do |spec|
425 solution_full_names.include?(spec.full_name)
426 end
427
428 if env_plugin_gem_path
429 # If we are cleaning locally, remove any global specs. If
430 # not, remove any local specs
431 if opts[:env_local]
432 @logger.debug("Removing specifications that are not environment local")
433 plugin_specs.delete_if do |spec|
434 spec.full_gem_path.to_s.include?(plugin_gem_path.realpath.to_s)
435 end
436 else
437 @logger.debug("Removing specifications that are environment local")
438 plugin_specs.delete_if do |spec|
439 spec.full_gem_path.to_s.include?(env_plugin_gem_path.realpath.to_s)
440 end
441 end
442 end
443
444 @logger.debug("Specifications to be removed - #{plugin_specs.map(&:full_name)}")
445
446 # Now delete all unused specs
447 plugin_specs.each do |spec|
448 @logger.debug("Uninstalling gem - #{spec.full_name}")
449 Gem::Uninstaller.new(spec.name,
450 version: spec.version,
451 install_dir: plugin_gem_path,
452 all: true,
453 executables: true,
454 force: true,
455 ignore: true,
456 ).uninstall_gem(spec)
457 end
458
459 solution.find_all do |spec|
460 plugins.keys.include?(spec.name)
461 end
462 end
463
464 # During the duration of the yielded block, Bundler loud output
465 # is enabled.
466 def verbose
467 if block_given?
468 initial_state = @verbose
469 @verbose = true
470 yield
471 @verbose = initial_state
472 else
473 @verbose = true
474 end
475 end
476
477 protected
478
479 def internal_install(plugins, update, **extra)
480 update = {} if !update.is_a?(Hash)
481 skips = []
482 source_list = {}
483 system_plugins = plugins.map do |plugin_name, plugin_info|
484 plugin_name if plugin_info["system"]
485 end.compact
486 installer_set = VagrantSet.new(:both)
487 installer_set.system_plugins = system_plugins
488
489 # Generate all required plugin deps
490 plugin_deps = plugins.map do |name, info|
491 gem_version = info['gem_version'].to_s.empty? ? '> 0' : info['gem_version']
492 if update[:gems] == true || (update[:gems].respond_to?(:include?) && update[:gems].include?(name))
493 if Gem::Requirement.new(gem_version).exact?
494 gem_version = "> 0"
495 @logger.debug("Detected exact version match for `#{name}` plugin update. Reset to loosen constraint #{gem_version.inspect}.")
496 end
497 skips << name
498 end
499 source_list[name] ||= []
500 if plugin_source = info.delete("local_source")
501 installer_set.add_local(plugin_source.spec.name, plugin_source.spec, plugin_source)
502 source_list[name] << plugin_source.path
503 end
504 Array(info["sources"]).each do |source|
505 if !source.end_with?("/")
506 source = source + "/"
507 end
508 source_list[name] << source
509 end
510 Gem::Dependency.new(name, *gem_version.split(","))
511 end
512
513 if Vagrant.strict_dependency_enforcement
514 @logger.debug("Enabling strict dependency enforcement")
515 plugin_deps += vagrant_internal_specs.map do |spec|
516 next if system_plugins.include?(spec.name)
517 # If we are not running within the installer and
518 # we are not within a bundler environment then we
519 # only want activated specs
520 if !Vagrant.in_installer? && !Vagrant.in_bundler?
521 next if !spec.activated?
522 end
523 Gem::Dependency.new(spec.name, spec.version)
524 end.compact
525 else
526 @logger.debug("Disabling strict dependency enforcement")
527 end
528
529 @logger.debug("Dependency list for installation:\n - " \
530 "#{plugin_deps.map{|d| "#{d.name} #{d.requirement}"}.join("\n - ")}")
531
532 all_sources = source_list.values.flatten.uniq
533 default_sources = DEFAULT_GEM_SOURCES & all_sources
534 all_sources -= DEFAULT_GEM_SOURCES
535
536 # Only allow defined Gem sources
537 Gem.sources.clear
538
539 @logger.debug("Enabling user defined remote RubyGems sources")
540 all_sources.each do |src|
541 begin
542 next if File.file?(src) || URI.parse(src).scheme.nil?
543 rescue URI::InvalidURIError
544 next
545 end
546 @logger.debug("Adding RubyGems source #{src}")
547 Gem.sources << src
548 end
549
550 @logger.debug("Enabling default remote RubyGems sources")
551 default_sources.each do |src|
552 @logger.debug("Adding source - #{src}")
553 Gem.sources << src
554 end
555
556 validate_configured_sources!
557
558 source_list.values.each{|srcs| srcs.delete_if{|src| default_sources.include?(src)}}
559 installer_set.prefer_sources = source_list
560
561 @logger.debug("Current source list for install: #{Gem.sources.to_a}")
562
563 # Create the request set for the new plugins
564 request_set = Gem::RequestSet.new(*plugin_deps)
565
566 installer_set = Gem::Resolver.compose_sets(
567 installer_set,
568 generate_builtin_set(system_plugins),
569 generate_plugin_set(skips)
570 )
571
572 if Vagrant.allow_prerelease_dependencies?
573 @logger.debug("enabling prerelease dependency matching based on user request")
574 request_set.prerelease = true
575 installer_set.prerelease = true
576 end
577
578 @logger.debug("Generating solution set for installation.")
579
580 # Generate the required solution set for new plugins
581 solution = request_set.resolve(installer_set)
582
583 activate_solution(solution)
584
585 # Remove gems which are already installed
586 request_set.sorted_requests.delete_if do |act_req|
587 rs = act_req.spec
588 if vagrant_internal_specs.detect{ |i| i.name == rs.name && i.version == rs.version }
589 @logger.debug("Removing activation request from install. Already installed. (#{rs.spec.full_name})")
590 true
591 end
592 end
593
594 @logger.debug("Installing required gems.")
595
596 # Install all remote gems into plugin path. Set the installer to ignore dependencies
597 # as we know the dependencies are satisfied and it will attempt to validate a gem's
598 # dependencies are satisfied by gems in the install directory (which will likely not
599 # be true)
600 install_path = extra[:env_local] ? env_plugin_gem_path : plugin_gem_path
601 result = request_set.install_into(install_path.to_s, true,
602 ignore_dependencies: true,
603 prerelease: Vagrant.prerelease? || Vagrant.allow_prerelease_dependencies?,
604 wrappers: true,
605 document: []
606 )
607
608 result = result.map(&:full_spec)
609 result.each do |spec|
610 existing_paths = $LOAD_PATH.find_all{|s| s.include?(spec.full_name) }
611 if !existing_paths.empty?
612 @logger.debug("Removing existing LOAD_PATHs for #{spec.full_name} - " +
613 existing_paths.join(", "))
614 existing_paths.each{|s| $LOAD_PATH.delete(s) }
615 end
616 spec.full_require_paths.each do |r_path|
617 if !$LOAD_PATH.include?(r_path)
618 @logger.debug("Adding path to LOAD_PATH - #{r_path}")
619 $LOAD_PATH.unshift(r_path)
620 end
621 end
622 end
623 result
624 end
625
626 # Generate the composite resolver set totally all of vagrant (builtin + plugin set)
627 def generate_vagrant_set
628 sets = [generate_builtin_set, generate_plugin_set]
629 if env_plugin_gem_path && env_plugin_gem_path.exist?
630 sets << generate_plugin_set(env_plugin_gem_path)
631 end
632 Gem::Resolver.compose_sets(*sets)
633 end
634
635 # @return [Array<[Gem::Specification]>] spec list
636 def vagrant_internal_specs
637 # activate any dependencies up front so we can always
638 # pin them when resolving
639 self_spec = Gem::Specification.find { |s| s.name == "vagrant" && s.activated? }
640 if !self_spec
641 @logger.warn("Failed to locate activated vagrant specification. Activating...")
642 self_spec = Gem::Specification.find { |s| s.name == "vagrant" }
643 if !self_spec
644 @logger.error("Failed to locate Vagrant RubyGem specification")
645 raise Vagrant::Errors::SourceSpecNotFound
646 end
647 self_spec.activate
648 @logger.info("Activated vagrant specification version - #{self_spec.version}")
649 end
650 self_spec.runtime_dependencies.each { |d| gem d.name, *d.requirement.as_list }
651 # discover all the gems we have available
652 list = {}
653 if Gem.respond_to?(:default_specifications_dir)
654 spec_dir = Gem.default_specifications_dir
655 else
656 spec_dir = Gem::Specification.default_specifications_dir
657 end
658 directories = [spec_dir]
659 Gem::Specification.find_all{true}.each do |spec|
660 list[spec.full_name] = spec
661 end
662 if(!Object.const_defined?(:Bundler))
663 directories += Gem::Specification.dirs.find_all do |path|
664 !path.start_with?(Gem.user_dir)
665 end
666 end
667 Gem::Specification.each_spec(directories) do |spec|
668 if !list[spec.full_name]
669 list[spec.full_name] = spec
670 end
671 end
672 list.values
673 end
674
675 # Iterates each configured RubyGem source to validate that it is properly
676 # available. If source is unavailable an exception is raised.
677 def validate_configured_sources!
678 Gem.sources.each_source do |src|
679 begin
680 src.load_specs(:released)
681 rescue Gem::Exception => source_error
682 if ENV["VAGRANT_ALLOW_PLUGIN_SOURCE_ERRORS"]
683 @logger.warn("Failed to load configured plugin source: #{src}!")
684 @logger.warn("Error received attempting to load source (#{src}): #{source_error}")
685 @logger.warn("Ignoring plugin source load failure due user request via env variable")
686 else
687 @logger.error("Failed to load configured plugin source `#{src}`: #{source_error}")
688 raise Vagrant::Errors::PluginSourceError,
689 source: src.uri.to_s,
690 error_msg: source_error.message
691 end
692 end
693 end
694 end
695
696 # Generate the builtin resolver set
697 def generate_builtin_set(system_plugins=[])
698 builtin_set = BuiltinSet.new
699 @logger.debug("Generating new builtin set instance.")
700 vagrant_internal_specs.each do |spec|
701 if !system_plugins.include?(spec.name)
702 builtin_set.add_builtin_spec(spec)
703 end
704 end
705 builtin_set
706 end
707
708 # Generate the plugin resolver set. Optionally provide specification names (short or
709 # full) that should be ignored
710 #
711 # @param [Pathname] path to plugins
712 # @param [Array<String>] gems to skip
713 # @return [PluginSet]
714 def generate_plugin_set(*args)
715 plugin_path = args.detect{|i| i.is_a?(Pathname) } || plugin_gem_path
716 skip = args.detect{|i| i.is_a?(Array) } || []
717 plugin_set = PluginSet.new
718 @logger.debug("Generating new plugin set instance. Skip gems - #{skip}")
719 Dir.glob(plugin_path.join('specifications/*.gemspec').to_s).each do |spec_path|
720 spec = Gem::Specification.load(spec_path)
721 desired_spec_path = File.join(spec.gem_dir, "#{spec.name}.gemspec")
722 # Vendor set requires the spec to be within the gem directory. Some gems will package their
723 # spec file, and that's not what we want to load.
724 if !File.exist?(desired_spec_path) || !FileUtils.cmp(spec.spec_file, desired_spec_path)
725 File.write(desired_spec_path, spec.to_ruby)
726 end
727 next if skip.include?(spec.name) || skip.include?(spec.full_name)
728 plugin_set.add_vendor_gem(spec.name, spec.gem_dir)
729 end
730 plugin_set
731 end
732
733 # Activate a given solution
734 def activate_solution(solution)
735 retried = false
736 begin
737 @logger.debug("Activating solution set: #{solution.map(&:full_name)}")
738 solution.each do |activation_request|
739 unless activation_request.full_spec.activated?
740 @logger.debug("Activating gem #{activation_request.full_spec.full_name}")
741 activation_request.full_spec.activate
742 if(defined?(::Bundler))
743 @logger.debug("Marking gem #{activation_request.full_spec.full_name} loaded within Bundler.")
744 ::Bundler.rubygems.mark_loaded activation_request.full_spec
745 end
746 end
747 end
748 rescue Gem::LoadError => e
749 # Depending on the version of Ruby, the ordering of the solution set
750 # will be either 0..n (molinillo) or n..0 (pre-molinillo). Instead of
751 # attempting to determine what's in use, or if it has some how changed
752 # again, just reverse order on failure and attempt again.
753 if retried
754 @logger.error("Failed to load solution set - #{e.class}: #{e}")
755 matcher = e.message.match(/Could not find '(?<gem_name>[^']+)'/)
756 if matcher && !matcher["gem_name"].empty?
757 desired_activation_request = solution.detect do |request|
758 request.name == matcher["gem_name"]
759 end
760 if desired_activation_request && !desired_activation_request.full_spec.activated?
761 @logger.warn("Found misordered activation request for #{desired_activation_request.full_name}. Moving to solution HEAD.")
762 solution.delete(desired_activation_request)
763 solution.unshift(desired_activation_request)
764 retry
765 end
766 end
767
768 raise
769 else
770 @logger.debug("Failed to load solution set. Retrying with reverse order.")
771 retried = true
772 solution.reverse!
773 retry
774 end
775 end
776 end
777
778 # This is a custom Gem::Resolver::InstallerSet. It will prefer sources which are
779 # explicitly provided over default sources when matches are found. This is generally
780 # the entire set used for performing full resolutions on install.
781 class VagrantSet < Gem::Resolver::InstallerSet
782 attr_accessor :prefer_sources
783 attr_accessor :system_plugins
784
785 def initialize(domain, defined_sources={})
786 @prefer_sources = defined_sources
787 @system_plugins = []
788 super(domain)
789 end
790
791 # Allow InstallerSet to find matching specs, then filter
792 # for preferred sources
793 def find_all(req)
794 result = super
795 if system_plugins.include?(req.name)
796 result.delete_if do |spec|
797 spec.is_a?(Gem::Resolver::InstalledSpecification)
798 end
799 end
800 subset = result.find_all do |idx_spec|
801 preferred = false
802 if prefer_sources[req.name]
803 if idx_spec.source.respond_to?(:path)
804 preferred = prefer_sources[req.name].include?(idx_spec.source.path.to_s)
805 end
806 if !preferred
807 preferred = prefer_sources[req.name].include?(idx_spec.source.uri.to_s)
808 end
809 end
810 preferred
811 end
812 subset.empty? ? result : subset
813 end
814 end
815
816 # This is a custom Gem::Resolver::Set for use with vagrant "system" gems. It
817 # allows the installed set of gems to be used for providing a solution while
818 # enforcing strict constraints. This ensures that plugins cannot "upgrade"
819 # gems that are builtin to vagrant itself.
820 class BuiltinSet < Gem::Resolver::Set
821 def initialize
822 super
823 @remote = false
824 @specs = []
825 end
826
827 def add_builtin_spec(spec)
828 @specs.push(spec).uniq!
829 end
830
831 def find_all(req)
832 r = @specs.select do |spec|
833 # When matching requests against builtin specs, we _always_ enable
834 # prerelease matching since any prerelease that's found in this
835 # set has been added explicitly and should be available for all
836 # plugins to resolve against. This includes Vagrant itself since
837 # it is considered a prerelease when in development mode
838 req.match?(spec, true)
839 end.map do |spec|
840 Gem::Resolver::InstalledSpecification.new(self, spec)
841 end
842 # If any of the results are a prerelease, we need to mark the request
843 # to allow prereleases so the solution can be properly fulfilled
844 if r.any? { |x| x.version.prerelease? }
845 req.dependency.prerelease = true
846 end
847 r
848 end
849 end
850
851 # This is a custom Gem::Resolver::Set for use with Vagrant plugins. It is
852 # a modified Gem::Resolver::VendorSet that supports multiple versions of
853 # a specific gem
854 class PluginSet < Gem::Resolver::VendorSet
855 ##
856 # Adds a specification to the set with the given +name+ which has been
857 # unpacked into the given +directory+.
858 def add_vendor_gem(name, directory)
859 gemspec = File.join(directory, "#{name}.gemspec")
860 spec = Gem::Specification.load(gemspec)
861 if !spec
862 raise Gem::GemNotFoundException,
863 "unable to find #{gemspec} for gem #{name}"
864 end
865
866 spec.full_gem_path = File.expand_path(directory)
867 spec.base_dir = File.dirname(spec.base_dir)
868
869 @specs[spec.name] ||= []
870 @specs[spec.name] << spec
871 @directories[spec] = directory
872
873 spec
874 end
875
876 ##
877 # Returns an Array of VendorSpecification objects matching the
878 # DependencyRequest +req+.
879 def find_all(req)
880 @specs.values.flatten.select do |spec|
881 req.match?(spec, prerelease)
882 end.map do |spec|
883 source = Gem::Source::Vendor.new(@directories[spec])
884 Gem::Resolver::VendorSpecification.new(self, spec, source)
885 end
886 end
887
888 ##
889 # Loads a spec with the given +name+. +version+, +platform+ and +source+ are
890 # ignored.
891 def load_spec(name, version, platform, source)
892 version = Gem::Version.new(version) if !version.is_a?(Gem::Version)
893 @specs.fetch(name, []).detect{|s| s.name == name && s.version == version}
894 end
895 end
896 end
897 end
898
899 # Patch for Ruby 2.2 and Bundler to behave properly when uninstalling plugins
900 if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
901 if defined?(::Bundler) && !::Bundler::SpecSet.instance_methods.include?(:delete)
902 class Gem::Specification
903 def self.remove_spec(spec)
904 Gem::Specification.reset
905 end
906 end
907 end
908 end