diff options
Diffstat (limited to 'lib/rubygems/commands')
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 |
