diff options
Diffstat (limited to 'lib/rubygems/installer.rb')
-rw-r--r-- | lib/rubygems/installer.rb | 370 |
1 files changed, 233 insertions, 137 deletions
diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 7af51056b7..8f6f9a5aa8 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/command' -require 'rubygems/installer_uninstaller_utils' -require 'rubygems/exceptions' -require 'rubygems/deprecate' -require 'rubygems/package' -require 'rubygems/ext' -require 'rubygems/user_interaction' +require_relative "installer_uninstaller_utils" +require_relative "exceptions" +require_relative "deprecate" +require_relative "package" +require_relative "ext" +require_relative "user_interaction" ## # The installer installs the files contained in the .gem into the Gem.home. @@ -68,19 +68,28 @@ class Gem::Installer @path_warning = false - @install_lock = Mutex.new - class << self - ## - # True if we've warned about PATH not including Gem.bindir + # + # Changes in rubygems to lazily loading `rubygems/command` (in order to + # lazily load `optparse` as a side effect) affect bundler's custom installer + # which uses `Gem::Command` without requiring it (up until bundler 2.2.29). + # This hook is to compensate for that missing require. + # + # TODO: Remove when rubygems no longer supports running on bundler older + # than 2.2.29. - attr_accessor :path_warning + def inherited(klass) + if klass.name == "Bundler::RubyGemsGemInstaller" + require "rubygems/command" + end + + super(klass) + end ## - # Certain aspects of the install process are not thread-safe. This lock is - # used to allow multiple threads to install Gems at the same time. + # True if we've warned about PATH not including Gem.bindir - attr_reader :install_lock + attr_accessor :path_warning ## # Overrides the executable format. @@ -117,14 +126,14 @@ class Gem::Installer @spec = spec end - def extract_files(destination_dir, pattern = '*') + def extract_files(destination_dir, pattern = "*") FileUtils.mkdir_p destination_dir spec.files.each do |file| file = File.join destination_dir, file next if File.exist? file FileUtils.mkdir_p File.dirname(file) - File.open file, 'w' do |fp| + File.open file, "w" do |fp| fp.puts "# #{file}" end end @@ -169,7 +178,7 @@ class Gem::Installer # :post_install_message:: Print gem post install message if true def initialize(package, options={}) - require 'fileutils' + require "fileutils" @options = options @package = package @@ -180,10 +189,12 @@ class Gem::Installer @package.prog_mode = options[:prog_mode] @package.data_mode = options[:data_mode] - if options[:user_install] - @gem_home = Gem.user_dir - @bin_dir = Gem.bindir gem_home unless options[:bin_dir] - @plugins_dir = Gem.plugindir(gem_home) + if @gem_home == Gem.user_dir + # If we get here, then one of the following likely happened: + # - `--user-install` was specified + # - `Gem::PathSupport#home` fell back to `Gem.user_dir` + # - GEM_HOME was manually set to `Gem.user_dir` + check_that_user_bin_dir_is_in_path end end @@ -193,8 +204,8 @@ class Gem::Installer # # If +@force+ is set +filename+ is overwritten. # - # If +filename+ exists and is a RubyGems wrapper for different gem the user - # is consulted. + # If +filename+ exists and it is a RubyGems wrapper for a different gem, then + # the user is consulted. # # If +filename+ exists and +@bin_dir+ is Gem.default_bindir (/usr/local) the # user is consulted. @@ -211,34 +222,45 @@ class Gem::Installer ruby_executable = false existing = nil - File.open generated_bin, 'rb' do |io| - next unless io.gets =~ /^#!/ # shebang + File.open generated_bin, "rb" do |io| + line = io.gets + shebang = /^#!.*ruby/o + + # TruffleRuby uses a bash prelude in default launchers + if load_relative_enabled? || RUBY_ENGINE == "truffleruby" + until line.nil? || shebang.match?(line) do + line = io.gets + end + end + + next unless line&.match?(shebang) + io.gets # blankline - # TODO detect a specially formatted comment instead of trying - # to run a regexp against Ruby code. - next unless io.gets =~ /This file was generated by RubyGems/ + # TODO: detect a specially formatted comment instead of trying + # to find a string inside Ruby code. + next unless io.gets&.include?("This file was generated by RubyGems") ruby_executable = true - existing = io.read.slice(%r{ + existing = io.read.slice(/ ^\s*( gem \s | load \s Gem\.bin_path\( | load \s Gem\.activate_bin_path\( ) (['"])(.*?)(\2), - }x, 3) + /x, 3) end return if spec.name == existing # somebody has written to RubyGems' directory, overwrite, too bad - return if Gem.default_bindir != @bin_dir and not ruby_executable + return if Gem.default_bindir != @bin_dir && !ruby_executable question = "#{spec.name}'s executable \"#{filename}\" conflicts with ".dup if ruby_executable - question << (existing || 'an unknown executable') + question << (existing || "an unknown executable") return if ask_yes_no "#{question}\nOverwrite the executable?", false @@ -267,8 +289,6 @@ class Gem::Installer def spec @package.spec - rescue Gem::Package::Error => e - raise Gem::InstallError, "invalid gem: #{e.message}" end ## @@ -285,8 +305,6 @@ class Gem::Installer def install pre_install_checks - FileUtils.rm_f File.join gem_home, 'specifications', spec.spec_name - run_pre_install_hooks # Set loaded_from to ensure extension_dir is correct @@ -301,7 +319,7 @@ class Gem::Installer FileUtils.rm_rf spec.extension_dir dir_mode = options[:dir_mode] - FileUtils.mkdir_p gem_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755 if @options[:install_as_default] extract_bin @@ -326,39 +344,37 @@ class Gem::Installer say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? - Gem::Installer.install_lock.synchronize { Gem::Specification.reset } + Gem::Specification.add_spec(spec) + + load_plugin run_post_install_hooks spec - - # TODO This rescue is in the wrong place. What is raising this exception? - # move this rescue to around the code that actually might raise it. - rescue Zlib::GzipFile::Error - raise Gem::InstallError, "gzip error installing #{gem}" + rescue Errno::EACCES => e + # Permission denied - /path/to/foo + raise Gem::FilePermissionError, e.message.split(" - ").last end def run_pre_install_hooks # :nodoc: Gem.pre_install_hooks.each do |hook| - if hook.call(self) == false - location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ + next unless hook.call(self) == false + location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ - message = "pre-install hook#{location} failed for #{spec.full_name}" - raise Gem::InstallError, message - end + message = "pre-install hook#{location} failed for #{spec.full_name}" + raise Gem::InstallError, message end end def run_post_build_hooks # :nodoc: Gem.post_build_hooks.each do |hook| - if hook.call(self) == false - FileUtils.rm_rf gem_dir + next unless hook.call(self) == false + FileUtils.rm_rf gem_dir - location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ + location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ - message = "post-build hook#{location} failed for #{spec.full_name}" - raise Gem::InstallError, message - end + message = "post-build hook#{location} failed for #{spec.full_name}" + raise Gem::InstallError, message end end @@ -374,11 +390,11 @@ class Gem::Installer # we'll be installing into. def installed_specs - @specs ||= begin + @installed_specs ||= begin specs = [] Gem::Util.glob_files_in_dir("*.gemspec", File.join(gem_home, "specifications")).each do |path| - spec = Gem::Specification.load path.tap(&Gem::UNTAINT) + spec = Gem::Specification.load path specs << spec if spec end @@ -404,10 +420,10 @@ class Gem::Installer # True if the gems in the system satisfy +dependency+. def installation_satisfies_dependency?(dependency) - return true if @options[:development] and dependency.type == :development + return true if @options[:development] && dependency.type == :development return true if installed_specs.detect {|s| dependency.matches_spec? s } return false if @only_install_dir - not dependency.matching_specs.empty? + !dependency.matching_specs.empty? end ## @@ -440,23 +456,20 @@ class Gem::Installer # specifications directory. def write_spec - File.open spec_file, 'w' do |file| - spec.installed_by_version = Gem.rubygems_version - - file.puts spec.to_ruby_for_cache + spec.installed_by_version = Gem.rubygems_version - file.fsync rescue nil # for filesystems without fsync(2) - end + Gem.write_binary(spec_file, spec.to_ruby_for_cache) end ## # Writes the full .gemspec specification (in Ruby) to the gem home's # specifications/default directory. + # + # In contrast to #write_spec, this keeps file lists, so the `gem contents` + # command works. def write_default_spec - File.open(default_spec_file, "w") do |file| - file.puts spec.to_ruby - end + Gem.write_binary(default_spec_file, spec.to_ruby) end ## @@ -466,7 +479,7 @@ class Gem::Installer if Gem.win_platform? script_name = formatted_program_filename(filename) + ".bat" script_path = File.join bindir, File.basename(script_name) - File.open script_path, 'w' do |file| + File.open script_path, "w" do |file| file.puts windows_stub_script(bindir, filename) end @@ -475,28 +488,19 @@ class Gem::Installer end def generate_bin # :nodoc: - return if spec.executables.nil? or spec.executables.empty? + return if spec.executables.nil? || spec.executables.empty? ensure_writable_dir @bin_dir spec.executables.each do |filename| - filename.tap(&Gem::UNTAINT) bin_path = File.join gem_dir, spec.bindir, filename - - unless File.exist? bin_path - if File.symlink? bin_path - alert_warning "`#{bin_path}` is dangling symlink pointing to `#{File.readlink bin_path}`" - else - alert_warning "`#{bin_path}` does not exist, maybe `gem pristine #{spec.name}` will fix it?" - end - next - end + next unless File.exist? bin_path mode = File.stat(bin_path).mode - dir_mode = options[:prog_mode] || (mode | 0111) + dir_mode = options[:prog_mode] || (mode | 0o111) unless dir_mode == mode - require 'fileutils' + require "fileutils" FileUtils.chmod dir_mode, bin_path end @@ -511,7 +515,7 @@ class Gem::Installer end def generate_plugins # :nodoc: - latest = Gem::Installer.install_lock.synchronize { Gem::Specification.latest_spec_for(spec.name) } + latest = Gem::Specification.latest_spec_for(spec.name) return if latest && latest.version > spec.version ensure_writable_dir @plugins_dir @@ -521,6 +525,8 @@ class Gem::Installer else regenerate_plugins_for(spec, @plugins_dir) end + rescue ArgumentError => e + raise e, "#{latest.name} #{latest.version} #{spec.name} #{spec.version}: #{e.message}" end ## @@ -528,17 +534,17 @@ class Gem::Installer #-- # The Windows script is generated in addition to the regular one due to a # bug or misfeature in the Windows shell's pipe. See - # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379 + # https://blade.ruby-lang.org/ruby-talk/193379 def generate_bin_script(filename, bindir) bin_script_path = File.join bindir, formatted_program_filename(filename) - require 'fileutils' + require "fileutils" FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers - File.open bin_script_path, 'wb', 0755 do |file| + File.open bin_script_path, "wb", 0o755 do |file| file.print app_script_text(filename) - file.chmod(options[:prog_mode] || 0755) + file.chmod(options[:prog_mode] || 0o755) end verbose bin_script_path @@ -557,13 +563,13 @@ class Gem::Installer if File.exist? dst if File.symlink? dst link = File.readlink(dst).split File::SEPARATOR - cur_version = Gem::Version.create(link[-3].sub(/^.*-/, '')) + cur_version = Gem::Version.create(link[-3].sub(/^.*-/, "")) return if spec.version < cur_version end File.unlink dst end - FileUtils.symlink src, dst, :verbose => Gem.configuration.really_verbose + FileUtils.symlink src, dst, verbose: Gem.configuration.really_verbose rescue NotImplementedError, SystemCallError alert_warning "Unable to use symlinks, installing wrapper" generate_bin_script filename, bindir @@ -585,9 +591,8 @@ class Gem::Installer # def shebang(bin_file_name) - ruby_name = RbConfig::CONFIG['ruby_install_name'] if @env_shebang path = File.join gem_dir, spec.bindir, bin_file_name - first_line = File.open(path, "rb") {|file| file.gets } || "" + first_line = File.open(path, "rb", &:gets) || "" if first_line.start_with?("#!") # Preserve extra words on shebang line, like "-w". Thanks RPA. @@ -598,7 +603,7 @@ class Gem::Installer if which = Gem.configuration[:custom_shebang] # replace bin_file_name with "ruby" to avoid endless loops - which = which.gsub(/ #{bin_file_name}$/," #{RbConfig::CONFIG['ruby_install_name']}") + which = which.gsub(/ #{bin_file_name}$/," #{ruby_install_name}") which = which.gsub(/\$(\w+)/) do case $1 @@ -614,14 +619,12 @@ class Gem::Installer end "#!#{which}" - elsif not ruby_name - "#!#{Gem.ruby}#{opts}" - elsif opts - "#!/bin/sh\n'exec' #{ruby_name.dump} '-x' \"$0\" \"$@\"\n#{shebang}" - else + elsif @env_shebang # Create a plain shebang line. @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path } - "#!#{@env_path} #{ruby_name}" + "#!#{@env_path} #{ruby_install_name}" + else + "#{bash_prolog_script}#!#{Gem.ruby}#{opts}" end end @@ -631,7 +634,6 @@ class Gem::Installer def ensure_loadable_spec ruby = spec.to_ruby_for_cache - ruby.tap(&Gem::UNTAINT) begin eval ruby @@ -652,36 +654,41 @@ class Gem::Installer def process_options # :nodoc: @options = { - :bin_dir => nil, - :env_shebang => false, - :force => false, - :only_install_dir => false, - :post_install_message => true, + bin_dir: nil, + env_shebang: false, + force: false, + only_install_dir: false, + post_install_message: true, }.merge options @env_shebang = options[:env_shebang] @force = options[:force] @install_dir = options[:install_dir] - @gem_home = options[:install_dir] || Gem.dir - @plugins_dir = Gem.plugindir(@gem_home) + @user_install = options[:user_install] @ignore_dependencies = options[:ignore_dependencies] @format_executable = options[:format_executable] @wrappers = options[:wrappers] @only_install_dir = options[:only_install_dir] + @bin_dir = options[:bin_dir] + @development = options[:development] + @build_root = options[:build_root] + + @build_args = options[:build_args] + + @gem_home = @install_dir || user_install_dir || Gem.dir + # If the user has asked for the gem to be installed in a directory that is # the system gem directory, then use the system bin directory, else create # (or use) a new bin dir under the gem_home. - @bin_dir = options[:bin_dir] || Gem.bindir(gem_home) - @development = options[:development] - @build_root = options[:build_root] + @bin_dir ||= Gem.bindir(@gem_home) - @build_args = options[:build_args] || Gem::Command.build_args + @plugins_dir = Gem.plugindir(@gem_home) unless @build_root.nil? - @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, '')) - @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, '')) - @plugins_dir = File.join(@build_root, @plugins_dir.gsub(/^[a-zA-Z]:/, '')) + @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, "")) + @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, "")) + @plugins_dir = File.join(@build_root, @plugins_dir.gsub(/^[a-zA-Z]:/, "")) alert_warning "You build with buildroot.\n Build root: #{@build_root}\n Bin dir: #{@bin_dir}\n Gem home: #{@gem_home}\n Plugins dir: #{@plugins_dir}" end end @@ -692,7 +699,7 @@ class Gem::Installer user_bin_dir = @bin_dir || Gem.bindir(gem_home) user_bin_dir = user_bin_dir.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR - path = ENV['PATH'] + path = ENV["PATH"] path = path.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR if Gem.win_platform? @@ -703,7 +710,7 @@ class Gem::Installer path = path.split(File::PATH_SEPARATOR) unless path.include? user_bin_dir - unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV['HOME'], '~')) + unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV["HOME"], "~")) alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run." self.class.path_warning = true end @@ -711,24 +718,27 @@ class Gem::Installer end def verify_gem_home # :nodoc: - FileUtils.mkdir_p gem_home, :mode => options[:dir_mode] && 0755 - raise Gem::FilePermissionError, gem_home unless File.writable?(gem_home) + FileUtils.mkdir_p gem_home, mode: options[:dir_mode] && 0o755 end def verify_spec - unless spec.name =~ Gem::Specification::VALID_NAME_PATTERN + unless Gem::Specification::VALID_NAME_PATTERN.match?(spec.name) raise Gem::InstallError, "#{spec} has an invalid name" end - if spec.raw_require_paths.any?{|path| path =~ /\R/ } + if spec.raw_require_paths.any? {|path| path =~ /\R/ } raise Gem::InstallError, "#{spec} has an invalid require_paths" end - if spec.extensions.any?{|ext| ext =~ /\R/ } + if spec.extensions.any? {|ext| ext =~ /\R/ } raise Gem::InstallError, "#{spec} has an invalid extensions" end - unless spec.specification_version.to_s =~ /\A\d+\z/ + if /\R/.match?(spec.platform.to_s) + raise Gem::InstallError, "#{spec.platform} is an invalid platform" + end + + unless /\A\d+\z/.match?(spec.specification_version.to_s) raise Gem::InstallError, "#{spec} has an invalid specification_version" end @@ -745,9 +755,9 @@ class Gem::Installer # Return the text for an application file. def app_script_text(bin_file_name) - # note that the `load` lines cannot be indented, as old RG versions match + # NOTE: that the `load` lines cannot be indented, as old RG versions match # against the beginning of the line - return <<-TEXT + <<-TEXT #{shebang bin_file_name} # # This file was generated by RubyGems. @@ -757,14 +767,14 @@ class Gem::Installer # require 'rubygems' - +#{gemdeps_load(spec.name)} version = "#{Gem::Requirement.default_prerelease}" str = ARGV.first if str str = str.b[/\\A_(.*)_\\z/, 1] if str and Gem::Version.correct?(str) - version = str + #{explicit_version_requirement(spec.name)} ARGV.shift end end @@ -778,18 +788,36 @@ end TEXT end + def gemdeps_load(name) + return "" if name == "bundler" + + <<-TEXT + +Gem.use_gemdeps +TEXT + end + + def explicit_version_requirement(name) + code = "version = str" + return code unless name == "bundler" + + code += <<-TEXT + + ENV['BUNDLER_VERSION'] = str +TEXT + end + ## # return the stub script text used to launch the true Ruby script def windows_stub_script(bindir, bin_file_name) - rb_config = RbConfig::CONFIG rb_topdir = RbConfig::TOPDIR || File.dirname(rb_config["bindir"]) # get ruby executable file name from RbConfig - ruby_exe = "#{rb_config['RUBY_INSTALL_NAME']}#{rb_config['EXEEXT']}" + ruby_exe = "#{rb_config["RUBY_INSTALL_NAME"]}#{rb_config["EXEEXT"]}" ruby_exe = "ruby.exe" if ruby_exe.empty? - if File.exist?(File.join bindir, ruby_exe) + if File.exist?(File.join(bindir, ruby_exe)) # stub & ruby.exe within same folder. Portable <<-TEXT @ECHO OFF @@ -797,7 +825,7 @@ TEXT TEXT elsif bindir.downcase.start_with? rb_topdir.downcase # stub within ruby folder, but not standard bin. Portable - require 'pathname' + require "pathname" from = Pathname.new bindir to = Pathname.new "#{rb_topdir}/bin" rel = to.relative_path_from from @@ -819,7 +847,7 @@ TEXT # configure scripts and rakefiles or mkrf_conf files. def build_extensions - builder = Gem::Ext::Builder.new spec, @build_args + builder = Gem::Ext::Builder.new spec, build_args builder.build_extensions end @@ -906,17 +934,17 @@ TEXT # extensions. def write_build_info_file - return if @build_args.empty? + return if build_args.empty? - build_info_dir = File.join gem_home, 'build_info' + build_info_dir = File.join gem_home, "build_info" dir_mode = options[:dir_mode] - FileUtils.mkdir_p build_info_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p build_info_dir, mode: dir_mode && 0o755 build_info_file = File.join build_info_dir, "#{spec.full_name}.info" - File.open build_info_file, 'w' do |io| - @build_args.each do |arg| + File.open build_info_file, "w" do |io| + build_args.each do |arg| io.puts arg end end @@ -928,17 +956,85 @@ TEXT # Writes the .gem file to the cache directory def write_cache_file - cache_file = File.join gem_home, 'cache', spec.file_name + cache_file = File.join gem_home, "cache", spec.file_name @package.copy_to cache_file end def ensure_writable_dir(dir) # :nodoc: begin - Dir.mkdir dir, *[options[:dir_mode] && 0755].compact + Dir.mkdir dir, *[options[:dir_mode] && 0o755].compact rescue SystemCallError raise unless File.directory? dir end raise Gem::FilePermissionError.new(dir) unless File.writable? dir end + + private + + def user_install_dir + # never install to user home in --build-root mode + return unless @build_root.nil? + + # Please note that @user_install might have three states: + # * `true`: `--user-install` + # * `false`: `--no-user-install` and + # * `nil`: option was not specified + if @user_install || (@user_install.nil? && Gem.default_user_install) + Gem.user_dir + end + end + + def build_args + @build_args ||= begin + require_relative "command" + Gem::Command.build_args + end + end + + def rb_config + RbConfig::CONFIG + end + + def ruby_install_name + rb_config["ruby_install_name"] + end + + def load_relative_enabled? + rb_config["LIBRUBY_RELATIVE"] == "yes" + end + + def bash_prolog_script + if load_relative_enabled? + script = +<<~EOS + bindir="${0%/*}" + EOS + + script << %(exec "$bindir/#{ruby_install_name}" "-x" "$0" "$@"\n) + + <<~EOS + #!/bin/sh + # -*- ruby -*- + _=_\\ + =begin + #{script.chomp} + =end + EOS + else + "" + end + end + + def load_plugin + specs = Gem::Specification.find_all_by_name(spec.name) + # If old version already exists, this plugin isn't loaded + # immediately. It's for avoiding a case that multiple versions + # are loaded at the same time. + return unless specs.size == 1 + + plugin_files = spec.plugins.map do |plugin| + File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + end + Gem.load_plugin_files(plugin_files) + end end |