summaryrefslogtreecommitdiff
path: root/lib/rubygems/commands
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/commands')
-rw-r--r--lib/rubygems/commands/build_command.rb120
-rw-r--r--lib/rubygems/commands/cert_command.rb325
-rw-r--r--lib/rubygems/commands/check_command.rb97
-rw-r--r--lib/rubygems/commands/cleanup_command.rb178
-rw-r--r--lib/rubygems/commands/contents_command.rb196
-rw-r--r--lib/rubygems/commands/dependency_command.rb206
-rw-r--r--lib/rubygems/commands/environment_command.rb182
-rw-r--r--lib/rubygems/commands/exec_command.rb259
-rw-r--r--lib/rubygems/commands/fetch_command.rb109
-rw-r--r--lib/rubygems/commands/generate_index_command.rb51
-rw-r--r--lib/rubygems/commands/help_command.rb377
-rw-r--r--lib/rubygems/commands/info_command.rb38
-rw-r--r--lib/rubygems/commands/install_command.rb268
-rw-r--r--lib/rubygems/commands/list_command.rb42
-rw-r--r--lib/rubygems/commands/lock_command.rb109
-rw-r--r--lib/rubygems/commands/mirror_command.rb26
-rw-r--r--lib/rubygems/commands/open_command.rb83
-rw-r--r--lib/rubygems/commands/outdated_command.rb33
-rw-r--r--lib/rubygems/commands/owner_command.rb125
-rw-r--r--lib/rubygems/commands/pristine_command.rb223
-rw-r--r--lib/rubygems/commands/push_command.rb185
-rw-r--r--lib/rubygems/commands/rdoc_command.rb90
-rw-r--r--lib/rubygems/commands/rebuild_command.rb261
-rw-r--r--lib/rubygems/commands/search_command.rb41
-rw-r--r--lib/rubygems/commands/server_command.rb26
-rw-r--r--lib/rubygems/commands/setup_command.rb667
-rw-r--r--lib/rubygems/commands/signin_command.rb34
-rw-r--r--lib/rubygems/commands/signout_command.rb32
-rw-r--r--lib/rubygems/commands/sources_command.rb348
-rw-r--r--lib/rubygems/commands/specification_command.rb156
-rw-r--r--lib/rubygems/commands/stale_command.rb40
-rw-r--r--lib/rubygems/commands/uninstall_command.rb204
-rw-r--r--lib/rubygems/commands/unpack_command.rb168
-rw-r--r--lib/rubygems/commands/update_command.rb326
-rw-r--r--lib/rubygems/commands/which_command.rb88
-rw-r--r--lib/rubygems/commands/yank_command.rb99
36 files changed, 5812 insertions, 0 deletions
diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb
new file mode 100644
index 0000000000..cfe1f8ec3c
--- /dev/null
+++ b/lib/rubygems/commands/build_command.rb
@@ -0,0 +1,120 @@
+# 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"
+
+ add_platform_option
+
+ 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 "-o", "--output FILE", "output gem with the given filename" do |value, options|
+ options[:output] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMSPEC_FILE gemspec file name to build a gem for"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The build command allows you to create a gem from a ruby gemspec.
+
+The best way to build a gem is to use a Rakefile and the Gem::PackageTask
+which ships with RubyGems.
+
+The gemspec can either be created by hand or extracted from an existing gem
+with gem spec:
+
+ $ gem unpack my_gem-1.0.gem
+ Unpacked gem: '.../my_gem-1.0'
+ $ gem spec my_gem-1.0.gem --ruby > my_gem-1.0/my_gem-1.0.gemspec
+ $ cd my_gem-1.0
+ [edit gem contents]
+ $ gem build my_gem-1.0.gemspec
+
+Gems can be saved to a specified filename with the output option:
+
+ $ gem build my_gem-1.0.gemspec --output=release.gem
+
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMSPEC_FILE"
+ end
+
+ def execute
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem }
+ return
+ end
+
+ build_gem
+ end
+
+ private
+
+ def build_gem
+ gemspec = resolve_gem_name
+
+ if gemspec
+ build_package(gemspec)
+ else
+ alert_error error_message
+ terminate_interaction(1)
+ end
+ end
+
+ def build_package(gemspec)
+ spec = Gem::Specification.load(gemspec)
+ if spec
+ Gem::Package.build(
+ spec,
+ options[:force],
+ options[:strict],
+ options[:output]
+ )
+ else
+ alert_error "Error loading gemspec. Aborting."
+ terminate_interaction 1
+ end
+ end
+
+ def resolve_gem_name
+ return find_gemspec unless gem_name
+
+ if File.exist?(gem_name)
+ gem_name
+ else
+ find_gemspec("#{gem_name}.gemspec") || find_gemspec(gem_name)
+ end
+ end
+
+ def error_message
+ 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 gem_name
+ get_one_optional_argument
+ end
+end
diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb
new file mode 100644
index 0000000000..fe03841ddb
--- /dev/null
+++ b/lib/rubygems/commands/cert_command.rb
@@ -0,0 +1,325 @@
+# 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_option("-a", "--add CERT",
+ "Add a trusted certificate.") do |cert_file, options|
+ options[:add] << open_cert(cert_file)
+ end
+
+ add_option("-l", "--list [FILTER]",
+ "List trusted certificates where the",
+ "subject contains FILTER") do |filter, options|
+ filter ||= ""
+
+ options[:list] << filter
+ end
+
+ add_option("-r", "--remove FILTER",
+ "Remove trusted certificates where the",
+ "subject contains FILTER") do |filter, options|
+ options[:remove] << filter
+ end
+
+ add_option("-b", "--build EMAIL_ADDR",
+ "Build private key and self-signed",
+ "certificate for EMAIL_ADDR") do |email_address, options|
+ options[:build] << email_address
+ end
+
+ add_option("-C", "--certificate CERT",
+ "Signing certificate for --sign") do |cert_file, options|
+ options[:issuer_cert] = open_cert(cert_file)
+ options[:issuer_cert_file] = cert_file
+ end
+
+ add_option("-K", "--private-key KEY",
+ "Key for --sign or --build") do |key_file, options|
+ options[:key] = open_private_key(key_file)
+ end
+
+ add_option("-A", "--key-algorithm ALGORITHM",
+ "Select which key algorithm to use for --build") do |algorithm, options|
+ options[:key_algorithm] = algorithm
+ end
+
+ add_option("-s", "--sign CERT",
+ "Signs CERT with the key from -K",
+ "and the certificate from -C") do |cert_file, options|
+ raise Gem::OptionParser::InvalidArgument, "#{cert_file}: does not exist" unless
+ File.file? cert_file
+
+ options[:sign] << cert_file
+ end
+
+ add_option("-d", "--days NUMBER_OF_DAYS",
+ "Days before the certificate expires") do |days, options|
+ options[:expiration_length_days] = days.to_i
+ end
+
+ add_option("-R", "--re-sign",
+ "Re-signs the certificate from -C with the key from -K") do |resign, options|
+ options[:resign] = resign
+ end
+ end
+
+ def add_certificate(certificate) # :nodoc:
+ Gem::Security.trust_dir.trust_cert certificate
+
+ say "Added '#{certificate.subject}'"
+ end
+
+ def check_openssl
+ return if Gem::HAVE_OPENSSL
+
+ alert_error "OpenSSL library is required for the cert command"
+ terminate_interaction 1
+ end
+
+ def open_cert(certificate_file)
+ check_openssl
+ OpenSSL::X509::Certificate.new File.read certificate_file
+ rescue Errno::ENOENT
+ raise Gem::OptionParser::InvalidArgument, "#{certificate_file}: does not exist"
+ rescue OpenSSL::X509::CertificateError
+ raise Gem::OptionParser::InvalidArgument,
+ "#{certificate_file}: invalid X509 certificate"
+ end
+
+ def open_private_key(key_file)
+ check_openssl
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ key = OpenSSL::PKey.read File.read(key_file), passphrase
+ raise Gem::OptionParser::InvalidArgument,
+ "#{key_file}: private key not found" unless key.private?
+ key
+ rescue Errno::ENOENT
+ raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist"
+ rescue OpenSSL::PKey::PKeyError, ArgumentError
+ raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key"
+ end
+
+ def execute
+ check_openssl
+
+ options[:add].each do |certificate|
+ add_certificate certificate
+ end
+
+ options[:remove].each do |filter|
+ remove_certificates_matching filter
+ end
+
+ options[:list].each do |filter|
+ list_certificates_matching filter
+ end
+
+ options[:build].each do |email|
+ build email
+ end
+
+ if options[:resign]
+ re_sign_cert(
+ options[:issuer_cert],
+ options[:issuer_cert_file],
+ options[:key]
+ )
+ end
+
+ sign_certificates unless options[:sign].empty?
+ end
+
+ def build(email)
+ unless valid_email?(email)
+ raise Gem::CommandLineError, "Invalid email address #{email}"
+ end
+
+ key, key_path = build_key
+ cert_path = build_cert email, key
+
+ say "Certificate: #{cert_path}"
+
+ if key_path
+ say "Private Key: #{key_path}"
+ say "Don't forget to move the key file to somewhere private!"
+ end
+ end
+
+ def build_cert(email, key) # :nodoc:
+ expiration_length_days = options[:expiration_length_days] ||
+ Gem.configuration.cert_expiration_length_days
+
+ cert = Gem::Security.create_cert_email(
+ email,
+ key,
+ Gem::Security::ONE_DAY * expiration_length_days
+ )
+
+ Gem::Security.write cert, "gem-public_cert.pem"
+ end
+
+ def build_key # :nodoc:
+ return options[:key] if options[:key]
+
+ passphrase = ask_for_password "Passphrase for your Private Key:"
+ say "\n"
+
+ passphrase_confirmation = ask_for_password "Please repeat the passphrase for your Private Key:"
+ say "\n"
+
+ raise Gem::CommandLineError,
+ "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation
+
+ 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", 0o600, passphrase
+
+ [key, key_path]
+ end
+
+ def certificates_matching(filter)
+ return enum_for __method__, filter unless block_given?
+
+ Gem::Security.trusted_certificates.select do |certificate, _|
+ subject = certificate.subject.to_s
+ subject.downcase.index filter
+ end.sort_by do |certificate, _|
+ certificate.subject.to_a.map {|name, data,| [name, data] }
+ end.each do |certificate, path|
+ yield certificate, path
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The cert command manages signing keys and certificates for creating signed
+gems. Your signing certificate and private key are typically stored in
+~/.gem/gem-public_cert.pem and ~/.gem/gem-private_key.pem respectively.
+
+To build a certificate for signing gems:
+
+ gem cert --build you@example
+
+If you already have an RSA key, or are creating a new certificate for an
+existing key:
+
+ gem cert --build you@example --private-key /path/to/key.pem
+
+If you wish to trust a certificate you can add it to the trust list with:
+
+ gem cert --add /path/to/cert.pem
+
+You can list trusted certificates with:
+
+ gem cert --list
+
+or:
+
+ gem cert --list cert_subject_substring
+
+If you wish to remove a previously trusted certificate:
+
+ gem cert --remove cert_subject_substring
+
+To sign another gem author's certificate:
+
+ gem cert --sign /path/to/other_cert.pem
+
+For further reading on signing gems see `ri Gem::Security`.
+ EOF
+ end
+
+ def list_certificates_matching(filter) # :nodoc:
+ certificates_matching filter do |certificate, _|
+ # this could probably be formatted more gracefully
+ say certificate.subject.to_s
+ end
+ end
+
+ def load_default_cert
+ cert_file = File.join Gem.default_cert_path
+ cert = File.read cert_file
+ options[:issuer_cert] = OpenSSL::X509::Certificate.new cert
+ rescue Errno::ENOENT
+ alert_error \
+ "--certificate not specified and ~/.gem/gem-public_cert.pem does not exist"
+
+ terminate_interaction 1
+ rescue OpenSSL::X509::CertificateError
+ alert_error \
+ "--certificate not specified and ~/.gem/gem-public_cert.pem is not valid"
+
+ terminate_interaction 1
+ end
+
+ def load_default_key
+ key_file = File.join Gem.default_key_path
+ key = File.read key_file
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ options[:key] = OpenSSL::PKey.read key, passphrase
+ rescue Errno::ENOENT
+ alert_error \
+ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist"
+
+ terminate_interaction 1
+ rescue OpenSSL::PKey::PKeyError
+ alert_error \
+ "--private-key not specified and ~/.gem/gem-private_key.pem is not valid"
+
+ terminate_interaction 1
+ end
+
+ def load_defaults # :nodoc:
+ load_default_cert unless options[:issuer_cert]
+ load_default_key unless options[:key]
+ end
+
+ def remove_certificates_matching(filter) # :nodoc:
+ certificates_matching filter do |certificate, path|
+ FileUtils.rm path
+ say "Removed '#{certificate.subject}'"
+ end
+ end
+
+ def sign(cert_file)
+ cert = File.read cert_file
+ cert = OpenSSL::X509::Certificate.new cert
+
+ permissions = File.stat(cert_file).mode & 0o777
+
+ issuer_cert = options[:issuer_cert]
+ issuer_key = options[:key]
+
+ cert = Gem::Security.sign cert, issuer_key, issuer_cert
+
+ Gem::Security.write cert, cert_file, permissions
+ end
+
+ def sign_certificates # :nodoc:
+ load_defaults unless options[:sign].empty?
+
+ options[:sign].each do |cert_file|
+ sign cert_file
+ end
+ end
+
+ def re_sign_cert(cert, cert_path, private_key)
+ Gem::Security::Signer.re_sign_cert(cert, cert_path, private_key) do |expired_cert_path, new_expired_cert_path|
+ alert("Your certificate #{expired_cert_path} has been re-signed")
+ alert("Your expired certificate will be located at: #{new_expired_cert_path}")
+ end
+ end
+
+ private
+
+ def valid_email?(email)
+ # It's simple, but is all we need
+ email =~ /\A.+@.+\z/
+ end
+end
diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb
new file mode 100644
index 0000000000..fb23dd9cb4
--- /dev/null
+++ b/lib/rubygems/commands/check_command.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../validator"
+require_relative "../doctor"
+
+class Gem::Commands::CheckCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "check", "Check a gem repository for added or missing files",
+ alien: true, doctor: false, dry_run: false, gems: true
+
+ add_option("-a", "--[no-]alien",
+ 'Report "unmanaged" or rogue files in the',
+ "gem repository") do |value, options|
+ options[:alien] = value
+ end
+
+ add_option("--[no-]doctor",
+ "Clean up uninstalled gems and broken",
+ "specifications") do |value, options|
+ options[:doctor] = value
+ end
+
+ add_option("--[no-]dry-run",
+ "Do not remove files, only report what",
+ "would be removed") do |value, options|
+ options[:dry_run] = value
+ end
+
+ add_option("--[no-]gems",
+ "Check installed gems for problems") do |value, options|
+ options[:gems] = value
+ end
+
+ add_version_option "check"
+ end
+
+ def check_gems
+ say "Checking gems..."
+ say
+ gems = begin
+ get_all_gem_names
+ rescue StandardError
+ []
+ end
+
+ Gem::Validator.new.alien(gems).sort.each do |key, val|
+ if val.empty?
+ say "#{key} is error-free" if Gem.configuration.verbose
+ else
+ say "#{key} has #{val.size} problems"
+ val.each do |error_entry|
+ say " #{error_entry.path}:"
+ say " #{error_entry.problem}"
+ end
+ end
+ say
+ end
+ end
+
+ def doctor
+ say "Checking for files from uninstalled gems..."
+ say
+
+ Gem.path.each do |gem_repo|
+ doctor = Gem::Doctor.new gem_repo, options[:dry_run]
+ doctor.doctor
+ end
+ end
+
+ def execute
+ check_gems if options[:gems]
+ doctor if options[:doctor]
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to check"
+ end
+
+ def defaults_str # :nodoc:
+ "--gems --alien"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The check command can list and repair problems with installed gems and
+specifications and will clean up gems that have been partially uninstalled.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [OPTIONS] [GEMNAME ...]"
+ end
+end
diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb
new file mode 100644
index 0000000000..c89a24eee9
--- /dev/null
+++ b/lib/rubygems/commands/cleanup_command.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../dependency_list"
+require_relative "../uninstaller"
+
+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
+
+ add_option("-n", "-d", "--dry-run",
+ "Do not uninstall gems") do |_value, options|
+ options[:dryrun] = true
+ end
+
+ add_option(:Deprecated, "--dryrun",
+ "Do not uninstall gems") do |_value, options|
+ options[:dryrun] = true
+ end
+ deprecate_option("--dryrun", extra_msg: "Use --dry-run instead")
+
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: true)") do |value, options|
+ options[:check_dev] = value
+ end
+
+ add_option("--[no-]user-install",
+ "Cleanup in user's home directory instead",
+ "of GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
+
+ @candidate_gems = nil
+ @default_gems = []
+ @full = nil
+ @gems_to_cleanup = nil
+ @primary_gems = nil
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to cleanup"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-dry-run"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The cleanup command removes old versions of gems from GEM_HOME that are not
+required to meet a dependency. If a gem is installed elsewhere in GEM_PATH
+the cleanup command won't delete it.
+
+If no gems are named all gems in GEM_HOME are cleaned.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [GEMNAME ...]"
+ end
+
+ def execute
+ say "Cleaning up installed gems..."
+
+ if options[:args].empty?
+ done = false
+ last_set = nil
+
+ until done do
+ clean_gems
+
+ this_set = @gems_to_cleanup.map(&:full_name).sort
+
+ done = this_set.empty? || last_set == this_set
+
+ last_set = this_set
+ end
+ else
+ clean_gems
+ end
+
+ say "Clean up complete"
+
+ verbose do
+ skipped = @default_gems.map(&:full_name)
+
+ "Skipped default gems: #{skipped.join ", "}"
+ end
+ end
+
+ def clean_gems
+ get_primary_gems
+ get_candidate_gems
+ get_gems_to_cleanup
+
+ @full = Gem::DependencyList.from_specs
+
+ deplist = Gem::DependencyList.new
+ @gems_to_cleanup.each {|spec| deplist.add spec }
+
+ deps = deplist.strongly_connected_components.flatten
+
+ deps.reverse_each do |spec|
+ uninstall_dep spec
+ end
+ end
+
+ def get_candidate_gems
+ @candidate_gems = if options[:args].empty?
+ Gem::Specification.to_a
+ else
+ options[:args].flat_map do |gem_name|
+ Gem::Specification.find_all_by_name gem_name
+ end
+ end
+ end
+
+ def get_gems_to_cleanup
+ gems_to_cleanup = @candidate_gems.select do |spec|
+ @primary_gems[spec.name].version != spec.version
+ end
+
+ default_gems, gems_to_cleanup = gems_to_cleanup.partition(&:default_gem?)
+
+ uninstall_from = options[:user_install] ? Gem.user_dir : Gem.dir
+
+ gems_to_cleanup = gems_to_cleanup.select do |spec|
+ spec.base_dir == uninstall_from
+ end
+
+ @default_gems += default_gems
+ @default_gems.uniq!
+ @gems_to_cleanup = gems_to_cleanup.uniq
+ end
+
+ def get_primary_gems
+ @primary_gems = {}
+
+ Gem::Specification.each do |spec|
+ if @primary_gems[spec.name].nil? ||
+ @primary_gems[spec.name].version < spec.version
+ @primary_gems[spec.name] = spec
+ end
+ end
+ end
+
+ def uninstall_dep(spec)
+ return unless @full.ok_to_remove?(spec.full_name, options[:check_dev])
+
+ if options[:dryrun]
+ say "Dry Run Mode: Would uninstall #{spec.full_name}"
+ return
+ end
+
+ say "Attempting to uninstall #{spec.full_name}"
+
+ uninstall_options = {
+ executables: false,
+ version: "= #{spec.version}",
+ }
+
+ uninstall_options[:user_install] = Gem.user_dir == spec.base_dir
+
+ uninstaller = Gem::Uninstaller.new spec.name, uninstall_options
+
+ begin
+ uninstaller.uninstall
+ rescue Gem::DependencyRemovalException, Gem::InstallError,
+ Gem::GemNotInHomeException, Gem::FilePermissionError => e
+ say "Unable to uninstall #{spec.full_name}:"
+ say "\t#{e.class}: #{e.message}"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb
new file mode 100644
index 0000000000..d4f9871868
--- /dev/null
+++ b/lib/rubygems/commands/contents_command.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+
+class Gem::Commands::ContentsCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "contents", "Display the contents of the installed gems",
+ specdirs: [], lib_only: false, prefix: true,
+ show_install_dir: false
+
+ add_version_option
+
+ add_option("--all",
+ "Contents for all gems") do |all, options|
+ options[:all] = all
+ end
+
+ add_option("-s", "--spec-dir a,b,c", Array,
+ "Search for gems under specific paths") do |spec_dirs, options|
+ options[:specdirs] = spec_dirs
+ end
+
+ add_option("-l", "--[no-]lib-only",
+ "Only return files in the Gem's lib_dirs") do |lib_only, options|
+ options[:lib_only] = lib_only
+ end
+
+ add_option("--[no-]prefix",
+ "Don't include installed path prefix") do |prefix, options|
+ options[:prefix] = prefix
+ end
+
+ add_option("--[no-]show-install-dir",
+ "Show only the gem install dir") do |show, options|
+ options[:show_install_dir] = show
+ end
+
+ @path_kind = nil
+ @spec_dirs = nil
+ @version = nil
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to list contents for"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-lib-only --prefix"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The contents command lists the files in an installed gem. The listing can
+be given as full file names, file names without the installed directory
+prefix or only the files that are requireable.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def execute
+ @version = options[:version] || Gem::Requirement.default
+ @spec_dirs = specification_directories
+ @path_kind = path_description @spec_dirs
+
+ names = gem_names
+
+ names.each do |name|
+ found =
+ if options[:show_install_dir]
+ gem_install_dir name
+ else
+ gem_contents name
+ end
+
+ terminate_interaction 1 unless found || names.length > 1
+ end
+ end
+
+ def files_in(spec)
+ if spec.default_gem?
+ files_in_default_gem spec
+ else
+ files_in_gem spec
+ end
+ end
+
+ def files_in_gem(spec)
+ gem_path = spec.full_gem_path
+ extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only]
+ glob = "#{gem_path}#{extra}/**/*"
+ prefix_re = %r{#{Regexp.escape(gem_path)}/}
+
+ Dir[glob].map do |file|
+ [gem_path, file.sub(prefix_re, "")]
+ end
+ end
+
+ def files_in_default_gem(spec)
+ spec.files.filter_map do |file|
+ if file.start_with?("#{spec.bindir}/")
+ [RbConfig::CONFIG["bindir"], file.delete_prefix("#{spec.bindir}/")]
+ else
+ 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
+
+ def gem_contents(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ files = files_in spec
+
+ show_files files
+
+ true
+ end
+
+ def gem_install_dir(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ say spec.gem_dir
+
+ true
+ end
+
+ def gem_names # :nodoc:
+ if options[:all]
+ Gem::Specification.map(&:name)
+ else
+ get_all_gem_names
+ end
+ end
+
+ def path_description(spec_dirs) # :nodoc:
+ if spec_dirs.empty?
+ "default gem paths"
+ else
+ "specified path"
+ end
+ end
+
+ def show_files(files)
+ files.sort.each do |prefix, basename|
+ absolute_path = File.join(prefix, basename)
+ next if File.directory? absolute_path
+
+ if options[:prefix]
+ say absolute_path
+ else
+ say basename
+ end
+ end
+ end
+
+ def spec_for(name)
+ spec = Gem::Specification.find_all_by_name(name, @version).first
+
+ return spec if spec
+
+ say "Unable to find gem '#{name}' in #{@path_kind}"
+
+ if Gem.configuration.verbose
+ say "\nDirectories searched:"
+ @spec_dirs.sort.each {|dir| say dir }
+ end
+
+ nil
+ end
+
+ def specification_directories # :nodoc:
+ options[:specdirs].flat_map do |i|
+ [i, File.join(i, "specifications")]
+ end
+ end
+end
diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb
new file mode 100644
index 0000000000..9aaefae999
--- /dev/null
+++ b/lib/rubygems/commands/dependency_command.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+
+class Gem::Commands::DependencyCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ super "dependency",
+ "Show the dependencies of an installed gem",
+ version: Gem::Requirement.default, domain: :local
+
+ add_version_option
+ add_platform_option
+ add_prerelease_option
+
+ add_option("-R", "--[no-]reverse-dependencies",
+ "Include reverse dependencies in the output") do |value, options|
+ options[:reverse_dependencies] = value
+ end
+
+ add_option("-p", "--pipe",
+ "Pipe Format (name --version ver)") do |value, options|
+ options[:pipe_format] = value
+ end
+
+ add_local_remote_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP show dependencies for gems whose names start with REGEXP"
+ end
+
+ def defaults_str # :nodoc:
+ "--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The dependency commands lists which other gems a given gem depends on. For
+local gems only the reverse dependencies can be shown (which gems depend on
+the named gem).
+
+The dependency list can be displayed in a format suitable for piping for
+use with other commands.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} REGEXP"
+ end
+
+ def fetch_remote_specs(name, requirement, prerelease) # :nodoc:
+ fetcher = Gem::SpecFetcher.fetcher
+
+ specs_type = prerelease ? :complete : :released
+
+ ss = if name.nil?
+ fetcher.detect(specs_type) { true }
+ else
+ fetcher.detect(specs_type) do |name_tuple|
+ name === name_tuple.name && requirement.satisfied_by?(name_tuple.version)
+ end
+ end
+
+ ss.map {|tuple, source| source.fetch_spec(tuple) }
+ end
+
+ def fetch_specs(name_pattern, requirement, prerelease) # :nodoc:
+ specs = []
+
+ if local?
+ specs.concat Gem::Specification.stubs.find_all {|spec|
+ name_matches = name_pattern ? name_pattern =~ spec.name : true
+ version_matches = requirement.satisfied_by?(spec.version)
+
+ name_matches && version_matches
+ }.map(&:to_spec)
+ end
+
+ specs.concat fetch_remote_specs name_pattern, requirement, prerelease if remote?
+
+ ensure_specs specs
+
+ specs.uniq.sort
+ end
+
+ def display_pipe(specs) # :nodoc:
+ specs.each do |spec|
+ next if spec.dependencies.empty?
+ spec.dependencies.sort_by(&:name).each do |dep|
+ say "#{dep.name} --version '#{dep.requirement}'"
+ end
+ end
+ end
+
+ def display_readable(specs, reverse) # :nodoc:
+ response = String.new
+
+ specs.each do |spec|
+ response << print_dependencies(spec)
+ unless reverse[spec.full_name].empty?
+ response << " Used by\n"
+ reverse[spec.full_name].each do |sp, dep|
+ response << " #{sp} (#{dep})\n"
+ end
+ end
+ response << "\n"
+ end
+
+ say response
+ end
+
+ def execute
+ ensure_local_only_reverse_dependencies
+
+ pattern = name_pattern options[:args]
+ requirement = Gem::Requirement.new options[:version]
+
+ specs = fetch_specs pattern, requirement, options[:prerelease]
+
+ reverse = reverse_dependencies specs
+
+ if options[:pipe_format]
+ display_pipe specs
+ else
+ display_readable specs, reverse
+ end
+ end
+
+ def ensure_local_only_reverse_dependencies # :nodoc:
+ if options[:reverse_dependencies] && remote? && !local?
+ alert_error "Only reverse dependencies for local gems are supported."
+ terminate_interaction 1
+ end
+ end
+
+ def ensure_specs(specs) # :nodoc:
+ return unless specs.empty?
+
+ patterns = options[:args].join ","
+ say "No gems found matching #{patterns} (#{options[:version]})" if
+ Gem.configuration.verbose
+
+ terminate_interaction 1
+ end
+
+ def print_dependencies(spec, level = 0) # :nodoc:
+ response = String.new
+ response << " " * level + "Gem #{spec.full_name}\n"
+ unless spec.dependencies.empty?
+ spec.dependencies.sort_by(&:name).each do |dep|
+ response << " " * level + " #{dep}\n"
+ end
+ end
+ response
+ end
+
+ def reverse_dependencies(specs) # :nodoc:
+ reverse = Hash.new {|h, k| h[k] = [] }
+
+ return reverse unless options[:reverse_dependencies]
+
+ specs.each do |spec|
+ reverse[spec.full_name] = find_reverse_dependencies spec
+ end
+
+ reverse
+ end
+
+ ##
+ # Returns an Array of [specification, dep] that are satisfied by +spec+.
+
+ def find_reverse_dependencies(spec) # :nodoc:
+ result = []
+
+ Gem::Specification.each do |sp|
+ sp.dependencies.each do |dep|
+ dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep
+
+ if spec.name == dep.name &&
+ dep.requirement.satisfied_by?(spec.version)
+ result << [sp.full_name, dep]
+ end
+ end
+ end
+
+ result
+ end
+
+ private
+
+ def name_pattern(args)
+ return if args.empty?
+
+ if args.length == 1 && args.first =~ /\A(.*)(i)?\z/m
+ flags = $2 ? Regexp::IGNORECASE : nil
+ Regexp.new $1, flags
+ else
+ /\A#{Regexp.union(*args)}/
+ end
+ end
+end
diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb
new file mode 100644
index 0000000000..a5eb521a53
--- /dev/null
+++ b/lib/rubygems/commands/environment_command.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::EnvironmentCommand < Gem::Command
+ def initialize
+ super "environment", "Display information about the RubyGems environment"
+ end
+
+ def arguments # :nodoc:
+ args = <<-EOF
+ home display the path where gems are installed. Aliases: gemhome, gemdir, GEM_HOME
+ path display path used to search for gems. Aliases: gempath, GEM_PATH
+ user_gemhome display the path where gems are installed when `--user-install` is given. Aliases: user_gemdir
+ 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
+ args.gsub(/^\s+/, "")
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The environment command lets you query rubygems for its configuration for
+use in shell scripts or as a debugging aid.
+
+The RubyGems environment can be controlled through command line arguments,
+gemrc files, environment variables and built-in defaults.
+
+Command line argument defaults and some RubyGems defaults can be set in a
+~/.gemrc file for individual users and a gemrc in the SYSTEM CONFIGURATION
+DIRECTORY for all users. These files are YAML files with the following YAML
+keys:
+
+ :sources: A YAML array of remote gem repositories to install gems from
+ :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
+ <gem_command>: A string containing arguments for the specified gem command
+
+Example:
+
+ :verbose: false
+ install: --no-wrappers
+ update: --no-wrappers
+ :disable_default_gem_server: true
+
+RubyGems' default local repository can be overridden with the GEM_PATH and
+GEM_HOME environment variables. GEM_HOME sets the default repository to
+install into. GEM_PATH allows multiple local repositories to be searched for
+gems.
+
+If you are behind a proxy server, RubyGems uses the HTTP_PROXY,
+HTTP_PROXY_USER and HTTP_PROXY_PASS environment variables to discover the
+proxy server.
+
+If you would like to push gems to a private gem server the RUBYGEMS_HOST
+environment variable can be set to the URI for that server.
+
+If you are packaging RubyGems all of RubyGems' defaults are in
+lib/rubygems/defaults.rb. You may override these in
+lib/rubygems/defaults/operating_system.rb
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [arg]"
+ end
+
+ def execute
+ out = String.new
+ arg = options[:args][0]
+ out <<
+ case arg
+ when /^version/ then
+ Gem::VERSION
+ when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then
+ Gem.dir
+ when /^gempath/, /^path/, /^GEM_PATH/ then
+ Gem.path.join(File::PATH_SEPARATOR)
+ when /^user_gemdir/, /^user_gemhome/ then
+ Gem.user_dir
+ when /^remotesources/ then
+ 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
+ raise Gem::CommandLineError, "Unknown environment option [#{arg}]"
+ end
+ say out
+ true
+ end
+
+ def add_path(out, path)
+ path.each do |component|
+ out << " - #{component}\n"
+ end
+ end
+
+ def show_environment # :nodoc:
+ out = "RubyGems Environment:\n".dup
+
+ out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n"
+
+ out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE} patchlevel #{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]\n"
+
+ out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n"
+
+ 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"
+
+ out << " - GIT EXECUTABLE: #{git_path}\n"
+
+ out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n"
+
+ out << " - SPEC CACHE DIRECTORY: #{Gem.spec_cache_dir}\n"
+
+ out << " - SYSTEM CONFIGURATION DIRECTORY: #{Gem::ConfigFile::SYSTEM_CONFIG_PATH}\n"
+
+ out << " - RUBYGEMS PLATFORMS:\n"
+ Gem.platforms.each do |platform|
+ out << " - #{platform}\n"
+ end
+
+ out << " - GEM PATHS:\n"
+ out << " - #{Gem.dir}\n"
+
+ gem_path = Gem.path.dup
+ gem_path.delete Gem.dir
+ add_path out, gem_path
+
+ out << " - GEM CONFIGURATION:\n"
+ Gem.configuration.each do |name, value|
+ value = value.gsub(/./, "*") if name == "gemcutter_key"
+ out << " - #{name.inspect} => #{value.inspect}\n"
+ end
+
+ out << " - REMOTE SOURCES:\n"
+ Gem.sources.each do |s|
+ out << " - #{s}\n"
+ end
+
+ out << " - SHELL PATH:\n"
+
+ shell_path = ENV["PATH"].split(File::PATH_SEPARATOR)
+ add_path out, shell_path
+
+ out
+ end
+
+ private
+
+ ##
+ # Git binary path
+
+ def git_path
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
+ exts.each do |ext|
+ exe = File.join(path, "git#{ext}")
+ return exe if File.executable?(exe) && !File.directory?(exe)
+ end
+ end
+
+ nil
+ end
+end
diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb
new file mode 100644
index 0000000000..1feafbdd35
--- /dev/null
+++ b/lib/rubygems/commands/exec_command.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../dependency_installer"
+require_relative "../gem_runner"
+require_relative "../package"
+require_relative "../version_option"
+
+class Gem::Commands::ExecCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "exec", "Run a command from a gem", {
+ version: Gem::Requirement.default,
+ }
+
+ add_version_option
+ add_prerelease_option "to be installed"
+
+ add_option "-g", "--gem GEM", "run the executable from the given gem" do |value, options|
+ options[:gem_name] = value
+ end
+
+ add_option(:"Install/Update", "--conservative",
+ "Prefer the most recent installed version, ",
+ "rather than the latest version overall") do |_value, options|
+ options[:conservative] = true
+ end
+ end
+
+ def arguments # :nodoc:
+ "COMMAND the executable command to run"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The exec command handles installing (if necessary) and running an executable
+from a gem, regardless of whether that gem is currently installed.
+
+The exec command can be thought of as a shortcut to running `gem install` and
+then the executable from the installed gem.
+
+For example, `gem exec rails new .` will run `rails new .` in the current
+directory, without having to manually run `gem install rails`.
+Additionally, the exec command ensures the most recent version of the gem
+is used (unless run with `--conservative`), and that the gem is not installed
+to the same gem path as user-installed gems.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [options --] COMMAND [args]"
+ end
+
+ def execute
+ check_executable
+
+ print_command
+ if options[:gem_name] == "gem" && options[:executable] == "gem"
+ set_gem_exec_install_paths
+ Gem::GemRunner.new.run options[:args]
+ return
+ elsif options[:conservative]
+ install_if_needed
+ else
+ install
+ activate!
+ end
+
+ load!
+ end
+
+ private
+
+ def handle_options(args)
+ args = add_extra_args(args)
+ check_deprecated_options(args)
+ @options = Marshal.load Marshal.dump @defaults # deep copy
+ parser.order!(args) do |v|
+ # put the non-option back at the front of the list of arguments
+ args.unshift(v)
+
+ # stop parsing once we hit the first non-option,
+ # so you can call `gem exec rails --version` and it prints the rails
+ # version rather than rubygem's
+ break
+ end
+ @options[:args] = args
+
+ options[:executable], gem_version = extract_gem_name_and_version(options[:args].shift)
+ options[:gem_name] ||= options[:executable]
+
+ if gem_version
+ if options[:version].none?
+ options[:version] = Gem::Requirement.new(gem_version)
+ else
+ options[:version].concat [gem_version]
+ end
+ end
+
+ if options[:prerelease] && !options[:version].prerelease?
+ if options[:version].none?
+ options[:version] = Gem::Requirement.default_prerelease
+ else
+ options[:version].concat [Gem::Requirement.default_prerelease]
+ end
+ end
+ end
+
+ def check_executable
+ if options[:executable].nil?
+ raise Gem::CommandLineError,
+ "Please specify an executable to run (e.g. #{program_name} COMMAND)"
+ end
+ end
+
+ def print_command
+ verbose "running #{program_name} with:\n"
+ opts = options.reject {|_, v| v.nil? || Array(v).empty? }
+ max_length = opts.map {|k, _| k.size }.max
+ opts.each do |k, v|
+ next if v.nil?
+ verbose "\t#{k.to_s.rjust(max_length)}: #{v}"
+ end
+ verbose ""
+ end
+
+ def install_if_needed
+ activate!
+ rescue Gem::MissingSpecError
+ verbose "#{Gem::Dependency.new(options[:gem_name], options[:version])} not available locally, installing from remote"
+ install
+ activate!
+ end
+
+ def set_gem_exec_install_paths
+ home = Gem.dir
+
+ ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR)
+ ENV["GEM_HOME"] = home
+ Gem.clear_paths
+ end
+
+ def install
+ set_gem_exec_install_paths
+
+ gem_name = options[:gem_name]
+ gem_version = options[:version]
+
+ install_options = options.merge(
+ minimal_deps: false,
+ wrappers: true
+ )
+
+ suppress_always_install do
+ dep_installer = Gem::DependencyInstaller.new install_options
+
+ request_set = dep_installer.resolve_dependencies gem_name, gem_version
+
+ verbose "Gems to install:"
+ request_set.sorted_requests.each do |activation_request|
+ verbose "\t#{activation_request.full_name}"
+ end
+
+ request_set.install install_options
+ end
+
+ Gem::Specification.reset
+ 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
+
+ terminate_interaction 2
+ rescue Gem::UnsatisfiableDependencyError => e
+ show_lookup_failure e.name, e.version, e.errors, false,
+ "'#{gem_name}' (#{gem_version})"
+
+ terminate_interaction 2
+ end
+
+ def activate!
+ gem(options[:gem_name], options[:version])
+ Gem.finish_resolve
+
+ verbose "activated #{options[:gem_name]} (#{Gem.loaded_specs[options[:gem_name]].version})"
+ end
+
+ def load!
+ argv = ARGV.clone
+ ARGV.replace options[:args]
+
+ executable = options[:executable]
+
+ contains_executable = Gem.loaded_specs.values.select do |spec|
+ spec.executables.include?(executable)
+ end
+
+ if contains_executable.any? {|s| s.name == executable }
+ contains_executable.select! {|s| s.name == executable }
+ end
+
+ if contains_executable.empty?
+ 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
+ alert_error "Ambiguous which gem `#{executable}` should come from: " \
+ "the options are #{contains_executable.map(&:name)}, " \
+ "specify one via `-g`"
+ terminate_interaction 1
+ end
+
+ 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
+
+ def suppress_always_install
+ name = :always_install
+ cls = ::Gem::Resolver::InstallerSet
+ method = cls.instance_method(name)
+ cls.remove_method(name)
+ cls.define_method(name) { [] }
+
+ begin
+ yield
+ ensure
+ cls.remove_method(name)
+ cls.define_method(name, method)
+ end
+ end
+end
diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb
new file mode 100644
index 0000000000..8e64a18cee
--- /dev/null
+++ b/lib/rubygems/commands/fetch_command.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+
+class Gem::Commands::FetchCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ defaults = {
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ }
+
+ super "fetch", "Download a gem and place it in the current directory", defaults
+
+ add_bulk_threshold_option
+ add_proxy_option
+ add_source_option
+ add_clear_sources_option
+
+ add_version_option
+ add_platform_option
+ add_prerelease_option
+
+ add_option "--[no-]suggestions", "Suggest alternates when gems are not found" do |value, options|
+ options[:suggest_alternate] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to download"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The fetch command fetches gem files that can be stored for later use or
+unpacked to examine their contents.
+
+See the build command help for an example of unpacking a gem, modifying it,
+then repackaging it.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ 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 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
+ gem_names = get_all_gem_names_and_versions
+
+ gem_names.each do |gem_name, gem_version|
+ gem_version ||= version
+ dep = Gem::Dependency.new gem_name, gem_version
+ dep.prerelease = options[:prerelease]
+ suppress_suggestions = !options[:suggest_alternate]
+
+ specs_and_sources, errors =
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ if platform
+ filtered = specs_and_sources.select {|s,| s.platform == platform }
+ specs_and_sources = filtered unless filtered.empty?
+ end
+
+ spec, source = specs_and_sources.max_by {|s,| s }
+
+ 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
new file mode 100644
index 0000000000..13be92593b
--- /dev/null
+++ b/lib/rubygems/commands/generate_index_command.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+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
+
+ def execute
+ alert_error "Install the rubygems-generate_index gem for the generate_index command"
+ end
+
+ 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
+
+ # 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
+
+ prepend(RubygemsTrampoline)
+ end
+end
diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb
new file mode 100644
index 0000000000..664f400561
--- /dev/null
+++ b/lib/rubygems/commands/help_command.rb
@@ -0,0 +1,377 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::HelpCommand < Gem::Command
+ # :stopdoc:
+ EXAMPLES = <<-EOF
+Some examples of 'gem' usage.
+
+* Install 'rake', either from local directory or remote server:
+
+ gem install rake
+
+* Install 'rake', only from remote server:
+
+ gem install rake --remote
+
+* Install 'rake', but only version 0.3.1, even if dependencies
+ are not met, and into a user-specific directory:
+
+ gem install rake --version 0.3.1 --force --user-install
+
+* List local gems whose name begins with 'D':
+
+ gem list D
+
+* List local and remote gems whose name contains 'log':
+
+ gem search log --both
+
+* List only remote gems whose name contains 'log':
+
+ gem search log --remote
+
+* Uninstall 'rake':
+
+ gem uninstall rake
+
+* Create a gem:
+
+ See https://guides.rubygems.org/make-your-own-gem/
+
+* See information about RubyGems:
+
+ gem environment
+
+* Update all gems on your system:
+
+ gem update
+
+* Update your local version of RubyGems
+
+ gem update --system
+ EOF
+
+ GEM_DEPENDENCIES = <<-EOF
+A gem dependencies file allows installation of a consistent set of gems across
+multiple environments. The RubyGems implementation is designed to be
+compatible with Bundler's Gemfile format. You can see additional
+documentation on the format at:
+
+ https://bundler.io
+
+RubyGems automatically looks for these gem dependencies files:
+
+* gem.deps.rb
+* Gemfile
+* Isolate
+
+These files are looked up automatically using `gem install -g`, or you can
+specify a custom file.
+
+When the RUBYGEMS_GEMDEPS environment variable is set to a gem dependencies
+file the gems from that file will be activated at startup time. Set it to a
+specific filename or to "-" to have RubyGems automatically discover the gem
+dependencies file by walking up from the current directory.
+
+You can also activate gem dependencies at program startup using
+Gem.use_gemdeps.
+
+NOTE: Enabling automatic discovery on multiuser systems can lead to execution
+of arbitrary code when used from directories outside your control.
+
+Gem Dependencies
+================
+
+Use #gem to declare which gems you directly depend upon:
+
+ gem 'rake'
+
+To depend on a specific set of versions:
+
+ 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
+require: option to override this behavior if the gem does not have a file of
+that name or you don't want to require those files:
+
+ gem 'my_gem', require: 'other_file'
+
+To prevent RubyGems from requiring any files use:
+
+ gem 'my_gem', require: false
+
+To load dependencies from a .gemspec file:
+
+ gemspec
+
+RubyGems looks for the first .gemspec file in the current directory. To
+override this use the name: option:
+
+ gemspec name: 'specific_gem'
+
+To look in a different directory use the path: option:
+
+ gemspec name: 'specific_gem', path: 'gemspecs'
+
+To depend on a gem unpacked into a local directory:
+
+ gem 'modified_gem', path: 'vendor/modified_gem'
+
+To depend on a gem from git:
+
+ gem 'private_gem', git: 'git@my.company.example:private_gem.git'
+
+To depend on a gem from github:
+
+ gem 'private_gem', github: 'my_company/private_gem'
+
+To depend on a gem from a github gist:
+
+ gem 'bang', gist: '1232884'
+
+Git, github and gist support the ref:, branch: and tag: options to specify a
+commit reference or hash, branch or tag respectively to use for the gem.
+
+Setting the submodules: option to true for git, github and gist dependencies
+causes fetching of submodules when fetching the repository.
+
+You can depend on multiple gems from a single repository with the git method:
+
+ git 'https://github.com/rails/rails.git' do
+ gem 'activesupport'
+ gem 'activerecord'
+ end
+
+Gem Sources
+===========
+
+RubyGems uses the default sources for regular `gem install` for gem
+dependencies files. Unlike bundler, you do need to specify a source.
+
+You can override the sources used for downloading gems with:
+
+ source 'https://gem_server.example'
+
+You may specify multiple sources. Unlike bundler the prepend: option is not
+supported. Sources are used in-order, to prepend a source place it at the
+front of the list.
+
+Gem Platform
+============
+
+You can restrict gem dependencies to specific platforms with the #platform
+and #platforms methods:
+
+ platform :ruby_21 do
+ gem 'debugger'
+ end
+
+See the bundler Gemfile manual page for a list of platforms supported in a gem
+dependencies file.:
+
+ https://bundler.io/v2.5/man/gemfile.5.html
+
+Ruby Version and Engine Dependency
+==================================
+
+You can specify the version, engine and engine version of ruby to use with
+your gem dependencies file. If you are not running the specified version
+RubyGems will raise an exception.
+
+To depend on a specific version of ruby:
+
+ ruby '2.1.2'
+
+To depend on a specific ruby engine:
+
+ ruby '1.9.3', engine: 'jruby'
+
+To depend on a specific ruby engine version:
+
+ ruby '1.9.3', engine: 'jruby', engine_version: '1.7.11'
+
+Grouping Dependencies
+=====================
+
+Gem dependencies may be placed in groups that can be excluded from install.
+Dependencies required for development or testing of your code may be excluded
+when installed in a production environment.
+
+A #gem dependency may be placed in a group using the group: option:
+
+ gem 'minitest', group: :test
+
+To install dependencies from a gemfile without specific groups use the
+`--without` option for `gem install -g`:
+
+ $ gem install -g --without test
+
+The group: option also accepts multiple groups if the gem fits in multiple
+categories.
+
+Multiple groups may be excluded during install by comma-separating the groups for `--without` or by specifying `--without` multiple times.
+
+The #group method can also be used to place gems in groups:
+
+ group :test do
+ gem 'minitest'
+ gem 'minitest-emoji'
+ end
+
+The #group method allows multiple groups.
+
+The #gemspec development dependencies are placed in the :development group by
+default. This may be overridden with the :development_group option:
+
+ gemspec development_group: :other
+
+ EOF
+
+ PLATFORMS = <<-'EOF'
+RubyGems platforms are composed of three parts, a CPU, an OS, and a
+version. These values are taken from values in rbconfig.rb. You can view
+your current platform by running `gem environment`.
+
+RubyGems matches platforms as follows:
+
+ * The CPU must match exactly unless one of the platforms has
+ "universal" as the CPU or the local CPU starts with "arm" and the gem's
+ CPU is exactly "arm" (for gems that support generic ARM architecture).
+ * The OS must match exactly.
+ * The versions must match exactly unless one of the versions is nil.
+
+For commands that install, uninstall and list gems, you can override what
+RubyGems thinks your platform is with the --platform option. The platform
+you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}". On mswin
+platforms, the version is the compiler version, not the OS version. (Ruby
+compiled with VC6 uses "60" as the compiler version, VC8 uses "80".)
+
+For the ARM architecture, gems with a platform of "arm-linux" should run on a
+reasonable set of ARM CPUs and not depend on instructions present on a limited
+subset of the architecture. For example, the binary should run on platforms
+armv5, armv6hf, armv6l, armv7, etc. If you use the "arm-linux" platform
+please test your gem on a variety of ARM hardware before release to ensure it
+functions correctly.
+
+Example platforms:
+
+ x86-freebsd # Any FreeBSD version on an x86 CPU
+ universal-darwin-8 # Darwin 8 only gems that run on any CPU
+ x86-mswin32-80 # Windows gems compiled with VC8
+ armv7-linux # Gem complied for an ARMv7 CPU running linux
+ arm-linux # Gem compiled for any ARM CPU running linux
+
+When building platform gems, set the platform in the gem specification to
+Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's
+platform.
+ EOF
+
+ # NOTE: when updating also update Gem::Command::HELP
+
+ SUBCOMMANDS = [
+ ["commands", :show_commands],
+ ["options", Gem::Command::HELP],
+ ["examples", EXAMPLES],
+ ["gem_dependencies", GEM_DEPENDENCIES],
+ ["platforms", PLATFORMS],
+ ].freeze
+ # :startdoc:
+
+ def initialize
+ super "help", "Provide help on the 'gem' command"
+
+ @command_manager = Gem::CommandManager.instance
+ end
+
+ def usage # :nodoc:
+ "#{program_name} ARGUMENT"
+ end
+
+ def execute
+ arg = options[:args][0]
+
+ _, help = SUBCOMMANDS.find do |command,|
+ begins? command, arg
+ end
+
+ if help
+ if Symbol === help
+ send help
+ else
+ say help
+ end
+ return
+ end
+
+ if options[:help]
+ show_help
+
+ elsif arg
+ show_command_help arg
+
+ else
+ say Gem::Command::HELP
+ end
+ end
+
+ def show_commands # :nodoc:
+ out = []
+ out << "GEM commands are:"
+ out << nil
+
+ margin_width = 4
+
+ desc_width = @command_manager.command_names.map(&:size).max + 4
+
+ summary_width = 80 - margin_width - desc_width
+ wrap_indent = " " * (margin_width + desc_width)
+ format = "#{" " * margin_width}%-#{desc_width}s%s"
+
+ @command_manager.command_names.each do |cmd_name|
+ command = @command_manager[cmd_name]
+
+ next if command&.deprecated?
+
+ summary =
+ if command
+ command.summary
+ else
+ "[No command found for #{cmd_name}]"
+ end
+
+ summary = wrap(summary, summary_width).split "\n"
+ out << format(format, cmd_name, summary.shift)
+ until summary.empty? do
+ out << "#{wrap_indent}#{summary.shift}"
+ end
+ end
+
+ out << nil
+ out << "For help on a particular command, use 'gem help COMMAND'."
+ out << nil
+ out << "Commands may be abbreviated, so long as they are unambiguous."
+ out << "e.g. 'gem i rake' is short for 'gem install rake'."
+
+ say out.join("\n")
+ end
+
+ def show_command_help(command_name) # :nodoc:
+ command_name = command_name.downcase
+
+ possibilities = @command_manager.find_command_possibilities command_name
+
+ if possibilities.size == 1
+ command = @command_manager[possibilities.first]
+ command.invoke("--help")
+ elsif possibilities.size > 1
+ alert_warning "Ambiguous command #{command_name} (#{possibilities.join(", ")})"
+ else
+ alert_warning "Unknown command #{command_name}. Try: gem help commands"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/info_command.rb b/lib/rubygems/commands/info_command.rb
new file mode 100644
index 0000000000..f65c639662
--- /dev/null
+++ b/lib/rubygems/commands/info_command.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::InfoCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "info", "Show information for the given gem",
+ name: //, domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+
+ remove_option("-d")
+
+ defaults[:details] = true
+ defaults[:exact] = true
+ end
+
+ def description # :nodoc:
+ "Info prints information about the gem such as name,"\
+ " description, website, license and installed paths"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME"
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of the gem to print information about"
+ end
+
+ def defaults_str
+ "--local"
+ end
+end
diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb
new file mode 100644
index 0000000000..6d3beec0b4
--- /dev/null
+++ b/lib/rubygems/commands/install_command.rb
@@ -0,0 +1,268 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../install_update_options"
+require_relative "../dependency_installer"
+require_relative "../local_remote_options"
+require_relative "../validator"
+require_relative "../version_option"
+require_relative "../update_suggestion"
+
+##
+# Gem installer command line tool
+#
+# See `gem help install`
+
+class Gem::Commands::InstallCommand < Gem::Command
+ attr_reader :installed_specs # :nodoc:
+
+ include Gem::VersionOption
+ include Gem::LocalRemoteOptions
+ include Gem::InstallUpdateOptions
+ include Gem::UpdateSuggestion
+
+ def initialize
+ defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
+ format_executable: false,
+ lock: true,
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ without_groups: [],
+ })
+
+ defaults.merge!(install_update_options)
+
+ super "install", "Install a gem into the local repository", defaults
+
+ add_install_update_options
+ add_local_remote_options
+ add_platform_option
+ add_version_option
+ add_prerelease_option "to be installed. (Only for listed gems)"
+
+ @installed_specs = []
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to install"
+ end
+
+ def defaults_str # :nodoc:
+ "--both --version '#{Gem::Requirement.default}' --no-force\n" \
+ "--install-dir #{Gem.dir} --lock\n" +
+ install_update_defaults_str
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The install command installs local or remote gem into a gem repository.
+
+For gems with executables ruby installs a wrapper file into the executable
+directory by default. This can be overridden with the --no-wrappers option.
+The wrapper allows you to choose among alternate gem versions using _version_.
+
+For example `rake _0.7.3_ --version` will run rake version 0.7.3 if a newer
+version is also installed.
+
+Gem Dependency Files
+====================
+
+RubyGems can install a consistent set of gems across multiple environments
+using `gem install -g` when a gem dependencies file (gem.deps.rb, Gemfile or
+Isolate) is present. If no explicit file is given RubyGems attempts to find
+one in the current directory.
+
+When the RUBYGEMS_GEMDEPS environment variable is set to a gem dependencies
+file the gems from that file will be activated at startup time. Set it to a
+specific filename or to "-" to have RubyGems automatically discover the gem
+dependencies file by walking up from the current directory.
+
+NOTE: Enabling automatic discovery on multiuser systems can lead to
+execution of arbitrary code when used from directories outside your control.
+
+Extension Install Failures
+==========================
+
+If an extension fails to compile during gem installation the gem
+specification is not written out, but the gem remains unpacked in the
+repository. You may need to specify the path to the library's headers and
+libraries to continue. You can do this by adding a -- between RubyGems'
+options and the extension's build options:
+
+ $ gem install some_extension_gem
+ [build fails]
+ Gem files will remain installed in \\
+ /path/to/gems/some_extension_gem-1.0 for inspection.
+ Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
+ $ gem install some_extension_gem -- --with-extension-lib=/path/to/lib
+ [build succeeds]
+ $ gem list some_extension_gem
+
+ *** LOCAL GEMS ***
+
+ some_extension_gem (1.0)
+ $
+
+If you correct the compilation errors by editing the gem files you will need
+to write the specification by hand. For example:
+
+ $ gem install some_extension_gem
+ [build fails]
+ Gem files will remain installed in \\
+ /path/to/gems/some_extension_gem-1.0 for inspection.
+ Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
+ $ [cd /path/to/gems/some_extension_gem-1.0]
+ $ [edit files or what-have-you and run make]
+ $ gem spec ../../cache/some_extension_gem-1.0.gem --ruby > \\
+ ../../specifications/some_extension_gem-1.0.gemspec
+ $ gem list some_extension_gem
+
+ *** LOCAL GEMS ***
+
+ some_extension_gem (1.0)
+ $
+
+Command Alias
+==========================
+
+You can use `i` command instead of `install`.
+
+ $ gem i GEMNAME
+
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [options] GEMNAME [GEMNAME ...] -- --build-flags"
+ 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'`"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ if options.include? :gemdeps
+ install_from_gemdeps
+ return # not reached
+ end
+
+ @installed_specs = []
+
+ ENV.delete "GEM_PATH" if options[:install_dir].nil?
+
+ check_version
+
+ load_hooks
+
+ exit_code = install_gems
+
+ show_installed
+
+ say update_suggestion if eligible_for_update?
+
+ terminate_interaction exit_code
+ end
+
+ def install_from_gemdeps # :nodoc:
+ require_relative "../request_set"
+ rs = Gem::RequestSet.new
+
+ specs = rs.install_from_gemdeps options do |req, inst|
+ s = req.full_spec
+
+ if inst
+ say "Installing #{s.name} (#{s.version})"
+ else
+ say "Using #{s.name} (#{s.version})"
+ end
+ end
+
+ @installed_specs = specs
+
+ terminate_interaction
+ end
+
+ def install_gem(name, version) # :nodoc:
+ return if options[:conservative] &&
+ !Gem::Dependency.new(name, version).matching_specs.empty?
+
+ req = Gem::Requirement.create(version)
+
+ dinst = Gem::DependencyInstaller.new options
+
+ request_set = dinst.resolve_dependencies name, req
+
+ if options[:explain]
+ say "Gems to install:"
+
+ request_set.sorted_requests.each do |activation_request|
+ say " #{activation_request.full_name}"
+ end
+ else
+ @installed_specs.concat request_set.install options
+ end
+
+ show_install_errors dinst.errors
+ end
+
+ def install_gems # :nodoc:
+ exit_code = 0
+
+ get_all_gem_names_and_versions.each do |gem_name, gem_version|
+ gem_version ||= options[:version]
+ domain = options[:domain]
+ domain = :local unless options[:suggest_alternate]
+ suppress_suggestions = (domain == :local)
+
+ begin
+ install_gem gem_name, gem_version
+ rescue Gem::InstallError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ exit_code |= 1
+ 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,
+ "'#{gem_name}' (#{gem_version})"
+
+ exit_code |= 2
+ end
+ end
+
+ exit_code
+ end
+
+ ##
+ # Loads post-install hooks
+
+ def load_hooks # :nodoc:
+ require_relative "../install_message"
+ require_relative "../rdoc"
+ end
+
+ def show_install_errors(errors) # :nodoc:
+ return unless errors
+
+ errors.each do |x|
+ next unless Gem::SourceFetchProblem === x
+
+ require_relative "../uri"
+ msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}"
+
+ alert_warning msg
+ end
+ end
+
+ def show_installed # :nodoc:
+ return if @installed_specs.empty?
+
+ gems = @installed_specs.length == 1 ? "gem" : "gems"
+ say "#{@installed_specs.length} #{gems} installed"
+ end
+end
diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb
new file mode 100644
index 0000000000..fab4b73814
--- /dev/null
+++ b/lib/rubygems/commands/list_command.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+##
+# Searches for gems starting with the supplied argument.
+
+class Gem::Commands::ListCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "list", "Display local gems whose name matches REGEXP",
+ domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP regexp to look for in gem name"
+ end
+
+ def defaults_str # :nodoc:
+ "--local --no-details"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The list command is used to view the gems you have installed locally.
+
+The --details option displays additional details including the summary, the
+homepage, the author, the locations of different versions of the gem.
+
+To search for remote gems use the search command.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [REGEXP ...]"
+ end
+end
diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb
new file mode 100644
index 0000000000..f7fd5ada16
--- /dev/null
+++ b/lib/rubygems/commands/lock_command.rb
@@ -0,0 +1,109 @@
+# 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
+
+ add_option "-s", "--[no-]strict",
+ "fail if unable to satisfy a dependency" do |strict, options|
+ options[:strict] = strict
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to lock\nVERSION version of gem to lock"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-strict"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The lock command will generate a list of +gem+ statements that will lock down
+the versions for the gem given in the command line. It will specify exact
+versions in the requirements list to ensure that the gems loaded will always
+be consistent. A full recursive search of all effected gems will be
+generated.
+
+Example:
+
+ gem lock rails-1.0.0 > lockdown.rb
+
+will produce in lockdown.rb:
+
+ require "rubygems"
+ gem 'rails', '= 1.0.0'
+ gem 'rake', '= 0.7.0.1'
+ gem 'activesupport', '= 1.2.5'
+ gem 'activerecord', '= 1.13.2'
+ gem 'actionpack', '= 1.11.2'
+ gem 'actionmailer', '= 1.1.5'
+ gem 'actionwebservice', '= 1.0.0'
+
+Just load lockdown.rb from your application to ensure that the current
+versions are loaded. Make sure that lockdown.rb is loaded *before* any
+other require statements.
+
+Notice that rails 1.0.0 only requires that rake 0.6.2 or better be used.
+Rake-0.7.0.1 is the most recent version installed that satisfies that, so we
+lock it down to the exact version.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME-VERSION [GEMNAME-VERSION ...]"
+ end
+
+ def complain(message)
+ if options[:strict]
+ raise Gem::Exception, message
+ else
+ say "# #{message}"
+ end
+ end
+
+ def execute
+ say "require 'rubygems'"
+
+ locked = {}
+
+ pending = options[:args]
+
+ until pending.empty? do
+ full_name = pending.shift
+
+ spec = Gem::Specification.load spec_path(full_name)
+
+ if spec.nil?
+ complain "Could not find gem #{full_name}, try using the full name"
+ next
+ end
+
+ say "gem '#{spec.name}', '= #{spec.version}'" unless locked[spec.name]
+ locked[spec.name] = true
+
+ spec.runtime_dependencies.each do |dep|
+ next if locked[dep.name]
+ candidates = dep.matching_specs
+
+ if candidates.empty?
+ complain "Unable to satisfy '#{dep}' from currently installed gems"
+ else
+ pending << candidates.last.full_name
+ end
+ end
+ end
+ end
+
+ def spec_path(gem_full_name)
+ gemspecs = Gem.path.map do |path|
+ File.join path, "specifications", "#{gem_full_name}.gemspec"
+ end
+
+ gemspecs.find {|path| File.exist? path }
+ end
+end
diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb
new file mode 100644
index 0000000000..b91a8db12d
--- /dev/null
+++ b/lib/rubygems/commands/mirror_command.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::MirrorCommand
+ class Gem::Commands::MirrorCommand < Gem::Command
+ def initialize
+ super("mirror", "Mirror all gem files (requires rubygems-mirror)")
+ begin
+ Gem::Specification.find_by_name("rubygems-mirror").activate
+ rescue Gem::LoadError
+ # no-op
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The mirror command has been moved to the rubygems-mirror gem.
+ EOF
+ end
+
+ def execute
+ alert_error "Install the rubygems-mirror gem for the mirror command"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb
new file mode 100644
index 0000000000..0fe90dc8b8
--- /dev/null
+++ b/lib/rubygems/commands/open_command.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+
+class Gem::Commands::OpenCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "open", "Open gem sources in editor"
+
+ add_option("-e", "--editor COMMAND", String,
+ "Prepends COMMAND to gem path. Could be used to specify editor.") do |command, options|
+ options[:editor] = command || get_env_editor
+ end
+ add_option("-v", "--version VERSION", String,
+ "Opens specific gem version") do |version|
+ options[:version] = version
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to open in editor"
+ end
+
+ def defaults_str # :nodoc:
+ "-e #{get_env_editor}"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+ The open command opens gem in editor and changes current path
+ to gem's source directory.
+ Editor command can be specified with -e option, otherwise rubygems
+ will look for editor in $EDITOR, $VISUAL and $GEM_EDITOR variables.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [-e COMMAND] GEMNAME"
+ end
+
+ def get_env_editor
+ ENV["GEM_EDITOR"] ||
+ ENV["VISUAL"] ||
+ ENV["EDITOR"] ||
+ "vi"
+ end
+
+ def execute
+ @version = options[:version] || Gem::Requirement.default
+ @editor = options[:editor] || get_env_editor
+
+ found = open_gem(get_one_gem_name)
+
+ terminate_interaction 1 unless found
+ end
+
+ def open_gem(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ if spec.default_gem?
+ say "'#{name}' is a default gem and can't be opened."
+ return false
+ end
+
+ open_editor(spec.full_gem_path)
+ end
+
+ def open_editor(path)
+ system(*@editor.split(/\s+/) + [path], { chdir: path })
+ end
+
+ def spec_for(name)
+ spec = Gem::Specification.find_all_by_name(name, @version).first
+
+ return spec if spec
+
+ say "Unable to find gem '#{name}'"
+ end
+end
diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb
new file mode 100644
index 0000000000..08a9221a26
--- /dev/null
+++ b/lib/rubygems/commands/outdated_command.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+
+class Gem::Commands::OutdatedCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ super "outdated", "Display all gems that need updates"
+
+ add_local_remote_options
+ add_platform_option
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The outdated command lists gems you may wish to upgrade to a newer version.
+
+You can check for dependency mismatches using the dependency command and
+update the gems with the update or install commands.
+ EOF
+ end
+
+ def execute
+ Gem::Specification.outdated_and_latest_version.each do |spec, remote_version|
+ say "#{spec.name} (#{spec.version} < #{remote_version})"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb
new file mode 100644
index 0000000000..675e866734
--- /dev/null
+++ b/lib/rubygems/commands/owner_command.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../gemcutter_utilities"
+require_relative "../text"
+
+class Gem::Commands::OwnerCommand < Gem::Command
+ include Gem::Text
+ include Gem::LocalRemoteOptions
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The owner command lets you add and remove owners of a gem on a push
+server (the default is https://rubygems.org). Multiple owners can be
+added or removed at the same time, if the flag is given multiple times.
+
+The supported user identifiers are dependent on the push server.
+For rubygems.org, both e-mail and handle are supported, even though the
+user identifier field is called "email".
+
+The owner of a gem has the permission to push new versions, yank existing
+versions or edit the HTML page of the gem. Be careful of who you give push
+permission to.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM gem to manage owners for"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM"
+ end
+
+ def initialize
+ super "owner", "Manage gem owners of a gem on the push server"
+ add_proxy_option
+ add_key_option
+ add_otp_option
+ defaults.merge! add: [], remove: []
+
+ add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options|
+ options[:add] << value
+ end
+
+ add_option "-r", "--remove OLD_OWNER", "Remove an owner by user identifier" do |value, options|
+ options[:remove] << value
+ end
+
+ add_option "-h", "--host HOST",
+ "Use another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)" do |value, options|
+ options[:host] = value
+ end
+ end
+
+ def execute
+ @host = options[:host]
+
+ sign_in(scope: get_owner_scope)
+ name = get_one_gem_name
+
+ add_owners name, options[:add]
+ remove_owners name, options[:remove]
+ show_owners name
+ end
+
+ def show_owners(name)
+ Gem.load_yaml
+
+ response = rubygems_api_request :get, "api/v1/gems/#{name}/owners.yaml" do |request|
+ request.add_field "Authorization", api_key
+ end
+
+ with_response response do |resp|
+ owners = Gem::SafeYAML.safe_load clean_text(resp.body)
+
+ say "Owners for gem: #{name}"
+ owners.each do |owner|
+ identifier = owner["email"] || owner["handle"] || owner["id"]
+ say "- #{identifier} (#{owner["role"]})"
+ end
+ end
+ end
+
+ def add_owners(name, owners)
+ manage_owners :post, name, owners
+ end
+
+ def remove_owners(name, owners)
+ manage_owners :delete, name, owners
+ end
+
+ def manage_owners(method, name, owners)
+ owners.each do |owner|
+ response = send_owner_request(method, name, owner)
+ action = method == :delete ? "Removing" : "Adding"
+
+ with_response response, "#{action} #{owner}"
+ rescue Gem::WebauthnVerificationError => e
+ raise e
+ rescue StandardError
+ # ignore early exits to allow for completing the iteration of all owners
+ end
+ end
+
+ private
+
+ def send_owner_request(method, name, owner)
+ rubygems_api_request method, "api/v1/gems/#{name}/owners", scope: get_owner_scope(method: method) do |request|
+ request.set_form_data "email" => owner
+ request.add_field "Authorization", api_key
+ end
+ end
+
+ def get_owner_scope(method: nil)
+ if method == :post || options.any? && options[:add].any?
+ :add_owner
+ elsif method == :delete || options.any? && options[:remove].any?
+ :remove_owner
+ end
+ end
+end
diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb
new file mode 100644
index 0000000000..10978c2af7
--- /dev/null
+++ b/lib/rubygems/commands/pristine_command.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../package"
+require_relative "../installer"
+require_relative "../version_option"
+
+class Gem::Commands::PristineCommand < Gem::Command
+ include Gem::VersionOption
+
+ 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
+
+ add_option("--all",
+ "Restore all installed gems to pristine",
+ "condition") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("--skip=gem_name",
+ "used on --all, skip if name == gem_name") do |value, options|
+ options[:skip] ||= []
+ options[:skip] << value
+ end
+
+ add_option("--[no-]extensions",
+ "Restore gems with extensions",
+ "in addition to regular gems") do |value, options|
+ options[:extensions_set] = true
+ options[:extensions] = value
+ end
+
+ add_option("--only-missing-extensions",
+ "Only restore gems with missing extensions") do |value, options|
+ options[:only_missing_extensions] = value
+ end
+
+ add_option("--only-executables",
+ "Only restore executables") do |value, options|
+ options[:only_executables] = value
+ end
+
+ add_option("--only-plugins",
+ "Only restore plugins") do |value, options|
+ options[:only_plugins] = value
+ end
+
+ add_option("-E", "--[no-]env-shebang",
+ "Rewrite executables with a shebang",
+ "of /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ add_option("-i", "--install-dir DIR",
+ "Gem repository to get gems restored") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
+
+ add_option("-n", "--bindir DIR",
+ "Directory where executables are",
+ "located") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
+
+ add_version_option("restore to", "pristine condition")
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME gem to restore to pristine condition (unless --all)"
+ end
+
+ def defaults_str # :nodoc:
+ "--extensions"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The pristine command compares an installed gem with the contents of its
+cached .gem file and restores any files that don't match the cached .gem's
+copy.
+
+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
+with an extension.
+
+If --extensions is given (but not --all or gem names) only gems with
+extensions will be restored.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [GEMNAME ...]"
+ 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]
+ specification_record.map
+
+ # `--extensions` must be explicitly given to pristine only gems
+ # with extensions.
+ elsif options[:extensions_set] &&
+ options[:extensions] && options[:args].empty?
+ specification_record.select do |spec|
+ spec.extensions && !spec.extensions.empty?
+ end
+ elsif options[:only_missing_extensions]
+ specification_record.select(&:missing_extensions?)
+ else
+ 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| 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.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
+ if options[:skip].include? spec.name
+ say "Skipped #{spec.full_name}, it was given through options"
+ next
+ end
+ end
+
+ 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) || only_executables || only_plugins
+ require_relative "../remote_fetcher"
+
+ 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
+
+ if found.empty?
+ say "Skipped #{spec.full_name}, it was not found from cache and remote sources"
+ next
+ end
+
+ spec_candidate, source = found.first
+ Gem::RemoteFetcher.fetcher.download spec_candidate, source.uri.to_s, spec.base_dir
+ end
+
+ env_shebang =
+ if options.include? :env_shebang
+ options[:env_shebang]
+ else
+ install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS["install"]
+ install_defaults.to_s["--env-shebang"]
+ end
+
+ bin_dir = options[:bin_dir] if options[:bin_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,
+ }
+
+ if only_executables
+ installer = Gem::Installer.for_spec(spec, installer_options)
+ installer.generate_bin
+ elsif only_plugins
+ installer = Gem::Installer.for_spec(spec, installer_options)
+ installer.generate_plugins
+ else
+ installer = Gem::Installer.at(gem, installer_options)
+ installer.install
+ end
+
+ 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
new file mode 100644
index 0000000000..02931b3025
--- /dev/null
+++ b/lib/rubygems/commands/push_command.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../gemcutter_utilities"
+require_relative "../package"
+
+class Gem::Commands::PushCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The push command uploads a gem to the push server (the default is
+https://rubygems.org) and adds it to the index.
+
+The gem can be removed from the index and deleted from the server using the yank
+command. For further discussion see the help for the yank command.
+
+The push command will use ~/.gem/credentials to authenticate to a server, but you can use the RubyGems environment variable GEM_HOST_API_KEY to set the api key to authenticate.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM built gem to push up"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM"
+ end
+
+ def initialize
+ super "push", "Push a gem up to the gem server", host: host, attestations: []
+
+ @user_defined_host = false
+
+ add_proxy_option
+ add_key_option
+ add_otp_option
+
+ add_option("--host HOST",
+ "Push to another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
+ options[:host] = value
+ @user_defined_host = true
+ end
+
+ add_option("--attestation FILE",
+ "Push with sigstore attestations") do |value, options|
+ options[:attestations] << value
+ end
+
+ @host = nil
+ end
+
+ def execute
+ gem_name = get_one_gem_name
+ default_gem_server, push_host = get_hosts_for(gem_name)
+
+ @host = if @user_defined_host
+ options[:host]
+ elsif default_gem_server
+ default_gem_server
+ elsif push_host
+ push_host
+ else
+ options[:host]
+ end
+
+ sign_in @host, scope: get_push_scope
+
+ send_gem(gem_name)
+ end
+
+ def send_gem(name)
+ args = [:post, "api/v1/gems"]
+
+ _, push_host = get_hosts_for(name)
+
+ @host ||= push_host
+
+ # Always include @host, even if it's nil
+ args += [@host, push_host]
+
+ say "Pushing gem to #{@host || Gem.host}..."
+
+ response = send_push_request(name, args)
+
+ with_response response
+ end
+
+ private
+
+ def send_push_request(name, args)
+ # 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 "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)
+ gem_metadata = Gem::Package.new(name).spec.metadata
+
+ [
+ gem_metadata["default_gem_server"],
+ gem_metadata["allowed_push_host"],
+ ]
+ end
+
+ 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/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb
new file mode 100644
index 0000000000..62c4bf8ce9
--- /dev/null
+++ b/lib/rubygems/commands/rdoc_command.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../rdoc"
+require "fileutils"
+
+class Gem::Commands::RdocCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "rdoc", "Generates RDoc for pre-installed gems",
+ version: Gem::Requirement.default,
+ include_rdoc: false, include_ri: true, overwrite: false
+
+ add_option("--all",
+ "Generate RDoc/RI documentation for all",
+ "installed gems") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("--[no-]rdoc",
+ "Generate RDoc HTML") do |value, options|
+ options[:include_rdoc] = value
+ end
+
+ add_option("--[no-]ri",
+ "Generate RI data") do |value, options|
+ options[:include_ri] = value
+ end
+
+ add_option("--[no-]overwrite",
+ "Overwrite installed documents") do |value, options|
+ options[:overwrite] = value
+ end
+
+ add_version_option
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME gem to generate documentation for (unless --all)"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}' --ri --no-overwrite"
+ end
+
+ def description # :nodoc:
+ <<-DESC
+The rdoc command builds documentation for installed gems. By default
+only documentation is built using rdoc, but additional types of
+documentation may be built through rubygems plugins and the
+Gem.post_installs hook.
+
+Use --overwrite to force rebuilding of documentation.
+ DESC
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [args]"
+ end
+
+ def execute
+ specs = if options[:all]
+ Gem::Specification.to_a
+ else
+ get_all_gem_names.flat_map do |name|
+ Gem::Specification.find_by_name name, options[:version]
+ end.uniq
+ end
+
+ if specs.empty?
+ alert_error "No matching gems found"
+ terminate_interaction 1
+ end
+
+ specs.each do |spec|
+ doc = Gem::RDoc.new spec, options[:include_rdoc], options[:include_ri]
+
+ doc.force = options[:overwrite]
+
+ if options[:overwrite]
+ FileUtils.rm_rf File.join(spec.doc_dir, "ri")
+ FileUtils.rm_rf File.join(spec.doc_dir, "rdoc")
+ 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
new file mode 100644
index 0000000000..50e161ac9b
--- /dev/null
+++ b/lib/rubygems/commands/search_command.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::SearchCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "search", "Display remote gems whose name matches REGEXP",
+ domain: :remote, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP regexp to search for in gem name"
+ end
+
+ def defaults_str # :nodoc:
+ "--remote --no-details"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The search command displays remote gems whose name matches the given
+regexp.
+
+The --details option displays additional details from the gem but will
+take a little longer to complete as it must download the information
+individually from the index.
+
+To list local gems use the list command.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [REGEXP]"
+ end
+end
diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb
new file mode 100644
index 0000000000..f1dde4aa02
--- /dev/null
+++ b/lib/rubygems/commands/server_command.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::ServerCommand
+ class Gem::Commands::ServerCommand < Gem::Command
+ def initialize
+ super("server", "Starts up a web server that hosts the RDoc (requires rubygems-server)")
+ begin
+ Gem::Specification.find_by_name("rubygems-server").activate
+ rescue Gem::LoadError
+ # no-op
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The server command has been moved to the rubygems-server gem.
+ EOF
+ end
+
+ def execute
+ alert_error "Install the rubygems-server gem for the server command"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb
new file mode 100644
index 0000000000..175599967c
--- /dev/null
+++ b/lib/rubygems/commands/setup_command.rb
@@ -0,0 +1,667 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+##
+# Installs RubyGems itself. This command is ordinarily only available from a
+# RubyGems checkout or tarball.
+
+class Gem::Commands::SetupCommand < Gem::Command
+ 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
+
+ add_option "--previous-version=VERSION",
+ "Previous version of RubyGems",
+ "Used for changelog processing" do |version, options|
+ options[:previous_version] = version
+ end
+
+ add_option "--prefix=PREFIX",
+ "Prefix path for installing RubyGems",
+ "Will not affect gem repository location" do |prefix, options|
+ options[:prefix] = File.expand_path prefix
+ end
+
+ add_option "--destdir=DESTDIR",
+ "Root directory to install RubyGems into",
+ "Mainly used for packaging RubyGems" do |destdir, options|
+ options[:destdir] = File.expand_path destdir
+ end
+
+ add_option "--[no-]vendor",
+ "Install into vendorlibdir not sitelibdir" do |vendor, options|
+ options[:site_or_vendor] = vendor ? "vendorlibdir" : "sitelibdir"
+ end
+
+ add_option "--[no-]format-executable",
+ "Makes `gem` match ruby",
+ "If Ruby is ruby18, gem will be gem18" do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option "--[no-]document [TYPES]", Array,
+ "Generate documentation for RubyGems",
+ "List the documentation types you wish to",
+ "generate. For example: rdoc,ri" do |value, options|
+ options[:document] = case value
+ when nil then %w[rdoc ri]
+ when false then []
+ else value
+ end
+ end
+
+ add_option "--[no-]rdoc",
+ "Generate RDoc documentation for RubyGems" do |value, options|
+ if value
+ options[:document] << "rdoc"
+ else
+ options[:document].delete "rdoc"
+ end
+
+ options[:document].uniq!
+ end
+
+ add_option "--[no-]ri",
+ "Generate RI documentation for RubyGems" do |value, options|
+ if value
+ options[:document] << "ri"
+ else
+ options[:document].delete "ri"
+ end
+
+ options[:document].uniq!
+ end
+
+ add_option "--[no-]regenerate-binstubs",
+ "Regenerate gem binstubs" do |value, options|
+ options[:regenerate_binstubs] = value
+ end
+
+ add_option "--[no-]regenerate-plugins",
+ "Regenerate gem plugins" do |value, options|
+ options[:regenerate_plugins] = value
+ end
+
+ add_option "-f", "--[no-]force",
+ "Forcefully overwrite binstubs" do |value, options|
+ options[:force] = value
+ end
+
+ add_option("-E", "--[no-]env-shebang",
+ "Rewrite executables with a shebang",
+ "of /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ @verbose = nil
+ end
+
+ def defaults_str # :nodoc:
+ "--format-executable --document ri --regenerate-binstubs"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+Installs RubyGems itself.
+
+RubyGems installs RDoc for itself in GEM_HOME. By default this is:
+ #{Gem.dir}
+
+If you prefer a different directory, set the GEM_HOME environment variable.
+
+RubyGems will install the gem command with a name matching ruby's
+prefix and suffix. If ruby was installed as `ruby18`, gem will be
+installed as `gem18`.
+
+By default, this RubyGems will install gem as:
+ #{Gem.default_exec_format % "gem"}
+ EOF
+ end
+
+ module MakeDirs
+ def mkdir_p(path, **opts)
+ super
+ (@mkdirs ||= []) << path
+ end
+ end
+
+ def execute
+ @verbose = Gem.configuration.really_verbose
+
+ require "fileutils"
+ if Gem.configuration.really_verbose
+ extend FileUtils::Verbose
+ else
+ extend FileUtils
+ end
+ extend MakeDirs
+
+ lib_dir, bin_dir = make_destination_dirs
+ man_dir = generate_default_man_dir
+
+ install_lib lib_dir
+
+ install_executables bin_dir
+
+ remove_old_bin_files bin_dir
+
+ remove_old_lib_files lib_dir
+
+ # Can be removed one we drop support for bundler 2.2.3 (the last version installing man files to man_dir)
+ remove_old_man_files man_dir if man_dir && File.exist?(man_dir)
+
+ install_default_bundler_gem bin_dir
+
+ if mode = options[:dir_mode]
+ @mkdirs.uniq!
+ File.chmod(mode, @mkdirs)
+ end
+
+ say "RubyGems #{Gem::VERSION} installed"
+
+ regenerate_binstubs(bin_dir) if options[:regenerate_binstubs]
+ regenerate_plugins(bin_dir) if options[:regenerate_plugins]
+
+ uninstall_old_gemcutter
+
+ documentation_success = install_rdoc
+
+ say
+ if @verbose
+ say "-" * 78
+ say
+ end
+
+ if options[:previous_version].empty?
+ options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, "0")
+ end
+
+ options[:previous_version] = Gem::Version.new(options[:previous_version])
+
+ show_release_notes
+
+ say
+ say "-" * 78
+ say
+
+ say "RubyGems installed the following executables:"
+ say bin_file_names.map {|name| "\t#{name}\n" }
+ say
+
+ unless bin_file_names.grep(/#{File::SEPARATOR}gem$/)
+ say "If `gem` was installed by a previous RubyGems installation, you may need"
+ say "to remove it by hand."
+ say
+ end
+
+ if documentation_success
+ if options[:document].include? "rdoc"
+ say "Rdoc documentation was installed. You may now invoke:"
+ say " gem server"
+ say "and then peruse beautifully formatted documentation for your gems"
+ say "with your web browser."
+ say "If you do not wish to install this documentation in the future, use the"
+ say "--no-document flag, or set it as the default in your ~/.gemrc file. See"
+ say "'gem help env' for details."
+ say
+ end
+
+ if options[:document].include? "ri"
+ say "Ruby Interactive (ri) documentation was installed. ri is kind of like man "
+ say "pages for Ruby libraries. You may access it like this:"
+ say " ri Classname"
+ say " ri Classname.class_method"
+ say " ri Classname#instance_method"
+ say "If you do not wish to install this documentation in the future, use the"
+ say "--no-document flag, or set it as the default in your ~/.gemrc file. See"
+ say "'gem help env' for details."
+ say
+ end
+ end
+ end
+
+ def install_executables(bin_dir)
+ prog_mode = options[:prog_mode] || 0o755
+
+ executables = { "gem" => "exe" }
+ executables.each do |tool, path|
+ say "Installing #{tool} executable" if @verbose
+
+ Dir.chdir path do
+ bin_file = "gem"
+
+ require "tmpdir"
+
+ dest_file = target_bin_path(bin_dir, bin_file)
+ bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}"
+
+ begin
+ bin = File.readlines bin_file
+ bin[0] = shebang
+
+ File.open bin_tmp_file, "w" do |fp|
+ fp.puts bin.join
+ end
+
+ install bin_tmp_file, dest_file, mode: prog_mode
+ bin_file_names << dest_file
+ ensure
+ rm bin_tmp_file
+ end
+
+ next unless Gem.win_platform?
+
+ begin
+ bin_cmd_file = File.join Dir.tmpdir, "#{bin_file}.bat"
+
+ File.open bin_cmd_file, "w" do |file|
+ file.puts <<-TEXT
+ @ECHO OFF
+ @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
+ TEXT
+ end
+
+ install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode
+ ensure
+ rm bin_cmd_file
+ end
+ end
+ end
+ end
+
+ def shebang
+ if options[:env_shebang]
+ ruby_name = RbConfig::CONFIG["ruby_install_name"]
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ "#!#{@env_path} #{ruby_name}\n"
+ else
+ "#!#{Gem.ruby}\n"
+ end
+ end
+
+ def install_lib(lib_dir)
+ libs = { "RubyGems" => "lib" }
+ libs["Bundler"] = "bundler/lib"
+ libs.each do |tool, path|
+ say "Installing #{tool}" if @verbose
+
+ lib_files = files_in path
+
+ Dir.chdir path do
+ install_file_list(lib_files, lib_dir)
+ end
+ end
+ end
+
+ def install_rdoc
+ gem_doc_dir = File.join Gem.dir, "doc"
+ rubygems_name = "rubygems-#{Gem::VERSION}"
+ rubygems_doc_dir = File.join gem_doc_dir, rubygems_name
+
+ begin
+ Gem.ensure_gem_subdirectories Gem.dir
+ rescue SystemCallError
+ # ignore
+ end
+
+ if File.writable?(gem_doc_dir) &&
+ (!File.exist?(rubygems_doc_dir) ||
+ File.writable?(rubygems_doc_dir))
+ say "Removing old RubyGems RDoc and ri" if @verbose
+ Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir|
+ rm_rf dir
+ end
+
+ 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__
+ end
+
+ generate_ri = options[:document].include? "ri"
+ generate_rdoc = options[:document].include? "rdoc"
+
+ rdoc = Gem::RDoc.new fake_spec, generate_rdoc, generate_ri
+ rdoc.generate
+
+ return true
+ elsif @verbose
+ say "Skipping RDoc generation, #{gem_doc_dir} not writable"
+ say "Set the GEM_HOME environment variable if you want RDoc generated"
+ end
+
+ 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: 0o755
+ target_specs_dir
+ end
+
+ 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", gemspec_path)
+ if File.file? normal_gemspec
+ File.delete normal_gemspec
+ end
+
+ # 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) == full_name }.
+ each {|default_gem| rm_r File.join(bundler_spec.gems_dir, default_gem) }
+ end
+
+ require_relative "../installer"
+
+ Dir.chdir("bundler") do
+ built_gem = Gem::Package.build(new_bundler_spec)
+ begin
+ installer = Gem::Installer.at(
+ built_gem,
+ env_shebang: options[:env_shebang],
+ format_executable: options[:format_executable],
+ force: options[:force],
+ bin_dir: bin_dir,
+ install_dir: default_dir,
+ wrappers: true
+ )
+ # 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
+
+ new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) }
+
+ say "Bundler #{new_bundler_spec.version} installed"
+ end
+
+ def make_destination_dirs
+ lib_dir, bin_dir = Gem.default_rubygems_dirs
+
+ unless lib_dir
+ lib_dir, bin_dir = generate_default_dirs
+ end
+
+ mkdir_p lib_dir, mode: 0o755
+ mkdir_p bin_dir, mode: 0o755
+
+ [lib_dir, bin_dir]
+ end
+
+ def generate_default_man_dir
+ prefix = options[:prefix]
+
+ if prefix.empty?
+ man_dir = RbConfig::CONFIG["mandir"]
+ return unless man_dir
+ else
+ man_dir = File.join prefix, "man"
+ end
+
+ prepend_destdir_if_present(man_dir)
+ end
+
+ def generate_default_dirs
+ prefix = options[:prefix]
+ site_or_vendor = options[:site_or_vendor]
+
+ if prefix.empty?
+ lib_dir = RbConfig::CONFIG[site_or_vendor]
+ bin_dir = RbConfig::CONFIG["bindir"]
+ else
+ lib_dir = File.join prefix, "lib"
+ bin_dir = File.join prefix, "bin"
+ end
+
+ [prepend_destdir_if_present(lib_dir), prepend_destdir_if_present(bin_dir)]
+ end
+
+ def files_in(dir)
+ Dir.chdir dir do
+ Dir.glob(File.join("**", "*"), File::FNM_DOTMATCH).
+ select {|f| !File.directory?(f) }
+ end
+ end
+
+ def remove_old_bin_files(bin_dir)
+ old_bin_files = {
+ "gem_mirror" => "gem mirror",
+ "gem_server" => "gem server",
+ "gemlock" => "gem lock",
+ "gemri" => "ri",
+ "gemwhich" => "gem which",
+ "index_gem_repository.rb" => "gem generate_index",
+ }
+
+ old_bin_files.each do |old_bin_file, new_name|
+ old_bin_path = File.join bin_dir, old_bin_file
+ next unless File.exist? old_bin_path
+
+ deprecation_message = "`#{old_bin_file}` has been deprecated. Use `#{new_name}` instead."
+
+ File.open old_bin_path, "w" do |fp|
+ fp.write <<-EOF
+#!#{Gem.ruby}
+
+abort "#{deprecation_message}"
+ EOF
+ end
+
+ next unless Gem.win_platform?
+
+ File.open "#{old_bin_path}.bat", "w" do |fp|
+ fp.puts %(@ECHO.#{deprecation_message})
+ end
+ end
+ end
+
+ def remove_old_lib_files(lib_dir)
+ lib_dirs = { File.join(lib_dir, "rubygems") => "lib/rubygems" }
+ lib_dirs[File.join(lib_dir, "bundler")] = "bundler/lib/bundler"
+ lib_dirs.each do |old_lib_dir, new_lib_dir|
+ lib_files = files_in(new_lib_dir)
+
+ old_lib_files = files_in(old_lib_dir)
+
+ to_remove = old_lib_files - lib_files
+
+ gauntlet_rubygems = File.join(lib_dir, "gauntlet_rubygems.rb")
+ to_remove << gauntlet_rubygems if File.exist? gauntlet_rubygems
+
+ to_remove.delete_if do |file|
+ file.start_with? "defaults"
+ end
+
+ remove_file_list(to_remove, old_lib_dir)
+ end
+ end
+
+ def remove_old_man_files(old_man_dir)
+ old_man1_dir = "#{old_man_dir}/man1"
+
+ if File.exist?(old_man1_dir)
+ man1_to_remove = Dir.chdir(old_man1_dir) { Dir["bundle*.1{,.txt,.ronn}"] }
+
+ remove_file_list(man1_to_remove, old_man1_dir)
+ end
+
+ old_man5_dir = "#{old_man_dir}/man5"
+
+ if File.exist?(old_man5_dir)
+ man5_to_remove = Dir.chdir(old_man5_dir) { Dir["gemfile.5{,.txt,.ronn}"] }
+
+ remove_file_list(man5_to_remove, old_man5_dir)
+ end
+ end
+
+ def show_release_notes
+ release_notes = File.join Dir.pwd, "CHANGELOG.md"
+
+ release_notes =
+ if File.exist? release_notes
+ history = File.read release_notes
+
+ history.force_encoding Encoding::UTF_8
+
+ text = history.split(HISTORY_HEADER)
+ text.shift # correct an off-by-one generated by split
+ version_lines = history.scan(HISTORY_HEADER)
+ versions = history.scan(VERSION_MATCHER).flatten.map do |x|
+ Gem::Version.new(x)
+ end
+
+ history_string = ""
+
+ until versions.length == 0 ||
+ versions.shift <= options[:previous_version] do
+ history_string += version_lines.shift + text.shift
+ end
+
+ history_string
+ else
+ "Oh-no! Unable to find release notes!"
+ end
+
+ say release_notes
+ end
+
+ def uninstall_old_gemcutter
+ require_relative "../uninstaller"
+
+ ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true,
+ version: "< 0.4")
+ ui.uninstall
+ rescue Gem::InstallError
+ end
+
+ def regenerate_binstubs(bindir)
+ require_relative "pristine_command"
+ say "Regenerating binstubs"
+
+ args = %w[--all --only-executables --silent]
+ args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
+ if options[:env_shebang]
+ args << "--env-shebang"
+ end
+
+ command = Gem::Commands::PristineCommand.new
+ command.invoke(*args)
+ end
+
+ def regenerate_plugins(bindir)
+ require_relative "pristine_command"
+ say "Regenerating plugins"
+
+ args = %w[--all --only-plugins --silent]
+ args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
+ command = Gem::Commands::PristineCommand.new
+ command.invoke(*args)
+ end
+
+ private
+
+ def default_dir
+ prefix = options[:prefix]
+
+ if prefix.empty?
+ dir = Gem.default_dir
+ else
+ dir = prefix
+ end
+
+ prepend_destdir_if_present(dir)
+ end
+
+ def prepend_destdir_if_present(path)
+ destdir = options[:destdir]
+ return path if destdir.empty?
+
+ File.join(options[:destdir], path.gsub(/^[a-zA-Z]:/, ""))
+ end
+
+ def install_file_list(files, dest_dir)
+ files.each do |file|
+ install_file file, dest_dir
+ end
+ end
+
+ def install_file(file, dest_dir)
+ dest_file = File.join dest_dir, file
+ dest_dir = File.dirname dest_file
+ unless File.directory? dest_dir
+ mkdir_p dest_dir, mode: 0o755
+ end
+
+ install file, dest_file, mode: options[:data_mode] || 0o644
+ end
+
+ def remove_file_list(files, dir)
+ Dir.chdir dir do
+ files.each do |file|
+ FileUtils.rm_f file
+
+ warn "unable to remove old file #{file} please remove it by hand" if
+ File.exist? file
+ end
+ end
+ end
+
+ def target_bin_path(bin_dir, bin_file)
+ bin_file_formatted = if options[:format_executable]
+ Gem.default_exec_format % bin_file
+ else
+ bin_file
+ end
+ File.join bin_dir, bin_file_formatted
+ end
+
+ def bin_file_names
+ @bin_file_names ||= []
+ end
+end
diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb
new file mode 100644
index 0000000000..0f77908c5b
--- /dev/null
+++ b/lib/rubygems/commands/signin_command.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../gemcutter_utilities"
+
+class Gem::Commands::SigninCommand < Gem::Command
+ include Gem::GemcutterUtilities
+
+ def initialize
+ super "signin", "Sign in to any gemcutter-compatible host. "\
+ "It defaults to https://rubygems.org"
+
+ add_option("--host HOST", "Push to another gemcutter-compatible host") do |value, options|
+ options[:host] = value
+ end
+
+ add_otp_option
+ end
+
+ def description # :nodoc:
+ "The signin command executes host sign in for a push server (the default is"\
+ " https://rubygems.org). The host can be provided with the host flag or can"\
+ " be inferred from the provided gem. Host resolution matches the resolution"\
+ " strategy for the push command."
+ end
+
+ def usage # :nodoc:
+ program_name
+ end
+
+ def execute
+ sign_in options[:host]
+ end
+end
diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb
new file mode 100644
index 0000000000..bdd01e4393
--- /dev/null
+++ b/lib/rubygems/commands/signout_command.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::SignoutCommand < Gem::Command
+ def initialize
+ super "signout", "Sign out from all the current sessions."
+ end
+
+ def description # :nodoc:
+ "The `signout` command is used to sign out from all current sessions,"\
+ " allowing you to sign in using a different set of credentials."
+ end
+
+ def usage # :nodoc:
+ program_name
+ end
+
+ def execute
+ credentials_path = Gem.configuration.credentials_path
+
+ if !File.exist?(credentials_path)
+ alert_error "You are not currently signed in."
+ elsif !File.writable?(credentials_path)
+ alert_error "File '#{Gem.configuration.credentials_path}' is read-only."\
+ " Please make sure it is writable."
+ else
+ Gem.configuration.unset_api_key!
+ say "You have successfully signed out from all sessions."
+ end
+ end
+end
diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb
new file mode 100644
index 0000000000..b399af2bd3
--- /dev/null
+++ b/lib/rubygems/commands/sources_command.rb
@@ -0,0 +1,348 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../remote_fetcher"
+require_relative "../spec_fetcher"
+require_relative "../local_remote_options"
+
+class Gem::Commands::SourcesCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+
+ def initialize
+ require "fileutils"
+
+ super "sources",
+ "Manage the sources and cache file RubyGems uses to search for gems"
+
+ add_option "-a", "--add SOURCE_URI", "Add source" do |value, options|
+ 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
+
+ add_option "-r", "--remove SOURCE_URI", "Remove source" do |value, options|
+ options[:remove] = value
+ end
+
+ add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options|
+ options[:clear_all] = value
+ end
+
+ add_option "-u", "--update", "Update source cache" do |value, options|
+ options[:update] = value
+ end
+
+ add_option "-f", "--[no-]force", "Do not show any confirmation prompts and behave as if 'yes' was always answered" do |value, options|
+ options[:force] = value
+ end
+
+ add_proxy_option
+ end
+
+ def add_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
+
+ begin
+ if Gem.sources.include? source
+ say "source #{source_uri} already present in the cache"
+ else
+ source.load_specs :released
+ Gem.sources << source
+ Gem.configuration.write
+
+ 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 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
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
+ end
+ end
+
+ def check_typo_squatting(source)
+ if source.typo_squatting?("rubygems.org")
+ question = <<-QUESTION.chomp
+#{source.uri} is too similar to https://rubygems.org
+
+Do you want to add this source?
+ QUESTION
+
+ terminate_interaction 1 unless options[:force] || ask_yes_no(question)
+ 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 = Gem::URI source_uri
+
+ if uri.scheme && uri.scheme.casecmp("http").zero? &&
+ uri.host.casecmp("rubygems.org").zero?
+ question = <<-QUESTION.chomp
+https://rubygems.org is recommended for security over #{uri}
+
+Do you want to add this insecure source?
+ QUESTION
+
+ terminate_interaction 1 unless options[:force] || ask_yes_no(question)
+ end
+ end
+
+ def clear_all # :nodoc:
+ path = Gem.spec_cache_dir
+ FileUtils.rm_rf path
+
+ if File.exist? path
+ if File.writable? path
+ say "*** Unable to remove source cache ***"
+ else
+ say "*** Unable to remove source cache (write protected) ***"
+ end
+
+ terminate_interaction 1
+ else
+ say "*** Removed specs cache ***"
+ end
+ end
+
+ def defaults_str # :nodoc:
+ "--list"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+RubyGems fetches gems from the sources you have configured (stored in your
+~/.gemrc).
+
+The default source is https://rubygems.org, but you may have other sources
+configured. This guide will help you update your sources or configure
+yourself to use your own gem server.
+
+Without any arguments the sources lists your currently configured sources:
+
+ $ gem sources
+ *** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***
+
+ https://rubygems.org
+
+This may list multiple sources or non-rubygems sources. You probably
+configured them before or have an old `~/.gemrc`. If you have sources you
+do not recognize you should remove them.
+
+RubyGems has been configured to serve gems via the following URLs through
+its history:
+
+* http://gems.rubyforge.org (RubyGems 1.3.5 and earlier)
+* http://rubygems.org (RubyGems 1.3.6 through 1.8.30, and 2.0.0)
+* https://rubygems.org (RubyGems 2.0.1 and newer)
+
+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 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 --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://my.private.source/
+ https://my.private.source/ removed from sources
+
+ EOF
+ end
+
+ def list # :nodoc:
+ 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
+
+ list.each do |src|
+ say src
+ end
+ end
+
+ def list? # :nodoc:
+ !(options[:add] ||
+ options[:prepend] ||
+ options[:append] ||
+ options[:clear_all] ||
+ options[:remove] ||
+ options[:update])
+ end
+
+ def execute
+ clear_all if options[:clear_all]
+
+ add_source options[:add] if options[:add]
+
+ prepend_source options[:prepend] if options[:prepend]
+
+ append_source options[:append] if options[:append]
+
+ remove_source options[:remove] if options[:remove]
+
+ update if options[:update]
+
+ list if list?
+ end
+
+ def remove_source(source_uri) # :nodoc:
+ source = build_source(source_uri)
+ source_uri = source.uri.to_s
+
+ if configured_sources&.include? source
+ Gem.sources.delete source
+ Gem.configuration.write
+
+ 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} cannot be removed because there are no configured sources in #{config_file_name}"
+ end
+ end
+
+ def update # :nodoc:
+ Gem.sources.each_source do |src|
+ src.load_specs :released
+ src.load_specs :latest
+ end
+
+ say "source cache successfully updated"
+ end
+
+ def remove_cache_file(desc, path) # :nodoc:
+ FileUtils.rm_rf path
+
+ if !File.exist?(path)
+ say "*** Removed #{desc} source cache ***"
+ elsif !File.writable?(path)
+ say "*** Unable to remove #{desc} source cache (write protected) ***"
+ else
+ 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
new file mode 100644
index 0000000000..15e543f1a6
--- /dev/null
+++ b/lib/rubygems/commands/specification_command.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+require_relative "../package"
+
+class Gem::Commands::SpecificationCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ Gem.load_yaml
+
+ super "specification", "Display gem specification (in yaml)",
+ domain: :local, version: Gem::Requirement.default,
+ format: :yaml
+
+ add_version_option("examine")
+ add_platform_option
+ add_prerelease_option
+
+ add_option("--all", "Output specifications for all versions of",
+ "the gem") do |_value, options|
+ options[:all] = true
+ end
+
+ add_option("--ruby", "Output ruby format") do |_value, options|
+ options[:format] = :ruby
+ end
+
+ add_option("--yaml", "Output YAML format") do |_value, options|
+ options[:format] = :yaml
+ end
+
+ add_option("--marshal", "Output Marshal format") do |_value, options|
+ options[:format] = :marshal
+ end
+
+ add_local_remote_options
+ end
+
+ def arguments # :nodoc:
+ <<-ARGS
+GEM_OR_FILE gem name or a .gem file to show the gemspec for
+FIELD name of gemspec field to show
+ ARGS
+ end
+
+ def defaults_str # :nodoc:
+ "--local --version '#{Gem::Requirement.default}' --yaml"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The specification command allows you to extract the specification from
+a gem for examination.
+
+The specification can be output in YAML, ruby or Marshal formats.
+
+Specific fields in the specification can be extracted in YAML format:
+
+ $ gem spec rake summary
+ --- Ruby based make-like utility.
+ ...
+
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [GEM_OR_FILE] [FIELD]"
+ end
+
+ def execute
+ specs = []
+ gem = options[:args].shift
+
+ unless gem
+ raise Gem::CommandLineError,
+ "Please specify a gem name or a .gem file on the command line"
+ end
+
+ case v = options[:version]
+ when String
+ req = Gem::Requirement.create v
+ when Gem::Requirement
+ req = v
+ else
+ raise Gem::CommandLineError, "Unsupported version type: '#{v}'"
+ end
+
+ if !req.none? && options[:all]
+ alert_error "Specify --all or -v, not both"
+ terminate_interaction 1
+ end
+
+ if options[:all]
+ dep = Gem::Dependency.new gem
+ else
+ dep = Gem::Dependency.new gem, req
+ end
+
+ field = get_one_optional_argument
+
+ raise Gem::CommandLineError, "--ruby and FIELD are mutually exclusive" if
+ field && options[:format] == :ruby
+
+ if local?
+ if File.exist? gem
+ begin
+ specs << Gem::Package.new(gem).spec
+ rescue StandardError
+ nil
+ end
+ end
+
+ if specs.empty?
+ specs.push(*dep.matching_specs)
+ end
+ end
+
+ if remote?
+ dep.prerelease = options[:prerelease]
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ specs.push(*found.map {|spec,| spec })
+ end
+
+ if specs.empty?
+ alert_error "No gem matching '#{dep}' found"
+ terminate_interaction 1
+ end
+
+ platform = get_platform_from_requirements(options)
+
+ if platform
+ specs = specs.select {|s| s.platform.to_s == platform }
+ end
+
+ unless options[:all]
+ specs = [specs.max_by(&:version)]
+ end
+
+ specs.each do |s|
+ s = s.send field if field
+
+ say case options[:format]
+ when :ruby then s.to_ruby
+ when :marshal then Marshal.dump s
+ else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s)
+ end
+
+ say "\n"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb
new file mode 100644
index 0000000000..0be2b85159
--- /dev/null
+++ b/lib/rubygems/commands/stale_command.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::StaleCommand < Gem::Command
+ def initialize
+ super("stale", "List gems along with access times")
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The stale command lists the latest access time for all the files in your
+installed gems.
+
+You can use this command to discover gems and gem versions you are no
+longer using.
+ EOF
+ end
+
+ def usage # :nodoc:
+ program_name.to_s
+ end
+
+ def execute
+ gem_to_atime = {}
+ Gem::Specification.each do |spec|
+ name = spec.full_name
+ Dir["#{spec.full_gem_path}/**/*.*"].each do |file|
+ next if File.directory?(file)
+ stat = File.stat(file)
+ gem_to_atime[name] ||= stat.atime
+ gem_to_atime[name] = stat.atime if gem_to_atime[name] < stat.atime
+ end
+ end
+
+ gem_to_atime.sort_by {|_, atime| atime }.each do |name, atime|
+ say "#{name} at #{atime.strftime "%c"}"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb
new file mode 100644
index 0000000000..3c26074f93
--- /dev/null
+++ b/lib/rubygems/commands/uninstall_command.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../uninstaller"
+require "fileutils"
+
+##
+# Gem uninstaller command line tool
+#
+# See `gem help uninstall`
+
+class Gem::Commands::UninstallCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "uninstall", "Uninstall gems from the local repository",
+ version: Gem::Requirement.default, user_install: true,
+ check_dev: false, vendor: false
+
+ add_option("-a", "--[no-]all",
+ "Uninstall all matching versions") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("-I", "--[no-]ignore-dependencies",
+ "Ignore dependency requirements while",
+ "uninstalling") do |value, options|
+ options[:ignore] = value
+ end
+
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: false)") do |value, options|
+ options[:check_dev] = value
+ end
+
+ add_option("-x", "--[no-]executables",
+ "Uninstall applicable executables without",
+ "confirmation") do |value, options|
+ options[:executables] = value
+ end
+
+ add_option("-i", "--install-dir DIR",
+ "Directory to uninstall gem from") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
+
+ add_option("-n", "--bindir DIR",
+ "Directory to remove executables from") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
+
+ add_option("--[no-]user-install",
+ "Uninstall from user's home directory",
+ "in addition to GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
+
+ add_option("--[no-]format-executable",
+ "Assume executable names match Ruby's prefix and suffix.") do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option("--[no-]force",
+ "Uninstall all versions of the named gems",
+ "ignoring dependencies") do |value, options|
+ options[:force] = value
+ end
+
+ add_option("--[no-]abort-on-dependent",
+ "Prevent uninstalling gems that are",
+ "depended on by other gems.") do |value, options|
+ options[:abort_on_dependent] = value
+ end
+
+ add_version_option
+ add_platform_option
+
+ add_option("--vendor",
+ "Uninstall gem from the vendor directory.",
+ "Only for use by gem repackagers.") do |_value, options|
+ unless Gem.vendor_dir
+ raise Gem::OptionParser::InvalidOption.new "your platform is not supported"
+ end
+
+ alert_warning "Use your OS package manager to uninstall vendor gems"
+ options[:vendor] = true
+ options[:install_dir] = Gem.vendor_dir
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to uninstall"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}' --no-force " \
+ "--user-install"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The uninstall command removes a previously installed gem.
+
+RubyGems will ask for confirmation if you are attempting to uninstall a gem
+that is a dependency of an existing gem. You can use the
+--ignore-dependencies option to skip this check.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ 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 uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ check_version
+
+ # Consider only gem specifications installed at `--install-dir`
+ Gem::Specification.dirs = options[:install_dir] if options[:install_dir]
+
+ if options[:all] && !options[:args].empty?
+ uninstall_specific
+ elsif options[:all]
+ uninstall_all
+ else
+ uninstall_specific
+ end
+ end
+
+ def uninstall_all
+ specs = Gem::Specification.reject(&:default_gem?)
+
+ specs.each do |spec|
+ options[:version] = spec.version
+ uninstall_gem spec.name
+ end
+
+ alert "Uninstalled all gems in #{options[:install_dir] || Gem.dir}"
+ end
+
+ def uninstall_specific
+ deplist = Gem::DependencyList.new
+ original_gem_version = {}
+
+ get_all_gem_names_and_versions.each do |name, version|
+ original_gem_version[name] = version || options[:version]
+
+ gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name])
+
+ 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
+
+ deps = deplist.strongly_connected_components.flatten.reverse
+
+ gems_to_uninstall = {}
+
+ deps.each do |dep|
+ if original_gem_version[dep.name] == Gem::Requirement.default
+ next if gems_to_uninstall[dep.name]
+ gems_to_uninstall[dep.name] = true
+ else
+ options[:version] = dep.version
+ end
+
+ uninstall_gem(dep.name)
+ end
+ end
+
+ def uninstall_gem(gem_name)
+ 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.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" \
+ "the current user does not have the appropriate permissions")
+ terminate_interaction 1
+ end
+
+ def uninstall(gem_name)
+ Gem::Uninstaller.new(gem_name, options).uninstall
+ end
+end
diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb
new file mode 100644
index 0000000000..c2fc720297
--- /dev/null
+++ b/lib/rubygems/commands/unpack_command.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../security_option"
+require_relative "../remote_fetcher"
+require_relative "../package"
+
+# forward-declare
+
+module Gem::Security # :nodoc:
+ class Policy # :nodoc:
+ end
+end
+
+class Gem::Commands::UnpackCommand < Gem::Command
+ include Gem::VersionOption
+ include Gem::SecurityOption
+
+ def initialize
+ require "fileutils"
+
+ super "unpack", "Unpack an installed gem to the current directory",
+ version: Gem::Requirement.default,
+ target: Dir.pwd
+
+ add_option("--target=DIR",
+ "target directory for unpacking") do |value, options|
+ options[:target] = value
+ end
+
+ add_option("--spec", "unpack the gem specification") do |_value, options|
+ options[:spec] = true
+ end
+
+ add_security_option
+ add_version_option
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to unpack"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description
+ <<-EOF
+The unpack command allows you to examine the contents of a gem or modify
+them to help diagnose a bug.
+
+You can add the contents of the unpacked gem to the load path using the
+RUBYLIB environment variable or -I:
+
+ $ gem unpack my_gem
+ Unpacked gem: '.../my_gem-1.0'
+ [edit my_gem-1.0/lib/my_gem.rb]
+ $ ruby -Imy_gem-1.0/lib -S other_program
+
+You can repackage an unpacked gem using the build command. See the build
+command help for an example.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME"
+ end
+
+ #--
+ # TODO: allow, e.g., 'gem unpack rake-0.3.1'. Find a general solution for
+ # this, so that it works for uninstall as well. (And check other commands
+ # at the same time.)
+
+ def execute
+ security_policy = options[:security_policy]
+
+ get_all_gem_names.each do |name|
+ dependency = Gem::Dependency.new name, options[:version]
+ path = get_path dependency
+
+ unless path
+ alert_error "Gem '#{name}' not installed nor fetchable."
+ next
+ end
+
+ if @options[:spec]
+ spec, metadata = Gem::Package.raw_spec(path, security_policy)
+
+ if metadata.nil?
+ alert_error "--spec is unsupported on '#{name}' (old format gem)"
+ next
+ end
+
+ spec_file = File.basename spec.spec_file
+
+ FileUtils.mkdir_p @options[:target] if @options[:target]
+
+ destination = if @options[:target]
+ File.join @options[:target], spec_file
+ else
+ spec_file
+ end
+
+ File.open destination, "w" do |io|
+ io.write metadata
+ end
+ else
+ basename = File.basename path, ".gem"
+ target_dir = File.expand_path basename, options[:target]
+
+ package = Gem::Package.new path, security_policy
+ package.extract_files target_dir
+
+ say "Unpacked gem: '#{target_dir}'"
+ end
+ end
+ end
+
+ ##
+ #
+ # Find cached filename in Gem.path. Returns nil if the file cannot be found.
+ #
+ #--
+ # TODO: see comments in get_path() about general service.
+
+ def find_in_cache(filename)
+ Gem.path.each do |path|
+ this_path = File.join(path, "cache", filename)
+ return this_path if File.exist? this_path
+ end
+
+ nil
+ end
+
+ ##
+ # Return the full path to the cached gem file matching the given
+ # name and version requirement. Returns 'nil' if no match.
+ #
+ # Example:
+ #
+ # get_path 'rake', '> 0.4' # "/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem"
+ # get_path 'rake', '< 0.1' # nil
+ # get_path 'rak' # nil (exact name required)
+ #--
+
+ def get_path(dependency)
+ return dependency.name if /\.gem$/i.match?(dependency.name)
+
+ specs = dependency.matching_specs
+
+ selected = specs.max_by(&:version)
+
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless
+ selected
+
+ 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).
+
+ path = find_in_cache File.basename selected.cache_file
+
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless path
+
+ path
+ end
+end
diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb
new file mode 100644
index 0000000000..d9740d814a
--- /dev/null
+++ b/lib/rubygems/commands/update_command.rb
@@ -0,0 +1,326 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../command_manager"
+require_relative "../dependency_installer"
+require_relative "../install_update_options"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+require_relative "../install_message" # must come before rdoc for messaging
+require_relative "../rdoc"
+
+class Gem::Commands::UpdateCommand < Gem::Command
+ include Gem::InstallUpdateOptions
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ attr_reader :installer # :nodoc:
+
+ attr_reader :updated # :nodoc:
+
+ def initialize
+ options = {
+ force: false,
+ }
+
+ options.merge!(install_update_options)
+
+ super "update", "Update installed gems to the latest version", options
+
+ add_install_update_options
+
+ Gem::OptionParser.accept Gem::Version do |value|
+ Gem::Version.new value
+
+ value
+ end
+
+ add_option("--system [VERSION]", Gem::Version,
+ "Update the RubyGems system software") do |value, opts|
+ value ||= true
+
+ opts[:system] = value
+ end
+
+ add_local_remote_options
+ add_platform_option
+ add_prerelease_option "as update targets"
+
+ @updated = []
+ @installer = nil
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to update"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-force --install-dir #{Gem.dir}\n" +
+ install_update_defaults_str
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The update command will update your gems to the latest version.
+
+The update command does not remove the previous version. Use the cleanup
+command to remove old versions.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def check_latest_rubygems(version) # :nodoc:
+ if Gem.rubygems_version == version
+ say "Latest version already installed. Done."
+ terminate_interaction
+ end
+ end
+
+ def check_oldest_rubygems(version) # :nodoc:
+ if oldest_supported_version > version
+ alert_error "rubygems #{version} is not supported on #{RUBY_VERSION}. The oldest version supported by this ruby is #{oldest_supported_version}"
+ terminate_interaction 1
+ end
+ end
+
+ def check_update_arguments # :nodoc:
+ unless options[:args].empty?
+ alert_error "Gem names are not allowed with the --system option"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ if options[:system]
+ update_rubygems
+ return
+ end
+
+ gems_to_update = which_to_update(
+ highest_installed_gems,
+ options[:args].uniq
+ )
+
+ if options[:explain]
+ say "Gems to update:"
+
+ gems_to_update.each do |name_tuple|
+ say " #{name_tuple.full_name}"
+ end
+
+ return
+ end
+
+ say "Updating installed gems"
+
+ updated = update_gems gems_to_update
+
+ installed_names = highest_installed_gems.keys
+ 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
+
+ if updated.empty?
+ say "Nothing to update"
+ else
+ say "Gems updated: #{updated_names.join(" ")}"
+ end
+ say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty?
+ say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty?
+ end
+
+ def fetch_remote_gems(spec) # :nodoc:
+ dependency = Gem::Dependency.new spec.name, "> #{spec.version}"
+ dependency.prerelease = options[:prerelease]
+
+ fetcher = Gem::SpecFetcher.fetcher
+
+ spec_tuples, errors = fetcher.search_for_dependency dependency
+
+ error = errors.find {|e| e.respond_to? :exception }
+
+ raise error if error
+
+ spec_tuples
+ end
+
+ def highest_installed_gems # :nodoc:
+ hig = {} # highest installed gems
+
+ # Get only gem specifications installed as --user-install
+ Gem::Specification.dirs = Gem.user_dir if options[:user_install]
+
+ Gem::Specification.each do |spec|
+ if hig[spec.name].nil? || hig[spec.name].version < spec.version
+ hig[spec.name] = spec
+ end
+ end
+
+ hig
+ end
+
+ def highest_remote_name_tuple(spec) # :nodoc:
+ spec_tuples = fetch_remote_gems spec
+
+ highest_remote_gem = spec_tuples.max
+ return unless highest_remote_gem
+
+ highest_remote_gem.first
+ end
+
+ def install_rubygems(spec) # :nodoc:
+ args = update_rubygems_arguments
+ version = spec.version
+
+ update_dir = File.join spec.base_dir, "gems", "rubygems-update-#{version}"
+
+ Dir.chdir update_dir do
+ say "Installing RubyGems #{version}" unless options[:silent]
+
+ installed = preparing_gem_layout_for(version) do
+ system Gem.ruby, "--disable-gems", "setup.rb", *args
+ end
+
+ unless options[:silent]
+ say "RubyGems system software updated" if installed
+ end
+ end
+ end
+
+ def preparing_gem_layout_for(version)
+ if Gem::Version.new(version) >= Gem::Version.new("3.2.a")
+ yield
+ else
+ require "tmpdir"
+ Dir.mktmpdir("gem_update") do |tmpdir|
+ FileUtils.mv Gem.plugindir, tmpdir
+
+ status = yield
+
+ unless status
+ FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir
+ end
+
+ status
+ end
+ end
+ end
+
+ def rubygems_target_version
+ version = options[:system]
+ update_latest = version == true
+
+ unless update_latest
+ version = Gem::Version.new version
+ requirement = Gem::Requirement.new version
+
+ return version, requirement
+ end
+
+ version = Gem::Version.new Gem::VERSION
+ requirement = Gem::Requirement.new ">= #{Gem::VERSION}"
+
+ rubygems_update = Gem::Specification.new
+ rubygems_update.name = "rubygems-update"
+ rubygems_update.version = version
+
+ highest_remote_tup = highest_remote_name_tuple(rubygems_update)
+ target = highest_remote_tup ? highest_remote_tup.version : version
+
+ [target, requirement]
+ end
+
+ def update_gem(name, version = Gem::Requirement.default)
+ return if @updated.any? {|spec| spec.name == name }
+
+ update_options = options.dup
+ update_options[:prerelease] = version.prerelease?
+
+ @installer = Gem::DependencyInstaller.new update_options
+
+ say "Updating #{name}" unless options[:system]
+ begin
+ @installer.install name, Gem::Requirement.new(version)
+ rescue Gem::InstallError, Gem::DependencyError => e
+ alert_error "Error installing #{name}:\n\t#{e.message}"
+ end
+
+ @installer.installed_gems.each do |spec|
+ @updated << spec
+ end
+ end
+
+ def update_gems(gems_to_update)
+ gems_to_update.uniq.sort.each do |name_tuple|
+ update_gem name_tuple.name, name_tuple.version
+ end
+
+ @updated
+ end
+
+ ##
+ # Update RubyGems software to the latest version.
+
+ def update_rubygems
+ if Gem.disable_system_update_message
+ alert_error Gem.disable_system_update_message
+ terminate_interaction 1
+ end
+
+ check_update_arguments
+
+ version, requirement = rubygems_target_version
+
+ check_latest_rubygems version
+
+ check_oldest_rubygems version
+
+ installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement
+ 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
+ end
+
+ def update_rubygems_arguments # :nodoc:
+ args = []
+ args << "--silent" if options[:silent]
+ 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
+ args
+ end
+
+ def which_to_update(highest_installed_gems, gem_names)
+ result = []
+
+ highest_installed_gems.each do |_l_name, l_spec|
+ next if !gem_names.empty? &&
+ gem_names.none? {|name| name == l_spec.name }
+
+ highest_remote_tup = highest_remote_name_tuple l_spec
+ next unless highest_remote_tup
+
+ result << highest_remote_tup
+ end
+
+ result
+ end
+
+ private
+
+ #
+ # Oldest version we support downgrading to. This is the version that
+ # originally ships with the oldest supported patch version of ruby.
+ #
+ def oldest_supported_version
+ @oldest_supported_version ||=
+ Gem::Version.new("3.3.3")
+ end
+end
diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb
new file mode 100644
index 0000000000..5ed4d9d142
--- /dev/null
+++ b/lib/rubygems/commands/which_command.rb
@@ -0,0 +1,88 @@
+# 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
+
+ add_option "-a", "--[no-]all", "show all matching files" do |show_all, options|
+ options[:show_all] = show_all
+ end
+
+ add_option "-g", "--[no-]gems-first",
+ "search gems before non-gems" do |gems_first, options|
+ options[:search_gems_first] = gems_first
+ end
+ end
+
+ def arguments # :nodoc:
+ "FILE name of file to find"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-gems-first --no-all"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The which command is like the shell which command and shows you where
+the file you wish to require lives.
+
+You can use the which command to help determine why you are requiring a
+version you did not expect or to look at the content of a file you are
+requiring to see why it does not behave as you expect.
+ EOF
+ end
+
+ def execute
+ found = true
+
+ options[:args].each do |arg|
+ arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, "")
+ dirs = $LOAD_PATH
+
+ spec = Gem::Specification.find_by_path arg
+
+ if spec
+ if options[:search_gems_first]
+ dirs = spec.full_require_paths + $LOAD_PATH
+ else
+ dirs = $LOAD_PATH + spec.full_require_paths
+ end
+ end
+
+ paths = find_paths arg, dirs
+
+ if paths.empty?
+ alert_error "Can't find Ruby library file or shared library #{arg}"
+ found = false
+ else
+ say paths
+ end
+ end
+
+ terminate_interaction 1 unless found
+ end
+
+ def find_paths(package_name, dirs)
+ result = []
+
+ dirs.each do |dir|
+ Gem.suffixes.each do |ext|
+ full_path = File.join dir, "#{package_name}#{ext}"
+ if File.exist?(full_path) && !File.directory?(full_path)
+ result << full_path
+ return result unless options[:show_all]
+ end
+ end
+ end
+
+ result
+ end
+
+ def usage # :nodoc:
+ "#{program_name} FILE [FILE ...]"
+ end
+end
diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb
new file mode 100644
index 0000000000..fbdc262549
--- /dev/null
+++ b/lib/rubygems/commands/yank_command.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+require_relative "../gemcutter_utilities"
+
+class Gem::Commands::YankCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The yank command permanently removes a gem you pushed to a server.
+
+Once you have pushed a gem several downloads will happen automatically
+via the webhooks. If you accidentally pushed passwords or other sensitive
+data you will need to change them immediately and yank your gem.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM name of gem"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} -v VERSION [-p PLATFORM] [--key KEY_NAME] [--host HOST] GEM"
+ end
+
+ def initialize
+ super "yank", "Remove a pushed gem from the index"
+
+ add_version_option("remove")
+ add_platform_option("remove")
+ add_otp_option
+
+ add_option("--host HOST",
+ "Yank from another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
+ options[:host] = value
+ end
+
+ add_key_option
+ @host = nil
+ end
+
+ def execute
+ @host = options[:host]
+
+ sign_in @host, scope: get_yank_scope
+
+ version = get_version_from_requirements(options[:version])
+ platform = get_platform_from_requirements(options)
+
+ if version
+ yank_gem(version, platform)
+ else
+ say "A version argument is required: #{usage}"
+ terminate_interaction
+ end
+ end
+
+ def yank_gem(version, platform)
+ say "Yanking gem from #{host}..."
+ args = [:delete, version, platform, "api/v1/gems/yank"]
+ response = yank_api_request(*args)
+
+ say response.body
+ end
+
+ private
+
+ def yank_api_request(method, version, platform, api)
+ name = get_one_gem_name
+ response = rubygems_api_request(method, api, host, scope: get_yank_scope) do |request|
+ request.add_field("Authorization", api_key)
+
+ data = {
+ "gem_name" => name,
+ "version" => version,
+ }
+ data["platform"] = platform if platform
+
+ request.set_form_data data
+ end
+ response
+ end
+
+ def get_version_from_requirements(requirements)
+ requirements.requirements.first[1].version
+ rescue StandardError
+ nil
+ end
+
+ def get_yank_scope
+ :yank_rubygem
+ end
+end