"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