lib/bundler/installer/parallel_installer.rb
# frozen_string_literal: true require "bundler/worker" require "bundler/installer/gem_installer" class ParallelInstaller class SpecInstallation attr_accessor :spec, :name, :post_install_message, :state def initialize(spec) @spec = spec @name = spec.name @state = :none @post_install_message = "" end def installed? state == :installed end def enqueued? state == :enqueued end # Only true when spec in neither installed nor already enqueued def ready_to_enqueue? !installed? && !enqueued? end def has_post_install_message? !post_install_message.empty? end def ignorable_dependency?(dep) dep.type == :development || dep.name == @name end # Checks installed dependencies against spec's dependencies to make # sure needed dependencies have been installed. def dependencies_installed?(all_specs) installed_specs = all_specs.select(&:installed?).map(&:name) dependencies(all_specs.map(&:name)).all? {|d| installed_specs.include? d.name } end # Represents only the non-development dependencies, the ones that are # itself and are in the total list. def dependencies(all_spec_names) @dependencies ||= begin deps = all_dependencies.reject {|dep| ignorable_dependency? dep } missing = deps.reject {|dep| all_spec_names.include? dep.name } unless missing.empty? raise Bundler::LockfileError, "Your Gemfile.lock is corrupt. The following #{missing.size > 1 ? "gems are" : "gem is"} missing " \ "from the DEPENDENCIES section: '#{missing.map(&:name).join('\' \'')}'" end deps end end # Represents all dependencies def all_dependencies @spec.dependencies end end def self.call(*args) new(*args).call end # Returns max number of threads machine can handle with a min of 1 def self.max_threads [Bundler.settings[:jobs].to_i - 1, 1].max end def initialize(installer, all_specs, size, standalone, force) @installer = installer @size = size @standalone = standalone @force = force @specs = all_specs.map {|s| SpecInstallation.new(s) } end def call enqueue_specs process_specs until @specs.all?(&:installed?) ensure worker_pool && worker_pool.stop end def worker_pool @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num| message = Bundler::GemInstaller.new( spec_install.spec, @installer, @standalone, worker_num, @force ).install_from_spec spec_install.post_install_message = message unless message.nil? spec_install } end # Dequeue a spec and save its post-install message and then enqueue the # remaining specs. # Some specs might've had to wait til this spec was installed to be # processed so the call to `enqueue_specs` is important after every # dequeue. def process_specs spec = worker_pool.deq spec.state = :installed collect_post_install_message spec if spec.has_post_install_message? enqueue_specs end def collect_post_install_message(spec) Bundler::Installer.post_install_messages[spec.name] = spec.post_install_message end # Keys in the remains hash represent uninstalled gems specs. # We enqueue all gem specs that do not have any dependencies. # Later we call this lambda again to install specs that depended on # previously installed specifications. We continue until all specs # are installed. def enqueue_specs @specs.select(&:ready_to_enqueue?).each do |spec| if spec.dependencies_installed? @specs worker_pool.enq spec spec.state = :enqueued end end end end