diff options
author | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-08 08:45:41 +0000 |
---|---|---|
committer | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-08 08:45:41 +0000 |
commit | 8598f8c2dc78c6d1ae87cb6ae19c34ba2cb29241 (patch) | |
tree | 0bbd28f684e745cb212761b7c74fe08668f85cc8 /lib/bundler/installer | |
parent | f2e04b77aa8a363d7e36ce5a9cdb60714a537a3c (diff) |
Merge bundler to standard libraries.
rubygems 2.7.x depends bundler-1.15.x. This is preparation for
rubygems and bundler migration.
* lib/bundler.rb, lib/bundler/*: files of bundler-1.15.4
* spec/bundler/*: rspec examples of bundler-1.15.4. I applied patches.
* https://github.com/bundler/bundler/pull/6007
* Exclude not working examples on ruby repository.
* Fake ruby interpriter instead of installed ruby.
* Makefile.in: Added test task named `test-bundler`. This task is only
working macOS/linux yet. I'm going to support Windows environment later.
* tool/sync_default_gems.rb: Added sync task for bundler.
[Feature #12733][ruby-core:77172]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@59779 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/bundler/installer')
-rw-r--r-- | lib/bundler/installer/gem_installer.rb | 76 | ||||
-rw-r--r-- | lib/bundler/installer/parallel_installer.rb | 197 | ||||
-rw-r--r-- | lib/bundler/installer/standalone.rb | 52 |
3 files changed, 325 insertions, 0 deletions
diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb new file mode 100644 index 0000000000..a4d9bcaa07 --- /dev/null +++ b/lib/bundler/installer/gem_installer.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +module Bundler + class GemInstaller + attr_reader :spec, :standalone, :worker, :force, :installer + + def initialize(spec, installer, standalone = false, worker = 0, force = false) + @spec = spec + @installer = installer + @standalone = standalone + @worker = worker + @force = force + end + + def install_from_spec + post_install_message = spec_settings ? install_with_settings : install + Bundler.ui.debug "#{worker}: #{spec.name} (#{spec.version}) from #{spec.loaded_from}" + generate_executable_stubs + return true, post_install_message + rescue Bundler::InstallHookError, Bundler::SecurityError, APIResponseMismatchError + raise + rescue Errno::ENOSPC + return false, out_of_space_message + rescue => e + return 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" if Bundler.ui.debug? + 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 + return install_error_message if spec.source.options["git"] + "#{install_error_message}\n#{gem_install_message}" + end + + def install_error_message + "An error occurred while installing #{spec.name} (#{spec.version}), and Bundler cannot continue." + end + + def gem_install_message + "Make sure that `gem install #{spec.name} -v '#{spec.version}'` succeeds before bundling." + end + + def spec_settings + # Fetch the build settings, if there are any + Bundler.settings["build.#{spec.name}"] + end + + def install + spec.source.install(spec, :force => force, :ensure_builtin_gems_cached => standalone, :build_args => Array(spec_settings)) + end + + def install_with_settings + # Build arguments are global, so this is mutexed + Bundler.rubygems.install_with_build_args([spec_settings]) { install } + 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 + + def generate_executable_stubs + return if Bundler.settings[:inline] + if Bundler.settings[:bin] && standalone + installer.generate_standalone_bundler_executable_stubs(spec) + elsif Bundler.settings[:bin] + installer.generate_bundler_executable_stubs(spec, :force => true) + end + end + end +end 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 diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb new file mode 100644 index 0000000000..03411d85e2 --- /dev/null +++ b/lib/bundler/installer/standalone.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +module Bundler + class Standalone + def initialize(groups, definition) + @specs = groups.empty? ? definition.requested_specs : definition.specs_for(groups.map(&:to_sym)) + 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 "# ruby 1.8.7 doesn't define RUBY_ENGINE" + file.puts "ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'" + file.puts "ruby_version = RbConfig::CONFIG[\"ruby_version\"]" + file.puts "path = File.expand_path('..', __FILE__)" + paths.each do |path| + file.puts %($:.unshift "\#{path}/#{path}") + end + end + end + + private + + def paths + @specs.map do |spec| + next if spec.name == "bundler" + Array(spec.require_paths).map do |path| + gem_path(path, spec).sub(version_dir, '#{ruby_engine}/#{ruby_version}') + # This is a static string intentionally. It's interpolated at a later time. + end + end.flatten + end + + def version_dir + "#{Bundler::RubyVersion.system.engine}/#{RbConfig::CONFIG["ruby_version"]}" + end + + def bundler_path + Bundler.root.join(Bundler.settings[:path], "bundler") + end + + def gem_path(path, spec) + full_path = Pathname.new(path).absolute? ? path : File.join(spec.full_gem_path, path) + Pathname.new(full_path).relative_path_from(Bundler.root.join(bundler_path)).to_s + rescue TypeError + error_message = "#{spec.name} #{spec.version} has an invalid gemspec" + raise Gem::InvalidSpecificationException.new(error_message) + end + end +end |