This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class GemInstaller
|
||||
attr_reader :spec, :standalone, :worker, :force, :local, :installer
|
||||
|
||||
def initialize(spec, installer, standalone = false, worker = 0, force = false, local = false)
|
||||
@spec = spec
|
||||
@installer = installer
|
||||
@standalone = standalone
|
||||
@worker = worker
|
||||
@force = force
|
||||
@local = local
|
||||
end
|
||||
|
||||
def install_from_spec
|
||||
post_install_message = install
|
||||
Bundler.ui.debug "#{worker}: #{spec.name} (#{spec.version}) from #{spec.loaded_from}"
|
||||
[true, post_install_message]
|
||||
rescue Bundler::InstallHookError, Bundler::SecurityError, Bundler::APIResponseMismatchError, Bundler::InsecureInstallPathError
|
||||
raise
|
||||
rescue Errno::ENOSPC
|
||||
[false, out_of_space_message]
|
||||
rescue Bundler::BundlerError, Gem::InstallError => e
|
||||
[false, specific_failure_message(e)]
|
||||
end
|
||||
|
||||
def download
|
||||
spec.source.download(
|
||||
spec,
|
||||
force: force,
|
||||
local: local,
|
||||
build_args: Array(spec_settings),
|
||||
previous_spec: previous_spec,
|
||||
)
|
||||
|
||||
[true, nil]
|
||||
rescue Bundler::BundlerError => e
|
||||
[false, specific_failure_message(e)]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def specific_failure_message(e)
|
||||
message = "#{e.class}: #{e.message}\n"
|
||||
message += " " + e.backtrace.join("\n ") + "\n\n"
|
||||
message = message.lines.first + Bundler.ui.add_color(message.lines.drop(1).join, :clear)
|
||||
message + Bundler.ui.add_color(failure_message, :red)
|
||||
end
|
||||
|
||||
def failure_message
|
||||
install_error_message
|
||||
end
|
||||
|
||||
def install_error_message
|
||||
"An error occurred while installing #{spec.name} (#{spec.version}), and Bundler cannot continue."
|
||||
end
|
||||
|
||||
def spec_settings
|
||||
# Fetch the build settings, if there are any
|
||||
if settings = Bundler.settings["build.#{spec.name}"]
|
||||
require "shellwords"
|
||||
Shellwords.shellsplit(settings)
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
spec.source.install(
|
||||
spec,
|
||||
force: force,
|
||||
local: local,
|
||||
build_args: Array(spec_settings),
|
||||
previous_spec: previous_spec,
|
||||
)
|
||||
end
|
||||
|
||||
def previous_spec
|
||||
locked_gems = installer.definition.locked_gems
|
||||
return unless locked_gems
|
||||
|
||||
locked_gems.specs.find {|s| s.name == spec.name }
|
||||
end
|
||||
|
||||
def out_of_space_message
|
||||
"#{install_error_message}\nYour disk is out of space. Free some space to be able to install your bundle."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,294 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../worker"
|
||||
require_relative "gem_installer"
|
||||
|
||||
module Bundler
|
||||
class ParallelInstaller
|
||||
class SpecInstallation
|
||||
attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error, :dependencies
|
||||
def initialize(spec)
|
||||
@spec = spec
|
||||
@name = spec.name
|
||||
@full_name = spec.full_name
|
||||
@state = :none
|
||||
@post_install_message = ""
|
||||
@error = nil
|
||||
end
|
||||
|
||||
def installed?
|
||||
state == :installed
|
||||
end
|
||||
|
||||
def enqueued?
|
||||
state == :enqueued
|
||||
end
|
||||
|
||||
def enqueue_with_priority?
|
||||
state == :installable && spec.extensions.any?
|
||||
end
|
||||
|
||||
def failed?
|
||||
state == :failed
|
||||
end
|
||||
|
||||
def ready_to_enqueue?
|
||||
state == :none
|
||||
end
|
||||
|
||||
def ready_to_install?(installed_specs)
|
||||
return false unless state == :downloaded
|
||||
|
||||
spec.extensions.none? || dependencies_installed?(installed_specs)
|
||||
end
|
||||
|
||||
def has_post_install_message?
|
||||
!post_install_message.empty?
|
||||
end
|
||||
|
||||
# Recursively checks that all dependencies (direct and transitive) have been installed.
|
||||
def dependencies_installed?(installed_specs)
|
||||
dependencies.all? do |dep|
|
||||
installed_specs.include?(dep.name) && dep.dependencies_installed?(installed_specs)
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#<#{self.class} #{full_name} (#{state})>"
|
||||
end
|
||||
end
|
||||
|
||||
def self.call(*args, **kwargs)
|
||||
new(*args, **kwargs).call
|
||||
end
|
||||
|
||||
attr_reader :size
|
||||
|
||||
def initialize(installer, all_specs, size, standalone, force, local: false, skip: nil)
|
||||
@installer = installer
|
||||
@size = size
|
||||
@standalone = standalone
|
||||
@force = force
|
||||
@local = local
|
||||
@specs = all_specs.map {|s| SpecInstallation.new(s) }
|
||||
specs_by_name = @specs.to_h {|s| [s.name, s] }
|
||||
@specs.each do |spec_install|
|
||||
spec_install.dependencies = spec_install.spec.dependencies.filter_map do |dep|
|
||||
specs_by_name[dep.name] unless dep.type == :development || dep.name == spec_install.name
|
||||
end
|
||||
end
|
||||
@specs.each do |spec_install|
|
||||
spec_install.state = :installed if skip.include?(spec_install.name)
|
||||
end if skip
|
||||
@spec_set = all_specs
|
||||
@rake = @specs.find {|s| s.name == "rake" unless s.installed? }
|
||||
end
|
||||
|
||||
def call
|
||||
if @rake
|
||||
do_download(@rake, 0)
|
||||
do_install(@rake, 0)
|
||||
Gem::Specification.reset
|
||||
end
|
||||
|
||||
if @size > 1
|
||||
install_with_worker
|
||||
else
|
||||
install_serially
|
||||
end
|
||||
|
||||
handle_error if failed_specs.any?
|
||||
@specs
|
||||
ensure
|
||||
worker_pool&.stop
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def failed_specs
|
||||
@specs.select(&:failed?)
|
||||
end
|
||||
|
||||
def install_with_worker
|
||||
with_jobserver do
|
||||
installed_specs = {}
|
||||
enqueue_specs(installed_specs)
|
||||
|
||||
process_specs(installed_specs) until finished_installing?
|
||||
end
|
||||
end
|
||||
|
||||
def with_jobserver
|
||||
# The jobserver hands tokens to child `make` processes through MAKEFLAGS
|
||||
# using the GNU make `--jobserver-auth` protocol. nmake, the default make
|
||||
# on mswin, instead reads MAKEFLAGS as bare option letters and aborts
|
||||
# every native extension build with `fatal error U1065: invalid option
|
||||
# '-'`. Skip the jobserver when nmake is in use. Other Windows toolchains
|
||||
# such as mingw use GNU make and keep working through the inherited pipe.
|
||||
return yield if nmake? || bsd_make?
|
||||
|
||||
begin
|
||||
r, w = IO.pipe
|
||||
r.close_on_exec = false
|
||||
w.close_on_exec = false
|
||||
w.write("*" * @size)
|
||||
|
||||
old_makeflags = ENV["MAKEFLAGS"]
|
||||
ENV["MAKEFLAGS"] = [old_makeflags, "--jobserver-auth=#{r.fileno},#{w.fileno}"].compact.join(" ")
|
||||
|
||||
yield
|
||||
ensure
|
||||
# Restore MAKEFLAGS before closing the pipe so a close failure can't
|
||||
# leave the process with descriptors that point at a closed pipe.
|
||||
old_makeflags ? ENV["MAKEFLAGS"] = old_makeflags : ENV.delete("MAKEFLAGS")
|
||||
|
||||
r&.close
|
||||
w&.close
|
||||
end
|
||||
end
|
||||
|
||||
# Mirror how RubyGems' extension builder picks the make program so the
|
||||
# jobserver is only set up when a GNU-compatible make will consume it.
|
||||
def nmake?
|
||||
make = ENV["MAKE"] || ENV["make"]
|
||||
make ||= "nmake" if RUBY_PLATFORM.include?("mswin")
|
||||
/\bnmake/i.match?(make.to_s)
|
||||
end
|
||||
|
||||
def bsd_make?
|
||||
return false unless Gem.freebsd_platform?
|
||||
make = ENV["MAKE"] || ENV["make"] || "make"
|
||||
!/\bgmake/i.match?(make)
|
||||
end
|
||||
|
||||
def install_serially
|
||||
until finished_installing?
|
||||
raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?)
|
||||
spec_install.state = :enqueued
|
||||
do_download(spec_install, 0)
|
||||
do_install(spec_install, 0)
|
||||
end
|
||||
end
|
||||
|
||||
def worker_pool
|
||||
@worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda {|spec_install, worker_num|
|
||||
case spec_install.state
|
||||
when :enqueued
|
||||
do_download(spec_install, worker_num)
|
||||
when :installable
|
||||
do_install(spec_install, worker_num)
|
||||
else
|
||||
spec_install
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def do_download(spec_install, worker_num)
|
||||
Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install)
|
||||
|
||||
gem_installer = Bundler::GemInstaller.new(
|
||||
spec_install.spec, @installer, @standalone, worker_num, @force, @local
|
||||
)
|
||||
|
||||
success, message = gem_installer.download
|
||||
|
||||
if success
|
||||
spec_install.state = :downloaded
|
||||
else
|
||||
spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}"
|
||||
spec_install.state = :failed
|
||||
end
|
||||
|
||||
spec_install
|
||||
end
|
||||
|
||||
def do_install(spec_install, worker_num)
|
||||
gem_installer = Bundler::GemInstaller.new(
|
||||
spec_install.spec, @installer, @standalone, worker_num, @force, @local
|
||||
)
|
||||
success, message = gem_installer.install_from_spec
|
||||
if success
|
||||
spec_install.state = :installed
|
||||
spec_install.post_install_message = message unless message.nil?
|
||||
else
|
||||
spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}"
|
||||
spec_install.state = :failed
|
||||
end
|
||||
Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL, spec_install)
|
||||
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(installed_specs)
|
||||
spec = worker_pool.deq
|
||||
|
||||
if spec.installed?
|
||||
installed_specs[spec.name] = true
|
||||
return
|
||||
elsif spec.failed?
|
||||
return
|
||||
elsif spec.ready_to_install?(installed_specs)
|
||||
spec.state = :installable
|
||||
end
|
||||
|
||||
worker_pool.enq(spec, priority: spec.enqueue_with_priority?)
|
||||
end
|
||||
|
||||
def finished_installing?
|
||||
@specs.all? do |spec|
|
||||
return true if spec.failed?
|
||||
spec.installed?
|
||||
end
|
||||
end
|
||||
|
||||
def handle_error
|
||||
errors = failed_specs.map(&:error)
|
||||
if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) }
|
||||
raise exception
|
||||
end
|
||||
raise Bundler::InstallError, errors.join("\n\n")
|
||||
end
|
||||
|
||||
def require_tree_for_spec(spec)
|
||||
tree = @spec_set.what_required(spec)
|
||||
gemfile_name = begin
|
||||
File.basename(SharedHelpers.default_gemfile)
|
||||
rescue GemfileNotFound
|
||||
# This runs while reporting an install error. When no Gemfile can be
|
||||
# located (e.g. Bundler used as a library), raising here would mask
|
||||
# the original error, so fall back to a generic header instead.
|
||||
"Gemfile"
|
||||
end
|
||||
t = String.new("In #{gemfile_name}:\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(installed_specs)
|
||||
@specs.each do |spec|
|
||||
if spec.installed?
|
||||
installed_specs[spec.name] = true
|
||||
next
|
||||
end
|
||||
|
||||
spec.state = :enqueued
|
||||
worker_pool.enq spec
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Standalone
|
||||
def initialize(groups, definition)
|
||||
@specs = definition.specs_for(groups)
|
||||
end
|
||||
|
||||
def generate
|
||||
SharedHelpers.filesystem_access(bundler_path) do |p|
|
||||
FileUtils.mkdir_p(p)
|
||||
end
|
||||
File.open File.join(bundler_path, "setup.rb"), "w" do |file|
|
||||
file.puts "require 'rbconfig'"
|
||||
file.puts prevent_gem_activation
|
||||
file.puts define_path_helpers
|
||||
file.puts reverse_rubygems_kernel_mixin
|
||||
paths.each do |path|
|
||||
if Pathname.new(path).absolute?
|
||||
file.puts %($:.unshift "#{path}")
|
||||
else
|
||||
file.puts %($:.unshift File.expand_path("\#{__dir__}/#{path}"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def paths
|
||||
@specs.flat_map do |spec|
|
||||
next if spec.name == "bundler"
|
||||
Array(spec.require_paths).map do |path|
|
||||
gem_path(path, spec).
|
||||
sub(version_dir, '#{RUBY_ENGINE}/#{Gem.ruby_api_version}').
|
||||
sub(extensions_dir, 'extensions/\k<platform>/#{Gem.extension_api_version}')
|
||||
# This is a static string intentionally. It's interpolated at a later time.
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
def version_dir
|
||||
"#{RUBY_ENGINE}/#{Gem.ruby_api_version}"
|
||||
end
|
||||
|
||||
def extensions_dir
|
||||
%r{extensions/(?<platform>[^/]+)/#{Regexp.escape(Gem.extension_api_version)}}
|
||||
end
|
||||
|
||||
def bundler_path
|
||||
Bundler.root.join(Bundler.settings[:path].to_s, "bundler")
|
||||
end
|
||||
|
||||
def gem_path(path, spec)
|
||||
full_path = Pathname.new(path).absolute? ? path : File.join(spec.full_gem_path, path)
|
||||
if spec.source.instance_of?(Source::Path) && spec.source.path.absolute?
|
||||
full_path
|
||||
else
|
||||
SharedHelpers.relative_path_to(full_path, from: Bundler.root.join(bundler_path))
|
||||
end
|
||||
end
|
||||
|
||||
def prevent_gem_activation
|
||||
<<~'END'
|
||||
module Kernel
|
||||
remove_method(:gem) if private_method_defined?(:gem)
|
||||
|
||||
def gem(*)
|
||||
end
|
||||
|
||||
private :gem
|
||||
end
|
||||
END
|
||||
end
|
||||
|
||||
def define_path_helpers
|
||||
<<~'END'
|
||||
unless defined?(Gem)
|
||||
module Gem
|
||||
def self.ruby_api_version
|
||||
RbConfig::CONFIG["ruby_version"]
|
||||
end
|
||||
|
||||
def self.extension_api_version
|
||||
if 'no' == RbConfig::CONFIG['ENABLE_SHARED']
|
||||
"#{ruby_api_version}-static"
|
||||
else
|
||||
ruby_api_version
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
END
|
||||
end
|
||||
|
||||
def reverse_rubygems_kernel_mixin
|
||||
<<~END
|
||||
if Gem.respond_to?(:discover_gems_on_require=)
|
||||
Gem.discover_gems_on_require = false
|
||||
else
|
||||
[::Kernel.singleton_class, ::Kernel].each do |k|
|
||||
if k.private_method_defined?(:gem_original_require)
|
||||
private_require = k.private_method_defined?(:require)
|
||||
k.send(:remove_method, :require)
|
||||
k.send(:define_method, :require, k.instance_method(:gem_original_require))
|
||||
k.send(:private, :require) if private_require
|
||||
end
|
||||
end
|
||||
end
|
||||
END
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user