diff options
Diffstat (limited to 'lib/rubygems.rb')
| -rw-r--r-- | lib/rubygems.rb | 346 |
1 files changed, 232 insertions, 114 deletions
diff --git a/lib/rubygems.rb b/lib/rubygems.rb index db9c73c248..d289cab0fd 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -8,15 +9,15 @@ require "rbconfig" module Gem - VERSION = "3.5.0.dev" + VERSION = "4.1.0.dev" end -# Must be first since it unloads the prelude from 1.9.2 -require_relative "rubygems/compatibility" - require_relative "rubygems/defaults" require_relative "rubygems/deprecate" require_relative "rubygems/errors" +require_relative "rubygems/target_rbconfig" +require_relative "rubygems/win_platform" +require_relative "rubygems/util/atomic_file_writer" ## # RubyGems is the Ruby standard for publishing and managing third party @@ -37,7 +38,7 @@ require_relative "rubygems/errors" # Further RubyGems documentation can be found at: # # * {RubyGems Guides}[https://guides.rubygems.org] -# * {RubyGems API}[https://www.rubydoc.info/github/rubygems/rubygems] (also available from +# * {RubyGems API}[https://guides.rubygems.org/rubygems-org-api/] (also available from # <tt>gem server</tt>) # # == RubyGems Plugins @@ -69,7 +70,7 @@ require_relative "rubygems/errors" # == Bugs # # You can submit bugs to the -# {RubyGems bug tracker}[https://github.com/rubygems/rubygems/issues] +# {RubyGems bug tracker}[https://github.com/ruby/rubygems/issues] # on GitHub # # == Credits @@ -105,7 +106,7 @@ require_relative "rubygems/errors" # # == License # -# See {LICENSE.txt}[rdoc-ref:lib/rubygems/LICENSE.txt] for permissions. +# See {LICENSE.txt}[https://github.com/ruby/rubygems/blob/master/LICENSE.txt] for permissions. # # Thanks! # @@ -114,23 +115,6 @@ require_relative "rubygems/errors" module Gem RUBYGEMS_DIR = __dir__ - # Taint support is deprecated in Ruby 2.7. - # This allows switching ".untaint" to ".tap(&Gem::UNTAINT)", - # to avoid deprecation warnings in Ruby 2.7. - UNTAINT = RUBY_VERSION < "2.7" ? :untaint.to_sym : proc {} - - ## - # An Array of Regexps that match windows Ruby platforms. - - WIN_PATTERNS = [ - /bccwin/i, - /cygwin/i, - /djgpp/i, - /mingw/i, - /mswin/i, - /wince/i, - ].freeze - GEM_DEP_FILES = %w[ gem.deps.rb gems.rb @@ -159,7 +143,12 @@ module Gem specifications/default ].freeze - @@win_platform = nil + ## + # The default value for SOURCE_DATE_EPOCH if not specified. + # We want a date after 1980-01-01, to prevent issues with Zip files. + # This particular timestamp is for 1980-01-02 00:00:00 GMT. + + DEFAULT_SOURCE_DATE_EPOCH = 315_619_200 @configuration = nil @gemdeps = nil @@ -183,6 +172,8 @@ module Gem @discover_gems_on_require = true + @target_rbconfig = nil + ## # Try to activate a gem containing +path+. Returns true if # activation succeeded or wasn't needed because it was already @@ -202,15 +193,17 @@ module Gem begin spec.activate rescue Gem::LoadError => e # this could fail due to gem dep collisions, go lax - spec_by_name = Gem::Specification.find_by_name(spec.name) - if spec_by_name.nil? + name = spec.name + spec = Gem::Specification.find_unloaded_by_path(path) + spec ||= Gem::Specification.find_by_name(name) + if spec.nil? raise e else - spec_by_name.activate + spec.activate end end - return true + true end def self.needs @@ -221,7 +214,7 @@ module Gem finish_resolve rs end - def self.finish_resolve(request_set=Gem::RequestSet.new) + def self.finish_resolve(request_set = Gem::RequestSet.new) request_set.import Gem::Specification.unresolved_deps.values request_set.import Gem.loaded_specs.values.map {|s| Gem::Dependency.new(s.name, s.version) } @@ -243,6 +236,16 @@ module Gem find_spec_for_exe(name, exec_name, requirements).bin_file exec_name end + def self.find_and_activate_spec_for_exe(name, exec_name, requirements) + spec = find_spec_for_exe name, exec_name, requirements + Gem::LOADED_SPECS_MUTEX.synchronize do + spec.activate + finish_resolve + end + spec + end + private_class_method :find_and_activate_spec_for_exe + def self.find_spec_for_exe(name, exec_name, requirements) raise ArgumentError, "you must supply exec_name" unless exec_name @@ -268,6 +271,42 @@ module Gem private_class_method :find_spec_for_exe ## + # Find and load the full path to the executable for gem +name+. If the + # +exec_name+ is not given, an exception will be raised, otherwise the + # specified executable's path is returned. +requirements+ allows + # you to specify specific gem versions. + # + # A side effect of this method is that it will activate the gem that + # contains the executable. + # + # This method should *only* be used in bin stub files. + + def self.activate_and_load_bin_path(name, exec_name = nil, *requirements) + spec = find_and_activate_spec_for_exe name, exec_name, requirements + + if spec.name == "bundler" + # Old versions of Bundler need a workaround to support nested `bundle + # exec` invocations by overriding `Gem.activate_bin_path`. However, + # RubyGems now uses this new `Gem.activate_and_load_bin_path` helper in + # binstubs, which is of course not overridden in Bundler since it didn't + # exist at the time. So, include the override here to workaround that. + load ENV["BUNDLE_BIN_PATH"] if ENV["BUNDLE_BIN_PATH"] && spec.version <= Gem::Version.create("2.5.22") + + # Make sure there's no version of Bundler in `$LOAD_PATH` that's different + # from the version we just activated. If that was the case (it happens + # when testing Bundler from ruby/ruby), we would load Bundler extensions + # to RubyGems from the copy in `$LOAD_PATH` but then load the binstub from + # an installed copy, causing those copies to be mixed and yet more + # redefinition warnings. + # + require_path = $LOAD_PATH.resolve_feature_path("bundler").last.delete_suffix("/bundler.rb") + Gem.load_bundler_extensions(spec.version) if spec.full_require_paths.include?(require_path) + end + + load spec.bin_file(exec_name) + end + + ## # Find the full path to the executable for gem +name+. If the +exec_name+ # is not given, an exception will be raised, otherwise the # specified executable's path is returned. +requirements+ allows @@ -279,12 +318,7 @@ module Gem # This method should *only* be used in bin stub files. def self.activate_bin_path(name, exec_name = nil, *requirements) # :nodoc: - spec = find_spec_for_exe name, exec_name, requirements - Gem::LOADED_SPECS_MUTEX.synchronize do - spec.activate - finish_resolve - end - spec.bin_file exec_name + find_and_activate_spec_for_exe(name, exec_name, requirements).bin_file exec_name end ## @@ -297,7 +331,7 @@ module Gem ## # The path where gem executables are to be installed. - def self.bindir(install_dir=Gem.dir) + def self.bindir(install_dir = Gem.dir) return File.join install_dir, "bin" unless install_dir.to_s == Gem.default_dir.to_s Gem.default_bindir @@ -306,7 +340,7 @@ module Gem ## # The path were rubygems plugins are to be installed. - def self.plugindir(install_dir=Gem.dir) + def self.plugindir(install_dir = Gem.dir) File.join install_dir, "plugins" end @@ -318,6 +352,8 @@ module Gem def self.clear_paths @paths = nil @user_home = nil + @cache_home = nil + @data_home = nil Gem::Specification.reset Gem::Security.reset if defined?(Gem::Security) end @@ -401,6 +437,23 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## + # The RbConfig object for the deployment target platform. + # + # This is usually the same as the running platform, but may be + # different if you are cross-compiling. + + def self.target_rbconfig + @target_rbconfig || Gem::TargetRbConfig.for_running_ruby + end + + def self.set_target_rbconfig(rbconfig_path) + @target_rbconfig = Gem::TargetRbConfig.from_path(rbconfig_path) + Gem::Platform.local(refresh: true) + Gem.platforms << Gem::Platform.local unless Gem.platforms.include? Gem::Platform.local + @target_rbconfig + end + + ## # Quietly ensure the Gem directory +dir+ contains all the proper # subdirectories. If we can't create a directory due to a permission # problem, then we will silently continue. @@ -428,7 +481,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} def self.ensure_subdirectories(dir, mode, subdirs) # :nodoc: old_umask = File.umask - File.umask old_umask | 002 + File.umask old_umask | 0o002 options = {} @@ -454,7 +507,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # distinction as extensions cannot be shared between the two. def self.extension_api_version # :nodoc: - if RbConfig::CONFIG["ENABLE_SHARED"] == "no" + if target_rbconfig["ENABLE_SHARED"] == "no" "#{ruby_api_version}-static" else ruby_api_version @@ -473,29 +526,29 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Note that find_files will return all files even if they are from different # versions of the same gem. See also find_latest_files - def self.find_files(glob, check_load_path=true) + def self.find_files(glob, check_load_path = true) files = [] files = find_files_from_load_path glob if check_load_path gem_specifications = @gemdeps ? Gem.loaded_specs.values : Gem::Specification.stubs - files.concat gem_specifications.map {|spec| + files.concat gem_specifications.flat_map {|spec| spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") - }.flatten + } # $LOAD_PATH might contain duplicate entries or reference # the spec dirs directly, so we prune. files.uniq! if check_load_path - return files + files end def self.find_files_from_load_path(glob) # :nodoc: glob_with_suffixes = "#{glob}#{Gem.suffix_pattern}" - $LOAD_PATH.map do |load_path| + $LOAD_PATH.flat_map do |load_path| Gem::Util.glob_files_in_dir(glob_with_suffixes, load_path) - end.flatten.select {|file| File.file? file.tap(&Gem::UNTAINT) } + end.select {|file| File.file? file } end ## @@ -510,20 +563,20 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Unlike find_files, find_latest_files will return only files from the # latest version of a gem. - def self.find_latest_files(glob, check_load_path=true) + def self.find_latest_files(glob, check_load_path = true) files = [] files = find_files_from_load_path glob if check_load_path - files.concat Gem::Specification.latest_specs(true).map {|spec| + files.concat Gem::Specification.latest_specs(true).flat_map {|spec| spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") - }.flatten + } # $LOAD_PATH might contain duplicate entries or reference # the spec dirs directly, so we prune. files.uniq! if check_load_path - return files + files end ## @@ -571,7 +624,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} ## # The number of paths in the +$LOAD_PATH+ from activated gems. Used to - # prioritize +-I+ and +ENV['RUBYLIB']+ entries during +require+. + # prioritize +-I+ and <code>ENV['RUBYLIB']</code> entries during +require+. def self.activated_gem_paths @activated_gem_paths ||= 0 @@ -588,6 +641,14 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end @yaml_loaded = false + @use_psych = nil + + ## + # Returns true if the Psych YAML parser is enabled via configuration. + + def self.use_psych? + @use_psych || false + end ## # Loads YAML, preferring Psych @@ -595,14 +656,54 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} def self.load_yaml return if @yaml_loaded - require "psych" - require_relative "rubygems/psych_tree" + @use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || + (defined?(@configuration) && @configuration && !@configuration[:use_psych].nil?) + if @use_psych + require "psych" + require_relative "rubygems/psych_tree" + end + + require_relative "rubygems/yaml_serializer" require_relative "rubygems/safe_yaml" @yaml_loaded = true end + @safe_marshal_loaded = false + + def self.load_safe_marshal + return if @safe_marshal_loaded + + require_relative "rubygems/safe_marshal" + + @safe_marshal_loaded = true + end + + ## + # Load Bundler extensions to RubyGems, making sure to avoid redefinition + # warnings in platform constants + + def self.load_bundler_extensions(version) + return unless version <= Gem::Version.create("2.6.9") + + previous_platforms = {} + + platform_const_list = ["JAVA", "MSWIN", "MSWIN64", "MINGW", "X64_MINGW_LEGACY", "X64_MINGW", "UNIVERSAL_MINGW", "WINDOWS", "X64_LINUX", "X64_LINUX_MUSL"] + + platform_const_list.each do |platform| + previous_platforms[platform] = Gem::Platform.const_get(platform) + Gem::Platform.send(:remove_const, platform) + end + + require "bundler/rubygems_ext" + + platform_const_list.each do |platform| + Gem::Platform.send(:remove_const, platform) if Gem::Platform.const_defined?(platform) + Gem::Platform.const_set(platform, previous_platforms[platform]) + end + end + ## # The file name and line number of the caller of the caller of this method. # @@ -747,48 +848,58 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Safely read a file in binary mode on all platforms. def self.read_binary(path) - open_file(path, "rb+") do |io| - io.read - end - rescue Errno::EACCES, Errno::EROFS - open_file(path, "rb") do |io| - io.read - end + File.binread(path) end ## - # Safely write a file in binary mode on all platforms. + # Atomically write a file in binary mode on all platforms. + def self.write_binary(path, data) - open_file(path, "wb") do |io| - io.write data + Gem::AtomicFileWriter.open(path) do |file| + file.write(data) end - rescue Errno::ENOSPC - # If we ran out of space but the file exists, it's *guaranteed* to be corrupted. - File.delete(path) if File.exist?(path) - raise end ## - # Open a file with given flags, and on Windows protect access with flock + # Open a file with given flags def self.open_file(path, flags, &block) - File.open(path, flags) do |io| - if !java_platform? && win_platform? - begin + File.open(path, flags, &block) + end + + ## + # Open a file with given flags, and protect access with a file lock + + def self.open_file_with_lock(path, &block) + file_lock = "#{path}.lock" + open_file_with_flock(file_lock, &block) + ensure + require "fileutils" + FileUtils.rm_f file_lock + end + + ## + # Open a file with given flags, and protect access with flock + + def self.open_file_with_flock(path, &block) + # read-write mode is used rather than read-only in order to support NFS + mode = IO::RDWR | IO::APPEND | IO::CREAT | IO::BINARY + mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE) + + File.open(path, mode) do |io| + begin + # Try to get a lock without blocking. + # If we do, the file is locked. + # Otherwise, explain why we're waiting and get a lock, but block this time. + if io.flock(File::LOCK_EX | File::LOCK_NB) != 0 + warn "Waiting for another process to let go of lock: #{path}" io.flock(File::LOCK_EX) - rescue Errno::ENOSYS, Errno::ENOTSUP end + io.puts(Process.pid) + rescue Errno::ENOSYS, Errno::ENOTSUP end yield io end - rescue Errno::ENOLCK # NFS - if Thread.main != Thread.current - raise - else - File.open(path, flags) do |io| - yield io - end - end end ## @@ -798,7 +909,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} if @ruby.nil? @ruby = RbConfig.ruby - @ruby = "\"#{@ruby}\"" if @ruby =~ /\s/ + @ruby = "\"#{@ruby}\"" if /\s/.match?(@ruby) end @ruby @@ -808,7 +919,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Returns a String containing the API compatibility version of Ruby def self.ruby_api_version - @ruby_api_version ||= RbConfig::CONFIG["ruby_version"].dup + @ruby_api_version ||= target_rbconfig["ruby_version"].dup end def self.env_requirement(gem_name) @@ -941,6 +1052,13 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## + # Suffixes for dynamic library require-able paths. + + def self.dynamic_library_suffixes + @dynamic_library_suffixes ||= suffixes - [".rb"] + end + + ## # Prints the amount of time the supplied block takes to run using the debug # UI output. @@ -951,7 +1069,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} elapsed = Time.now - now - ui.say "%2$*1$s: %3$3.3fs" % [-width, msg, elapsed] if display + ui.say format("%2$*1$s: %3$3.3fs", -width, msg, elapsed) if display value end @@ -978,18 +1096,6 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## - # Is this a windows platform? - - def self.win_platform? - if @@win_platform.nil? - ruby_platform = RbConfig::CONFIG["host_os"] - @@win_platform = !WIN_PATTERNS.find {|r| ruby_platform =~ r }.nil? - end - - @@win_platform - end - - ## # Is this a java platform? def self.java_platform? @@ -1004,6 +1110,13 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## + # Is this platform FreeBSD + + def self.freebsd_platform? + RbConfig::CONFIG["host_os"].to_s.include?("bsd") + end + + ## # Load +plugins+ as Ruby files def self.load_plugin_files(plugins) # :nodoc: @@ -1011,11 +1124,11 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Skip older versions of the GemCutter plugin: Its commands are in # RubyGems proper now. - next if plugin =~ /gemcutter-0\.[0-3]/ + next if /gemcutter-0\.[0-3]/.match?(plugin) begin load plugin - rescue ::Exception => e + rescue ScriptError, StandardError => e details = "#{plugin.inspect}: #{e.message} (#{e.class})" warn "Error loading RubyGems plugin #{details}" end @@ -1077,8 +1190,6 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end end - path.tap(&Gem::UNTAINT) - unless File.file? path return unless raise_exception @@ -1105,8 +1216,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} ## # If the SOURCE_DATE_EPOCH environment variable is set, returns it's value. - # Otherwise, returns the time that +Gem.source_date_epoch_string+ was - # first called in the same format as SOURCE_DATE_EPOCH. + # Otherwise, returns DEFAULT_SOURCE_DATE_EPOCH as a string. # # NOTE(@duckinator): The implementation is a tad weird because we want to: # 1. Make builds reproducible by default, by having this function always @@ -1121,15 +1231,12 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # https://reproducible-builds.org/specs/source-date-epoch/ def self.source_date_epoch_string - # The value used if $SOURCE_DATE_EPOCH is not set. - @default_source_date_epoch ||= Time.now.to_i.to_s - specified_epoch = ENV["SOURCE_DATE_EPOCH"] # If it's empty or just whitespace, treat it like it wasn't set at all. specified_epoch = nil if !specified_epoch.nil? && specified_epoch.strip.empty? - epoch = specified_epoch || @default_source_date_epoch + epoch = specified_epoch || DEFAULT_SOURCE_DATE_EPOCH.to_s epoch.strip end @@ -1140,7 +1247,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # This is used throughout RubyGems for enabling reproducible builds. def self.source_date_epoch - Time.at(self.source_date_epoch_string.to_i).utc.freeze + Time.at(source_date_epoch_string.to_i).utc.freeze end # FIX: Almost everywhere else we use the `def self.` way of defining class @@ -1193,10 +1300,17 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} prefix_pattern = /^(#{prefix_group})/ end + native_extension_suffixes = Gem.dynamic_library_suffixes.reject(&:empty?) + spec.files.each do |file| if new_format file = file.sub(prefix_pattern, "") - next unless $~ + unless $~ + # Also register native extension files (e.g. date_core.bundle) + # that are listed without require path prefix in the gemspec + next if file.include?("/") + next unless file.end_with?(*native_extension_suffixes) + end end spec.activate if already_loaded?(file) @@ -1209,9 +1323,16 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} ## # Find a Gem::Specification of default gem from +path+ + def find_default_spec(path) + @path_to_default_spec_map[path] + end + + ## + # Find an unresolved Gem::Specification of default gem from +path+ + def find_unresolved_default_spec(path) default_spec = @path_to_default_spec_map[path] - return default_spec if default_spec && loaded_specs[default_spec.name] != default_spec + default_spec if default_spec && loaded_specs[default_spec.name] != default_spec end ## @@ -1287,9 +1408,10 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} ## # Location of Marshal quick gemspecs on remote repositories - MARSHAL_SPEC_DIR = "quick/Marshal.#{Gem.marshal_version}/" + MARSHAL_SPEC_DIR = "quick/Marshal.#{Gem.marshal_version}/".freeze autoload :ConfigFile, File.expand_path("rubygems/config_file", __dir__) + autoload :CIDetector, File.expand_path("rubygems/ci_detector", __dir__) autoload :Dependency, File.expand_path("rubygems/dependency", __dir__) autoload :DependencyList, File.expand_path("rubygems/dependency_list", __dir__) autoload :Installer, File.expand_path("rubygems/installer", __dir__) @@ -1312,9 +1434,7 @@ require_relative "rubygems/specification" # REFACTOR: This should be pulled out into some kind of hacks file. begin - ## # Defaults the operating system (or packager) wants to provide for RubyGems. - require "rubygems/defaults/operating_system" rescue LoadError # Ignored @@ -1329,9 +1449,7 @@ rescue StandardError => e end begin - ## # Defaults the Ruby implementation wants to provide for RubyGems - require "rubygems/defaults/#{RUBY_ENGINE}" rescue LoadError end @@ -1345,7 +1463,7 @@ require_relative "rubygems/core_ext/kernel_gem" path = File.join(__dir__, "rubygems/core_ext/kernel_require.rb") # When https://bugs.ruby-lang.org/issues/17259 is available, there is no need to override Kernel#warn if RUBY_ENGINE == "truffleruby" || - (RUBY_ENGINE == "ruby" && RUBY_VERSION >= "3.0") + RUBY_ENGINE == "ruby" file = "<internal:#{path}>" else require_relative "rubygems/core_ext/kernel_warn" |
