diff options
Diffstat (limited to 'lib/rubygems/commands')
37 files changed, 845 insertions, 442 deletions
diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 1dd4de8c48..cfe1f8ec3c 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true + require_relative "../command" +require_relative "../gemspec_helpers" require_relative "../package" require_relative "../version_option" class Gem::Commands::BuildCommand < Gem::Command include Gem::VersionOption + include Gem::GemspecHelpers def initialize super "build", "Build a gem from a gemspec" @@ -22,13 +25,6 @@ class Gem::Commands::BuildCommand < Gem::Command add_option "-o", "--output FILE", "output gem with the given filename" do |value, options| options[:output] = value end - - add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options| - options[:build_path] = value - end - deprecate_option "-C", - version: "4.0", - extra_msg: "-C is a global flag now. Use `gem -C PATH build GEMSPEC_FILE [options]` instead" end def arguments # :nodoc: @@ -74,17 +70,6 @@ Gems can be saved to a specified filename with the output option: private - def find_gemspec(glob = "*.gemspec") - gemspecs = Dir.glob(glob).sort - - if gemspecs.size > 1 - alert_error "Multiple gemspecs found: #{gemspecs}, please specify one" - terminate_interaction(1) - end - - gemspecs.first - end - def build_gem gemspec = resolve_gem_name diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index e7fcfdca4e..fe03841ddb 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../security" class Gem::Commands::CertCommand < Gem::Command def initialize super "cert", "Manage RubyGems certificates and signing settings", - :add => [], :remove => [], :list => [], :build => [], :sign => [] + add: [], remove: [], list: [], build: [], sign: [] add_option("-a", "--add CERT", "Add a trusted certificate.") do |cert_file, options| @@ -157,7 +158,7 @@ class Gem::Commands::CertCommand < Gem::Command cert = Gem::Security.create_cert_email( email, key, - (Gem::Security::ONE_DAY * expiration_length_days) + Gem::Security::ONE_DAY * expiration_length_days ) Gem::Security.write cert, "gem-public_cert.pem" @@ -177,9 +178,9 @@ class Gem::Commands::CertCommand < Gem::Command algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM key = Gem::Security.create_key(algorithm) - key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase + key_path = Gem::Security.write key, "gem-private_key.pem", 0o600, passphrase - return key, key_path + [key, key_path] end def certificates_matching(filter) @@ -290,7 +291,7 @@ For further reading on signing gems see `ri Gem::Security`. cert = File.read cert_file cert = OpenSSL::X509::Certificate.new cert - permissions = File.stat(cert_file).mode & 0777 + permissions = File.stat(cert_file).mode & 0o777 issuer_cert = options[:issuer_cert] issuer_key = options[:key] diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index 4cf50ffc1e..fb23dd9cb4 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../validator" @@ -9,7 +10,7 @@ class Gem::Commands::CheckCommand < Gem::Command def initialize super "check", "Check a gem repository for added or missing files", - :alien => true, :doctor => false, :dry_run => false, :gems => true + alien: true, doctor: false, dry_run: false, gems: true add_option("-a", "--[no-]alien", 'Report "unmanaged" or rogue files in the', diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 0f53227d9a..c89a24eee9 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../dependency_list" require_relative "../uninstaller" @@ -7,8 +8,8 @@ class Gem::Commands::CleanupCommand < Gem::Command def initialize super "cleanup", "Clean up old versions of installed gems", - :force => false, :install_dir => Gem.dir, - :check_dev => true + force: false, install_dir: Gem.dir, + check_dev: true add_option("-n", "-d", "--dry-run", "Do not uninstall gems") do |_value, options| @@ -37,8 +38,6 @@ class Gem::Commands::CleanupCommand < Gem::Command @default_gems = [] @full = nil @gems_to_cleanup = nil - @original_home = nil - @original_path = nil @primary_gems = nil end @@ -74,7 +73,7 @@ If no gems are named all gems in GEM_HOME are cleaned. until done do clean_gems - this_set = @gems_to_cleanup.map {|spec| spec.full_name }.sort + this_set = @gems_to_cleanup.map(&:full_name).sort done = this_set.empty? || last_set == this_set @@ -87,16 +86,13 @@ If no gems are named all gems in GEM_HOME are cleaned. say "Clean up complete" verbose do - skipped = @default_gems.map {|spec| spec.full_name } + skipped = @default_gems.map(&:full_name) "Skipped default gems: #{skipped.join ", "}" end end def clean_gems - @original_home = Gem.dir - @original_path = Gem.path - get_primary_gems get_candidate_gems get_gems_to_cleanup @@ -111,17 +107,15 @@ If no gems are named all gems in GEM_HOME are cleaned. deps.reverse_each do |spec| uninstall_dep spec end - - Gem::Specification.reset end def get_candidate_gems @candidate_gems = if options[:args].empty? Gem::Specification.to_a else - options[:args].map do |gem_name| + options[:args].flat_map do |gem_name| Gem::Specification.find_all_by_name gem_name - end.flatten + end end end @@ -130,11 +124,9 @@ If no gems are named all gems in GEM_HOME are cleaned. @primary_gems[spec.name].version != spec.version end - default_gems, gems_to_cleanup = gems_to_cleanup.partition do |spec| - spec.default_gem? - end + default_gems, gems_to_cleanup = gems_to_cleanup.partition(&:default_gem?) - uninstall_from = options[:user_install] ? Gem.user_dir : @original_home + uninstall_from = options[:user_install] ? Gem.user_dir : Gem.dir gems_to_cleanup = gems_to_cleanup.select do |spec| spec.base_dir == uninstall_from @@ -167,8 +159,8 @@ If no gems are named all gems in GEM_HOME are cleaned. say "Attempting to uninstall #{spec.full_name}" uninstall_options = { - :executables => false, - :version => "= #{spec.version}", + executables: false, + version: "= #{spec.version}", } uninstall_options[:user_install] = Gem.user_dir == spec.base_dir @@ -182,8 +174,5 @@ If no gems are named all gems in GEM_HOME are cleaned. say "Unable to uninstall #{spec.full_name}:" say "\t#{e.class}: #{e.message}" end - ensure - # Restore path Gem::Uninstaller may have changed - Gem.use_paths @original_home, *@original_path end end diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index 314b50e93e..d4f9871868 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" @@ -7,8 +8,8 @@ class Gem::Commands::ContentsCommand < Gem::Command def initialize super "contents", "Display the contents of the installed gems", - :specdirs => [], :lib_only => false, :prefix => true, - :show_install_dir => false + specdirs: [], lib_only: false, prefix: true, + show_install_dir: false add_version_option @@ -93,7 +94,7 @@ prefix or only the files that are requireable. gem_path = spec.full_gem_path extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only] glob = "#{gem_path}#{extra}/**/*" - prefix_re = /#{Regexp.escape(gem_path)}\// + prefix_re = %r{#{Regexp.escape(gem_path)}/} Dir[glob].map do |file| [gem_path, file.sub(prefix_re, "")] @@ -101,15 +102,22 @@ prefix or only the files that are requireable. end def files_in_default_gem(spec) - spec.files.map do |file| - case file - when /\A#{spec.bindir}\// - # $' is POSTMATCH - [RbConfig::CONFIG["bindir"], $'] - when /\.so\z/ - [RbConfig::CONFIG["archdir"], file] + spec.files.filter_map do |file| + if file.start_with?("#{spec.bindir}/") + [RbConfig::CONFIG["bindir"], file.delete_prefix("#{spec.bindir}/")] else - [RbConfig::CONFIG["rubylibdir"], file] + gem spec.name, spec.version + + require_path = spec.require_paths.find do |path| + file.start_with?("#{path}/") + end + + requirable_part = file.delete_prefix("#{require_path}/") + + resolve = $LOAD_PATH.resolve_feature_path(requirable_part)&.last + next unless resolve + + [resolve.delete_suffix(requirable_part), requirable_part] end end end @@ -177,12 +185,12 @@ prefix or only the files that are requireable. @spec_dirs.sort.each {|dir| say dir } end - return nil + nil end def specification_directories # :nodoc: - options[:specdirs].map do |i| + options[:specdirs].flat_map do |i| [i, File.join(i, "specifications")] - end.flatten + end end end diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb index a5870e9106..9aaefae999 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -10,7 +11,7 @@ class Gem::Commands::DependencyCommand < Gem::Command def initialize super "dependency", "Show the dependencies of an installed gem", - :version => Gem::Requirement.default, :domain => :local + version: Gem::Requirement.default, domain: :local add_version_option add_platform_option @@ -89,10 +90,9 @@ use with other commands. def display_pipe(specs) # :nodoc: specs.each do |spec| - unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| - say "#{dep.name} --version '#{dep.requirement}'" - end + next if spec.dependencies.empty? + spec.dependencies.sort_by(&:name).each do |dep| + say "#{dep.name} --version '#{dep.requirement}'" end end end @@ -152,7 +152,7 @@ use with other commands. response = String.new response << " " * level + "Gem #{spec.full_name}\n" unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| + spec.dependencies.sort_by(&:name).each do |dep| response << " " * level + " #{dep}\n" end end diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index d1b7d07a1d..a5eb521a53 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::EnvironmentCommand < Gem::Command @@ -14,9 +15,10 @@ class Gem::Commands::EnvironmentCommand < Gem::Command version display the gem format version remotesources display the remote gem servers platform display the supported gem platforms + credentials display the path where credentials are stored <omitted> display everything EOF - return args.gsub(/^\s+/, "") + args.gsub(/^\s+/, "") end def description # :nodoc: @@ -36,6 +38,7 @@ keys: :verbose: Verbosity of the gem command. false, true, and :really are the levels :update_sources: Enable/disable automatic updating of repository metadata + :concurrent_downloads: The number of gem downloads to perform concurrently :backtrace: Print backtrace when RubyGems encounters an error :gempath: The paths in which to look for gems :disable_default_gem_server: Force specification of gem server host on push @@ -87,6 +90,8 @@ lib/rubygems/defaults/operating_system.rb Gem.sources.to_a.join("\n") when /^platform/ then Gem.platforms.join(File::PATH_SEPARATOR) + when /^credentials/, /^creds/ then + Gem.configuration.credentials_path when nil then show_environment else @@ -113,6 +118,8 @@ lib/rubygems/defaults/operating_system.rb out << " - USER INSTALLATION DIRECTORY: #{Gem.user_dir}\n" + out << " - CREDENTIALS FILE: #{Gem.configuration.credentials_path}\n" + out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil? out << " - RUBY EXECUTABLE: #{Gem.ruby}\n" @@ -170,6 +177,6 @@ lib/rubygems/defaults/operating_system.rb end end - return nil + nil end end diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb index 3855dc47bb..1feafbdd35 100644 --- a/lib/rubygems/commands/exec_command.rb +++ b/lib/rubygems/commands/exec_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../dependency_installer" require_relative "../gem_runner" @@ -56,8 +57,6 @@ to the same gem path as user-installed gems. end def execute - gem_paths = { "GEM_HOME" => Gem.paths.home, "GEM_PATH" => Gem.paths.path.join(File::PATH_SEPARATOR), "GEM_SPEC_CACHE" => Gem.paths.spec_cache_dir }.compact - check_executable print_command @@ -73,9 +72,6 @@ to the same gem path as user-installed gems. end load! - ensure - ENV.update(gem_paths) if gem_paths - Gem.clear_paths end private @@ -142,7 +138,7 @@ to the same gem path as user-installed gems. end def set_gem_exec_install_paths - home = File.join(Gem.dir, "gem_exec") + home = Gem.dir ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR) ENV["GEM_HOME"] = home @@ -177,6 +173,9 @@ to the same gem path as user-installed gems. rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" terminate_interaction 1 + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + terminate_interaction 2 rescue Gem::GemNotFoundException => e show_lookup_failure e.name, e.version, e.errors, false @@ -199,7 +198,7 @@ to the same gem path as user-installed gems. argv = ARGV.clone ARGV.replace options[:args] - exe = executable = options[:executable] + executable = options[:executable] contains_executable = Gem.loaded_specs.values.select do |spec| spec.executables.include?(executable) @@ -210,13 +209,22 @@ to the same gem path as user-installed gems. end if contains_executable.empty? - if (spec = Gem.loaded_specs[executable]) && (exe = spec.executable) - contains_executable << spec - else + spec = Gem.loaded_specs[executable] + + if spec.nil? || spec.executables.empty? alert_error "Failed to load executable `#{executable}`," \ " are you sure the gem `#{options[:gem_name]}` contains it?" terminate_interaction 1 end + + if spec.executables.size > 1 + alert_error "Ambiguous which executable from gem `#{executable}` should be run: " \ + "the options are #{spec.executables.sort}, specify one via COMMAND, and use `-g` and `-v` to specify gem and version" + terminate_interaction 1 + end + + contains_executable << spec + executable = spec.executable end if contains_executable.size > 1 @@ -226,8 +234,11 @@ to the same gem path as user-installed gems. terminate_interaction 1 end - load Gem.activate_bin_path(contains_executable.first.name, exe, ">= 0.a") + old_exe = $0 + $0 = executable + load Gem.activate_bin_path(contains_executable.first.name, executable, ">= 0.a") ensure + $0 = old_exe if old_exe ARGV.replace argv end diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index 5eb45d259c..8e64a18cee 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -9,8 +10,8 @@ class Gem::Commands::FetchCommand < Gem::Command def initialize defaults = { - :suggest_alternate => true, - :version => Gem::Requirement.default, + suggest_alternate: true, + version: Gem::Requirement.default, } super "fetch", "Download a gem and place it in the current directory", defaults @@ -55,13 +56,24 @@ then repackaging it. if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end def execute check_version + + exit_code = fetch_gems + + terminate_interaction exit_code + end + + private + + def fetch_gems + exit_code = 0 + version = options[:version] platform = Gem.platforms.last @@ -85,10 +97,13 @@ then repackaging it. if spec.nil? show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain] + exit_code |= 2 next end source.download spec say "Downloaded #{spec.full_name}" end + + exit_code end end diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index bc71e60ff0..13be92593b 100644 --- a/lib/rubygems/commands/generate_index_command.rb +++ b/lib/rubygems/commands/generate_index_command.rb @@ -1,85 +1,51 @@ # frozen_string_literal: true + require_relative "../command" -require_relative "../indexer" -## -# Generates a index files for use as a gem server. -# -# See `gem help generate_index` +unless defined? Gem::Commands::GenerateIndexCommand + class Gem::Commands::GenerateIndexCommand < Gem::Command + module RubygemsTrampoline + def description # :nodoc: + <<~EOF + The generate_index command has been moved to the rubygems-generate_index gem. + EOF + end -class Gem::Commands::GenerateIndexCommand < Gem::Command - def initialize - super "generate_index", - "Generates the index files for a gem server directory", - :directory => ".", :build_modern => true + def execute + alert_error "Install the rubygems-generate_index gem for the generate_index command" + end - add_option "-d", "--directory=DIRNAME", - "repository base dir containing gems subdir" do |dir, options| - options[:directory] = File.expand_path dir + def invoke_with_build_args(args, build_args) + name = "rubygems-generate_index" + spec = begin + Gem::Specification.find_by_name(name) + rescue Gem::LoadError + require "rubygems/dependency_installer" + Gem.install(name, Gem::Requirement.default, Gem::DependencyInstaller::DEFAULT_OPTIONS).find {|s| s.name == name } + end + + # remove the methods defined in this file so that the methods defined in the gem are used instead, + # and without a method redefinition warning + %w[description execute invoke_with_build_args].each do |method| + RubygemsTrampoline.remove_method(method) + end + self.class.singleton_class.remove_method(:new) + + spec.activate + Gem.load_plugin_files spec.matches_for_glob("rubygems_plugin#{Gem.suffix_pattern}") + + self.class.new.invoke_with_build_args(args, build_args) + end end + private_constant :RubygemsTrampoline - add_option "--[no-]modern", - "Generate indexes for RubyGems", - "(always true)" do |value, options| - options[:build_modern] = value + # remove_method(:initialize) warns, but removing new does not warn + def self.new + command = allocate + command.send(:initialize, "generate_index", "Generates the index files for a gem server directory (requires rubygems-generate_index)") + command end - deprecate_option("--modern", version: "4.0", extra_msg: "Modern indexes (specs, latest_specs, and prerelease_specs) are always generated, so this option is not needed.") - deprecate_option("--no-modern", version: "4.0", extra_msg: "The `--no-modern` option is currently ignored. Modern indexes (specs, latest_specs, and prerelease_specs) are always generated.") - - add_option "--update", - "Update modern indexes with gems added", - "since the last update" do |value, options| - options[:update] = value - end - end - - def defaults_str # :nodoc: - "--directory . --modern" - end - - def description # :nodoc: - <<-EOF -The generate_index command creates a set of indexes for serving gems -statically. The command expects a 'gems' directory under the path given to -the --directory option. The given directory will be the directory you serve -as the gem repository. - -For `gem generate_index --directory /path/to/repo`, expose /path/to/repo via -your HTTP server configuration (not /path/to/repo/gems). - -When done, it will generate a set of files like this: - - gems/*.gem # .gem files you want to - # index - - specs.<version>.gz # specs index - latest_specs.<version>.gz # latest specs index - prerelease_specs.<version>.gz # prerelease specs index - quick/Marshal.<version>/<gemname>.gemspec.rz # Marshal quick index file - -The .rz extension files are compressed with the inflate algorithm. -The Marshal version number comes from ruby's Marshal::MAJOR_VERSION and -Marshal::MINOR_VERSION constants. It is used to ensure compatibility. - EOF - end - - def execute - # This is always true because it's the only way now. - options[:build_modern] = true - - if !File.exist?(options[:directory]) || - !File.directory?(options[:directory]) - alert_error "unknown directory name #{options[:directory]}." - terminate_interaction 1 - else - indexer = Gem::Indexer.new options.delete(:directory), options - - if options[:update] - indexer.update_index - else - indexer.generate_index - end - end + prepend(RubygemsTrampoline) end end diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index a64c3e128d..664f400561 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::HelpCommand < Gem::Command @@ -58,7 +59,7 @@ multiple environments. The RubyGems implementation is designed to be compatible with Bundler's Gemfile format. You can see additional documentation on the format at: - http://bundler.io + https://bundler.io RubyGems automatically looks for these gem dependencies files: @@ -89,7 +90,9 @@ Use #gem to declare which gems you directly depend upon: To depend on a specific set of versions: - gem 'rake', '~> 10.3', '>= 10.3.2' + gem 'rake', '>= 10.3.2' + # or for multiple version restrictions + gem 'rake', '>= 10.3.2', "< 13" RubyGems will require the gem name when activating the gem using the RUBYGEMS_GEMDEPS environment variable or Gem::use_gemdeps. Use the @@ -171,7 +174,7 @@ and #platforms methods: See the bundler Gemfile manual page for a list of platforms supported in a gem dependencies file.: - http://bundler.io/v1.6/man/gemfile.5.html + https://bundler.io/v2.5/man/gemfile.5.html Ruby Version and Engine Dependency ================================== @@ -323,7 +326,7 @@ platform. margin_width = 4 - desc_width = @command_manager.command_names.map {|n| n.size }.max + 4 + desc_width = @command_manager.command_names.map(&:size).max + 4 summary_width = 80 - margin_width - desc_width wrap_indent = " " * (margin_width + desc_width) @@ -332,7 +335,7 @@ platform. @command_manager.command_names.each do |cmd_name| command = @command_manager[cmd_name] - next if command.deprecated? + next if command&.deprecated? summary = if command @@ -342,7 +345,7 @@ platform. end summary = wrap(summary, summary_width).split "\n" - out << sprintf(format, cmd_name, summary.shift) + out << format(format, cmd_name, summary.shift) until summary.empty? do out << "#{wrap_indent}#{summary.shift}" end diff --git a/lib/rubygems/commands/info_command.rb b/lib/rubygems/commands/info_command.rb index ced7751ff5..f65c639662 100644 --- a/lib/rubygems/commands/info_command.rb +++ b/lib/rubygems/commands/info_command.rb @@ -8,8 +8,8 @@ class Gem::Commands::InfoCommand < Gem::Command def initialize super "info", "Show information for the given gem", - :name => //, :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + name: //, domain: :local, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index c04c01f258..6d3beec0b4 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../install_update_options" require_relative "../dependency_installer" @@ -22,11 +23,11 @@ class Gem::Commands::InstallCommand < Gem::Command def initialize defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ - :format_executable => false, - :lock => true, - :suggest_alternate => true, - :version => Gem::Requirement.default, - :without_groups => [], + format_executable: false, + lock: true, + suggest_alternate: true, + version: Gem::Requirement.default, + without_groups: [], }) defaults.merge!(install_update_options) @@ -47,7 +48,7 @@ class Gem::Commands::InstallCommand < Gem::Command end def defaults_str # :nodoc: - "--both --version '#{Gem::Requirement.default}' --no-force\n" + + "--both --version '#{Gem::Requirement.default}' --no-force\n" \ "--install-dir #{Gem.dir} --lock\n" + install_update_defaults_str end @@ -135,18 +136,11 @@ You can use `i` command instead of `install`. "#{program_name} [options] GEMNAME [GEMNAME ...] -- --build-flags" end - def check_install_dir # :nodoc: - if options[:install_dir] && options[:user_install] - alert_error "Use --install-dir or --user-install but not both" - terminate_interaction 1 - end - end - def check_version # :nodoc: if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end @@ -161,7 +155,6 @@ You can use `i` command instead of `install`. ENV.delete "GEM_PATH" if options[:install_dir].nil? - check_install_dir check_version load_hooks @@ -170,7 +163,7 @@ You can use `i` command instead of `install`. show_installed - say update_suggestion if eglible_for_update? + say update_suggestion if eligible_for_update? terminate_interaction exit_code end @@ -231,9 +224,8 @@ You can use `i` command instead of `install`. rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 - rescue Gem::GemNotFoundException => e - show_lookup_failure e.name, e.version, e.errors, suppress_suggestions - + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 2 rescue Gem::UnsatisfiableDependencyError => e show_lookup_failure e.name, e.version, e.errors, suppress_suggestions, @@ -250,11 +242,7 @@ You can use `i` command instead of `install`. # Loads post-install hooks def load_hooks # :nodoc: - if options[:install_as_default] - require_relative "../install_default_message" - else - require_relative "../install_message" - end + require_relative "../install_message" require_relative "../rdoc" end @@ -262,7 +250,7 @@ You can use `i` command instead of `install`. return unless errors errors.each do |x| - return unless Gem::SourceFetchProblem === x + next unless Gem::SourceFetchProblem === x require_relative "../uri" msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}" diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index 011873b99c..fab4b73814 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" @@ -10,8 +11,8 @@ class Gem::Commands::ListCommand < Gem::Command def initialize super "list", "Display local gems whose name matches REGEXP", - :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + domain: :local, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options end diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb index da636492c9..f7fd5ada16 100644 --- a/lib/rubygems/commands/lock_command.rb +++ b/lib/rubygems/commands/lock_command.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::LockCommand < Gem::Command def initialize super "lock", "Generate a lockdown list of gems", - :strict => false + strict: false add_option "-s", "--[no-]strict", "fail if unable to satisfy a dependency" do |strict, options| diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb index b633cd3d81..b91a8db12d 100644 --- a/lib/rubygems/commands/mirror_command.rb +++ b/lib/rubygems/commands/mirror_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" unless defined? Gem::Commands::MirrorCommand diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb index d5283f72dd..0fe90dc8b8 100644 --- a/lib/rubygems/commands/open_command.rb +++ b/lib/rubygems/commands/open_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" @@ -69,9 +70,7 @@ class Gem::Commands::OpenCommand < Gem::Command end def open_editor(path) - Dir.chdir(path) do - system(*@editor.split(/\s+/) + [path]) - end + system(*@editor.split(/\s+/) + [path], { chdir: path }) end def spec_for(name) diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb index 1785194389..08a9221a26 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../spec_fetcher" diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index cd60157911..675e866734 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../gemcutter_utilities" @@ -38,7 +39,7 @@ permission to. add_proxy_option add_key_option add_otp_option - defaults.merge! :add => [], :remove => [] + defaults.merge! add: [], remove: [] add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options| options[:add] << value @@ -74,11 +75,12 @@ permission to. end with_response response do |resp| - owners = Gem::SafeYAML.load clean_text(resp.body) + owners = Gem::SafeYAML.safe_load clean_text(resp.body) say "Owners for gem: #{name}" owners.each do |owner| - say "- #{owner["email"] || owner["handle"] || owner["id"]}" + identifier = owner["email"] || owner["handle"] || owner["id"] + say "- #{identifier} (#{owner["role"]})" end end end @@ -97,8 +99,10 @@ permission to. action = method == :delete ? "Removing" : "Adding" with_response response, "#{action} #{owner}" + rescue Gem::WebauthnVerificationError => e + raise e rescue StandardError - # ignore + # ignore early exits to allow for completing the iteration of all owners end end diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 6f0f7be7c9..10978c2af7 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../package" require_relative "../installer" @@ -10,10 +11,10 @@ class Gem::Commands::PristineCommand < Gem::Command def initialize super "pristine", "Restores installed gems to pristine condition from files located in the gem cache", - :version => Gem::Requirement.default, - :extensions => true, - :extensions_set => false, - :all => false + version: Gem::Requirement.default, + extensions: true, + extensions_set: false, + all: false add_option("--all", "Restore all installed gems to pristine", @@ -56,7 +57,7 @@ class Gem::Commands::PristineCommand < Gem::Command end add_option("-i", "--install-dir DIR", - "Gem repository to get binstubs and plugins installed") do |value, options| + "Gem repository to get gems restored") do |value, options| options[:install_dir] = File.expand_path(value) end @@ -87,6 +88,10 @@ If you have made modifications to an installed gem, the pristine command will revert them. All extensions are rebuilt and all bin stubs for the gem are regenerated after checking for modifications. +Rebuilding extensions also refreshes C-extension gems against updated system +libraries (for example after OS or package upgrades) to avoid mismatches like +outdated library version warnings. + If the cached gem cannot be found it will be downloaded. If --no-extensions is provided pristine will not attempt to restore a gem @@ -102,39 +107,53 @@ extensions will be restored. end def execute + install_dir = options[:install_dir] + + specification_record = install_dir ? Gem::SpecificationRecord.from_path(install_dir) : Gem::Specification.specification_record + specs = if options[:all] - Gem::Specification.map + specification_record.map # `--extensions` must be explicitly given to pristine only gems # with extensions. elsif options[:extensions_set] && options[:extensions] && options[:args].empty? - Gem::Specification.select do |spec| + specification_record.select do |spec| spec.extensions && !spec.extensions.empty? end elsif options[:only_missing_extensions] - Gem::Specification.select do |spec| - spec.missing_extensions? - end + specification_record.select(&:missing_extensions?) else - get_all_gem_names.sort.map do |gem_name| - Gem::Specification.find_all_by_name(gem_name, options[:version]).reverse - end.flatten + get_all_gem_names.sort.flat_map do |gem_name| + specification_record.find_all_by_name(gem_name, options[:version]).reverse + end end - specs = specs.select {|spec| RUBY_ENGINE == spec.platform || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } + specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } if specs.to_a.empty? + if options[:only_missing_extensions] + say "No gems with missing extensions to restore" + return + end + raise Gem::Exception, "Failed to find gems #{options[:args]} #{options[:version]}" end say "Restoring gems to pristine condition..." - specs.each do |spec| - if spec.default_gem? - say "Skipped #{spec.full_name}, it is a default gem" - next + specs.group_by(&:full_name_with_location).values.each do |grouped_specs| + spec = grouped_specs.find {|s| !s.default_gem? } || grouped_specs.first + + only_executables = options[:only_executables] + only_plugins = options[:only_plugins] + + unless only_executables || only_plugins + # Default gemspecs include changes provided by ruby-core installer that + # can't currently be pristined (inclusion of compiled extension targets in + # the file list). So stick to resetting executables if it's a default gem. + only_executables = true if spec.default_gem? end if options.key? :skip @@ -144,17 +163,17 @@ extensions will be restored. end end - unless spec.extensions.empty? || options[:extensions] || options[:only_executables] || options[:only_plugins] - say "Skipped #{spec.full_name}, it needs to compile an extension" + unless spec.extensions.empty? || options[:extensions] || only_executables || only_plugins + say "Skipped #{spec.full_name_with_location}, it needs to compile an extension" next end gem = spec.cache_file - unless File.exist?(gem) || options[:only_executables] || options[:only_plugins] + unless File.exist?(gem) || only_executables || only_plugins require_relative "../remote_fetcher" - say "Cached gem for #{spec.full_name} not found, attempting to fetch..." + say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..." dep = Gem::Dependency.new spec.name, spec.version found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep @@ -177,21 +196,20 @@ extensions will be restored. end bin_dir = options[:bin_dir] if options[:bin_dir] - install_dir = options[:install_dir] if options[:install_dir] installer_options = { - :wrappers => true, - :force => true, - :install_dir => install_dir || spec.base_dir, - :env_shebang => env_shebang, - :build_args => spec.build_args, - :bin_dir => bin_dir, + wrappers: true, + force: true, + install_dir: install_dir || spec.base_dir, + env_shebang: env_shebang, + build_args: spec.build_args, + bin_dir: bin_dir, } - if options[:only_executables] + if only_executables installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_bin - elsif options[:only_plugins] + elsif only_plugins installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_plugins else @@ -199,7 +217,7 @@ extensions will be restored. installer.install end - say "Restored #{spec.full_name}" + say "Restored #{spec.full_name_with_location}" end end end diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 7c5cb7a81a..02931b3025 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../gemcutter_utilities" @@ -29,7 +30,7 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo end def initialize - super "push", "Push a gem up to the gem server", :host => self.host + super "push", "Push a gem up to the gem server", host: host, attestations: [] @user_defined_host = false @@ -44,6 +45,11 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo @user_defined_host = true end + add_option("--attestation FILE", + "Push with sigstore attestations") do |value, options| + options[:attestations] << value + end + @host = nil end @@ -86,12 +92,77 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo private def send_push_request(name, args) - rubygems_api_request(*args, scope: get_push_scope) do |request| - request.body = Gem.read_binary name - request.add_field "Content-Length", request.body.size + # Always honor explicit --attestation option + # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby) + if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"]) + send_push_request_with_attestation(name, args) + else + send_push_request_without_attestation(name, args) + end + end + + def send_push_request_without_attestation(name, args) + scope = get_push_scope + rubygems_api_request(*args, scope: scope) do |request| + body = Gem.read_binary name + request.body = body request.add_field "Content-Type", "application/octet-stream" - request.add_field "Authorization", api_key + request.add_field "Content-Length", request.body.size + request.add_field "Authorization", api_key + end + end + + def send_push_request_with_attestation(name, args) + attestations = if options[:attestations].any? + options[:attestations].map do |attestation| + Gem.read_binary(attestation) + end + else + bundle_path = attest!(name) + begin + [Gem.read_binary(bundle_path)] + ensure + File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path) + end + end + bundles = "[" + attestations.join(",") + "]" + + rubygems_api_request(*args, scope: get_push_scope) do |request| + request.set_form([ + ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }], + ["attestations", bundles, { content_type: "application/json" }], + ], "multipart/form-data") + request.add_field "Authorization", api_key end + rescue StandardError => e + message = "Failed to push with attestation, retrying without attestation.\n" + message += if Gem.configuration.really_verbose + e.full_message + else + e.message + end + alert_warning message + send_push_request_without_attestation(name, args) + end + + def attest!(name) + require "open3" + require "tempfile" + + tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"]) + bundle = tempfile.path + tempfile.close(false) + + env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h + out, st = Open3.capture2e( + env, + Gem.ruby, "-S", "gem", "exec", "--conservative", + "sigstore-cli", "sign", name, "--bundle", bundle, + unsetenv_others: true + ) + raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? + + bundle end def get_hosts_for(name) @@ -106,4 +177,9 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo def get_push_scope :push_rubygem end + + def attestation_supported_host? + host = (@host || Gem.host).to_s.chomp("/") + host == Gem::DEFAULT_HOST + end end diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb deleted file mode 100644 index 16e94e626a..0000000000 --- a/lib/rubygems/commands/query_command.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true -require_relative "../command" -require_relative "../query_utils" -require_relative "../deprecate" - -class Gem::Commands::QueryCommand < Gem::Command - extend Gem::Deprecate - rubygems_deprecate_command - - include Gem::QueryUtils - - alias_method :warning_without_suggested_alternatives, :deprecation_warning - def deprecation_warning - warning_without_suggested_alternatives - - message = "It is recommended that you use `gem search` or `gem list` instead.\n" - alert_warning message unless Gem::Deprecate.skip - end - - def initialize(name = "query", summary = "Query gem information in local or remote repositories") - super name, summary, - :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default - - add_option("-n", "--name-matches REGEXP", - "Name of gem(s) to query on matches the", - "provided REGEXP") do |value, options| - options[:name] = /#{value}/i - end - - add_query_options - end - - def description # :nodoc: - <<-EOF -The query command is the basis for the list and search commands. - -You should really use the list and search commands instead. This command -is too hard to use. - EOF - end -end diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index a998a9704c..62c4bf8ce9 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../rdoc" @@ -9,8 +10,8 @@ class Gem::Commands::RdocCommand < Gem::Command def initialize super "rdoc", "Generates RDoc for pre-installed gems", - :version => Gem::Requirement.default, - :include_rdoc => false, :include_ri => true, :overwrite => false + version: Gem::Requirement.default, + include_rdoc: false, include_ri: true, overwrite: false add_option("--all", "Generate RDoc/RI documentation for all", @@ -63,9 +64,9 @@ Use --overwrite to force rebuilding of documentation. specs = if options[:all] Gem::Specification.to_a else - get_all_gem_names.map do |name| + get_all_gem_names.flat_map do |name| Gem::Specification.find_by_name name, options[:version] - end.flatten.uniq + end.uniq end if specs.empty? @@ -83,14 +84,7 @@ Use --overwrite to force rebuilding of documentation. FileUtils.rm_rf File.join(spec.doc_dir, "rdoc") end - begin - doc.generate - rescue Errno::ENOENT => e - match = / - /.match(e.message) - alert_error "Unable to document #{spec.full_name}, " \ - " #{match.post_match} is missing, skipping" - terminate_interaction 1 if specs.length == 1 - end + doc.generate end end end diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb new file mode 100644 index 0000000000..23b9d7b3ba --- /dev/null +++ b/lib/rubygems/commands/rebuild_command.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true + +require "digest" +require "fileutils" +require "tmpdir" +require_relative "../gemspec_helpers" +require_relative "../package" + +class Gem::Commands::RebuildCommand < Gem::Command + include Gem::GemspecHelpers + + def initialize + super "rebuild", "Attempt to reproduce a build of a gem." + + add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options| + options[:diff] = true + end + + add_option "--force", "Skip validation of the spec." do |_value, options| + options[:force] = true + end + + add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options| + options[:strict] = true + end + + add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options| + options[:source] = value + end + + add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options| + options[:original_gem_file] = value + end + + add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options| + options[:gemspec_file] = value + end + + add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options| + options[:build_path] = value + end + end + + def arguments # :nodoc: + "GEM_NAME gem name on gem server\n" \ + "GEM_VERSION gem version you are attempting to rebuild" + end + + def description # :nodoc: + <<-EOF +The rebuild command allows you to (attempt to) reproduce a build of a gem +from a ruby gemspec. + +This command assumes the gemspec can be built with the `gem build` command. +If you use any of `gem build`, `rake build`, or`rake release` in the +build/release process for a gem, it is a potential candidate. + +You will need to match the RubyGems version used, since this is included in +the Gem metadata. + +If the gem includes lockfiles (e.g. Gemfile.lock) and similar, it will +require more effort to reproduce a build. For example, it might require +more precisely matched versions of Ruby and/or Bundler to be used. + EOF + end + + def usage # :nodoc: + "#{program_name} GEM_NAME GEM_VERSION" + end + + def execute + gem_name, gem_version = get_gem_name_and_version + + old_dir, new_dir = prep_dirs + + gem_filename = "#{gem_name}-#{gem_version}.gem" + old_file = File.join(old_dir, gem_filename) + new_file = File.join(new_dir, gem_filename) + + if options[:original_gem_file] + FileUtils.copy_file(options[:original_gem_file], old_file) + else + download_gem(gem_name, gem_version, old_file) + end + + rg_version = rubygems_version(old_file) + unless rg_version == Gem::VERSION + alert_error <<-EOF +You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with. + +#{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}. +Gem files include the version of RubyGems used to build them. +This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}. + +You're using RubyGems v#{Gem::VERSION}. + +Please install RubyGems v#{rg_version} and try again. + EOF + terminate_interaction 1 + end + + source_date_epoch = get_timestamp(old_file).to_s + + if build_path = options[:build_path] + Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) } + else + build_gem(gem_name, source_date_epoch, new_file) + end + + compare(source_date_epoch, old_file, new_file) + end + + private + + def sha256(file) + Digest::SHA256.hexdigest(Gem.read_binary(file)) + end + + def get_timestamp(file) + mtime = nil + File.open(file, Gem.binary_mode) do |f| + Gem::Package::TarReader.new(f) do |tar| + mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime } + end + end + + mtime + end + + def compare(source_date_epoch, old_file, new_file) + date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z") + + old_hash = sha256(old_file) + new_hash = sha256(new_file) + + say + say "Built at: #{date} (#{source_date_epoch})" + say "Original build saved to: #{old_file}" + say "Reproduced build saved to: #{new_file}" + say "Working directory: #{options[:build_path] || Dir.pwd}" + say + say "Hash comparison:" + say " #{old_hash}\t#{old_file}" + say " #{new_hash}\t#{new_file}" + say + + if old_hash == new_hash + say "SUCCESS - original and rebuild hashes matched" + else + say "FAILURE - original and rebuild hashes did not match" + say + + if options[:diff] + if system("diffoscope", old_file, new_file).nil? + alert_error "error: could not find `diffoscope` executable" + end + else + say "Pass --diff for more details (requires diffoscope to be installed)." + end + + terminate_interaction 1 + end + end + + def prep_dirs + rebuild_dir = Dir.mktmpdir("gem_rebuild") + old_dir = File.join(rebuild_dir, "old") + new_dir = File.join(rebuild_dir, "new") + + FileUtils.mkdir_p(old_dir) + FileUtils.mkdir_p(new_dir) + + [old_dir, new_dir] + end + + def get_gem_name_and_version + args = options[:args] || [] + if args.length == 2 + gem_name, gem_version = args + elsif args.length > 2 + raise Gem::CommandLineError, "Too many arguments" + else + raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)" + end + + [gem_name, gem_version] + end + + def build_gem(gem_name, source_date_epoch, output_file) + gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec") + + if gemspec + build_package(gemspec, source_date_epoch, output_file) + else + alert_error error_message(gem_name) + terminate_interaction(1) + end + end + + def build_package(gemspec, source_date_epoch, output_file) + with_source_date_epoch(source_date_epoch) do + spec = Gem::Specification.load(gemspec) + if spec + Gem::Package.build( + spec, + options[:force], + options[:strict], + output_file + ) + else + alert_error "Error loading gemspec. Aborting." + terminate_interaction 1 + end + end + end + + def with_source_date_epoch(source_date_epoch) + old_sde = ENV["SOURCE_DATE_EPOCH"] + ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s + + yield + ensure + ENV["SOURCE_DATE_EPOCH"] = old_sde + end + + def error_message(gem_name) + if gem_name + "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}" + else + "Couldn't find a gemspec file in #{Dir.pwd}" + end + end + + def download_gem(gem_name, gem_version, old_file) + # This code was based loosely off the `gem fetch` command. + version = "= #{gem_version}" + dep = Gem::Dependency.new gem_name, version + + specs_and_sources, errors = + Gem::SpecFetcher.fetcher.spec_for_dependency dep + + # There should never be more than one item in specs_and_sources, + # since we search for an exact version. + spec, source = specs_and_sources[0] + + if spec.nil? + show_lookup_failure gem_name, version, errors, options[:domain] + terminate_interaction 1 + end + + download_path = source.download spec + + FileUtils.move(download_path, old_file) + + say "Downloaded #{gem_name} version #{gem_version} as #{old_file}." + end + + def rubygems_version(gem_file) + Gem::Package.new(gem_file).spec.rubygems_version + end +end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb index 3f8f7e13f2..50e161ac9b 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" @@ -7,8 +8,8 @@ class Gem::Commands::SearchCommand < Gem::Command def initialize super "search", "Display remote gems whose name matches REGEXP", - :domain => :remote, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + domain: :remote, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options end diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb index 56be07c79d..f1dde4aa02 100644 --- a/lib/rubygems/commands/server_command.rb +++ b/lib/rubygems/commands/server_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" unless defined? Gem::Commands::ServerCommand diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index efc387a69f..175599967c 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" ## @@ -6,19 +7,19 @@ require_relative "../command" # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command - HISTORY_HEADER = /^#\s*[\d.a-zA-Z]+\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze - VERSION_MATCHER = /^#\s*([\d.a-zA-Z]+)\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze + HISTORY_HEADER = %r{^##\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$} + VERSION_MATCHER = %r{^##\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$} ENV_PATHS = %w[/usr/bin/env /bin/env].freeze def initialize super "setup", "Install RubyGems", - :format_executable => false, :document => %w[ri], - :force => true, - :site_or_vendor => "sitelibdir", - :destdir => "", :prefix => "", :previous_version => "", - :regenerate_binstubs => true, - :regenerate_plugins => true + format_executable: false, document: %w[ri], + force: true, + site_or_vendor: "sitelibdir", + destdir: "", prefix: "", previous_version: "", + regenerate_binstubs: true, + regenerate_plugins: true add_option "--previous-version=VERSION", "Previous version of RubyGems", @@ -106,15 +107,6 @@ class Gem::Commands::SetupCommand < Gem::Command @verbose = nil end - def check_ruby_version - required_version = Gem::Requirement.new ">= 2.6.0" - - unless required_version.satisfied_by? Gem.ruby_version - alert_error "Expected Ruby version #{required_version}, is #{Gem.ruby_version}" - terminate_interaction 1 - end - end - def defaults_str # :nodoc: "--format-executable --document ri --regenerate-binstubs" end @@ -147,8 +139,6 @@ By default, this RubyGems will install gem as: def execute @verbose = Gem.configuration.really_verbose - check_ruby_version - require "fileutils" if Gem.configuration.really_verbose extend FileUtils::Verbose @@ -242,9 +232,9 @@ By default, this RubyGems will install gem as: end def install_executables(bin_dir) - prog_mode = options[:prog_mode] || 0755 + prog_mode = options[:prog_mode] || 0o755 - executables = { "gem" => "bin" } + executables = { "gem" => "exe" } executables.each do |tool, path| say "Installing #{tool} executable" if @verbose @@ -264,7 +254,7 @@ By default, this RubyGems will install gem as: fp.puts bin.join end - install bin_tmp_file, dest_file, :mode => prog_mode + install bin_tmp_file, dest_file, mode: prog_mode bin_file_names << dest_file ensure rm bin_tmp_file @@ -278,15 +268,11 @@ By default, this RubyGems will install gem as: File.open bin_cmd_file, "w" do |file| file.puts <<-TEXT @ECHO OFF - IF NOT "%~f0" == "~f0" GOTO :WinNT - @"#{File.basename(Gem.ruby).chomp('"')}" "#{dest_file}" %1 %2 %3 %4 %5 %6 %7 %8 %9 - GOTO :EOF - :WinNT - @"#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* + @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* TEXT end - install bin_cmd_file, "#{dest_file}.bat", :mode => prog_mode + install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode ensure rm bin_cmd_file end @@ -339,6 +325,8 @@ By default, this RubyGems will install gem as: require_relative "../rdoc" + return false unless defined?(Gem::RDoc) + fake_spec = Gem::Specification.new "rubygems", Gem::VERSION def fake_spec.full_gem_path File.expand_path "../../..", __dir__ @@ -356,30 +344,39 @@ By default, this RubyGems will install gem as: say "Set the GEM_HOME environment variable if you want RDoc generated" end - return false + false end def install_default_bundler_gem(bin_dir) current_default_spec = Gem::Specification.default_stubs.find {|s| s.name == "bundler" } specs_dir = if current_default_spec && default_dir == Gem.default_dir + all_specs_current_version = Gem::Specification.stubs.select {|s| s.full_name == current_default_spec.full_name } + Gem::Specification.remove_spec current_default_spec loaded_from = current_default_spec.loaded_from File.delete(loaded_from) + + # Remove previous default gem executables if they were not shadowed by a regular gem + FileUtils.rm_rf current_default_spec.full_gem_path if all_specs_current_version.size == 1 + File.dirname(loaded_from) else target_specs_dir = File.join(default_dir, "specifications", "default") - mkdir_p target_specs_dir, :mode => 0755 + mkdir_p target_specs_dir, mode: 0o755 target_specs_dir end - bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } - default_spec_path = File.join(specs_dir, "#{bundler_spec.full_name}.gemspec") - Gem.write_binary(default_spec_path, bundler_spec.to_ruby) + new_bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } + full_name = new_bundler_spec.full_name + gemspec_path = "#{full_name}.gemspec" + + default_spec_path = File.join(specs_dir, gemspec_path) + Gem.write_binary(default_spec_path, new_bundler_spec.to_ruby) bundler_spec = Gem::Specification.load(default_spec_path) # Remove gemspec that was same version of vendored bundler. - normal_gemspec = File.join(default_dir, "specifications", "bundler-#{bundler_spec.version}.gemspec") + normal_gemspec = File.join(default_dir, "specifications", gemspec_path) if File.file? normal_gemspec File.delete normal_gemspec end @@ -387,39 +384,37 @@ By default, this RubyGems will install gem as: # Remove gem files that were same version of vendored bundler. if File.directory? bundler_spec.gems_dir Dir.entries(bundler_spec.gems_dir). - select {|default_gem| File.basename(default_gem) == "bundler-#{bundler_spec.version}" }. + select {|default_gem| File.basename(default_gem) == full_name }. each {|default_gem| rm_r File.join(bundler_spec.gems_dir, default_gem) } end - bundler_bin_dir = bundler_spec.bin_dir - mkdir_p bundler_bin_dir, :mode => 0755 - bundler_spec.executables.each do |e| - cp File.join("bundler", bundler_spec.bindir, e), File.join(bundler_bin_dir, e) - end - require_relative "../installer" Dir.chdir("bundler") do - built_gem = Gem::Package.build(bundler_spec) + built_gem = Gem::Package.build(new_bundler_spec) begin - Gem::Installer.at( + installer = Gem::Installer.at( built_gem, env_shebang: options[:env_shebang], format_executable: options[:format_executable], force: options[:force], - install_as_default: true, bin_dir: bin_dir, install_dir: default_dir, wrappers: true - ).install + ) + # We need to install only executable and default spec files. + # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory. + installer.extract_bin + installer.generate_bin + installer.write_default_spec ensure FileUtils.rm_f built_gem end end - bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } + new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } - say "Bundler #{bundler_spec.version} installed" + say "Bundler #{new_bundler_spec.version} installed" end def make_destination_dirs @@ -429,10 +424,10 @@ By default, this RubyGems will install gem as: lib_dir, bin_dir = generate_default_dirs end - mkdir_p lib_dir, :mode => 0755 - mkdir_p bin_dir, :mode => 0755 + mkdir_p lib_dir, mode: 0o755 + mkdir_p bin_dir, mode: 0o755 - return lib_dir, bin_dir + [lib_dir, bin_dir] end def generate_default_man_dir @@ -575,8 +570,8 @@ abort "#{deprecation_message}" def uninstall_old_gemcutter require_relative "../uninstaller" - ui = Gem::Uninstaller.new("gemcutter", :all => true, :ignore => true, - :version => "< 0.4") + ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true, + version: "< 0.4") ui.uninstall rescue Gem::InstallError end @@ -587,6 +582,8 @@ abort "#{deprecation_message}" args = %w[--all --only-executables --silent] args << "--bindir=#{bindir}" + args << "--install-dir=#{default_dir}" + if options[:env_shebang] args << "--env-shebang" end @@ -638,10 +635,10 @@ abort "#{deprecation_message}" dest_file = File.join dest_dir, file dest_dir = File.dirname dest_file unless File.directory? dest_dir - mkdir_p dest_dir, :mode => 0755 + mkdir_p dest_dir, mode: 0o755 end - install file, dest_file, :mode => options[:data_mode] || 0644 + install file, dest_file, mode: options[:data_mode] || 0o644 end def remove_file_list(files, dir) diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb index 2660eee4f3..0f77908c5b 100644 --- a/lib/rubygems/commands/signin_command.rb +++ b/lib/rubygems/commands/signin_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../gemcutter_utilities" diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb index fa688ea3f8..bdd01e4393 100644 --- a/lib/rubygems/commands/signout_command.rb +++ b/lib/rubygems/commands/signout_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::SignoutCommand < Gem::Command diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index 239b4f5884..b399af2bd3 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../remote_fetcher" require_relative "../spec_fetcher" @@ -17,6 +18,14 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:add] = value end + add_option "--append SOURCE_URI", "Append source (can be used multiple times)" do |value, options| + options[:append] = value + end + + add_option "-p", "--prepend SOURCE_URI", "Prepend source (can be used multiple times)" do |value, options| + options[:prepend] = value + end + add_option "-l", "--list", "List sources" do |value, options| options[:list] = value end @@ -25,8 +34,7 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:remove] = value end - add_option "-c", "--clear-all", - "Remove all sources (clear the cache)" do |value, options| + add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options| options[:clear_all] = value end @@ -42,11 +50,8 @@ class Gem::Commands::SourcesCommand < Gem::Command end def add_source(source_uri) # :nodoc: - check_rubygems_https source_uri - - source = Gem::Source.new source_uri - - check_typo_squatting(source) + source = build_new_source(source_uri) + source_uri = source.uri.to_s begin if Gem.sources.include? source @@ -58,7 +63,55 @@ class Gem::Commands::SourcesCommand < Gem::Command say "#{source_uri} added to sources" end - rescue URI::Error, ArgumentError + rescue Gem::URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" + terminate_interaction 1 + end + end + + def append_source(source_uri) # :nodoc: + source = build_new_source(source_uri) + source_uri = source.uri.to_s + + begin + source.load_specs :released + was_present = Gem.sources.include?(source) + Gem.sources.append source + Gem.configuration.write + + if was_present + say "#{source_uri} moved to end of sources" + else + say "#{source_uri} added to sources" + end + rescue Gem::URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" + terminate_interaction 1 + end + end + + def prepend_source(source_uri) # :nodoc: + source = build_new_source(source_uri) + source_uri = source.uri.to_s + + begin + source.load_specs :released + was_present = Gem.sources.include?(source) + Gem.sources.prepend source + Gem.configuration.write + + if was_present + say "#{source_uri} moved to top of sources" + else + say "#{source_uri} added to sources" + end + rescue Gem::URI::Error, ArgumentError say "#{source_uri} is not a URI" terminate_interaction 1 rescue Gem::RemoteFetcher::FetchError => e @@ -79,8 +132,21 @@ Do you want to add this source? end end + def normalize_source_uri(source_uri) # :nodoc: + # Ensure the source URI has a trailing slash for proper RFC 2396 path merging + # Without a trailing slash, the last path segment is treated as a file and removed + # during relative path resolution (e.g., "/blish" + "gems/foo.gem" = "/gems/foo.gem") + # With a trailing slash, it's treated as a directory (e.g., "/blish/" + "gems/foo.gem" = "/blish/gems/foo.gem") + uri = Gem::URI.parse(source_uri) + uri.path = uri.path.gsub(%r{/+\z}, "") + "/" if uri.path && !uri.path.empty? + uri.to_s + rescue Gem::URI::Error + # If parsing fails, return the original URI and let later validation handle it + source_uri + end + def check_rubygems_https(source_uri) # :nodoc: - uri = URI source_uri + uri = Gem::URI source_uri if uri.scheme && uri.scheme.casecmp("http").zero? && uri.host.casecmp("rubygems.org").zero? @@ -127,7 +193,7 @@ yourself to use your own gem server. Without any arguments the sources lists your currently configured sources: $ gem sources - *** CURRENT SOURCES *** + *** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW *** https://rubygems.org @@ -146,33 +212,49 @@ Since all of these sources point to the same set of gems you only need one of them in your list. https://rubygems.org is recommended as it brings the protections of an SSL connection to gem downloads. -To add a source use the --add argument: +To add a private gem source use the --prepend argument to insert it before +the default source. This is usually the best place for private gem sources: - $ gem sources --add https://rubygems.org - https://rubygems.org added to sources + $ gem sources --prepend https://my.private.source + https://my.private.source added to sources RubyGems will check to see if gems can be installed from the source given before it is added. +To add or move a source after all other sources, use --append: + + $ gem sources --append https://rubygems.org + https://rubygems.org moved to end of sources + To remove a source use the --remove argument: - $ gem sources --remove https://rubygems.org/ - https://rubygems.org/ removed from sources + $ gem sources --remove https://my.private.source/ + https://my.private.source/ removed from sources EOF end def list # :nodoc: - say "*** CURRENT SOURCES ***" + if configured_sources + header = "*** CURRENT SOURCES ***" + list = configured_sources + else + header = "*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***" + list = Gem.sources + end + + say header say - Gem.sources.each do |src| + list.each do |src| say src end end def list? # :nodoc: !(options[:add] || + options[:prepend] || + options[:append] || options[:clear_all] || options[:remove] || options[:update]) @@ -181,11 +263,13 @@ To remove a source use the --remove argument: def execute clear_all if options[:clear_all] - source_uri = options[:add] - add_source source_uri if source_uri + add_source options[:add] if options[:add] + + prepend_source options[:prepend] if options[:prepend] - source_uri = options[:remove] - remove_source source_uri if source_uri + append_source options[:append] if options[:append] + + remove_source options[:remove] if options[:remove] update if options[:update] @@ -193,13 +277,22 @@ To remove a source use the --remove argument: end def remove_source(source_uri) # :nodoc: - if Gem.sources.include? source_uri - Gem.sources.delete source_uri + source = build_source(source_uri) + source_uri = source.uri.to_s + + if configured_sources&.include? source + Gem.sources.delete source Gem.configuration.write - say "#{source_uri} removed from sources" + if default_sources.include?(source) && configured_sources.one? + alert_warning "Removing a default source when it is the only source has no effect. Add a different source to #{config_file_name} if you want to stop using it as a source." + else + say "#{source_uri} removed from sources" + end + elsif configured_sources + say "source #{source_uri} cannot be removed because it's not present in #{config_file_name}" else - say "source #{source_uri} not present in cache" + say "source #{source_uri} cannot be removed because there are no configured sources in #{config_file_name}" end end @@ -223,4 +316,33 @@ To remove a source use the --remove argument: say "*** Unable to remove #{desc} source cache ***" end end + + private + + def default_sources + Gem::SourceList.from(Gem.default_sources) + end + + def configured_sources + return @configured_sources if defined?(@configured_sources) + + configuration_sources = Gem.configuration.sources + @configured_sources = Gem::SourceList.from(configuration_sources) if configuration_sources + end + + def config_file_name + Gem.configuration.config_file_name + end + + def build_source(source_uri) + source_uri = normalize_source_uri(source_uri) + Gem::Source.new(source_uri) + end + + def build_new_source(source_uri) + source = build_source(source_uri) + check_rubygems_https(source.uri.to_s) + check_typo_squatting(source) + source + end end diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index fc2fb49ceb..15e543f1a6 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -12,8 +13,8 @@ class Gem::Commands::SpecificationCommand < Gem::Command Gem.load_yaml super "specification", "Display gem specification (in yaml)", - :domain => :local, :version => Gem::Requirement.default, - :format => :yaml + domain: :local, version: Gem::Requirement.default, + format: :yaml add_version_option("examine") add_platform_option @@ -41,7 +42,7 @@ class Gem::Commands::SpecificationCommand < Gem::Command def arguments # :nodoc: <<-ARGS -GEMFILE name of gem to show the gemspec for +GEM_OR_FILE gem name or a .gem file to show the gemspec for FIELD name of gemspec field to show ARGS end @@ -67,7 +68,7 @@ Specific fields in the specification can be extracted in YAML format: end def usage # :nodoc: - "#{program_name} [GEMFILE] [FIELD]" + "#{program_name} [GEM_OR_FILE] [FIELD]" end def execute @@ -76,7 +77,7 @@ Specific fields in the specification can be extracted in YAML format: unless gem raise Gem::CommandLineError, - "Please specify a gem name or file on the command line" + "Please specify a gem name or a .gem file on the command line" end case v = options[:version] @@ -137,7 +138,7 @@ Specific fields in the specification can be extracted in YAML format: end unless options[:all] - specs = [specs.max_by {|s| s.version }] + specs = [specs.max_by(&:version)] end specs.each do |s| @@ -146,7 +147,7 @@ Specific fields in the specification can be extracted in YAML format: say case options[:format] when :ruby then s.to_ruby when :marshal then Marshal.dump s - else s.to_yaml + else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s) end say "\n" diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index 2812179d92..0be2b85159 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::StaleCommand < Gem::Command diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index aa05cda2c8..3c26074f93 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../uninstaller" @@ -14,8 +15,8 @@ class Gem::Commands::UninstallCommand < Gem::Command def initialize super "uninstall", "Uninstall gems from the local repository", - :version => Gem::Requirement.default, :user_install => true, - :check_dev => false, :vendor => false + version: Gem::Requirement.default, user_install: true, + check_dev: false, vendor: false add_option("-a", "--[no-]all", "Uninstall all matching versions") do |value, options| @@ -94,7 +95,7 @@ class Gem::Commands::UninstallCommand < Gem::Command end def defaults_str # :nodoc: - "--version '#{Gem::Requirement.default}' --no-force " + + "--version '#{Gem::Requirement.default}' --no-force " \ "--user-install" end @@ -116,7 +117,7 @@ that is a dependency of an existing gem. You can use the if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end @@ -137,7 +138,7 @@ that is a dependency of an existing gem. You can use the end def uninstall_all - specs = Gem::Specification.reject {|spec| spec.default_gem? } + specs = Gem::Specification.reject(&:default_gem?) specs.each do |spec| options[:version] = spec.version @@ -156,9 +157,14 @@ that is a dependency of an existing gem. You can use the gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name]) - say("Gem '#{name}' is not installed") if gem_specs.empty? - gem_specs.each do |spec| - deplist.add spec + if gem_specs.empty? + say("Gem '#{name}' is not installed") + else + gem_specs.reject!(&:default_gem?) if gem_specs.size > 1 + + gem_specs.each do |spec| + deplist.add spec + end end end @@ -167,15 +173,14 @@ that is a dependency of an existing gem. You can use the gems_to_uninstall = {} deps.each do |dep| - unless gems_to_uninstall[dep.name] + if original_gem_version[dep.name] == Gem::Requirement.default + next if gems_to_uninstall[dep.name] gems_to_uninstall[dep.name] = true - - unless original_gem_version[dep.name] == Gem::Requirement.default - options[:version] = dep.version - end - - uninstall_gem(dep.name) + else + options[:version] = dep.version end + + uninstall_gem(dep.name) end end @@ -183,12 +188,12 @@ that is a dependency of an existing gem. You can use the uninstall(gem_name) rescue Gem::GemNotInHomeException => e spec = e.spec - alert("In order to remove #{spec.name}, please execute:\n" + - "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") + alert("In order to remove #{spec.name}, please execute:\n" \ + "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}") rescue Gem::UninstallError => e spec = e.spec - alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " + - "located at '#{spec.full_gem_path}'. This is most likely because" + + alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \ + "located at '#{spec.full_gem_path}'. This is most likely because" \ "the current user does not have the appropriate permissions") terminate_interaction 1 end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb index 314571dee6..c2fc720297 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../security_option" @@ -20,8 +21,8 @@ class Gem::Commands::UnpackCommand < Gem::Command require "fileutils" super "unpack", "Unpack an installed gem to the current directory", - :version => Gem::Requirement.default, - :target => Dir.pwd + version: Gem::Requirement.default, + target: Dir.pwd add_option("--target=DIR", "target directory for unpacking") do |value, options| @@ -129,7 +130,7 @@ command help for an example. return this_path if File.exist? this_path end - return nil + nil end ## @@ -142,24 +143,18 @@ command help for an example. # get_path 'rake', '< 0.1' # nil # get_path 'rak' # nil (exact name required) #-- - # TODO: This should be refactored so that it's a general service. I don't - # think any of our existing classes are the right place though. Just maybe - # 'Cache'? - # - # TODO: It just uses Gem.dir for now. What's an easy way to get the list of - # source directories? def get_path(dependency) - return dependency.name if dependency.name =~ /\.gem$/i + return dependency.name if /\.gem$/i.match?(dependency.name) specs = dependency.matching_specs - selected = specs.max_by {|s| s.version } + selected = specs.max_by(&:version) return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless selected - return unless dependency.name =~ /^#{selected.name}$/i + return unless /^#{selected.name}$/i.match?(dependency.name) # We expect to find (basename).gem in the 'cache' directory. Furthermore, # the name match must be exact (ignoring case). diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index 837972a8e7..d9740d814a 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../command_manager" require_relative "../dependency_installer" @@ -20,7 +21,7 @@ class Gem::Commands::UpdateCommand < Gem::Command def initialize options = { - :force => false, + force: false, } options.merge!(install_update_options) @@ -36,10 +37,10 @@ class Gem::Commands::UpdateCommand < Gem::Command end add_option("--system [VERSION]", Gem::Version, - "Update the RubyGems system software") do |value, options| + "Update the RubyGems system software") do |value, opts| value ||= true - options[:system] = value + opts[:system] = value end add_local_remote_options @@ -119,7 +120,7 @@ command to remove old versions. updated = update_gems gems_to_update installed_names = highest_installed_gems.keys - updated_names = updated.map {|spec| spec.name } + updated_names = updated.map(&:name) not_updated_names = options[:args].uniq - updated_names not_installed_names = not_updated_names - installed_names up_to_date_names = not_updated_names - not_installed_names @@ -196,18 +197,17 @@ command to remove old versions. yield else require "tmpdir" - tmpdir = Dir.mktmpdir - FileUtils.mv Gem.plugindir, tmpdir + Dir.mktmpdir("gem_update") do |tmpdir| + FileUtils.mv Gem.plugindir, tmpdir - status = yield + status = yield - if status - FileUtils.rm_rf tmpdir - else - FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir - end + unless status + FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir + end - status + status + end end end @@ -232,7 +232,7 @@ command to remove old versions. highest_remote_tup = highest_remote_name_tuple(rubygems_update) target = highest_remote_tup ? highest_remote_tup.version : version - return target, requirement + [target, requirement] end def update_gem(name, version = Gem::Requirement.default) @@ -243,7 +243,7 @@ command to remove old versions. @installer = Gem::DependencyInstaller.new update_options - say "Updating #{name}" unless options[:system] && options[:silent] + say "Updating #{name}" unless options[:system] begin @installer.install name, Gem::Requirement.new(version) rescue Gem::InstallError, Gem::DependencyError => e @@ -281,7 +281,7 @@ command to remove old versions. check_oldest_rubygems version installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement - installed_gems = update_gem("rubygems-update", version) if installed_gems.empty? || installed_gems.first.version != version + installed_gems = update_gem("rubygems-update", requirement) if installed_gems.empty? || installed_gems.first.version != version return if installed_gems.empty? install_rubygems installed_gems.first @@ -293,9 +293,7 @@ command to remove old versions. args << "--prefix" << Gem.prefix if Gem.prefix args << "--no-document" unless options[:document].include?("rdoc") || options[:document].include?("ri") args << "--no-format-executable" if options[:no_format_executable] - args << "--previous-version" << Gem::VERSION if - options[:system] == true || - Gem::Version.new(options[:system]) >= Gem::Version.new(2) + args << "--previous-version" << Gem::VERSION args end @@ -319,20 +317,10 @@ command to remove old versions. # # Oldest version we support downgrading to. This is the version that - # originally ships with the first patch version of each ruby, because we never - # test each ruby against older rubygems, so we can't really guarantee it - # works. Version list can be checked here: https://stdgems.org/rubygems + # originally ships with the oldest supported patch version of ruby. # def oldest_supported_version @oldest_supported_version ||= - if Gem.ruby_version > Gem::Version.new("3.1.a") - Gem::Version.new("3.3.3") - elsif Gem.ruby_version > Gem::Version.new("3.0.a") - Gem::Version.new("3.2.3") - elsif Gem.ruby_version > Gem::Version.new("2.7.a") - Gem::Version.new("3.1.2") - else - Gem::Version.new("3.0.1") - end + Gem::Version.new("3.3.3") end end diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb index 5b9a79b734..5ed4d9d142 100644 --- a/lib/rubygems/commands/which_command.rb +++ b/lib/rubygems/commands/which_command.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::WhichCommand < Gem::Command def initialize super "which", "Find the location of a library file you can require", - :search_gems_first => false, :show_all => false + search_gems_first: false, show_all: false add_option "-a", "--[no-]all", "show all matching files" do |show_all, options| options[:show_all] = show_all diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb index f4e962f201..fbdc262549 100644 --- a/lib/rubygems/commands/yank_command.rb +++ b/lib/rubygems/commands/yank_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -61,7 +62,7 @@ data you will need to change them immediately and yank your gem. end def yank_gem(version, platform) - say "Yanking gem from #{self.host}..." + say "Yanking gem from #{host}..." args = [:delete, version, platform, "api/v1/gems/yank"] response = yank_api_request(*args) |
