aboutsummaryrefslogtreecommitdiffstats
path: root/lib/bundler/installer/parallel_installer.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/installer/parallel_installer.rb')
-rw-r--r--lib/bundler/installer/parallel_installer.rb197
1 files changed, 197 insertions, 0 deletions
diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb
new file mode 100644
index 0000000000..97c124e015
--- /dev/null
+++ b/lib/bundler/installer/parallel_installer.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+require "bundler/worker"
+require "bundler/installer/gem_installer"
+
+module Bundler
+ class ParallelInstaller
+ class SpecInstallation
+ attr_accessor :spec, :name, :post_install_message, :state, :error
+ def initialize(spec)
+ @spec = spec
+ @name = spec.name
+ @state = :none
+ @post_install_message = ""
+ @error = nil
+ end
+
+ def installed?
+ state == :installed
+ end
+
+ def enqueued?
+ state == :enqueued
+ end
+
+ def failed?
+ state == :failed
+ end
+
+ def installation_attempted?
+ installed? || failed?
+ end
+
+ # Only true when spec in neither installed nor already enqueued
+ def ready_to_enqueue?
+ !enqueued? && !installation_attempted?
+ 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? {|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
+ @dependencies ||= begin
+ all_dependencies.reject {|dep| ignorable_dependency? dep }
+ end
+ end
+
+ def missing_lockfile_dependencies(all_spec_names)
+ deps = all_dependencies.reject {|dep| ignorable_dependency? dep }
+ deps.reject {|dep| all_spec_names.include? dep.name }
+ end
+
+ # Represents all dependencies
+ def all_dependencies
+ @spec.dependencies
+ end
+
+ def to_s
+ "#<#{self.class} #{@spec.full_name} (#{state})>"
+ 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
+
+ attr_reader :size
+
+ def initialize(installer, all_specs, size, standalone, force)
+ @installer = installer
+ @size = size
+ @standalone = standalone
+ @force = force
+ @specs = all_specs.map {|s| SpecInstallation.new(s) }
+ @spec_set = all_specs
+ end
+
+ def call
+ # Since `autoload` has the potential for threading issues on 1.8.7
+ # TODO: remove in bundler 2.0
+ require "bundler/gem_remote_fetcher" if RUBY_VERSION < "1.9"
+
+ check_for_corrupt_lockfile
+ enqueue_specs
+ process_specs until @specs.all?(&:installed?) || @specs.any?(&:failed?)
+ handle_error if @specs.any?(&:failed?)
+ @specs
+ ensure
+ worker_pool && worker_pool.stop
+ end
+
+ def worker_pool
+ @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num|
+ gem_installer = Bundler::GemInstaller.new(
+ spec_install.spec, @installer, @standalone, worker_num, @force
+ )
+ success, message = gem_installer.install_from_spec
+ if success && !message.nil?
+ spec_install.post_install_message = message
+ elsif !success
+ spec_install.state = :failed
+ spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}"
+ end
+ 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 unless spec.failed?
+ enqueue_specs
+ end
+
+ def handle_error
+ errors = @specs.select(&:failed?).map(&:error)
+ if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) }
+ raise exception
+ end
+ raise Bundler::InstallError, errors.map(&:to_s).join("\n\n")
+ end
+
+ def check_for_corrupt_lockfile
+ missing_dependencies = @specs.map do |s|
+ [
+ s,
+ s.missing_lockfile_dependencies(@specs.map(&:name)),
+ ]
+ end.reject { |a| a.last.empty? }
+ return if missing_dependencies.empty?
+
+ warning = []
+ warning << "Your lockfile was created by an old Bundler that left some things out."
+ if @size != 1
+ warning << "Because of the missing DEPENDENCIES, we can only install gems one at a time, instead of installing #{@size} at a time."
+ @size = 1
+ end
+ warning << "You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile."
+ warning << "The missing gems are:"
+
+ missing_dependencies.each do |spec, missing|
+ warning << "* #{missing.map(&:name).join(", ")} depended upon by #{spec.name}"
+ end
+
+ Bundler.ui.warn(warning.join("\n"))
+ end
+
+ def require_tree_for_spec(spec)
+ tree = @spec_set.what_required(spec)
+ t = String.new("In #{File.basename(SharedHelpers.default_gemfile)}:\n")
+ tree.each_with_index do |s, depth|
+ t << " " * depth.succ << s.name
+ unless tree.last == s
+ t << %( was resolved to #{s.version}, which depends on)
+ end
+ t << %(\n)
+ end
+ t
+ 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
+ spec.state = :enqueued
+ worker_pool.enq spec
+ end
+ end
+ end
+ end
+end